From 4f197ffd5a0dd310a1dd7bfc663dc29e76d8be51 Mon Sep 17 00:00:00 2001 From: admin Date: Mon, 1 Jun 2009 09:43:25 +0000 Subject: [PATCH 001/275] Creating initial repository structure --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%401 From e9a97afdf80ca85a2940385328dbe61d27592b3e Mon Sep 17 00:00:00 2001 From: hsoft Date: Mon, 1 Jun 2009 09:55:11 +0000 Subject: [PATCH 002/275] Initial commit. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%402 --- base/cocoa/AppDelegate.h | 17 + base/cocoa/AppDelegate.m | 30 + base/cocoa/Consts.h | 17 + base/cocoa/DirectoryPanel.h | 25 + base/cocoa/DirectoryPanel.m | 178 +++ base/cocoa/English.lproj/InfoPlist.strings | Bin 0 -> 204 bytes base/cocoa/Info.plist | 26 + base/cocoa/PyDupeGuru.h | 56 + base/cocoa/ResultWindow.h | 34 + base/cocoa/ResultWindow.m | 316 ++++ base/cocoa/dgbase.xcodeproj/project.pbxproj | 408 +++++ base/qt/__init__.py | 0 base/qt/about_box.py | 35 + base/qt/about_box.ui | 133 ++ base/qt/app.py | 269 ++++ base/qt/details_table.py | 78 + base/qt/dg.qrc | 17 + base/qt/directories_dialog.py | 80 + base/qt/directories_dialog.ui | 133 ++ base/qt/directories_model.py | 108 ++ base/qt/error_report_dialog.py | 25 + base/qt/error_report_dialog.ui | 117 ++ base/qt/gen.py | 20 + base/qt/main_window.py | 304 ++++ base/qt/main_window.ui | 911 +++++++++++ base/qt/preferences.py | 109 ++ base/qt/reg.py | 34 + base/qt/reg_demo_dialog.py | 45 + base/qt/reg_demo_dialog.ui | 140 ++ base/qt/reg_submit_dialog.py | 47 + base/qt/reg_submit_dialog.ui | 149 ++ base/qt/results_model.py | 175 +++ base/qt/tree_model.py | 66 + images/actions32.png | Bin 0 -> 3039 bytes images/delta32.png | Bin 0 -> 1941 bytes images/details32.png | Bin 0 -> 5090 bytes images/dgme_logo.ico | Bin 0 -> 17542 bytes images/dgme_logo_128.png | Bin 0 -> 18092 bytes images/dgme_logo_32.png | Bin 0 -> 6079 bytes images/dgpe_logo.ico | Bin 0 -> 17542 bytes images/dgpe_logo_128.png | Bin 0 -> 19168 bytes images/dgpe_logo_32.png | Bin 0 -> 5818 bytes images/dgse_logo.ico | Bin 0 -> 17542 bytes images/dgse_logo_128.png | Bin 0 -> 14474 bytes images/dgse_logo_32.png | Bin 0 -> 1828 bytes images/folder32.png | Bin 0 -> 2427 bytes images/folderwin32.png | Bin 0 -> 1519 bytes images/gear.png | Bin 0 -> 394 bytes images/power_marker32.png | Bin 0 -> 2132 bytes images/preferences32.png | Bin 0 -> 2899 bytes me/cocoa/AppDelegate.h | 22 + me/cocoa/AppDelegate.m | 158 ++ me/cocoa/Consts.h | 5 + me/cocoa/DetailsPanel.h | 13 + me/cocoa/DetailsPanel.m | 16 + me/cocoa/DirectoryPanel.h | 8 + me/cocoa/DirectoryPanel.m | 23 + .../English.lproj/Details.nib/classes.nib | 18 + me/cocoa/English.lproj/Details.nib/info.nib | 16 + .../Details.nib/keyedobjects.nib | Bin 0 -> 6122 bytes .../English.lproj/Directories.nib/classes.nib | 64 + .../English.lproj/Directories.nib/info.nib | 20 + .../Directories.nib/keyedobjects.nib | Bin 0 -> 9698 bytes me/cocoa/English.lproj/InfoPlist.strings | Bin 0 -> 204 bytes .../English.lproj/MainMenu.nib/classes.nib | 257 +++ me/cocoa/English.lproj/MainMenu.nib/info.nib | 20 + .../MainMenu.nib/keyedobjects.nib | Bin 0 -> 57645 bytes me/cocoa/Info.plist | 34 + me/cocoa/PyDupeGuru.h | 15 + me/cocoa/ResultWindow.h | 56 + me/cocoa/ResultWindow.m | 505 ++++++ me/cocoa/dupeguru.icns | Bin 0 -> 58956 bytes me/cocoa/dupeguru.xcodeproj/hsoft.mode1 | 1376 ++++++++++++++++ me/cocoa/dupeguru.xcodeproj/project.pbxproj | 563 +++++++ me/cocoa/gen.py | 14 + me/cocoa/main.m | 21 + me/cocoa/py/dg_cocoa.py | 225 +++ me/cocoa/py/gen.py | 18 + me/cocoa/py/setup.py | 14 + me/cocoa/w3/dg.xsl | 75 + me/cocoa/w3/hardcoded.css | 71 + me/help/changelog.yaml | 542 +++++++ me/help/gen.py | 7 + me/help/skeleton/hardcoded.css | 409 +++++ me/help/skeleton/images/hs_title.png | Bin 0 -> 1817 bytes me/help/templates/base_dg.mako | 14 + me/help/templates/credits.mako | 20 + me/help/templates/directories.mako | 24 + me/help/templates/faq.mako | 67 + me/help/templates/intro.mako | 13 + me/help/templates/power_marker.mako | 33 + me/help/templates/preferences.mako | 36 + me/help/templates/quick_start.mako | 18 + me/help/templates/results.mako | 73 + me/help/templates/versions.mako | 9 + me/qt/app.py | 60 + me/qt/app_win.py | 16 + me/qt/build.py | 41 + me/qt/details_dialog.py | 20 + me/qt/details_dialog.ui | 53 + me/qt/dgme.spec | 19 + me/qt/gen.py | 25 + me/qt/installer.aip | 241 +++ me/qt/preferences.py | 70 + me/qt/preferences_dialog.py | 85 + me/qt/preferences_dialog.ui | 434 ++++++ me/qt/profile.py | 20 + me/qt/start.py | 20 + me/qt/verinfo | 28 + pe/cocoa/AppDelegate.h | 21 + pe/cocoa/AppDelegate.m | 116 ++ pe/cocoa/Consts.h | 4 + pe/cocoa/Consts.m | 5 + pe/cocoa/DetailsPanel.h | 21 + pe/cocoa/DetailsPanel.m | 79 + pe/cocoa/DirectoryPanel.h | 8 + pe/cocoa/DirectoryPanel.m | 44 + .../English.lproj/Details.nib/classes.nib | 24 + pe/cocoa/English.lproj/Details.nib/info.nib | 16 + .../Details.nib/keyedobjects.nib | Bin 0 -> 9001 bytes .../English.lproj/Directories.nib/classes.nib | 62 + .../English.lproj/Directories.nib/info.nib | 20 + .../Directories.nib/keyedobjects.nib | Bin 0 -> 9698 bytes pe/cocoa/English.lproj/InfoPlist.strings | Bin 0 -> 204 bytes .../English.lproj/MainMenu.nib/classes.nib | 235 +++ pe/cocoa/English.lproj/MainMenu.nib/info.nib | 20 + .../MainMenu.nib/keyedobjects.nib | Bin 0 -> 45275 bytes pe/cocoa/Info.plist | 34 + pe/cocoa/PictureBlocks.h | 11 + pe/cocoa/PictureBlocks.m | 139 ++ pe/cocoa/PyDupeGuru.h | 9 + pe/cocoa/ResultWindow.h | 59 + pe/cocoa/ResultWindow.m | 569 +++++++ pe/cocoa/dupeguru.icns | Bin 0 -> 59921 bytes pe/cocoa/dupeguru.xcodeproj/hsoft.mode1 | 1380 +++++++++++++++++ pe/cocoa/dupeguru.xcodeproj/project.pbxproj | 588 +++++++ pe/cocoa/main.m | 21 + pe/cocoa/py/build_py.sh | 2 + pe/cocoa/py/dg_cocoa.py | 199 +++ pe/cocoa/py/setup.py | 11 + pe/cocoa/w3/dg.xsl | 75 + pe/cocoa/w3/hardcoded.css | 71 + pe/help/changelog.yaml | 174 +++ pe/help/gen.py | 12 + pe/help/skeleton/hardcoded.css | 409 +++++ pe/help/skeleton/images/hs_title.png | Bin 0 -> 1817 bytes pe/help/templates/base_dg.mako | 14 + pe/help/templates/credits.mako | 25 + pe/help/templates/directories.mako | 24 + pe/help/templates/faq.mako | 64 + pe/help/templates/intro.mako | 13 + pe/help/templates/power_marker.mako | 33 + pe/help/templates/preferences.mako | 23 + pe/help/templates/quick_start.mako | 18 + pe/help/templates/results.mako | 73 + pe/help/templates/versions.mako | 6 + pe/qt/app.py | 97 ++ pe/qt/app_win.py | 16 + pe/qt/base/__init__.py | 0 pe/qt/base/about_box.py | 35 + pe/qt/base/about_box.ui | 133 ++ pe/qt/base/app.py | 269 ++++ pe/qt/base/details_table.py | 78 + pe/qt/base/dg.qrc | 17 + pe/qt/base/directories_dialog.py | 80 + pe/qt/base/directories_dialog.ui | 133 ++ pe/qt/base/directories_model.py | 108 ++ pe/qt/base/error_report_dialog.py | 25 + pe/qt/base/error_report_dialog.ui | 117 ++ pe/qt/base/gen.py | 20 + pe/qt/base/images/actions32.png | Bin 0 -> 3039 bytes pe/qt/base/images/delta32.png | Bin 0 -> 1941 bytes pe/qt/base/images/details32.png | Bin 0 -> 5090 bytes pe/qt/base/images/dgme_logo.ico | Bin 0 -> 17542 bytes pe/qt/base/images/dgme_logo_128.png | Bin 0 -> 18092 bytes pe/qt/base/images/dgme_logo_32.png | Bin 0 -> 6079 bytes pe/qt/base/images/dgpe_logo.ico | Bin 0 -> 17542 bytes pe/qt/base/images/dgpe_logo_128.png | Bin 0 -> 19168 bytes pe/qt/base/images/dgpe_logo_32.png | Bin 0 -> 5818 bytes pe/qt/base/images/dgse_logo.ico | Bin 0 -> 17542 bytes pe/qt/base/images/dgse_logo_128.png | Bin 0 -> 14474 bytes pe/qt/base/images/dgse_logo_32.png | Bin 0 -> 1828 bytes pe/qt/base/images/folder32.png | Bin 0 -> 2427 bytes pe/qt/base/images/folderwin32.png | Bin 0 -> 1519 bytes pe/qt/base/images/gear.png | Bin 0 -> 394 bytes pe/qt/base/images/power_marker32.png | Bin 0 -> 2132 bytes pe/qt/base/images/preferences32.png | Bin 0 -> 2899 bytes pe/qt/base/main_window.py | 304 ++++ pe/qt/base/main_window.ui | 911 +++++++++++ pe/qt/base/preferences.py | 109 ++ pe/qt/base/reg.py | 34 + pe/qt/base/reg_demo_dialog.py | 45 + pe/qt/base/reg_demo_dialog.ui | 140 ++ pe/qt/base/reg_submit_dialog.py | 47 + pe/qt/base/reg_submit_dialog.ui | 149 ++ pe/qt/base/results_model.py | 175 +++ pe/qt/base/tree_model.py | 66 + pe/qt/block.py | 41 + pe/qt/build.py | 40 + pe/qt/details_dialog.py | 61 + pe/qt/details_dialog.ui | 113 ++ pe/qt/dgpe.spec | 19 + pe/qt/dupeguru/__init__.py | 1 + pe/qt/dupeguru/app.py | 229 +++ pe/qt/dupeguru/app_cocoa.py | 304 ++++ pe/qt/dupeguru/app_cocoa_test.py | 320 ++++ pe/qt/dupeguru/app_me_cocoa.py | 68 + pe/qt/dupeguru/app_pe_cocoa.py | 212 +++ pe/qt/dupeguru/app_se_cocoa.py | 13 + pe/qt/dupeguru/app_test.py | 137 ++ pe/qt/dupeguru/data.py | 105 ++ pe/qt/dupeguru/data_me.py | 100 ++ pe/qt/dupeguru/data_pe.py | 77 + pe/qt/dupeguru/directories.py | 161 ++ pe/qt/dupeguru/directories_test.py | 280 ++++ pe/qt/dupeguru/engine.py | 360 +++++ pe/qt/dupeguru/engine_test.py | 822 ++++++++++ pe/qt/dupeguru/export.py | 67 + pe/qt/dupeguru/export_test.py | 91 ++ pe/qt/dupeguru/gen.py | 28 + pe/qt/dupeguru/ignore.py | 117 ++ pe/qt/dupeguru/ignore_test.py | 158 ++ pe/qt/dupeguru/modules/block/block.pyx | 93 ++ pe/qt/dupeguru/modules/block/setup.py | 14 + pe/qt/dupeguru/modules/cache/cache.pyx | 34 + pe/qt/dupeguru/modules/cache/setup.py | 14 + pe/qt/dupeguru/picture/__init__.py | 0 pe/qt/dupeguru/picture/block.py | 124 ++ pe/qt/dupeguru/picture/block_test.py | 313 ++++ pe/qt/dupeguru/picture/cache.py | 134 ++ pe/qt/dupeguru/picture/cache_test.py | 159 ++ pe/qt/dupeguru/picture/matchbase.py | 136 ++ pe/qt/dupeguru/results.py | 359 +++++ pe/qt/dupeguru/results_test.py | 742 +++++++++ pe/qt/dupeguru/scanner.py | 131 ++ pe/qt/dupeguru/scanner_test.py | 468 ++++++ pe/qt/gen.py | 42 + pe/qt/help/changelog.yaml | 174 +++ pe/qt/help/gen.py | 12 + pe/qt/help/skeleton/hardcoded.css | 409 +++++ pe/qt/help/skeleton/images/hs_title.png | Bin 0 -> 1817 bytes pe/qt/help/templates/base_dg.mako | 14 + pe/qt/help/templates/credits.mako | 25 + pe/qt/help/templates/directories.mako | 24 + pe/qt/help/templates/faq.mako | 64 + pe/qt/help/templates/intro.mako | 13 + pe/qt/help/templates/power_marker.mako | 33 + pe/qt/help/templates/preferences.mako | 23 + pe/qt/help/templates/quick_start.mako | 18 + pe/qt/help/templates/results.mako | 73 + pe/qt/help/templates/versions.mako | 6 + pe/qt/installer.aip | 249 +++ pe/qt/main_window.py | 26 + pe/qt/modules/block/block.pyx | 39 + pe/qt/modules/block/setup.py | 10 + pe/qt/preferences.py | 35 + pe/qt/preferences_dialog.py | 56 + pe/qt/preferences_dialog.ui | 257 +++ pe/qt/profile.py | 20 + pe/qt/start.py | 20 + pe/qt/verinfo | 28 + py/__init__.py | 1 + py/app.py | 229 +++ py/app_cocoa.py | 304 ++++ py/app_cocoa_test.py | 320 ++++ py/app_me_cocoa.py | 68 + py/app_pe_cocoa.py | 212 +++ py/app_se_cocoa.py | 13 + py/app_test.py | 137 ++ py/data.py | 105 ++ py/data_me.py | 100 ++ py/data_pe.py | 77 + py/directories.py | 161 ++ py/directories_test.py | 280 ++++ py/engine.py | 360 +++++ py/engine_test.py | 822 ++++++++++ py/export.py | 67 + py/export_test.py | 91 ++ py/gen.py | 28 + py/ignore.py | 117 ++ py/ignore_test.py | 158 ++ py/modules/block/block.pyx | 93 ++ py/modules/block/setup.py | 14 + py/modules/cache/cache.pyx | 34 + py/modules/cache/setup.py | 14 + py/picture/__init__.py | 0 py/picture/block.py | 124 ++ py/picture/block_test.py | 313 ++++ py/picture/cache.py | 134 ++ py/picture/cache_test.py | 159 ++ py/picture/matchbase.py | 136 ++ py/results.py | 359 +++++ py/results_test.py | 742 +++++++++ py/scanner.py | 131 ++ py/scanner_test.py | 468 ++++++ se/cocoa/AppDelegate.h | 18 + se/cocoa/AppDelegate.m | 108 ++ se/cocoa/Consts.h | 3 + se/cocoa/DetailsPanel.h | 13 + se/cocoa/DetailsPanel.m | 16 + se/cocoa/DirectoryPanel.h | 7 + se/cocoa/DirectoryPanel.m | 4 + .../English.lproj/Details.nib/classes.nib | 18 + se/cocoa/English.lproj/Details.nib/info.nib | 16 + .../Details.nib/keyedobjects.nib | Bin 0 -> 6122 bytes .../English.lproj/Directories.nib/classes.nib | 62 + .../English.lproj/Directories.nib/info.nib | 20 + .../Directories.nib/keyedobjects.nib | Bin 0 -> 9698 bytes se/cocoa/English.lproj/InfoPlist.strings | Bin 0 -> 204 bytes .../English.lproj/MainMenu.nib/classes.nib | 229 +++ se/cocoa/English.lproj/MainMenu.nib/info.nib | 20 + .../MainMenu.nib/keyedobjects.nib | Bin 0 -> 48638 bytes se/cocoa/Info.plist | 34 + se/cocoa/PyDupeGuru.h | 9 + se/cocoa/ResultWindow.h | 55 + se/cocoa/ResultWindow.m | 460 ++++++ se/cocoa/dupeguru.icns | Bin 0 -> 53620 bytes se/cocoa/dupeguru.xcodeproj/hsoft.mode1 | 1369 ++++++++++++++++ se/cocoa/dupeguru.xcodeproj/project.pbxproj | 548 +++++++ se/cocoa/gen.py | 14 + se/cocoa/main.m | 21 + se/cocoa/py/dg_cocoa.py | 208 +++ se/cocoa/py/gen.py | 15 + se/cocoa/py/setup.py | 14 + se/cocoa/w3/dg.xsl | 75 + se/cocoa/w3/hardcoded.css | 71 + se/help/changelog.yaml | 230 +++ se/help/gen.py | 12 + se/help/skeleton/hardcoded.css | 409 +++++ se/help/skeleton/images/hs_title.png | Bin 0 -> 1817 bytes se/help/templates/base_dg.mako | 14 + se/help/templates/credits.mako | 20 + se/help/templates/directories.mako | 24 + se/help/templates/faq.mako | 64 + se/help/templates/intro.mako | 13 + se/help/templates/power_marker.mako | 33 + se/help/templates/preferences.mako | 27 + se/help/templates/quick_start.mako | 18 + se/help/templates/results.mako | 73 + se/help/templates/versions.mako | 6 + se/qt/app.py | 41 + se/qt/app_win.py | 16 + se/qt/build.py | 43 + se/qt/details_dialog.py | 20 + se/qt/details_dialog.ui | 53 + se/qt/dgse.spec | 19 + se/qt/gen.py | 27 + se/qt/installer.aip | 257 +++ se/qt/preferences.py | 47 + se/qt/preferences_dialog.py | 82 + se/qt/preferences_dialog.ui | 389 +++++ se/qt/profile.py | 20 + se/qt/start.py | 20 + se/qt/verinfo | 28 + 354 files changed, 38083 insertions(+) create mode 100644 base/cocoa/AppDelegate.h create mode 100644 base/cocoa/AppDelegate.m create mode 100644 base/cocoa/Consts.h create mode 100644 base/cocoa/DirectoryPanel.h create mode 100644 base/cocoa/DirectoryPanel.m create mode 100644 base/cocoa/English.lproj/InfoPlist.strings create mode 100644 base/cocoa/Info.plist create mode 100644 base/cocoa/PyDupeGuru.h create mode 100644 base/cocoa/ResultWindow.h create mode 100644 base/cocoa/ResultWindow.m create mode 100644 base/cocoa/dgbase.xcodeproj/project.pbxproj create mode 100644 base/qt/__init__.py create mode 100644 base/qt/about_box.py create mode 100644 base/qt/about_box.ui create mode 100644 base/qt/app.py create mode 100644 base/qt/details_table.py create mode 100644 base/qt/dg.qrc create mode 100644 base/qt/directories_dialog.py create mode 100644 base/qt/directories_dialog.ui create mode 100644 base/qt/directories_model.py create mode 100644 base/qt/error_report_dialog.py create mode 100644 base/qt/error_report_dialog.ui create mode 100644 base/qt/gen.py create mode 100644 base/qt/main_window.py create mode 100644 base/qt/main_window.ui create mode 100644 base/qt/preferences.py create mode 100644 base/qt/reg.py create mode 100644 base/qt/reg_demo_dialog.py create mode 100644 base/qt/reg_demo_dialog.ui create mode 100644 base/qt/reg_submit_dialog.py create mode 100644 base/qt/reg_submit_dialog.ui create mode 100644 base/qt/results_model.py create mode 100644 base/qt/tree_model.py create mode 100755 images/actions32.png create mode 100755 images/delta32.png create mode 100644 images/details32.png create mode 100644 images/dgme_logo.ico create mode 100644 images/dgme_logo_128.png create mode 100644 images/dgme_logo_32.png create mode 100644 images/dgpe_logo.ico create mode 100644 images/dgpe_logo_128.png create mode 100755 images/dgpe_logo_32.png create mode 100644 images/dgse_logo.ico create mode 100644 images/dgse_logo_128.png create mode 100755 images/dgse_logo_32.png create mode 100755 images/folder32.png create mode 100755 images/folderwin32.png create mode 100755 images/gear.png create mode 100755 images/power_marker32.png create mode 100755 images/preferences32.png create mode 100644 me/cocoa/AppDelegate.h create mode 100644 me/cocoa/AppDelegate.m create mode 100644 me/cocoa/Consts.h create mode 100644 me/cocoa/DetailsPanel.h create mode 100644 me/cocoa/DetailsPanel.m create mode 100644 me/cocoa/DirectoryPanel.h create mode 100644 me/cocoa/DirectoryPanel.m create mode 100644 me/cocoa/English.lproj/Details.nib/classes.nib create mode 100644 me/cocoa/English.lproj/Details.nib/info.nib create mode 100644 me/cocoa/English.lproj/Details.nib/keyedobjects.nib create mode 100644 me/cocoa/English.lproj/Directories.nib/classes.nib create mode 100644 me/cocoa/English.lproj/Directories.nib/info.nib create mode 100644 me/cocoa/English.lproj/Directories.nib/keyedobjects.nib create mode 100644 me/cocoa/English.lproj/InfoPlist.strings create mode 100644 me/cocoa/English.lproj/MainMenu.nib/classes.nib create mode 100644 me/cocoa/English.lproj/MainMenu.nib/info.nib create mode 100644 me/cocoa/English.lproj/MainMenu.nib/keyedobjects.nib create mode 100644 me/cocoa/Info.plist create mode 100644 me/cocoa/PyDupeGuru.h create mode 100644 me/cocoa/ResultWindow.h create mode 100644 me/cocoa/ResultWindow.m create mode 100755 me/cocoa/dupeguru.icns create mode 100644 me/cocoa/dupeguru.xcodeproj/hsoft.mode1 create mode 100644 me/cocoa/dupeguru.xcodeproj/project.pbxproj create mode 100644 me/cocoa/gen.py create mode 100644 me/cocoa/main.m create mode 100644 me/cocoa/py/dg_cocoa.py create mode 100644 me/cocoa/py/gen.py create mode 100644 me/cocoa/py/setup.py create mode 100644 me/cocoa/w3/dg.xsl create mode 100644 me/cocoa/w3/hardcoded.css create mode 100644 me/help/changelog.yaml create mode 100644 me/help/gen.py create mode 100644 me/help/skeleton/hardcoded.css create mode 100644 me/help/skeleton/images/hs_title.png create mode 100644 me/help/templates/base_dg.mako create mode 100644 me/help/templates/credits.mako create mode 100644 me/help/templates/directories.mako create mode 100644 me/help/templates/faq.mako create mode 100644 me/help/templates/intro.mako create mode 100644 me/help/templates/power_marker.mako create mode 100644 me/help/templates/preferences.mako create mode 100644 me/help/templates/quick_start.mako create mode 100644 me/help/templates/results.mako create mode 100644 me/help/templates/versions.mako create mode 100644 me/qt/app.py create mode 100644 me/qt/app_win.py create mode 100644 me/qt/build.py create mode 100644 me/qt/details_dialog.py create mode 100644 me/qt/details_dialog.ui create mode 100644 me/qt/dgme.spec create mode 100644 me/qt/gen.py create mode 100644 me/qt/installer.aip create mode 100644 me/qt/preferences.py create mode 100644 me/qt/preferences_dialog.py create mode 100644 me/qt/preferences_dialog.ui create mode 100644 me/qt/profile.py create mode 100644 me/qt/start.py create mode 100644 me/qt/verinfo create mode 100644 pe/cocoa/AppDelegate.h create mode 100644 pe/cocoa/AppDelegate.m create mode 100644 pe/cocoa/Consts.h create mode 100644 pe/cocoa/Consts.m create mode 100644 pe/cocoa/DetailsPanel.h create mode 100644 pe/cocoa/DetailsPanel.m create mode 100644 pe/cocoa/DirectoryPanel.h create mode 100644 pe/cocoa/DirectoryPanel.m create mode 100644 pe/cocoa/English.lproj/Details.nib/classes.nib create mode 100644 pe/cocoa/English.lproj/Details.nib/info.nib create mode 100644 pe/cocoa/English.lproj/Details.nib/keyedobjects.nib create mode 100644 pe/cocoa/English.lproj/Directories.nib/classes.nib create mode 100644 pe/cocoa/English.lproj/Directories.nib/info.nib create mode 100644 pe/cocoa/English.lproj/Directories.nib/keyedobjects.nib create mode 100644 pe/cocoa/English.lproj/InfoPlist.strings create mode 100644 pe/cocoa/English.lproj/MainMenu.nib/classes.nib create mode 100644 pe/cocoa/English.lproj/MainMenu.nib/info.nib create mode 100644 pe/cocoa/English.lproj/MainMenu.nib/keyedobjects.nib create mode 100644 pe/cocoa/Info.plist create mode 100644 pe/cocoa/PictureBlocks.h create mode 100644 pe/cocoa/PictureBlocks.m create mode 100644 pe/cocoa/PyDupeGuru.h create mode 100644 pe/cocoa/ResultWindow.h create mode 100644 pe/cocoa/ResultWindow.m create mode 100755 pe/cocoa/dupeguru.icns create mode 100644 pe/cocoa/dupeguru.xcodeproj/hsoft.mode1 create mode 100644 pe/cocoa/dupeguru.xcodeproj/project.pbxproj create mode 100644 pe/cocoa/main.m create mode 100755 pe/cocoa/py/build_py.sh create mode 100644 pe/cocoa/py/dg_cocoa.py create mode 100644 pe/cocoa/py/setup.py create mode 100644 pe/cocoa/w3/dg.xsl create mode 100644 pe/cocoa/w3/hardcoded.css create mode 100644 pe/help/changelog.yaml create mode 100644 pe/help/gen.py create mode 100644 pe/help/skeleton/hardcoded.css create mode 100644 pe/help/skeleton/images/hs_title.png create mode 100644 pe/help/templates/base_dg.mako create mode 100644 pe/help/templates/credits.mako create mode 100644 pe/help/templates/directories.mako create mode 100644 pe/help/templates/faq.mako create mode 100644 pe/help/templates/intro.mako create mode 100644 pe/help/templates/power_marker.mako create mode 100644 pe/help/templates/preferences.mako create mode 100644 pe/help/templates/quick_start.mako create mode 100644 pe/help/templates/results.mako create mode 100644 pe/help/templates/versions.mako create mode 100644 pe/qt/app.py create mode 100644 pe/qt/app_win.py create mode 100644 pe/qt/base/__init__.py create mode 100644 pe/qt/base/about_box.py create mode 100644 pe/qt/base/about_box.ui create mode 100644 pe/qt/base/app.py create mode 100644 pe/qt/base/details_table.py create mode 100644 pe/qt/base/dg.qrc create mode 100644 pe/qt/base/directories_dialog.py create mode 100644 pe/qt/base/directories_dialog.ui create mode 100644 pe/qt/base/directories_model.py create mode 100644 pe/qt/base/error_report_dialog.py create mode 100644 pe/qt/base/error_report_dialog.ui create mode 100644 pe/qt/base/gen.py create mode 100755 pe/qt/base/images/actions32.png create mode 100755 pe/qt/base/images/delta32.png create mode 100644 pe/qt/base/images/details32.png create mode 100644 pe/qt/base/images/dgme_logo.ico create mode 100644 pe/qt/base/images/dgme_logo_128.png create mode 100644 pe/qt/base/images/dgme_logo_32.png create mode 100644 pe/qt/base/images/dgpe_logo.ico create mode 100644 pe/qt/base/images/dgpe_logo_128.png create mode 100755 pe/qt/base/images/dgpe_logo_32.png create mode 100644 pe/qt/base/images/dgse_logo.ico create mode 100644 pe/qt/base/images/dgse_logo_128.png create mode 100755 pe/qt/base/images/dgse_logo_32.png create mode 100755 pe/qt/base/images/folder32.png create mode 100755 pe/qt/base/images/folderwin32.png create mode 100755 pe/qt/base/images/gear.png create mode 100755 pe/qt/base/images/power_marker32.png create mode 100755 pe/qt/base/images/preferences32.png create mode 100644 pe/qt/base/main_window.py create mode 100644 pe/qt/base/main_window.ui create mode 100644 pe/qt/base/preferences.py create mode 100644 pe/qt/base/reg.py create mode 100644 pe/qt/base/reg_demo_dialog.py create mode 100644 pe/qt/base/reg_demo_dialog.ui create mode 100644 pe/qt/base/reg_submit_dialog.py create mode 100644 pe/qt/base/reg_submit_dialog.ui create mode 100644 pe/qt/base/results_model.py create mode 100644 pe/qt/base/tree_model.py create mode 100644 pe/qt/block.py create mode 100644 pe/qt/build.py create mode 100644 pe/qt/details_dialog.py create mode 100644 pe/qt/details_dialog.ui create mode 100644 pe/qt/dgpe.spec create mode 100644 pe/qt/dupeguru/__init__.py create mode 100644 pe/qt/dupeguru/app.py create mode 100644 pe/qt/dupeguru/app_cocoa.py create mode 100644 pe/qt/dupeguru/app_cocoa_test.py create mode 100644 pe/qt/dupeguru/app_me_cocoa.py create mode 100644 pe/qt/dupeguru/app_pe_cocoa.py create mode 100644 pe/qt/dupeguru/app_se_cocoa.py create mode 100644 pe/qt/dupeguru/app_test.py create mode 100644 pe/qt/dupeguru/data.py create mode 100644 pe/qt/dupeguru/data_me.py create mode 100644 pe/qt/dupeguru/data_pe.py create mode 100644 pe/qt/dupeguru/directories.py create mode 100644 pe/qt/dupeguru/directories_test.py create mode 100644 pe/qt/dupeguru/engine.py create mode 100644 pe/qt/dupeguru/engine_test.py create mode 100644 pe/qt/dupeguru/export.py create mode 100644 pe/qt/dupeguru/export_test.py create mode 100644 pe/qt/dupeguru/gen.py create mode 100644 pe/qt/dupeguru/ignore.py create mode 100644 pe/qt/dupeguru/ignore_test.py create mode 100644 pe/qt/dupeguru/modules/block/block.pyx create mode 100644 pe/qt/dupeguru/modules/block/setup.py create mode 100644 pe/qt/dupeguru/modules/cache/cache.pyx create mode 100644 pe/qt/dupeguru/modules/cache/setup.py create mode 100644 pe/qt/dupeguru/picture/__init__.py create mode 100644 pe/qt/dupeguru/picture/block.py create mode 100644 pe/qt/dupeguru/picture/block_test.py create mode 100644 pe/qt/dupeguru/picture/cache.py create mode 100644 pe/qt/dupeguru/picture/cache_test.py create mode 100644 pe/qt/dupeguru/picture/matchbase.py create mode 100644 pe/qt/dupeguru/results.py create mode 100644 pe/qt/dupeguru/results_test.py create mode 100644 pe/qt/dupeguru/scanner.py create mode 100644 pe/qt/dupeguru/scanner_test.py create mode 100644 pe/qt/gen.py create mode 100644 pe/qt/help/changelog.yaml create mode 100644 pe/qt/help/gen.py create mode 100644 pe/qt/help/skeleton/hardcoded.css create mode 100644 pe/qt/help/skeleton/images/hs_title.png create mode 100644 pe/qt/help/templates/base_dg.mako create mode 100644 pe/qt/help/templates/credits.mako create mode 100644 pe/qt/help/templates/directories.mako create mode 100644 pe/qt/help/templates/faq.mako create mode 100644 pe/qt/help/templates/intro.mako create mode 100644 pe/qt/help/templates/power_marker.mako create mode 100644 pe/qt/help/templates/preferences.mako create mode 100644 pe/qt/help/templates/quick_start.mako create mode 100644 pe/qt/help/templates/results.mako create mode 100644 pe/qt/help/templates/versions.mako create mode 100644 pe/qt/installer.aip create mode 100644 pe/qt/main_window.py create mode 100644 pe/qt/modules/block/block.pyx create mode 100644 pe/qt/modules/block/setup.py create mode 100644 pe/qt/preferences.py create mode 100644 pe/qt/preferences_dialog.py create mode 100644 pe/qt/preferences_dialog.ui create mode 100644 pe/qt/profile.py create mode 100644 pe/qt/start.py create mode 100644 pe/qt/verinfo create mode 100644 py/__init__.py create mode 100644 py/app.py create mode 100644 py/app_cocoa.py create mode 100644 py/app_cocoa_test.py create mode 100644 py/app_me_cocoa.py create mode 100644 py/app_pe_cocoa.py create mode 100644 py/app_se_cocoa.py create mode 100644 py/app_test.py create mode 100644 py/data.py create mode 100644 py/data_me.py create mode 100644 py/data_pe.py create mode 100644 py/directories.py create mode 100644 py/directories_test.py create mode 100644 py/engine.py create mode 100644 py/engine_test.py create mode 100644 py/export.py create mode 100644 py/export_test.py create mode 100644 py/gen.py create mode 100644 py/ignore.py create mode 100644 py/ignore_test.py create mode 100644 py/modules/block/block.pyx create mode 100644 py/modules/block/setup.py create mode 100644 py/modules/cache/cache.pyx create mode 100644 py/modules/cache/setup.py create mode 100644 py/picture/__init__.py create mode 100644 py/picture/block.py create mode 100644 py/picture/block_test.py create mode 100644 py/picture/cache.py create mode 100644 py/picture/cache_test.py create mode 100644 py/picture/matchbase.py create mode 100644 py/results.py create mode 100644 py/results_test.py create mode 100644 py/scanner.py create mode 100644 py/scanner_test.py create mode 100644 se/cocoa/AppDelegate.h create mode 100644 se/cocoa/AppDelegate.m create mode 100644 se/cocoa/Consts.h create mode 100644 se/cocoa/DetailsPanel.h create mode 100644 se/cocoa/DetailsPanel.m create mode 100644 se/cocoa/DirectoryPanel.h create mode 100644 se/cocoa/DirectoryPanel.m create mode 100644 se/cocoa/English.lproj/Details.nib/classes.nib create mode 100644 se/cocoa/English.lproj/Details.nib/info.nib create mode 100644 se/cocoa/English.lproj/Details.nib/keyedobjects.nib create mode 100644 se/cocoa/English.lproj/Directories.nib/classes.nib create mode 100644 se/cocoa/English.lproj/Directories.nib/info.nib create mode 100644 se/cocoa/English.lproj/Directories.nib/keyedobjects.nib create mode 100644 se/cocoa/English.lproj/InfoPlist.strings create mode 100644 se/cocoa/English.lproj/MainMenu.nib/classes.nib create mode 100644 se/cocoa/English.lproj/MainMenu.nib/info.nib create mode 100644 se/cocoa/English.lproj/MainMenu.nib/keyedobjects.nib create mode 100644 se/cocoa/Info.plist create mode 100644 se/cocoa/PyDupeGuru.h create mode 100644 se/cocoa/ResultWindow.h create mode 100644 se/cocoa/ResultWindow.m create mode 100755 se/cocoa/dupeguru.icns create mode 100644 se/cocoa/dupeguru.xcodeproj/hsoft.mode1 create mode 100644 se/cocoa/dupeguru.xcodeproj/project.pbxproj create mode 100644 se/cocoa/gen.py create mode 100644 se/cocoa/main.m create mode 100644 se/cocoa/py/dg_cocoa.py create mode 100644 se/cocoa/py/gen.py create mode 100644 se/cocoa/py/setup.py create mode 100644 se/cocoa/w3/dg.xsl create mode 100644 se/cocoa/w3/hardcoded.css create mode 100644 se/help/changelog.yaml create mode 100644 se/help/gen.py create mode 100644 se/help/skeleton/hardcoded.css create mode 100644 se/help/skeleton/images/hs_title.png create mode 100644 se/help/templates/base_dg.mako create mode 100644 se/help/templates/credits.mako create mode 100644 se/help/templates/directories.mako create mode 100644 se/help/templates/faq.mako create mode 100644 se/help/templates/intro.mako create mode 100644 se/help/templates/power_marker.mako create mode 100644 se/help/templates/preferences.mako create mode 100644 se/help/templates/quick_start.mako create mode 100644 se/help/templates/results.mako create mode 100644 se/help/templates/versions.mako create mode 100644 se/qt/app.py create mode 100644 se/qt/app_win.py create mode 100644 se/qt/build.py create mode 100644 se/qt/details_dialog.py create mode 100644 se/qt/details_dialog.ui create mode 100644 se/qt/dgse.spec create mode 100644 se/qt/gen.py create mode 100644 se/qt/installer.aip create mode 100644 se/qt/preferences.py create mode 100644 se/qt/preferences_dialog.py create mode 100644 se/qt/preferences_dialog.ui create mode 100644 se/qt/profile.py create mode 100644 se/qt/start.py create mode 100644 se/qt/verinfo diff --git a/base/cocoa/AppDelegate.h b/base/cocoa/AppDelegate.h new file mode 100644 index 00000000..055e0a67 --- /dev/null +++ b/base/cocoa/AppDelegate.h @@ -0,0 +1,17 @@ +#import +#import "RecentDirectories.h" +#import "PyDupeGuru.h" + +@interface AppDelegateBase : NSObject +{ + IBOutlet PyDupeGuruBase *py; + IBOutlet RecentDirectories *recentDirectories; + IBOutlet NSMenuItem *unlockMenuItem; + + NSString *_appName; +} +- (IBAction)unlockApp:(id)sender; + +- (PyDupeGuruBase *)py; +- (RecentDirectories *)recentDirectories; +@end diff --git a/base/cocoa/AppDelegate.m b/base/cocoa/AppDelegate.m new file mode 100644 index 00000000..ef3d9162 --- /dev/null +++ b/base/cocoa/AppDelegate.m @@ -0,0 +1,30 @@ +#import "AppDelegate.h" +#import "ProgressController.h" +#import "RegistrationInterface.h" +#import "Utils.h" +#import "Consts.h" + +@implementation AppDelegateBase +- (id)init +{ + self = [super init]; + _appName = @""; + return self; +} + +- (IBAction)unlockApp:(id)sender +{ + if ([[self py] isRegistered]) + return; + RegistrationInterface *ri = [[RegistrationInterface alloc] initWithApp:[self py] name:_appName limitDescription:LIMIT_DESC]; + if ([ri enterCode] == NSOKButton) + { + NSString *menuTitle = [NSString stringWithFormat:@"Thanks for buying %@",_appName]; + [unlockMenuItem setTitle:menuTitle]; + } + [ri release]; +} + +- (PyDupeGuruBase *)py { return py; } +- (RecentDirectories *)recentDirectories { return recentDirectories; } +@end diff --git a/base/cocoa/Consts.h b/base/cocoa/Consts.h new file mode 100644 index 00000000..c0c8a7ee --- /dev/null +++ b/base/cocoa/Consts.h @@ -0,0 +1,17 @@ +#import + +#define DuplicateSelectionChangedNotification @"DuplicateSelectionChangedNotification" +#define ResultsChangedNotification @"ResultsChangedNotification" +#define ResultsMarkingChangedNotification @"ResultsMarkingChangedNotification" +#define RegistrationRequired @"RegistrationRequired" +#define JobStarted @"JobStarted" +#define JobInProgress @"JobInProgress" + +#define jobLoad @"job_load" +#define jobScan @"job_scan" +#define jobCopy @"job_copy" +#define jobMove @"job_move" +#define jobDelete @"job_delete" + +#define DEMO_MAX_ACTION_COUNT 10 +#define LIMIT_DESC @"In the demo version, only 10 duplicates per session can be sent to Trash, moved or copied." \ No newline at end of file diff --git a/base/cocoa/DirectoryPanel.h b/base/cocoa/DirectoryPanel.h new file mode 100644 index 00000000..d5738b29 --- /dev/null +++ b/base/cocoa/DirectoryPanel.h @@ -0,0 +1,25 @@ +#import +#import "RecentDirectories.h" +#import "Outline.h" +#import "PyDupeGuru.h" + +@interface DirectoryPanelBase : NSWindowController +{ + IBOutlet NSPopUpButton *addButtonPopUp; + IBOutlet OutlineView *directories; + IBOutlet NSButton *removeButton; + + PyDupeGuruBase *_py; + RecentDirectories *_recentDirectories; +} +- (id)initWithParentApp:(id)aParentApp; + +- (IBAction)askForDirectory:(id)sender; +- (IBAction)changeDirectoryState:(id)sender; +- (IBAction)popupAddDirectoryMenu:(id)sender; +- (IBAction)removeSelectedDirectory:(id)sender; +- (IBAction)toggleVisible:(id)sender; + +- (void)addDirectory:(NSString *)directory; +- (void)refreshRemoveButtonText; +@end diff --git a/base/cocoa/DirectoryPanel.m b/base/cocoa/DirectoryPanel.m new file mode 100644 index 00000000..b62b7ae8 --- /dev/null +++ b/base/cocoa/DirectoryPanel.m @@ -0,0 +1,178 @@ +#import "DirectoryPanel.h" +#import "Dialogs.h" +#import "Utils.h" +#import "AppDelegate.h" + +@implementation DirectoryPanelBase +- (id)initWithParentApp:(id)aParentApp +{ + self = [super initWithWindowNibName:@"Directories"]; + [self window]; + AppDelegateBase *app = aParentApp; + _py = [app py]; + _recentDirectories = [app recentDirectories]; + [directories setPy:_py]; + NSPopUpButtonCell *cell = [[directories tableColumnWithIdentifier:@"1"] dataCell]; + [cell addItemWithTitle:@"Normal"]; + [cell addItemWithTitle:@"Reference"]; + [cell addItemWithTitle:@"Excluded"]; + for (int i=0;i<[[cell itemArray] count];i++) + { + NSMenuItem *mi = [[cell itemArray] objectAtIndex:i]; + [mi setTarget:self]; + [mi setAction:@selector(changeDirectoryState:)]; + [mi setTag:i]; + } + [self refreshRemoveButtonText]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(directorySelectionChanged:) name:NSOutlineViewSelectionDidChangeNotification object:directories]; + return self; +} + +/* Actions */ + +- (IBAction)askForDirectory:(id)sender +{ + NSOpenPanel *op = [NSOpenPanel openPanel]; + [op setCanChooseFiles:YES]; + [op setCanChooseDirectories:YES]; + [op setAllowsMultipleSelection:NO]; + [op setTitle:@"Select a directory to add to the scanning list"]; + [op setDelegate:self]; + if ([op runModalForTypes:nil] == NSOKButton) + { + NSString *directory = [[op filenames] objectAtIndex:0]; + [self addDirectory:directory]; + } +} + +- (IBAction)changeDirectoryState:(id)sender +{ + OVNode *node = [directories itemAtRow:[directories clickedRow]]; + [_py setDirectory:p2a([node indexPath]) state:i2n([sender tag])]; + [node resetAllBuffers]; + [directories display]; +} + +- (IBAction)popupAddDirectoryMenu:(id)sender +{ + if ([[_recentDirectories directories] count] == 0) + { + [self askForDirectory:sender]; + return; + } + NSMenu *m = [addButtonPopUp menu]; + while ([m numberOfItems] > 0) + [m removeItemAtIndex:0]; + NSMenuItem *mi = [m addItemWithTitle:@"Add New Directory..." action:@selector(askForDirectory:) keyEquivalent:@""]; + [mi setTarget:self]; + [m addItem:[NSMenuItem separatorItem]]; + [_recentDirectories fillMenu:m]; + [addButtonPopUp selectItem:nil]; + [[addButtonPopUp cell] performClickWithFrame:[sender frame] inView:[sender superview]]; +} + +- (IBAction)removeSelectedDirectory:(id)sender +{ + [[self window] makeKeyAndOrderFront:nil]; + if ([directories selectedRow] < 0) + return; + OVNode *node = [directories itemAtRow:[directories selectedRow]]; + if ([node level] == 1) + { + [_py removeDirectory:i2n([node index])]; + [directories reloadData]; + } + else + { + int state = n2i([[node buffer] objectAtIndex:1]); + int newState = state == 2 ? 0 : 2; // If excluded, put it back + [_py setDirectory:p2a([node indexPath]) state:i2n(newState)]; + [node resetAllBuffers]; + [directories display]; + } + [self refreshRemoveButtonText]; +} + +- (IBAction)toggleVisible:(id)sender +{ + if ([[self window] isVisible]) + [[self window] close]; + else + [[self window] makeKeyAndOrderFront:nil]; +} + +/* Public */ + +- (void)addDirectory:(NSString *)directory +{ + int r = [[_py addDirectory:directory] intValue]; + if (r) + { + NSString *m; + switch (r) + { + case 1: + { + m = @"This directory already is in the list."; + break; + } + case 2: + { + m = @"This directory does not exist."; + break; + } + } + [Dialogs showMessage:m]; + } + [directories reloadData]; + [_recentDirectories addDirectory:directory]; + [[self window] makeKeyAndOrderFront:nil]; +} + +- (void)refreshRemoveButtonText +{ + if ([directories selectedRow] < 0) + { + [removeButton setEnabled:NO]; + return; + } + [removeButton setEnabled:YES]; + OVNode *node = [directories itemAtRow:[directories selectedRow]]; + int state = n2i([[node buffer] objectAtIndex:1]); + NSString *buttonText = state == 2 ? @"Put Back" : @"Remove"; + [removeButton setTitle:buttonText]; +} + +/* Delegate */ + +- (void)outlineView:(NSOutlineView *)outlineView willDisplayCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn item:(id)item +{ + OVNode *node = item; + int state = n2i([[node buffer] objectAtIndex:1]); + if ([cell isKindOfClass:[NSTextFieldCell class]]) + { + NSTextFieldCell *textCell = cell; + if (state == 1) + [textCell setTextColor:[NSColor blueColor]]; + else if (state == 2) + [textCell setTextColor:[NSColor redColor]]; + else + [textCell setTextColor:[NSColor blackColor]]; + } +} + +- (BOOL)panel:(id)sender shouldShowFilename:(NSString *)path +{ + BOOL isdir; + [[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:&isdir]; + return isdir; +} + +/* Notifications */ + +- (void)directorySelectionChanged:(NSNotification *)aNotification +{ + [self refreshRemoveButtonText]; +} + +@end diff --git a/base/cocoa/English.lproj/InfoPlist.strings b/base/cocoa/English.lproj/InfoPlist.strings new file mode 100644 index 0000000000000000000000000000000000000000..948e12b991d433e9dc3ebc46350cb859c5bc1e07 GIT binary patch literal 204 zcmW-a%?`m}5Jk`0Q#6*1sMuJDl?@3NJb)A}LVwaCsW + + + + CFBundleDevelopmentRegion + English + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleName + ${PRODUCT_NAME} + CFBundleIconFile + + CFBundleIdentifier + com.yourcompany.yourcocoaframework + CFBundleInfoDictionaryVersion + 6.0 + CFBundlePackageType + FMWK + CFBundleSignature + ???? + CFBundleVersion + 1.0 + NSPrincipalClass + + + diff --git a/base/cocoa/PyDupeGuru.h b/base/cocoa/PyDupeGuru.h new file mode 100644 index 00000000..6a660b03 --- /dev/null +++ b/base/cocoa/PyDupeGuru.h @@ -0,0 +1,56 @@ +#import +#import "PyApp.h" + +@interface PyDupeGuruBase : PyApp +//Actions +- (NSNumber *)addDirectory:(NSString *)name; +- (void)removeDirectory:(NSNumber *)index; +- (void)setDirectory:(NSArray *)indexPath state:(NSNumber *)state; +- (void)loadResults; +- (void)saveResults; +- (void)loadIgnoreList; +- (void)saveIgnoreList; +- (void)clearIgnoreList; +- (void)purgeIgnoreList; +- (NSString *)exportToXHTMLwithColumns:(NSArray *)aColIds xslt:(NSString *)xsltPath css:(NSString *)cssPath; + +- (NSNumber *)doScan; + +- (void)selectPowerMarkerNodePaths:(NSArray *)aIndexPaths; +- (void)selectResultNodePaths:(NSArray *)aIndexPaths; + +- (void)toggleSelectedMark; +- (void)markAll; +- (void)markInvert; +- (void)markNone; + +- (void)addSelectedToIgnoreList; +- (void)refreshDetailsWithSelected; +- (void)removeSelected; +- (void)openSelected; +- (NSNumber *)renameSelected:(NSString *)aNewName; +- (void)revealSelected; +- (void)makeSelectedReference; +- (void)applyFilter:(NSString *)filter; + +- (void)sortGroupsBy:(NSNumber *)aIdentifier ascending:(NSNumber *)aAscending; +- (void)sortDupesBy:(NSNumber *)aIdentifier ascending:(NSNumber *)aAscending; + +- (void)copyOrMove:(NSNumber *)aCopy markedTo:(NSString *)destination recreatePath:(NSNumber *)aRecreateType; +- (void)deleteMarked; +- (void)removeMarked; + +//Data +- (NSNumber *)getIgnoreListCount; +- (NSNumber *)getMarkCount; +- (NSString *)getStatLine; +- (NSNumber *)getOperationalErrorCount; + +//Scanning options +- (void)setMinMatchPercentage:(NSNumber *)percentage; +- (void)setMixFileKind:(NSNumber *)mix_file_kind; +- (void)setDisplayDeltaValues:(NSNumber *)display_delta_values; +- (void)setEscapeFilterRegexp:(NSNumber *)escape_filter_regexp; +- (void)setRemoveEmptyFolders:(NSNumber *)remove_empty_folders; +- (void)setSizeThreshold:(int)size_threshold; +@end diff --git a/base/cocoa/ResultWindow.h b/base/cocoa/ResultWindow.h new file mode 100644 index 00000000..1cdfb70f --- /dev/null +++ b/base/cocoa/ResultWindow.h @@ -0,0 +1,34 @@ +#import +#import "Outline.h" +#import "DirectoryPanel.h" +#import "PyDupeGuru.h" + +@interface MatchesView : OutlineView +- (void)keyDown:(NSEvent *)theEvent; +@end + +@interface ResultWindowBase : NSWindowController +{ +@protected + IBOutlet PyDupeGuruBase *py; + IBOutlet id app; + IBOutlet NSView *actionMenuView; + IBOutlet NSSegmentedControl *deltaSwitch; + IBOutlet NSView *deltaSwitchView; + IBOutlet NSView *filterFieldView; + IBOutlet MatchesView *matches; + IBOutlet NSView *pmSwitchView; + + BOOL _powerMode; + BOOL _displayDelta; +} +- (NSString *)logoImageName; +/* Actions */ +- (IBAction)changeDelta:(id)sender; +- (IBAction)copyMarked:(id)sender; +- (IBAction)deleteMarked:(id)sender; +- (IBAction)expandAll:(id)sender; +- (IBAction)moveMarked:(id)sender; +/* Notifications */ +- (void)jobCompleted:(NSNotification *)aNotification; +@end diff --git a/base/cocoa/ResultWindow.m b/base/cocoa/ResultWindow.m new file mode 100644 index 00000000..1aa6cb8a --- /dev/null +++ b/base/cocoa/ResultWindow.m @@ -0,0 +1,316 @@ +#import "ResultWindow.h" +#import "Dialogs.h" +#import "ProgressController.h" +#import "Utils.h" +#import "RegistrationInterface.h" +#import "AppDelegate.h" +#import "Consts.h" + +#define tbbDirectories @"tbbDirectories" +#define tbbDetails @"tbbDetail" +#define tbbPreferences @"tbbPreferences" +#define tbbPowerMarker @"tbbPowerMarker" +#define tbbScan @"tbbScan" +#define tbbAction @"tbbAction" +#define tbbDelta @"tbbDelta" +#define tbbFilter @"tbbFilter" + +@implementation MatchesView +- (void)keyDown:(NSEvent *)theEvent +{ + unichar key = [[theEvent charactersIgnoringModifiers] characterAtIndex:0]; + // get flags and strip the lower 16 (device dependant) bits + unsigned int flags = ( [theEvent modifierFlags] & 0x00FF ); + if (((key == NSDeleteFunctionKey) || (key == NSDeleteCharacter)) && (flags == 0)) + [self sendAction:@selector(removeSelected:) to:[self delegate]]; + else + if ((key == 0x20) && (flags == 0)) // Space + [self sendAction:@selector(markSelected:) to:[self delegate]]; + else + [super keyDown:theEvent]; +} + +- (void)outlineView:(NSOutlineView *)outlineView setObjectValue:(id)object forTableColumn:(NSTableColumn *)tableColumn byItem:(id)item +{ + if (![[tableColumn identifier] isEqual:@"0"]) + return; //We only want to cover renames. + OVNode *node = item; + NSString *oldName = [[node buffer] objectAtIndex:0]; + NSString *newName = object; + if (![newName isEqual:oldName]) + { + BOOL renamed = n2b([(PyDupeGuruBase *)py renameSelected:newName]); + if (renamed) + [[NSNotificationCenter defaultCenter] postNotificationName:ResultsChangedNotification object:self]; + else + [Dialogs showMessage:[NSString stringWithFormat:@"The name '%@' already exists.",newName]]; + } +} +@end + +@implementation ResultWindowBase +- (void)awakeFromNib +{ + [self window]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(registrationRequired:) name:RegistrationRequired object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(jobStarted:) name:JobStarted object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(jobInProgress:) name:JobInProgress object:nil]; +} + +/* Virtual */ +- (NSString *)logoImageName +{ + return @"dg_logo32"; +} + +/* Actions */ +- (IBAction)changeDelta:(id)sender +{ + _displayDelta = [deltaSwitch selectedSegment] == 1; + [py setDisplayDeltaValues:b2n(_displayDelta)]; + [matches reloadData]; + [self expandAll:nil]; +} + +- (IBAction)copyMarked:(id)sender +{ + int mark_count = [[py getMarkCount] intValue]; + if (!mark_count) + return; + NSOpenPanel *op = [NSOpenPanel openPanel]; + [op setCanChooseFiles:NO]; + [op setCanChooseDirectories:YES]; + [op setCanCreateDirectories:YES]; + [op setAllowsMultipleSelection:NO]; + [op setTitle:@"Select a directory to copy marked files to"]; + if ([op runModalForTypes:nil] == NSOKButton) + { + NSString *directory = [[op filenames] objectAtIndex:0]; + NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; + [py copyOrMove:b2n(YES) markedTo:directory recreatePath:[ud objectForKey:@"recreatePathType"]]; + } +} + +- (IBAction)deleteMarked:(id)sender +{ + int mark_count = [[py getMarkCount] intValue]; + if (!mark_count) + return; + if ([Dialogs askYesNo:[NSString stringWithFormat:@"You are about to send %d files to Trash. Continue?",mark_count]] == NSAlertSecondButtonReturn) // NO + return; + NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; + [py setRemoveEmptyFolders:[ud objectForKey:@"removeEmptyFolders"]]; + [py deleteMarked]; +} + +- (IBAction)expandAll:(id)sender +{ + for (int i=0;i < [matches numberOfRows];i++) + [matches expandItem:[matches itemAtRow:i]]; +} + +- (IBAction)moveMarked:(id)sender +{ + int mark_count = [[py getMarkCount] intValue]; + if (!mark_count) + return; + NSOpenPanel *op = [NSOpenPanel openPanel]; + [op setCanChooseFiles:NO]; + [op setCanChooseDirectories:YES]; + [op setCanCreateDirectories:YES]; + [op setAllowsMultipleSelection:NO]; + [op setTitle:@"Select a directory to move marked files to"]; + if ([op runModalForTypes:nil] == NSOKButton) + { + NSString *directory = [[op filenames] objectAtIndex:0]; + NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; + [py setRemoveEmptyFolders:[ud objectForKey:@"removeEmptyFolders"]]; + [py copyOrMove:b2n(NO) markedTo:directory recreatePath:[ud objectForKey:@"recreatePathType"]]; + } +} + +/* Delegate */ + +- (void)outlineView:(NSOutlineView *)outlineView didClickTableColumn:(NSTableColumn *)tableColumn +{ + if ([[outlineView sortDescriptors] count] < 1) + return; + NSSortDescriptor *sd = [[outlineView sortDescriptors] objectAtIndex:0]; + if (_powerMode) + [py sortDupesBy:i2n([[sd key] intValue]) ascending:b2n([sd ascending])]; + else + [py sortGroupsBy:i2n([[sd key] intValue]) ascending:b2n([sd ascending])]; + [matches reloadData]; + [self expandAll:nil]; +} + +/* Notifications */ +- (void)windowWillClose:(NSNotification *)aNotification +{ + [NSApp hide:NSApp]; +} + +- (void)jobCompleted:(NSNotification *)aNotification +{ + [[NSNotificationCenter defaultCenter] postNotificationName:ResultsChangedNotification object:self]; + int r = n2i([py getOperationalErrorCount]); + id lastAction = [[ProgressController mainProgressController] jobId]; + if ([lastAction isEqualTo:jobCopy]) + { + if (r > 0) + [Dialogs showMessage:[NSString stringWithFormat:@"%d file(s) couldn't be copied.",r]]; + else + [Dialogs showMessage:@"All marked files were copied sucessfully."]; + } + if ([lastAction isEqualTo:jobMove]) + { + if (r > 0) + [Dialogs showMessage:[NSString stringWithFormat:@"%d file(s) couldn't be moved. They were kept in the results, and still are marked.",r]]; + else + [Dialogs showMessage:@"All marked files were moved sucessfully."]; + } + if ([lastAction isEqualTo:jobDelete]) + { + if (r > 0) + [Dialogs showMessage:[NSString stringWithFormat:@"%d file(s) couldn't be sent to Trash. They were kept in the results, and still are marked.",r]]; + else + [Dialogs showMessage:@"All marked files were sucessfully sent to Trash."]; + } + // Re-activate toolbar items right after the progress bar stops showing instead of waiting until + // a mouse-over is performed + [[[self window] toolbar] validateVisibleItems]; +} + +- (void)jobInProgress:(NSNotification *)aNotification +{ + [Dialogs showMessage:@"A previous action is still hanging in there. You can't start a new one yet. Wait a few seconds, then try again."]; +} + +- (void)jobStarted:(NSNotification *)aNotification +{ + NSDictionary *ui = [aNotification userInfo]; + NSString *desc = [ui valueForKey:@"desc"]; + [[ProgressController mainProgressController] setJobDesc:desc]; + NSString *jobid = [ui valueForKey:@"jobid"]; + // NSLog(jobid); + [[ProgressController mainProgressController] setJobId:jobid]; + [[ProgressController mainProgressController] showSheetForParent:[self window]]; +} + +- (void)registrationRequired:(NSNotification *)aNotification +{ + NSString *msg = @"This is a demo version, which only allows you 10 delete/copy/move actions per session. You cannot continue."; + [Dialogs showMessage:msg]; +} + +/* Toolbar */ +- (NSToolbarItem *)toolbar:(NSToolbar *)toolbar itemForItemIdentifier:(NSString *)itemIdentifier willBeInsertedIntoToolbar:(BOOL)flag +{ + NSToolbarItem *tbi = [[[NSToolbarItem alloc] initWithItemIdentifier:itemIdentifier] autorelease]; + if ([itemIdentifier isEqualTo:tbbDirectories]) + { + [tbi setLabel: @"Directories"]; + [tbi setToolTip: @"Show/Hide the directories panel."]; + [tbi setImage: [NSImage imageNamed: @"folder32"]]; + [tbi setTarget: app]; + [tbi setAction: @selector(toggleDirectories:)]; + } + else if ([itemIdentifier isEqualTo:tbbDetails]) + { + [tbi setLabel: @"Details"]; + [tbi setToolTip: @"Show/Hide the details panel."]; + [tbi setImage: [NSImage imageNamed: @"details32"]]; + [tbi setTarget: self]; + [tbi setAction: @selector(toggleDetailsPanel:)]; + } + else if ([itemIdentifier isEqualTo:tbbPreferences]) + { + [tbi setLabel: @"Preferences"]; + [tbi setToolTip: @"Show the preferences panel."]; + [tbi setImage: [NSImage imageNamed: @"preferences32"]]; + [tbi setTarget: self]; + [tbi setAction: @selector(showPreferencesPanel:)]; + } + else if ([itemIdentifier isEqualTo:tbbPowerMarker]) + { + [tbi setLabel: @"Power Marker"]; + [tbi setToolTip: @"When enabled, only the duplicates are shown, not the references."]; + [tbi setView:pmSwitchView]; + [tbi setMinSize:[pmSwitchView frame].size]; + [tbi setMaxSize:[pmSwitchView frame].size]; + } + else if ([itemIdentifier isEqualTo:tbbScan]) + { + [tbi setLabel: @"Start Scanning"]; + [tbi setToolTip: @"Start scanning for duplicates in the selected directories."]; + [tbi setImage: [NSImage imageNamed:[self logoImageName]]]; + [tbi setTarget: self]; + [tbi setAction: @selector(startDuplicateScan:)]; + } + else if ([itemIdentifier isEqualTo:tbbAction]) + { + [tbi setLabel: @"Action"]; + [tbi setView:actionMenuView]; + [tbi setMinSize:[actionMenuView frame].size]; + [tbi setMaxSize:[actionMenuView frame].size]; + } + else if ([itemIdentifier isEqualTo:tbbDelta]) + { + [tbi setLabel: @"Delta Values"]; + [tbi setToolTip: @"When enabled, this option makes dupeGuru display, where applicable, delta values instead of absolute values."]; + [tbi setView:deltaSwitchView]; + [tbi setMinSize:[deltaSwitchView frame].size]; + [tbi setMaxSize:[deltaSwitchView frame].size]; + } + else if ([itemIdentifier isEqualTo:tbbFilter]) + { + [tbi setLabel: @"Filter"]; + [tbi setToolTip: @"Filters the results using regular expression."]; + [tbi setView:filterFieldView]; + [tbi setMinSize:[filterFieldView frame].size]; + [tbi setMaxSize:NSMakeSize(1000, [filterFieldView frame].size.height)]; + } + [tbi setPaletteLabel: [tbi label]]; + return tbi; +} + +- (NSArray *)toolbarAllowedItemIdentifiers:(NSToolbar *)toolbar +{ + return [NSArray arrayWithObjects: + tbbDirectories, + tbbDetails, + tbbPreferences, + tbbPowerMarker, + tbbScan, + tbbAction, + tbbDelta, + tbbFilter, + NSToolbarSeparatorItemIdentifier, + NSToolbarSpaceItemIdentifier, + NSToolbarFlexibleSpaceItemIdentifier, + nil]; +} + +- (NSArray *)toolbarDefaultItemIdentifiers:(NSToolbar *)toolbar +{ + return [NSArray arrayWithObjects: + tbbScan, + tbbAction, + tbbDirectories, + tbbDetails, + tbbPowerMarker, + tbbDelta, + tbbFilter, + nil]; +} + +- (BOOL)validateToolbarItem:(NSToolbarItem *)theItem +{ + return ![[ProgressController mainProgressController] isShown]; +} + +- (BOOL)validateMenuItem:(NSMenuItem *)item +{ + return ![[ProgressController mainProgressController] isShown]; +} +@end diff --git a/base/cocoa/dgbase.xcodeproj/project.pbxproj b/base/cocoa/dgbase.xcodeproj/project.pbxproj new file mode 100644 index 00000000..54889c77 --- /dev/null +++ b/base/cocoa/dgbase.xcodeproj/project.pbxproj @@ -0,0 +1,408 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 44; + objects = { + +/* Begin PBXBuildFile section */ + 8DC2EF530486A6940098B216 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 089C1666FE841158C02AAC07 /* InfoPlist.strings */; }; + 8DC2EF570486A6940098B216 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1058C7B1FEA5585E11CA2CBB /* Cocoa.framework */; }; + CE62CA730CFAF80D0001B6E0 /* ResultWindow.h in Headers */ = {isa = PBXBuildFile; fileRef = CE62CA710CFAF80D0001B6E0 /* ResultWindow.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CE62CA740CFAF80D0001B6E0 /* ResultWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = CE62CA720CFAF80D0001B6E0 /* ResultWindow.m */; }; + CE70A0FC0CF8C2560048C314 /* PyDupeGuru.h in Headers */ = {isa = PBXBuildFile; fileRef = CE70A0FB0CF8C2560048C314 /* PyDupeGuru.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CE7D77050CF8987800BA287E /* Consts.h in Headers */ = {isa = PBXBuildFile; fileRef = CE7D77030CF8987800BA287E /* Consts.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CE80DA190FC191320086DCA6 /* RecentDirectories.h in Headers */ = {isa = PBXBuildFile; fileRef = CE80DA170FC191320086DCA6 /* RecentDirectories.h */; settings = {ATTRIBUTES = (Private, ); }; }; + CE80DA1A0FC191320086DCA6 /* RecentDirectories.m in Sources */ = {isa = PBXBuildFile; fileRef = CE80DA180FC191320086DCA6 /* RecentDirectories.m */; }; + CE80DA2E0FC191980086DCA6 /* Dialogs.h in Headers */ = {isa = PBXBuildFile; fileRef = CE80DA250FC191980086DCA6 /* Dialogs.h */; settings = {ATTRIBUTES = (Private, ); }; }; + CE80DA2F0FC191980086DCA6 /* Dialogs.m in Sources */ = {isa = PBXBuildFile; fileRef = CE80DA260FC191980086DCA6 /* Dialogs.m */; }; + CE80DA300FC191980086DCA6 /* ProgressController.h in Headers */ = {isa = PBXBuildFile; fileRef = CE80DA270FC191980086DCA6 /* ProgressController.h */; settings = {ATTRIBUTES = (Private, ); }; }; + CE80DA310FC191980086DCA6 /* ProgressController.m in Sources */ = {isa = PBXBuildFile; fileRef = CE80DA280FC191980086DCA6 /* ProgressController.m */; }; + CE80DA320FC191980086DCA6 /* PyApp.h in Headers */ = {isa = PBXBuildFile; fileRef = CE80DA290FC191980086DCA6 /* PyApp.h */; settings = {ATTRIBUTES = (Private, ); }; }; + CE80DA330FC191980086DCA6 /* RegistrationInterface.h in Headers */ = {isa = PBXBuildFile; fileRef = CE80DA2A0FC191980086DCA6 /* RegistrationInterface.h */; settings = {ATTRIBUTES = (Private, ); }; }; + CE80DA340FC191980086DCA6 /* RegistrationInterface.m in Sources */ = {isa = PBXBuildFile; fileRef = CE80DA2B0FC191980086DCA6 /* RegistrationInterface.m */; }; + CE80DA350FC191980086DCA6 /* Utils.h in Headers */ = {isa = PBXBuildFile; fileRef = CE80DA2C0FC191980086DCA6 /* Utils.h */; settings = {ATTRIBUTES = (Private, ); }; }; + CE80DA360FC191980086DCA6 /* Utils.m in Sources */ = {isa = PBXBuildFile; fileRef = CE80DA2D0FC191980086DCA6 /* Utils.m */; }; + CE80DA3B0FC191C40086DCA6 /* Outline.h in Headers */ = {isa = PBXBuildFile; fileRef = CE80DA370FC191C40086DCA6 /* Outline.h */; settings = {ATTRIBUTES = (Private, ); }; }; + CE80DA3C0FC191C40086DCA6 /* Outline.m in Sources */ = {isa = PBXBuildFile; fileRef = CE80DA380FC191C40086DCA6 /* Outline.m */; }; + CE80DA3D0FC191C40086DCA6 /* Table.h in Headers */ = {isa = PBXBuildFile; fileRef = CE80DA390FC191C40086DCA6 /* Table.h */; settings = {ATTRIBUTES = (Private, ); }; }; + CE80DA3E0FC191C40086DCA6 /* Table.m in Sources */ = {isa = PBXBuildFile; fileRef = CE80DA3A0FC191C40086DCA6 /* Table.m */; }; + CE80DB6D0FC194560086DCA6 /* progress.nib in Resources */ = {isa = PBXBuildFile; fileRef = CE80DB690FC194560086DCA6 /* progress.nib */; }; + CE80DB6E0FC194560086DCA6 /* registration.nib in Resources */ = {isa = PBXBuildFile; fileRef = CE80DB6B0FC194560086DCA6 /* registration.nib */; }; + CE895B5F0CFAE78300B5ADEA /* AppDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = CE895B5D0CFAE78300B5ADEA /* AppDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CE895B600CFAE78300B5ADEA /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = CE895B5E0CFAE78300B5ADEA /* AppDelegate.m */; }; + CE895C150CFAF0C900B5ADEA /* DirectoryPanel.h in Headers */ = {isa = PBXBuildFile; fileRef = CE895C130CFAF0C900B5ADEA /* DirectoryPanel.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CE895C160CFAF0C900B5ADEA /* DirectoryPanel.m in Sources */ = {isa = PBXBuildFile; fileRef = CE895C140CFAF0C900B5ADEA /* DirectoryPanel.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 0867D69BFE84028FC02AAC07 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = /System/Library/Frameworks/Foundation.framework; sourceTree = ""; }; + 0867D6A5FE840307C02AAC07 /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = /System/Library/Frameworks/AppKit.framework; sourceTree = ""; }; + 089C1667FE841158C02AAC07 /* English */ = {isa = PBXFileReference; fileEncoding = 10; lastKnownFileType = text.plist.strings; name = English; path = English.lproj/InfoPlist.strings; sourceTree = ""; }; + 1058C7B1FEA5585E11CA2CBB /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = /System/Library/Frameworks/Cocoa.framework; sourceTree = ""; }; + 8DC2EF5A0486A6940098B216 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 8DC2EF5B0486A6940098B216 /* dgbase.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = dgbase.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + CE62CA710CFAF80D0001B6E0 /* ResultWindow.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ResultWindow.h; sourceTree = ""; }; + CE62CA720CFAF80D0001B6E0 /* ResultWindow.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ResultWindow.m; sourceTree = ""; }; + CE70A0FB0CF8C2560048C314 /* PyDupeGuru.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PyDupeGuru.h; sourceTree = ""; }; + CE7D77030CF8987800BA287E /* Consts.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Consts.h; sourceTree = ""; }; + CE80DA170FC191320086DCA6 /* RecentDirectories.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RecentDirectories.h; path = ../../../cocoalib/RecentDirectories.h; sourceTree = SOURCE_ROOT; }; + CE80DA180FC191320086DCA6 /* RecentDirectories.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RecentDirectories.m; path = ../../../cocoalib/RecentDirectories.m; sourceTree = SOURCE_ROOT; }; + CE80DA250FC191980086DCA6 /* Dialogs.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Dialogs.h; path = ../../../cocoalib/Dialogs.h; sourceTree = SOURCE_ROOT; }; + CE80DA260FC191980086DCA6 /* Dialogs.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Dialogs.m; path = ../../../cocoalib/Dialogs.m; sourceTree = SOURCE_ROOT; }; + CE80DA270FC191980086DCA6 /* ProgressController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ProgressController.h; path = ../../../cocoalib/ProgressController.h; sourceTree = SOURCE_ROOT; }; + CE80DA280FC191980086DCA6 /* ProgressController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ProgressController.m; path = ../../../cocoalib/ProgressController.m; sourceTree = SOURCE_ROOT; }; + CE80DA290FC191980086DCA6 /* PyApp.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PyApp.h; path = ../../../cocoalib/PyApp.h; sourceTree = SOURCE_ROOT; }; + CE80DA2A0FC191980086DCA6 /* RegistrationInterface.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RegistrationInterface.h; path = ../../../cocoalib/RegistrationInterface.h; sourceTree = SOURCE_ROOT; }; + CE80DA2B0FC191980086DCA6 /* RegistrationInterface.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RegistrationInterface.m; path = ../../../cocoalib/RegistrationInterface.m; sourceTree = SOURCE_ROOT; }; + CE80DA2C0FC191980086DCA6 /* Utils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Utils.h; path = ../../../cocoalib/Utils.h; sourceTree = SOURCE_ROOT; }; + CE80DA2D0FC191980086DCA6 /* Utils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Utils.m; path = ../../../cocoalib/Utils.m; sourceTree = SOURCE_ROOT; }; + CE80DA370FC191C40086DCA6 /* Outline.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Outline.h; path = ../../../cocoalib/Outline.h; sourceTree = SOURCE_ROOT; }; + CE80DA380FC191C40086DCA6 /* Outline.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Outline.m; path = ../../../cocoalib/Outline.m; sourceTree = SOURCE_ROOT; }; + CE80DA390FC191C40086DCA6 /* Table.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Table.h; path = ../../../cocoalib/Table.h; sourceTree = SOURCE_ROOT; }; + CE80DA3A0FC191C40086DCA6 /* Table.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Table.m; path = ../../../cocoalib/Table.m; sourceTree = SOURCE_ROOT; }; + CE80DB6A0FC194560086DCA6 /* English */ = {isa = PBXFileReference; lastKnownFileType = wrapper.nib; name = English; path = ../../../cocoalib/English.lproj/progress.nib; sourceTree = SOURCE_ROOT; }; + CE80DB6C0FC194560086DCA6 /* English */ = {isa = PBXFileReference; lastKnownFileType = wrapper.nib; name = English; path = ../../../cocoalib/English.lproj/registration.nib; sourceTree = SOURCE_ROOT; }; + CE895B5D0CFAE78300B5ADEA /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + CE895B5E0CFAE78300B5ADEA /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + CE895C130CFAF0C900B5ADEA /* DirectoryPanel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DirectoryPanel.h; sourceTree = ""; }; + CE895C140CFAF0C900B5ADEA /* DirectoryPanel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DirectoryPanel.m; sourceTree = ""; }; + D2F7E79907B2D74100F64583 /* CoreData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreData.framework; path = /System/Library/Frameworks/CoreData.framework; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 8DC2EF560486A6940098B216 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 8DC2EF570486A6940098B216 /* Cocoa.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 034768DFFF38A50411DB9C8B /* Products */ = { + isa = PBXGroup; + children = ( + 8DC2EF5B0486A6940098B216 /* dgbase.framework */, + ); + name = Products; + sourceTree = ""; + }; + 0867D691FE84028FC02AAC07 /* dgbase */ = { + isa = PBXGroup; + children = ( + 08FB77AEFE84172EC02AAC07 /* Classes */, + CE80DA160FC1910F0086DCA6 /* cocoalib */, + 32C88DFF0371C24200C91783 /* Other Sources */, + 089C1665FE841158C02AAC07 /* Resources */, + 0867D69AFE84028FC02AAC07 /* External Frameworks and Libraries */, + 034768DFFF38A50411DB9C8B /* Products */, + ); + name = dgbase; + sourceTree = ""; + }; + 0867D69AFE84028FC02AAC07 /* External Frameworks and Libraries */ = { + isa = PBXGroup; + children = ( + 1058C7B0FEA5585E11CA2CBB /* Linked Frameworks */, + 1058C7B2FEA5585E11CA2CBB /* Other Frameworks */, + ); + name = "External Frameworks and Libraries"; + sourceTree = ""; + }; + 089C1665FE841158C02AAC07 /* Resources */ = { + isa = PBXGroup; + children = ( + CE80DB690FC194560086DCA6 /* progress.nib */, + CE80DB6B0FC194560086DCA6 /* registration.nib */, + 8DC2EF5A0486A6940098B216 /* Info.plist */, + 089C1666FE841158C02AAC07 /* InfoPlist.strings */, + ); + name = Resources; + sourceTree = ""; + }; + 08FB77AEFE84172EC02AAC07 /* Classes */ = { + isa = PBXGroup; + children = ( + CE62CA710CFAF80D0001B6E0 /* ResultWindow.h */, + CE62CA720CFAF80D0001B6E0 /* ResultWindow.m */, + CE895C130CFAF0C900B5ADEA /* DirectoryPanel.h */, + CE895C140CFAF0C900B5ADEA /* DirectoryPanel.m */, + CE895B5D0CFAE78300B5ADEA /* AppDelegate.h */, + CE895B5E0CFAE78300B5ADEA /* AppDelegate.m */, + ); + name = Classes; + sourceTree = ""; + }; + 1058C7B0FEA5585E11CA2CBB /* Linked Frameworks */ = { + isa = PBXGroup; + children = ( + 1058C7B1FEA5585E11CA2CBB /* Cocoa.framework */, + ); + name = "Linked Frameworks"; + sourceTree = ""; + }; + 1058C7B2FEA5585E11CA2CBB /* Other Frameworks */ = { + isa = PBXGroup; + children = ( + 0867D6A5FE840307C02AAC07 /* AppKit.framework */, + D2F7E79907B2D74100F64583 /* CoreData.framework */, + 0867D69BFE84028FC02AAC07 /* Foundation.framework */, + ); + name = "Other Frameworks"; + sourceTree = ""; + }; + 32C88DFF0371C24200C91783 /* Other Sources */ = { + isa = PBXGroup; + children = ( + CE70A0FB0CF8C2560048C314 /* PyDupeGuru.h */, + CE7D77030CF8987800BA287E /* Consts.h */, + ); + name = "Other Sources"; + sourceTree = ""; + }; + CE80DA160FC1910F0086DCA6 /* cocoalib */ = { + isa = PBXGroup; + children = ( + CE80DA370FC191C40086DCA6 /* Outline.h */, + CE80DA380FC191C40086DCA6 /* Outline.m */, + CE80DA390FC191C40086DCA6 /* Table.h */, + CE80DA3A0FC191C40086DCA6 /* Table.m */, + CE80DA250FC191980086DCA6 /* Dialogs.h */, + CE80DA260FC191980086DCA6 /* Dialogs.m */, + CE80DA270FC191980086DCA6 /* ProgressController.h */, + CE80DA280FC191980086DCA6 /* ProgressController.m */, + CE80DA290FC191980086DCA6 /* PyApp.h */, + CE80DA2A0FC191980086DCA6 /* RegistrationInterface.h */, + CE80DA2B0FC191980086DCA6 /* RegistrationInterface.m */, + CE80DA2C0FC191980086DCA6 /* Utils.h */, + CE80DA2D0FC191980086DCA6 /* Utils.m */, + CE80DA170FC191320086DCA6 /* RecentDirectories.h */, + CE80DA180FC191320086DCA6 /* RecentDirectories.m */, + ); + name = cocoalib; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 8DC2EF500486A6940098B216 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + CE7D77050CF8987800BA287E /* Consts.h in Headers */, + CE70A0FC0CF8C2560048C314 /* PyDupeGuru.h in Headers */, + CE895B5F0CFAE78300B5ADEA /* AppDelegate.h in Headers */, + CE895C150CFAF0C900B5ADEA /* DirectoryPanel.h in Headers */, + CE62CA730CFAF80D0001B6E0 /* ResultWindow.h in Headers */, + CE80DA190FC191320086DCA6 /* RecentDirectories.h in Headers */, + CE80DA2E0FC191980086DCA6 /* Dialogs.h in Headers */, + CE80DA300FC191980086DCA6 /* ProgressController.h in Headers */, + CE80DA320FC191980086DCA6 /* PyApp.h in Headers */, + CE80DA330FC191980086DCA6 /* RegistrationInterface.h in Headers */, + CE80DA350FC191980086DCA6 /* Utils.h in Headers */, + CE80DA3B0FC191C40086DCA6 /* Outline.h in Headers */, + CE80DA3D0FC191C40086DCA6 /* Table.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 8DC2EF4F0486A6940098B216 /* dgbase */ = { + isa = PBXNativeTarget; + buildConfigurationList = 1DEB91AD08733DA50010E9CD /* Build configuration list for PBXNativeTarget "dgbase" */; + buildPhases = ( + 8DC2EF500486A6940098B216 /* Headers */, + 8DC2EF520486A6940098B216 /* Resources */, + 8DC2EF540486A6940098B216 /* Sources */, + 8DC2EF560486A6940098B216 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = dgbase; + productInstallPath = "$(HOME)/Library/Frameworks"; + productName = dgbase; + productReference = 8DC2EF5B0486A6940098B216 /* dgbase.framework */; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 0867D690FE84028FC02AAC07 /* Project object */ = { + isa = PBXProject; + buildConfigurationList = 1DEB91B108733DA50010E9CD /* Build configuration list for PBXProject "dgbase" */; + compatibilityVersion = "Xcode 3.0"; + hasScannedForEncodings = 1; + mainGroup = 0867D691FE84028FC02AAC07 /* dgbase */; + productRefGroup = 034768DFFF38A50411DB9C8B /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 8DC2EF4F0486A6940098B216 /* dgbase */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 8DC2EF520486A6940098B216 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 8DC2EF530486A6940098B216 /* InfoPlist.strings in Resources */, + CE80DB6D0FC194560086DCA6 /* progress.nib in Resources */, + CE80DB6E0FC194560086DCA6 /* registration.nib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 8DC2EF540486A6940098B216 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + CE895B600CFAE78300B5ADEA /* AppDelegate.m in Sources */, + CE895C160CFAF0C900B5ADEA /* DirectoryPanel.m in Sources */, + CE62CA740CFAF80D0001B6E0 /* ResultWindow.m in Sources */, + CE80DA1A0FC191320086DCA6 /* RecentDirectories.m in Sources */, + CE80DA2F0FC191980086DCA6 /* Dialogs.m in Sources */, + CE80DA310FC191980086DCA6 /* ProgressController.m in Sources */, + CE80DA340FC191980086DCA6 /* RegistrationInterface.m in Sources */, + CE80DA360FC191980086DCA6 /* Utils.m in Sources */, + CE80DA3C0FC191C40086DCA6 /* Outline.m in Sources */, + CE80DA3E0FC191C40086DCA6 /* Table.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 089C1666FE841158C02AAC07 /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + 089C1667FE841158C02AAC07 /* English */, + ); + name = InfoPlist.strings; + sourceTree = ""; + }; + CE80DB690FC194560086DCA6 /* progress.nib */ = { + isa = PBXVariantGroup; + children = ( + CE80DB6A0FC194560086DCA6 /* English */, + ); + name = progress.nib; + sourceTree = SOURCE_ROOT; + }; + CE80DB6B0FC194560086DCA6 /* registration.nib */ = { + isa = PBXVariantGroup; + children = ( + CE80DB6C0FC194560086DCA6 /* English */, + ); + name = registration.nib; + sourceTree = SOURCE_ROOT; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 1DEB91AE08733DA50010E9CD /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + COPY_PHASE_STRIP = NO; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "\"$(SRCROOT)/../../../cocoalib/build/Release\"", + ); + FRAMEWORK_VERSION = A; + GCC_DYNAMIC_NO_PIC = NO; + GCC_ENABLE_FIX_AND_CONTINUE = YES; + GCC_MODEL_TUNING = G5; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + INFOPLIST_FILE = Info.plist; + INSTALL_PATH = "$(HOME)/Library/Frameworks"; + PRODUCT_NAME = dgbase; + WRAPPER_EXTENSION = framework; + ZERO_LINK = YES; + }; + name = Debug; + }; + 1DEB91AF08733DA50010E9CD /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "\"$(SRCROOT)/../../../cocoalib/build/Release\"", + ); + FRAMEWORK_VERSION = A; + GCC_MODEL_TUNING = G5; + GCC_PRECOMPILE_PREFIX_HEADER = NO; + GCC_PREFIX_HEADER = ""; + INFOPLIST_FILE = Info.plist; + INSTALL_PATH = "@executable_path/../Frameworks"; + PRODUCT_NAME = dgbase; + WRAPPER_EXTENSION = framework; + }; + name = Release; + }; + 1DEB91B208733DA50010E9CD /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + GCC_C_LANGUAGE_STANDARD = c99; + GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + PREBINDING = NO; + SDKROOT = "$(DEVELOPER_SDK_DIR)/MacOSX10.4u.sdk"; + }; + name = Debug; + }; + 1DEB91B308733DA50010E9CD /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = ( + ppc, + i386, + ); + GCC_C_LANGUAGE_STANDARD = c99; + GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + PREBINDING = NO; + SDKROOT = "$(DEVELOPER_SDK_DIR)/MacOSX10.4u.sdk"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 1DEB91AD08733DA50010E9CD /* Build configuration list for PBXNativeTarget "dgbase" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 1DEB91AE08733DA50010E9CD /* Debug */, + 1DEB91AF08733DA50010E9CD /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 1DEB91B108733DA50010E9CD /* Build configuration list for PBXProject "dgbase" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 1DEB91B208733DA50010E9CD /* Debug */, + 1DEB91B308733DA50010E9CD /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 0867D690FE84028FC02AAC07 /* Project object */; +} diff --git a/base/qt/__init__.py b/base/qt/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/base/qt/about_box.py b/base/qt/about_box.py new file mode 100644 index 00000000..55a36eb1 --- /dev/null +++ b/base/qt/about_box.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python +# Unit Name: about_box +# Created By: Virgil Dupras +# Created On: 2009-05-09 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +from PyQt4.QtCore import Qt, QCoreApplication, SIGNAL +from PyQt4.QtGui import QDialog, QDialogButtonBox, QPixmap + +from about_box_ui import Ui_AboutBox + +class AboutBox(QDialog, Ui_AboutBox): + def __init__(self, parent, app): + flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint | Qt.MSWindowsFixedSizeDialogHint + QDialog.__init__(self, parent, flags) + self.app = app + self._setupUi() + + self.connect(self.buttonBox, SIGNAL('clicked(QAbstractButton*)'), self.buttonClicked) + + def _setupUi(self): + self.setupUi(self) + # Stuff that can't be done in the Designer + self.setWindowTitle(u"About %s" % QCoreApplication.instance().applicationName()) + self.nameLabel.setText(QCoreApplication.instance().applicationName()) + self.versionLabel.setText('Version ' + QCoreApplication.instance().applicationVersion()) + self.logoLabel.setPixmap(QPixmap(':/%s_big' % self.app.LOGO_NAME)) + self.registerButton = self.buttonBox.addButton("Register", QDialogButtonBox.ActionRole) + + #--- Events + def buttonClicked(self, button): + if button is self.registerButton: + self.app.ask_for_reg_code() + diff --git a/base/qt/about_box.ui b/base/qt/about_box.ui new file mode 100644 index 00000000..aa9c5ce5 --- /dev/null +++ b/base/qt/about_box.ui @@ -0,0 +1,133 @@ + + + AboutBox + + + + 0 + 0 + 400 + 190 + + + + + 0 + 0 + + + + About dupeGuru + + + + + + + + + :/logo_me_big + + + + + + + + + + 75 + true + + + + dupeGuru + + + + + + + Version + + + + + + + Copyright Hardcoded Software 2009 + + + + + + + + 75 + true + + + + Registered To: + + + + + + + UNREGISTERED + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Ok + + + + + + + + + + + + + buttonBox + accepted() + AboutBox + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + AboutBox + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/base/qt/app.py b/base/qt/app.py new file mode 100644 index 00000000..3fa340bf --- /dev/null +++ b/base/qt/app.py @@ -0,0 +1,269 @@ +#!/usr/bin/env python +# Unit Name: app +# Created By: Virgil Dupras +# Created On: 2009-04-25 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +import logging +import os.path as op +import traceback + +from PyQt4.QtCore import Qt, QTimer, QObject, QCoreApplication, QUrl, SIGNAL +from PyQt4.QtGui import QProgressDialog, QDesktopServices, QFileDialog, QDialog, QMessageBox + +from hsutil import job +from hsutil.reg import RegistrationRequired + +from dupeguru import data_pe +from dupeguru.app import (DupeGuru as DupeGuruBase, JOB_SCAN, JOB_LOAD, JOB_MOVE, JOB_COPY, + JOB_DELETE) + +from main_window import MainWindow +from directories_dialog import DirectoriesDialog +from about_box import AboutBox +from reg import Registration +from error_report_dialog import ErrorReportDialog + +JOBID2TITLE = { + JOB_SCAN: "Scanning for duplicates", + JOB_LOAD: "Loading", + JOB_MOVE: "Moving", + JOB_COPY: "Copying", + JOB_DELETE: "Sending files to the recycle bin", +} + +class Progress(QProgressDialog, job.ThreadedJobPerformer): + def __init__(self, parent): + flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint + QProgressDialog.__init__(self, '', u"Cancel", 0, 100, parent, flags) + self.setModal(True) + self.setAutoReset(False) + self.setAutoClose(False) + self._timer = QTimer() + self._jobid = '' + self.connect(self._timer, SIGNAL('timeout()'), self.updateProgress) + + def updateProgress(self): + # the values might change before setValue happens + last_progress = self.last_progress + last_desc = self.last_desc + if not self._job_running or last_progress is None: + self._timer.stop() + self.close() + self.emit(SIGNAL('finished(QString)'), self._jobid) + if self._last_error is not None: + s = ''.join(traceback.format_exception(*self._last_error)) + dialog = ErrorReportDialog(self.parent(), s) + dialog.exec_() + return + if self.wasCanceled(): + self.job_cancelled = True + return + if last_desc: + self.setLabelText(last_desc) + self.setValue(last_progress) + + def run(self, jobid, title, target, args=()): + self._jobid = jobid + self.reset() + self.setLabelText('') + self.run_threaded(target, args) + self.setWindowTitle(title) + self.show() + self._timer.start(500) + + +def demo_method(method): + def wrapper(self, *args, **kwargs): + try: + return method(self, *args, **kwargs) + except RegistrationRequired: + msg = "The demo version of dupeGuru only allows 10 actions (delete/move/copy) per session." + QMessageBox.information(self.main_window, 'Demo', msg) + + return wrapper + +class DupeGuru(DupeGuruBase, QObject): + LOGO_NAME = '' + NAME = '' + DELTA_COLUMNS = frozenset() + + def __init__(self, data_module, appid): + appdata = unicode(QDesktopServices.storageLocation(QDesktopServices.DataLocation)) + DupeGuruBase.__init__(self, data_module, appdata, appid) + QObject.__init__(self) + self._setup() + + #--- Private + def _setup(self): + self.selected_dupe = None + self.prefs = self._create_preferences() + self.prefs.load() + self._update_options() + self.main_window = self._create_main_window() + self._progress = Progress(self.main_window) + self.directories_dialog = DirectoriesDialog(self.main_window, self) + self.details_dialog = self._create_details_dialog(self.main_window) + self.preferences_dialog = self._create_preferences_dialog(self.main_window) + self.about_box = AboutBox(self.main_window, self) + + self.reg = Registration(self) + self.set_registration(self.prefs.registration_code, self.prefs.registration_email) + if not self.registered: + self.reg.show_nag() + self.main_window.show() + self.load() + + self.connect(QCoreApplication.instance(), SIGNAL('aboutToQuit()'), self.application_will_terminate) + self.connect(self._progress, SIGNAL('finished(QString)'), self.job_finished) + + def _setup_as_registered(self): + self.prefs.registration_code = self.registration_code + self.prefs.registration_email = self.registration_email + self.main_window.actionRegister.setVisible(False) + self.about_box.registerButton.hide() + self.about_box.registeredEmailLabel.setText(self.prefs.registration_email) + + def _update_options(self): + self.scanner.mix_file_kind = self.prefs.mix_file_kind + self.options['escape_filter_regexp'] = self.prefs.use_regexp + self.options['clean_empty_dirs'] = self.prefs.remove_empty_folders + + #--- Virtual + def _create_details_dialog(self, parent): + raise NotImplementedError() + + def _create_main_window(self): + return MainWindow(app=self) + + def _create_preferences(self): + raise NotImplementedError() + + def _create_preferences_dialog(self, parent): + raise NotImplementedError() + + #--- Override + def _start_job(self, jobid, func): + title = JOBID2TITLE[jobid] + try: + j = self._progress.create_job() + self._progress.run(jobid, title, func, args=(j, )) + except job.JobInProgressError: + msg = "A previous action is still hanging in there. You can't start a new one yet. Wait a few seconds, then try again." + QMessageBox.information(self.main_window, 'Action in progress', msg) + + #--- Public + def add_dupes_to_ignore_list(self, duplicates): + for dupe in duplicates: + self.AddToIgnoreList(dupe) + self.remove_duplicates(duplicates) + + def ApplyFilter(self, filter): + DupeGuruBase.ApplyFilter(self, filter) + self.emit(SIGNAL('resultsChanged()')) + + def ask_for_reg_code(self): + if self.reg.ask_for_code(): + self._setup_ui_as_registered() + + @demo_method + def copy_or_move_marked(self, copy): + opname = 'copy' if copy else 'move' + title = "Select a directory to {0} marked files to".format(opname) + flags = QFileDialog.ShowDirsOnly + destination = unicode(QFileDialog.getExistingDirectory(self.main_window, title, '', flags)) + if not destination: + return + recreate_path = self.prefs.destination_type + DupeGuruBase.copy_or_move_marked(self, copy, destination, recreate_path) + + delete_marked = demo_method(DupeGuruBase.delete_marked) + + def make_reference(self, duplicates): + DupeGuruBase.make_reference(self, duplicates) + self.emit(SIGNAL('resultsChanged()')) + + def mark_all(self): + self.results.mark_all() + self.emit(SIGNAL('dupeMarkingChanged()')) + + def mark_invert(self): + self.results.mark_invert() + self.emit(SIGNAL('dupeMarkingChanged()')) + + def mark_none(self): + self.results.mark_none() + self.emit(SIGNAL('dupeMarkingChanged()')) + + def open_selected(self): + if self.selected_dupe is None: + return + url = QUrl.fromLocalFile(unicode(self.selected_dupe.path)) + QDesktopServices.openUrl(url) + + def remove_duplicates(self, duplicates): + self.results.remove_duplicates(duplicates) + self.emit(SIGNAL('resultsChanged()')) + + def remove_marked_duplicates(self): + marked = [d for d in self.results.dupes if self.results.is_marked(d)] + self.remove_duplicates(marked) + + def rename_dupe(self, dupe, newname): + try: + dupe.move(dupe.parent, newname) + return True + except (IndexError, fs.FSError) as e: + logging.warning("dupeGuru Warning: %s" % unicode(e)) + return False + + def reveal_selected(self): + if self.selected_dupe is None: + return + url = QUrl.fromLocalFile(unicode(self.selected_dupe.path[:-1])) + QDesktopServices.openUrl(url) + + def select_duplicate(self, dupe): + self.selected_dupe = dupe + self.emit(SIGNAL('duplicateSelected()')) + + def show_about_box(self): + self.about_box.show() + + def show_details(self): + self.details_dialog.show() + + def show_directories(self): + self.directories_dialog.show() + + def show_help(self): + url = QUrl.fromLocalFile(op.abspath('help/intro.htm')) + QDesktopServices.openUrl(url) + + def show_preferences(self): + self.preferences_dialog.load() + result = self.preferences_dialog.exec_() + if result == QDialog.Accepted: + self.preferences_dialog.save() + self.prefs.save() + self._update_options() + + def toggle_marking_for_dupes(self, dupes): + for dupe in dupes: + self.results.mark_toggle(dupe) + self.emit(SIGNAL('dupeMarkingChanged()')) + + #--- Events + def application_will_terminate(self): + self.Save() + self.SaveIgnoreList() + + def job_finished(self, jobid): + self.emit(SIGNAL('resultsChanged()')) + if jobid == JOB_LOAD: + self.emit(SIGNAL('directoriesChanged()')) + if jobid in (JOB_MOVE, JOB_COPY, JOB_DELETE) and self.last_op_error_count > 0: + msg = "{0} files could not be processed.".format(self.results.mark_count) + QMessageBox.warning(self.main_window, 'Warning', msg) + diff --git a/base/qt/details_table.py b/base/qt/details_table.py new file mode 100644 index 00000000..1c45de1e --- /dev/null +++ b/base/qt/details_table.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python +# Unit Name: details_table +# Created By: Virgil Dupras +# Created On: 2009-05-17 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +from PyQt4.QtCore import Qt, SIGNAL, QAbstractTableModel, QVariant +from PyQt4.QtGui import QHeaderView, QTableView + +HEADER = ['Attribute', 'Selected', 'Reference'] + +class DetailsModel(QAbstractTableModel): + def __init__(self, app): + QAbstractTableModel.__init__(self) + self._app = app + self._data = app.data + self._dupe_data = None + self._ref_data = None + self.connect(app, SIGNAL('duplicateSelected()'), self.duplicateSelected) + + def columnCount(self, parent): + return len(HEADER) + + def data(self, index, role): + if not index.isValid(): + return QVariant() + if role != Qt.DisplayRole: + return QVariant() + column = index.column() + row = index.row() + if column == 0: + return QVariant(self._data.COLUMNS[row]['display']) + elif column == 1 and self._dupe_data: + return QVariant(self._dupe_data[row]) + elif column == 2 and self._ref_data: + return QVariant(self._ref_data[row]) + return QVariant() + + def headerData(self, section, orientation, role): + if orientation == Qt.Horizontal and role == Qt.DisplayRole and section < len(HEADER): + return QVariant(HEADER[section]) + return QVariant() + + def rowCount(self, parent): + return len(self._data.COLUMNS) + + #--- Events + def duplicateSelected(self): + dupe = self._app.selected_dupe + group = self._app.results.get_group_of_duplicate(dupe) + ref = group.ref + self._dupe_data = self._data.GetDisplayInfo(dupe, group) + self._ref_data = self._data.GetDisplayInfo(ref, group) + self.reset() + + +class DetailsTable(QTableView): + def __init__(self, *args): + QTableView.__init__(self, *args) + self.setAlternatingRowColors(True) + self.setSelectionBehavior(QTableView.SelectRows) + self.setShowGrid(False) + + def setModel(self, model): + QTableView.setModel(self, model) + # The model needs to be set to set header stuff + hheader = self.horizontalHeader() + hheader.setHighlightSections(False) + hheader.setStretchLastSection(False) + hheader.resizeSection(0, 100) + hheader.setResizeMode(0, QHeaderView.Fixed) + hheader.setResizeMode(1, QHeaderView.Stretch) + hheader.setResizeMode(2, QHeaderView.Stretch) + vheader = self.verticalHeader() + vheader.setVisible(False) + vheader.setDefaultSectionSize(18) + diff --git a/base/qt/dg.qrc b/base/qt/dg.qrc new file mode 100644 index 00000000..f2f5e936 --- /dev/null +++ b/base/qt/dg.qrc @@ -0,0 +1,17 @@ + + + images/details32.png + images/dgpe_logo_32.png + images/dgpe_logo_128.png + images/dgme_logo_32.png + images/dgme_logo_128.png + images/dgse_logo_32.png + images/dgse_logo_128.png + images/folderwin32.png + images/gear.png + images/preferences32.png + images/actions32.png + images/delta32.png + images/power_marker32.png + + \ No newline at end of file diff --git a/base/qt/directories_dialog.py b/base/qt/directories_dialog.py new file mode 100644 index 00000000..e2f3ddb3 --- /dev/null +++ b/base/qt/directories_dialog.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python +# Unit Name: directories_dialog +# Created By: Virgil Dupras +# Created On: 2009-04-25 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +from PyQt4.QtCore import SIGNAL, Qt +from PyQt4.QtGui import QDialog, QFileDialog, QHeaderView + +from directories_dialog_ui import Ui_DirectoriesDialog +from directories_model import DirectoriesModel, DirectoriesDelegate + +class DirectoriesDialog(QDialog, Ui_DirectoriesDialog): + def __init__(self, parent, app): + flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint + QDialog.__init__(self, parent, flags) + self.app = app + self._setupUi() + self._updateRemoveButton() + + self.connect(self.doneButton, SIGNAL('clicked()'), self.doneButtonClicked) + self.connect(self.addButton, SIGNAL('clicked()'), self.addButtonClicked) + self.connect(self.removeButton, SIGNAL('clicked()'), self.removeButtonClicked) + self.connect(self.treeView.selectionModel(), SIGNAL('selectionChanged(QItemSelection,QItemSelection)'), self.selectionChanged) + self.connect(self.app, SIGNAL('directoriesChanged()'), self.directoriesChanged) + + def _setupUi(self): + self.setupUi(self) + # Stuff that can't be done in the Designer + self.directoriesModel = DirectoriesModel(self.app) + self.directoriesDelegate = DirectoriesDelegate() + self.treeView.setItemDelegate(self.directoriesDelegate) + self.treeView.setModel(self.directoriesModel) + + header = self.treeView.header() + header.setStretchLastSection(False) + header.setResizeMode(0, QHeaderView.Stretch) + header.setResizeMode(1, QHeaderView.Fixed) + header.resizeSection(1, 100) + + def _updateRemoveButton(self): + indexes = self.treeView.selectedIndexes() + if not indexes: + self.removeButton.setEnabled(False) + return + self.removeButton.setEnabled(True) + index = indexes[0] + node = index.internalPointer() + # label = 'Remove' if node.parent is None else 'Exclude' + + def addButtonClicked(self): + title = u"Select a directory to add to the scanning list" + flags = QFileDialog.ShowDirsOnly + dirpath = unicode(QFileDialog.getExistingDirectory(self, title, '', flags)) + if not dirpath: + return + self.app.AddDirectory(dirpath) + self.directoriesModel.reset() + + def directoriesChanged(self): + self.directoriesModel.reset() + + def doneButtonClicked(self): + self.hide() + + def removeButtonClicked(self): + indexes = self.treeView.selectedIndexes() + if not indexes: + return + index = indexes[0] + node = index.internalPointer() + if node.parent is None: + row = index.row() + del self.app.directories[row] + self.directoriesModel.reset() + + def selectionChanged(self, selected, deselected): + self._updateRemoveButton() + diff --git a/base/qt/directories_dialog.ui b/base/qt/directories_dialog.ui new file mode 100644 index 00000000..68bc8d84 --- /dev/null +++ b/base/qt/directories_dialog.ui @@ -0,0 +1,133 @@ + + + DirectoriesDialog + + + + 0 + 0 + 420 + 338 + + + + Directories + + + + + + QAbstractItemView::DoubleClicked|QAbstractItemView::EditKeyPressed|QAbstractItemView::SelectedClicked + + + true + + + false + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 91 + 0 + + + + + 16777215 + 32 + + + + Remove + + + + + + + + 91 + 0 + + + + + 16777215 + 32 + + + + Add + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + + 91 + 0 + + + + + 16777215 + 32 + + + + Done + + + true + + + + + + + + + + diff --git a/base/qt/directories_model.py b/base/qt/directories_model.py new file mode 100644 index 00000000..cae88f39 --- /dev/null +++ b/base/qt/directories_model.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python +# Unit Name: directories_model +# Created By: Virgil Dupras +# Created On: 2009-04-25 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +from PyQt4.QtCore import QVariant, QModelIndex, Qt, QRect, QEvent, QPoint +from PyQt4.QtGui import QComboBox, QStyledItemDelegate, QMouseEvent, QApplication, QBrush + +from tree_model import TreeNode, TreeModel + +HEADERS = ['Name', 'State'] +STATES = ['Normal', 'Reference', 'Excluded'] + +class DirectoriesDelegate(QStyledItemDelegate): + def createEditor(self, parent, option, index): + editor = QComboBox(parent); + editor.addItems(STATES) + return editor + + def setEditorData(self, editor, index): + value, ok = index.model().data(index, Qt.EditRole).toInt() + assert ok + editor.setCurrentIndex(value); + press = QMouseEvent(QEvent.MouseButtonPress, QPoint(0, 0), Qt.LeftButton, Qt.LeftButton, Qt.NoModifier) + release = QMouseEvent(QEvent.MouseButtonRelease, QPoint(0, 0), Qt.LeftButton, Qt.LeftButton, Qt.NoModifier) + QApplication.sendEvent(editor, press) + QApplication.sendEvent(editor, release) + # editor.showPopup() # this causes a weird glitch. the ugly workaround is above. + + def setModelData(self, editor, model, index): + value = QVariant(editor.currentIndex()) + model.setData(index, value, Qt.EditRole) + + def updateEditorGeometry(self, editor, option, index): + editor.setGeometry(option.rect) + + +class DirectoryNode(TreeNode): + def __init__(self, parent, ref, row): + TreeNode.__init__(self, parent, row) + self.ref = ref + + def _get_children(self): + children = [] + for index, directory in enumerate(self.ref.dirs): + node = DirectoryNode(self, directory, index) + children.append(node) + return children + + +class DirectoriesModel(TreeModel): + def __init__(self, app): + self._dirs = app.directories + TreeModel.__init__(self) + + def _root_nodes(self): + nodes = [] + for index, directory in enumerate(self._dirs): + nodes.append(DirectoryNode(None, directory, index)) + return nodes + + def columnCount(self, parent): + return 2 + + def data(self, index, role): + if not index.isValid(): + return QVariant() + node = index.internalPointer() + if role == Qt.DisplayRole: + if index.column() == 0: + return QVariant(node.ref.name) + else: + return QVariant(STATES[self._dirs.GetState(node.ref.path)]) + elif role == Qt.EditRole and index.column() == 1: + return QVariant(self._dirs.GetState(node.ref.path)) + elif role == Qt.ForegroundRole: + state = self._dirs.GetState(node.ref.path) + if state == 1: + return QVariant(QBrush(Qt.blue)) + elif state == 2: + return QVariant(QBrush(Qt.red)) + return QVariant() + + def flags(self, index): + if not index.isValid(): + return 0 + result = Qt.ItemIsEnabled | Qt.ItemIsSelectable + if index.column() == 1: + result |= Qt.ItemIsEditable + return result + + def headerData(self, section, orientation, role): + if orientation == Qt.Horizontal: + if role == Qt.DisplayRole and section < len(HEADERS): + return QVariant(HEADERS[section]) + return QVariant() + + def setData(self, index, value, role): + if not index.isValid() or role != Qt.EditRole or index.column() != 1: + return False + node = index.internalPointer() + state, ok = value.toInt() + assert ok + self._dirs.SetState(node.ref.path, state) + return True + diff --git a/base/qt/error_report_dialog.py b/base/qt/error_report_dialog.py new file mode 100644 index 00000000..4aa8f977 --- /dev/null +++ b/base/qt/error_report_dialog.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python +# Unit Name: error_report_dialog +# Created By: Virgil Dupras +# Created On: 2009-05-23 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +from PyQt4.QtCore import Qt, QUrl +from PyQt4.QtGui import QDialog, QDesktopServices + +from error_report_dialog_ui import Ui_ErrorReportDialog + +class ErrorReportDialog(QDialog, Ui_ErrorReportDialog): + def __init__(self, parent, error): + flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint + QDialog.__init__(self, parent, flags) + self.setupUi(self) + self.errorTextEdit.setPlainText(error) + + def accept(self): + text = self.errorTextEdit.toPlainText() + url = QUrl("mailto:support@hardcoded.net?SUBJECT=Error Report&BODY=%s" % text) + QDesktopServices.openUrl(url) + QDialog.accept(self) + diff --git a/base/qt/error_report_dialog.ui b/base/qt/error_report_dialog.ui new file mode 100644 index 00000000..0974dd2f --- /dev/null +++ b/base/qt/error_report_dialog.ui @@ -0,0 +1,117 @@ + + + ErrorReportDialog + + + + 0 + 0 + 553 + 349 + + + + Error Report + + + + + + Something went wrong. Would you like to send the error report to Hardcoded Software? + + + true + + + + + + + true + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 110 + 0 + + + + Don't Send + + + + + + + + 110 + 0 + + + + Send + + + true + + + + + + + + + + + sendButton + clicked() + ErrorReportDialog + accept() + + + 485 + 320 + + + 276 + 174 + + + + + dontSendButton + clicked() + ErrorReportDialog + reject() + + + 373 + 320 + + + 276 + 174 + + + + + diff --git a/base/qt/gen.py b/base/qt/gen.py new file mode 100644 index 00000000..3b0df2fa --- /dev/null +++ b/base/qt/gen.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python +# Unit Name: gen +# Created By: Virgil Dupras +# Created On: 2009-05-22 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +import os + +def print_and_do(cmd): + print cmd + os.system(cmd) + +print_and_do("pyuic4 main_window.ui > main_window_ui.py") +print_and_do("pyuic4 directories_dialog.ui > directories_dialog_ui.py") +print_and_do("pyuic4 about_box.ui > about_box_ui.py") +print_and_do("pyuic4 reg_submit_dialog.ui > reg_submit_dialog_ui.py") +print_and_do("pyuic4 reg_demo_dialog.ui > reg_demo_dialog_ui.py") +print_and_do("pyuic4 error_report_dialog.ui > error_report_dialog_ui.py") +print_and_do("pyrcc4 dg.qrc > dg_rc.py") \ No newline at end of file diff --git a/base/qt/main_window.py b/base/qt/main_window.py new file mode 100644 index 00000000..3ca20de3 --- /dev/null +++ b/base/qt/main_window.py @@ -0,0 +1,304 @@ +#!/usr/bin/env python +# Unit Name: main_window +# Created By: Virgil Dupras +# Created On: 2009-04-25 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +from PyQt4.QtCore import Qt, QCoreApplication, QProcess, SIGNAL +from PyQt4.QtGui import (QMainWindow, QMenu, QPixmap, QIcon, QToolButton, QLabel, QHeaderView, + QMessageBox, QInputDialog, QLineEdit) + +from hsutil.misc import nonone + +from dupeguru.app import NoScannableFileError, AllFilesAreRefError + +import dg_rc +from main_window_ui import Ui_MainWindow +from results_model import ResultsDelegate, ResultsModel + +class MainWindow(QMainWindow, Ui_MainWindow): + def __init__(self, app): + QMainWindow.__init__(self, None) + self.app = app + self._last_filter = None + self._setupUi() + self.resultsDelegate = ResultsDelegate() + self.resultsModel = ResultsModel(self.app) + self.resultsView.setModel(self.resultsModel) + self.resultsView.setItemDelegate(self.resultsDelegate) + self._load_columns() + self._update_column_actions_status() + self.resultsView.expandAll() + self._update_status_line() + + self.connect(self.app, SIGNAL('resultsChanged()'), self.resultsChanged) + self.connect(self.app, SIGNAL('dupeMarkingChanged()'), self.dupeMarkingChanged) + self.connect(self.actionQuit, SIGNAL('triggered()'), QCoreApplication.instance().quit) + self.connect(self.resultsView.selectionModel(), SIGNAL('selectionChanged(QItemSelection,QItemSelection)'), self.selectionChanged) + self.connect(self.menuColumns, SIGNAL('triggered(QAction*)'), self.columnToggled) + self.connect(QCoreApplication.instance(), SIGNAL('aboutToQuit()'), self.application_will_terminate) + self.connect(self.resultsModel, SIGNAL('modelReset()'), self.resultsReset) + + def _setupUi(self): + self.setupUi(self) + # Stuff that can't be setup in the Designer + h = self.resultsView.header() + h.setHighlightSections(False) + h.setMovable(True) + h.setStretchLastSection(False) + h.setDefaultAlignment(Qt.AlignLeft) + + self.setWindowTitle(QCoreApplication.instance().applicationName()) + self.actionScan.setIcon(QIcon(QPixmap(':/%s' % self.app.LOGO_NAME))) + + # Columns menu + menu = self.menuColumns + self._column_actions = [] + for index, column in enumerate(self.app.data.COLUMNS): + action = menu.addAction(column['display']) + action.setCheckable(True) + action.column_index = index + self._column_actions.append(action) + menu.addSeparator() + action = menu.addAction("Reset to Defaults") + action.column_index = -1 + + # Action menu + actionMenu = QMenu('Actions', self.toolBar) + actionMenu.setIcon(QIcon(QPixmap(":/actions"))) + actionMenu.addAction(self.actionDeleteMarked) + actionMenu.addAction(self.actionMoveMarked) + actionMenu.addAction(self.actionCopyMarked) + actionMenu.addAction(self.actionRemoveMarked) + actionMenu.addSeparator() + actionMenu.addAction(self.actionRemoveSelected) + actionMenu.addAction(self.actionIgnoreSelected) + actionMenu.addAction(self.actionMakeSelectedReference) + actionMenu.addSeparator() + actionMenu.addAction(self.actionOpenSelected) + actionMenu.addAction(self.actionRevealSelected) + actionMenu.addAction(self.actionRenameSelected) + self.actionActions.setMenu(actionMenu) + button = QToolButton(self.toolBar) + button.setDefaultAction(actionMenu.menuAction()) + button.setToolButtonStyle(Qt.ToolButtonTextUnderIcon) + self.actionsButton = button + self.toolBar.insertWidget(self.actionActions, button) # the action is a placeholder + self.toolBar.removeAction(self.actionActions) + + self.statusLabel = QLabel(self) + self.statusbar.addPermanentWidget(self.statusLabel, 1) + + #--- Private + def _confirm(self, title, msg, default_button=QMessageBox.Yes): + buttons = QMessageBox.Yes | QMessageBox.No + answer = QMessageBox.question(self, title, msg, buttons, default_button) + return answer == QMessageBox.Yes + + def _load_columns(self): + h = self.resultsView.header() + h.setResizeMode(QHeaderView.Interactive) + prefs = self.app.prefs + attrs = zip(prefs.columns_width, prefs.columns_visible) + for index, (width, visible) in enumerate(attrs): + h.resizeSection(index, width) + h.setSectionHidden(index, not visible) + h.setResizeMode(0, QHeaderView.Stretch) + + def _redraw_results(self): + # HACK. this is the only way I found to update the widget without reseting everything + self.resultsView.scroll(0, 1) + self.resultsView.scroll(0, -1) + + def _save_columns(self): + h = self.resultsView.header() + widths = [] + visible = [] + for i in range(len(self.app.data.COLUMNS)): + widths.append(h.sectionSize(i)) + visible.append(not h.isSectionHidden(i)) + prefs = self.app.prefs + prefs.columns_width = widths + prefs.columns_visible = visible + prefs.save() + + def _update_column_actions_status(self): + h = self.resultsView.header() + for action in self._column_actions: + colid = action.column_index + action.setChecked(not h.isSectionHidden(colid)) + + def _update_status_line(self): + self.statusLabel.setText(self.app.stat_line) + + #--- Actions + def aboutTriggered(self): + self.app.show_about_box() + + def actionsTriggered(self): + self.actionsButton.showMenu() + + def addToIgnoreListTriggered(self): + dupes = self.resultsView.selectedDupes() + if not dupes: + return + title = "Add to Ignore List" + msg = "All selected {0} matches are going to be ignored in all subsequent scans. Continue?".format(len(dupes)) + if self._confirm(title, msg): + self.app.add_dupes_to_ignore_list(dupes) + + def applyFilterTriggered(self): + title = "Apply Filter" + msg = "Type the filter you want to apply on your results. See help for details." + text = nonone(self._last_filter, '[*]') + answer, ok = QInputDialog.getText(self, title, msg, QLineEdit.Normal, text) + if not ok: + return + answer = unicode(answer) + self.app.ApplyFilter(answer) + self._last_filter = answer + + def cancelFilterTriggered(self): + self.app.ApplyFilter('') + + def checkForUpdateTriggered(self): + QProcess.execute('updater.exe', ['/checknow']) + + def clearIgnoreListTriggered(self): + title = "Clear Ignore List" + count = len(self.app.scanner.ignore_list) + if not count: + QMessageBox.information(self, title, "Nothing to clear.") + return + msg = "Do you really want to remove all {0} items from the ignore list?".format(count) + if self._confirm(title, msg, QMessageBox.No): + self.app.scanner.ignore_list.Clear() + QMessageBox.information(self, title, "Ignore list cleared.") + + def copyTriggered(self): + self.app.copy_or_move_marked(True) + + def deleteTriggered(self): + count = self.app.results.mark_count + if not count: + return + title = "Delete duplicates" + msg = "You are about to send {0} files to the recycle bin. Continue?".format(count) + if self._confirm(title, msg): + self.app.delete_marked() + + def deltaTriggered(self): + self.resultsModel.delta = self.actionDelta.isChecked() + self._redraw_results() + + def detailsTriggered(self): + self.app.show_details() + + def directoriesTriggered(self): + self.app.show_directories() + + def makeReferenceTriggered(self): + self.app.make_reference(self.resultsView.selectedDupes()) + + def markAllTriggered(self): + self.app.mark_all() + + def markInvertTriggered(self): + self.app.mark_invert() + + def markNoneTriggered(self): + self.app.mark_none() + + def markSelectedTriggered(self): + dupes = self.resultsView.selectedDupes() + self.app.toggle_marking_for_dupes(dupes) + + def moveTriggered(self): + self.app.copy_or_move_marked(False) + + def openTriggered(self): + self.app.open_selected() + + def powerMarkerTriggered(self): + self.resultsModel.power_marker = self.actionPowerMarker.isChecked() + + def preferencesTriggered(self): + self.app.show_preferences() + + def registerTrigerred(self): + self.app.ask_for_reg_code() + + def removeMarkedTriggered(self): + count = self.app.results.mark_count + if not count: + return + title = "Remove duplicates" + msg = "You are about to remove {0} files from results. Continue?".format(count) + if self._confirm(title, msg): + self.app.remove_marked_duplicates() + + def removeSelectedTriggered(self): + dupes = self.resultsView.selectedDupes() + if not dupes: + return + title = "Remove duplicates" + msg = "You are about to remove {0} files from results. Continue?".format(len(dupes)) + if self._confirm(title, msg): + self.app.remove_duplicates(dupes) + + def renameTriggered(self): + self.resultsView.edit(self.resultsView.selectionModel().currentIndex()) + + def revealTriggered(self): + self.app.reveal_selected() + + def scanTriggered(self): + title = "Start a new scan" + if len(self.app.results.groups) > 0: + msg = "Are you sure you want to start a new duplicate scan?" + if not self._confirm(title, msg): + return + try: + self.app.start_scanning() + except NoScannableFileError: + msg = "The selected directories contain no scannable file." + QMessageBox.warning(self, title, msg) + self.app.show_directories() + except AllFilesAreRefError: + msg = "You cannot make a duplicate scan with only reference directories." + QMessageBox.warning(self, title, msg) + + def showHelpTriggered(self): + self.app.show_help() + + #--- Events + def application_will_terminate(self): + self._save_columns() + + def columnToggled(self, action): + colid = action.column_index + if colid == -1: + self.app.prefs.reset_columns() + self._load_columns() + else: + h = self.resultsView.header() + h.setSectionHidden(colid, not h.isSectionHidden(colid)) + self._update_column_actions_status() + + def dupeMarkingChanged(self): + self._redraw_results() + self._update_status_line() + + def resultsChanged(self): + self.resultsView.model().reset() + + def resultsReset(self): + self.resultsView.expandAll() + self._update_status_line() + + def selectionChanged(self, selected, deselected): + index = self.resultsView.selectionModel().currentIndex() + dupe = index.internalPointer().dupe if index.isValid() else None + self.app.select_duplicate(dupe) + diff --git a/base/qt/main_window.ui b/base/qt/main_window.ui new file mode 100644 index 00000000..754f265c --- /dev/null +++ b/base/qt/main_window.ui @@ -0,0 +1,911 @@ + + + MainWindow + + + + 0 + 0 + 630 + 514 + + + + dupeGuru + + + + + 0 + + + + + QAbstractItemView::ExtendedSelection + + + QAbstractItemView::SelectRows + + + false + + + true + + + false + + + true + + + false + + + false + + + + + + + + + 0 + 0 + 630 + 22 + + + + + Columns + + + + + Actions + + + + + + + + + + + + + + + + + + + + Mark + + + + + + + + + Modes + + + + + + + Windows + + + + + + + + Help + + + + + + + + + File + + + + + + + + + + + + + + + + + + toolBar + + + false + + + Qt::ToolButtonTextUnderIcon + + + false + + + TopToolBarArea + + + false + + + + + + + + + + + + true + + + + + + :/logo_pe:/logo_pe + + + Start Scan + + + Start scanning for duplicates + + + Ctrl+S + + + + + + :/folder:/folder + + + Directories + + + Ctrl+4 + + + + + + :/details:/details + + + Details + + + Ctrl+3 + + + + + + :/actions:/actions + + + Actions + + + + + + :/preferences:/preferences + + + Preferences + + + Ctrl+5 + + + + + true + + + + :/delta:/delta + + + Delta Values + + + Ctrl+2 + + + + + true + + + + :/power_marker:/power_marker + + + Power Marker + + + Ctrl+1 + + + + + Send Marked to Recycle Bin + + + Ctrl+D + + + + + Move Marked to... + + + Ctrl+M + + + + + Copy Marked to... + + + Ctrl+Shift+M + + + + + Remove Marked from Results + + + Ctrl+R + + + + + Remove Selected from Results + + + Ctrl+Del + + + + + Add Selected to Ignore List + + + Ctrl+Shift+Del + + + + + Make Selected Reference + + + Ctrl+Space + + + + + Open Selected with Default Application + + + Ctrl+O + + + + + Open Containing Folder of Selected + + + Ctrl+Shift+O + + + + + Rename Selected + + + F2 + + + + + Mark All + + + Ctrl+A + + + + + Mark None + + + Ctrl+Shift+A + + + + + Invert Marking + + + Ctrl+Alt+A + + + + + Mark Selected + + + + + Clear Ignore List + + + + + Quit + + + Ctrl+Q + + + + + Apply Filter + + + Ctrl+F + + + + + Cancel Filter + + + Ctrl+Shift+F + + + + + dupeGuru Help + + + F1 + + + + + About dupeGuru + + + + + Register dupeGuru + + + + + Check for Update + + + + + + ResultsView + QTreeView +
results_model
+
+
+ + + + + + actionDirectories + triggered() + MainWindow + directoriesTriggered() + + + -1 + -1 + + + 314 + 256 + + + + + actionActions + triggered() + MainWindow + actionsTriggered() + + + -1 + -1 + + + 314 + 256 + + + + + actionCopyMarked + triggered() + MainWindow + copyTriggered() + + + -1 + -1 + + + 314 + 256 + + + + + actionDeleteMarked + triggered() + MainWindow + deleteTriggered() + + + -1 + -1 + + + 314 + 256 + + + + + actionDelta + triggered() + MainWindow + deltaTriggered() + + + -1 + -1 + + + 314 + 256 + + + + + actionDetails + triggered() + MainWindow + detailsTriggered() + + + -1 + -1 + + + 314 + 256 + + + + + actionIgnoreSelected + triggered() + MainWindow + addToIgnoreListTriggered() + + + -1 + -1 + + + 314 + 256 + + + + + actionMakeSelectedReference + triggered() + MainWindow + makeReferenceTriggered() + + + -1 + -1 + + + 314 + 256 + + + + + actionMoveMarked + triggered() + MainWindow + moveTriggered() + + + -1 + -1 + + + 314 + 256 + + + + + actionOpenSelected + triggered() + MainWindow + openTriggered() + + + -1 + -1 + + + 314 + 256 + + + + + actionPowerMarker + triggered() + MainWindow + powerMarkerTriggered() + + + -1 + -1 + + + 314 + 256 + + + + + actionPreferences + triggered() + MainWindow + preferencesTriggered() + + + -1 + -1 + + + 314 + 256 + + + + + actionRemoveMarked + triggered() + MainWindow + removeMarkedTriggered() + + + -1 + -1 + + + 314 + 256 + + + + + actionRemoveSelected + triggered() + MainWindow + removeSelectedTriggered() + + + -1 + -1 + + + 314 + 256 + + + + + actionRevealSelected + triggered() + MainWindow + revealTriggered() + + + -1 + -1 + + + 314 + 256 + + + + + actionRenameSelected + triggered() + MainWindow + renameTriggered() + + + -1 + -1 + + + 314 + 256 + + + + + actionScan + triggered() + MainWindow + scanTriggered() + + + -1 + -1 + + + 314 + 256 + + + + + actionClearIgnoreList + triggered() + MainWindow + clearIgnoreListTriggered() + + + -1 + -1 + + + 314 + 256 + + + + + actionMarkAll + triggered() + MainWindow + markAllTriggered() + + + -1 + -1 + + + 314 + 256 + + + + + actionMarkNone + triggered() + MainWindow + markNoneTriggered() + + + -1 + -1 + + + 314 + 256 + + + + + actionMarkSelected + triggered() + MainWindow + markSelectedTriggered() + + + -1 + -1 + + + 314 + 256 + + + + + actionInvertMarking + triggered() + MainWindow + markInvertTriggered() + + + -1 + -1 + + + 314 + 256 + + + + + actionApplyFilter + triggered() + MainWindow + applyFilterTriggered() + + + -1 + -1 + + + 314 + 256 + + + + + actionCancelFilter + triggered() + MainWindow + cancelFilterTriggered() + + + -1 + -1 + + + 314 + 256 + + + + + actionShowHelp + triggered() + MainWindow + showHelpTriggered() + + + -1 + -1 + + + 314 + 256 + + + + + actionAbout + triggered() + MainWindow + aboutTriggered() + + + -1 + -1 + + + 314 + 256 + + + + + actionRegister + triggered() + MainWindow + registerTrigerred() + + + -1 + -1 + + + 314 + 256 + + + + + actionCheckForUpdate + triggered() + MainWindow + checkForUpdateTriggered() + + + -1 + -1 + + + 314 + 256 + + + + + + directoriesTriggered() + scanTriggered() + actionsTriggered() + detailsTriggered() + preferencesTriggered() + deltaTriggered() + powerMarkerTriggered() + deleteTriggered() + moveTriggered() + copyTriggered() + removeMarkedTriggered() + removeSelectedTriggered() + addToIgnoreListTriggered() + makeReferenceTriggered() + openTriggered() + revealTriggered() + renameTriggered() + clearIgnoreListTriggered() + clearPictureCacheTriggered() + markAllTriggered() + markNoneTriggered() + markInvertTriggered() + markSelectedTriggered() + applyFilterTriggered() + cancelFilterTriggered() + showHelpTriggered() + aboutTriggered() + registerTrigerred() + checkForUpdateTriggered() + +
diff --git a/base/qt/preferences.py b/base/qt/preferences.py new file mode 100644 index 00000000..64ad64d5 --- /dev/null +++ b/base/qt/preferences.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python +# Unit Name: preferences +# Created By: Virgil Dupras +# Created On: 2009-05-03 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +from PyQt4.QtCore import QSettings, QVariant + +from hsutil.misc import tryint + +def variant_to_py(v): + value = None + ok = False + t = v.type() + if t == QVariant.String: + value = unicode(v.toString()) + ok = True # anyway + # might be bool or int, try them + if v == 'true': + value = True + elif value == 'false': + value = False + else: + value = tryint(value, value) + elif t == QVariant.Int: + value, ok = v.toInt() + elif t == QVariant.Bool: + value, ok = v.toBool(), True + elif t in (QVariant.List, QVariant.StringList): + value, ok = map(variant_to_py, v.toList()), True + if not ok: + raise TypeError() + return value + +class Preferences(object): + # (width, is_visible) + COLUMNS_DEFAULT_ATTRS = [] + + def __init__(self): + self.reset() + self.reset_columns() + + def _load_specific(self, settings, get): + # load prefs specific to the dg edition + pass + + def load(self): + self.reset() + settings = QSettings() + def get(name, default): + if settings.contains(name): + return variant_to_py(settings.value(name)) + else: + return default + + self.filter_hardness = get('FilterHardness', self.filter_hardness) + self.mix_file_kind = get('MixFileKind', self.mix_file_kind) + self.use_regexp = get('UseRegexp', self.use_regexp) + self.remove_empty_folders = get('RemoveEmptyFolders', self.remove_empty_folders) + self.destination_type = get('DestinationType', self.destination_type) + widths = get('ColumnsWidth', self.columns_width) + # only set nonzero values + for index, width in enumerate(widths[:len(self.columns_width)]): + if width > 0: + self.columns_width[index] = width + self.columns_visible = get('ColumnsVisible', self.columns_visible) + self.registration_code = get('RegistrationCode', self.registration_code) + self.registration_email = get('RegistrationEmail', self.registration_email) + self._load_specific(settings, get) + + def _reset_specific(self): + # reset prefs specific to the dg edition + pass + + def reset(self): + self.filter_hardness = 95 + self.mix_file_kind = True + self.use_regexp = False + self.remove_empty_folders = False + self.destination_type = 1 + self.registration_code = '' + self.registration_email = '' + self._reset_specific() + + def reset_columns(self): + self.columns_width = [width for width, _ in self.COLUMNS_DEFAULT_ATTRS] + self.columns_visible = [visible for _, visible in self.COLUMNS_DEFAULT_ATTRS] + + def _save_specific(self, settings, set_): + # save prefs specific to the dg edition + pass + + def save(self): + settings = QSettings() + def set_(name, value): + settings.setValue(name, QVariant(value)) + + set_('FilterHardness', self.filter_hardness) + set_('MixFileKind', self.mix_file_kind) + set_('UseRegexp', self.use_regexp) + set_('RemoveEmptyFolders', self.remove_empty_folders) + set_('DestinationType', self.destination_type) + set_('ColumnsWidth', self.columns_width) + set_('ColumnsVisible', self.columns_visible) + set_('RegistrationCode', self.registration_code) + set_('RegistrationEmail', self.registration_email) + self._save_specific(settings, set_) + diff --git a/base/qt/reg.py b/base/qt/reg.py new file mode 100644 index 00000000..59fd0bc3 --- /dev/null +++ b/base/qt/reg.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python +# Unit Name: reg +# Created By: Virgil Dupras +# Created On: 2009-05-09 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +from hashlib import md5 + +from PyQt4.QtGui import QDialog + +from reg_submit_dialog import RegSubmitDialog +from reg_demo_dialog import RegDemoDialog + +class Registration(object): + def __init__(self, app): + self.app = app + + def ask_for_code(self): + dialog = RegSubmitDialog(self.app.main_window, self.app.is_code_valid) + result = dialog.exec_() + code = unicode(dialog.codeEdit.text()) + email = unicode(dialog.emailEdit.text()) + dialog.setParent(None) # free it + if result == QDialog.Accepted and self.app.is_code_valid(code, email): + self.app.set_registration(code, email) + return True + return False + + def show_nag(self): + dialog = RegDemoDialog(self.app.main_window, self) + dialog.exec_() + dialog.setParent(None) # free it + diff --git a/base/qt/reg_demo_dialog.py b/base/qt/reg_demo_dialog.py new file mode 100644 index 00000000..95280314 --- /dev/null +++ b/base/qt/reg_demo_dialog.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python +# Unit Name: reg_demo_dialog +# Created By: Virgil Dupras +# Created On: 2009-05-10 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +from PyQt4.QtCore import SIGNAL, Qt, QUrl, QCoreApplication +from PyQt4.QtGui import QDialog, QMessageBox, QDesktopServices + +from reg_demo_dialog_ui import Ui_RegDemoDialog + +class RegDemoDialog(QDialog, Ui_RegDemoDialog): + def __init__(self, parent, reg): + flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint + QDialog.__init__(self, parent, flags) + self.reg = reg + self._setupUi() + + self.connect(self.enterCodeButton, SIGNAL('clicked()'), self.enterCodeClicked) + self.connect(self.purchaseButton, SIGNAL('clicked()'), self.purchaseClicked) + + def _setupUi(self): + self.setupUi(self) + # Stuff that can't be setup in the Designer + appname = QCoreApplication.instance().applicationName() + title = self.windowTitle() + title = title.replace('$appname', appname) + self.setWindowTitle(title) + title = self.titleLabel.text() + title = title.replace('$appname', appname) + self.titleLabel.setText(title) + desc = self.demoDescLabel.text() + desc = desc.replace('$appname', appname) + self.demoDescLabel.setText(desc) + + #--- Events + def enterCodeClicked(self): + if self.reg.ask_for_code(): + self.accept() + + def purchaseClicked(self): + url = QUrl('http://www.hardcoded.net/purchase.htm') + QDesktopServices.openUrl(url) + diff --git a/base/qt/reg_demo_dialog.ui b/base/qt/reg_demo_dialog.ui new file mode 100644 index 00000000..ef918225 --- /dev/null +++ b/base/qt/reg_demo_dialog.ui @@ -0,0 +1,140 @@ + + + RegDemoDialog + + + + 0 + 0 + 387 + 161 + + + + $appname Demo Version + + + + + + + 75 + true + + + + $appname Demo Version + + + + + + + You are currently running a demo version of $appname. This version has limited functionalities, and you need to buy it to have access to these functionalities. + + + true + + + + + + + In the demo version, only 10 duplicates per session can be sent to the recycle bin, moved or copied. + + + true + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 110 + 0 + + + + Try Demo + + + + + + + + 110 + 0 + + + + Enter Code + + + + + + + + 110 + 0 + + + + Purchase + + + + + + + + + + + tryButton + clicked() + RegDemoDialog + accept() + + + 112 + 161 + + + 201 + 94 + + + + + diff --git a/base/qt/reg_submit_dialog.py b/base/qt/reg_submit_dialog.py new file mode 100644 index 00000000..4ba680b6 --- /dev/null +++ b/base/qt/reg_submit_dialog.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python +# Unit Name: reg_submit_dialog +# Created By: Virgil Dupras +# Created On: 2009-05-09 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +from PyQt4.QtCore import SIGNAL, Qt, QUrl, QCoreApplication +from PyQt4.QtGui import QDialog, QMessageBox, QDesktopServices + +from reg_submit_dialog_ui import Ui_RegSubmitDialog + +class RegSubmitDialog(QDialog, Ui_RegSubmitDialog): + def __init__(self, parent, is_valid_func): + flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint + QDialog.__init__(self, parent, flags) + self._setupUi() + self.is_valid_func = is_valid_func + + self.connect(self.submitButton, SIGNAL('clicked()'), self.submitClicked) + self.connect(self.purchaseButton, SIGNAL('clicked()'), self.purchaseClicked) + + def _setupUi(self): + self.setupUi(self) + # Stuff that can't be setup in the Designer + appname = QCoreApplication.instance().applicationName() + prompt = self.promptLabel.text() + prompt = prompt.replace('$appname', appname) + self.promptLabel.setText(prompt) + + #--- Events + def purchaseClicked(self): + url = QUrl('http://www.hardcoded.net/purchase.htm') + QDesktopServices.openUrl(url) + + def submitClicked(self): + code = unicode(self.codeEdit.text()) + email = unicode(self.emailEdit.text()) + title = "Registration" + if self.is_valid_func(code, email): + msg = "This code is valid. Thanks!" + QMessageBox.information(self, title, msg) + self.accept() + else: + msg = "This code is invalid" + QMessageBox.warning(self, title, msg) + diff --git a/base/qt/reg_submit_dialog.ui b/base/qt/reg_submit_dialog.ui new file mode 100644 index 00000000..06de4191 --- /dev/null +++ b/base/qt/reg_submit_dialog.ui @@ -0,0 +1,149 @@ + + + RegSubmitDialog + + + + 0 + 0 + 365 + 134 + + + + Enter your registration code + + + + + + Please enter your $appname registration code and registered e-mail (the e-mail you used for the purchase), then press "Submit". + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + + + + + QLayout::SetNoConstraint + + + QFormLayout::ExpandingFieldsGrow + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + + + Registration code: + + + + + + + Registered e-mail: + + + + + + + + + + + + + + + + + Purchase + + + false + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + Cancel + + + false + + + + + + + + 0 + 0 + + + + Submit + + + false + + + true + + + + + + + + + + + cancelButton + clicked() + RegSubmitDialog + reject() + + + 260 + 159 + + + 198 + 97 + + + + + diff --git a/base/qt/results_model.py b/base/qt/results_model.py new file mode 100644 index 00000000..d28d6da3 --- /dev/null +++ b/base/qt/results_model.py @@ -0,0 +1,175 @@ +#!/usr/bin/env python +# Unit Name: +# Created By: Virgil Dupras +# Created On: 2009-04-23 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +from PyQt4.QtCore import SIGNAL, Qt, QAbstractItemModel, QVariant, QModelIndex, QRect +from PyQt4.QtGui import QBrush, QStyledItemDelegate, QFont, QTreeView, QColor + +from tree_model import TreeNode, TreeModel + +class ResultNode(TreeNode): + def __init__(self, model, parent, row, dupe, group): + TreeNode.__init__(self, parent, row) + self.model = model + self.dupe = dupe + self.group = group + self._normalData = None + self._deltaData = None + + def _get_children(self): + children = [] + if self.dupe is self.group.ref: + for index, dupe in enumerate(self.group.dupes): + children.append(ResultNode(self.model, self, index, dupe, self.group)) + return children + + def reset(self): + self._normalData = None + self._deltaData = None + + @property + def normalData(self): + if self._normalData is None: + self._normalData = self.model._data.GetDisplayInfo(self.dupe, self.group, delta=False) + return self._normalData + + @property + def deltaData(self): + if self._deltaData is None: + self._deltaData = self.model._data.GetDisplayInfo(self.dupe, self.group, delta=True) + return self._deltaData + + +class ResultsDelegate(QStyledItemDelegate): + def initStyleOption(self, option, index): + QStyledItemDelegate.initStyleOption(self, option, index) + node = index.internalPointer() + if node.group.ref is node.dupe: + newfont = QFont(option.font) + newfont.setBold(True) + option.font = newfont + + +class ResultsModel(TreeModel): + def __init__(self, app): + self._app = app + self._results = app.results + self._data = app.data + self._delta_columns = app.DELTA_COLUMNS + self.delta = False + self._power_marker = False + TreeModel.__init__(self) + + def _root_nodes(self): + nodes = [] + if self.power_marker: + for index, dupe in enumerate(self._results.dupes): + group = self._results.get_group_of_duplicate(dupe) + nodes.append(ResultNode(self, None, index, dupe, group)) + else: + for index, group in enumerate(self._results.groups): + nodes.append(ResultNode(self, None, index, group.ref, group)) + return nodes + + def columnCount(self, parent): + return len(self._data.COLUMNS) + + def data(self, index, role): + if not index.isValid(): + return QVariant() + node = index.internalPointer() + if role == Qt.DisplayRole: + data = node.deltaData if self.delta else node.normalData + return QVariant(data[index.column()]) + elif role == Qt.CheckStateRole: + if index.column() == 0 and node.dupe is not node.group.ref: + state = Qt.Checked if self._results.is_marked(node.dupe) else Qt.Unchecked + return QVariant(state) + elif role == Qt.ForegroundRole: + if node.dupe is node.group.ref or node.dupe.is_ref: + return QVariant(QBrush(Qt.blue)) + elif self.delta and index.column() in self._delta_columns: + return QVariant(QBrush(QColor(255, 142, 40))) # orange + elif role == Qt.EditRole: + if index.column() == 0: + return QVariant(node.normalData[index.column()]) + return QVariant() + + def dupesForIndexes(self, indexes): + nodes = [index.internalPointer() for index in indexes] + return [node.dupe for node in nodes] + + def flags(self, index): + if not index.isValid(): + return Qt.ItemIsEnabled + flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable + if index.column() == 0: + flags |= Qt.ItemIsUserCheckable | Qt.ItemIsEditable + return flags + + def headerData(self, section, orientation, role): + if orientation == Qt.Horizontal and role == Qt.DisplayRole and section < len(self._data.COLUMNS): + return QVariant(self._data.COLUMNS[section]['display']) + + return QVariant() + + def setData(self, index, value, role): + if not index.isValid(): + return False + node = index.internalPointer() + if role == Qt.CheckStateRole: + if index.column() == 0: + self._app.toggle_marking_for_dupes([node.dupe]) + return True + if role == Qt.EditRole: + if index.column() == 0: + value = unicode(value.toString()) + if self._app.rename_dupe(node.dupe, value): + node.reset() + return True + return False + + def sort(self, column, order): + if self.power_marker: + self._results.sort_dupes(column, order == Qt.AscendingOrder, self.delta) + else: + self._results.sort_groups(column, order == Qt.AscendingOrder) + self.reset() + + def toggleMarked(self, indexes): + assert indexes + dupes = self.dupesForIndexes(indexes) + self._app.toggle_marking_for_dupes(dupes) + + #--- Properties + @property + def power_marker(self): + return self._power_marker + + @power_marker.setter + def power_marker(self, value): + if value == self._power_marker: + return + self._power_marker = value + self.reset() + + +class ResultsView(QTreeView): + #--- Override + def keyPressEvent(self, event): + if event.text() == ' ': + self.model().toggleMarked(self.selectionModel().selectedRows()) + return + QTreeView.keyPressEvent(self, event) + + def setModel(self, model): + assert isinstance(model, ResultsModel) + QTreeView.setModel(self, model) + + #--- Public + def selectedDupes(self): + return self.model().dupesForIndexes(self.selectionModel().selectedRows()) + diff --git a/base/qt/tree_model.py b/base/qt/tree_model.py new file mode 100644 index 00000000..b3a994b3 --- /dev/null +++ b/base/qt/tree_model.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python +# Unit Name: tree_model +# Created By: Virgil Dupras +# Created On: 2009-05-04 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +from PyQt4.QtCore import Qt, QAbstractItemModel, QVariant, QModelIndex + +class TreeNode(object): + def __init__(self, parent, row): + self.parent = parent + self.row = row + self._children = None + + def _get_children(self): + raise NotImplementedError() + + @property + def children(self): + if self._children is None: + self._children = self._get_children() + return self._children + + +class TreeModel(QAbstractItemModel): + def __init__(self): + QAbstractItemModel.__init__(self) + self._nodes = None + + def _root_nodes(self): + raise NotImplementedError() + + def index(self, row, column, parent): + if not self.nodes: + return QModelIndex() + if not parent.isValid(): + return self.createIndex(row, column, self.nodes[row]) + node = parent.internalPointer() + return self.createIndex(row, column, node.children[row]) + + def parent(self, index): + if not index.isValid(): + return QModelIndex() + node = index.internalPointer() + if node.parent is None: + return QModelIndex() + else: + return self.createIndex(node.parent.row, 0, node.parent) + + def reset(self): + self._nodes = None + QAbstractItemModel.reset(self) + + def rowCount(self, parent): + if not parent.isValid(): + return len(self.nodes) + node = parent.internalPointer() + return len(node.children) + + @property + def nodes(self): + if self._nodes is None: + self._nodes = self._root_nodes() + return self._nodes + diff --git a/images/actions32.png b/images/actions32.png new file mode 100755 index 0000000000000000000000000000000000000000..16d64fc1e439ab6ae90a1e11cc99831813b2416f GIT binary patch literal 3039 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 literal 0 HcmV?d00001 diff --git a/images/delta32.png b/images/delta32.png new file mode 100755 index 0000000000000000000000000000000000000000..f7d95da07919c34aa960d37448642e229e75f1e6 GIT binary patch literal 1941 zcmV;G2Wt3RCwCt zS8Gs|*BO4k%iebZcVR&!3IRdrf+*408YCDms3l`#Qytr+HZj&|%@Yh+mbXj@U6o(toJChVaZb-Lv_IXNj18GV4^IK0!*0jJZM_Wb%P^Xe6)u{Duc z66EDF}KQKfSUMGn@lO; zwOUzKU2W97_}#z81i>1WB*bJ1;yt;ewb8n(K{X}{I z^AyMolN5s?n8@_Gu^4j5{6?Q2Mz0tCYu6xs*$dg{d=(=3I=$>kdu_k%|@y8f>UXBhj%kD+@?;YuETVyevoDhY_WNL#`0Za6G!WILW`cvw7iF9pl!0(hmn2>ln|<5RJ>b?5;1JPgllEk`$85O2DqE!mFQN z!a!yUDCACl)J1U)xTn2vhe8ODC8CrKCrG^H{Xr5f-H$V>Zhxmy^XibphjZ6cCJriY zV_HxgwvR16R4-ilIY*jx8Vbn>&=1y)reziJV7 zWb^@EN&WO4l#;kZH}%kg0s2moBgPc?^xs9i%3iB8{ABz3oP86MZhSU~^h?z4oZIKR;IGDok1U7SN7!T z@hK0bV$IAPxw1nL{48mz(P&;@$?5yc*68091c&Ox0EX#?pFNC;}=J8tI38Z z6J*wkV&It?w0+czvyTo#a^-j;0YQm&dc*6H^Z$Q;be4bN*# zcNd);KwvtABkAJ1dkBX{rx4?3eW9TOIkv^v{YVPTl}oYZ&wqp%i_eoM2yyujTekel zH#LPW0`QlHytc^+4^CZ9mD-VX00`hM%8}<;2D@E_J#|Lnc6m(|;;OZDo_B&wb`579NW`!c+$I9=l z+j70$fI^GGyloY;v+sHky$*EJbUYgXimAJL@oqBtRfKG!W<{^Y(m4l7so=3$SGqJ;|)3(~3=Ka%lfbnWbcSI%|0K~V^;+EvLguF8i}K)1V?RNAx|xp&;4Y&h4=DG{!O<-q8_ z+Z;DX|Ctd$-$cdlMhtsCAbOKXV=Z38LuFJU8jU_Y;j6wf8fRdc>b1H=JQm`a|NGbR be+3u-4f`}H8YPeZ00000NkvXXu0mjf0Q9d* literal 0 HcmV?d00001 diff --git a/images/details32.png b/images/details32.png new file mode 100644 index 0000000000000000000000000000000000000000..8ab245b41ba6f367f19d01227df99b85872bac81 GIT binary patch literal 5090 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 YiNbjNhnHL=%16}s>3mJzOVs%P0VM`{F#rGn literal 0 HcmV?d00001 diff --git a/images/dgme_logo_128.png b/images/dgme_logo_128.png new file mode 100644 index 0000000000000000000000000000000000000000..187c21c8cfb4e7541a63ce36689f8cb444d17b68 GIT binary patch literal 18092 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 literal 0 HcmV?d00001 diff --git a/images/dgme_logo_32.png b/images/dgme_logo_32.png new file mode 100644 index 0000000000000000000000000000000000000000..88488462f4a694cc1156f7894a0ebf78aa235d88 GIT binary patch literal 6079 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 literal 0 HcmV?d00001 diff --git a/images/dgpe_logo_32.png b/images/dgpe_logo_32.png new file mode 100755 index 0000000000000000000000000000000000000000..5384a81c130023c54c59af9e3a10893998f0ad57 GIT binary patch literal 5818 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 NuboDV(okz!|9`pBT*UwY literal 0 HcmV?d00001 diff --git a/images/dgse_logo_128.png b/images/dgse_logo_128.png new file mode 100644 index 0000000000000000000000000000000000000000..f50221b216d9dcae2626bb246dab97898b7b4e75 GIT binary patch literal 14474 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 literal 0 HcmV?d00001 diff --git a/images/dgse_logo_32.png b/images/dgse_logo_32.png new file mode 100755 index 0000000000000000000000000000000000000000..29d53d2d12fdd9932032c2e60195edd76d7b5f63 GIT binary patch literal 1828 zcmV+<2iy3GP)c$e2xSyP&*6Mde7Eb{o!Olk-rYNg1oB}g zMT|VR>zUo1XP$ZHeV-x582B;^kA9?(Ffy_qX2FCSKxqwxfEBe+LIR7DxKCk&0mJdX z=+7`h!l-3FMJWZvVlOQ2UyNVCL606e3b);I3kWGe1BI)4$j#roy8G$IB+E8^1#eGH zzqj8D?nNN=0@06jbNc;;2&fl^;WXth7|VVR=E0=nIIwZkCV1r0EiDG3!Z-(xv-WX5 zYgTlw|7O2+!=cI))Tb@T6R7}s=hyZy4Ak_)b)o-9T50K;5mC=kX zMm0TS2A5mT2_VupLvHLeKu6v{X-NnJ-S$_d(!}P>OECHc1Bg1XhuZ+j4gw4EKCCJy zR)$JvDPvO|mId_x4FK6iQK6JobTvW5LJT5FvHGy)8hdGHnp_<)dZ;IrJS`r44c{U` zi#c*>0HRpXmDWqT(q^F8MV77TO|6!G5UY#e6NUz`yrjwMa%%k)5&vA8?|=-Dwu*JX z(duhP<24MIoln>o1Q6gHmiL6WZQIa0vg59S{Wo;Tb@kc|gcA380Ie7!8}O60U3VTA z?vcgitY5NHK#;Cv(=U}me@~C}J~Bc)tT8g0`2vW-?B7T2S6=+v$zR!lvb{c2pDLAKGSaT)oyXK0-x`Vv=+Sjk`g)D|1jZ9IcS)K7WOeQnPeeYo={NI|GyEy`g zmM)3Jy|K@GYRj(bkj(ahtlMyETw{f}HmU^b$0uR)w+nZ_Q=J~otxS)u9wR2^}Al>%$@y4DvaE_?%$U7~lV0 z@rPZf3U~JveG{d0!*Bjo)m)ka7GMoAD&(oaTZDZ!di3plZIF>dKu=Tl~}X6oW8r z5b~T%Tr_AzAVxhoyrVK{b0F96^W>d37jm0_ykOyzN8cKQS|NbG<;Cu`%errU{oTpo z6}ik7A9d2^;q-Da$Adx#3{GO2!M=^yEHBEyY{66vqf$L_K!iV7qSZa?dO9DU6hu3s z3&!5#@b>E`VYz4Dq&XX#qI9oqTX3dBiakKm#)wKG7Y6rguesF_!eKsPljBDi!ge^{ z{rpE0*Ge$<50@6VmBZBPYSlrFH=vlZ*G|kdU+7QpHE1H*$-axb}dO9nvNX*>cTFv$6_N!5{?%Z7yh*HC(Sj%Y3jA`>Gc zE8k`z@{HJ=z?6I>f!OW7ED$Rutv?^A3`FID>1hT0^0c2NwYZ4BHs?#XG_n`@GpO0^qtXNGVUBSS~tytnbV?zi+1PRSq7W zTs*AQ9QR#ehljCCj08QrYa-F$Gcz-=`^{arhU%#l17_%h2U0t^5%t1P6U Sm55~k00003552EP)Nkl$NWTs#~O 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 VY%2RVTYUfk002ovPDHLkV1oUj+W`Oo literal 0 HcmV?d00001 diff --git a/images/gear.png b/images/gear.png new file mode 100755 index 0000000000000000000000000000000000000000..41ff2dd6d78b9827ab401185d625f35184687699 GIT binary patch literal 394 zcmeAS@N?(olHy`uVBq!ia0vp^JRr=$1|-8uW1a&kwj^(N7l!{JxM1({$v_d#0*}aI z1_o|n5N2eUHAey{$X?><>&kwWhn-hLtURzvhk=1n+|$J|MB{w!#e-f~3I*6doPYCA zR5WthRol+0h;3G_;z!LtNbZuns_b!IL1bS;(uI}T0&liP-(8ZtU_&XVbw`}jtz0XM zq!#AGH9u?X57(scyW_h1l~P=1MeL7wztvg%{u?`4bxy}BwrGp#p)m@*T*{pZ$N2B8h+e&7oveb317H3V|%s+_Ln#?%p zU9;-$wvF+^YCmo!?As)JX4;fI(~`WMlkDyvOYc$2;i%;)=nLJWP$hV7LpwjO-bahe z28VRy9xl+cQ2d`#)xlnIf0cGlqGfRNdew!Y70YaH>?{2+eRlQvhZ65UJUPa8zOYGK m=FuVBjZOViwf|49XOukW9JuC4h#N4}7(8A5T-G@yGywqT51()V literal 0 HcmV?d00001 diff --git a/images/power_marker32.png b/images/power_marker32.png new file mode 100755 index 0000000000000000000000000000000000000000..48129026bc3e9d827169bb5e8d4c7160690c5444 GIT binary patch literal 2132 zcmV-a2&?yrP)Oc*tiKUB_?Sbf!w6DHHj1|DpjEhRip|PB~n$T zl~P4XTA`*&(@;u7fCi_$48)jJEx0(?u`%`rWBghcjJ#}q)?ecHg|Xa z%zSs|+;hKkzVA|oVc@Hb-riocw6uWddDPU@U}R(jBg3OuS-B!>-D3?xR%Vv^|K_s` z-86Iqkw5@KAV6DPj)xeA;b0=AZa<%o(}pX3GPitrFp)^?TD*9170a^VI1ZjXFG7(Z zP7LhIn+{Dqwe2S__`Wm%O_tF6?rvP#yJyK0+kf1xXtYg^MhuFgAYSw1`oCJ}(Z`>D zX6K&w4_G;C&F0OU>uyaXe3Qtk-Y?XA=h@i37+eqlMSccFBh#B(Js+935A_d2 z4u>gCmN85KTnLIcx7Dxz$Rwn1piAU48DQQC)gs`0_-&kg?*fXnwL_aXH~CWnkffPh z?kxA$LxFKj`+U&iafCDt-SIGVuXo?7va&fd?nbO1ANX3ZzIZ$O!Uxg%eh)URcoCPG zep*)Kd*%>*x6jioO{HaK)Wn213`K!K)0i`K%S@aGT5PuRSU3WMtXw8HU95o&8}|=yBy9AZ`)8#YXScfS zD=eGPdq~E*2ezW9qyVva)O-zpKL8fq0-^#V5v$O3q>0g3QUDCuaF3NWa$W9HZ+x_; zYxwH(j}a9Pja)hV$-WDMu3C-WL=R54oq`|;lshebfVcChGr-7+I$0(wk46a|11wR1 zg|btONYyom40&nonvcxb^78Um`h0y&IN_T0H2re)jyJUtt*Q^Fi<3{Ci?F)J##Na&q*XCGZwTr8NeRn^r8JI6*=eyP zw?>!~-1Teg-I64kCIHbgi3@5w&L0~Zf8wWQH&S9i=ekh(J_;u@cVKe#;msTps&WWisM$>t#sT185z46Wxcyc(`EW#FHDoJ(W@pTUXi*eXQ$g+4=bO2NacFeUUt!Nouh$q0W}*><4GsB><&cABNTM3aWX-25FslnV zG4?8MwB}&Tw_buPlgIs-&|Mkucsxc%niC??zbh-fIx-hUM4Si1pudo_b*j*LD!BoG zD2vWQX&wAWa!~)!_n{ghg25mZNlv3u^#M}_8u>VJL&o7ws|fgKkj`kxrY$G4t;(wd zig=&__|%M@3{L#vjW_m}Ivp;JPX}86OS<#oq755xT>nEj@H&zpPlE8ij^xdPhtlyUq#j;OVQBipu4i7N=OX= zaYKj8Q4T_aP8gwKvCJkCOS>X?y|I`R_1kgFmoMK#jOP)R)C+}NKYDH}24~J;SMx7$ z`0(Lc0-!5}Vnp27^x+IwRU$Hvc_D%u2^}4A8Fq?0=AfxtNk~`o6!$a%p*AJw;1!l(Cp;m`3_aTxH5Yjb7b$z&;<-3eq3QVTq&}6qEUl>QJ zwH{$-A3p4Q9W(9uSX8GK8AyJF?Ra61J8pk@{##~1Mwluy7E6)-oX;jMP*qj+r^~r?DZ*0iRpsY@=C_Xti z;br-bI~q4qAt$GA+8j~|rrQ@|P1R~#8S*88MI%u=P1C6XFbU4h$eLX3bP*9ykI1UT zhz3sQvs~2lh%XLBm0#HL^Iv7=Wak>0ya(MqJ@EDSQyVul3TCUGx(^T)kVXtH+VBWs z92~_Ragi0txbqxQQ(Nn4YHISBrTEb`Ymk|l3Et{J_7Zrzx~x)>XPE(U+(?e-iOUQs@W4TEc)k8;HB5Lq3!g?$G;)~3LNAICnWuB zKEs8y1!AsTxe_ZbF2&5u3@C%agtZ-iIr19(Z8qO;b~jy0-3OA5DFs1Oy zk`Pq%hfcz0*^NVO?;v+-J$}%*1v;%!GO^|q88DKqxUNW9gOW73L}y3UWcnG<$?3?D zm1M0jbD%KoYjh6F=7`_#FA>55Z?oD+yqsjkWXGB26v?T1^j-k|E5HDx{l9{o3w;Ct0000< KMNUMnLSTa2=m_or literal 0 HcmV?d00001 diff --git a/images/preferences32.png b/images/preferences32.png new file mode 100755 index 0000000000000000000000000000000000000000..bdbc4051bed43e22ea6057ac4844866396219458 GIT binary patch literal 2899 zcmWlbdpy(oAIHDD4_~Yl8$hqQgSWp0I3ayzVZ>Qtvr z*5#Db#Wtf0GC!#lHJeLH<<4Z=_qYD|eBO`GAD_?X@p^xrulMJj9~u&1Y-ni+0Dy5& zpnsS?Qhp9RRzDxj%9_##bdqndF96g%r(+5}>(`|8jp6A#QWMjU#~eujeB)DN6DUC` zF-Zww2{G}RhhHRk1AyURkiYNF-`>Y+V3;9eA|W6a9UDg(La25yHNS5jI)b?ENH7A% z4^Vsr!T(dra!CLZ@1I1Yxju=HP4q#=Z^NkUC~Hx%F*N5yj4GDmgKnyW)J6mbN_C9+ zHUCoig$oD@#Iz;6Y?rz!+NJr^&*TcQZm;+Jr0zq{Y^@Lbw~78|#Rr&kTSBv~^J-D! zF66Qs$niy#44Ex#mS=%Fyow-(43KDo5kah-omCt^dLY0&nKaF)i(apt|Ne62_hK8x z^#;yt#Nc3I(q4Iu&kx;n>%~0^G3UgXw&y`4tkXUMyN;vsM)^~y+YW?OMC_V!sh^)8 z$UFwx_+&!{w^4@;2}gm$s|kF2y}kj6uHU0w>gd>-l-%E>nUzOBrKj%Ic^%XAiQTT6 zxZ?2zce=gisMVgCfuH{;^V&})R~Mb$+Qwi!J~8?EThH56b~enOme-0cK@gUfSrLg; zJ3BjSUST2MdaO^15`2Dj)y+F|0zFi+kK$v{GzqD#m;jeNW_fLFq)x#ho!VGmQAGA= z?CROBh1*`pB*SxZ;gthZjODJy0W(NV-BYtW>s(;+~lZ7-EhaTQ|i$Q&ok#eAad;tuzMy_~hn^ zj6(w}Z|#E-_b$#U#NSwTS1w9wg!AI%=Hl8mAuU5SfF1)AwejZg z;w2vwyKf(}mdR@w#I*;zn?%P(%tfg0y$$Wulvtxz{Qm=GEk77Hwo60}});k$AVIvKa<00jDogh60Ws|;k%W>ZP!L=vfit!?QW z9Bdv_kH-nB=&7{qA5;0#r!Cde&dyTb9a{=xbecKVj=fK^P1>YeS@(rP3TnlQ6&}MJ zSZC}QPn2?>LRUod#=)l#{yZKCoMCw3y!cgn0GTTD6{*p01L?ijds0M1BtL^>KC+&L*pQY+ zr5;}6HZ?QDGv2-XHPZ5C>zL+UoZ!Hoc5XBCT}pGoVopbMI6v?+7>7uyA)y?l=g~OiU0B#1@WHmxc#nVC zw2rW~c?ryC-0VkxEQexJj@rq3t{x}qbh_rTY3<4ZXZ7MrBMDYt8QTzIFXwhGTef3Q&U#?0t{GzCJO6DEW;mCiM%9U390wTlWI zFj(z2fr8P1n(XHYTx=-YCE!g9Xeb}Jk&Z1#>8th-v`y%d`^$uKy&alqKo|f@Mq~a% z#^W6p916dir_?=(6sm~MCQc;o1#?+Nu$#mn^j(*A6j_7H`a0qjh=_Ns z-s3g`1g6M+%piQLPNUNr)#Ec~zprsOPUD%GKR+V$zDL4rPWuW<)}myZUrvI1Txv0e z9C!v3RRl@qzD-JPfxlO<5id2m&(>WCa^Dd_Ma3^9b6Y(CMb*5 z*pZvj%9384*;NGf*!%7Q6EMcK}43*}Jd!4t5a_nawfNOuKCo#iELk%%&eUyU|g%~FK zuz#R~V5WkvN3c zAvP-60BN*5bd1;S%`D9EZ<4;7Qq0OhPD2BZ{z9$BK%Ptimw(QxyWKr8bZ6LYjJ=V* zLGp8|oGR1+XGDru=L5?tHXpxUBRnZclnPQUg8|BnrE3APG$8z~IZp3dpf6 zCuc!-=h+1H)cCRv+sEK2gB_!u8pL$~p4iY$B^P$Y6VU5URpE9n0ebRC8=19tktd%U zZH1!1%})CzODoR!ZuMJqO>jn8d=HJXxv>D{MRmWhb0^8)e~wrW1+_MI7Q}rk$jJZ@ zO}z8ij}0g0EyGtnLH%GFhR=vmPg^;HEf-yokv?+npcwG*_hDzt(!Mn96?i>Zjoi>j z_ZTrSFrY +#import "dgbase/AppDelegate.h" +#import "ResultWindow.h" +#import "DirectoryPanel.h" +#import "PyDupeGuru.h" + +@interface AppDelegate : AppDelegateBase +{ + IBOutlet NSButton *presetsButton; + IBOutlet NSPopUpButton *presetsPopup; + IBOutlet ResultWindow *result; + + DirectoryPanel *_directoryPanel; +} +- (IBAction)openWebsite:(id)sender; +- (IBAction)popupPresets:(id)sender; +- (IBAction)toggleDirectories:(id)sender; +- (IBAction)usePreset:(id)sender; + +- (DirectoryPanel *)directoryPanel; +- (PyDupeGuru *)py; +@end diff --git a/me/cocoa/AppDelegate.m b/me/cocoa/AppDelegate.m new file mode 100644 index 00000000..804999de --- /dev/null +++ b/me/cocoa/AppDelegate.m @@ -0,0 +1,158 @@ +#import "AppDelegate.h" +#import "cocoalib/ProgressController.h" +#import "cocoalib/RegistrationInterface.h" +#import "cocoalib/Utils.h" +#import "cocoalib/ValueTransformers.h" +#import "cocoalib/Dialogs.h" +#import "Consts.h" + +@implementation AppDelegate ++ (void)initialize +{ + NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; + NSMutableDictionary *d = [NSMutableDictionary dictionaryWithCapacity:10]; + [d setObject:i2n(3) forKey:@"scanType"]; + [d setObject:i2n(80) forKey:@"minMatchPercentage"]; + [d setObject:i2n(1) forKey:@"recreatePathType"]; + [d setObject:b2n(NO) forKey:@"wordWeighting"]; + [d setObject:b2n(NO) forKey:@"matchSimilarWords"]; + [d setObject:b2n(YES) forKey:@"mixFileKind"]; + [d setObject:b2n(NO) forKey:@"useRegexpFilter"]; + [d setObject:b2n(NO) forKey:@"removeEmptyFolders"]; + [d setObject:b2n(NO) forKey:@"debug"]; + [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:[NSArray array] forKey:@"recentDirectories"]; + [d setObject:[NSArray array] forKey:@"columnsOrder"]; + [d setObject:[NSDictionary dictionary] forKey:@"columnsWidth"]; + [[NSUserDefaultsController sharedUserDefaultsController] setInitialValues:d]; + [ud registerDefaults: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; + _appName = APPNAME; + return self; +} + +- (IBAction)openWebsite:(id)sender +{ + [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"http://www.hardcoded.net/dupeguru_me"]]; +} + +- (IBAction)popupPresets:(id)sender +{ + [presetsPopup selectItem: nil]; + [[presetsPopup cell] performClickWithFrame:[sender frame] inView:[sender superview]]; +} + +- (IBAction)toggleDirectories:(id)sender +{ + [[self directoryPanel] toggleVisible:sender]; +} + +- (IBAction)usePreset:(id)sender +{ + NSUserDefaultsController *ud = [NSUserDefaultsController sharedUserDefaultsController]; + [ud revertToInitialValues:nil]; + NSUserDefaults *d = [ud defaults]; + switch ([sender tag]) + { + case 0: + { + [d setInteger:5 forKey:@"scanType"]; + break; + } + //case 1 is defaults + case 2: + { + [d setInteger:2 forKey:@"scanType"]; + break; + } + case 3: + { + [d setInteger:0 forKey:@"scanType"]; + [d setInteger:50 forKey:@"minMatchPercentage"]; + break; + } + case 4: + { + [d setInteger:0 forKey:@"scanType"]; + [d setInteger:50 forKey:@"minMatchPercentage"]; + [d setBool:YES forKey:@"matchSimilarWords"]; + [d setBool:YES forKey:@"wordWeighting"]; + break; + } + } +} + +- (DirectoryPanel *)directoryPanel +{ + if (!_directoryPanel) + _directoryPanel = [[DirectoryPanel alloc] initWithParentApp:self]; + return _directoryPanel; +} +- (PyDupeGuru *)py { return (PyDupeGuru *)py; } + +//Delegate +- (void)applicationDidFinishLaunching:(NSNotification *)aNotification +{ + [[ProgressController mainProgressController] setWorker:py]; + NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; + //Restore Columns + NSArray *columnsOrder = [ud arrayForKey:@"columnsOrder"]; + NSDictionary *columnsWidth = [ud dictionaryForKey:@"columnsWidth"]; + if ([columnsOrder count]) + [result restoreColumnsPosition:columnsOrder widths:columnsWidth]; + //Reg stuff + if ([RegistrationInterface showNagWithApp:[self py] name:APPNAME limitDescription:LIMIT_DESC]) + [unlockMenuItem setTitle:@"Thanks for buying dupeGuru ME!"]; + //Restore results + [py loadIgnoreList]; + [py loadResults]; +} + +- (void)applicationWillBecomeActive:(NSNotification *)aNotification +{ + if (![[result window] isVisible]) + [result showWindow:NSApp]; +} + +- (void)applicationWillTerminate:(NSNotification *)aNotification +{ + NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; + [ud setObject: [result getColumnsOrder] forKey:@"columnsOrder"]; + [ud setObject: [result getColumnsWidth] forKey:@"columnsWidth"]; + [py saveIgnoreList]; + [py saveResults]; + int sc = [ud integerForKey:@"sessionCountSinceLastIgnorePurge"]; + if (sc >= 10) + { + sc = -1; + [py purgeIgnoreList]; + } + sc++; + [ud setInteger:sc forKey:@"sessionCountSinceLastIgnorePurge"]; + // NSApplication does not release nib instances objects, we must do it manually + // Well, it isn't needed because the memory is freed anyway (we are quitting the application + // But I need to release RecentDirectories so it saves the user defaults + [recentDirectories release]; +} + +- (void)recentDirecoryClicked:(NSString *)directory +{ + [[self directoryPanel] addDirectory:directory]; +} +@end diff --git a/me/cocoa/Consts.h b/me/cocoa/Consts.h new file mode 100644 index 00000000..94d203f7 --- /dev/null +++ b/me/cocoa/Consts.h @@ -0,0 +1,5 @@ +#import "dgbase/Consts.h" + +#define APPNAME @"dupeGuru ME" + +#define jobScanDeadTracks @"jobScanDeadTracks" \ No newline at end of file diff --git a/me/cocoa/DetailsPanel.h b/me/cocoa/DetailsPanel.h new file mode 100644 index 00000000..0d4c025d --- /dev/null +++ b/me/cocoa/DetailsPanel.h @@ -0,0 +1,13 @@ +#import +#import "cocoalib/PyApp.h" +#import "cocoalib/Table.h" + + +@interface DetailsPanel : NSWindowController +{ + IBOutlet TableView *detailsTable; +} +- (id)initWithPy:(PyApp *)aPy; + +- (void)refresh; +@end \ No newline at end of file diff --git a/me/cocoa/DetailsPanel.m b/me/cocoa/DetailsPanel.m new file mode 100644 index 00000000..1baac387 --- /dev/null +++ b/me/cocoa/DetailsPanel.m @@ -0,0 +1,16 @@ +#import "DetailsPanel.h" + +@implementation DetailsPanel +- (id)initWithPy:(PyApp *)aPy +{ + self = [super initWithWindowNibName:@"Details"]; + [self window]; //So the detailsTable is initialized. + [detailsTable setPy:aPy]; + return self; +} + +- (void)refresh +{ + [detailsTable reloadData]; +} +@end diff --git a/me/cocoa/DirectoryPanel.h b/me/cocoa/DirectoryPanel.h new file mode 100644 index 00000000..86b00388 --- /dev/null +++ b/me/cocoa/DirectoryPanel.h @@ -0,0 +1,8 @@ +#import +#import "dgbase/DirectoryPanel.h" + +@interface DirectoryPanel : DirectoryPanelBase +{ +} +- (IBAction)addiTunes:(id)sender; +@end diff --git a/me/cocoa/DirectoryPanel.m b/me/cocoa/DirectoryPanel.m new file mode 100644 index 00000000..0f9831ab --- /dev/null +++ b/me/cocoa/DirectoryPanel.m @@ -0,0 +1,23 @@ +#import "DirectoryPanel.h" + +@implementation DirectoryPanel +- (IBAction)addiTunes:(id)sender +{ + [self addDirectory:[@"~/Music/iTunes/iTunes Music" stringByExpandingTildeInPath]]; +} + +- (IBAction)popupAddDirectoryMenu:(id)sender +{ + NSMenu *m = [addButtonPopUp menu]; + while ([m numberOfItems] > 0) + [m removeItemAtIndex:0]; + NSMenuItem *mi = [m addItemWithTitle:@"Add New Directory..." action:@selector(askForDirectory:) keyEquivalent:@""]; + [mi setTarget:self]; + mi = [m addItemWithTitle:@"Add iTunes Directory" action:@selector(addiTunes:) keyEquivalent:@""]; + [mi setTarget:self]; + [m addItem:[NSMenuItem separatorItem]]; + [_recentDirectories fillMenu:m]; + [addButtonPopUp selectItem: nil]; + [[addButtonPopUp cell] performClickWithFrame:[sender frame] inView:[sender superview]]; +} +@end diff --git a/me/cocoa/English.lproj/Details.nib/classes.nib b/me/cocoa/English.lproj/Details.nib/classes.nib new file mode 100644 index 00000000..e1b7cb92 --- /dev/null +++ b/me/cocoa/English.lproj/Details.nib/classes.nib @@ -0,0 +1,18 @@ +{ + IBClasses = ( + { + CLASS = DetailsPanel; + LANGUAGE = ObjC; + OUTLETS = {detailsTable = NSTableView; }; + SUPERCLASS = NSWindowController; + }, + {CLASS = FirstResponder; LANGUAGE = ObjC; SUPERCLASS = NSObject; }, + { + CLASS = TableView; + LANGUAGE = ObjC; + OUTLETS = {py = PyApp; }; + SUPERCLASS = NSTableView; + } + ); + IBVersion = 1; +} \ No newline at end of file diff --git a/me/cocoa/English.lproj/Details.nib/info.nib b/me/cocoa/English.lproj/Details.nib/info.nib new file mode 100644 index 00000000..3f14ee77 --- /dev/null +++ b/me/cocoa/English.lproj/Details.nib/info.nib @@ -0,0 +1,16 @@ + + + + + IBDocumentLocation + 432 54 356 240 0 0 1024 746 + IBFramework Version + 443.0 + IBOpenObjects + + 5 + + IBSystem Version + 8I127 + + diff --git a/me/cocoa/English.lproj/Details.nib/keyedobjects.nib b/me/cocoa/English.lproj/Details.nib/keyedobjects.nib new file mode 100644 index 0000000000000000000000000000000000000000..e50621e9b70e67ef9ab1837bad20b6e5a12a053a GIT binary patch literal 6122 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 + + + + IBClasses + + + CLASS + FirstResponder + LANGUAGE + ObjC + SUPERCLASS + NSObject + + + ACTIONS + + addiTunes + id + askForDirectory + id + changeDirectoryState + id + popupAddDirectoryMenu + id + removeSelectedDirectory + id + toggleVisible + id + + CLASS + DirectoryPanel + LANGUAGE + ObjC + OUTLETS + + addButtonPopUp + NSPopUpButton + directories + NSOutlineView + removeButton + NSButton + + SUPERCLASS + DirectoryPanelBase + + + CLASS + OutlineView + LANGUAGE + ObjC + OUTLETS + + py + PyApp + + SUPERCLASS + NSOutlineView + + + IBVersion + 1 + + diff --git a/me/cocoa/English.lproj/Directories.nib/info.nib b/me/cocoa/English.lproj/Directories.nib/info.nib new file mode 100644 index 00000000..77f19ce7 --- /dev/null +++ b/me/cocoa/English.lproj/Directories.nib/info.nib @@ -0,0 +1,20 @@ + + + + + IBFramework Version + 629 + IBLastKnownRelativeProjectPath + ../../dupeguru.xcodeproj + IBOldestOS + 5 + IBOpenObjects + + 5 + + IBSystem Version + 9B18 + targetFramework + IBCocoaFramework + + diff --git a/me/cocoa/English.lproj/Directories.nib/keyedobjects.nib b/me/cocoa/English.lproj/Directories.nib/keyedobjects.nib new file mode 100644 index 0000000000000000000000000000000000000000..c541159e828cdd61d88e04c1cd76746986f25db6 GIT binary patch literal 9698 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 literal 0 HcmV?d00001 diff --git a/me/cocoa/English.lproj/InfoPlist.strings b/me/cocoa/English.lproj/InfoPlist.strings new file mode 100644 index 0000000000000000000000000000000000000000..b0430081705b43f55219aa5ab386484da1f3f892 GIT binary patch literal 204 zcmW-bOA5k35Cv=PDT2!&M%;ffpr(+TGdEs?=PZk+ZTdy;YuvTm#?IsUD@Dfr?4!S!+@j+&G(iVdFkCH)E@- sWNyM$M`QkueaM)Z)8}np$TiZrR3ZKUPa59uc!XWaKyA#(n&_JH1B0<7GXMYp literal 0 HcmV?d00001 diff --git a/me/cocoa/English.lproj/MainMenu.nib/classes.nib b/me/cocoa/English.lproj/MainMenu.nib/classes.nib new file mode 100644 index 00000000..b3d34484 --- /dev/null +++ b/me/cocoa/English.lproj/MainMenu.nib/classes.nib @@ -0,0 +1,257 @@ + + + + + IBClasses + + + CLASS + NSSegmentedControl + LANGUAGE + ObjC + SUPERCLASS + NSControl + + + ACTIONS + + openWebsite + id + popupPresets + id + toggleDirectories + id + unlockApp + id + usePreset + id + + CLASS + AppDelegate + LANGUAGE + ObjC + OUTLETS + + defaultsController + NSUserDefaultsController + presetsButton + NSButton + presetsPopup + NSPopUpButton + py + PyDupeGuru + recentDirectories + RecentDirectories + result + ResultWindow + unlockMenuItem + NSMenuItem + + SUPERCLASS + AppDelegateBase + + + CLASS + PyApp + LANGUAGE + ObjC + SUPERCLASS + NSObject + + + CLASS + MatchesView + LANGUAGE + ObjC + SUPERCLASS + OutlineView + + + CLASS + PyDupeGuruBase + LANGUAGE + ObjC + SUPERCLASS + PyApp + + + CLASS + PyDupeGuru + LANGUAGE + ObjC + SUPERCLASS + PyDupeGuruBase + + + ACTIONS + + changeDelta + id + changePowerMarker + id + clearIgnoreList + id + collapseAll + id + copyMarked + id + deleteMarked + id + expandAll + id + exportToXHTML + id + filter + id + ignoreSelected + id + markAll + id + markInvert + id + markNone + id + markSelected + id + markToggle + id + moveMarked + id + openSelected + id + refresh + id + removeDeadTracks + id + removeMarked + id + removeSelected + id + renameSelected + id + resetColumnsToDefault + id + revealSelected + id + showPreferencesPanel + id + startDuplicateScan + id + switchSelected + id + toggleColumn + id + toggleDelta + id + toggleDetailsPanel + id + togglePowerMarker + id + + CLASS + ResultWindow + LANGUAGE + ObjC + OUTLETS + + actionMenu + NSPopUpButton + actionMenuView + NSView + app + id + columnsMenu + NSMenu + deltaSwitch + NSSegmentedControl + deltaSwitchView + NSView + filterField + NSSearchField + filterFieldView + NSView + matches + MatchesView + pmSwitch + NSSegmentedControl + pmSwitchView + NSView + preferencesPanel + NSWindow + py + PyDupeGuru + stats + NSTextField + + SUPERCLASS + ResultWindowBase + + + CLASS + FirstResponder + LANGUAGE + ObjC + SUPERCLASS + NSObject + + + ACTIONS + + checkForUpdates + id + + CLASS + SUUpdater + LANGUAGE + ObjC + SUPERCLASS + NSObject + + + ACTIONS + + clearMenu + id + menuClick + id + + CLASS + RecentDirectories + LANGUAGE + ObjC + OUTLETS + + delegate + id + menu + NSMenu + + SUPERCLASS + NSObject + + + CLASS + ResultWindowBase + LANGUAGE + ObjC + SUPERCLASS + NSWindowController + + + CLASS + OutlineView + LANGUAGE + ObjC + OUTLETS + + py + PyApp + + SUPERCLASS + NSOutlineView + + + IBVersion + 1 + + diff --git a/me/cocoa/English.lproj/MainMenu.nib/info.nib b/me/cocoa/English.lproj/MainMenu.nib/info.nib new file mode 100644 index 00000000..7cd1eddb --- /dev/null +++ b/me/cocoa/English.lproj/MainMenu.nib/info.nib @@ -0,0 +1,20 @@ + + + + + IBFramework Version + 629 + IBLastKnownRelativeProjectPath + ../../dupeguru.xcodeproj + IBOldestOS + 5 + IBOpenObjects + + 598 + + IBSystem Version + 9E17 + targetFramework + IBCocoaFramework + + diff --git a/me/cocoa/English.lproj/MainMenu.nib/keyedobjects.nib b/me/cocoa/English.lproj/MainMenu.nib/keyedobjects.nib new file mode 100644 index 0000000000000000000000000000000000000000..649ce170df6fbe0c363a3f9127126dc86a6a10ef GIT binary patch literal 57645 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 literal 0 HcmV?d00001 diff --git a/me/cocoa/Info.plist b/me/cocoa/Info.plist new file mode 100644 index 00000000..97d43767 --- /dev/null +++ b/me/cocoa/Info.plist @@ -0,0 +1,34 @@ + + + + + CFBundleDevelopmentRegion + English + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleHelpBookFolder + dupeguru_me_help + CFBundleHelpBookName + dupeGuru ME Help + CFBundleIconFile + dupeguru + CFBundleIdentifier + com.hardcoded_software.dupeguru_me + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + APPL + CFBundleSignature + hsft + CFBundleVersion + 5.6.1 + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + SUFeedURL + http://www.hardcoded.net/updates/dupeguru_me.appcast + + diff --git a/me/cocoa/PyDupeGuru.h b/me/cocoa/PyDupeGuru.h new file mode 100644 index 00000000..04dfce20 --- /dev/null +++ b/me/cocoa/PyDupeGuru.h @@ -0,0 +1,15 @@ +#import +#import "dgbase/PyDupeGuru.h" + +@interface PyDupeGuru : PyDupeGuruBase +//Scanning options +- (void)setScanType:(NSNumber *)scan_type; +- (void)setMinWordCount:(NSNumber *)word_count; +- (void)setMinWordLength:(NSNumber *)word_length; +- (void)setWordWeighting:(NSNumber *)words_are_weighted; +- (void)setMatchSimilarWords:(NSNumber *)match_similar_words; +- (void)enable:(NSNumber *)enable scanForTag:(NSString *)tag; +- (void)scanDeadTracks; +- (void)removeDeadTracks; +- (int)deadTrackCount; +@end diff --git a/me/cocoa/ResultWindow.h b/me/cocoa/ResultWindow.h new file mode 100644 index 00000000..e421b99d --- /dev/null +++ b/me/cocoa/ResultWindow.h @@ -0,0 +1,56 @@ +#import +#import "cocoalib/Outline.h" +#import "dgbase/ResultWindow.h" +#import "DetailsPanel.h" +#import "DirectoryPanel.h" + +@interface ResultWindow : ResultWindowBase +{ + IBOutlet NSPopUpButton *actionMenu; + IBOutlet NSMenu *columnsMenu; + IBOutlet NSSearchField *filterField; + IBOutlet NSSegmentedControl *pmSwitch; + IBOutlet NSWindow *preferencesPanel; + IBOutlet NSTextField *stats; + + NSString *_lastAction; + DetailsPanel *_detailsPanel; + NSMutableArray *_resultColumns; + NSMutableIndexSet *_deltaColumns; +} +- (IBAction)changePowerMarker:(id)sender; +- (IBAction)clearIgnoreList:(id)sender; +- (IBAction)exportToXHTML:(id)sender; +- (IBAction)filter:(id)sender; +- (IBAction)ignoreSelected:(id)sender; +- (IBAction)markAll:(id)sender; +- (IBAction)markInvert:(id)sender; +- (IBAction)markNone:(id)sender; +- (IBAction)markSelected:(id)sender; +- (IBAction)markToggle:(id)sender; +- (IBAction)openSelected:(id)sender; +- (IBAction)refresh:(id)sender; +- (IBAction)removeDeadTracks:(id)sender; +- (IBAction)removeMarked:(id)sender; +- (IBAction)removeSelected:(id)sender; +- (IBAction)renameSelected:(id)sender; +- (IBAction)resetColumnsToDefault:(id)sender; +- (IBAction)revealSelected:(id)sender; +- (IBAction)showPreferencesPanel:(id)sender; +- (IBAction)startDuplicateScan:(id)sender; +- (IBAction)switchSelected:(id)sender; +- (IBAction)toggleColumn:(id)sender; +- (IBAction)toggleDelta:(id)sender; +- (IBAction)toggleDetailsPanel:(id)sender; +- (IBAction)togglePowerMarker:(id)sender; + +- (NSTableColumn *)getColumnForIdentifier:(int)aIdentifier title:(NSString *)aTitle width:(int)aWidth refCol:(NSTableColumn *)aColumn; +- (NSArray *)getColumnsOrder; +- (NSDictionary *)getColumnsWidth; +- (NSArray *)getSelected:(BOOL)aDupesOnly; +- (NSArray *)getSelectedPaths:(BOOL)aDupesOnly; +- (void)initResultColumns; +- (void)performPySelection:(NSArray *)aIndexPaths; +- (void)refreshStats; +- (void)restoreColumnsPosition:(NSArray *)aColumnsOrder widths:(NSDictionary *)aColumnsWidth; +@end diff --git a/me/cocoa/ResultWindow.m b/me/cocoa/ResultWindow.m new file mode 100644 index 00000000..540216e3 --- /dev/null +++ b/me/cocoa/ResultWindow.m @@ -0,0 +1,505 @@ +#import "ResultWindow.h" +#import "cocoalib/Dialogs.h" +#import "cocoalib/ProgressController.h" +#import "cocoalib/RegistrationInterface.h" +#import "cocoalib/Utils.h" +#import "AppDelegate.h" +#import "Consts.h" + +@implementation ResultWindow +/* Override */ +- (void)awakeFromNib +{ + [super awakeFromNib]; + _detailsPanel = nil; + _displayDelta = NO; + _powerMode = NO; + _deltaColumns = [[NSMutableIndexSet indexSetWithIndexesInRange:NSMakeRange(2,7)] retain]; + [_deltaColumns removeIndex:6]; + [deltaSwitch setSelectedSegment:0]; + [pmSwitch setSelectedSegment:0]; + [py setDisplayDeltaValues:b2n(_displayDelta)]; + [matches setTarget:self]; + [matches setDoubleAction:@selector(openSelected:)]; + [[actionMenu itemAtIndex:0] setImage:[NSImage imageNamed: @"gear"]]; + [self initResultColumns]; + [self refreshStats]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(resultsMarkingChanged:) name:ResultsMarkingChangedNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(duplicateSelectionChanged:) name:DuplicateSelectionChangedNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(resultsChanged:) name:ResultsChangedNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(jobCompleted:) name:JobCompletedNotification object:nil]; + + NSToolbar *t = [[[NSToolbar alloc] initWithIdentifier:@"ResultWindowToolbar"] autorelease]; + [t setAllowsUserCustomization:YES]; + [t setAutosavesConfiguration:YES]; + [t setDisplayMode:NSToolbarDisplayModeIconAndLabel]; + [t setDelegate:self]; + [[self window] setToolbar:t]; +} + +/* Overrides */ +- (NSString *)logoImageName +{ + return @"dgme_logo32"; +} + +/* Actions */ + +- (IBAction)changePowerMarker:(id)sender +{ + _powerMode = [pmSwitch selectedSegment] == 1; + if (_powerMode) + [matches setTag:2]; + else + [matches setTag:0]; + [self expandAll:nil]; + [self outlineView:matches didClickTableColumn:nil]; +} + +- (IBAction)clearIgnoreList:(id)sender +{ + int i = n2i([py getIgnoreListCount]); + if (!i) + return; + if ([Dialogs askYesNo:[NSString stringWithFormat:@"Do you really want to remove all %d items from the ignore list?",i]] == NSAlertSecondButtonReturn) // NO + return; + [py clearIgnoreList]; +} + +- (IBAction)exportToXHTML:(id)sender +{ + NSString *xsltPath = [[NSBundle mainBundle] pathForResource:@"dg" ofType:@"xsl"]; + NSString *cssPath = [[NSBundle mainBundle] pathForResource:@"hardcoded" ofType:@"css"]; + NSString *exported = [py exportToXHTMLwithColumns:[self getColumnsOrder] xslt:xsltPath css:cssPath]; + [[NSWorkspace sharedWorkspace] openFile:exported]; +} + +- (IBAction)filter:(id)sender +{ + NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; + [py setEscapeFilterRegexp:b2n(!n2b([ud objectForKey:@"useRegexpFilter"]))]; + [py applyFilter:[filterField stringValue]]; + [[NSNotificationCenter defaultCenter] postNotificationName:ResultsChangedNotification object:self]; +} + +- (IBAction)ignoreSelected:(id)sender +{ + NSArray *nodeList = [self getSelected:YES]; + if (![nodeList count]) + return; + if ([Dialogs askYesNo:[NSString stringWithFormat:@"All selected %d matches are going to be ignored in all subsequent scans. Continue?",[nodeList count]]] == NSAlertSecondButtonReturn) // NO + return; + [self performPySelection:[self getSelectedPaths:YES]]; + [py addSelectedToIgnoreList]; + [py removeSelected]; + [[NSNotificationCenter defaultCenter] postNotificationName:ResultsChangedNotification object:self]; +} + +- (IBAction)markAll:(id)sender +{ + [py markAll]; + [[NSNotificationCenter defaultCenter] postNotificationName:ResultsMarkingChangedNotification object:self]; +} + +- (IBAction)markInvert:(id)sender +{ + [py markInvert]; + [[NSNotificationCenter defaultCenter] postNotificationName:ResultsMarkingChangedNotification object:self]; +} + +- (IBAction)markNone:(id)sender +{ + [py markNone]; + [[NSNotificationCenter defaultCenter] postNotificationName:ResultsMarkingChangedNotification object:self]; +} + +- (IBAction)markSelected:(id)sender +{ + [self performPySelection:[self getSelectedPaths:YES]]; + [py toggleSelectedMark]; + [[NSNotificationCenter defaultCenter] postNotificationName:ResultsMarkingChangedNotification object:self]; +} + +- (IBAction)markToggle:(id)sender +{ + OVNode *node = [matches itemAtRow:[matches clickedRow]]; + [self performPySelection:[NSArray arrayWithObject:p2a([node indexPath])]]; + [py toggleSelectedMark]; + [[NSNotificationCenter defaultCenter] postNotificationName:ResultsMarkingChangedNotification object:self]; +} + +- (IBAction)openSelected:(id)sender +{ + [self performPySelection:[self getSelectedPaths:NO]]; + [py openSelected]; +} + +- (IBAction)refresh:(id)sender +{ + [[NSNotificationCenter defaultCenter] postNotificationName:ResultsChangedNotification object:self]; +} + +- (IBAction)removeDeadTracks:(id)sender +{ + [(PyDupeGuru *)py scanDeadTracks]; +} + +- (IBAction)removeMarked:(id)sender +{ + int mark_count = [[py getMarkCount] intValue]; + if (!mark_count) + return; + if ([Dialogs askYesNo:[NSString stringWithFormat:@"You are about to remove %d files from results. Continue?",mark_count]] == NSAlertSecondButtonReturn) // NO + return; + [py removeMarked]; + [[NSNotificationCenter defaultCenter] postNotificationName:ResultsChangedNotification object:self]; +} + +- (IBAction)removeSelected:(id)sender +{ + NSArray *nodeList = [self getSelected:YES]; + if (![nodeList count]) + return; + if ([Dialogs askYesNo:[NSString stringWithFormat:@"You are about to remove %d files from results. Continue?",[nodeList count]]] == NSAlertSecondButtonReturn) // NO + return; + [self performPySelection:[self getSelectedPaths:YES]]; + [py removeSelected]; + [[NSNotificationCenter defaultCenter] postNotificationName:ResultsChangedNotification object:self]; +} + +- (IBAction)renameSelected:(id)sender +{ + int col = [matches columnWithIdentifier:@"0"]; + int row = [matches selectedRow]; + [matches editColumn:col row:row withEvent:[NSApp currentEvent] select:YES]; +} + +- (IBAction)resetColumnsToDefault:(id)sender +{ + NSMutableArray *columnsOrder = [NSMutableArray array]; + [columnsOrder addObject:@"0"]; + [columnsOrder addObject:@"2"]; + [columnsOrder addObject:@"3"]; + [columnsOrder addObject:@"4"]; + [columnsOrder addObject:@"16"]; + NSMutableDictionary *columnsWidth = [NSMutableDictionary dictionary]; + [columnsWidth setObject:i2n(214) forKey:@"0"]; + [columnsWidth setObject:i2n(63) forKey:@"2"]; + [columnsWidth setObject:i2n(50) forKey:@"3"]; + [columnsWidth setObject:i2n(50) forKey:@"4"]; + [columnsWidth setObject:i2n(57) forKey:@"16"]; + [self restoreColumnsPosition:columnsOrder widths:columnsWidth]; +} + +- (IBAction)revealSelected:(id)sender +{ + [self performPySelection:[self getSelectedPaths:NO]]; + [py revealSelected]; +} + +- (IBAction)showPreferencesPanel:(id)sender +{ + [preferencesPanel makeKeyAndOrderFront:sender]; +} + +- (IBAction)startDuplicateScan:(id)sender +{ + if ([matches numberOfRows] > 0) + { + if ([Dialogs askYesNo:@"Are you sure you want to start a new duplicate scan?"] == NSAlertSecondButtonReturn) // NO + return; + } + NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; + PyDupeGuru *_py = (PyDupeGuru *)py; + [_py setScanType:[ud objectForKey:@"scanType"]]; + [_py enable:[ud objectForKey:@"scanTagTrack"] scanForTag:@"track"]; + [_py enable:[ud objectForKey:@"scanTagArtist"] scanForTag:@"artist"]; + [_py enable:[ud objectForKey:@"scanTagAlbum"] scanForTag:@"album"]; + [_py enable:[ud objectForKey:@"scanTagTitle"] scanForTag:@"title"]; + [_py enable:[ud objectForKey:@"scanTagGenre"] scanForTag:@"genre"]; + [_py enable:[ud objectForKey:@"scanTagYear"] scanForTag:@"year"]; + [_py setMinMatchPercentage:[ud objectForKey:@"minMatchPercentage"]]; + [_py setWordWeighting:[ud objectForKey:@"wordWeighting"]]; + [_py setMixFileKind:[ud objectForKey:@"mixFileKind"]]; + [_py setMatchSimilarWords:[ud objectForKey:@"matchSimilarWords"]]; + int r = n2i([py doScan]); + [matches reloadData]; + [self refreshStats]; + if (r == 1) + [Dialogs showMessage:@"You cannot make a duplicate scan with only reference directories."]; + if (r == 3) + { + [Dialogs showMessage:@"The selected directories contain no scannable file."]; + [app toggleDirectories:nil]; + } +} + +- (IBAction)switchSelected:(id)sender +{ + [self performPySelection:[self getSelectedPaths:YES]]; + [py makeSelectedReference]; + [[NSNotificationCenter defaultCenter] postNotificationName:ResultsChangedNotification object:self]; +} + +- (IBAction)toggleColumn:(id)sender +{ + NSMenuItem *mi = sender; + NSString *colId = [NSString stringWithFormat:@"%d",[mi tag]]; + NSTableColumn *col = [matches tableColumnWithIdentifier:colId]; + if (col == nil) + { + //Add Column + col = [_resultColumns objectAtIndex:[mi tag]]; + [matches addTableColumn:col]; + [mi setState:NSOnState]; + } + else + { + //Remove column + [matches removeTableColumn:col]; + [mi setState:NSOffState]; + } +} + +- (IBAction)toggleDelta:(id)sender +{ + if ([deltaSwitch selectedSegment] == 1) + [deltaSwitch setSelectedSegment:0]; + else + [deltaSwitch setSelectedSegment:1]; + [self changeDelta:sender]; +} + +- (IBAction)toggleDetailsPanel:(id)sender +{ + if (!_detailsPanel) + _detailsPanel = [[DetailsPanel alloc] initWithPy:py]; + if ([[_detailsPanel window] isVisible]) + [[_detailsPanel window] close]; + else + [[_detailsPanel window] orderFront:nil]; +} + +- (IBAction)togglePowerMarker:(id)sender +{ + if ([pmSwitch selectedSegment] == 1) + [pmSwitch setSelectedSegment:0]; + else + [pmSwitch setSelectedSegment:1]; + [self changePowerMarker:sender]; +} + +/* Public */ +- (NSTableColumn *)getColumnForIdentifier:(int)aIdentifier title:(NSString *)aTitle width:(int)aWidth refCol:(NSTableColumn *)aColumn +{ + NSNumber *n = [NSNumber numberWithInt:aIdentifier]; + NSTableColumn *col = [[NSTableColumn alloc] initWithIdentifier:[n stringValue]]; + [col setWidth:aWidth]; + [col setEditable:NO]; + [[col dataCell] setFont:[[aColumn dataCell] font]]; + [[col headerCell] setStringValue:aTitle]; + [col setResizingMask:NSTableColumnUserResizingMask]; + [col setSortDescriptorPrototype:[[NSSortDescriptor alloc] initWithKey:[n stringValue] ascending:YES]]; + return col; +} + +//Returns an array of identifiers, in order. +- (NSArray *)getColumnsOrder +{ + NSTableColumn *col; + NSString *colId; + NSMutableArray *result = [NSMutableArray array]; + NSEnumerator *e = [[matches tableColumns] objectEnumerator]; + while (col = [e nextObject]) + { + colId = [col identifier]; + [result addObject:colId]; + } + return result; +} + +- (NSDictionary *)getColumnsWidth +{ + NSMutableDictionary *result = [NSMutableDictionary dictionary]; + NSTableColumn *col; + NSString *colId; + NSNumber *width; + NSEnumerator *e = [[matches tableColumns] objectEnumerator]; + while (col = [e nextObject]) + { + colId = [col identifier]; + width = [NSNumber numberWithFloat:[col width]]; + [result setObject:width forKey:colId]; + } + return result; +} + +- (NSArray *)getSelected:(BOOL)aDupesOnly +{ + if (_powerMode) + aDupesOnly = NO; + NSIndexSet *indexes = [matches selectedRowIndexes]; + NSMutableArray *nodeList = [NSMutableArray array]; + OVNode *node; + int i = [indexes firstIndex]; + while (i != NSNotFound) + { + node = [matches itemAtRow:i]; + if (!aDupesOnly || ([node level] > 1)) + [nodeList addObject:node]; + i = [indexes indexGreaterThanIndex:i]; + } + return nodeList; +} + +- (NSArray *)getSelectedPaths:(BOOL)aDupesOnly +{ + NSMutableArray *r = [NSMutableArray array]; + NSArray *selected = [self getSelected:aDupesOnly]; + NSEnumerator *e = [selected objectEnumerator]; + OVNode *node; + while (node = [e nextObject]) + [r addObject:p2a([node indexPath])]; + return r; +} + +- (void)performPySelection:(NSArray *)aIndexPaths +{ + if (_powerMode) + [py selectPowerMarkerNodePaths:aIndexPaths]; + else + [py selectResultNodePaths:aIndexPaths]; +} + +- (void)initResultColumns +{ + NSTableColumn *refCol = [matches tableColumnWithIdentifier:@"0"]; + _resultColumns = [[NSMutableArray alloc] init]; + [_resultColumns addObject:[matches tableColumnWithIdentifier:@"0"]]; // File Name + [_resultColumns addObject:[self getColumnForIdentifier:1 title:@"Directory" width:120 refCol:refCol]]; + [_resultColumns addObject:[matches tableColumnWithIdentifier:@"2"]]; // Size + [_resultColumns addObject:[matches tableColumnWithIdentifier:@"3"]]; // Time + [_resultColumns addObject:[matches tableColumnWithIdentifier:@"4"]]; // Bitrate + [_resultColumns addObject:[self getColumnForIdentifier:5 title:@"Sample Rate" width:60 refCol:refCol]]; + [_resultColumns addObject:[self getColumnForIdentifier:6 title:@"Kind" width:40 refCol:refCol]]; + [_resultColumns addObject:[self getColumnForIdentifier:7 title:@"Creation" width:120 refCol:refCol]]; + [_resultColumns addObject:[self getColumnForIdentifier:8 title:@"Modification" width:120 refCol:refCol]]; + [_resultColumns addObject:[self getColumnForIdentifier:9 title:@"Title" width:120 refCol:refCol]]; + [_resultColumns addObject:[self getColumnForIdentifier:10 title:@"Artist" width:120 refCol:refCol]]; + [_resultColumns addObject:[self getColumnForIdentifier:11 title:@"Album" width:120 refCol:refCol]]; + [_resultColumns addObject:[self getColumnForIdentifier:12 title:@"Genre" width:80 refCol:refCol]]; + [_resultColumns addObject:[self getColumnForIdentifier:13 title:@"Year" width:40 refCol:refCol]]; + [_resultColumns addObject:[self getColumnForIdentifier:14 title:@"Track Number" width:40 refCol:refCol]]; + [_resultColumns addObject:[self getColumnForIdentifier:15 title:@"Comment" width:120 refCol:refCol]]; + [_resultColumns addObject:[matches tableColumnWithIdentifier:@"16"]]; // Match % + [_resultColumns addObject:[self getColumnForIdentifier:17 title:@"Words Used" width:120 refCol:refCol]]; + [_resultColumns addObject:[self getColumnForIdentifier:18 title:@"Dupe Count" width:80 refCol:refCol]]; +} + +-(void)refreshStats +{ + [stats setStringValue:[py getStatLine]]; +} + +- (void)restoreColumnsPosition:(NSArray *)aColumnsOrder widths:(NSDictionary *)aColumnsWidth +{ + NSTableColumn *col; + NSString *colId; + NSNumber *width; + NSMenuItem *mi; + //Remove all columns + NSEnumerator *e = [[columnsMenu itemArray] objectEnumerator]; + while (mi = [e nextObject]) + { + if ([mi state] == NSOnState) + [self toggleColumn:mi]; + } + //Add columns and set widths + e = [aColumnsOrder objectEnumerator]; + while (colId = [e nextObject]) + { + if (![colId isEqual:@"mark"]) + { + col = [_resultColumns objectAtIndex:[colId intValue]]; + width = [aColumnsWidth objectForKey:[col identifier]]; + mi = [columnsMenu itemWithTag:[colId intValue]]; + if (width) + [col setWidth:[width floatValue]]; + [self toggleColumn:mi]; + } + } +} + +/* Delegate */ +- (void)outlineView:(NSOutlineView *)outlineView willDisplayCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn item:(id)item +{ + OVNode *node = item; + if ([[tableColumn identifier] isEqual:@"mark"]) + { + [cell setEnabled: [node isMarkable]]; + } + if ([cell isKindOfClass:[NSTextFieldCell class]]) + { + // Determine if the text color will be blue due to directory being reference. + NSTextFieldCell *textCell = cell; + if ([node isMarkable]) + [textCell setTextColor:[NSColor blackColor]]; + else + [textCell setTextColor:[NSColor blueColor]]; + if ((_displayDelta) && (_powerMode || ([node level] > 1))) + { + int i = [[tableColumn identifier] intValue]; + if ([_deltaColumns containsIndex:i]) + [textCell setTextColor:[NSColor orangeColor]]; + } + } +} + +/* Notifications */ +- (void)duplicateSelectionChanged:(NSNotification *)aNotification +{ + if (_detailsPanel) + [_detailsPanel refresh]; +} + +- (void)jobCompleted:(NSNotification *)aNotification +{ + [super jobCompleted:aNotification]; + id lastAction = [[ProgressController mainProgressController] jobId]; + if ([lastAction isEqualTo:jobScanDeadTracks]) + { + int deadTrackCount = [(PyDupeGuru *)py deadTrackCount]; + if (deadTrackCount > 0) + { + NSString *msg = @"Your iTunes Library contains %d dead tracks ready to be removed. Continue?"; + if ([Dialogs askYesNo:[NSString stringWithFormat:msg,deadTrackCount]] == NSAlertFirstButtonReturn) + [(PyDupeGuru *)py removeDeadTracks]; + } + else + { + [Dialogs showMessage:@"You have no dead tracks in your iTunes Library"]; + } + } +} + +- (void)outlineViewSelectionDidChange:(NSNotification *)notification +{ + [self performPySelection:[self getSelectedPaths:NO]]; + [py refreshDetailsWithSelected]; + [[NSNotificationCenter defaultCenter] postNotificationName:DuplicateSelectionChangedNotification object:self]; +} + +- (void)resultsChanged:(NSNotification *)aNotification +{ + [matches reloadData]; + [self expandAll:nil]; + [self outlineViewSelectionDidChange:nil]; + [self refreshStats]; +} + +- (void)resultsMarkingChanged:(NSNotification *)aNotification +{ + [matches invalidateMarkings]; + [self refreshStats]; +} +@end diff --git a/me/cocoa/dupeguru.icns b/me/cocoa/dupeguru.icns new file mode 100755 index 0000000000000000000000000000000000000000..42f8641e50bfec321f2ce0facc5cdf11c2922bc2 GIT binary patch literal 58956 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�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 + + + + ActivePerspectiveName + Project + AllowedModules + + + BundleLoadPath + + MaxInstances + n + Module + PBXSmartGroupTreeModule + Name + Groups and Files Outline View + + + BundleLoadPath + + MaxInstances + n + Module + PBXNavigatorGroup + Name + Editor + + + BundleLoadPath + + MaxInstances + n + Module + XCTaskListModule + Name + Task List + + + BundleLoadPath + + MaxInstances + n + Module + XCDetailModule + Name + File and Smart Group Detail Viewer + + + BundleLoadPath + + MaxInstances + 1 + Module + PBXBuildResultsModule + Name + Detailed Build Results Viewer + + + BundleLoadPath + + MaxInstances + 1 + Module + PBXProjectFindModule + Name + Project Batch Find Tool + + + BundleLoadPath + + MaxInstances + n + Module + PBXRunSessionModule + Name + Run Log + + + BundleLoadPath + + MaxInstances + n + Module + PBXBookmarksModule + Name + Bookmarks Tool + + + BundleLoadPath + + MaxInstances + n + Module + PBXClassBrowserModule + Name + Class Browser + + + BundleLoadPath + + MaxInstances + n + Module + PBXCVSModule + Name + Source Code Control Tool + + + BundleLoadPath + + MaxInstances + n + Module + PBXDebugBreakpointsModule + Name + Debug Breakpoints Tool + + + BundleLoadPath + + MaxInstances + n + Module + XCDockableInspector + Name + Inspector + + + BundleLoadPath + + MaxInstances + n + Module + PBXOpenQuicklyModule + Name + Open Quickly Tool + + + BundleLoadPath + + MaxInstances + 1 + Module + PBXDebugSessionModule + Name + Debugger + + + BundleLoadPath + + MaxInstances + 1 + Module + PBXDebugCLIModule + Name + Debug Console + + + Description + DefaultDescriptionKey + DockingSystemVisible + + Extension + mode1 + FavBarConfig + + PBXProjectModuleGUID + CE381CB409914B41003581CE + XCBarModuleItemNames + + XCBarModuleItems + + + FirstTimeWindowDisplayed + + Identifier + com.apple.perspectives.project.mode1 + MajorVersion + 31 + MinorVersion + 1 + Name + Default + Notifications + + OpenEditors + + PerspectiveWidths + + -1 + -1 + + Perspectives + + + ChosenToolbarItems + + active-executable-popup + action + active-buildstyle-popup + active-target-popup + buildOrClean + build-and-runOrDebug + com.apple.ide.PBXToolbarStopButton + get-info + toggle-editor + + ControllerClassBaseName + + IconName + WindowOfProjectWithEditor + Identifier + perspective.project + IsVertical + + Layout + + + BecomeActive + + ContentConfiguration + + PBXBottomSmartGroupGIDs + + 1C37FBAC04509CD000000102 + 1C37FAAC04509CD000000102 + 1C08E77C0454961000C914BD + 1C37FABC05509CD000000102 + 1C37FABC05539CD112110102 + E2644B35053B69B200211256 + 1C37FABC04509CD000100104 + 1CC0EA4004350EF90044410B + 1CC0EA4004350EF90041110B + + PBXProjectModuleGUID + 1CE0B1FE06471DED0097A5F4 + PBXProjectModuleLabel + Files + PBXProjectStructureProvided + yes + PBXSmartGroupTreeModuleColumnData + + PBXSmartGroupTreeModuleColumnWidthsKey + + 194 + + PBXSmartGroupTreeModuleColumnsKey_v4 + + MainColumn + + + PBXSmartGroupTreeModuleOutlineStateKey_v7 + + PBXSmartGroupTreeModuleOutlineStateExpansionKey + + 29B97314FDCFA39411CA2CEA + 080E96DDFE201D6D7F000001 + 29B97315FDCFA39411CA2CEA + 29B97317FDCFA39411CA2CEA + 29B97323FDCFA39411CA2CEA + 1058C7A0FEA54F0111CA2CBB + CE1425880AFB718500BD5167 + 19C28FACFE9D520D11CA2CBB + 1C37FBAC04509CD000000102 + 1C37FABC05509CD000000102 + + PBXSmartGroupTreeModuleOutlineStateSelectionKey + + + 37 + + + PBXSmartGroupTreeModuleOutlineStateVisibleRectKey + {{0, 0}, {194, 764}} + + PBXTopSmartGroupGIDs + + XCIncludePerspectivesSwitch + + XCSharingToken + com.apple.Xcode.GFSharingToken + + GeometryConfiguration + + Frame + {{0, 0}, {211, 782}} + GroupTreeTableConfiguration + + MainColumn + 194 + + RubberWindowFrame + 0 55 1372 823 0 0 1440 878 + + Module + PBXSmartGroupTreeModule + Proportion + 211pt + + + Dock + + + ContentConfiguration + + PBXProjectModuleGUID + 1CE0B20306471E060097A5F4 + PBXProjectModuleLabel + Info.plist + PBXSplitModuleInNavigatorKey + + Split0 + + PBXProjectModuleGUID + 1CE0B20406471E060097A5F4 + PBXProjectModuleLabel + Info.plist + _historyCapacity + 10 + bookmark + CE6D39470C9B111800C7FE6C + history + + CEEEF20D0BAD825F00F7AD7F + CEEEF3350BAD8AC700F7AD7F + CE7E210C0BB5B7DD00C69A50 + CE7E21360BB5BD8200C69A50 + CEE301FD0BF73B1900D6840C + CEE301FE0BF73B1900D6840C + CEE301FF0BF73B1900D6840C + CEE302000BF73B1900D6840C + CE6D38650C9B0E1A00C7FE6C + CE6D38680C9B0E1A00C7FE6C + + prevStack + + CE2CB4DA09AE70AA0015538F + CE9DA31409E03DC700B0AAC8 + CE962FD809E1A2310049C9D7 + CECD332A09FEDD9D00964507 + CEF3113D0A06AA42002EC022 + CE2AFF3E0A07838000443588 + CEEEF2130BAD825F00F7AD7F + CE6A176E0BB5A8310090A314 + CE6A176F0BB5A8310090A314 + + + SplitCount + 1 + + StatusBarVisibility + + + GeometryConfiguration + + Frame + {{0, 0}, {1156, 544}} + RubberWindowFrame + 0 55 1372 823 0 0 1440 878 + + Module + PBXNavigatorGroup + Proportion + 544pt + + + ContentConfiguration + + PBXProjectModuleGUID + 1CE0B20506471E060097A5F4 + PBXProjectModuleLabel + Detail + + GeometryConfiguration + + Frame + {{0, 549}, {1156, 233}} + RubberWindowFrame + 0 55 1372 823 0 0 1440 878 + + Module + XCDetailModule + Proportion + 233pt + + + Proportion + 1156pt + + + Name + Project + ServiceClasses + + XCModuleDock + PBXSmartGroupTreeModule + XCModuleDock + PBXNavigatorGroup + XCDetailModule + + TableOfContents + + CE6D39480C9B111800C7FE6C + 1CE0B1FE06471DED0097A5F4 + CE6D39490C9B111800C7FE6C + 1CE0B20306471E060097A5F4 + 1CE0B20506471E060097A5F4 + + ToolbarConfiguration + xcode.toolbar.config.default + + + ControllerClassBaseName + + IconName + WindowOfProject + Identifier + perspective.morph + IsVertical + 0 + Layout + + + BecomeActive + 1 + ContentConfiguration + + PBXBottomSmartGroupGIDs + + 1C37FBAC04509CD000000102 + 1C37FAAC04509CD000000102 + 1C08E77C0454961000C914BD + 1C37FABC05509CD000000102 + 1C37FABC05539CD112110102 + E2644B35053B69B200211256 + 1C37FABC04509CD000100104 + 1CC0EA4004350EF90044410B + 1CC0EA4004350EF90041110B + + PBXProjectModuleGUID + 11E0B1FE06471DED0097A5F4 + PBXProjectModuleLabel + Files + PBXProjectStructureProvided + yes + PBXSmartGroupTreeModuleColumnData + + PBXSmartGroupTreeModuleColumnWidthsKey + + 186 + + PBXSmartGroupTreeModuleColumnsKey_v4 + + MainColumn + + + PBXSmartGroupTreeModuleOutlineStateKey_v7 + + PBXSmartGroupTreeModuleOutlineStateExpansionKey + + 29B97314FDCFA39411CA2CEA + 1C37FABC05509CD000000102 + + PBXSmartGroupTreeModuleOutlineStateSelectionKey + + + 0 + + + PBXSmartGroupTreeModuleOutlineStateVisibleRectKey + {{0, 0}, {186, 337}} + + PBXTopSmartGroupGIDs + + XCIncludePerspectivesSwitch + 1 + XCSharingToken + com.apple.Xcode.GFSharingToken + + GeometryConfiguration + + Frame + {{0, 0}, {203, 355}} + GroupTreeTableConfiguration + + MainColumn + 186 + + RubberWindowFrame + 373 269 690 397 0 0 1440 878 + + Module + PBXSmartGroupTreeModule + Proportion + 100% + + + Name + Morph + PreferredWidth + 300 + ServiceClasses + + XCModuleDock + PBXSmartGroupTreeModule + + TableOfContents + + 11E0B1FE06471DED0097A5F4 + + ToolbarConfiguration + xcode.toolbar.config.default.short + + + PerspectivesBarVisible + + ShelfIsVisible + + SourceDescription + file at '/System/Library/PrivateFrameworks/DevToolsInterface.framework/Versions/A/Resources/XCPerspectivesSpecificationMode1.xcperspec' + StatusbarIsVisible + + TimeStamp + 0.0 + ToolbarDisplayMode + 1 + ToolbarIsVisible + + ToolbarSizeMode + 1 + Type + Perspectives + UpdateMessage + The Default Workspace in this version of Xcode now includes support to hide and show the detail view (what has been referred to as the "Metro-Morph" feature). You must discard your current Default Workspace settings and update to the latest Default Workspace in order to gain this feature. Do you wish to update to the latest Workspace defaults for project '%@'? + WindowJustification + 5 + WindowOrderList + + /Users/hsoft/src/dupeguru_me_cocoa/dupeguru.xcodeproj + + WindowString + 0 55 1372 823 0 0 1440 878 + WindowTools + + + FirstTimeWindowDisplayed + + Identifier + windowTool.build + IsVertical + + Layout + + + Dock + + + ContentConfiguration + + PBXProjectModuleGUID + 1CD0528F0623707200166675 + PBXProjectModuleLabel + + StatusBarVisibility + + + GeometryConfiguration + + Frame + {{0, 0}, {1024, 404}} + RubberWindowFrame + 289 192 1024 686 0 0 1440 878 + + Module + PBXNavigatorGroup + Proportion + 404pt + + + BecomeActive + + ContentConfiguration + + PBXProjectModuleGUID + XCMainBuildResultsModuleGUID + PBXProjectModuleLabel + Build + XCBuildResultsTrigger_Collapse + 1021 + XCBuildResultsTrigger_Open + 1011 + + GeometryConfiguration + + Frame + {{0, 409}, {1024, 236}} + RubberWindowFrame + 289 192 1024 686 0 0 1440 878 + + Module + PBXBuildResultsModule + Proportion + 236pt + + + Proportion + 645pt + + + Name + Build Results + ServiceClasses + + PBXBuildResultsModule + + StatusbarIsVisible + + TableOfContents + + CE381CCE09914BC8003581CE + CE6D38450C9B0D2500C7FE6C + 1CD0528F0623707200166675 + XCMainBuildResultsModuleGUID + + ToolbarConfiguration + xcode.toolbar.config.build + WindowString + 289 192 1024 686 0 0 1440 878 + WindowToolGUID + CE381CCE09914BC8003581CE + WindowToolIsVisible + + + + FirstTimeWindowDisplayed + + Identifier + windowTool.debugger + IsVertical + + Layout + + + Dock + + + ContentConfiguration + + Debugger + + HorizontalSplitView + + _collapsingFrameDimension + 0.0 + _indexOfCollapsedView + 0 + _percentageOfCollapsedView + 0.0 + isCollapsed + yes + sizes + + {{0, 0}, {150, 322}} + {{150, 0}, {874, 322}} + + + VerticalSplitView + + _collapsingFrameDimension + 0.0 + _indexOfCollapsedView + 0 + _percentageOfCollapsedView + 0.0 + isCollapsed + yes + sizes + + {{0, 0}, {1024, 322}} + {{0, 322}, {1024, 323}} + + + + LauncherConfigVersion + 8 + PBXProjectModuleGUID + 1C162984064C10D400B95A72 + PBXProjectModuleLabel + Debug - GLUTExamples (Underwater) + + GeometryConfiguration + + DebugConsoleDrawerSize + {100, 120} + DebugConsoleVisible + None + DebugConsoleWindowFrame + {{200, 200}, {500, 300}} + DebugSTDIOWindowFrame + {{200, 200}, {500, 300}} + Frame + {{0, 0}, {1024, 645}} + RubberWindowFrame + 348 192 1024 686 0 0 1440 878 + + Module + PBXDebugSessionModule + Proportion + 645pt + + + Proportion + 645pt + + + Name + Debugger + ServiceClasses + + PBXDebugSessionModule + + StatusbarIsVisible + + TableOfContents + + 1CD10A99069EF8BA00B06720 + CE7E21210BB5BCA400C69A50 + 1C162984064C10D400B95A72 + CE7E21220BB5BCA400C69A50 + CE7E21230BB5BCA400C69A50 + CE7E21240BB5BCA400C69A50 + CE7E21250BB5BCA400C69A50 + CE7E21260BB5BCA400C69A50 + CE7E21270BB5BCA400C69A50 + + ToolbarConfiguration + xcode.toolbar.config.debug + WindowString + 348 192 1024 686 0 0 1440 878 + WindowToolGUID + 1CD10A99069EF8BA00B06720 + WindowToolIsVisible + + + + FirstTimeWindowDisplayed + + Identifier + windowTool.find + IsVertical + + Layout + + + Dock + + + Dock + + + ContentConfiguration + + PBXProjectModuleGUID + 1CDD528C0622207200134675 + PBXProjectModuleLabel + ResultWindow.m + StatusBarVisibility + + + GeometryConfiguration + + Frame + {{0, 0}, {1377, 620}} + RubberWindowFrame + 0 0 1377 878 0 0 1440 878 + + Module + PBXNavigatorGroup + Proportion + 1377pt + + + Proportion + 620pt + + + BecomeActive + + ContentConfiguration + + PBXProjectModuleGUID + 1CD0528E0623707200166675 + PBXProjectModuleLabel + Project Find + + GeometryConfiguration + + Frame + {{0, 625}, {1377, 212}} + RubberWindowFrame + 0 0 1377 878 0 0 1440 878 + + Module + PBXProjectFindModule + Proportion + 212pt + + + Proportion + 837pt + + + Name + Project Find + ServiceClasses + + PBXProjectFindModule + + StatusbarIsVisible + + TableOfContents + + 1C530D57069F1CE1000CFCEE + CE6A17790BB5A8310090A314 + CE6A177A0BB5A8310090A314 + 1CDD528C0622207200134675 + 1CD0528E0623707200166675 + + WindowString + 0 0 1377 878 0 0 1440 878 + WindowToolGUID + 1C530D57069F1CE1000CFCEE + WindowToolIsVisible + + + + Identifier + MENUSEPARATOR + + + FirstTimeWindowDisplayed + + Identifier + windowTool.debuggerConsole + IsVertical + + Layout + + + Dock + + + ContentConfiguration + + PBXProjectModuleGUID + 1C78EAAC065D492600B07095 + PBXProjectModuleLabel + Debugger Console + + GeometryConfiguration + + Frame + {{0, 0}, {440, 358}} + RubberWindowFrame + 72 414 440 400 0 0 1440 878 + + Module + PBXDebugCLIModule + Proportion + 358pt + + + Proportion + 359pt + + + Name + Debugger Console + ServiceClasses + + PBXDebugCLIModule + + StatusbarIsVisible + + TableOfContents + + CECD0ADE099294C1003DC359 + CE7E21280BB5BCA400C69A50 + 1C78EAAC065D492600B07095 + + WindowString + 72 414 440 400 0 0 1440 878 + WindowToolGUID + CECD0ADE099294C1003DC359 + WindowToolIsVisible + + + + FirstTimeWindowDisplayed + + Identifier + windowTool.run + IsVertical + + Layout + + + Dock + + + ContentConfiguration + + LauncherConfigVersion + 3 + PBXProjectModuleGUID + 1CD0528B0623707200166675 + PBXProjectModuleLabel + Run + Runner + + HorizontalSplitView + + _collapsingFrameDimension + 0.0 + _indexOfCollapsedView + 0 + _percentageOfCollapsedView + 0.0 + isCollapsed + yes + sizes + + {{0, 0}, {367, 168}} + {{0, 173}, {367, 270}} + + + VerticalSplitView + + _collapsingFrameDimension + 0.0 + _indexOfCollapsedView + 0 + _percentageOfCollapsedView + 0.0 + isCollapsed + yes + sizes + + {{0, 0}, {406, 443}} + {{411, 0}, {517, 443}} + + + + + GeometryConfiguration + + Frame + {{0, 0}, {1024, 645}} + RubberWindowFrame + 262 127 1024 686 0 0 1440 878 + + Module + PBXRunSessionModule + Proportion + 645pt + + + Proportion + 645pt + + + Name + Run Log + ServiceClasses + + PBXRunSessionModule + + StatusbarIsVisible + + TableOfContents + + 1C0AD2B3069F1EA900FABCE6 + CECCEDD50BB6B39F00873A67 + 1CD0528B0623707200166675 + CECCEDD60BB6B39F00873A67 + + ToolbarConfiguration + xcode.toolbar.config.run + WindowString + 262 127 1024 686 0 0 1440 878 + WindowToolGUID + 1C0AD2B3069F1EA900FABCE6 + WindowToolIsVisible + + + + Identifier + windowTool.scm + Layout + + + Dock + + + ContentConfiguration + + PBXProjectModuleGUID + 1C78EAB2065D492600B07095 + PBXProjectModuleLabel + <No Editor> + PBXSplitModuleInNavigatorKey + + Split0 + + PBXProjectModuleGUID + 1C78EAB3065D492600B07095 + + SplitCount + 1 + + StatusBarVisibility + 1 + + GeometryConfiguration + + Frame + {{0, 0}, {452, 0}} + RubberWindowFrame + 743 379 452 308 0 0 1280 1002 + + Module + PBXNavigatorGroup + Proportion + 0pt + + + BecomeActive + 1 + ContentConfiguration + + PBXProjectModuleGUID + 1CD052920623707200166675 + PBXProjectModuleLabel + SCM + + GeometryConfiguration + + ConsoleFrame + {{0, 259}, {452, 0}} + Frame + {{0, 7}, {452, 259}} + RubberWindowFrame + 743 379 452 308 0 0 1280 1002 + TableConfiguration + + Status + 30 + FileName + 199 + Path + 197.09500122070312 + + TableFrame + {{0, 0}, {452, 250}} + + Module + PBXCVSModule + Proportion + 262pt + + + Proportion + 266pt + + + Name + SCM + ServiceClasses + + PBXCVSModule + + StatusbarIsVisible + 1 + TableOfContents + + 1C78EAB4065D492600B07095 + 1C78EAB5065D492600B07095 + 1C78EAB2065D492600B07095 + 1CD052920623707200166675 + + ToolbarConfiguration + xcode.toolbar.config.scm + WindowString + 743 379 452 308 0 0 1280 1002 + + + FirstTimeWindowDisplayed + + Identifier + windowTool.breakpoints + IsVertical + + Layout + + + Dock + + + BecomeActive + + ContentConfiguration + + PBXBottomSmartGroupGIDs + + 1C77FABC04509CD000000102 + + PBXProjectModuleGUID + 1CE0B1FE06471DED0097A5F4 + PBXProjectModuleLabel + Files + PBXProjectStructureProvided + no + PBXSmartGroupTreeModuleColumnData + + PBXSmartGroupTreeModuleColumnWidthsKey + + 168 + + PBXSmartGroupTreeModuleColumnsKey_v4 + + MainColumn + + + PBXSmartGroupTreeModuleOutlineStateKey_v7 + + PBXSmartGroupTreeModuleOutlineStateExpansionKey + + 1C77FABC04509CD000000102 + 1C3E0DCA080725EA00A55177 + + PBXSmartGroupTreeModuleOutlineStateSelectionKey + + + 2 + 0 + + + PBXSmartGroupTreeModuleOutlineStateVisibleRectKey + {{0, 0}, {168, 350}} + + PBXTopSmartGroupGIDs + + XCIncludePerspectivesSwitch + + + GeometryConfiguration + + Frame + {{0, 0}, {185, 368}} + GroupTreeTableConfiguration + + MainColumn + 168 + + RubberWindowFrame + 52 435 744 409 0 0 1440 878 + + Module + PBXSmartGroupTreeModule + Proportion + 185pt + + + ContentConfiguration + + PBXProjectModuleGUID + 1CA1AED706398EBD00589147 + PBXProjectModuleLabel + Detail + + GeometryConfiguration + + Frame + {{190, 0}, {554, 368}} + RubberWindowFrame + 52 435 744 409 0 0 1440 878 + + Module + XCDetailModule + Proportion + 554pt + + + Proportion + 368pt + + + MajorVersion + 2 + MinorVersion + 0 + Name + Breakpoints + ServiceClasses + + PBXSmartGroupTreeModule + XCDetailModule + + StatusbarIsVisible + + TableOfContents + + CE6B28F20AFB890700508D93 + CE6B28F30AFB890700508D93 + 1CE0B1FE06471DED0097A5F4 + 1CA1AED706398EBD00589147 + + ToolbarConfiguration + xcode.toolbar.config.breakpoints + WindowString + 52 435 744 409 0 0 1440 878 + WindowToolGUID + CE6B28F20AFB890700508D93 + WindowToolIsVisible + + + + Identifier + windowTool.debugAnimator + Layout + + + Dock + + + Module + PBXNavigatorGroup + Proportion + 100% + + + Proportion + 100% + + + Name + Debug Visualizer + ServiceClasses + + PBXNavigatorGroup + + StatusbarIsVisible + 1 + ToolbarConfiguration + xcode.toolbar.config.debugAnimator + WindowString + 100 100 700 500 0 0 1280 1002 + + + Identifier + windowTool.bookmarks + Layout + + + Dock + + + Module + PBXBookmarksModule + Proportion + 100% + + + Proportion + 100% + + + Name + Bookmarks + ServiceClasses + + PBXBookmarksModule + + StatusbarIsVisible + 0 + WindowString + 538 42 401 187 0 0 1280 1002 + + + Identifier + windowTool.classBrowser + Layout + + + Dock + + + BecomeActive + 1 + ContentConfiguration + + OptionsSetName + Hierarchy, all classes + PBXProjectModuleGUID + 1CA6456E063B45B4001379D8 + PBXProjectModuleLabel + Class Browser - NSObject + + GeometryConfiguration + + ClassesFrame + {{0, 0}, {374, 96}} + ClassesTreeTableConfiguration + + PBXClassNameColumnIdentifier + 208 + PBXClassBookColumnIdentifier + 22 + + Frame + {{0, 0}, {630, 331}} + MembersFrame + {{0, 105}, {374, 395}} + MembersTreeTableConfiguration + + PBXMemberTypeIconColumnIdentifier + 22 + PBXMemberNameColumnIdentifier + 216 + PBXMemberTypeColumnIdentifier + 97 + PBXMemberBookColumnIdentifier + 22 + + PBXModuleWindowStatusBarHidden2 + 1 + RubberWindowFrame + 385 179 630 352 0 0 1440 878 + + Module + PBXClassBrowserModule + Proportion + 332pt + + + Proportion + 332pt + + + Name + Class Browser + ServiceClasses + + PBXClassBrowserModule + + StatusbarIsVisible + 0 + TableOfContents + + 1C0AD2AF069F1E9B00FABCE6 + 1C0AD2B0069F1E9B00FABCE6 + 1CA6456E063B45B4001379D8 + + ToolbarConfiguration + xcode.toolbar.config.classbrowser + WindowString + 385 179 630 352 0 0 1440 878 + WindowToolGUID + 1C0AD2AF069F1E9B00FABCE6 + WindowToolIsVisible + 0 + + + + diff --git a/me/cocoa/dupeguru.xcodeproj/project.pbxproj b/me/cocoa/dupeguru.xcodeproj/project.pbxproj new file mode 100644 index 00000000..da9f71bb --- /dev/null +++ b/me/cocoa/dupeguru.xcodeproj/project.pbxproj @@ -0,0 +1,563 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 44; + objects = { + +/* Begin PBXAppleScriptBuildPhase section */ + CE6B288A0AFB7FC900508D93 /* AppleScript */ = { + isa = PBXAppleScriptBuildPhase; + buildActionMask = 2147483647; + contextName = ""; + files = ( + ); + isSharedContext = 0; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXAppleScriptBuildPhase section */ + +/* Begin PBXBuildFile section */ + 8D11072A0486CEB800E47090 /* MainMenu.nib in Resources */ = {isa = PBXBuildFile; fileRef = 29B97318FDCFA39411CA2CEA /* MainMenu.nib */; }; + 8D11072B0486CEB800E47090 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 089C165CFE840E0CC02AAC07 /* InfoPlist.strings */; }; + 8D11072D0486CEB800E47090 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 29B97316FDCFA39411CA2CEA /* main.m */; settings = {ATTRIBUTES = (); }; }; + 8D11072F0486CEB800E47090 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */; }; + CE073F6309CAE1A3005C1D2F /* dupeguru_me_help in Resources */ = {isa = PBXBuildFile; fileRef = CE073F5409CAE1A3005C1D2F /* dupeguru_me_help */; }; + CE12149E0AC86DB900E93983 /* dg.xsl in Resources */ = {isa = PBXBuildFile; fileRef = CE12149C0AC86DB900E93983 /* dg.xsl */; }; + CE12149F0AC86DB900E93983 /* hardcoded.css in Resources */ = {isa = PBXBuildFile; fileRef = CE12149D0AC86DB900E93983 /* hardcoded.css */; }; + CE1425890AFB718500BD5167 /* Sparkle.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE1425880AFB718500BD5167 /* Sparkle.framework */; }; + CE14259F0AFB719300BD5167 /* Sparkle.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = CE1425880AFB718500BD5167 /* Sparkle.framework */; }; + CE381C9609914ACE003581CE /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = CE381C9409914ACE003581CE /* AppDelegate.m */; }; + CE381C9C09914ADF003581CE /* ResultWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = CE381C9A09914ADF003581CE /* ResultWindow.m */; }; + CE381D0509915304003581CE /* dg_cocoa.plugin in Resources */ = {isa = PBXBuildFile; fileRef = CE381CF509915304003581CE /* dg_cocoa.plugin */; }; + CE3AA46709DB207900DB3A21 /* Directories.nib in Resources */ = {isa = PBXBuildFile; fileRef = CE3AA46509DB207900DB3A21 /* Directories.nib */; }; + CE515DF30FC6C12E00EC695D /* Dialogs.m in Sources */ = {isa = PBXBuildFile; fileRef = CE515DE10FC6C12E00EC695D /* Dialogs.m */; }; + CE515DF40FC6C12E00EC695D /* HSErrorReportWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = CE515DE30FC6C12E00EC695D /* HSErrorReportWindow.m */; }; + CE515DF50FC6C12E00EC695D /* Outline.m in Sources */ = {isa = PBXBuildFile; fileRef = CE515DE50FC6C12E00EC695D /* Outline.m */; }; + CE515DF60FC6C12E00EC695D /* ProgressController.m in Sources */ = {isa = PBXBuildFile; fileRef = CE515DE70FC6C12E00EC695D /* ProgressController.m */; }; + CE515DF70FC6C12E00EC695D /* RecentDirectories.m in Sources */ = {isa = PBXBuildFile; fileRef = CE515DEA0FC6C12E00EC695D /* RecentDirectories.m */; }; + CE515DF80FC6C12E00EC695D /* RegistrationInterface.m in Sources */ = {isa = PBXBuildFile; fileRef = CE515DEC0FC6C12E00EC695D /* RegistrationInterface.m */; }; + CE515DF90FC6C12E00EC695D /* Table.m in Sources */ = {isa = PBXBuildFile; fileRef = CE515DEE0FC6C12E00EC695D /* Table.m */; }; + CE515DFA0FC6C12E00EC695D /* Utils.m in Sources */ = {isa = PBXBuildFile; fileRef = CE515DF00FC6C12E00EC695D /* Utils.m */; }; + CE515DFB0FC6C12E00EC695D /* ValueTransformers.m in Sources */ = {isa = PBXBuildFile; fileRef = CE515DF20FC6C12E00EC695D /* ValueTransformers.m */; }; + CE515E020FC6C13E00EC695D /* ErrorReportWindow.xib in Resources */ = {isa = PBXBuildFile; fileRef = CE515DFC0FC6C13E00EC695D /* ErrorReportWindow.xib */; }; + CE515E030FC6C13E00EC695D /* progress.nib in Resources */ = {isa = PBXBuildFile; fileRef = CE515DFE0FC6C13E00EC695D /* progress.nib */; }; + CE515E040FC6C13E00EC695D /* registration.nib in Resources */ = {isa = PBXBuildFile; fileRef = CE515E000FC6C13E00EC695D /* registration.nib */; }; + CE515E1D0FC6C19300EC695D /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = CE515E160FC6C19300EC695D /* AppDelegate.m */; }; + CE515E1E0FC6C19300EC695D /* DirectoryPanel.m in Sources */ = {isa = PBXBuildFile; fileRef = CE515E190FC6C19300EC695D /* DirectoryPanel.m */; }; + CE515E1F0FC6C19300EC695D /* ResultWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = CE515E1C0FC6C19300EC695D /* ResultWindow.m */; }; + CE68EE6809ABC48000971085 /* DirectoryPanel.m in Sources */ = {isa = PBXBuildFile; fileRef = CE68EE6609ABC48000971085 /* DirectoryPanel.m */; }; + CE848A1909DD85810004CB44 /* Consts.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = CE848A1809DD85810004CB44 /* Consts.h */; }; + CECA899909DB12CA00A3D774 /* Details.nib in Resources */ = {isa = PBXBuildFile; fileRef = CECA899709DB12CA00A3D774 /* Details.nib */; }; + CECA899C09DB132E00A3D774 /* DetailsPanel.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = CECA899A09DB132E00A3D774 /* DetailsPanel.h */; }; + CECA899D09DB132E00A3D774 /* DetailsPanel.m in Sources */ = {isa = PBXBuildFile; fileRef = CECA899B09DB132E00A3D774 /* DetailsPanel.m */; }; + CED2A6880A05102700AC4C3F /* power_marker32.png in Resources */ = {isa = PBXBuildFile; fileRef = CED2A6870A05102600AC4C3F /* power_marker32.png */; }; + CED2A6970A05128900AC4C3F /* dgme_logo32.png in Resources */ = {isa = PBXBuildFile; fileRef = CED2A6960A05128900AC4C3F /* dgme_logo32.png */; }; + CEEB135209C837A2004D2330 /* dupeguru.icns in Resources */ = {isa = PBXBuildFile; fileRef = CEEB135109C837A2004D2330 /* dupeguru.icns */; }; + CEF7823809C8AA0200EF38FF /* gear.png in Resources */ = {isa = PBXBuildFile; fileRef = CEF7823709C8AA0200EF38FF /* gear.png */; }; + CEFC294609C89E3D00D9F998 /* folder32.png in Resources */ = {isa = PBXBuildFile; fileRef = CEFC294509C89E3D00D9F998 /* folder32.png */; }; + CEFC295509C89FF200D9F998 /* details32.png in Resources */ = {isa = PBXBuildFile; fileRef = CEFC295309C89FF200D9F998 /* details32.png */; }; + CEFC295609C89FF200D9F998 /* preferences32.png in Resources */ = {isa = PBXBuildFile; fileRef = CEFC295409C89FF200D9F998 /* preferences32.png */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + CECC02B709A36E8200CC0A94 /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + CE14259F0AFB719300BD5167 /* Sparkle.framework in CopyFiles */, + CECA899C09DB132E00A3D774 /* DetailsPanel.h in CopyFiles */, + CE848A1909DD85810004CB44 /* Consts.h in CopyFiles */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 089C165DFE840E0CC02AAC07 /* English */ = {isa = PBXFileReference; fileEncoding = 10; lastKnownFileType = text.plist.strings; name = English; path = English.lproj/InfoPlist.strings; sourceTree = ""; }; + 1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = /System/Library/Frameworks/Cocoa.framework; sourceTree = ""; }; + 13E42FB307B3F0F600E4EEF1 /* CoreData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreData.framework; path = /System/Library/Frameworks/CoreData.framework; sourceTree = ""; }; + 29B97316FDCFA39411CA2CEA /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = SOURCE_ROOT; }; + 29B97319FDCFA39411CA2CEA /* English */ = {isa = PBXFileReference; lastKnownFileType = wrapper.nib; name = English; path = English.lproj/MainMenu.nib; sourceTree = ""; }; + 29B97324FDCFA39411CA2CEA /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = /System/Library/Frameworks/AppKit.framework; sourceTree = ""; }; + 29B97325FDCFA39411CA2CEA /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = /System/Library/Frameworks/Foundation.framework; sourceTree = ""; }; + 8D1107310486CEB800E47090 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = SOURCE_ROOT; }; + 8D1107320486CEB800E47090 /* dupeGuru ME.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "dupeGuru ME.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + CE073F5409CAE1A3005C1D2F /* dupeguru_me_help */ = {isa = PBXFileReference; lastKnownFileType = folder; name = dupeguru_me_help; path = help/dupeguru_me_help; sourceTree = ""; }; + CE12149C0AC86DB900E93983 /* dg.xsl */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = text.xml; name = dg.xsl; path = w3/dg.xsl; sourceTree = SOURCE_ROOT; }; + CE12149D0AC86DB900E93983 /* hardcoded.css */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = text; name = hardcoded.css; path = w3/hardcoded.css; sourceTree = SOURCE_ROOT; }; + CE1425880AFB718500BD5167 /* Sparkle.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Sparkle.framework; path = /Library/Frameworks/Sparkle.framework; sourceTree = ""; }; + CE381C9409914ACE003581CE /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = SOURCE_ROOT; }; + CE381C9509914ACE003581CE /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = SOURCE_ROOT; }; + CE381C9A09914ADF003581CE /* ResultWindow.m */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.objc; path = ResultWindow.m; sourceTree = SOURCE_ROOT; }; + CE381C9B09914ADF003581CE /* ResultWindow.h */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.h; path = ResultWindow.h; sourceTree = SOURCE_ROOT; }; + CE381CF509915304003581CE /* dg_cocoa.plugin */ = {isa = PBXFileReference; lastKnownFileType = folder; name = dg_cocoa.plugin; path = py/dist/dg_cocoa.plugin; sourceTree = SOURCE_ROOT; }; + CE3AA46609DB207900DB3A21 /* English */ = {isa = PBXFileReference; lastKnownFileType = wrapper.nib; name = English; path = English.lproj/Directories.nib; sourceTree = ""; }; + CE515DE00FC6C12E00EC695D /* Dialogs.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Dialogs.h; path = cocoalib/Dialogs.h; sourceTree = SOURCE_ROOT; }; + CE515DE10FC6C12E00EC695D /* Dialogs.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Dialogs.m; path = cocoalib/Dialogs.m; sourceTree = SOURCE_ROOT; }; + CE515DE20FC6C12E00EC695D /* HSErrorReportWindow.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = HSErrorReportWindow.h; path = cocoalib/HSErrorReportWindow.h; sourceTree = SOURCE_ROOT; }; + CE515DE30FC6C12E00EC695D /* HSErrorReportWindow.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = HSErrorReportWindow.m; path = cocoalib/HSErrorReportWindow.m; sourceTree = SOURCE_ROOT; }; + CE515DE40FC6C12E00EC695D /* Outline.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Outline.h; path = cocoalib/Outline.h; sourceTree = SOURCE_ROOT; }; + CE515DE50FC6C12E00EC695D /* Outline.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Outline.m; path = cocoalib/Outline.m; sourceTree = SOURCE_ROOT; }; + CE515DE60FC6C12E00EC695D /* ProgressController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ProgressController.h; path = cocoalib/ProgressController.h; sourceTree = SOURCE_ROOT; }; + CE515DE70FC6C12E00EC695D /* ProgressController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ProgressController.m; path = cocoalib/ProgressController.m; sourceTree = SOURCE_ROOT; }; + CE515DE80FC6C12E00EC695D /* PyApp.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PyApp.h; path = cocoalib/PyApp.h; sourceTree = SOURCE_ROOT; }; + CE515DE90FC6C12E00EC695D /* RecentDirectories.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RecentDirectories.h; path = cocoalib/RecentDirectories.h; sourceTree = SOURCE_ROOT; }; + CE515DEA0FC6C12E00EC695D /* RecentDirectories.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RecentDirectories.m; path = cocoalib/RecentDirectories.m; sourceTree = SOURCE_ROOT; }; + CE515DEB0FC6C12E00EC695D /* RegistrationInterface.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RegistrationInterface.h; path = cocoalib/RegistrationInterface.h; sourceTree = SOURCE_ROOT; }; + CE515DEC0FC6C12E00EC695D /* RegistrationInterface.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RegistrationInterface.m; path = cocoalib/RegistrationInterface.m; sourceTree = SOURCE_ROOT; }; + CE515DED0FC6C12E00EC695D /* Table.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Table.h; path = cocoalib/Table.h; sourceTree = SOURCE_ROOT; }; + CE515DEE0FC6C12E00EC695D /* Table.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Table.m; path = cocoalib/Table.m; sourceTree = SOURCE_ROOT; }; + CE515DEF0FC6C12E00EC695D /* Utils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Utils.h; path = cocoalib/Utils.h; sourceTree = SOURCE_ROOT; }; + CE515DF00FC6C12E00EC695D /* Utils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Utils.m; path = cocoalib/Utils.m; sourceTree = SOURCE_ROOT; }; + CE515DF10FC6C12E00EC695D /* ValueTransformers.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ValueTransformers.h; path = cocoalib/ValueTransformers.h; sourceTree = SOURCE_ROOT; }; + CE515DF20FC6C12E00EC695D /* ValueTransformers.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ValueTransformers.m; path = cocoalib/ValueTransformers.m; sourceTree = SOURCE_ROOT; }; + CE515DFD0FC6C13E00EC695D /* English */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = English; path = cocoalib/English.lproj/ErrorReportWindow.xib; sourceTree = ""; }; + CE515DFF0FC6C13E00EC695D /* English */ = {isa = PBXFileReference; lastKnownFileType = wrapper.nib; name = English; path = cocoalib/English.lproj/progress.nib; sourceTree = ""; }; + CE515E010FC6C13E00EC695D /* English */ = {isa = PBXFileReference; lastKnownFileType = wrapper.nib; name = English; path = cocoalib/English.lproj/registration.nib; sourceTree = ""; }; + CE515E150FC6C19300EC695D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = dgbase/AppDelegate.h; sourceTree = SOURCE_ROOT; }; + CE515E160FC6C19300EC695D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AppDelegate.m; path = dgbase/AppDelegate.m; sourceTree = SOURCE_ROOT; }; + CE515E170FC6C19300EC695D /* Consts.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Consts.h; path = dgbase/Consts.h; sourceTree = SOURCE_ROOT; }; + CE515E180FC6C19300EC695D /* DirectoryPanel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = DirectoryPanel.h; path = dgbase/DirectoryPanel.h; sourceTree = SOURCE_ROOT; }; + CE515E190FC6C19300EC695D /* DirectoryPanel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = DirectoryPanel.m; path = dgbase/DirectoryPanel.m; sourceTree = SOURCE_ROOT; }; + CE515E1A0FC6C19300EC695D /* PyDupeGuru.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PyDupeGuru.h; path = dgbase/PyDupeGuru.h; sourceTree = SOURCE_ROOT; }; + CE515E1B0FC6C19300EC695D /* ResultWindow.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ResultWindow.h; path = dgbase/ResultWindow.h; sourceTree = SOURCE_ROOT; }; + CE515E1C0FC6C19300EC695D /* ResultWindow.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ResultWindow.m; path = dgbase/ResultWindow.m; sourceTree = SOURCE_ROOT; }; + CE68EE6509ABC48000971085 /* DirectoryPanel.h */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.h; path = DirectoryPanel.h; sourceTree = SOURCE_ROOT; }; + CE68EE6609ABC48000971085 /* DirectoryPanel.m */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.objc; path = DirectoryPanel.m; sourceTree = SOURCE_ROOT; }; + CE848A1809DD85810004CB44 /* Consts.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Consts.h; sourceTree = ""; }; + CECA899809DB12CA00A3D774 /* English */ = {isa = PBXFileReference; lastKnownFileType = wrapper.nib; name = English; path = English.lproj/Details.nib; sourceTree = ""; }; + CECA899A09DB132E00A3D774 /* DetailsPanel.h */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.h; path = DetailsPanel.h; sourceTree = ""; }; + CECA899B09DB132E00A3D774 /* DetailsPanel.m */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.objc; path = DetailsPanel.m; sourceTree = ""; }; + CED2A6870A05102600AC4C3F /* power_marker32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = power_marker32.png; path = images/power_marker32.png; sourceTree = SOURCE_ROOT; }; + CED2A6960A05128900AC4C3F /* dgme_logo32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = dgme_logo32.png; path = images/dgme_logo32.png; sourceTree = SOURCE_ROOT; }; + CEEB135109C837A2004D2330 /* dupeguru.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; path = dupeguru.icns; sourceTree = ""; }; + CEF7823709C8AA0200EF38FF /* gear.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = gear.png; path = images/gear.png; sourceTree = ""; }; + CEFC294509C89E3D00D9F998 /* folder32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = folder32.png; path = images/folder32.png; sourceTree = SOURCE_ROOT; }; + CEFC295309C89FF200D9F998 /* details32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = details32.png; path = images/details32.png; sourceTree = SOURCE_ROOT; }; + CEFC295409C89FF200D9F998 /* preferences32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = preferences32.png; path = images/preferences32.png; sourceTree = SOURCE_ROOT; }; + CEFF18A009A4D387005E6321 /* PyDupeGuru.h */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.h; path = PyDupeGuru.h; sourceTree = SOURCE_ROOT; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 8D11072E0486CEB800E47090 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 8D11072F0486CEB800E47090 /* Cocoa.framework in Frameworks */, + CE1425890AFB718500BD5167 /* Sparkle.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 080E96DDFE201D6D7F000001 /* Classes */ = { + isa = PBXGroup; + children = ( + CE381C9509914ACE003581CE /* AppDelegate.h */, + CE381C9409914ACE003581CE /* AppDelegate.m */, + CE848A1809DD85810004CB44 /* Consts.h */, + CECA899A09DB132E00A3D774 /* DetailsPanel.h */, + CECA899B09DB132E00A3D774 /* DetailsPanel.m */, + CE68EE6509ABC48000971085 /* DirectoryPanel.h */, + CE68EE6609ABC48000971085 /* DirectoryPanel.m */, + CEFF18A009A4D387005E6321 /* PyDupeGuru.h */, + CE381C9B09914ADF003581CE /* ResultWindow.h */, + CE381C9A09914ADF003581CE /* ResultWindow.m */, + ); + name = Classes; + sourceTree = ""; + }; + 1058C7A0FEA54F0111CA2CBB /* Linked Frameworks */ = { + isa = PBXGroup; + children = ( + CE1425880AFB718500BD5167 /* Sparkle.framework */, + 1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */, + ); + name = "Linked Frameworks"; + sourceTree = ""; + }; + 1058C7A2FEA54F0111CA2CBB /* Other Frameworks */ = { + isa = PBXGroup; + children = ( + 29B97324FDCFA39411CA2CEA /* AppKit.framework */, + 13E42FB307B3F0F600E4EEF1 /* CoreData.framework */, + 29B97325FDCFA39411CA2CEA /* Foundation.framework */, + ); + name = "Other Frameworks"; + sourceTree = ""; + }; + 19C28FACFE9D520D11CA2CBB /* Products */ = { + isa = PBXGroup; + children = ( + 8D1107320486CEB800E47090 /* dupeGuru ME.app */, + ); + name = Products; + sourceTree = ""; + }; + 29B97314FDCFA39411CA2CEA /* dupeguru */ = { + isa = PBXGroup; + children = ( + 080E96DDFE201D6D7F000001 /* Classes */, + CE515E140FC6C17900EC695D /* dgbase */, + CE515DDD0FC6C09400EC695D /* cocoalib */, + 29B97315FDCFA39411CA2CEA /* Other Sources */, + 29B97317FDCFA39411CA2CEA /* Resources */, + 29B97323FDCFA39411CA2CEA /* Frameworks */, + 19C28FACFE9D520D11CA2CBB /* Products */, + ); + name = dupeguru; + sourceTree = ""; + }; + 29B97315FDCFA39411CA2CEA /* Other Sources */ = { + isa = PBXGroup; + children = ( + 29B97316FDCFA39411CA2CEA /* main.m */, + ); + name = "Other Sources"; + sourceTree = ""; + }; + 29B97317FDCFA39411CA2CEA /* Resources */ = { + isa = PBXGroup; + children = ( + CE073F5409CAE1A3005C1D2F /* dupeguru_me_help */, + CE381CF509915304003581CE /* dg_cocoa.plugin */, + CEFC294309C89E0000D9F998 /* images */, + CE12149B0AC86DB900E93983 /* w3 */, + CEEB135109C837A2004D2330 /* dupeguru.icns */, + 8D1107310486CEB800E47090 /* Info.plist */, + 089C165CFE840E0CC02AAC07 /* InfoPlist.strings */, + CECA899709DB12CA00A3D774 /* Details.nib */, + CE3AA46509DB207900DB3A21 /* Directories.nib */, + 29B97318FDCFA39411CA2CEA /* MainMenu.nib */, + ); + name = Resources; + sourceTree = ""; + }; + 29B97323FDCFA39411CA2CEA /* Frameworks */ = { + isa = PBXGroup; + children = ( + 1058C7A0FEA54F0111CA2CBB /* Linked Frameworks */, + 1058C7A2FEA54F0111CA2CBB /* Other Frameworks */, + ); + name = Frameworks; + sourceTree = ""; + }; + CE12149B0AC86DB900E93983 /* w3 */ = { + isa = PBXGroup; + children = ( + CE12149C0AC86DB900E93983 /* dg.xsl */, + CE12149D0AC86DB900E93983 /* hardcoded.css */, + ); + path = w3; + sourceTree = SOURCE_ROOT; + }; + CE515DDD0FC6C09400EC695D /* cocoalib */ = { + isa = PBXGroup; + children = ( + CE515DFC0FC6C13E00EC695D /* ErrorReportWindow.xib */, + CE515DFE0FC6C13E00EC695D /* progress.nib */, + CE515E000FC6C13E00EC695D /* registration.nib */, + CE515DE00FC6C12E00EC695D /* Dialogs.h */, + CE515DE10FC6C12E00EC695D /* Dialogs.m */, + CE515DE20FC6C12E00EC695D /* HSErrorReportWindow.h */, + CE515DE30FC6C12E00EC695D /* HSErrorReportWindow.m */, + CE515DE40FC6C12E00EC695D /* Outline.h */, + CE515DE50FC6C12E00EC695D /* Outline.m */, + CE515DE60FC6C12E00EC695D /* ProgressController.h */, + CE515DE70FC6C12E00EC695D /* ProgressController.m */, + CE515DE80FC6C12E00EC695D /* PyApp.h */, + CE515DE90FC6C12E00EC695D /* RecentDirectories.h */, + CE515DEA0FC6C12E00EC695D /* RecentDirectories.m */, + CE515DEB0FC6C12E00EC695D /* RegistrationInterface.h */, + CE515DEC0FC6C12E00EC695D /* RegistrationInterface.m */, + CE515DED0FC6C12E00EC695D /* Table.h */, + CE515DEE0FC6C12E00EC695D /* Table.m */, + CE515DEF0FC6C12E00EC695D /* Utils.h */, + CE515DF00FC6C12E00EC695D /* Utils.m */, + CE515DF10FC6C12E00EC695D /* ValueTransformers.h */, + CE515DF20FC6C12E00EC695D /* ValueTransformers.m */, + ); + name = cocoalib; + sourceTree = ""; + }; + CE515E140FC6C17900EC695D /* dgbase */ = { + isa = PBXGroup; + children = ( + CE515E150FC6C19300EC695D /* AppDelegate.h */, + CE515E160FC6C19300EC695D /* AppDelegate.m */, + CE515E170FC6C19300EC695D /* Consts.h */, + CE515E180FC6C19300EC695D /* DirectoryPanel.h */, + CE515E190FC6C19300EC695D /* DirectoryPanel.m */, + CE515E1A0FC6C19300EC695D /* PyDupeGuru.h */, + CE515E1B0FC6C19300EC695D /* ResultWindow.h */, + CE515E1C0FC6C19300EC695D /* ResultWindow.m */, + ); + name = dgbase; + sourceTree = ""; + }; + CEFC294309C89E0000D9F998 /* images */ = { + isa = PBXGroup; + children = ( + CED2A6960A05128900AC4C3F /* dgme_logo32.png */, + CED2A6870A05102600AC4C3F /* power_marker32.png */, + CEF7823709C8AA0200EF38FF /* gear.png */, + CEFC295309C89FF200D9F998 /* details32.png */, + CEFC295409C89FF200D9F998 /* preferences32.png */, + CEFC294509C89E3D00D9F998 /* folder32.png */, + ); + name = images; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 8D1107260486CEB800E47090 /* dupeguru */ = { + isa = PBXNativeTarget; + buildConfigurationList = C01FCF4A08A954540054247B /* Build configuration list for PBXNativeTarget "dupeguru" */; + buildPhases = ( + 8D1107290486CEB800E47090 /* Resources */, + 8D11072C0486CEB800E47090 /* Sources */, + 8D11072E0486CEB800E47090 /* Frameworks */, + CECC02B709A36E8200CC0A94 /* CopyFiles */, + CE6B288A0AFB7FC900508D93 /* AppleScript */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = dupeguru; + productInstallPath = "$(HOME)/Applications"; + productName = dupeguru; + productReference = 8D1107320486CEB800E47090 /* dupeGuru ME.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 29B97313FDCFA39411CA2CEA /* Project object */ = { + isa = PBXProject; + buildConfigurationList = C01FCF4E08A954540054247B /* Build configuration list for PBXProject "dupeguru" */; + compatibilityVersion = "Xcode 3.0"; + hasScannedForEncodings = 1; + mainGroup = 29B97314FDCFA39411CA2CEA /* dupeguru */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 8D1107260486CEB800E47090 /* dupeguru */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 8D1107290486CEB800E47090 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 8D11072A0486CEB800E47090 /* MainMenu.nib in Resources */, + 8D11072B0486CEB800E47090 /* InfoPlist.strings in Resources */, + CE381D0509915304003581CE /* dg_cocoa.plugin in Resources */, + CE073F6309CAE1A3005C1D2F /* dupeguru_me_help in Resources */, + CEEB135209C837A2004D2330 /* dupeguru.icns in Resources */, + CEFC294609C89E3D00D9F998 /* folder32.png in Resources */, + CEFC295509C89FF200D9F998 /* details32.png in Resources */, + CEFC295609C89FF200D9F998 /* preferences32.png in Resources */, + CEF7823809C8AA0200EF38FF /* gear.png in Resources */, + CECA899909DB12CA00A3D774 /* Details.nib in Resources */, + CE3AA46709DB207900DB3A21 /* Directories.nib in Resources */, + CED2A6880A05102700AC4C3F /* power_marker32.png in Resources */, + CED2A6970A05128900AC4C3F /* dgme_logo32.png in Resources */, + CE12149E0AC86DB900E93983 /* dg.xsl in Resources */, + CE12149F0AC86DB900E93983 /* hardcoded.css in Resources */, + CE515E020FC6C13E00EC695D /* ErrorReportWindow.xib in Resources */, + CE515E030FC6C13E00EC695D /* progress.nib in Resources */, + CE515E040FC6C13E00EC695D /* registration.nib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 8D11072C0486CEB800E47090 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 8D11072D0486CEB800E47090 /* main.m in Sources */, + CE381C9609914ACE003581CE /* AppDelegate.m in Sources */, + CE381C9C09914ADF003581CE /* ResultWindow.m in Sources */, + CE68EE6809ABC48000971085 /* DirectoryPanel.m in Sources */, + CECA899D09DB132E00A3D774 /* DetailsPanel.m in Sources */, + CE515DF30FC6C12E00EC695D /* Dialogs.m in Sources */, + CE515DF40FC6C12E00EC695D /* HSErrorReportWindow.m in Sources */, + CE515DF50FC6C12E00EC695D /* Outline.m in Sources */, + CE515DF60FC6C12E00EC695D /* ProgressController.m in Sources */, + CE515DF70FC6C12E00EC695D /* RecentDirectories.m in Sources */, + CE515DF80FC6C12E00EC695D /* RegistrationInterface.m in Sources */, + CE515DF90FC6C12E00EC695D /* Table.m in Sources */, + CE515DFA0FC6C12E00EC695D /* Utils.m in Sources */, + CE515DFB0FC6C12E00EC695D /* ValueTransformers.m in Sources */, + CE515E1D0FC6C19300EC695D /* AppDelegate.m in Sources */, + CE515E1E0FC6C19300EC695D /* DirectoryPanel.m in Sources */, + CE515E1F0FC6C19300EC695D /* ResultWindow.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 089C165CFE840E0CC02AAC07 /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + 089C165DFE840E0CC02AAC07 /* English */, + ); + name = InfoPlist.strings; + sourceTree = ""; + }; + 29B97318FDCFA39411CA2CEA /* MainMenu.nib */ = { + isa = PBXVariantGroup; + children = ( + 29B97319FDCFA39411CA2CEA /* English */, + ); + name = MainMenu.nib; + sourceTree = SOURCE_ROOT; + }; + CE3AA46509DB207900DB3A21 /* Directories.nib */ = { + isa = PBXVariantGroup; + children = ( + CE3AA46609DB207900DB3A21 /* English */, + ); + name = Directories.nib; + sourceTree = ""; + }; + CE515DFC0FC6C13E00EC695D /* ErrorReportWindow.xib */ = { + isa = PBXVariantGroup; + children = ( + CE515DFD0FC6C13E00EC695D /* English */, + ); + name = ErrorReportWindow.xib; + sourceTree = SOURCE_ROOT; + }; + CE515DFE0FC6C13E00EC695D /* progress.nib */ = { + isa = PBXVariantGroup; + children = ( + CE515DFF0FC6C13E00EC695D /* English */, + ); + name = progress.nib; + sourceTree = SOURCE_ROOT; + }; + CE515E000FC6C13E00EC695D /* registration.nib */ = { + isa = PBXVariantGroup; + children = ( + CE515E010FC6C13E00EC695D /* English */, + ); + name = registration.nib; + sourceTree = SOURCE_ROOT; + }; + CECA899709DB12CA00A3D774 /* Details.nib */ = { + isa = PBXVariantGroup; + children = ( + CECA899809DB12CA00A3D774 /* English */, + ); + name = Details.nib; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + C01FCF4B08A954540054247B /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + COPY_PHASE_STRIP = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(FRAMEWORK_SEARCH_PATHS)", + "$(SRCROOT)/../../../cocoalib/build/Release", + "\"$(SRCROOT)/../../base/cocoa/build/Release\"", + ); + GCC_DYNAMIC_NO_PIC = NO; + GCC_ENABLE_FIX_AND_CONTINUE = YES; + GCC_MODEL_TUNING = G5; + GCC_OPTIMIZATION_LEVEL = 0; + INFOPLIST_FILE = Info.plist; + INSTALL_PATH = "$(HOME)/Applications"; + PRODUCT_NAME = dupeGuru; + WRAPPER_EXTENSION = app; + ZERO_LINK = YES; + }; + name = Debug; + }; + C01FCF4C08A954540054247B /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + FRAMEWORK_SEARCH_PATHS = ( + "$(FRAMEWORK_SEARCH_PATHS)", + "$(SRCROOT)/../../../cocoalib/build/Release", + "\"$(SRCROOT)/../../base/cocoa/build/Release\"", + ); + GCC_GENERATE_DEBUGGING_SYMBOLS = NO; + GCC_MODEL_TUNING = G5; + INFOPLIST_FILE = Info.plist; + INSTALL_PATH = "$(HOME)/Applications"; + PRODUCT_NAME = "dupeGuru ME"; + WRAPPER_EXTENSION = app; + }; + name = Release; + }; + C01FCF4F08A954540054247B /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + GCC_C_LANGUAGE_STANDARD = c99; + GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.4; + PREBINDING = NO; + SDKROOT = "$(DEVELOPER_SDK_DIR)/MacOSX10.4u.sdk"; + }; + name = Debug; + }; + C01FCF5008A954540054247B /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = ( + i386, + ppc, + ); + COPY_PHASE_STRIP = NO; + GCC_C_LANGUAGE_STANDARD = c99; + GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.4; + PREBINDING = NO; + SDKROOT = "$(DEVELOPER_SDK_DIR)/MacOSX10.4u.sdk"; + STRIP_INSTALLED_PRODUCT = NO; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + C01FCF4A08A954540054247B /* Build configuration list for PBXNativeTarget "dupeguru" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + C01FCF4B08A954540054247B /* Debug */, + C01FCF4C08A954540054247B /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + C01FCF4E08A954540054247B /* Build configuration list for PBXProject "dupeguru" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + C01FCF4F08A954540054247B /* Debug */, + C01FCF5008A954540054247B /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 29B97313FDCFA39411CA2CEA /* Project object */; +} diff --git a/me/cocoa/gen.py b/me/cocoa/gen.py new file mode 100644 index 00000000..45ae1e20 --- /dev/null +++ b/me/cocoa/gen.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python + +import os + +print "Generating help" +os.chdir('help') +os.system('python -u gen.py') +os.system('/Developer/Applications/Utilities/Help\\ Indexer.app/Contents/MacOS/Help\\ Indexer dupeguru_me_help') +os.chdir('..') + +print "Generating py plugin" +os.chdir('py') +os.system('python -u gen.py') +os.chdir('..') \ No newline at end of file diff --git a/me/cocoa/main.m b/me/cocoa/main.m new file mode 100644 index 00000000..c5f30658 --- /dev/null +++ b/me/cocoa/main.m @@ -0,0 +1,21 @@ +// +// main.m +// dupeguru +// +// Created by Virgil Dupras on 2006/02/01. +// Copyright __MyCompanyName__ 2006. All rights reserved. +// + +#import + +int main(int argc, char *argv[]) +{ + NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; + NSString *pluginPath = [[NSBundle mainBundle] + pathForResource:@"dg_cocoa" + ofType:@"plugin"]; + NSBundle *pluginBundle = [NSBundle bundleWithPath:pluginPath]; + [pluginBundle load]; + [pool release]; + return NSApplicationMain(argc, (const char **) argv); +} diff --git a/me/cocoa/py/dg_cocoa.py b/me/cocoa/py/dg_cocoa.py new file mode 100644 index 00000000..53413c71 --- /dev/null +++ b/me/cocoa/py/dg_cocoa.py @@ -0,0 +1,225 @@ +#!/usr/bin/env python +import objc +from AppKit import * + +from dupeguru import app_me_cocoa, scanner + +# Fix py2app imports which chokes on relative imports +from dupeguru import app, app_cocoa, data, directories, engine, export, ignore, results, scanner +from hsfs import auto, manual, stats, tree, utils, music +from hsfs.phys import music +from hsmedia import aiff, flac, genres, id3v1, id3v2, mp4, mpeg, ogg, wma + +class PyApp(NSObject): + pass #fake class + +class PyDupeGuru(PyApp): + def init(self): + self = super(PyDupeGuru,self).init() + self.app = app_me_cocoa.DupeGuruME() + return self + + #---Directories + def addDirectory_(self,directory): + return self.app.AddDirectory(directory) + + def removeDirectory_(self,index): + self.app.RemoveDirectory(index) + + def setDirectory_state_(self,node_path,state): + self.app.SetDirectoryState(node_path,state) + + #---Results + def clearIgnoreList(self): + self.app.scanner.ignore_list.Clear() + + def doScan(self): + return self.app.start_scanning() + + def exportToXHTMLwithColumns_xslt_css_(self,column_ids,xslt_path,css_path): + return self.app.ExportToXHTML(column_ids,xslt_path,css_path) + + def loadIgnoreList(self): + self.app.LoadIgnoreList() + + def loadResults(self): + self.app.load() + + def markAll(self): + self.app.results.mark_all() + + def markNone(self): + self.app.results.mark_none() + + def markInvert(self): + self.app.results.mark_invert() + + def purgeIgnoreList(self): + self.app.PurgeIgnoreList() + + def toggleSelectedMark(self): + self.app.ToggleSelectedMarkState() + + def saveIgnoreList(self): + self.app.SaveIgnoreList() + + def saveResults(self): + self.app.Save() + + def refreshDetailsWithSelected(self): + self.app.RefreshDetailsWithSelected() + + def selectResultNodePaths_(self,node_paths): + self.app.SelectResultNodePaths(node_paths) + + def selectPowerMarkerNodePaths_(self,node_paths): + self.app.SelectPowerMarkerNodePaths(node_paths) + + #---Actions + def addSelectedToIgnoreList(self): + self.app.AddSelectedToIgnoreList() + + def applyFilter_(self, filter): + self.app.ApplyFilter(filter) + + def deleteMarked(self): + self.app.delete_marked() + + def makeSelectedReference(self): + self.app.MakeSelectedReference() + + def copyOrMove_markedTo_recreatePath_(self,copy,destination,recreate_path): + self.app.copy_or_move_marked(copy, destination, recreate_path) + + def openSelected(self): + self.app.OpenSelected() + + def removeDeadTracks(self): + self.app.remove_dead_tracks() + + def removeMarked(self): + self.app.results.perform_on_marked(lambda x:True, True) + + def removeSelected(self): + self.app.RemoveSelected() + + def renameSelected_(self,newname): + return self.app.RenameSelected(newname) + + def revealSelected(self): + self.app.RevealSelected() + + def scanDeadTracks(self): + self.app.scan_dead_tracks() + + #---Misc + def sortDupesBy_ascending_(self,key,asc): + self.app.sort_dupes(key,asc) + + def sortGroupsBy_ascending_(self,key,asc): + self.app.sort_groups(key,asc) + + #---Information + @objc.signature('i@:') + def deadTrackCount(self): + return len(self.app.dead_tracks) + + def getIgnoreListCount(self): + return len(self.app.scanner.ignore_list) + + def getMarkCount(self): + return self.app.results.mark_count + + def getStatLine(self): + return self.app.stat_line + + def getOperationalErrorCount(self): + return self.app.last_op_error_count + + #---Data + @objc.signature('i@:i') + def getOutlineViewMaxLevel_(self, tag): + return self.app.GetOutlineViewMaxLevel(tag) + + @objc.signature('@@:i@') + def getOutlineView_childCountsForPath_(self, tag, node_path): + return self.app.GetOutlineViewChildCounts(tag, node_path) + + def getOutlineView_valuesForIndexes_(self,tag,node_path): + return self.app.GetOutlineViewValues(tag,node_path) + + def getOutlineView_markedAtIndexes_(self,tag,node_path): + return self.app.GetOutlineViewMarked(tag,node_path) + + def getTableViewCount_(self,tag): + return self.app.GetTableViewCount(tag) + + def getTableViewMarkedIndexes_(self,tag): + return self.app.GetTableViewMarkedIndexes(tag) + + def getTableView_valuesForRow_(self,tag,row): + return self.app.GetTableViewValues(tag,row) + + #---Properties + def setMinMatchPercentage_(self, percentage): + self.app.scanner.min_match_percentage = int(percentage) + + def setScanType_(self, scan_type): + try: + self.app.scanner.scan_type = [ + scanner.SCAN_TYPE_FILENAME, + scanner.SCAN_TYPE_FIELDS, + scanner.SCAN_TYPE_FIELDS_NO_ORDER, + scanner.SCAN_TYPE_TAG, + scanner.SCAN_TYPE_CONTENT, + scanner.SCAN_TYPE_CONTENT_AUDIO + ][scan_type] + except IndexError: + pass + + def setWordWeighting_(self, words_are_weighted): + self.app.scanner.word_weighting = words_are_weighted + + def setMixFileKind_(self, mix_file_kind): + self.app.scanner.mix_file_kind = mix_file_kind + + def setDisplayDeltaValues_(self, display_delta_values): + self.app.display_delta_values = display_delta_values + + def setMatchSimilarWords_(self, match_similar_words): + self.app.scanner.match_similar_words = match_similar_words + + def setEscapeFilterRegexp_(self, escape_filter_regexp): + self.app.options['escape_filter_regexp'] = escape_filter_regexp + + def setRemoveEmptyFolders_(self, remove_empty_folders): + self.app.options['clean_empty_dirs'] = remove_empty_folders + + def enable_scanForTag_(self, enable, scan_tag): + if enable: + self.app.scanner.scanned_tags.add(scan_tag) + else: + self.app.scanner.scanned_tags.discard(scan_tag) + + #---Worker + def getJobProgress(self): + return self.app.progress.last_progress + + def getJobDesc(self): + return self.app.progress.last_desc + + def cancelJob(self): + self.app.progress.job_cancelled = True + + #---Registration + @objc.signature('i@:') + def isRegistered(self): + return self.app.registered + + @objc.signature('i@:@@') + def isCodeValid_withEmail_(self, code, email): + return self.app.is_code_valid(code, email) + + def setRegisteredCode_andEmail_(self, code, email): + self.app.set_registration(code, email) + diff --git a/me/cocoa/py/gen.py b/me/cocoa/py/gen.py new file mode 100644 index 00000000..6195927d --- /dev/null +++ b/me/cocoa/py/gen.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python + +import os +import os.path as op +import shutil + +from hsutil.build import print_and_do + +os.chdir('dupeguru') +print_and_do('python gen.py') +os.chdir('..') + +if op.exists('build'): + shutil.rmtree('build') +if op.exists('dist'): + shutil.rmtree('dist') + +print_and_do('python -u setup.py py2app') \ No newline at end of file diff --git a/me/cocoa/py/setup.py b/me/cocoa/py/setup.py new file mode 100644 index 00000000..af81b3ed --- /dev/null +++ b/me/cocoa/py/setup.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python + +from distutils.core import setup +import py2app + +from hsutil.build import move_testdata_out, put_testdata_back + +move_log = move_testdata_out() +try: + setup( + plugin = ['dg_cocoa.py'], + ) +finally: + put_testdata_back(move_log) diff --git a/me/cocoa/w3/dg.xsl b/me/cocoa/w3/dg.xsl new file mode 100644 index 00000000..4f982fce --- /dev/null +++ b/me/cocoa/w3/dg.xsl @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + indented + + + + + + + + + + + + + + + + + + + + + + + + + + + + dupeGuru Results + + + +

dupeGuru Results

+ + + + + +
+ + + + + \ No newline at end of file diff --git a/me/cocoa/w3/hardcoded.css b/me/cocoa/w3/hardcoded.css new file mode 100644 index 00000000..ed243bcc --- /dev/null +++ b/me/cocoa/w3/hardcoded.css @@ -0,0 +1,71 @@ +BODY +{ + background-color:white; +} + +BODY,A,P,UL,TABLE,TR,TD +{ + font-family:Tahoma,Arial,sans-serif; + font-size:10pt; + color: #4477AA; +} + +TABLE +{ + background-color: #225588; + margin-left: auto; + margin-right: auto; + width: 90%; +} + +TR +{ + background-color: white; +} + +TH +{ + font-weight: bold; + color: black; + background-color: #C8D6E5; +} + +TH TD +{ + color:black; +} + +TD +{ + padding-left: 2pt; +} + +TD.rightelem +{ + text-align:right; + /*padding-left:0pt;*/ + padding-right: 2pt; + width: 17%; +} + +TD.indented +{ + padding-left: 12pt; +} + +H1 +{ + font-family:"Courier New",monospace; + color:#6699CC; + font-size:18pt; + color:#6da500; + border-color: #70A0CF; + border-width: 1pt; + border-style: solid; + margin-top: 16pt; + margin-left: 5%; + margin-right: 5%; + padding-top: 2pt; + padding-bottom:2pt; + text-align: center; +} \ No newline at end of file diff --git a/me/help/changelog.yaml b/me/help/changelog.yaml new file mode 100644 index 00000000..db4644f2 --- /dev/null +++ b/me/help/changelog.yaml @@ -0,0 +1,542 @@ +- date: 2009-05-30 + version: 5.6.1 + description: | + * Fixed a bug causing a GUI freeze at the beginning of a scan with a lot of files. + * Fixed a bug that sometimes caused a crash when an action was cancelled, and then started again. +- date: 2009-05-23 + version: 5.6.0 + description: | + * Converted the Windows GUI to Qt. + * Improved the reliability of the scanning process. +- date: 2009-03-28 + version: 5.5.2 + description: | + * **Fixed** an occasional crash caused by permission issues. + * **Fixed** a bug where the "X discarded" notice would show a too large number of discarded duplicates. +- date: 2008-09-28 + version: 5.5.1 + description: | + * **Improved** support for AIFF files. + * **Improved** Remove Dead Tracks in iTunes for very large library (Mac OS X). +- date: 2008-09-10 + description: "
    \n\t\t\t\t\t\t
  • Added support for AIFF files.
  • \n\t\ + \t\t\t\t\t
  • Added a notice in the status bar when matches were discarded\ + \ during the scan.
  • \n\t\t\t\t\t\t
  • Improved duplicate prioritization\ + \ (smartly chooses which file you will keep).
  • \n\t\t\t\t\t\t
  • Improved\ + \ scan progress feedback.
  • \n\t\t\t\t\t\t
  • Improved responsiveness\ + \ of the user interface for certain actions.
  • \n\t\t
" + version: 5.5.0 +- date: 2008-08-07 + description: "
    \n\t\t\t\t\t\t
  • Improved the \"Remove Dead Tracks in\ + \ iTunes\" feature.
  • \n\t\t\t\t\t\t
  • Improved the speed of results\ + \ loading and saving.
  • \n\t\t\t\t\t\t
  • Fixed a crash sometimes occurring\ + \ during duplicate deletion.
  • \n\t\t
" + version: 5.4.3 +- date: 2008-06-20 + description: "
    \n\t\t\t\t\t\t
  • Improved unicode handling for filenames\ + \ and tags. dupeGuru ME will now find a lot more duplicates if your files have\ + \ non-ascii characters in it.
  • \n\t\t\t\t\t\t
  • Improved MPEG files\ + \ duration detection.
  • \n\t\t\t\t\t\t
  • Fixed \"Clear Ignore List\"\ + \ crash in Windows.
  • \n\t\t
" + version: 5.4.2 +- date: 2008-01-15 + description: "
    \n\t\t\t\t\t\t
  • Improved scan, delete and move speed\ + \ in situations where there were a lot of duplicates.
  • \n\t\t\t\t\t\t
  • Fixed\ + \ occasional crashes when moving a lot of files at once.
  • \n\t\t \ + \
" + version: 5.4.1 +- date: 2007-12-06 + description: "
    \n\t\t\t\t\t\t
  • Added customizable tag scans.
  • \n\t\ + \t\t\t\t\t
  • Improved the handling of low memory situations.
  • \n\t\t\ + \t\t\t\t
  • Improved the directory panel. The \"Remove\" button changes\ + \ to \"Put Back\" when an excluded directory is selected.
  • \n\t\t \ + \
" + version: 5.4.0 +- date: 2007-11-26 + description: "
    \n\t\t\t\t\t\t
  • Added the \"Remove empty folders\" option.
  • \n\ + \t\t\t\t\t\t
  • Fixed results load/save issues.
  • \n\t\t\t\t\t\t
  • Fixed\ + \ occasional status bar inaccuracies when the results are filtered.
  • \n\t\t\ + \
" + version: 5.3.2 +- date: 2007-08-12 + description: "
    \n\t\t\t\t\t\t
  • Fixed a crash with copy and move.
  • \n\ + \t\t
" + version: 5.3.1 +- date: 2007-07-01 + description: "
    \n\t\t\t\t\t\t
  • Added post scan filtering.
  • \n\t\t\ + \t\t\t\t
  • Fixed a small issue with AAC decoding.
  • \n\t\t\t\t\t\t
  • Fixed\ + \ issues with the rename feature under Windows
  • \n\t\t\t\t\t\t
  • Fixed\ + \ some user interface annoyances under Windows
  • \n\t\t
" + version: 5.3.0 +- date: 2007-03-31 + description: "
    \n\t\t\t\t\t\t
  • Fixed a crash sometimes happening while\ + \ loading results.
  • \n\t\t
" + version: 5.2.7 +- date: 2007-03-25 + description: "
    \n\t\t\t\t\t\t
  • Improved UI responsiveness (using threads)\ + \ under Mac OS X.
  • \n\t\t\t\t\t\t
  • Improved result load/save speed\ + \ and memory usage.
  • \n\t\t\t\t\t\t
  • Fixed a \"bad file descriptor\"\ + \ error occasionally popping up.
  • \n\t\t\t\t\t\t
  • Fixed a bug with\ + \ non-latin directory names.
  • \n\t\t\t\t\t\t
  • Fixed a column mixup\ + \ under Windows. The Artist column couldn't be shown.
  • \n\t\t\t\t\t\t
  • Fixed\ + \ a bug causing the sorting under Power Marker mode not to work under Mac OS X.
  • \n\ + \t\t
" + version: 5.2.6 +- date: 2007-02-14 + description: "
    \n\t\t\t\t\t\t
  • Added Re-orderable columns. In fact,\ + \ I re-added the feature which was lost in the C# conversion in 5.2.0 (Windows).
  • \n\ + \t\t\t\t\t\t
  • Changed the behavior of the scanning engine when setting\ + \ the hardness to 100. It will now only match files that have their words in the\ + \ same order.
  • \n\t\t\t\t\t\t
  • Fixed a bug with all the Delete/Move/Copy\ + \ actions with certain kinds of files.
  • \n\t\t
" + version: 5.2.5 +- date: 2007-01-10 + description: "
    \n\t\t\t\t\t\t
  • Fixed a bug with the Move action.
  • \n\ + \t\t\t\t\t\t
  • Fixed a \"ghosting\" bug. Dupes deleted by dupeGuru would\ + \ sometimes come back in subsequent scans (Windows).
  • \n\t\t\t\t\t\t
  • Fixed\ + \ a bug introduced in the last version that caused the status bar not to update\ + \ when dupes were marked (Windows).
  • \n\t\t
" + version: 5.2.4 +- date: 2007-01-04 + description: "
    \n\t\t\t\t\t\t
  • Fixed bugs sometimes making dupeGuru\ + \ crash when marking a dupe (Windows).
  • \n\t\t\t\t\t\t
  • Fixed some\ + \ minor visual glitches (Windows).
  • \n\t\t
" + version: 5.2.3 +- date: 2006-12-21 + description: "
    \n\t\t\t\t\t\t
  • Improved Id3v2.4 tags decoding to support\ + \ some malformed tags that iTunes sometimes produce.
  • \n\t\t\t\t\t\t
  • Improved\ + \ the rename file dialog to exclude the extension from the original selection\ + \ (so when you start typing your new filename, it doesn't overwrite it) (Windows).
  • \n\ + \t\t\t\t\t\t
  • Changed some menu key shortcuts that created conflicts\ + \ (Windows).
  • \n\t\t\t\t\t\t
  • Fixed a bug preventing files from \"\ + reference\" directories to be displayed in blue in the results (Windows).
  • \n\ + \t\t\t\t\t\t
  • Fixed a bug preventing some files to be sent to the recycle\ + \ bin (Windows).
  • \n\t\t\t\t\t\t
  • Fixed a bug with the \"Remove\"\ + \ button of the directories panel (Windows).
  • \n\t\t\t\t\t\t
  • Fixed\ + \ a bug in the packaging preventing certain Windows configurations to start dupeGuru\ + \ at all.
  • \n\t\t
" + version: 5.2.2 +- date: 2006-11-18 + description: "
    \n\t\t\t\t\t\t
  • Fixed a bug with directory states.
  • \n\ + \t\t
" + version: 5.2.1 +- date: 2006-11-17 + description: "
    \n\t\t\t\t\t\t
  • Changed the Windows interface. It is\ + \ now .NET based.
  • \n\t\t\t\t\t\t
  • Added an auto-update feature to\ + \ the windows version.
  • \n\t\t\t\t\t\t
  • Changed the way power marking\ + \ works. It is now a mode instead of a separate window.
  • \n\t\t\t\t\t\t
  • Removed\ + \ the min word length/count options. These came from Mp3 Filter, and just aren't\ + \ used anymore. Word weighting does pretty much the same job.
  • \n\t\t\t\t\t\ + \t
  • Fixed a bug sometimes making delete and move operations stall.
  • \n\ + \t\t
" + version: 5.2.0 +- date: 2006-11-03 + description: "
    \n\t\t\t\t\t\t
  • Added an auto-update feature in the Mac\ + \ OS X version (with Sparkle).
  • \n\t\t\t\t\t\t
  • Added a \"Remove Dead\ + \ Tracks in iTunes\" feature in the Mac OS X version.
  • \n\t\t\t\t\t\t
  • Improved\ + \ speed and memory usage of the scanning engine, especially when the scan results\ + \ in a lot of duplicates.
  • \n\t\t\t\t\t\t
  • Improved VBR mp3 support.
  • \n\ + \t\t\t\t\t\t
  • Fixed a bug preventing some duplicate reports to be created\ + \ correctly under Windows.
  • \n\t\t
" + version: 5.1.2 +- date: 2006-09-29 + description: "
    \n\t\t\t\t\t\t
  • Fixed a bug (no, not the same as in 5.1.0)\ + \ preventing some duplicates to be found, especially in huge collections.
  • \n\ + \t\t
" + version: 5.1.1 +- date: 2006-09-26 + description: "
    \n\t\t\t\t\t\t
  • Added XHTML export feature.
  • \n\t\t\ + \t\t\t\t
  • Fixed a bug preventing some duplicates to be found when using\ + \ the \"Filename - Fields (No Order)\" scan method.
  • \n\t\t
" + version: 5.1.0 +- date: 2006-08-30 + description: "
    \n\t\t\t\t\t\t
  • Added sticky columns.
  • \n\t\t\t\t\t\ + \t
  • Fixed an issue with file caching between scans.
  • \n\t\t\t\t\t\t\ +
  • Fixed an issue preventing some duplicates from being deleted/moved/copied.
  • \n\ + \t\t
" + version: 5.0.11 +- date: 2006-08-27 + description: "
    \n\t\t\t\t\t\t
  • Fixed an issue with ignore list and unicode.
  • \n\ + \t\t\t\t\t\t
  • Fixed an issue with file attribute fetching sometimes causing\ + \ dupeGuru ME to crash.
  • \n\t\t\t\t\t\t
  • Fixed an issue in the directories\ + \ panel under Windows.
  • \n\t\t
" + version: 5.0.10 +- date: 2006-08-17 + description: "
    \n\t\t\t\t\t\t
  • Fixed an issue in the duplicate seeking\ + \ engine preventing some duplicates to be found.
  • \n\t\t\t\t\t\t
  • (Yeah,\ + \ I'm in a bug fixing frenzy right now :) )
  • \n\t\t
" + version: 5.0.9 +- date: 2006-08-16 + description: "
    \n\t\t\t\t\t\t
  • Fixed an issue with the new track column\ + \ occasionally causing crash.
  • \n\t\t\t\t\t\t
  • Fixed an issue with\ + \ the handling of corrupted files that occasionally caused crash.
  • \n\t\t \ + \
" + version: 5.0.8 +- date: 2006-08-12 + description: "
    \n\t\t\t\t\t\t
  • Improved unicode support.
  • \n\t\t\t\ + \t\t\t
  • Improved the \"Reveal in Finder\" (\"Open Containing Folder\"\ + \ in Windows) feature so it selects the file in the folder it opens.
  • \n\t\t\ + \
" + version: 5.0.7 +- date: 2006-08-08 + description: "
    \n\t\t\t\t\t\t
  • Added the the Track Number detail column.
  • \n\ + \t\t\t\t\t\t
  • Improved the ignore list system.
  • \n\t\t\t\t\t\t
  • Fixed\ + \ a bug in the mp3 metadata decoding unit.
  • \n\t\t\t\t\t\t
  • dupeGuru Music\ + \ Edition is now a Universal application on Mac OS X.
  • \n\t\t
" + version: 5.0.6 +- date: 2006-07-28 + description: "
    \n\t\t\t\t\t\t
  • Improved VBR mp3 metadata decoding.
  • \n\ + \t\t\t\t\t\t
  • Fixed an issue that occasionally made dupeGuru ME crash\ + \ on startup.
  • \n\t\t
" + version: 5.0.5 +- date: 2006-06-26 + description: "
    \n\t\t\t\t\t\t
  • Fixed an issue with Move and Copy features.
  • \n\ + \t\t
" + version: 5.0.4 +- date: 2006-06-17 + description: "
    \n\t\t\t\t\t\t
  • Improved duplicate scanning speed.
  • \n\ + \t\t\t\t\t\t
  • Added a warning that a file couldn't be renamed if a file\ + \ with the same name already exists.
  • \n\t\t
" + version: 5.0.3 +- date: 2006-06-06 + description: "
    \n\t\t\t\t\t\t
  • Added \"Rename Selected\" feature.
  • \n\ + \t\t \t
  • Improved MP3 metadata decoding.
  • \n\t\t\t\t\t\t\ +
  • Fixed some minor issues with \"Reload Last Results\" feature.
  • \n\ + \t\t\t\t\t\t
  • Fixed ignore list issues.
  • \n\t\t
" + version: 5.0.2 +- date: 2006-05-26 + description: "
    \n\t\t \t
  • Fixed occasional progress bar woes\ + \ under Windows.
  • \n\t\t\t\t\t\t
  • Nothing has been changed in the Mac OS\ + \ X version, but I want to keep version in sync.
  • \n\t\t
" + version: 5.0.1 +- date: 2006-05-19 + description: "
    \n\t\t
  • Complete rewrite
  • \n\t\t\t\t\t\t\ +
  • Changed \"Mp3 Filter\" name to \"dupeGuru Music Edition\"
  • \n\t\t\t\t\t\ + \t
  • Now runs on Mac OS X.
  • \n\t\t
" + version: 5.0.0 +- date: 2006-04-13 + description: "
    \n\t\t
  • *fixed* a critical bug introduced\ + \ in 4.2.5: Files couldn't be deleted anymore!
  • \n\t\t
  • *fixed*\ + \ some more issues with WMA decoding.
  • \n\t\t
  • *fixed*\ + \ an issue with profile wizard.
  • \n\t\t
" + version: 4.2.6 +- date: 2006-04-11 + description: "
    \n\t\t
  • *added* a test zone in the Exclusions\ + \ profile section.
  • \n\t\t
  • *fixed* a bug with exclusion\ + \ patterns.
  • \n\t\t
  • *fixed* an issue occuring when\ + \ reading some kinds of WMA files.
  • \n\t\t
" + version: 4.2.5 +- date: 2006-02-16 + description: "
    \n\t\t
  • *fixed* MPL occasional issues\ + \ when saving.
  • \n\t\t
  • *fixed* m4p (protected AAC\ + \ files) bitrate reading.
  • \n\t\t
" + version: 4.2.4 +- date: 2005-10-15 + description: "
    \n\t\t
  • *improved* Added the \"Add Custom\ + \ Extension\" button in the File Priority section of the profile editor.
  • \n\ + \t\t
" + version: 4.2.3 +- date: 2005-10-07 + description: "
    \n\t\t
  • *improved* Results management\ + \ by adding the possibility to remove selected (not only checked) duplicates from\ + \ the list.
  • \n\t\t
  • *fixed* An issue with the \"\ + Switch with reference\" feature.
  • \n\t\t
  • *fixed*\ + \ A stability issue with the result pane.
  • \n\t\t
" + version: 4.2.2 +- date: 2005-09-06 + description: "
    \n\t\t
  • *fixed* A little bug with M4A/M4P\ + \ support.
  • \n\t\t
" + version: 4.2.1 +- date: 2005-08-30 + description: "
    \n\t\t
  • *added* M4A/M4P (iTunes format)\ + \ support.
  • \n\t\t
  • *added* \"Field order doesn't\ + \ matter\" option in Comparison Options.
  • \n\t\t
  • *added*\ + \ A \"Open directory containing this file\" option in the result window's context\ + \ menu.
  • \n\t\t \t
  • *fixed* Some bugs with the \"Load last\ + \ results\" function.
  • \n\t\t
" + version: 4.2.0 +- date: 2005-03-22 + description: "
    \n\t\t \t
  • *fixed* Nasty bug in the wizard\ + \ system.
  • \n\t\t \t
  • *fixed* Yet another nasty bug in\ + \ the Move/Copy option of the result pane.
  • \n\t\t
" + version: 4.1.5 +- date: 2004-11-10 + description: "
    \n\t\t \t
  • *added* \"Load last results\" function.
  • \n\ + \t\t \t
  • *added* Customizable columns in the results window.
  • \n\ + \t\t \t
  • *fixed* A bug related to special characters in the\ + \ XML profiles.
  • \n\t\t \t
  • *fixed* The result window scroll\ + \ didn't move properly on \"Switch with ref.\".
  • \n\t\t \t
  • *fixed*\ + \ A bug with the WMA plugin.
  • \n\t\t
" + version: 4.1.4 +- date: 2004-10-30 + description: "
    \n\t\t \t
  • *added* Profile summary in the\ + \ main window.
  • \n\t\t \t
  • *added* An (Artist + title)\ + \ ID3 tag comparison type.
  • \n\t\t \t
  • *improved* The profile\ + \ system by making it XML based.
  • \n\t\t
" + version: 4.1.3 +- date: 2004-09-28 + description: "
    \n\t\t \t
  • *improved* Changed the ID3 tag\ + \ comparison from (Artist + Title) to (Artist + Title + Album).
  • \n\t\t \ + \
" + version: 4.1.2 +- date: 2004-09-22 + description: "
    \n\t\t \t
  • *fixed* A couple of bugs.
  • \n\ + \t\t
" + version: 4.1.1 +- date: 2004-08-28 + description: "
    \n\t\t \t
  • *added* A \"special selection\"\ + \ wizard in the results window.
  • \n\t\t \t
  • *improved*\ + \ Changed the File content comparison system.
  • \n\t\t \t
  • *fixed*\ + \ A sorting bug in the directory tree displays
  • \n\t\t
" + version: 4.1.0 +- date: 2004-08-10 + description: "
    \n\t\t \t
  • *improved* Redesigned the configuration\ + \ wizard (again!).
  • \n\t\t
" + version: 4.0.6 +- date: 2004-07-23 + description: "
    \n\t\t \t
  • *improved* Redesigned the profile\ + \ directory frame.
  • \n\t\t
  • *fixed* A quite big bug\ + \ with file priority system.
  • \n\t\t
  • *fixed* A bug\ + \ with offline registration.
  • \n\t\t
" + version: 4.0.5 +- date: 2004-07-15 + description: "
    \n\t\t
  • *fixed* A couple of minor bugs\ + \ with profile directories/priorities.
  • \n\t\t
  • *improved*\ + \ Reduced, thus clarified, most of the text in the profile wizard.
  • \n\t\t\ + \
" + version: 4.0.4 +- date: 2004-07-12 + description: "
    \n\t\t
  • *fixed* An issue with \"Similar\ + \ word threshold\" setting, and boosted it's performance.
  • \n\t\t \ + \
  • *fixed* Some issues with the registering system.
  • \n\t\t\ + \
" + version: 4.0.3 +- date: 2004-07-10 + description: "
    \n\t\t
  • *fixed* A couple of obscure bugs.
  • \n\ + \t\t
  • *improved* Changed a couple of minor things in\ + \ this help file.
  • \n\t\t
" + version: 4.0.2 +- date: 2004-07-07 + description: "
    \n\t\t
  • *fixed* A couple of issues with\ + \ the configuration wizard.
  • \n\t\t
  • *fixed* A bug\ + \ with the View Details button when not using WinXP.
  • \n\t\t
" + version: 4.0.1 +- date: 2004-07-05 + description: Mp3 Filter has been rebuilt from scratch for this version. It features + a completely new interface, a profile system and a redesigned configuration wizard. + version: 4.0.0 +- date: 2002-12-31 + description: I never made a history entry for this version, although it has been + the version that went without changes for the most time (1 year and a half). I + also lost track of when I made it, but a quick fix (3.20.0.5) has been made on + 2002/12/31. + version: '3.20' +- date: 2002-08-14 + description: Enhanced the Mp3 List system with locking and improved searching. + version: '3.16' +- date: 2002-08-13 + description: Added Wizard, tips and installation program. + version: '3.15' +- date: 2002-08-12 + description: Added funny animation plugin and Windows Explorer shell extension. + version: '3.14' +- date: 2002-08-11 + description: Minor bugfixes + changed the Edit tag interface. + version: '3.12' +- date: 2002-08-10 + description: Added Import list feature + first 5kb of the files comparison. + version: '3.11' +- date: 2002-07-26 + description: Added extension plugins. + version: '3.10' +- date: 2002-01-30 + description: 'Fixed the ID3 Tag editor a bit. Changed the way comparison works: + it now can use ID3 Tags.' + version: '3.01' +- date: 2002-01-29 + description: The interface simply has been C-O-M-P-L-E-T-E-L-Y redesigned. Customization + level is at it's maximum, too cool. + version: '3.00' +- date: 2002-01-28 + description: Added some speed ONCE AGAIN, improved the memory management and added + a "favourite directories" feature. I also removed some confusing options. The + final result is quite cute! + version: '2.21' +- date: 2002-01-27 + description: Interface has been COMPLETELY rebuilt. Now there are MUCH more place + for everything! Several minor bugs has also been fixed Added mass ID3 tag editing. + version: '2.20' +- date: 2001-12-02 + description: Shareware again. Fixed some major bugs. Rebuilt (again) the mp3 list + system, it's now much more flexible. Added a configuration wizard. Added a renameing + preview. Well, it's a good update after all eh! + version: '2.10' +- date: 2001-12-01 + description: Added multi-language support. Added a "Send to recycle bin" option. + Enhanced rename feature. Corrected some bugs with rename function. Enhanced list + search function. + version: '2.01' +- date: 2001-11-30 + description: "As 11 Sept 2001 entered in the History, the release date of this program\ + \ will too! Ok, here is the list of Mp3 Filter version 2.00 godly features:\r\n\ + \r\n* **SPEED!!!!!!!!!!!!** Forget about what I said before. Previous versions\ + \ were TURTLES compared to that one. (Imagine what other programs are eh! :P).\ + \ What took 1 minute take 3-5 seconds now, and the more files you have to compare\ + \ together, the better will be the files/time ratio will be!\r\n* Multi-list system.\ + \ It is now easier than ever to exchange lists with your friends and select songs!\r\ + \n* Cuter interface." + version: '2.00' +- date: 2001-06-29 + description: There was some stability issues with the internal player I was using. + Mp3 Filter is now using Winamp. Thus, all files playable by Winamp are now playable + by Mp3 Filter. Fixed some minor bugs. Changed the way word exclusion system work. + AND added a song selection system. Now you can select songs from your mp3 list + and copy them to your hard drive without having to worry about where are these + songs. + version: '1.61' +- date: 2001-06-28 + description: 'The main theme of this update is efficiency. Mp3 Filter v1.53 was + already pure speed, you will NOT believe this version''s one. 60% faster on ALL + comparisons! Do not search for God anymore, you found Him and He even got an e-mail + adress: cathedly@hotmail.com :P. Ok, to tell you the truth I did not make Mp3 + Filter 60 % faster, I made it 60% less slow. My previous algorithm wasn''t bad, + but I thought about another one (this one) that has much better performances. + ALSO: Created an option form. Changed the results display (Added some info along + with the results (size,length,bitrate). Added a word excluding system. Also added + a backup system (Instead of deleting it, you can now move your file to the Mp3 + Filter backup directory (Mp3 filter does not compare files in the backup directory).' + version: '1.60' +- date: 2001-06-27 + description: Damnit, big update. Added the conditional file searching, file copying, + and rethought the Mp3List system. That new Mp3List system is damn cool! It load + instantly, even with HUGE lists, and it reduces the comparing time with list by + 30 godly % !!! You're not gonna believe it! This program is now PURE SPEED! + version: '1.53' +- date: 2001-05-05 + description: Quite cool update too. This version now can check if new versions are + available. I also grouped all options in the same menu. I moved the search function. + This function is now a lot cooler. Instead of giving you a list of matching results, + it shows you, in the Mp3 List Stats form, where the song is by positioning itself + in the List Tree. + version: '1.52' +- date: 2001-05-04 + description: Waa! I'm so happy! I implemented a poll system to Mp3 Filter! Now you + can answer my questions directly on the program! I can't wait to see if you, people, + will answer! + version: '1.51' +- date: 2001-05-03 + description: 'MAJOR UPDATE. This one is quite cool :). You ever used the "Edit Mp3 + List" feature? I improved it a lot. Now, when you add a CD to your list, it not + only saves the CD name, but it also saves the whole CD directory system. So when + you use ''Edit Mp3 List'' now, you can browse your CDs as if you would browse + anything. (There''s only one problem: you must REbuild your mp3 list to make it + fit with v1.5)' + version: '1.50' +- date: 2001-05-02 + description: Added an equalizer. This equalizer has been a good reason to add an + INI file to the program to store changed parameters. + version: '1.46' +- date: 2001-05-01 + description: Added a Banlist to the program. You write down a list of unwanted songs + in your ban list and start a scan. This function will not compare as the rest + does. If ALL words contained in a banlist line are in a filename, it will match. + version: '1.45' +- date: 2001-04-30 + description: I made the Mp3 Filter window to minimize when it compares so it can + do it faster (a LOT faster). I also modified the program so it checks the playlist + integrity each time there's a file deleted after a comparison). There was a bug + with the v1.43. When you had the ID3 Tag window up and you closed the program, + it would crash. Fixed that. + version: '1.44' +- date: 2001-04-29 + description: I noticed some days ago that people who had a good resolution but the + option to enlarge the icons on, Mp3 Filter had some big problems to display its + main form right. Since you cant resize the form without having the objects in + to resize too, I had to fix it. I also implemented a MUCH faster file searching + system. It takes less than 2 seconds to find all mp3 on my hard disk now. + version: '1.43' +- date: 2001-04-09 + description: Added some fun and useful feats. First, I made a cute playlist right-click + menu with Play File, Edit Tag, Locate, Remove from list and delete from disk. + I also added a recursive function to add songs to the playlist (Why didn't I think + about it before?? I have no clue...). I also made the Shuffle thing less.... random. + (It builds a random list and play it, so before a song play again, all songs will + be played (I added that feat some time after v1.41 release, but I didn't thought + that it worth a version change, so I only announce it on 1.42)) + version: '1.42' +- date: 2001-04-08 + description: I can't avoid it. there is always some bugs after a major update. I + didn't thought about the fact that it was possible to make a playlist with unplayable + files :) fixed that. + version: '1.41' +- date: 2001-04-07 + description: MAJOR UPDATE! You wanted a playlist. You got it in this version. Those + big buttons were ugly? Made a cute standard menu. You didn't seem to want to buy + that program. I gave up. Here is it. Freeware again. + version: '1.40' +- date: 2001-03-16 + description: Made it possible to play files that are listed after a "Find all Mp3 + on this drive". It also tells what song is currently playing on the main title + bar. + version: '1.36' +- date: 2001-03-15 + description: Added a system icon. Wow! it almost looks like winamp! + version: '1.35' +- date: 2001-03-14 + description: Added music progress bar and made the music playing continuous. + version: '1.34' +- date: 2001-03-13 + description: Added mass renaming functions. + version: '1.33' +- date: 2001-03-12 + description: Fixed some bugs with those useful function :) (and made the program + shareware) + version: '1.32' +- date: 2001-03-11 + description: Added some useful functions. + version: '1.31' +- date: 2001-03-10 + description: MAJOR CHANGE. yeah! I scrapped those radio buttons, and extended the + "recurse" function. Now, you only have 2 choices. Or you compare with your list, + or you compare within the folder (and sub-folders). With that system, you can + tell the program to just compare ALL mp3 in your hard drive. You just have to + select your drive root, and press "Find dupe files in this folder" having "Recurse" + checked. + version: '1.30' +- date: 2001-03-09 + description: Added the Music Control panel. I just love it. do you? + version: '1.23' +- date: 2001-03-08 + description: Fixed some inaccuracy with folder to folder comparison. (Will I be + done fixing someday??) and made mp3 search slightly faster. + version: '1.22' +- date: 2001-03-07 + description: Added Mp3 Player (Didn't know it was so easy to include in a program! + I woulda done this before if I knew...) and ID3 Tag Editor. Hum, The Mp3 Player + has some problems reading some mp3s... know that. + version: '1.21' +- date: 2001-03-04 + description: When I removed the "find mp3 in my list" thing, some people told me + it was useful. However, I still think that the old way to search mp3 was too messed + up, so I just added a little textbox and a search button for quick search. + version: '1.20' +- date: 2001-03-03 + description: Damnit! why didn't I see it? There was a bug with displaying the right + filename on the result boxes with List comparing. The results were switched! Thus, + deleting was impossible after a List compare. Corrected it in 1.18. + version: '1.18' +- date: 2001-03-03 + description: "When I read Yippee review (www.yippee.net thanks for review), I tried\ + \ to somewhat improve it. What changed? This:\r\n\r\n* Compare engine is less\ + \ strict. if one word contains the other, it now match (now, \"Limp Bizkit\" and\ + \ \"Limp Bizkitt\" would match)\r\n* Removed some useless features to simplify\ + \ the interface. \"Save as...\" (why did I put this on???) and \"Find file in\ + \ my list\" (Easier to find a song with \"Edit List\")\r\n* Added some hints to\ + \ buttons and some explicative labels.\r\n* \"List Stats\" changed to \"Edit\ + \ List\" so you don't have to \"hard change\" your mp3 list." + version: '1.19' +- date: 2001-02-06 + description: I never thought that a software history would be useful for such a + small program, but since Mp3 filter won't stop improving, I decided to start it. + So 1.17 is the base version. + version: '1.17' diff --git a/me/help/gen.py b/me/help/gen.py new file mode 100644 index 00000000..7fa6be01 --- /dev/null +++ b/me/help/gen.py @@ -0,0 +1,7 @@ +#!/usr/bin/env python + +import os + +from web import generate_help + +generate_help.main('.', 'dupeguru_me_help', force_render=True) \ No newline at end of file diff --git a/me/help/skeleton/hardcoded.css b/me/help/skeleton/hardcoded.css new file mode 100644 index 00000000..a3b17c5b --- /dev/null +++ b/me/help/skeleton/hardcoded.css @@ -0,0 +1,409 @@ +/***************************************************** + General settings +*****************************************************/ + +BODY +{ + background-color:white; +} + +BODY,A,P,UL,TABLE,TR,TD +{ + font-family:Tahoma,Arial,sans-serif; + font-size:10pt; + color: #4477AA;/*darker than 5588bb for the sake of the eyes*/ +} + +/***************************************************** + "A" settings +*****************************************************/ + +A +{ + color: #ae322b; + text-decoration:underline; + font-weight:bold; +} + +A.glossaryword {color:#A0A0A0;} + +A.noline +{ + text-decoration: none; +} + + +/***************************************************** + Menu and mainframe settings +*****************************************************/ + +.maincontainer +{ + display:block; + margin-left:7%; + margin-right:7%; + padding-left:5px; + padding-right:0px; + border-color:#CCCCCC; + border-style:solid; + border-width:2px; + border-right-width:0px; + border-bottom-width:0px; + border-top-color:#ae322b; + vertical-align:top; +} + +TD.menuframe +{ + width:30%; +} + +.menu +{ + margin:4px 4px 4px 4px; + margin-top: 16pt; + border-color:gray; + border-width:1px; + border-style:dotted; + padding-top:10pt; + padding-bottom:10pt; + padding-right:6pt; +} + +.submenu +{ + list-style-type: none; + margin-left:26pt; + margin-top:0pt; + margin-bottom:0pt; + padding-left:0pt; +} + +A.menuitem,A.menuitem_selected +{ + font-size:14pt; + font-family:Tahoma,Arial,sans-serif; + font-weight:normal; + padding-left:10pt; + color:#5588bb; + margin-right:2pt; + margin-left:4pt; + text-decoration:none; +} + +A.menuitem_selected +{ + font-weight:bold; +} + +A.submenuitem +{ + font-family:Tahoma,Arial,sans-serif; + font-weight:normal; + color:#5588bb; + text-decoration:none; +} + +.titleline +{ + border-width:3px; + border-style:solid; + border-left-width:0px; + border-right-width:0px; + border-top-width:0px; + border-color:#CCCCCC; + margin-left:28pt; + margin-right:2pt; + line-height:1px; + padding-top:0px; + margin-top:0px; + display:block; +} + +.titledescrip +{ + text-align:left; + display:block; + margin-left:26pt; + color:#ae322b; +} + +.mainlogo +{ + display:block; + margin-left:8%; + margin-top:4pt; + margin-bottom:4pt; +} + +/***************************************************** + IMG settings +*****************************************************/ + +IMG +{ + border-style:none; +} + +IMG.smallbutton +{ + margin-right: 20px; + float:none; +} + +IMG.floating +{ + float:left; + margin-right: 4pt; + margin-bottom: 4pt; +} + +IMG.lefticon +{ + vertical-align: middle; + padding-right: 2pt; +} + +IMG.righticon +{ + vertical-align: middle; + padding-left: 2pt; +} + +/***************************************************** + TABLE settings +*****************************************************/ + +TABLE +{ + border-style:none; +} + +TABLE.box +{ + width: 90%; + margin-left:5%; +} + +TABLE.centered +{ + margin-left: auto; + margin-right: auto; +} + +TABLE.hardcoded +{ + background-color: #225588; + margin-left: auto; + margin-right: auto; + width: 90%; +} + +TR { background-color: transparent; } + +TABLE.hardcoded TR { background-color: white } + +TABLE.hardcoded TR.header +{ + font-weight: bold; + color: black; + background-color: #C8D6E5; +} + +TABLE.hardcoded TR.header TD {color:black;} + +TABLE.hardcoded TD { padding-left: 2pt; } + +TD.minimelem { + padding-right:0px; + padding-left:0px; + text-align:center; +} + +TD.rightelem +{ + text-align:right; + /*padding-left:0pt;*/ + padding-right: 2pt; + width: 17%; +} + +/***************************************************** + P settings +*****************************************************/ + +p,.sub{text-align:justify;} +.centered{text-align:center;} +.sub +{ + padding-left: 16pt; + padding-right:16pt; +} + +.Note, .ContactInfo +{ + border-color: #ae322b; + border-width: 1pt; + border-style: dashed; + text-align:justify; + padding: 2pt 2pt 2pt 2pt; + margin-bottom:4pt; + margin-top:8pt; + list-style-position:inside; +} + +.ContactInfo +{ + width:60%; + margin-left:5%; +} + +.NewsItem +{ + border-color:#ae322b; + border-style: solid; + border-right:none; + border-top:none; + border-left:none; + border-bottom-width:1px; + text-align:justify; + padding-left:4pt; + padding-right:4pt; + padding-bottom:8pt; +} + +/***************************************************** + Lists settings +*****************************************************/ +UL.plain +{ + list-style-type: none; + padding-left:0px; + margin-left:0px; +} + +LI.plain +{ + list-style-type: none; +} + +LI.section +{ + padding-top: 6pt; +} + +UL.longtext LI +{ + border-color: #ae322b; + border-width:0px; + border-top-width:1px; + border-style:solid; + margin-top:12px; +} + +/* + with UL.longtext LI, there can be anything between + the UL and the LI, and it will still make the + lontext thing, I must break it with this hack +*/ +UL.longtext UL LI +{ + border-style:none; + margin-top:2px; +} + + +/***************************************************** + Titles settings +*****************************************************/ + +H1,H2,H3 +{ + font-family:"Courier New",monospace; + color:#5588bb; +} + +H1 +{ + font-size:18pt; + color: #ae322b; + border-color: #70A0CF; + border-width: 1pt; + border-style: solid; + margin-top: 16pt; + margin-left: 5%; + margin-right: 5%; + padding-top: 2pt; + padding-bottom:2pt; + text-align: center; +} + +H2 +{ + border-color: #ae322b; + border-bottom-width: 2px; + border-top-width: 0pt; + border-left-width: 2px; + border-right-width: 0pt; + border-bottom-color: #cccccc; + border-style: solid; + margin-top: 16pt; + margin-left: 0pt; + margin-right: 0pt; + padding-bottom:3pt; + padding-left:5pt; + text-align: left; + font-size:16pt; +} + +H3 +{ + display:block; + color:#ae322b; + border-color: #70A0CF; + border-bottom-width: 2px; + border-top-width: 0pt; + border-left-width: 0pt; + border-right-width: 0pt; + border-style: dashed; + margin-top: 12pt; + margin-left: 0pt; + margin-bottom: 4pt; + width:auto; + padding-bottom:3pt; + padding-right:2pt; + padding-left:2pt; + text-align: left; + font-weight:bold; +} + + +/***************************************************** + Misc. classes +*****************************************************/ +.longtext:first-letter {font-size: 150%} + +.price, .loweredprice, .specialprice {font-weight:bold;} + +.loweredprice {text-decoration:line-through} + +.specialprice {color:red} + +form +{ + margin:0px; +} + +.program_summary +{ + float:right; + margin: 32pt; + margin-top:0pt; + margin-bottom:0pt; +} + +.screenshot +{ + float:left; + margin: 8pt; +} \ No newline at end of file diff --git a/me/help/skeleton/images/hs_title.png b/me/help/skeleton/images/hs_title.png new file mode 100644 index 0000000000000000000000000000000000000000..07bd89c69dd50a3967b46a441741f274b8854de8 GIT binary patch literal 1817 zcmWkt2~Y|q^t%aD27PTfWQbSo7y0Xrdp{OF7oMwX4{+byFr3&y7B zlLpel1d+*^<>$!E@c2A9sOuxRq}jS+G}rcy>y2h&rngp=tT$NjxX)#-@ zR;$fwvyo;C3xs)cj4wl35`-y%c{0>!qGG~a8Nvb)0K&L3RHVdNQtA}p%Q4Dz$v^^f z%s`q=W-AZEN|l62NeGp=(QGM^zcEYpnJq53Zg_psX$pDf}jksh9!YZ6*y0d6t`*06htZIC4pHW%9Epf zh*AWzsT7ofh)_XrrKsL$;mT2tj52IVPjY1#SB?oFlp&##5=^Q9qlF{I7FJ9G2rF*a z2s?-pFR&PpFjtBRA?yV{E9}sUAgt)Ih9}1>x(SvT zsqP+UieWa0F{DUM&ub2ZbHs4dvq=VsEE&xV>e|H!OM+4TQTc&M3CEU=q(F(^fAYG# zOS_;aD|@ud29hH~gpd|cpbwr+aO8+kiBoq^eznVg<_$ zP1%d1$UV?R~TV5+i6w>mO|X i&$iNccmR(ItgDH2&`ti^@h8+@ro}}kMz!wGy!C(b`7gZy literal 0 HcmV?d00001 diff --git a/me/help/templates/base_dg.mako b/me/help/templates/base_dg.mako new file mode 100644 index 00000000..7767c49f --- /dev/null +++ b/me/help/templates/base_dg.mako @@ -0,0 +1,14 @@ +<%inherit file="/base_help.mako"/> +${next.body()} + +<%def name="menu()"><% +self.menuitem('intro.htm', 'Introduction', 'Introduction to dupeGuru') +self.menuitem('quick_start.htm', 'Quick Start', 'Quickly get into the action') +self.menuitem('directories.htm', 'Directories', 'Managing dupeGuru directories') +self.menuitem('preferences.htm', 'Preferences', 'Setting dupeGuru preferences') +self.menuitem('results.htm', 'Results', 'Time to delete these duplicates!') +self.menuitem('power_marker.htm', 'Power Marker', 'Take control of your duplicates') +self.menuitem('faq.htm', 'F.A.Q.', 'Frequently Asked Questions') +self.menuitem('versions.htm', 'Version History', 'Changes dupeGuru went through') +self.menuitem('credits.htm', 'Credits', 'People who contributed to dupeGuru') +%> \ No newline at end of file diff --git a/me/help/templates/credits.mako b/me/help/templates/credits.mako new file mode 100644 index 00000000..a170ec3d --- /dev/null +++ b/me/help/templates/credits.mako @@ -0,0 +1,20 @@ +<%! + title = 'Credits' + selected_menu_item = 'Credits' +%> +<%inherit file="/base_dg.mako"/> +Below is the list of people who contributed, directly or indirectly to dupeGuru. + +${self.credit('Virgil Dupras', 'Developer', "That's me, Hardcoded Software founder", 'www.hardcoded.net', 'hsoft@hardcoded.net')} + +${self.credit('Python', 'Programming language', "The bestest of the bests", 'www.python.org')} + +${self.credit('PyObjC', 'Python-to-Cocoa bridge', "Used for the Mac OS X version", 'pyobjc.sourceforge.net')} + +${self.credit('PyQt', 'Python-to-Qt bridge', "Used for the Windows version", 'www.riverbankcomputing.co.uk')} + +${self.credit('Qt', 'GUI Toolkit', "Used for the Windows version", 'www.qtsoftware.com')} + +${self.credit('Sparkle', 'Auto-update library', "Used for the Mac OS X version", 'andymatuschak.org/pages/sparkle')} + +${self.credit('You', 'dupeGuru user', "What would I do without you?")} diff --git a/me/help/templates/directories.mako b/me/help/templates/directories.mako new file mode 100644 index 00000000..e75b47bd --- /dev/null +++ b/me/help/templates/directories.mako @@ -0,0 +1,24 @@ +<%! + title = 'Directories' + selected_menu_item = 'Directories' +%> +<%inherit file="/base_dg.mako"/> + +There is a panel in dupeGuru called **Directories**. You can open it by clicking on the **Directories** button. This directory contains the list of the directories that will be scanned when you click on **Start Scanning**. + +This panel is quite straightforward to use. If you want to add a directory, click on **Add**. If you added directories before, a popup menu with a list of recent directories you added will pop. You can click on one of them to add it directly to your list. If you click on the first item of the popup menu, **Add New Directory...**, you will be prompted for a directory to add. If you never added a directory, no menu will pop and you will directly be prompted for a new directory to add. + +To remove a directory, select the directory to remove and click on **Remove**. If a subdirectory is selected when you click remove, the selected directory will be set to **excluded** state (see below) instead of being removed. + +Directory states +----- + +Every directory can be in one of these 3 states: + +* **Normal:** Duplicates found in these directories can be deleted. +* **Reference:** Duplicates found in this directory **cannot** be deleted. Files in reference directories will be in a blue color in the results. +* **Excluded:** Files in this directory will not be included in the scan. + +The default state of a directory is, of course, **Normal**. You can use **Reference** state for a directory if you want to be sure that you won't delete any file from it. + +When you set the state of a directory, all subdirectories of this directory automatically inherit this state unless you explicitly set a subdirectory's state. diff --git a/me/help/templates/faq.mako b/me/help/templates/faq.mako new file mode 100644 index 00000000..13e2e601 --- /dev/null +++ b/me/help/templates/faq.mako @@ -0,0 +1,67 @@ +<%! + title = 'dupeGuru ME F.A.Q.' + selected_menu_item = 'F.A.Q.' +%> +<%inherit file="/base_dg.mako"/> + +<%text filter="md"> +### What is dupeGuru Music Edition? + +dupeGuru Music Edition is a tool to find duplicate songs in your music collection. It can base its scan on filenames, tags or content. The filename and tag scans feature a fuzzy matching algorithm that can find duplicate filenames or tags even when they are not exactly the same. + +### What makes it better than other duplicate scanners? + +The scanning engine is extremely flexible. You can tweak it to really get the kind of results you want. You can read more about dupeGuru tweaking option at the [Preferences page](preferences.htm). + +### How safe is it to use dupeGuru ME? + +Very safe. dupeGuru has been designed to make sure you don't delete files you didn't mean to delete. First, there is the reference directory system that lets you define directories where you absolutely **don't** want dupeGuru to let you delete files there, and then there is the group reference system that makes sure that you will **always** keep at least one member of the duplicate group. + +### What are the demo limitations of dupeGuru ME? + +In demo mode, you can only perform actions (delete/copy/move) on 10 duplicates per session. + +### The mark box of a file I want to delete is disabled. What must I do? + +You cannot mark the reference (The first file) of a duplicate group. However, what you can do is to promote a duplicate file to reference. Thus, if a file you want to mark is reference, select a duplicate file in the group that you want to promote to reference, and click on **Actions-->Make Selected Reference**. If the reference file is from a reference directory (filename written in blue letters), you cannot remove it from the reference position. + +### I have a directory from which I really don't want to delete files. + +If you want to be sure that dupeGuru will never delete file from a particular directory, just open the **Directories panel**, select that directory, and set its state to **Reference**. + +### What is this '(X discarded)' notice in the status bar? + +In some cases, some matches are not included in the final results for security reasons. Let me use an example. We have 3 file: A, B and C. We scan them using a low filter hardness. The scanner determines that A matches with B, A matches with C, but B does **not** match with C. Here, dupeGuru has kind of a problem. It cannot create a duplicate group with A, B and C in it because not all files in the group would match together. It could create 2 groups: one A-B group and then one A-C group, but it will not, for security reasons. Lets think about it: If B doesn't match with C, it probably means that either B, C or both are not actually duplicates. If there would be 2 groups (A-B and A-C), you would end up delete both B and C. And if one of them is not a duplicate, that is really not what you want to do, right? So what dupeGuru does in a case like this is to discard the A-C match (and adds a notice in the status bar). Thus, if you delete B and re-run a scan, you will have a A-C match in your next results. + +### I want to mark all files from a specific directory. What can I do? + +Enable the [Power Marker](power_marker.htm) mode and click on the Directory column to sort your duplicates by Directory. It will then be easy for you to select all duplicates from the same directory, and then press Space to mark all selected duplicates. + +### I want to remove all songs that are more than 3 seconds away from their reference file. What can I do? + +* Enable the [Power Marker](power_marker.htm) mode. +* Enable the **Delta Values** mode. +* Click on the "Time" column to sort the results by time. +* Select all duplicates below -00:03. +* Click on **Remove Selected from Results**. +* Select all duplicates over 00:03. +* Click on **Remove Selected from Results**. + +### I want to make my highest bitrate songs reference files. What can I do? + +* Enable the [Power Marker](power_marker.htm) mode. +* Enable the **Delta Values** mode. +* Click on the "Bitrate" column to sort the results by bitrate. +* Click on the "Bitrate" column again to reverse the sort order (see Power Marker page to know why). +* Select all duplicates over 0. +* Click on **Make Selected Reference**. + +### I don't want [live] and [remix] versions of my songs counted as duplicates. How do I do that? + +If your comparison threshold is low enough, you will probably end up with live and remix versions of your songs in your results. There's nothing you can do to prevent that, but there's something you can do to easily remove them from your results after the scan: post-scan filtering. If, for example, you want to remove every song with anything inside square brackets []: + +* **Windows**: Click on **Actions --> Apply Filter**, then type "[*]", then click OK. +* **Mac OS X**: Type "[*]" in the "Filter" field in the toolbar. +* Click on **Mark --> Mark All**. +* Click on **Actions --> Remove Selected from Results**. + \ No newline at end of file diff --git a/me/help/templates/intro.mako b/me/help/templates/intro.mako new file mode 100644 index 00000000..2381895d --- /dev/null +++ b/me/help/templates/intro.mako @@ -0,0 +1,13 @@ +<%! + title = 'Introduction to dupeGuru ME' + selected_menu_item = 'introduction' +%> +<%inherit file="/base_dg.mako"/> + +dupeGuru Music Edition is a tool to find duplicate files on your computer. It can scan either filenames or contents. The filename scan features a fuzzy matching algorithm that can find duplicate filenames even when they are not exactly the same. + +Although dupeGuru can easily be used without documentation, reading this file will help you to master it. If you are looking for guidance for your first duplicate scan, you can take a look at the [Quick Start](quick_start.htm) section. + +It is a good idea to keep dupeGuru updated. You can download the latest version on the [dupeGuru ME homepage](http://www.hardcoded.net/dupeguru_me/). + +<%def name="meta()"> diff --git a/me/help/templates/power_marker.mako b/me/help/templates/power_marker.mako new file mode 100644 index 00000000..26078f3d --- /dev/null +++ b/me/help/templates/power_marker.mako @@ -0,0 +1,33 @@ +<%! + title = 'Power Marker' + selected_menu_item = 'Power Marker' +%> +<%inherit file="/base_dg.mako"/> + +You will probably not use the Power Marker feature very often, but if you get into a situation where you need it, you will be pretty happy that this feature exists. + +What is it? +----- + +When the Power Marker mode is enabled, the duplicates are shown without their respective reference file. You can select, mark and sort this list, just like in normal mode. + +So, what is it for? +----- + +The dupeGuru results, when in normal mode, are sorted according to duplicate groups' **reference file**. This means that if you want, for example, to mark all duplicates with the "exe" extension, you cannot just sort the results by "Kind" to have all exe duplicates together because a group can be composed of more than one kind of files. That is where Power Marker comes into play. To mark all your "exe" duplicates, you just have to: + +* Enable the Power marker mode. +* Add the "Kind" column with the "Columns" menu. +* Click on that "Kind" column to sort the list by kind. +* Locate the first duplicate with a "exe" kind. +* Select it. +* Scroll down the list to locate the last duplicate with a "exe" kind. +* Hold Shift and click on it. +* Press Space to mark all selected duplicates. + +Power Marker and delta values +----- + +The Power Marker unveil its true power when you use it with the **Delta Values** switch turned on. When you turn it on, relative values will be displayed instead of absolute ones. So if, for example, you want to remove from your results all duplicates that are more than 300 KB away from their reference, you could sort the Power Marker by Size, select all duplicates under -300 in the Size column, delete them, and then do the same for duplicates over 300 at the bottom of the list. + +You could also use it to change the reference priority of your duplicate list. When you make a fresh scan, if there are no reference directories, the reference file of every group is the biggest file. If you want to change that, for example, to the latest modification time, you can sort the Power Marker by modification time in **descending** order, select all duplicates with a modification time delta value higher than 0 and click on **Make Selected Reference**. The reason why you must make the sort order descending is because if 2 files among the same duplicate group are selected when you click on **Make Selected Reference**, only the first of the list will be made reference, the other will be ignored. And since you want the last modified file to be reference, having the sort order descending assures you that the first item of the list will be the last modified. diff --git a/me/help/templates/preferences.mako b/me/help/templates/preferences.mako new file mode 100644 index 00000000..52006d49 --- /dev/null +++ b/me/help/templates/preferences.mako @@ -0,0 +1,36 @@ +<%! + title = 'Preferences' + selected_menu_item = 'Preferences' +%> +<%inherit file="/base_dg.mako"/> + +**Scan Type:** This option determines what aspect of the files will be compared in the duplicate scan. The nature of the duplicate scan varies greatly depending on what you select for this option. + +* **Filename:** Every song will have its filename split into words, and then every word will be compared to compute a matching percentage. If this percentage is higher or equal to the **Filter Hardness** (see below for more details), dupeGuru will consider the 2 songs duplicates. +* **Filename - Fields:** Like **Filename**, except that once filename have been split into words, these words are then grouped into fields. The field separator is " - ". The final matching percentage will be the lowest matching percentage among the fields. Thus, "An Artist - The Title" and "An Artist - Other Title" would have a matching percentage of 50 (With a **Filename** scan, it would be 75). +* **Filename - Fields (No Order):** Like **Filename - Fields**, except that field order doesn't matter. For example, "An Artist - The Title" and "The Title - An Artist" would have a matching percentage of 100 instead of 0. +* **Tags:** This method reads the tag (metadata) of every song and compare their fields. This method, like the **Filename - Fields**, considers the lowest matching field as its final matching percentage. +* **Content:** This scan method use the actual content of the songs to determine which are duplicates. For 2 songs to match with this method, they must have the **exact same content**. +* **Audio Content:** Same as content, but only the audio content is compared (without metadata). + +**Filter Hardness:** If you chose a filename or tag based scan type, this option determines how similar two filenames/tags must be for dupeGuru to consider them duplicates. If the filter hardness is, for example 80, it means that 80% of the words of two filenames must match. To determine the matching percentage, dupeGuru first counts the total number of words in **both** filenames, then count the number of words matching (every word matching count as 2), and then divide the number of words matching by the total number of words. If the result is higher or equal to the filter hardness, we have a duplicate match. For example, "a b c d" and "c d e" have a matching percentage of 57 (4 words matching, 7 total words). + +**Tags to scan:** When using the **Tags** scan type, you can select the tags that will be used for comparison. + +**Word weighting:** If you chose a filename or tag based scan type, this option slightly changes how matching percentage is calculated. With word weighting, instead of having a value of 1 in the duplicate count and total word count, every word have a value equal to the number of characters they have. With word weighting, "ab cde fghi" and "ab cde fghij" would have a matching percentage of 53% (19 total characters, 10 characters matching (4 for "ab" and 6 for "cde")). + +**Match similar words:** If you turn this option on, similar words will be counted as matches. For example "The White Stripes" and "The White Stripe" would have a match % of 100 instead of 66 with that option turned on. **Warning:** Use this option with caution. It is likely that you will get a lot of false positives in your results when turning it on. However, it will help you to find duplicates that you wouldn't have found otherwise. The scan process also is significantly slower with this option turned on. + +**Can mix file kind:** If you check this box, duplicate groups are allowed to have files with different extensions. If you don't check it, well, they aren't! + +**Use regular expressions when filtering:** If you check this box, the filtering feature will treat your filter query as a **regular expression**. Explaining them is beyond the scope of this document. A good place to start learning it is . + +**Remove empty folders after delete or move:** When this option is enabled, folders are deleted after a file is deleted or moved and the folder is empty. + +**Copy and Move:** Determines how the Copy and Move operations (in the Action menu) will behave. + +* **Right in destination:** All files will be sent directly in the selected destination, without trying to recreate the source path at all. +* **Recreate relative path:** The source file's path will be re-created in the destination directory up to the root selection in the Directories panel. For example, if you added "/Users/foobar/Music" to your Directories panel and you move "/Users/foobar/Music/Artist/Album/the_song.mp3" to the destination "/Users/foobar/MyDestination", the final destination for the file will be "/Users/foobar/MyDestination/Artist/Album" ("/Users/foobar/Music" has been trimmed from source's path in the final destination.). +* **Recreate absolute path:** The source file's path will be re-created in the destination directory in it's entirety. For example, if you move "/Users/foobar/Music/Artist/Album/the_song.mp3" to the destination "/Users/foobar/MyDestination", the final destination for the file will be "/Users/foobar/MyDestination/Users/foobar/Music/Artist/Album". + +In all cases, dupeGuru nicely handles naming conflicts by prepending a number to the destination filename if the filename already exists in the destination. diff --git a/me/help/templates/quick_start.mako b/me/help/templates/quick_start.mako new file mode 100644 index 00000000..dde33c65 --- /dev/null +++ b/me/help/templates/quick_start.mako @@ -0,0 +1,18 @@ +<%! + title = 'Quick Start' + selected_menu_item = 'Quick Start' +%> +<%inherit file="/base_dg.mako"/> + +To get you quickly started with dupeGuru, let's just make a standard scan using default preferences. + +* Click on **Directories**. +* Click on **Add**. +* Choose a directory you want to scan for duplicates. +* Click on **Start Scanning**. +* Wait until the scan process is over. +* Look at every duplicate (The files that are indented) and verify that it is indeed a duplicate to the group's reference (The file above the duplicate that is not indented and have a disabled mark box). +* If a file is a false duplicate, select it and click on **Actions-->Remove Selected from Results**. +* Once you are sure that there is no false duplicate in your results, click on **Edit-->Mark All**, and then **Actions-->Send Marked to Recycle bin**. + +That is only a basic scan. There are a lot of tweaking you can do to get different results and several methods of examining and modifying your results. To know about them, just read the rest of this help file. diff --git a/me/help/templates/results.mako b/me/help/templates/results.mako new file mode 100644 index 00000000..53aa176f --- /dev/null +++ b/me/help/templates/results.mako @@ -0,0 +1,73 @@ +<%! + title = 'Results' + selected_menu_item = 'Results' +%> +<%inherit file="/base_dg.mako"/> + +When dupeGuru is finished scanning for duplicates, it will show its results in the form of duplicate group list. + +About duplicate groups +----- + +A duplicate group is a group of files that all match together. Every group has a **reference file** and one or more **duplicate files**. The reference file is the first file of the group. Its mark box is disabled. Below it, and indented, are the duplicate files. + +You can mark duplicate files, but you can never mark the reference file of a group. This is a security measure to prevent dupeGuru from deleting not only duplicate files, but their reference. You sure don't want that, do you? + +What determines which files are reference and which files are duplicates is first their directory state. A files from a reference directory will always be reference in a duplicate group. If all files are from a normal directory, the size determine which file will be the reference of a duplicate group. dupeGuru assumes that you always want to keep the biggest file, so the biggest files will take the reference position. + +You can change the reference file of a group manually. To do so, select the duplicate file you want to promote to reference, and click on **Actions-->Make Selected Reference**. + +Reviewing results +----- + +Although you can just click on **Edit-->Mark All** and then **Actions-->Send Marked to Recycle bin** to quickly delete all duplicate files in your results, it is always recommended to review all duplicates before deleting them. + +To help you reviewing the results, you can bring up the **Details panel**. This panel shows all the details of the currently selected file as well as its reference's details. This is very handy to quickly determine if a duplicate really is a duplicate. You can also double-click on a file to open it with its associated application. + +If you have more false duplicates than true duplicates (If your filter hardness is very low), the best way to proceed would be to review duplicates, mark true duplicates and then click on **Actions-->Send Marked to Recycle bin**. If you have more true duplicates than false duplicates, you can instead mark all files that are false duplicates, and use **Actions-->Remove Marked from Results**. + +Marking and Selecting +----- + +A **marked** duplicate is a duplicate with the little box next to it having a check-mark. A **selected** duplicate is a duplicate being highlighted. The multiple selection actions can be performed in dupeGuru in the standard way (Shift/Command/Control click). You can toggle all selected duplicates' mark state by pressing **space**. + +Delta Values +----- + +If you turn this switch on, some columns will display the value relative to the duplicate's reference instead of the absolute values. These delta values will also be displayed in a different color so you can spot them easily. For example, if a duplicate is 1.2 MB and its reference is 1.4 MB, the Size column will display -0.2 MB. This option is a killer feature when combined with the [Power Marker](power_marker.htm). + +Filtering +----- + +dupeGuru supports post-scan filtering. With it, you can narrow down your results so you can perform actions on a subset of it. For example, you could easily mark all duplicates with their filename containing "copy" from your results using the filter. + +**Windows:** To use the filtering feature, click on Actions --> Apply Filter, write down the filter you want to apply and click OK. To go back to unfiltered results, click on Actions --> Cancel Filter. + +**Mac OS X:** To use the filtering feature, type your filter in the "Filter" search field in the toolbar. To go back to unfiltered result, blank out the field, or click on the "X". + +In simple mode (the default mode), whatever you type as the filter is the string used to perform the actual filtering, with the exception of one wildcard: **\***. Thus, if you type "[*]" as your filter, it will match anything with [] brackets in it, whatever is in between those brackets. + +For more advanced filtering, you can turn "Use regular expressions when filtering" on. The filtering feature will then use **regular expressions**. A regular expression is a language for matching text. Explaining them is beyond the scope of this document. A good place to start learning it is . + +Matches are case insensitive in both simple and regexp mode. + +For the filter to match, your regular expression don't have to match the whole filename, it just have to contain a string matching the expression. + +You might notice that not all duplicates in the filtered results will match your filter. That is because as soon as one single duplicate in a group matches the filter, the whole group stays in the results so you can have a better view of the duplicate's context. However, non-matching duplicates are in "reference mode". Therefore, you can perform actions like Mark All and be sure to only mark filtered duplicates. + +Action Menu +----- + +* **Start Duplicate Scan:** Starts a new duplicate scan. +* **Clear Ignore List:** Remove all ignored matches you added. You have to start a new scan for the newly cleared ignore list to be effective. +* **Export Results to XHTML:** Take the current results, and create an XHTML file out of it. The columns that are visible when you click on this button will be the columns present in the XHTML file. The file will automatically be opened in your default browser. +* **Send Marked to Trash:** Send all marked duplicates to trash, obviously. +* **Move Marked to...:** Prompt you for a destination, and then move all marked files to that destination. Source file's path might be re-created in destination, depending on the "Copy and Move" preference. +* **Copy Marked to...:** Prompt you for a destination, and then copy all marked files to that destination. Source file's path might be re-created in destination, depending on the "Copy and Move" preference. +* **Remove Marked from Results:** Remove all marked duplicates from results. The actual files will not be touched and will stay where they are. +* **Remove Selected from Results:** Remove all selected duplicates from results. Note that all selected reference files will be ignored, only duplicates can be removed with this action. +* **Make Selected Reference:** Promote all selected duplicates to reference. If a duplicate is a part of a group having a reference file coming from a reference directory (in blue color), no action will be taken for this duplicate. If more than one duplicate among the same group are selected, only the first of each group will be promoted. +* **Add Selected to Ignore List:** This first removes all selected duplicates from results, and then add the match of that duplicate and the current reference in the ignore list. This match will not come up again in further scan. The duplicate itself might come back, but it will be matched with another reference file. You can clear the ignore list with the Clear Ignore List command. +* **Open Selected with Default Application:** Open the file with the application associated with selected file's type. +* **Reveal Selected in Finder:** Open the folder containing selected file. +* **Rename Selected:** Prompts you for a new name, and then rename the selected file. diff --git a/me/help/templates/versions.mako b/me/help/templates/versions.mako new file mode 100644 index 00000000..946cbaa1 --- /dev/null +++ b/me/help/templates/versions.mako @@ -0,0 +1,9 @@ +<%! + title = 'dupeGuru ME version history' + selected_menu_item = 'Version History' +%> +<%inherit file="/base_dg.mako"/> + +A large part of this version history is not serious (especially before v3), but it is always interesting to remember that I learnt most of what I know about designing/implementing/supporting a software through that. + +${self.output_changelogs(changelog)} \ No newline at end of file diff --git a/me/qt/app.py b/me/qt/app.py new file mode 100644 index 00000000..65314e2b --- /dev/null +++ b/me/qt/app.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python +# Unit Name: app +# Created By: Virgil Dupras +# Created On: 2009-05-21 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +import hsfs.phys.music + +from dupeguru import data_me, scanner + +from base.app import DupeGuru as DupeGuruBase +from details_dialog import DetailsDialog +from preferences import Preferences +from preferences_dialog import PreferencesDialog + +class DupeGuru(DupeGuruBase): + LOGO_NAME = 'logo_me' + NAME = 'dupeGuru Music Edition' + VERSION = '5.6.1' + DELTA_COLUMNS = frozenset([2, 3, 4, 5, 7, 8]) + + def __init__(self): + DupeGuruBase.__init__(self, data_me, appid=1) + + def _setup(self): + self.scanner = scanner.ScannerME() + self.directories.dirclass = hsfs.phys.music.Directory + DupeGuruBase._setup(self) + + def _update_options(self): + DupeGuruBase._update_options(self) + self.scanner.min_match_percentage = self.prefs.filter_hardness + self.scanner.scan_type = self.prefs.scan_type + self.scanner.word_weighting = self.prefs.word_weighting + self.scanner.match_similar_words = self.prefs.match_similar + scanned_tags = set() + if self.prefs.scan_tag_track: + scanned_tags.add('track') + if self.prefs.scan_tag_artist: + scanned_tags.add('artist') + if self.prefs.scan_tag_album: + scanned_tags.add('album') + if self.prefs.scan_tag_title: + scanned_tags.add('title') + if self.prefs.scan_tag_genre: + scanned_tags.add('genre') + if self.prefs.scan_tag_year: + scanned_tags.add('year') + self.scanner.scanned_tags = scanned_tags + + def _create_details_dialog(self, parent): + return DetailsDialog(parent, self) + + def _create_preferences(self): + return Preferences() + + def _create_preferences_dialog(self, parent): + return PreferencesDialog(parent, self) + diff --git a/me/qt/app_win.py b/me/qt/app_win.py new file mode 100644 index 00000000..697f8335 --- /dev/null +++ b/me/qt/app_win.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python +# Unit Name: app_win +# Created By: Virgil Dupras +# Created On: 2009-05-21 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +import winshell + +import app + +class DupeGuru(app.DupeGuru): + @staticmethod + def _recycle_dupe(dupe): + winshell.delete_file(unicode(dupe.path), no_confirm=True) + diff --git a/me/qt/build.py b/me/qt/build.py new file mode 100644 index 00000000..6e7e8b5f --- /dev/null +++ b/me/qt/build.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python +# Unit Name: build +# Created By: Virgil Dupras +# Created On: 2009-05-22 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +# On Windows, PyInstaller is used to build an exe (py2exe creates a very bad looking icon) +# The release version is outdated. Use at least r672 on http://svn.pyinstaller.org/trunk + +import os +import os.path as op +import shutil + +from hsutil.build import print_and_do +from app import DupeGuru + +# Removing build and dist +if op.exists('build'): + shutil.rmtree('build') +if op.exists('dist'): + shutil.rmtree('dist') + +version = DupeGuru.VERSION +versioncomma = version.replace('.', ', ') + ', 0' +verinfo = open('verinfo').read() +verinfo = verinfo.replace('$versioncomma', versioncomma).replace('$version', version) +fp = open('verinfo_tmp', 'w') +fp.write(verinfo) +fp.close() +print_and_do("python C:\\Python26\\pyinstaller\\Build.py dgme.spec") +os.remove('verinfo_tmp') + +print_and_do("xcopy /Y C:\\src\\vs_comp\\msvcrt dist") +print_and_do("xcopy /Y /S /I help\\dupeguru_me_help dist\\help") + +aicom = '"\\Program Files\\Caphyon\\Advanced Installer\\AdvancedInstaller.com"' +shutil.copy('installer.aip', 'installer_tmp.aip') # this is so we don'a have to re-commit installer.aip at every version change +print_and_do('%s /edit installer_tmp.aip /SetVersion %s' % (aicom, version)) +print_and_do('%s /build installer_tmp.aip -force' % aicom) +os.remove('installer_tmp.aip') \ No newline at end of file diff --git a/me/qt/details_dialog.py b/me/qt/details_dialog.py new file mode 100644 index 00000000..b680309c --- /dev/null +++ b/me/qt/details_dialog.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python +# Unit Name: details_dialog +# Created By: Virgil Dupras +# Created On: 2009-04-27 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +from PyQt4.QtCore import Qt +from PyQt4.QtGui import QDialog + +from base.details_table import DetailsModel +from details_dialog_ui import Ui_DetailsDialog + +class DetailsDialog(QDialog, Ui_DetailsDialog): + def __init__(self, parent, app): + QDialog.__init__(self, parent, Qt.Tool) + self.app = app + self.setupUi(self) + self.model = DetailsModel(app) + self.tableView.setModel(self.model) diff --git a/me/qt/details_dialog.ui b/me/qt/details_dialog.ui new file mode 100644 index 00000000..c0557003 --- /dev/null +++ b/me/qt/details_dialog.ui @@ -0,0 +1,53 @@ + + + DetailsDialog + + + + 0 + 0 + 502 + 295 + + + + + 250 + 250 + + + + Details + + + + 0 + + + 0 + + + + + true + + + QAbstractItemView::SelectRows + + + false + + + + + + + + DetailsTable + QTableView +
base.details_table
+
+
+ + +
diff --git a/me/qt/dgme.spec b/me/qt/dgme.spec new file mode 100644 index 00000000..4d47e350 --- /dev/null +++ b/me/qt/dgme.spec @@ -0,0 +1,19 @@ +# -*- mode: python -*- +a = Analysis([os.path.join(HOMEPATH,'support\\_mountzlib.py'), os.path.join(HOMEPATH,'support\\useUnicode.py'), 'start.py'], + pathex=['C:\\src\\dupeguru\\me\\qt']) +pyz = PYZ(a.pure) +exe = EXE(pyz, + a.scripts, + exclude_binaries=1, + name=os.path.join('build\\pyi.win32\\dupeGuru ME', 'dupeGuru ME.exe'), + debug=False, + strip=False, + upx=True, + console=False , icon='base\\images\\dgme_logo.ico', version='verinfo_tmp') +coll = COLLECT( exe, + a.binaries, + a.zipfiles, + a.datas, + strip=False, + upx=True, + name=os.path.join('dist')) diff --git a/me/qt/gen.py b/me/qt/gen.py new file mode 100644 index 00000000..9edb6040 --- /dev/null +++ b/me/qt/gen.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python +# Unit Name: gen +# Created By: Virgil Dupras +# Created On: 2009-05-22 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +import os + +from hsutil.build import print_and_do + +os.chdir('dupeguru') +print_and_do('python gen.py') +os.chdir('..') + +os.chdir('base') +print_and_do('python gen.py') +os.chdir('..') + +print_and_do("pyuic4 details_dialog.ui > details_dialog_ui.py") +print_and_do("pyuic4 preferences_dialog.ui > preferences_dialog_ui.py") + +os.chdir('help') +print_and_do('python gen.py') +os.chdir('..') diff --git a/me/qt/installer.aip b/me/qt/installer.aip new file mode 100644 index 00000000..51cfc4d9 --- /dev/null +++ b/me/qt/installer.aip @@ -0,0 +1,241 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/me/qt/preferences.py b/me/qt/preferences.py new file mode 100644 index 00000000..bdb4c914 --- /dev/null +++ b/me/qt/preferences.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python +# Unit Name: preferences +# Created By: Virgil Dupras +# Created On: 2009-05-17 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +from dupeguru.scanner import (SCAN_TYPE_FILENAME, SCAN_TYPE_FIELDS, SCAN_TYPE_FIELDS_NO_ORDER, + SCAN_TYPE_TAG, SCAN_TYPE_CONTENT, SCAN_TYPE_CONTENT_AUDIO) + +from base.preferences import Preferences as PreferencesBase + +class Preferences(PreferencesBase): + # (width, is_visible) + COLUMNS_DEFAULT_ATTRS = [ + (200, True), # name + (180, True), # path + (60, True), # size + (60, True), # Time + (50, True), # Bitrate + (60, False), # Sample Rate + (40, False), # Kind + (120, False), # creation + (120, False), # modification + (120, False), # Title + (120, False), # Artist + (120, False), # Album + (80, False), # Genre + (40, False), # Year + (40, False), # Track Number + (120, False), # Comment + (60, True), # match % + (120, False), # Words Used + (80, False), # dupe count + ] + + def _load_specific(self, settings, get): + self.scan_type = get('ScanType', self.scan_type) + self.word_weighting = get('WordWeighting', self.word_weighting) + self.match_similar = get('MatchSimilar', self.match_similar) + self.scan_tag_track = get('ScanTagTrack', self.scan_tag_track) + self.scan_tag_artist = get('ScanTagArtist', self.scan_tag_artist) + self.scan_tag_album = get('ScanTagAlbum', self.scan_tag_album) + self.scan_tag_title = get('ScanTagTitle', self.scan_tag_title) + self.scan_tag_genre = get('ScanTagGenre', self.scan_tag_genre) + self.scan_tag_year = get('ScanTagYear', self.scan_tag_year) + + def _reset_specific(self): + self.filter_hardness = 80 + self.scan_type = SCAN_TYPE_TAG + self.word_weighting = True + self.match_similar = False + self.scan_tag_track = False + self.scan_tag_artist = True + self.scan_tag_album = True + self.scan_tag_title = True + self.scan_tag_genre = False + self.scan_tag_year = False + + def _save_specific(self, settings, set_): + set_('ScanType', self.scan_type) + set_('WordWeighting', self.word_weighting) + set_('MatchSimilar', self.match_similar) + set_('ScanTagTrack', self.scan_tag_track) + set_('ScanTagArtist', self.scan_tag_artist) + set_('ScanTagAlbum', self.scan_tag_album) + set_('ScanTagTitle', self.scan_tag_title) + set_('ScanTagGenre', self.scan_tag_genre) + set_('ScanTagYear', self.scan_tag_year) + diff --git a/me/qt/preferences_dialog.py b/me/qt/preferences_dialog.py new file mode 100644 index 00000000..0ec53364 --- /dev/null +++ b/me/qt/preferences_dialog.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python +# Unit Name: preferences_dialog +# Created By: Virgil Dupras +# Created On: 2009-04-29 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +from PyQt4.QtCore import SIGNAL, Qt +from PyQt4.QtGui import QDialog, QDialogButtonBox + +from dupeguru.scanner import (SCAN_TYPE_FILENAME, SCAN_TYPE_FIELDS, SCAN_TYPE_FIELDS_NO_ORDER, + SCAN_TYPE_TAG, SCAN_TYPE_CONTENT, SCAN_TYPE_CONTENT_AUDIO) + +from preferences_dialog_ui import Ui_PreferencesDialog +import preferences + +SCAN_TYPE_ORDER = [ + SCAN_TYPE_FILENAME, + SCAN_TYPE_FIELDS, + SCAN_TYPE_FIELDS_NO_ORDER, + SCAN_TYPE_TAG, + SCAN_TYPE_CONTENT, + SCAN_TYPE_CONTENT_AUDIO, +] + +class PreferencesDialog(QDialog, Ui_PreferencesDialog): + def __init__(self, parent, app): + flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint + QDialog.__init__(self, parent, flags) + self.app = app + self._setupUi() + + self.connect(self.buttonBox, SIGNAL('clicked(QAbstractButton*)'), self.buttonClicked) + + def _setupUi(self): + self.setupUi(self) + + def load(self, prefs=None): + if prefs is None: + prefs = self.app.prefs + self.filterHardnessSlider.setValue(prefs.filter_hardness) + self.filterHardnessLabel.setNum(prefs.filter_hardness) + scan_type_index = SCAN_TYPE_ORDER.index(prefs.scan_type) + self.scanTypeComboBox.setCurrentIndex(scan_type_index) + setchecked = lambda cb, b: cb.setCheckState(Qt.Checked if b else Qt.Unchecked) + setchecked(self.tagTrackBox, prefs.scan_tag_track) + setchecked(self.tagArtistBox, prefs.scan_tag_artist) + setchecked(self.tagAlbumBox, prefs.scan_tag_album) + setchecked(self.tagTitleBox, prefs.scan_tag_title) + setchecked(self.tagGenreBox, prefs.scan_tag_genre) + setchecked(self.tagYearBox, prefs.scan_tag_year) + setchecked(self.matchSimilarBox, prefs.match_similar) + setchecked(self.wordWeightingBox, prefs.word_weighting) + setchecked(self.mixFileKindBox, prefs.mix_file_kind) + setchecked(self.useRegexpBox, prefs.use_regexp) + setchecked(self.removeEmptyFoldersBox, prefs.remove_empty_folders) + self.copyMoveDestinationComboBox.setCurrentIndex(prefs.destination_type) + + def save(self): + prefs = self.app.prefs + prefs.filter_hardness = self.filterHardnessSlider.value() + prefs.scan_type = SCAN_TYPE_ORDER[self.scanTypeComboBox.currentIndex()] + ischecked = lambda cb: cb.checkState() == Qt.Checked + prefs.scan_tag_track = ischecked(self.tagTrackBox) + prefs.scan_tag_artist = ischecked(self.tagArtistBox) + prefs.scan_tag_album = ischecked(self.tagAlbumBox) + prefs.scan_tag_title = ischecked(self.tagTitleBox) + prefs.scan_tag_genre = ischecked(self.tagGenreBox) + prefs.scan_tag_year = ischecked(self.tagYearBox) + prefs.match_similar = ischecked(self.matchSimilarBox) + prefs.word_weighting = ischecked(self.wordWeightingBox) + prefs.mix_file_kind = ischecked(self.mixFileKindBox) + prefs.use_regexp = ischecked(self.useRegexpBox) + prefs.remove_empty_folders = ischecked(self.removeEmptyFoldersBox) + prefs.destination_type = self.copyMoveDestinationComboBox.currentIndex() + + def resetToDefaults(self): + self.load(preferences.Preferences()) + + #--- Events + def buttonClicked(self, button): + role = self.buttonBox.buttonRole(button) + if role == QDialogButtonBox.ResetRole: + self.resetToDefaults() + diff --git a/me/qt/preferences_dialog.ui b/me/qt/preferences_dialog.ui new file mode 100644 index 00000000..9eefc864 --- /dev/null +++ b/me/qt/preferences_dialog.ui @@ -0,0 +1,434 @@ + + + PreferencesDialog + + + + 0 + 0 + 350 + 322 + + + + Preferences + + + false + + + true + + + + + + + + + + + 100 + 0 + + + + + 100 + 16777215 + + + + Scan Type: + + + + + + + + Filename + + + + + Filename - Fields + + + + + Filename - Fields (No Order) + + + + + Tags + + + + + Contents + + + + + Audio Contents + + + + + + + + + + + + + 100 + 0 + + + + + 100 + 16777215 + + + + Filter Hardness: + + + + + + + 0 + + + + + 12 + + + + + + 0 + 0 + + + + 1 + + + 100 + + + true + + + Qt::Horizontal + + + + + + + + 21 + 0 + + + + 100 + + + + + + + + + 0 + + + + + More Results + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Less Results + + + + + + + + + + + + + + 0 + 40 + + + + + + 0 + 0 + 91 + 17 + + + + Tags to scan: + + + + + + 10 + 20 + 51 + 21 + + + + Track + + + + + + 60 + 20 + 51 + 21 + + + + Artist + + + + + + 110 + 20 + 61 + 21 + + + + Album + + + + + + 170 + 20 + 51 + 21 + + + + Title + + + + + + 220 + 20 + 61 + 21 + + + + Genre + + + + + + 280 + 20 + 51 + 21 + + + + Year + + + + + + + + Word weighting + + + + + + + Match similar words + + + + + + + Can mix file kind + + + + + + + Use regular expressions when filtering + + + + + + + Remove empty folders on delete or move + + + + + + + + + + 100 + 0 + + + + + 100 + 16777215 + + + + Copy and Move: + + + + + + + + 0 + 0 + + + + + Right in destination + + + + + Recreate relative path + + + + + Recreate absolute path + + + + + + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok|QDialogButtonBox::RestoreDefaults + + + + + + + + + filterHardnessSlider + valueChanged(int) + filterHardnessLabel + setNum(int) + + + 182 + 26 + + + 271 + 26 + + + + + buttonBox + accepted() + PreferencesDialog + accept() + + + 182 + 228 + + + 182 + 124 + + + + + buttonBox + rejected() + PreferencesDialog + reject() + + + 182 + 228 + + + 182 + 124 + + + + + diff --git a/me/qt/profile.py b/me/qt/profile.py new file mode 100644 index 00000000..195b1775 --- /dev/null +++ b/me/qt/profile.py @@ -0,0 +1,20 @@ +import sys +import cProfile +import pstats + +from PyQt4.QtCore import QCoreApplication +from PyQt4.QtGui import QApplication + +if sys.platform == 'win32': + from app_win import DupeGuru +else: + from app import DupeGuru + +if __name__ == "__main__": + app = QApplication(sys.argv) + QCoreApplication.setOrganizationName('Hardcoded Software') + QCoreApplication.setApplicationName('dupeGuru Music Edition') + dgapp = DupeGuru() + cProfile.run('app.exec_()', '/tmp/prof') + p = pstats.Stats('/tmp/prof') + p.sort_stats('time').print_stats() \ No newline at end of file diff --git a/me/qt/start.py b/me/qt/start.py new file mode 100644 index 00000000..d4315875 --- /dev/null +++ b/me/qt/start.py @@ -0,0 +1,20 @@ +import sys + +from PyQt4.QtCore import QCoreApplication +from PyQt4.QtGui import QApplication, QIcon, QPixmap + +import base.dg_rc + +if sys.platform == 'win32': + from app_win import DupeGuru +else: + from app import DupeGuru + +if __name__ == "__main__": + app = QApplication(sys.argv) + app.setWindowIcon(QIcon(QPixmap(":/logo_me"))) + QCoreApplication.setOrganizationName('Hardcoded Software') + QCoreApplication.setApplicationName(DupeGuru.NAME) + QCoreApplication.setApplicationVersion(DupeGuru.VERSION) + dgapp = DupeGuru() + sys.exit(app.exec_()) \ No newline at end of file diff --git a/me/qt/verinfo b/me/qt/verinfo new file mode 100644 index 00000000..6384c49e --- /dev/null +++ b/me/qt/verinfo @@ -0,0 +1,28 @@ +VSVersionInfo( + ffi=FixedFileInfo( + filevers=($versioncomma), + prodvers=($versioncomma), + mask=0x17, + flags=0x0, + OS=0x4, + fileType=0x1, + subtype=0x0, + date=(0, 0) + ), + kids=[ + StringFileInfo( + [ + StringTable( + '040904b0', + [StringStruct('CompanyName', 'Hardcoded Software'), + StringStruct('FileDescription', 'dupeGuru Music Edition'), + StringStruct('FileVersion', '$version'), + StringStruct('InternalName', 'dupeGuru ME.exe'), + StringStruct('LegalCopyright', '(c) Hardcoded Software. All rights reserved.'), + StringStruct('OriginalFilename', 'dupeGuru ME.exe'), + StringStruct('ProductName', 'dupeGuru Music Edition'), + StringStruct('ProductVersion', '$versioncomma')]) + ]), + VarFileInfo([VarStruct('Translation', [1033])]) + ] +) diff --git a/pe/cocoa/AppDelegate.h b/pe/cocoa/AppDelegate.h new file mode 100644 index 00000000..70203dfb --- /dev/null +++ b/pe/cocoa/AppDelegate.h @@ -0,0 +1,21 @@ +#import +#import "dgbase/AppDelegate.h" +#import "ResultWindow.h" +#import "DirectoryPanel.h" +#import "DetailsPanel.h" +#import "PyDupeGuru.h" + +@interface AppDelegate : AppDelegateBase +{ + IBOutlet ResultWindow *result; + + DetailsPanel *_detailsPanel; + DirectoryPanel *_directoryPanel; +} +- (IBAction)openWebsite:(id)sender; +- (IBAction)toggleDetailsPanel:(id)sender; +- (IBAction)toggleDirectories:(id)sender; + +- (DirectoryPanel *)directoryPanel; +- (PyDupeGuru *)py; +@end diff --git a/pe/cocoa/AppDelegate.m b/pe/cocoa/AppDelegate.m new file mode 100644 index 00000000..0a515656 --- /dev/null +++ b/pe/cocoa/AppDelegate.m @@ -0,0 +1,116 @@ +#import "AppDelegate.h" +#import "ProgressController.h" +#import "RegistrationInterface.h" +#import "Utils.h" +#import "ValueTransformers.h" +#import "Consts.h" + +@implementation AppDelegate ++ (void)initialize +{ + NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; + NSMutableDictionary *d = [NSMutableDictionary dictionaryWithCapacity:10]; + [d setObject:[NSNumber numberWithInt:95] forKey:@"minMatchPercentage"]; + [d setObject:[NSNumber numberWithInt:1] forKey:@"recreatePathType"]; + [d setObject:[NSNumber numberWithBool:NO] forKey:@"matchScaled"]; + [d setObject:[NSNumber numberWithBool:YES] forKey:@"mixFileKind"]; + [d setObject:[NSNumber numberWithBool:NO] forKey:@"useRegexpFilter"]; + [d setObject:[NSNumber numberWithBool:NO] forKey:@"removeEmptyFolders"]; + [d setObject:[NSNumber numberWithBool:NO] forKey:@"debug"]; + [d setObject:[NSArray array] forKey:@"recentDirectories"]; + [d setObject:[NSArray array] forKey:@"columnsOrder"]; + [d setObject:[NSDictionary dictionary] forKey:@"columnsWidth"]; + [[NSUserDefaultsController sharedUserDefaultsController] setInitialValues:d]; + [ud registerDefaults:d]; +} + +- (id)init +{ + self = [super init]; + _directoryPanel = nil; + _detailsPanel = nil; + _appName = APPNAME; + return self; +} + +- (IBAction)openWebsite:(id)sender +{ + [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"http://www.hardcoded.net/dupeguru_pe"]]; +} + +- (IBAction)toggleDetailsPanel:(id)sender +{ + if (!_detailsPanel) + _detailsPanel = [[DetailsPanel alloc] initWithPy:py]; + if ([[_detailsPanel window] isVisible]) + [[_detailsPanel window] close]; + else + { + [[_detailsPanel window] orderFront:nil]; + [_detailsPanel refresh]; + } +} + +- (IBAction)toggleDirectories:(id)sender +{ + [[self directoryPanel] toggleVisible:sender]; +} + +- (DirectoryPanel *)directoryPanel +{ + if (!_directoryPanel) + _directoryPanel = [[DirectoryPanel alloc] initWithParentApp:self]; + return _directoryPanel; +} +- (PyDupeGuru *)py { return (PyDupeGuru *)py; } + +//Delegate +- (void)applicationDidFinishLaunching:(NSNotification *)aNotification +{ + [[ProgressController mainProgressController] setWorker:py]; + NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; + //Restore Columns + NSArray *columnsOrder = [ud arrayForKey:@"columnsOrder"]; + NSDictionary *columnsWidth = [ud dictionaryForKey:@"columnsWidth"]; + if ([columnsOrder count]) + [result restoreColumnsPosition:columnsOrder widths:columnsWidth]; + //Reg stuff + if ([RegistrationInterface showNagWithApp:[self py] name:APPNAME limitDescription:LIMIT_DESC]) + [unlockMenuItem setTitle:@"Thanks for buying dupeGuru Picture Edition!"]; + //Restore results + [py loadIgnoreList]; + [py loadResults]; +} + +- (void)applicationWillBecomeActive:(NSNotification *)aNotification +{ + if (![[result window] isVisible]) + [result showWindow:NSApp]; +} + +- (void)applicationWillTerminate:(NSNotification *)aNotification +{ + NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; + [ud setObject: [result getColumnsOrder] forKey:@"columnsOrder"]; + [ud setObject: [result getColumnsWidth] forKey:@"columnsWidth"]; + [py saveIgnoreList]; + [py saveResults]; + int sc = [ud integerForKey:@"sessionCountSinceLastIgnorePurge"]; + if (sc >= 10) + { + sc = -1; + [py purgeIgnoreList]; + } + sc++; + [ud setInteger:sc forKey:@"sessionCountSinceLastIgnorePurge"]; + // NSApplication does not release nib instances objects, we must do it manually + // Well, it isn't needed because the memory is freed anyway (we are quitting the application + // But I need to release RecentDirectories so it saves the user defaults + [recentDirectories release]; +} + +- (void)recentDirecoryClicked:(NSString *)directory +{ + [[self directoryPanel] addDirectory:directory]; +} +@end diff --git a/pe/cocoa/Consts.h b/pe/cocoa/Consts.h new file mode 100644 index 00000000..c8aea9fa --- /dev/null +++ b/pe/cocoa/Consts.h @@ -0,0 +1,4 @@ +#import "dgbase/Consts.h" + +extern NSString *ImageLoadedNotification; +extern NSString *APPNAME; \ No newline at end of file diff --git a/pe/cocoa/Consts.m b/pe/cocoa/Consts.m new file mode 100644 index 00000000..b7c57178 --- /dev/null +++ b/pe/cocoa/Consts.m @@ -0,0 +1,5 @@ +#import "Consts.h" + +NSString *ImageLoadedNotification = @"ImageLoadedNotification"; +NSString *APPNAME = @"dupeGuru PE"; + diff --git a/pe/cocoa/DetailsPanel.h b/pe/cocoa/DetailsPanel.h new file mode 100644 index 00000000..119e6cf6 --- /dev/null +++ b/pe/cocoa/DetailsPanel.h @@ -0,0 +1,21 @@ +#import +#import "PyApp.h" +#import "Table.h" + + +@interface DetailsPanel : NSWindowController +{ + IBOutlet TableView *detailsTable; + IBOutlet NSImageView *dupeImage; + IBOutlet NSProgressIndicator *dupeProgressIndicator; + IBOutlet NSImageView *refImage; + IBOutlet NSProgressIndicator *refProgressIndicator; + + PyApp *py; + BOOL _needsRefresh; + NSString *_dupePath; + NSString *_refPath; +} +- (id)initWithPy:(PyApp *)aPy; +- (void)refresh; +@end \ No newline at end of file diff --git a/pe/cocoa/DetailsPanel.m b/pe/cocoa/DetailsPanel.m new file mode 100644 index 00000000..00845b33 --- /dev/null +++ b/pe/cocoa/DetailsPanel.m @@ -0,0 +1,79 @@ +#import "Utils.h" +#import "NSNotificationAdditions.h" +#import "NSImageAdditions.h" +#import "PyDupeGuru.h" +#import "DetailsPanel.h" +#import "Consts.h" + +@implementation DetailsPanel +- (id)initWithPy:(PyApp *)aPy +{ + self = [super initWithWindowNibName:@"Details"]; + [self window]; //So the detailsTable is initialized. + [detailsTable setPy:aPy]; + py = aPy; + _needsRefresh = YES; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(duplicateSelectionChanged:) name:DuplicateSelectionChangedNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(imageLoaded:) name:ImageLoadedNotification object:self]; + return self; +} + +- (void)loadImageAsync:(NSString *)imagePath +{ + NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; + NSImage *image = [[NSImage alloc] initByReferencingFile:imagePath]; + NSImage *thumbnail = [image imageByScalingProportionallyToSize:NSMakeSize(512,512)]; + [image release]; + NSMutableDictionary *params = [NSMutableDictionary dictionary]; + [params setValue:imagePath forKey:@"imagePath"]; + [params setValue:thumbnail forKey:@"image"]; + [[NSNotificationCenter defaultCenter] postNotificationOnMainThreadWithName:ImageLoadedNotification object:self userInfo:params waitUntilDone:YES]; + [pool release]; +} + +- (void)refresh +{ + if (!_needsRefresh) + return; + [detailsTable reloadData]; + + NSString *refPath = [(PyDupeGuru *)py getSelectedDupeRefPath]; + if (_refPath != nil) + [_refPath autorelease]; + _refPath = [refPath retain]; + [NSThread detachNewThreadSelector:@selector(loadImageAsync:) toTarget:self withObject:refPath]; + NSString *dupePath = [(PyDupeGuru *)py getSelectedDupePath]; + if (_dupePath != nil) + [_dupePath autorelease]; + _dupePath = [dupePath retain]; + if (![dupePath isEqual: refPath]) + [NSThread detachNewThreadSelector:@selector(loadImageAsync:) toTarget:self withObject:dupePath]; + [refProgressIndicator startAnimation:nil]; + [dupeProgressIndicator startAnimation:nil]; + _needsRefresh = NO; +} + +/* Notifications */ +- (void)duplicateSelectionChanged:(NSNotification *)aNotification +{ + _needsRefresh = YES; + if ([[self window] isVisible]) + [self refresh]; +} + +- (void)imageLoaded:(NSNotification *)aNotification +{ + NSString *imagePath = [[aNotification userInfo] valueForKey:@"imagePath"]; + NSImage *image = [[aNotification userInfo] valueForKey:@"image"]; + if ([imagePath isEqual: _refPath]) + { + [refImage setImage:image]; + [refProgressIndicator stopAnimation:nil]; + } + if ([imagePath isEqual: _dupePath]) + { + [dupeImage setImage:image]; + [dupeProgressIndicator stopAnimation:nil]; + } +} +@end diff --git a/pe/cocoa/DirectoryPanel.h b/pe/cocoa/DirectoryPanel.h new file mode 100644 index 00000000..6033367f --- /dev/null +++ b/pe/cocoa/DirectoryPanel.h @@ -0,0 +1,8 @@ +#import +#import "dgbase/DirectoryPanel.h" + +@interface DirectoryPanel : DirectoryPanelBase +{ +} +- (IBAction)addiPhoto:(id)sender; +@end diff --git a/pe/cocoa/DirectoryPanel.m b/pe/cocoa/DirectoryPanel.m new file mode 100644 index 00000000..9347ddd5 --- /dev/null +++ b/pe/cocoa/DirectoryPanel.m @@ -0,0 +1,44 @@ +#import "DirectoryPanel.h" +#import "ProgressController.h" + +static NSString* jobAddIPhoto = @"jobAddIPhoto"; + +@implementation DirectoryPanel +- (id)initWithParentApp:(id)aParentApp +{ + self = [super initWithParentApp:aParentApp]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(jobCompleted:) name:JobCompletedNotification object:nil]; + return self; +} + +- (IBAction)addiPhoto:(id)sender +{ + [[ProgressController mainProgressController] setJobDesc:@"Adding iPhoto Library..."]; + [[ProgressController mainProgressController] setJobId:jobAddIPhoto]; + [[ProgressController mainProgressController] showSheetForParent:[self window]]; + [self addDirectory:@"iPhoto Library"]; +} + +- (IBAction)popupAddDirectoryMenu:(id)sender +{ + NSMenu *m = [addButtonPopUp menu]; + while ([m numberOfItems] > 0) + [m removeItemAtIndex:0]; + NSMenuItem *mi = [m addItemWithTitle:@"Add New Directory..." action:@selector(askForDirectory:) keyEquivalent:@""]; + [mi setTarget:self]; + mi = [m addItemWithTitle:@"Add iPhoto Directory" action:@selector(addiPhoto:) keyEquivalent:@""]; + [mi setTarget:self]; + [m addItem:[NSMenuItem separatorItem]]; + [_recentDirectories fillMenu:m]; + [addButtonPopUp selectItem:nil]; + [[addButtonPopUp cell] performClickWithFrame:[sender frame] inView:[sender superview]]; +} + +- (void)jobCompleted:(NSNotification *)aNotification +{ + if ([[ProgressController mainProgressController] jobId] == jobAddIPhoto) + { + [directories reloadData]; + } +} +@end diff --git a/pe/cocoa/English.lproj/Details.nib/classes.nib b/pe/cocoa/English.lproj/Details.nib/classes.nib new file mode 100644 index 00000000..f39cb617 --- /dev/null +++ b/pe/cocoa/English.lproj/Details.nib/classes.nib @@ -0,0 +1,24 @@ +{ + IBClasses = ( + { + CLASS = DetailsPanel; + LANGUAGE = ObjC; + OUTLETS = { + detailsTable = NSTableView; + dupeImage = NSImageView; + dupeProgressIndicator = NSProgressIndicator; + refImage = NSImageView; + refProgressIndicator = NSProgressIndicator; + }; + SUPERCLASS = NSWindowController; + }, + {CLASS = FirstResponder; LANGUAGE = ObjC; SUPERCLASS = NSObject; }, + { + CLASS = TableView; + LANGUAGE = ObjC; + OUTLETS = {py = PyApp; }; + SUPERCLASS = NSTableView; + } + ); + IBVersion = 1; +} \ No newline at end of file diff --git a/pe/cocoa/English.lproj/Details.nib/info.nib b/pe/cocoa/English.lproj/Details.nib/info.nib new file mode 100644 index 00000000..3ad9dc9e --- /dev/null +++ b/pe/cocoa/English.lproj/Details.nib/info.nib @@ -0,0 +1,16 @@ + + + + + IBDocumentLocation + 701 68 356 240 0 0 1440 878 + IBFramework Version + 446.1 + IBOpenObjects + + 5 + + IBSystem Version + 8R2232 + + diff --git a/pe/cocoa/English.lproj/Details.nib/keyedobjects.nib b/pe/cocoa/English.lproj/Details.nib/keyedobjects.nib new file mode 100644 index 0000000000000000000000000000000000000000..dfc61c70bdcc3191c61f1ae5160b485223253e9f GIT binary patch literal 9001 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 AaR2}S literal 0 HcmV?d00001 diff --git a/pe/cocoa/English.lproj/Directories.nib/classes.nib b/pe/cocoa/English.lproj/Directories.nib/classes.nib new file mode 100644 index 00000000..3ebaa96a --- /dev/null +++ b/pe/cocoa/English.lproj/Directories.nib/classes.nib @@ -0,0 +1,62 @@ + + + + + IBClasses + + + CLASS + FirstResponder + LANGUAGE + ObjC + SUPERCLASS + NSObject + + + ACTIONS + + askForDirectory + id + changeDirectoryState + id + popupAddDirectoryMenu + id + removeSelectedDirectory + id + toggleVisible + id + + CLASS + DirectoryPanel + LANGUAGE + ObjC + OUTLETS + + addButtonPopUp + NSPopUpButton + directories + NSOutlineView + removeButton + NSButton + + SUPERCLASS + DirectoryPanelBase + + + CLASS + OutlineView + LANGUAGE + ObjC + OUTLETS + + py + PyApp + + SUPERCLASS + NSOutlineView + + + IBVersion + 1 + + diff --git a/pe/cocoa/English.lproj/Directories.nib/info.nib b/pe/cocoa/English.lproj/Directories.nib/info.nib new file mode 100644 index 00000000..77f19ce7 --- /dev/null +++ b/pe/cocoa/English.lproj/Directories.nib/info.nib @@ -0,0 +1,20 @@ + + + + + IBFramework Version + 629 + IBLastKnownRelativeProjectPath + ../../dupeguru.xcodeproj + IBOldestOS + 5 + IBOpenObjects + + 5 + + IBSystem Version + 9B18 + targetFramework + IBCocoaFramework + + diff --git a/pe/cocoa/English.lproj/Directories.nib/keyedobjects.nib b/pe/cocoa/English.lproj/Directories.nib/keyedobjects.nib new file mode 100644 index 0000000000000000000000000000000000000000..906ea9c34734659e9d70e20070a6b8f4dc797a98 GIT binary patch literal 9698 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 LQhrn4zM20Aagm87 literal 0 HcmV?d00001 diff --git a/pe/cocoa/English.lproj/InfoPlist.strings b/pe/cocoa/English.lproj/InfoPlist.strings new file mode 100644 index 0000000000000000000000000000000000000000..d224a14bd2062242e455ea370c921c67e7045c94 GIT binary patch literal 204 zcmW-bOA5k35Cv=PDT2!&D(*yFxrrby%n4#XD*i$e6}^#{RLh~Ed*=0fHS_s0A|_(R zm7I(d2VRsEYIkQtt8(SyjGUEy>8yVS6Mq literal 0 HcmV?d00001 diff --git a/pe/cocoa/English.lproj/MainMenu.nib/classes.nib b/pe/cocoa/English.lproj/MainMenu.nib/classes.nib new file mode 100644 index 00000000..fbce5b56 --- /dev/null +++ b/pe/cocoa/English.lproj/MainMenu.nib/classes.nib @@ -0,0 +1,235 @@ + + + + + IBClasses + + + CLASS + NSSegmentedControl + LANGUAGE + ObjC + SUPERCLASS + NSControl + + + ACTIONS + + openWebsite + id + toggleDetailsPanel + id + toggleDirectories + id + unlockApp + id + + CLASS + AppDelegate + LANGUAGE + ObjC + OUTLETS + + py + PyDupeGuru + recentDirectories + RecentDirectories + result + ResultWindow + unlockMenuItem + NSMenuItem + + SUPERCLASS + NSObject + + + CLASS + PyApp + LANGUAGE + ObjC + SUPERCLASS + NSObject + + + CLASS + MatchesView + LANGUAGE + ObjC + SUPERCLASS + OutlineView + + + CLASS + PyDupeGuru + LANGUAGE + ObjC + SUPERCLASS + PyApp + + + ACTIONS + + changeDelta + id + changePowerMarker + id + clearIgnoreList + id + clearPictureCache + id + collapseAll + id + copyMarked + id + deleteMarked + id + expandAll + id + exportToXHTML + id + filter + id + ignoreSelected + id + markAll + id + markInvert + id + markNone + id + markSelected + id + markToggle + id + moveMarked + id + openSelected + id + refresh + id + removeMarked + id + removeSelected + id + renameSelected + id + resetColumnsToDefault + id + revealSelected + id + showPreferencesPanel + id + startDuplicateScan + id + switchSelected + id + toggleColumn + id + toggleDelta + id + toggleDetailsPanel + id + toggleDirectories + id + togglePowerMarker + id + + CLASS + ResultWindow + LANGUAGE + ObjC + OUTLETS + + actionMenu + NSPopUpButton + actionMenuView + NSView + app + id + columnsMenu + NSMenu + deltaSwitch + NSSegmentedControl + deltaSwitchView + NSView + filterField + NSSearchField + filterFieldView + NSView + matches + MatchesView + pmSwitch + NSSegmentedControl + pmSwitchView + NSView + preferencesPanel + NSWindow + py + PyDupeGuru + stats + NSTextField + + SUPERCLASS + NSWindowController + + + CLASS + FirstResponder + LANGUAGE + ObjC + SUPERCLASS + NSObject + + + ACTIONS + + checkForUpdates + id + + CLASS + SUUpdater + LANGUAGE + ObjC + SUPERCLASS + NSObject + + + ACTIONS + + clearMenu + id + menuClick + id + + CLASS + RecentDirectories + LANGUAGE + ObjC + OUTLETS + + delegate + id + menu + NSMenu + + SUPERCLASS + NSObject + + + CLASS + OutlineView + LANGUAGE + ObjC + OUTLETS + + py + PyApp + + SUPERCLASS + NSOutlineView + + + IBVersion + 1 + + diff --git a/pe/cocoa/English.lproj/MainMenu.nib/info.nib b/pe/cocoa/English.lproj/MainMenu.nib/info.nib new file mode 100644 index 00000000..a75b8270 --- /dev/null +++ b/pe/cocoa/English.lproj/MainMenu.nib/info.nib @@ -0,0 +1,20 @@ + + + + + IBFramework Version + 629 + IBLastKnownRelativeProjectPath + ../../dupeguru.xcodeproj + IBOldestOS + 4 + IBOpenObjects + + 524 + + IBSystem Version + 9B18 + targetFramework + IBCocoaFramework + + diff --git a/pe/cocoa/English.lproj/MainMenu.nib/keyedobjects.nib b/pe/cocoa/English.lproj/MainMenu.nib/keyedobjects.nib new file mode 100644 index 0000000000000000000000000000000000000000..1c9f4e3c32d40623fd26a3a2d4f3bff499750da6 GIT binary patch literal 45275 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

NS*?bdVCbz5HmH(L&L*Ij$7Si*|A*Zmt)+P9 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 + + + + CFBundleDevelopmentRegion + English + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleHelpBookFolder + dupeguru_pe_help + CFBundleHelpBookName + dupeGuru PE Help + CFBundleIconFile + dupeguru + CFBundleIdentifier + com.hardcoded_software.dupeguru_pe + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + APPL + CFBundleSignature + hsft + CFBundleVersion + 1.7.2 + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + SUFeedURL + http://www.hardcoded.net/updates/dupeguru_pe.appcast + + diff --git a/pe/cocoa/PictureBlocks.h b/pe/cocoa/PictureBlocks.h new file mode 100644 index 00000000..6b1a5160 --- /dev/null +++ b/pe/cocoa/PictureBlocks.h @@ -0,0 +1,11 @@ +#import + + +@interface PictureBlocks : NSObject { +} ++ (NSString *)getBlocksFromImagePath:(NSString *)imagePath blockCount:(NSNumber *)blockCount scanArea:(NSNumber *)scanArea; ++ (NSSize)getImageSize:(NSString *)imagePath; +@end + + +NSString* GetBlocks(NSString *filePath, int blockCount, int scanSize); \ No newline at end of file diff --git a/pe/cocoa/PictureBlocks.m b/pe/cocoa/PictureBlocks.m new file mode 100644 index 00000000..bc743694 --- /dev/null +++ b/pe/cocoa/PictureBlocks.m @@ -0,0 +1,139 @@ +#import "PictureBlocks.h" +#import "Utils.h" + + +@implementation PictureBlocks ++ (NSString *)getBlocksFromImagePath:(NSString *)imagePath blockCount:(NSNumber *)blockCount scanArea:(NSNumber *)scanArea +{ + return GetBlocks(imagePath, n2i(blockCount), n2i(scanArea)); +} + ++ (NSSize)getImageSize:(NSString *)imagePath +{ + CFURLRef fileURL = CFURLCreateWithFileSystemPath(NULL, (CFStringRef)imagePath, kCFURLPOSIXPathStyle, FALSE); + CGImageSourceRef source = CGImageSourceCreateWithURL(fileURL, NULL); + if (source == NULL) + return NSMakeSize(0, 0); + CGImageRef image = CGImageSourceCreateImageAtIndex(source, 0, NULL); + if (image == NULL) + return NSMakeSize(0, 0); + size_t width = CGImageGetWidth(image); + size_t height = CGImageGetHeight(image); + CGImageRelease(image); + CFRelease(source); + CFRelease(fileURL); + return NSMakeSize(width, height); +} +@end + + CGContextRef MyCreateBitmapContext (int width, int height) + { + CGContextRef context = NULL; + CGColorSpaceRef colorSpace; + void * bitmapData; + int bitmapByteCount; + int bitmapBytesPerRow; + + bitmapBytesPerRow = (width * 4); + bitmapByteCount = (bitmapBytesPerRow * height); + + colorSpace = CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB); + + bitmapData = malloc( bitmapByteCount ); + if (bitmapData == NULL) + { + fprintf (stderr, "Memory not allocated!"); + return NULL; + } + + context = CGBitmapContextCreate (bitmapData,width,height,8,bitmapBytesPerRow,colorSpace,kCGImageAlphaNoneSkipLast); + if (context== NULL) + { + free (bitmapData); + fprintf (stderr, "Context not created!"); + return NULL; + } + CGColorSpaceRelease( colorSpace ); + return context; + } + + // returns 0x00RRGGBB + int GetBlock(unsigned char *imageData, int imageWidth, int imageHeight, int boxX, int boxY, int boxW, int boxH) + { + int i,j; + int totalR = 0; + int totalG = 0; + int totalB = 0; + for(i = boxY; i < boxY + boxH; i++) + { + for(j = boxX; j < boxX + boxW; j++) + { + int offset = (i * imageWidth * 4) + (j * 4); + totalR += *(imageData + offset); + totalG += *(imageData + offset + 1); + totalB += *(imageData + offset + 2); + } + } + int pixelCount = boxH * boxW; + int result = 0; + result += (totalR / pixelCount) << 16; + result += (totalG / pixelCount) << 8; + result += (totalB / pixelCount); + return result; + } + + NSString* GetBlocks (NSString* filePath, int blockCount, int scanSize) + { + CFURLRef fileURL = CFURLCreateWithFileSystemPath(NULL, (CFStringRef)filePath, kCFURLPOSIXPathStyle, FALSE); + CGImageSourceRef source = CGImageSourceCreateWithURL(fileURL, NULL); + if (source == NULL) + return NULL; + CGImageRef image = CGImageSourceCreateImageAtIndex(source, 0, NULL); + if (image == NULL) + return NULL; + size_t width = CGImageGetWidth(image); + size_t height = CGImageGetHeight(image); + if ((scanSize > 0) && (width > scanSize)) + width = scanSize; + if ((scanSize > 0) && (height > scanSize)) + height = scanSize; + CGContextRef myContext = MyCreateBitmapContext(width, height); + CGRect myBoundingBox = CGRectMake (0, 0, width, height); + CGContextDrawImage(myContext, myBoundingBox, image); + unsigned char *bitmapData = CGBitmapContextGetData(myContext); + if (bitmapData == NULL) + return NULL; + + int blockHeight = height / blockCount; + if (blockHeight < 1) + blockHeight = 1; + int blockWidth = width / blockCount; + if (blockWidth < 1) + blockWidth = 1; + //blockCount might have changed + int blockXCount = (width / blockWidth); + int blockYCount = (height / blockHeight); + + CFMutableArrayRef blocks = CFArrayCreateMutable(NULL, blockXCount * blockYCount, &kCFTypeArrayCallBacks); + int i,j; + for(i = 0; i < blockYCount; i++) + { + for(j = 0; j < blockXCount; j++) + { + int block = GetBlock(bitmapData, width, height, j * blockWidth, i * blockHeight, blockWidth, blockHeight); + CFStringRef strBlock = CFStringCreateWithFormat(NULL, NULL, CFSTR("%06x"), block); + CFArrayAppendValue(blocks, strBlock); + CFRelease(strBlock); + } + } + + CGContextRelease (myContext); + if (bitmapData) free(bitmapData); + CGImageRelease(image); + CFRelease(source); + CFRelease(fileURL); + + CFStringRef result = CFStringCreateByCombiningStrings(NULL, blocks, CFSTR("")); + CFRelease(blocks); + return (NSString *)result; + } \ No newline at end of file diff --git a/pe/cocoa/PyDupeGuru.h b/pe/cocoa/PyDupeGuru.h new file mode 100644 index 00000000..8ced12b6 --- /dev/null +++ b/pe/cocoa/PyDupeGuru.h @@ -0,0 +1,9 @@ +#import +#import "dgbase/PyDupeGuru.h" + +@interface PyDupeGuru : PyDupeGuruBase +- (void)clearPictureCache; +- (NSString *)getSelectedDupePath; +- (NSString *)getSelectedDupeRefPath; +- (void)setMatchScaled:(NSNumber *)match_scaled; +@end diff --git a/pe/cocoa/ResultWindow.h b/pe/cocoa/ResultWindow.h new file mode 100644 index 00000000..dcc96079 --- /dev/null +++ b/pe/cocoa/ResultWindow.h @@ -0,0 +1,59 @@ +#import +#import "Outline.h" +#import "dgbase/ResultWindow.h" +#import "DirectoryPanel.h" + +@interface ResultWindow : ResultWindowBase +{ + IBOutlet NSPopUpButton *actionMenu; + IBOutlet NSView *actionMenuView; + IBOutlet id app; + IBOutlet NSMenu *columnsMenu; + IBOutlet NSView *deltaSwitchView; + IBOutlet NSSearchField *filterField; + IBOutlet NSView *filterFieldView; + IBOutlet NSSegmentedControl *pmSwitch; + IBOutlet NSView *pmSwitchView; + IBOutlet NSWindow *preferencesPanel; + IBOutlet NSTextField *stats; + + NSMutableArray *_resultColumns; + NSMutableIndexSet *_deltaColumns; +} +- (IBAction)changePowerMarker:(id)sender; +- (IBAction)clearIgnoreList:(id)sender; +- (IBAction)clearPictureCache:(id)sender; +- (IBAction)exportToXHTML:(id)sender; +- (IBAction)filter:(id)sender; +- (IBAction)ignoreSelected:(id)sender; +- (IBAction)markAll:(id)sender; +- (IBAction)markInvert:(id)sender; +- (IBAction)markNone:(id)sender; +- (IBAction)markSelected:(id)sender; +- (IBAction)markToggle:(id)sender; +- (IBAction)openSelected:(id)sender; +- (IBAction)refresh:(id)sender; +- (IBAction)removeMarked:(id)sender; +- (IBAction)removeSelected:(id)sender; +- (IBAction)renameSelected:(id)sender; +- (IBAction)resetColumnsToDefault:(id)sender; +- (IBAction)revealSelected:(id)sender; +- (IBAction)showPreferencesPanel:(id)sender; +- (IBAction)startDuplicateScan:(id)sender; +- (IBAction)switchSelected:(id)sender; +- (IBAction)toggleColumn:(id)sender; +- (IBAction)toggleDelta:(id)sender; +- (IBAction)toggleDetailsPanel:(id)sender; +- (IBAction)togglePowerMarker:(id)sender; +- (IBAction)toggleDirectories:(id)sender; + +- (NSTableColumn *)getColumnForIdentifier:(int)aIdentifier title:(NSString *)aTitle width:(int)aWidth refCol:(NSTableColumn *)aColumn; +- (NSArray *)getColumnsOrder; +- (NSDictionary *)getColumnsWidth; +- (NSArray *)getSelected:(BOOL)aDupesOnly; +- (NSArray *)getSelectedPaths:(BOOL)aDupesOnly; +- (void)performPySelection:(NSArray *)aIndexPaths; +- (void)initResultColumns; +- (void)refreshStats; +- (void)restoreColumnsPosition:(NSArray *)aColumnsOrder widths:(NSDictionary *)aColumnsWidth; +@end diff --git a/pe/cocoa/ResultWindow.m b/pe/cocoa/ResultWindow.m new file mode 100644 index 00000000..e0679e4f --- /dev/null +++ b/pe/cocoa/ResultWindow.m @@ -0,0 +1,569 @@ +#import "ResultWindow.h" +#import "Dialogs.h" +#import "ProgressController.h" +#import "RegistrationInterface.h" +#import "Utils.h" +#import "AppDelegate.h" +#import "Consts.h" + +static NSString* tbbDirectories = @"tbbDirectories"; +static NSString* tbbDetails = @"tbbDetail"; +static NSString* tbbPreferences = @"tbbPreferences"; +static NSString* tbbPowerMarker = @"tbbPowerMarker"; +static NSString* tbbScan = @"tbbScan"; +static NSString* tbbAction = @"tbbAction"; +static NSString* tbbDelta = @"tbbDelta"; +static NSString* tbbFilter = @"tbbFilter"; + +@implementation ResultWindow +/* Override */ +- (void)awakeFromNib +{ + [super awakeFromNib]; + _displayDelta = NO; + _powerMode = NO; + _deltaColumns = [[NSMutableIndexSet indexSetWithIndexesInRange:NSMakeRange(2,5)] retain]; + [_deltaColumns removeIndex:3]; + [_deltaColumns removeIndex:4]; + [deltaSwitch setSelectedSegment:0]; + [pmSwitch setSelectedSegment:0]; + [py setDisplayDeltaValues:b2n(_displayDelta)]; + [matches setTarget:self]; + [matches setDoubleAction:@selector(openSelected:)]; + [[actionMenu itemAtIndex:0] setImage:[NSImage imageNamed: @"gear"]]; + [self initResultColumns]; + [self refreshStats]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(resultsMarkingChanged:) name:ResultsMarkingChangedNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(resultsChanged:) name:ResultsChangedNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(jobCompleted:) name:JobCompletedNotification object:nil]; + + NSToolbar *t = [[[NSToolbar alloc] initWithIdentifier:@"ResultWindowToolbar"] autorelease]; + [t setAllowsUserCustomization:YES]; + [t setAutosavesConfiguration:NO]; + [t setDisplayMode:NSToolbarDisplayModeIconAndLabel]; + [t setDelegate:self]; + [[self window] setToolbar:t]; +} + +/* Actions */ + +- (IBAction)changePowerMarker:(id)sender +{ + _powerMode = [pmSwitch selectedSegment] == 1; + if (_powerMode) + [matches setTag:2]; + else + [matches setTag:0]; + [self expandAll:nil]; + [self outlineView:matches didClickTableColumn:nil]; +} + +- (IBAction)clearIgnoreList:(id)sender +{ + int i = n2i([py getIgnoreListCount]); + if (!i) + return; + if ([Dialogs askYesNo:[NSString stringWithFormat:@"Do you really want to remove all %d items from the ignore list?",i]] == NSAlertSecondButtonReturn) // NO + return; + [py clearIgnoreList]; +} + +- (IBAction)clearPictureCache:(id)sender +{ + if ([Dialogs askYesNo:@"Do you really want to remove all your cached picture analysis?"] == NSAlertSecondButtonReturn) // NO + return; + [(PyDupeGuru *)py clearPictureCache]; +} + +- (IBAction)exportToXHTML:(id)sender +{ + NSString *xsltPath = [[NSBundle mainBundle] pathForResource:@"dg" ofType:@"xsl"]; + NSString *cssPath = [[NSBundle mainBundle] pathForResource:@"hardcoded" ofType:@"css"]; + NSString *exported = [py exportToXHTMLwithColumns:[self getColumnsOrder] xslt:xsltPath css:cssPath]; + [[NSWorkspace sharedWorkspace] openFile:exported]; +} + +- (IBAction)filter:(id)sender +{ + NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; + [py setEscapeFilterRegexp:b2n(!n2b([ud objectForKey:@"useRegexpFilter"]))]; + [py applyFilter:[filterField stringValue]]; + [[NSNotificationCenter defaultCenter] postNotificationName:ResultsChangedNotification object:self]; +} + +- (IBAction)ignoreSelected:(id)sender +{ + NSArray *nodeList = [self getSelected:YES]; + if (![nodeList count]) + return; + if ([Dialogs askYesNo:[NSString stringWithFormat:@"All selected %d matches are going to be ignored in all subsequent scans. Continue?",[nodeList count]]] == NSAlertSecondButtonReturn) // NO + return; + [self performPySelection:[self getSelectedPaths:YES]]; + [py addSelectedToIgnoreList]; + [py removeSelected]; + [[NSNotificationCenter defaultCenter] postNotificationName:ResultsChangedNotification object:self]; +} + +- (IBAction)markAll:(id)sender +{ + [py markAll]; + [[NSNotificationCenter defaultCenter] postNotificationName:ResultsMarkingChangedNotification object:self]; +} + +- (IBAction)markInvert:(id)sender +{ + [py markInvert]; + [[NSNotificationCenter defaultCenter] postNotificationName:ResultsMarkingChangedNotification object:self]; +} + +- (IBAction)markNone:(id)sender +{ + [py markNone]; + [[NSNotificationCenter defaultCenter] postNotificationName:ResultsMarkingChangedNotification object:self]; +} + +- (IBAction)markSelected:(id)sender +{ + [self performPySelection:[self getSelectedPaths:YES]]; + [py toggleSelectedMark]; + [[NSNotificationCenter defaultCenter] postNotificationName:ResultsMarkingChangedNotification object:self]; +} + +- (IBAction)markToggle:(id)sender +{ + OVNode *node = [matches itemAtRow:[matches clickedRow]]; + [self performPySelection:[NSArray arrayWithObject:p2a([node indexPath])]]; + [py toggleSelectedMark]; + [[NSNotificationCenter defaultCenter] postNotificationName:ResultsMarkingChangedNotification object:self]; +} + +- (IBAction)openSelected:(id)sender +{ + [self performPySelection:[self getSelectedPaths:NO]]; + [py openSelected]; +} + +- (IBAction)refresh:(id)sender +{ + [[NSNotificationCenter defaultCenter] postNotificationName:ResultsChangedNotification object:self]; +} + +- (IBAction)removeMarked:(id)sender +{ + int mark_count = [[py getMarkCount] intValue]; + if (!mark_count) + return; + if ([Dialogs askYesNo:[NSString stringWithFormat:@"You are about to remove %d files from results. Continue?",mark_count]] == NSAlertSecondButtonReturn) // NO + return; + [py removeMarked]; + [[NSNotificationCenter defaultCenter] postNotificationName:ResultsChangedNotification object:self]; +} + +- (IBAction)removeSelected:(id)sender +{ + NSArray *nodeList = [self getSelected:YES]; + if (![nodeList count]) + return; + if ([Dialogs askYesNo:[NSString stringWithFormat:@"You are about to remove %d files from results. Continue?",[nodeList count]]] == NSAlertSecondButtonReturn) // NO + return; + [self performPySelection:[self getSelectedPaths:YES]]; + [py removeSelected]; + [[NSNotificationCenter defaultCenter] postNotificationName:ResultsChangedNotification object:self]; +} + +- (IBAction)renameSelected:(id)sender +{ + int col = [matches columnWithIdentifier:@"0"]; + int row = [matches selectedRow]; + [matches editColumn:col row:row withEvent:[NSApp currentEvent] select:YES]; +} + +- (IBAction)resetColumnsToDefault:(id)sender +{ + NSMutableArray *columnsOrder = [NSMutableArray array]; + [columnsOrder addObject:@"0"]; + [columnsOrder addObject:@"1"]; + [columnsOrder addObject:@"2"]; + [columnsOrder addObject:@"4"]; + [columnsOrder addObject:@"7"]; + NSMutableDictionary *columnsWidth = [NSMutableDictionary dictionary]; + [columnsWidth setObject:i2n(125) forKey:@"0"]; + [columnsWidth setObject:i2n(120) forKey:@"1"]; + [columnsWidth setObject:i2n(63) forKey:@"2"]; + [columnsWidth setObject:i2n(73) forKey:@"4"]; + [columnsWidth setObject:i2n(58) forKey:@"7"]; + [self restoreColumnsPosition:columnsOrder widths:columnsWidth]; +} + +- (IBAction)revealSelected:(id)sender +{ + [self performPySelection:[self getSelectedPaths:NO]]; + [py revealSelected]; +} + +- (IBAction)showPreferencesPanel:(id)sender +{ + [preferencesPanel makeKeyAndOrderFront:sender]; +} + +- (IBAction)startDuplicateScan:(id)sender +{ + if ([matches numberOfRows] > 0) + { + if ([Dialogs askYesNo:@"Are you sure you want to start a new duplicate scan?"] == NSAlertSecondButtonReturn) // NO + return; + } + NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; + PyDupeGuru *_py = (PyDupeGuru *)py; + [_py setMinMatchPercentage:[ud objectForKey:@"minMatchPercentage"]]; + [_py setMixFileKind:[ud objectForKey:@"mixFileKind"]]; + [_py setMatchScaled:[ud objectForKey:@"matchScaled"]]; + int r = n2i([py doScan]); + [matches reloadData]; + [self refreshStats]; + if (r != 0) + [[ProgressController mainProgressController] hide]; + if (r == 1) + [Dialogs showMessage:@"You cannot make a duplicate scan with only reference directories."]; + if (r == 3) + { + [Dialogs showMessage:@"The selected directories contain no scannable file."]; + [app toggleDirectories:nil]; + } +} + +- (IBAction)switchSelected:(id)sender +{ + [self performPySelection:[self getSelectedPaths:YES]]; + [py makeSelectedReference]; + [[NSNotificationCenter defaultCenter] postNotificationName:ResultsChangedNotification object:self]; +} + +- (IBAction)toggleColumn:(id)sender +{ + NSMenuItem *mi = sender; + NSString *colId = [NSString stringWithFormat:@"%d",[mi tag]]; + NSTableColumn *col = [matches tableColumnWithIdentifier:colId]; + if (col == nil) + { + //Add Column + col = [_resultColumns objectAtIndex:[mi tag]]; + [matches addTableColumn:col]; + [mi setState:NSOnState]; + } + else + { + //Remove column + [matches removeTableColumn:col]; + [mi setState:NSOffState]; + } +} + +- (IBAction)toggleDelta:(id)sender +{ + if ([deltaSwitch selectedSegment] == 1) + [deltaSwitch setSelectedSegment:0]; + else + [deltaSwitch setSelectedSegment:1]; + [self changeDelta:sender]; +} + + +- (IBAction)toggleDetailsPanel:(id)sender +{ + [(AppDelegate *)app toggleDetailsPanel:sender]; +} + +- (IBAction)togglePowerMarker:(id)sender +{ + if ([pmSwitch selectedSegment] == 1) + [pmSwitch setSelectedSegment:0]; + else + [pmSwitch setSelectedSegment:1]; + [self changePowerMarker:sender]; +} + +- (IBAction)toggleDirectories:(id)sender +{ + [(AppDelegate *)app toggleDirectories:sender]; +} + +/* Public */ +- (NSTableColumn *)getColumnForIdentifier:(int)aIdentifier title:(NSString *)aTitle width:(int)aWidth refCol:(NSTableColumn *)aColumn +{ + NSNumber *n = [NSNumber numberWithInt:aIdentifier]; + NSTableColumn *col = [[NSTableColumn alloc] initWithIdentifier:[n stringValue]]; + [col setWidth:aWidth]; + [col setEditable:NO]; + [[col dataCell] setFont:[[aColumn dataCell] font]]; + [[col headerCell] setStringValue:aTitle]; + [col setResizingMask:NSTableColumnUserResizingMask]; + [col setSortDescriptorPrototype:[[NSSortDescriptor alloc] initWithKey:[n stringValue] ascending:YES]]; + return col; +} + +//Returns an array of identifiers, in order. +- (NSArray *)getColumnsOrder +{ + NSTableColumn *col; + NSString *colId; + NSMutableArray *result = [NSMutableArray array]; + NSEnumerator *e = [[matches tableColumns] objectEnumerator]; + while (col = [e nextObject]) + { + colId = [col identifier]; + [result addObject:colId]; + } + return result; +} + +- (NSDictionary *)getColumnsWidth +{ + NSMutableDictionary *result = [NSMutableDictionary dictionary]; + NSTableColumn *col; + NSString *colId; + NSNumber *width; + NSEnumerator *e = [[matches tableColumns] objectEnumerator]; + while (col = [e nextObject]) + { + colId = [col identifier]; + width = [NSNumber numberWithFloat:[col width]]; + [result setObject:width forKey:colId]; + } + return result; +} + +- (NSArray *)getSelected:(BOOL)aDupesOnly +{ + if (_powerMode) + aDupesOnly = NO; + NSIndexSet *indexes = [matches selectedRowIndexes]; + NSMutableArray *nodeList = [NSMutableArray array]; + OVNode *node; + int i = [indexes firstIndex]; + while (i != NSNotFound) + { + node = [matches itemAtRow:i]; + if (!aDupesOnly || ([node level] > 1)) + [nodeList addObject:node]; + i = [indexes indexGreaterThanIndex:i]; + } + return nodeList; +} + +- (NSArray *)getSelectedPaths:(BOOL)aDupesOnly +{ + NSMutableArray *r = [NSMutableArray array]; + NSArray *selected = [self getSelected:aDupesOnly]; + NSEnumerator *e = [selected objectEnumerator]; + OVNode *node; + while (node = [e nextObject]) + [r addObject:p2a([node indexPath])]; + return r; +} + +- (void)performPySelection:(NSArray *)aIndexPaths +{ + if (_powerMode) + [py selectPowerMarkerNodePaths:aIndexPaths]; + else + [py selectResultNodePaths:aIndexPaths]; +} + +- (void)initResultColumns +{ + NSTableColumn *refCol = [matches tableColumnWithIdentifier:@"0"]; + _resultColumns = [[NSMutableArray alloc] init]; + [_resultColumns addObject:[matches tableColumnWithIdentifier:@"0"]]; // File Name + [_resultColumns addObject:[matches tableColumnWithIdentifier:@"1"]]; // Directory + [_resultColumns addObject:[matches tableColumnWithIdentifier:@"2"]]; // Size + [_resultColumns addObject:[self getColumnForIdentifier:3 title:@"Kind" width:40 refCol:refCol]]; + [_resultColumns addObject:[self getColumnForIdentifier:4 title:@"Dimensions" width:80 refCol:refCol]]; + [_resultColumns addObject:[self getColumnForIdentifier:5 title:@"Creation" width:120 refCol:refCol]]; + [_resultColumns addObject:[self getColumnForIdentifier:6 title:@"Modification" width:120 refCol:refCol]]; + [_resultColumns addObject:[matches tableColumnWithIdentifier:@"7"]]; // Match % + [_resultColumns addObject:[self getColumnForIdentifier:8 title:@"Dupe Count" width:80 refCol:refCol]]; +} + +-(void)refreshStats +{ + [stats setStringValue:[py getStatLine]]; +} + +- (void)restoreColumnsPosition:(NSArray *)aColumnsOrder widths:(NSDictionary *)aColumnsWidth +{ + NSTableColumn *col; + NSString *colId; + NSNumber *width; + NSMenuItem *mi; + //Remove all columns + NSEnumerator *e = [[columnsMenu itemArray] objectEnumerator]; + while (mi = [e nextObject]) + { + if ([mi state] == NSOnState) + [self toggleColumn:mi]; + } + //Add columns and set widths + e = [aColumnsOrder objectEnumerator]; + while (colId = [e nextObject]) + { + if (![colId isEqual:@"mark"]) + { + col = [_resultColumns objectAtIndex:[colId intValue]]; + width = [aColumnsWidth objectForKey:[col identifier]]; + mi = [columnsMenu itemWithTag:[colId intValue]]; + if (width) + [col setWidth:[width floatValue]]; + [self toggleColumn:mi]; + } + } +} + +/* Delegate */ +- (void)outlineView:(NSOutlineView *)outlineView willDisplayCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn item:(id)item +{ + OVNode *node = item; + if ([[tableColumn identifier] isEqual:@"mark"]) + { + [cell setEnabled: [node isMarkable]]; + } + if ([cell isKindOfClass:[NSTextFieldCell class]]) + { + // Determine if the text color will be blue due to directory being reference. + NSTextFieldCell *textCell = cell; + if ([node isMarkable]) + [textCell setTextColor:[NSColor blackColor]]; + else + [textCell setTextColor:[NSColor blueColor]]; + if ((_displayDelta) && (_powerMode || ([node level] > 1))) + { + int i = [[tableColumn identifier] intValue]; + if ([_deltaColumns containsIndex:i]) + [textCell setTextColor:[NSColor orangeColor]]; + } + } +} + +- (void)outlineViewSelectionDidChange:(NSNotification *)notification +{ + [self performPySelection:[self getSelectedPaths:NO]]; + [py refreshDetailsWithSelected]; + [[NSNotificationCenter defaultCenter] postNotificationName:DuplicateSelectionChangedNotification object:self]; +} + +- (void)resultsChanged:(NSNotification *)aNotification +{ + [matches reloadData]; + [self expandAll:nil]; + [self outlineViewSelectionDidChange:nil]; + [self refreshStats]; +} + +- (void)resultsMarkingChanged:(NSNotification *)aNotification +{ + [matches invalidateMarkings]; + [self refreshStats]; +} + +/* Toolbar */ + +- (NSToolbarItem *)toolbar:(NSToolbar *)toolbar itemForItemIdentifier:(NSString *)itemIdentifier willBeInsertedIntoToolbar:(BOOL)flag +{ + NSToolbarItem *tbi = [[[NSToolbarItem alloc] initWithItemIdentifier:itemIdentifier] autorelease]; + if (itemIdentifier == tbbDirectories) + { + [tbi setLabel: @"Directories"]; + [tbi setToolTip: @"Show/Hide the directories panel."]; + [tbi setImage: [NSImage imageNamed: @"folder32"]]; + [tbi setTarget: self]; + [tbi setAction: @selector(toggleDirectories:)]; + } + else if (itemIdentifier == tbbDetails) + { + [tbi setLabel: @"Details"]; + [tbi setToolTip: @"Show/Hide the details panel."]; + [tbi setImage: [NSImage imageNamed: @"details32"]]; + [tbi setTarget: self]; + [tbi setAction: @selector(toggleDetailsPanel:)]; + } + else if (itemIdentifier == tbbPreferences) + { + [tbi setLabel: @"Preferences"]; + [tbi setToolTip: @"Show the preferences panel."]; + [tbi setImage: [NSImage imageNamed: @"preferences32"]]; + [tbi setTarget: self]; + [tbi setAction: @selector(showPreferencesPanel:)]; + } + else if (itemIdentifier == tbbPowerMarker) + { + [tbi setLabel: @"Power Marker"]; + [tbi setToolTip: @"When enabled, only the duplicates are shown, not the references."]; + [tbi setView:pmSwitchView]; + [tbi setMinSize:[pmSwitchView frame].size]; + [tbi setMaxSize:[pmSwitchView frame].size]; + } + else if (itemIdentifier == tbbScan) + { + [tbi setLabel: @"Start Scanning"]; + [tbi setToolTip: @"Start scanning for duplicates in the selected diectories."]; + [tbi setImage: [NSImage imageNamed: @"dgpe_logo_32"]]; + [tbi setTarget: self]; + [tbi setAction: @selector(startDuplicateScan:)]; + } + else if (itemIdentifier == tbbAction) + { + [tbi setLabel: @"Action"]; + [tbi setView:actionMenuView]; + [tbi setMinSize:[actionMenuView frame].size]; + [tbi setMaxSize:[actionMenuView frame].size]; + } + else if (itemIdentifier == tbbDelta) + { + [tbi setLabel: @"Delta Values"]; + [tbi setToolTip: @"When enabled, this option makes dupeGuru display, where applicable, delta values instead of absolute values."]; + [tbi setView:deltaSwitchView]; + [tbi setMinSize:[deltaSwitchView frame].size]; + [tbi setMaxSize:[deltaSwitchView frame].size]; + } + else if (itemIdentifier == tbbFilter) + { + [tbi setLabel: @"Filter"]; + [tbi setToolTip: @"Filters the results using regular expression."]; + [tbi setView:filterFieldView]; + [tbi setMinSize:[filterFieldView frame].size]; + [tbi setMaxSize:NSMakeSize(1000, [filterFieldView frame].size.height)]; + } + [tbi setPaletteLabel: [tbi label]]; + return tbi; +} + +- (NSArray *)toolbarAllowedItemIdentifiers:(NSToolbar *)toolbar +{ + return [NSArray arrayWithObjects: + tbbDirectories, + tbbDetails, + tbbPreferences, + tbbPowerMarker, + tbbScan, + tbbAction, + tbbDelta, + tbbFilter, + NSToolbarSeparatorItemIdentifier, + NSToolbarSpaceItemIdentifier, + NSToolbarFlexibleSpaceItemIdentifier, + nil]; +} + +- (NSArray *)toolbarDefaultItemIdentifiers:(NSToolbar *)toolbar +{ + return [NSArray arrayWithObjects: + tbbScan, + tbbAction, + tbbDirectories, + tbbDetails, + tbbPowerMarker, + tbbDelta, + tbbFilter, + nil]; +} +@end \ No newline at end of file diff --git a/pe/cocoa/dupeguru.icns b/pe/cocoa/dupeguru.icns new file mode 100755 index 0000000000000000000000000000000000000000..c143ed8670c29efd911d9db402978756a26b01b5 GIT binary patch literal 59921 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 + + + + ActivePerspectiveName + Project + AllowedModules + + + BundleLoadPath + + MaxInstances + n + Module + PBXSmartGroupTreeModule + Name + Groups and Files Outline View + + + BundleLoadPath + + MaxInstances + n + Module + PBXNavigatorGroup + Name + Editor + + + BundleLoadPath + + MaxInstances + n + Module + XCTaskListModule + Name + Task List + + + BundleLoadPath + + MaxInstances + n + Module + XCDetailModule + Name + File and Smart Group Detail Viewer + + + BundleLoadPath + + MaxInstances + 1 + Module + PBXBuildResultsModule + Name + Detailed Build Results Viewer + + + BundleLoadPath + + MaxInstances + 1 + Module + PBXProjectFindModule + Name + Project Batch Find Tool + + + BundleLoadPath + + MaxInstances + n + Module + PBXRunSessionModule + Name + Run Log + + + BundleLoadPath + + MaxInstances + n + Module + PBXBookmarksModule + Name + Bookmarks Tool + + + BundleLoadPath + + MaxInstances + n + Module + PBXClassBrowserModule + Name + Class Browser + + + BundleLoadPath + + MaxInstances + n + Module + PBXCVSModule + Name + Source Code Control Tool + + + BundleLoadPath + + MaxInstances + n + Module + PBXDebugBreakpointsModule + Name + Debug Breakpoints Tool + + + BundleLoadPath + + MaxInstances + n + Module + XCDockableInspector + Name + Inspector + + + BundleLoadPath + + MaxInstances + n + Module + PBXOpenQuicklyModule + Name + Open Quickly Tool + + + BundleLoadPath + + MaxInstances + 1 + Module + PBXDebugSessionModule + Name + Debugger + + + BundleLoadPath + + MaxInstances + 1 + Module + PBXDebugCLIModule + Name + Debug Console + + + Description + DefaultDescriptionKey + DockingSystemVisible + + Extension + mode1 + FavBarConfig + + PBXProjectModuleGUID + CE381CB409914B41003581CE + XCBarModuleItemNames + + XCBarModuleItems + + + FirstTimeWindowDisplayed + + Identifier + com.apple.perspectives.project.mode1 + MajorVersion + 31 + MinorVersion + 1 + Name + Default + Notifications + + OpenEditors + + PerspectiveWidths + + -1 + -1 + + Perspectives + + + ChosenToolbarItems + + active-executable-popup + action + active-buildstyle-popup + active-target-popup + buildOrClean + build-and-runOrDebug + com.apple.ide.PBXToolbarStopButton + get-info + toggle-editor + + ControllerClassBaseName + + IconName + WindowOfProjectWithEditor + Identifier + perspective.project + IsVertical + + Layout + + + BecomeActive + + ContentConfiguration + + PBXBottomSmartGroupGIDs + + 1C37FBAC04509CD000000102 + 1C37FAAC04509CD000000102 + 1C08E77C0454961000C914BD + 1C37FABC05509CD000000102 + 1C37FABC05539CD112110102 + E2644B35053B69B200211256 + 1C37FABC04509CD000100104 + 1CC0EA4004350EF90044410B + 1CC0EA4004350EF90041110B + + PBXProjectModuleGUID + 1CE0B1FE06471DED0097A5F4 + PBXProjectModuleLabel + Files + PBXProjectStructureProvided + yes + PBXSmartGroupTreeModuleColumnData + + PBXSmartGroupTreeModuleColumnWidthsKey + + 194 + + PBXSmartGroupTreeModuleColumnsKey_v4 + + MainColumn + + + PBXSmartGroupTreeModuleOutlineStateKey_v7 + + PBXSmartGroupTreeModuleOutlineStateExpansionKey + + 29B97314FDCFA39411CA2CEA + 080E96DDFE201D6D7F000001 + 29B97315FDCFA39411CA2CEA + 29B97317FDCFA39411CA2CEA + 29B97323FDCFA39411CA2CEA + 1058C7A0FEA54F0111CA2CBB + 19C28FACFE9D520D11CA2CBB + 1C37FBAC04509CD000000102 + CE2ACA9A0CA214440012E1E8 + CE2ACA9B0CA214440012E1E8 + CE2ACA9C0CA214440012E1E8 + CE2ACA9D0CA214440012E1E8 + 1C37FAAC04509CD000000102 + 1C37FABC05509CD000000102 + + PBXSmartGroupTreeModuleOutlineStateSelectionKey + + + 17 + 15 + 0 + + + PBXSmartGroupTreeModuleOutlineStateVisibleRectKey + {{0, 0}, {194, 764}} + + PBXTopSmartGroupGIDs + + XCIncludePerspectivesSwitch + + XCSharingToken + com.apple.Xcode.GFSharingToken + + GeometryConfiguration + + Frame + {{0, 0}, {211, 782}} + GroupTreeTableConfiguration + + MainColumn + 194 + + RubberWindowFrame + 4 54 1366 823 0 0 1440 878 + + Module + PBXSmartGroupTreeModule + Proportion + 211pt + + + Dock + + + ContentConfiguration + + PBXProjectModuleGUID + 1CE0B20306471E060097A5F4 + PBXProjectModuleLabel + PyDupeGuru.h + PBXSplitModuleInNavigatorKey + + Split0 + + PBXProjectModuleGUID + 1CE0B20406471E060097A5F4 + PBXProjectModuleLabel + PyDupeGuru.h + _historyCapacity + 10 + bookmark + CE2ACAA20CA214B50012E1E8 + history + + CEE22C420B9A163B000D3096 + CEE584280BACAE4E004F9755 + CEE586070BACBCA6004F9755 + CEE301880BF7350900D6840C + CEE301890BF7350900D6840C + CEE3018A0BF7350900D6840C + CEE301DC0BF73A0300D6840C + CEE301DD0BF73A0300D6840C + CEE301F30BF73A9A00D6840C + CEB6A23E0C9EA80D00767CC9 + + prevStack + + CE2CB4DA09AE70AA0015538F + CEF411510A11093E00E7F110 + CE6E6AE70AA528B2002F29BE + CEFA86C20AAEE6DE00E0FAA1 + CEFA86C30AAEE6DE00E0FAA1 + CED527320AB0C9EB00D70726 + CEE2E1F30AB0E4A900D458B6 + CEE2E1F40AB0E4A900D458B6 + CEF650770ABABB44009F3C83 + CEE3018C0BF7350900D6840C + CEE3018F0BF7350900D6840C + + + SplitCount + 1 + + StatusBarVisibility + + + GeometryConfiguration + + Frame + {{0, 0}, {1150, 544}} + RubberWindowFrame + 4 54 1366 823 0 0 1440 878 + + Module + PBXNavigatorGroup + Proportion + 544pt + + + ContentConfiguration + + PBXProjectModuleGUID + 1CE0B20506471E060097A5F4 + PBXProjectModuleLabel + Detail + + GeometryConfiguration + + Frame + {{0, 549}, {1150, 233}} + RubberWindowFrame + 4 54 1366 823 0 0 1440 878 + + Module + XCDetailModule + Proportion + 233pt + + + Proportion + 1150pt + + + Name + Project + ServiceClasses + + XCModuleDock + PBXSmartGroupTreeModule + XCModuleDock + PBXNavigatorGroup + XCDetailModule + + TableOfContents + + CE2ACA9F0CA214440012E1E8 + 1CE0B1FE06471DED0097A5F4 + CE2ACAA00CA214440012E1E8 + 1CE0B20306471E060097A5F4 + 1CE0B20506471E060097A5F4 + + ToolbarConfiguration + xcode.toolbar.config.default + + + ControllerClassBaseName + + IconName + WindowOfProject + Identifier + perspective.morph + IsVertical + 0 + Layout + + + BecomeActive + 1 + ContentConfiguration + + PBXBottomSmartGroupGIDs + + 1C37FBAC04509CD000000102 + 1C37FAAC04509CD000000102 + 1C08E77C0454961000C914BD + 1C37FABC05509CD000000102 + 1C37FABC05539CD112110102 + E2644B35053B69B200211256 + 1C37FABC04509CD000100104 + 1CC0EA4004350EF90044410B + 1CC0EA4004350EF90041110B + + PBXProjectModuleGUID + 11E0B1FE06471DED0097A5F4 + PBXProjectModuleLabel + Files + PBXProjectStructureProvided + yes + PBXSmartGroupTreeModuleColumnData + + PBXSmartGroupTreeModuleColumnWidthsKey + + 186 + + PBXSmartGroupTreeModuleColumnsKey_v4 + + MainColumn + + + PBXSmartGroupTreeModuleOutlineStateKey_v7 + + PBXSmartGroupTreeModuleOutlineStateExpansionKey + + 29B97314FDCFA39411CA2CEA + 1C37FABC05509CD000000102 + + PBXSmartGroupTreeModuleOutlineStateSelectionKey + + + 0 + + + PBXSmartGroupTreeModuleOutlineStateVisibleRectKey + {{0, 0}, {186, 337}} + + PBXTopSmartGroupGIDs + + XCIncludePerspectivesSwitch + 1 + XCSharingToken + com.apple.Xcode.GFSharingToken + + GeometryConfiguration + + Frame + {{0, 0}, {203, 355}} + GroupTreeTableConfiguration + + MainColumn + 186 + + RubberWindowFrame + 373 269 690 397 0 0 1440 878 + + Module + PBXSmartGroupTreeModule + Proportion + 100% + + + Name + Morph + PreferredWidth + 300 + ServiceClasses + + XCModuleDock + PBXSmartGroupTreeModule + + TableOfContents + + 11E0B1FE06471DED0097A5F4 + + ToolbarConfiguration + xcode.toolbar.config.default.short + + + PerspectivesBarVisible + + ShelfIsVisible + + SourceDescription + file at '/System/Library/PrivateFrameworks/DevToolsInterface.framework/Versions/A/Resources/XCPerspectivesSpecificationMode1.xcperspec' + StatusbarIsVisible + + TimeStamp + 0.0 + ToolbarDisplayMode + 1 + ToolbarIsVisible + + ToolbarSizeMode + 1 + Type + Perspectives + UpdateMessage + The Default Workspace in this version of Xcode now includes support to hide and show the detail view (what has been referred to as the "Metro-Morph" feature). You must discard your current Default Workspace settings and update to the latest Default Workspace in order to gain this feature. Do you wish to update to the latest Workspace defaults for project '%@'? + WindowJustification + 5 + WindowOrderList + + /Users/hsoft/src/dupeguru_pe_cocoa/dupeguru.xcodeproj + + WindowString + 4 54 1366 823 0 0 1440 878 + WindowTools + + + FirstTimeWindowDisplayed + + Identifier + windowTool.build + IsVertical + + Layout + + + Dock + + + ContentConfiguration + + PBXProjectModuleGUID + 1CD0528F0623707200166675 + PBXProjectModuleLabel + + StatusBarVisibility + + + GeometryConfiguration + + Frame + {{0, 0}, {1366, 540}} + RubberWindowFrame + 0 56 1366 822 0 0 1440 878 + + Module + PBXNavigatorGroup + Proportion + 540pt + + + BecomeActive + + ContentConfiguration + + PBXProjectModuleGUID + XCMainBuildResultsModuleGUID + PBXProjectModuleLabel + Build + XCBuildResultsTrigger_Collapse + 1021 + XCBuildResultsTrigger_Open + 1011 + + GeometryConfiguration + + Frame + {{0, 545}, {1366, 236}} + RubberWindowFrame + 0 56 1366 822 0 0 1440 878 + + Module + PBXBuildResultsModule + Proportion + 236pt + + + Proportion + 781pt + + + Name + Build Results + ServiceClasses + + PBXBuildResultsModule + + StatusbarIsVisible + + TableOfContents + + CE381CCE09914BC8003581CE + CEB6A2360C9EA7D700767CC9 + 1CD0528F0623707200166675 + XCMainBuildResultsModuleGUID + + ToolbarConfiguration + xcode.toolbar.config.build + WindowString + 0 56 1366 822 0 0 1440 878 + WindowToolGUID + CE381CCE09914BC8003581CE + WindowToolIsVisible + + + + FirstTimeWindowDisplayed + + Identifier + windowTool.debugger + IsVertical + + Layout + + + Dock + + + ContentConfiguration + + Debugger + + HorizontalSplitView + + _collapsingFrameDimension + 0.0 + _indexOfCollapsedView + 0 + _percentageOfCollapsedView + 0.0 + isCollapsed + yes + sizes + + {{0, 0}, {150, 339}} + {{150, 0}, {874, 339}} + + + VerticalSplitView + + _collapsingFrameDimension + 0.0 + _indexOfCollapsedView + 0 + _percentageOfCollapsedView + 0.0 + isCollapsed + yes + sizes + + {{0, 0}, {1024, 339}} + {{0, 339}, {1024, 306}} + + + + LauncherConfigVersion + 8 + PBXProjectModuleGUID + 1C162984064C10D400B95A72 + PBXProjectModuleLabel + Debug - GLUTExamples (Underwater) + + GeometryConfiguration + + DebugConsoleDrawerSize + {100, 120} + DebugConsoleVisible + None + DebugConsoleWindowFrame + {{200, 200}, {500, 300}} + DebugSTDIOWindowFrame + {{200, 200}, {500, 300}} + Frame + {{0, 0}, {1024, 645}} + RubberWindowFrame + 328 62 1024 686 0 0 1440 878 + + Module + PBXDebugSessionModule + Proportion + 645pt + + + Proportion + 645pt + + + Name + Debugger + ServiceClasses + + PBXDebugSessionModule + + StatusbarIsVisible + + TableOfContents + + 1CD10A99069EF8BA00B06720 + CEC94B9A0BA3652F009F7CBD + 1C162984064C10D400B95A72 + CEC94B9B0BA3652F009F7CBD + CEC94B9C0BA3652F009F7CBD + CEC94B9D0BA3652F009F7CBD + CEC94B9E0BA3652F009F7CBD + CEC94B9F0BA3652F009F7CBD + CEC94BA00BA3652F009F7CBD + + ToolbarConfiguration + xcode.toolbar.config.debug + WindowString + 328 62 1024 686 0 0 1440 878 + WindowToolGUID + 1CD10A99069EF8BA00B06720 + WindowToolIsVisible + + + + FirstTimeWindowDisplayed + + Identifier + windowTool.find + IsVertical + + Layout + + + Dock + + + Dock + + + ContentConfiguration + + PBXProjectModuleGUID + 1CDD528C0622207200134675 + PBXProjectModuleLabel + + StatusBarVisibility + + + GeometryConfiguration + + Frame + {{0, 0}, {781, 212}} + RubberWindowFrame + 31 253 781 470 0 0 1024 746 + + Module + PBXNavigatorGroup + Proportion + 781pt + + + Proportion + 212pt + + + ContentConfiguration + + PBXProjectModuleGUID + 1CD0528E0623707200166675 + PBXProjectModuleLabel + Project Find + + GeometryConfiguration + + Frame + {{0, 217}, {781, 212}} + RubberWindowFrame + 31 253 781 470 0 0 1024 746 + + Module + PBXProjectFindModule + Proportion + 212pt + + + Proportion + 429pt + + + Name + Project Find + ServiceClasses + + PBXProjectFindModule + + StatusbarIsVisible + + TableOfContents + + 1C530D57069F1CE1000CFCEE + CE3755460A37628100022F3B + CE3755470A37628100022F3B + 1CDD528C0622207200134675 + 1CD0528E0623707200166675 + + WindowString + 31 253 781 470 0 0 1024 746 + WindowToolGUID + 1C530D57069F1CE1000CFCEE + WindowToolIsVisible + + + + Identifier + MENUSEPARATOR + + + FirstTimeWindowDisplayed + + Identifier + windowTool.debuggerConsole + IsVertical + + Layout + + + Dock + + + ContentConfiguration + + PBXProjectModuleGUID + 1C78EAAC065D492600B07095 + PBXProjectModuleLabel + Debugger Console + + GeometryConfiguration + + Frame + {{0, 0}, {440, 358}} + RubberWindowFrame + 72 414 440 400 0 0 1440 878 + + Module + PBXDebugCLIModule + Proportion + 358pt + + + Proportion + 359pt + + + Name + Debugger Console + ServiceClasses + + PBXDebugCLIModule + + StatusbarIsVisible + + TableOfContents + + CECD0ADE099294C1003DC359 + CEC94BA10BA3652F009F7CBD + 1C78EAAC065D492600B07095 + + WindowString + 72 414 440 400 0 0 1440 878 + WindowToolGUID + CECD0ADE099294C1003DC359 + WindowToolIsVisible + + + + FirstTimeWindowDisplayed + + Identifier + windowTool.run + IsVertical + + Layout + + + Dock + + + ContentConfiguration + + LauncherConfigVersion + 3 + PBXProjectModuleGUID + 1CD0528B0623707200166675 + PBXProjectModuleLabel + Run + Runner + + HorizontalSplitView + + _collapsingFrameDimension + 0.0 + _indexOfCollapsedView + 0 + _percentageOfCollapsedView + 0.0 + isCollapsed + yes + sizes + + {{0, 0}, {366, 168}} + {{0, 173}, {366, 270}} + + + VerticalSplitView + + _collapsingFrameDimension + 0.0 + _indexOfCollapsedView + 0 + _percentageOfCollapsedView + 0.0 + isCollapsed + yes + sizes + + {{0, 0}, {406, 443}} + {{411, 0}, {517, 443}} + + + + + GeometryConfiguration + + Frame + {{0, 0}, {1377, 781}} + RubberWindowFrame + 0 56 1377 822 0 0 1440 878 + + Module + PBXRunSessionModule + Proportion + 781pt + + + Proportion + 781pt + + + Name + Run Log + ServiceClasses + + PBXRunSessionModule + + StatusbarIsVisible + + TableOfContents + + 1C0AD2B3069F1EA900FABCE6 + CE89234E0BFF46580079C065 + 1CD0528B0623707200166675 + CE89234F0BFF46580079C065 + + ToolbarConfiguration + xcode.toolbar.config.run + WindowString + 0 56 1377 822 0 0 1440 878 + WindowToolGUID + 1C0AD2B3069F1EA900FABCE6 + WindowToolIsVisible + + + + Identifier + windowTool.scm + Layout + + + Dock + + + ContentConfiguration + + PBXProjectModuleGUID + 1C78EAB2065D492600B07095 + PBXProjectModuleLabel + <No Editor> + PBXSplitModuleInNavigatorKey + + Split0 + + PBXProjectModuleGUID + 1C78EAB3065D492600B07095 + + SplitCount + 1 + + StatusBarVisibility + 1 + + GeometryConfiguration + + Frame + {{0, 0}, {452, 0}} + RubberWindowFrame + 743 379 452 308 0 0 1280 1002 + + Module + PBXNavigatorGroup + Proportion + 0pt + + + BecomeActive + 1 + ContentConfiguration + + PBXProjectModuleGUID + 1CD052920623707200166675 + PBXProjectModuleLabel + SCM + + GeometryConfiguration + + ConsoleFrame + {{0, 259}, {452, 0}} + Frame + {{0, 7}, {452, 259}} + RubberWindowFrame + 743 379 452 308 0 0 1280 1002 + TableConfiguration + + Status + 30 + FileName + 199 + Path + 197.09500122070312 + + TableFrame + {{0, 0}, {452, 250}} + + Module + PBXCVSModule + Proportion + 262pt + + + Proportion + 266pt + + + Name + SCM + ServiceClasses + + PBXCVSModule + + StatusbarIsVisible + 1 + TableOfContents + + 1C78EAB4065D492600B07095 + 1C78EAB5065D492600B07095 + 1C78EAB2065D492600B07095 + 1CD052920623707200166675 + + ToolbarConfiguration + xcode.toolbar.config.scm + WindowString + 743 379 452 308 0 0 1280 1002 + + + FirstTimeWindowDisplayed + + Identifier + windowTool.breakpoints + IsVertical + + Layout + + + Dock + + + ContentConfiguration + + PBXBottomSmartGroupGIDs + + 1C77FABC04509CD000000102 + + PBXProjectModuleGUID + 1CE0B1FE06471DED0097A5F4 + PBXProjectModuleLabel + Files + PBXProjectStructureProvided + no + PBXSmartGroupTreeModuleColumnData + + PBXSmartGroupTreeModuleColumnWidthsKey + + 168 + + PBXSmartGroupTreeModuleColumnsKey_v4 + + MainColumn + + + PBXSmartGroupTreeModuleOutlineStateKey_v7 + + PBXSmartGroupTreeModuleOutlineStateExpansionKey + + 1C77FABC04509CD000000102 + + PBXSmartGroupTreeModuleOutlineStateSelectionKey + + + 0 + + + PBXSmartGroupTreeModuleOutlineStateVisibleRectKey + {{0, 0}, {168, 350}} + + PBXTopSmartGroupGIDs + + XCIncludePerspectivesSwitch + + + GeometryConfiguration + + Frame + {{0, 0}, {185, 368}} + GroupTreeTableConfiguration + + MainColumn + 168 + + RubberWindowFrame + 21 314 744 409 0 0 1024 746 + + Module + PBXSmartGroupTreeModule + Proportion + 185pt + + + BecomeActive + + ContentConfiguration + + PBXProjectModuleGUID + 1CA1AED706398EBD00589147 + PBXProjectModuleLabel + Detail + + GeometryConfiguration + + Frame + {{190, 0}, {554, 368}} + RubberWindowFrame + 21 314 744 409 0 0 1024 746 + + Module + XCDetailModule + Proportion + 554pt + + + Proportion + 368pt + + + MajorVersion + 2 + MinorVersion + 0 + Name + Breakpoints + ServiceClasses + + PBXSmartGroupTreeModule + XCDetailModule + + StatusbarIsVisible + + TableOfContents + + CEDA9EAC09D2BBCE00741F3F + CEDA9EAD09D2BBCE00741F3F + 1CE0B1FE06471DED0097A5F4 + 1CA1AED706398EBD00589147 + + ToolbarConfiguration + xcode.toolbar.config.breakpoints + WindowString + 21 314 744 409 0 0 1024 746 + WindowToolGUID + CEDA9EAC09D2BBCE00741F3F + WindowToolIsVisible + + + + Identifier + windowTool.debugAnimator + Layout + + + Dock + + + Module + PBXNavigatorGroup + Proportion + 100% + + + Proportion + 100% + + + Name + Debug Visualizer + ServiceClasses + + PBXNavigatorGroup + + StatusbarIsVisible + 1 + ToolbarConfiguration + xcode.toolbar.config.debugAnimator + WindowString + 100 100 700 500 0 0 1280 1002 + + + Identifier + windowTool.bookmarks + Layout + + + Dock + + + Module + PBXBookmarksModule + Proportion + 100% + + + Proportion + 100% + + + Name + Bookmarks + ServiceClasses + + PBXBookmarksModule + + StatusbarIsVisible + 0 + WindowString + 538 42 401 187 0 0 1280 1002 + + + Identifier + windowTool.classBrowser + Layout + + + Dock + + + BecomeActive + 1 + ContentConfiguration + + OptionsSetName + Hierarchy, all classes + PBXProjectModuleGUID + 1CA6456E063B45B4001379D8 + PBXProjectModuleLabel + Class Browser - NSObject + + GeometryConfiguration + + ClassesFrame + {{0, 0}, {374, 96}} + ClassesTreeTableConfiguration + + PBXClassNameColumnIdentifier + 208 + PBXClassBookColumnIdentifier + 22 + + Frame + {{0, 0}, {630, 331}} + MembersFrame + {{0, 105}, {374, 395}} + MembersTreeTableConfiguration + + PBXMemberTypeIconColumnIdentifier + 22 + PBXMemberNameColumnIdentifier + 216 + PBXMemberTypeColumnIdentifier + 97 + PBXMemberBookColumnIdentifier + 22 + + PBXModuleWindowStatusBarHidden2 + 1 + RubberWindowFrame + 385 179 630 352 0 0 1440 878 + + Module + PBXClassBrowserModule + Proportion + 332pt + + + Proportion + 332pt + + + Name + Class Browser + ServiceClasses + + PBXClassBrowserModule + + StatusbarIsVisible + 0 + TableOfContents + + 1C0AD2AF069F1E9B00FABCE6 + 1C0AD2B0069F1E9B00FABCE6 + 1CA6456E063B45B4001379D8 + + ToolbarConfiguration + xcode.toolbar.config.classbrowser + WindowString + 385 179 630 352 0 0 1440 878 + WindowToolGUID + 1C0AD2AF069F1E9B00FABCE6 + WindowToolIsVisible + 0 + + + + diff --git a/pe/cocoa/dupeguru.xcodeproj/project.pbxproj b/pe/cocoa/dupeguru.xcodeproj/project.pbxproj new file mode 100644 index 00000000..02043065 --- /dev/null +++ b/pe/cocoa/dupeguru.xcodeproj/project.pbxproj @@ -0,0 +1,588 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 42; + objects = { + +/* Begin PBXBuildFile section */ + 8D11072A0486CEB800E47090 /* MainMenu.nib in Resources */ = {isa = PBXBuildFile; fileRef = 29B97318FDCFA39411CA2CEA /* MainMenu.nib */; }; + 8D11072B0486CEB800E47090 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 089C165CFE840E0CC02AAC07 /* InfoPlist.strings */; }; + 8D11072D0486CEB800E47090 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 29B97316FDCFA39411CA2CEA /* main.m */; settings = {ATTRIBUTES = (); }; }; + 8D11072F0486CEB800E47090 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */; }; + CE073F6309CAE1A3005C1D2F /* dupeguru_pe_help in Resources */ = {isa = PBXBuildFile; fileRef = CE073F5409CAE1A3005C1D2F /* dupeguru_pe_help */; }; + CE0C46AA0FA0647E000BE99B /* PictureBlocks.m in Sources */ = {isa = PBXBuildFile; fileRef = CE0C46A90FA0647E000BE99B /* PictureBlocks.m */; }; + CE15C8A80ADEB8B50061D4A5 /* Sparkle.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE15C8A70ADEB8B50061D4A5 /* Sparkle.framework */; }; + CE15C8C00ADEB8D40061D4A5 /* Sparkle.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = CE15C8A70ADEB8B50061D4A5 /* Sparkle.framework */; }; + CE381C9609914ACE003581CE /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = CE381C9409914ACE003581CE /* AppDelegate.m */; }; + CE381C9C09914ADF003581CE /* ResultWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = CE381C9A09914ADF003581CE /* ResultWindow.m */; }; + CE381D0509915304003581CE /* dg_cocoa.plugin in Resources */ = {isa = PBXBuildFile; fileRef = CE381CF509915304003581CE /* dg_cocoa.plugin */; }; + CE3AA46709DB207900DB3A21 /* Directories.nib in Resources */ = {isa = PBXBuildFile; fileRef = CE3AA46509DB207900DB3A21 /* Directories.nib */; }; + CE68EE6809ABC48000971085 /* DirectoryPanel.m in Sources */ = {isa = PBXBuildFile; fileRef = CE68EE6609ABC48000971085 /* DirectoryPanel.m */; }; + CE80DB2E0FC192D60086DCA6 /* Dialogs.m in Sources */ = {isa = PBXBuildFile; fileRef = CE80DB1C0FC192D60086DCA6 /* Dialogs.m */; }; + CE80DB2F0FC192D60086DCA6 /* HSErrorReportWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = CE80DB1E0FC192D60086DCA6 /* HSErrorReportWindow.m */; }; + CE80DB300FC192D60086DCA6 /* Outline.m in Sources */ = {isa = PBXBuildFile; fileRef = CE80DB200FC192D60086DCA6 /* Outline.m */; }; + CE80DB310FC192D60086DCA6 /* ProgressController.m in Sources */ = {isa = PBXBuildFile; fileRef = CE80DB220FC192D60086DCA6 /* ProgressController.m */; }; + CE80DB320FC192D60086DCA6 /* RecentDirectories.m in Sources */ = {isa = PBXBuildFile; fileRef = CE80DB250FC192D60086DCA6 /* RecentDirectories.m */; }; + CE80DB330FC192D60086DCA6 /* RegistrationInterface.m in Sources */ = {isa = PBXBuildFile; fileRef = CE80DB270FC192D60086DCA6 /* RegistrationInterface.m */; }; + CE80DB340FC192D60086DCA6 /* Table.m in Sources */ = {isa = PBXBuildFile; fileRef = CE80DB290FC192D60086DCA6 /* Table.m */; }; + CE80DB350FC192D60086DCA6 /* Utils.m in Sources */ = {isa = PBXBuildFile; fileRef = CE80DB2B0FC192D60086DCA6 /* Utils.m */; }; + CE80DB360FC192D60086DCA6 /* ValueTransformers.m in Sources */ = {isa = PBXBuildFile; fileRef = CE80DB2D0FC192D60086DCA6 /* ValueTransformers.m */; }; + CE80DB470FC193650086DCA6 /* NSNotificationAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = CE80DB460FC193650086DCA6 /* NSNotificationAdditions.m */; }; + CE80DB4A0FC193770086DCA6 /* NSImageAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = CE80DB490FC193770086DCA6 /* NSImageAdditions.m */; }; + CE80DB760FC194760086DCA6 /* ErrorReportWindow.xib in Resources */ = {isa = PBXBuildFile; fileRef = CE80DB700FC194760086DCA6 /* ErrorReportWindow.xib */; }; + CE80DB770FC194760086DCA6 /* progress.nib in Resources */ = {isa = PBXBuildFile; fileRef = CE80DB720FC194760086DCA6 /* progress.nib */; }; + CE80DB780FC194760086DCA6 /* registration.nib in Resources */ = {isa = PBXBuildFile; fileRef = CE80DB740FC194760086DCA6 /* registration.nib */; }; + CE80DB8A0FC1951C0086DCA6 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = CE80DB830FC1951C0086DCA6 /* AppDelegate.m */; }; + CE80DB8B0FC1951C0086DCA6 /* DirectoryPanel.m in Sources */ = {isa = PBXBuildFile; fileRef = CE80DB860FC1951C0086DCA6 /* DirectoryPanel.m */; }; + CE80DB8C0FC1951C0086DCA6 /* ResultWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = CE80DB890FC1951C0086DCA6 /* ResultWindow.m */; }; + CE848A1909DD85810004CB44 /* Consts.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = CE848A1809DD85810004CB44 /* Consts.h */; }; + CECA899909DB12CA00A3D774 /* Details.nib in Resources */ = {isa = PBXBuildFile; fileRef = CECA899709DB12CA00A3D774 /* Details.nib */; }; + CECA899C09DB132E00A3D774 /* DetailsPanel.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = CECA899A09DB132E00A3D774 /* DetailsPanel.h */; }; + CECA899D09DB132E00A3D774 /* DetailsPanel.m in Sources */ = {isa = PBXBuildFile; fileRef = CECA899B09DB132E00A3D774 /* DetailsPanel.m */; }; + CEDA432E0B07C6E600B3091A /* dg.xsl in Resources */ = {isa = PBXBuildFile; fileRef = CEDA432C0B07C6E600B3091A /* dg.xsl */; }; + CEDA432F0B07C6E600B3091A /* hardcoded.css in Resources */ = {isa = PBXBuildFile; fileRef = CEDA432D0B07C6E600B3091A /* hardcoded.css */; }; + CEEB135209C837A2004D2330 /* dupeguru.icns in Resources */ = {isa = PBXBuildFile; fileRef = CEEB135109C837A2004D2330 /* dupeguru.icns */; }; + CEF4112B0A11069600E7F110 /* Consts.m in Sources */ = {isa = PBXBuildFile; fileRef = CEF4112A0A11069600E7F110 /* Consts.m */; }; + CEF7823809C8AA0200EF38FF /* gear.png in Resources */ = {isa = PBXBuildFile; fileRef = CEF7823709C8AA0200EF38FF /* gear.png */; }; + CEFC294609C89E3D00D9F998 /* folder32.png in Resources */ = {isa = PBXBuildFile; fileRef = CEFC294509C89E3D00D9F998 /* folder32.png */; }; + CEFC295509C89FF200D9F998 /* details32.png in Resources */ = {isa = PBXBuildFile; fileRef = CEFC295309C89FF200D9F998 /* details32.png */; }; + CEFC295609C89FF200D9F998 /* preferences32.png in Resources */ = {isa = PBXBuildFile; fileRef = CEFC295409C89FF200D9F998 /* preferences32.png */; }; + CEFCDE2D0AB0418600C33A93 /* dgpe_logo_32.png in Resources */ = {isa = PBXBuildFile; fileRef = CEFCDE2C0AB0418600C33A93 /* dgpe_logo_32.png */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + CECC02B709A36E8200CC0A94 /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + CE15C8C00ADEB8D40061D4A5 /* Sparkle.framework in CopyFiles */, + CECA899C09DB132E00A3D774 /* DetailsPanel.h in CopyFiles */, + CE848A1909DD85810004CB44 /* Consts.h in CopyFiles */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 089C165DFE840E0CC02AAC07 /* English */ = {isa = PBXFileReference; fileEncoding = 10; lastKnownFileType = text.plist.strings; name = English; path = English.lproj/InfoPlist.strings; sourceTree = ""; }; + 1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = /System/Library/Frameworks/Cocoa.framework; sourceTree = ""; }; + 13E42FB307B3F0F600E4EEF1 /* CoreData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreData.framework; path = /System/Library/Frameworks/CoreData.framework; sourceTree = ""; }; + 29B97316FDCFA39411CA2CEA /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = SOURCE_ROOT; }; + 29B97319FDCFA39411CA2CEA /* English */ = {isa = PBXFileReference; lastKnownFileType = wrapper.nib; name = English; path = English.lproj/MainMenu.nib; sourceTree = ""; }; + 29B97324FDCFA39411CA2CEA /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = /System/Library/Frameworks/AppKit.framework; sourceTree = ""; }; + 29B97325FDCFA39411CA2CEA /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = /System/Library/Frameworks/Foundation.framework; sourceTree = ""; }; + 8D1107310486CEB800E47090 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = SOURCE_ROOT; }; + 8D1107320486CEB800E47090 /* dupeGuru PE.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "dupeGuru PE.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + CE073F5409CAE1A3005C1D2F /* dupeguru_pe_help */ = {isa = PBXFileReference; lastKnownFileType = folder; name = dupeguru_pe_help; path = help/dupeguru_pe_help; sourceTree = SOURCE_ROOT; }; + CE0C46A80FA0647E000BE99B /* PictureBlocks.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PictureBlocks.h; sourceTree = ""; }; + CE0C46A90FA0647E000BE99B /* PictureBlocks.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PictureBlocks.m; sourceTree = ""; }; + CE15C8A70ADEB8B50061D4A5 /* Sparkle.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Sparkle.framework; path = /Library/Frameworks/Sparkle.framework; sourceTree = ""; }; + CE381C9409914ACE003581CE /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = SOURCE_ROOT; }; + CE381C9509914ACE003581CE /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = SOURCE_ROOT; }; + CE381C9A09914ADF003581CE /* ResultWindow.m */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.objc; path = ResultWindow.m; sourceTree = SOURCE_ROOT; }; + CE381C9B09914ADF003581CE /* ResultWindow.h */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.h; path = ResultWindow.h; sourceTree = SOURCE_ROOT; }; + CE381CF509915304003581CE /* dg_cocoa.plugin */ = {isa = PBXFileReference; lastKnownFileType = folder; name = dg_cocoa.plugin; path = py/dist/dg_cocoa.plugin; sourceTree = SOURCE_ROOT; }; + CE3AA46609DB207900DB3A21 /* English */ = {isa = PBXFileReference; lastKnownFileType = wrapper.nib; name = English; path = English.lproj/Directories.nib; sourceTree = ""; }; + CE68EE6509ABC48000971085 /* DirectoryPanel.h */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.h; path = DirectoryPanel.h; sourceTree = SOURCE_ROOT; }; + CE68EE6609ABC48000971085 /* DirectoryPanel.m */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.objc; path = DirectoryPanel.m; sourceTree = SOURCE_ROOT; }; + CE80DB1B0FC192D60086DCA6 /* Dialogs.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Dialogs.h; path = cocoalib/Dialogs.h; sourceTree = SOURCE_ROOT; }; + CE80DB1C0FC192D60086DCA6 /* Dialogs.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Dialogs.m; path = cocoalib/Dialogs.m; sourceTree = SOURCE_ROOT; }; + CE80DB1D0FC192D60086DCA6 /* HSErrorReportWindow.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = HSErrorReportWindow.h; path = cocoalib/HSErrorReportWindow.h; sourceTree = SOURCE_ROOT; }; + CE80DB1E0FC192D60086DCA6 /* HSErrorReportWindow.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = HSErrorReportWindow.m; path = cocoalib/HSErrorReportWindow.m; sourceTree = SOURCE_ROOT; }; + CE80DB1F0FC192D60086DCA6 /* Outline.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Outline.h; path = cocoalib/Outline.h; sourceTree = SOURCE_ROOT; }; + CE80DB200FC192D60086DCA6 /* Outline.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Outline.m; path = cocoalib/Outline.m; sourceTree = SOURCE_ROOT; }; + CE80DB210FC192D60086DCA6 /* ProgressController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ProgressController.h; path = cocoalib/ProgressController.h; sourceTree = SOURCE_ROOT; }; + CE80DB220FC192D60086DCA6 /* ProgressController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ProgressController.m; path = cocoalib/ProgressController.m; sourceTree = SOURCE_ROOT; }; + CE80DB230FC192D60086DCA6 /* PyApp.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PyApp.h; path = cocoalib/PyApp.h; sourceTree = SOURCE_ROOT; }; + CE80DB240FC192D60086DCA6 /* RecentDirectories.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RecentDirectories.h; path = cocoalib/RecentDirectories.h; sourceTree = SOURCE_ROOT; }; + CE80DB250FC192D60086DCA6 /* RecentDirectories.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RecentDirectories.m; path = cocoalib/RecentDirectories.m; sourceTree = SOURCE_ROOT; }; + CE80DB260FC192D60086DCA6 /* RegistrationInterface.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RegistrationInterface.h; path = cocoalib/RegistrationInterface.h; sourceTree = SOURCE_ROOT; }; + CE80DB270FC192D60086DCA6 /* RegistrationInterface.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RegistrationInterface.m; path = cocoalib/RegistrationInterface.m; sourceTree = SOURCE_ROOT; }; + CE80DB280FC192D60086DCA6 /* Table.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Table.h; path = cocoalib/Table.h; sourceTree = SOURCE_ROOT; }; + CE80DB290FC192D60086DCA6 /* Table.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Table.m; path = cocoalib/Table.m; sourceTree = SOURCE_ROOT; }; + CE80DB2A0FC192D60086DCA6 /* Utils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Utils.h; path = cocoalib/Utils.h; sourceTree = SOURCE_ROOT; }; + CE80DB2B0FC192D60086DCA6 /* Utils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Utils.m; path = cocoalib/Utils.m; sourceTree = SOURCE_ROOT; }; + CE80DB2C0FC192D60086DCA6 /* ValueTransformers.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ValueTransformers.h; path = cocoalib/ValueTransformers.h; sourceTree = SOURCE_ROOT; }; + CE80DB2D0FC192D60086DCA6 /* ValueTransformers.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ValueTransformers.m; path = cocoalib/ValueTransformers.m; sourceTree = SOURCE_ROOT; }; + CE80DB450FC193650086DCA6 /* NSNotificationAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = NSNotificationAdditions.h; path = cocoalib/NSNotificationAdditions.h; sourceTree = SOURCE_ROOT; }; + CE80DB460FC193650086DCA6 /* NSNotificationAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = NSNotificationAdditions.m; path = cocoalib/NSNotificationAdditions.m; sourceTree = SOURCE_ROOT; }; + CE80DB480FC193770086DCA6 /* NSImageAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = NSImageAdditions.h; path = cocoalib/NSImageAdditions.h; sourceTree = SOURCE_ROOT; }; + CE80DB490FC193770086DCA6 /* NSImageAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = NSImageAdditions.m; path = cocoalib/NSImageAdditions.m; sourceTree = SOURCE_ROOT; }; + CE80DB710FC194760086DCA6 /* English */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = English; path = cocoalib/English.lproj/ErrorReportWindow.xib; sourceTree = SOURCE_ROOT; }; + CE80DB730FC194760086DCA6 /* English */ = {isa = PBXFileReference; lastKnownFileType = wrapper.nib; name = English; path = cocoalib/English.lproj/progress.nib; sourceTree = SOURCE_ROOT; }; + CE80DB750FC194760086DCA6 /* English */ = {isa = PBXFileReference; lastKnownFileType = wrapper.nib; name = English; path = cocoalib/English.lproj/registration.nib; sourceTree = SOURCE_ROOT; }; + CE80DB820FC1951C0086DCA6 /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = dgbase/AppDelegate.h; sourceTree = SOURCE_ROOT; }; + CE80DB830FC1951C0086DCA6 /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AppDelegate.m; path = dgbase/AppDelegate.m; sourceTree = SOURCE_ROOT; }; + CE80DB840FC1951C0086DCA6 /* Consts.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Consts.h; path = dgbase/Consts.h; sourceTree = SOURCE_ROOT; }; + CE80DB850FC1951C0086DCA6 /* DirectoryPanel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = DirectoryPanel.h; path = dgbase/DirectoryPanel.h; sourceTree = SOURCE_ROOT; }; + CE80DB860FC1951C0086DCA6 /* DirectoryPanel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = DirectoryPanel.m; path = dgbase/DirectoryPanel.m; sourceTree = SOURCE_ROOT; }; + CE80DB870FC1951C0086DCA6 /* PyDupeGuru.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PyDupeGuru.h; path = dgbase/PyDupeGuru.h; sourceTree = SOURCE_ROOT; }; + CE80DB880FC1951C0086DCA6 /* ResultWindow.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ResultWindow.h; path = dgbase/ResultWindow.h; sourceTree = SOURCE_ROOT; }; + CE80DB890FC1951C0086DCA6 /* ResultWindow.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ResultWindow.m; path = dgbase/ResultWindow.m; sourceTree = SOURCE_ROOT; }; + CE848A1809DD85810004CB44 /* Consts.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Consts.h; sourceTree = ""; }; + CECA899809DB12CA00A3D774 /* English */ = {isa = PBXFileReference; lastKnownFileType = wrapper.nib; name = English; path = English.lproj/Details.nib; sourceTree = ""; }; + CECA899A09DB132E00A3D774 /* DetailsPanel.h */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.h; path = DetailsPanel.h; sourceTree = ""; }; + CECA899B09DB132E00A3D774 /* DetailsPanel.m */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.objc; path = DetailsPanel.m; sourceTree = ""; }; + CEDA432C0B07C6E600B3091A /* dg.xsl */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = text.xml; name = dg.xsl; path = w3/dg.xsl; sourceTree = SOURCE_ROOT; }; + CEDA432D0B07C6E600B3091A /* hardcoded.css */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = text; name = hardcoded.css; path = w3/hardcoded.css; sourceTree = SOURCE_ROOT; }; + CEEB135109C837A2004D2330 /* dupeguru.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; path = dupeguru.icns; sourceTree = ""; }; + CEF4112A0A11069600E7F110 /* Consts.m */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.objc; path = Consts.m; sourceTree = SOURCE_ROOT; }; + CEF7823709C8AA0200EF38FF /* gear.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = gear.png; path = images/gear.png; sourceTree = ""; }; + CEFC294509C89E3D00D9F998 /* folder32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = folder32.png; path = images/folder32.png; sourceTree = SOURCE_ROOT; }; + CEFC295309C89FF200D9F998 /* details32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = details32.png; path = images/details32.png; sourceTree = SOURCE_ROOT; }; + CEFC295409C89FF200D9F998 /* preferences32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = preferences32.png; path = images/preferences32.png; sourceTree = SOURCE_ROOT; }; + CEFCDE2C0AB0418600C33A93 /* dgpe_logo_32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = dgpe_logo_32.png; path = images/dgpe_logo_32.png; sourceTree = SOURCE_ROOT; }; + CEFF18A009A4D387005E6321 /* PyDupeGuru.h */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.h; path = PyDupeGuru.h; sourceTree = SOURCE_ROOT; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 8D11072E0486CEB800E47090 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 8D11072F0486CEB800E47090 /* Cocoa.framework in Frameworks */, + CE15C8A80ADEB8B50061D4A5 /* Sparkle.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 080E96DDFE201D6D7F000001 /* Classes */ = { + isa = PBXGroup; + children = ( + CE0C46A80FA0647E000BE99B /* PictureBlocks.h */, + CE0C46A90FA0647E000BE99B /* PictureBlocks.m */, + CE381C9509914ACE003581CE /* AppDelegate.h */, + CE381C9409914ACE003581CE /* AppDelegate.m */, + CE848A1809DD85810004CB44 /* Consts.h */, + CEF4112A0A11069600E7F110 /* Consts.m */, + CECA899A09DB132E00A3D774 /* DetailsPanel.h */, + CECA899B09DB132E00A3D774 /* DetailsPanel.m */, + CE68EE6509ABC48000971085 /* DirectoryPanel.h */, + CE68EE6609ABC48000971085 /* DirectoryPanel.m */, + CEFF18A009A4D387005E6321 /* PyDupeGuru.h */, + CE381C9B09914ADF003581CE /* ResultWindow.h */, + CE381C9A09914ADF003581CE /* ResultWindow.m */, + ); + name = Classes; + sourceTree = ""; + }; + 1058C7A0FEA54F0111CA2CBB /* Linked Frameworks */ = { + isa = PBXGroup; + children = ( + CE15C8A70ADEB8B50061D4A5 /* Sparkle.framework */, + 1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */, + ); + name = "Linked Frameworks"; + sourceTree = ""; + }; + 1058C7A2FEA54F0111CA2CBB /* Other Frameworks */ = { + isa = PBXGroup; + children = ( + 29B97324FDCFA39411CA2CEA /* AppKit.framework */, + 13E42FB307B3F0F600E4EEF1 /* CoreData.framework */, + 29B97325FDCFA39411CA2CEA /* Foundation.framework */, + ); + name = "Other Frameworks"; + sourceTree = ""; + }; + 19C28FACFE9D520D11CA2CBB /* Products */ = { + isa = PBXGroup; + children = ( + 8D1107320486CEB800E47090 /* dupeGuru PE.app */, + ); + name = Products; + sourceTree = ""; + }; + 29B97314FDCFA39411CA2CEA /* dupeguru */ = { + isa = PBXGroup; + children = ( + 080E96DDFE201D6D7F000001 /* Classes */, + CE80DB1A0FC192AB0086DCA6 /* cocoalib */, + CE80DB810FC194BD0086DCA6 /* dgbase */, + 29B97315FDCFA39411CA2CEA /* Other Sources */, + 29B97317FDCFA39411CA2CEA /* Resources */, + 29B97323FDCFA39411CA2CEA /* Frameworks */, + 19C28FACFE9D520D11CA2CBB /* Products */, + ); + name = dupeguru; + sourceTree = ""; + }; + 29B97315FDCFA39411CA2CEA /* Other Sources */ = { + isa = PBXGroup; + children = ( + 29B97316FDCFA39411CA2CEA /* main.m */, + ); + name = "Other Sources"; + sourceTree = ""; + }; + 29B97317FDCFA39411CA2CEA /* Resources */ = { + isa = PBXGroup; + children = ( + CE073F5409CAE1A3005C1D2F /* dupeguru_pe_help */, + CE381CF509915304003581CE /* dg_cocoa.plugin */, + CEFC294309C89E0000D9F998 /* images */, + CEDA432B0B07C6E600B3091A /* w3 */, + CEEB135109C837A2004D2330 /* dupeguru.icns */, + 8D1107310486CEB800E47090 /* Info.plist */, + 089C165CFE840E0CC02AAC07 /* InfoPlist.strings */, + CECA899709DB12CA00A3D774 /* Details.nib */, + CE3AA46509DB207900DB3A21 /* Directories.nib */, + 29B97318FDCFA39411CA2CEA /* MainMenu.nib */, + ); + name = Resources; + sourceTree = ""; + }; + 29B97323FDCFA39411CA2CEA /* Frameworks */ = { + isa = PBXGroup; + children = ( + 1058C7A0FEA54F0111CA2CBB /* Linked Frameworks */, + 1058C7A2FEA54F0111CA2CBB /* Other Frameworks */, + ); + name = Frameworks; + sourceTree = ""; + }; + CE80DB1A0FC192AB0086DCA6 /* cocoalib */ = { + isa = PBXGroup; + children = ( + CE80DB700FC194760086DCA6 /* ErrorReportWindow.xib */, + CE80DB720FC194760086DCA6 /* progress.nib */, + CE80DB740FC194760086DCA6 /* registration.nib */, + CE80DB480FC193770086DCA6 /* NSImageAdditions.h */, + CE80DB490FC193770086DCA6 /* NSImageAdditions.m */, + CE80DB450FC193650086DCA6 /* NSNotificationAdditions.h */, + CE80DB460FC193650086DCA6 /* NSNotificationAdditions.m */, + CE80DB1B0FC192D60086DCA6 /* Dialogs.h */, + CE80DB1C0FC192D60086DCA6 /* Dialogs.m */, + CE80DB1D0FC192D60086DCA6 /* HSErrorReportWindow.h */, + CE80DB1E0FC192D60086DCA6 /* HSErrorReportWindow.m */, + CE80DB1F0FC192D60086DCA6 /* Outline.h */, + CE80DB200FC192D60086DCA6 /* Outline.m */, + CE80DB210FC192D60086DCA6 /* ProgressController.h */, + CE80DB220FC192D60086DCA6 /* ProgressController.m */, + CE80DB230FC192D60086DCA6 /* PyApp.h */, + CE80DB240FC192D60086DCA6 /* RecentDirectories.h */, + CE80DB250FC192D60086DCA6 /* RecentDirectories.m */, + CE80DB260FC192D60086DCA6 /* RegistrationInterface.h */, + CE80DB270FC192D60086DCA6 /* RegistrationInterface.m */, + CE80DB280FC192D60086DCA6 /* Table.h */, + CE80DB290FC192D60086DCA6 /* Table.m */, + CE80DB2A0FC192D60086DCA6 /* Utils.h */, + CE80DB2B0FC192D60086DCA6 /* Utils.m */, + CE80DB2C0FC192D60086DCA6 /* ValueTransformers.h */, + CE80DB2D0FC192D60086DCA6 /* ValueTransformers.m */, + ); + name = cocoalib; + sourceTree = ""; + }; + CE80DB810FC194BD0086DCA6 /* dgbase */ = { + isa = PBXGroup; + children = ( + CE80DB820FC1951C0086DCA6 /* AppDelegate.h */, + CE80DB830FC1951C0086DCA6 /* AppDelegate.m */, + CE80DB840FC1951C0086DCA6 /* Consts.h */, + CE80DB850FC1951C0086DCA6 /* DirectoryPanel.h */, + CE80DB860FC1951C0086DCA6 /* DirectoryPanel.m */, + CE80DB870FC1951C0086DCA6 /* PyDupeGuru.h */, + CE80DB880FC1951C0086DCA6 /* ResultWindow.h */, + CE80DB890FC1951C0086DCA6 /* ResultWindow.m */, + ); + name = dgbase; + sourceTree = ""; + }; + CEDA432B0B07C6E600B3091A /* w3 */ = { + isa = PBXGroup; + children = ( + CEDA432C0B07C6E600B3091A /* dg.xsl */, + CEDA432D0B07C6E600B3091A /* hardcoded.css */, + ); + path = w3; + sourceTree = SOURCE_ROOT; + }; + CEFC294309C89E0000D9F998 /* images */ = { + isa = PBXGroup; + children = ( + CEFCDE2C0AB0418600C33A93 /* dgpe_logo_32.png */, + CEF7823709C8AA0200EF38FF /* gear.png */, + CEFC295309C89FF200D9F998 /* details32.png */, + CEFC295409C89FF200D9F998 /* preferences32.png */, + CEFC294509C89E3D00D9F998 /* folder32.png */, + ); + name = images; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 8D1107260486CEB800E47090 /* dupeguru */ = { + isa = PBXNativeTarget; + buildConfigurationList = C01FCF4A08A954540054247B /* Build configuration list for PBXNativeTarget "dupeguru" */; + buildPhases = ( + CEABE1A60FCC00E3005F8031 /* ShellScript */, + 8D1107290486CEB800E47090 /* Resources */, + 8D11072C0486CEB800E47090 /* Sources */, + 8D11072E0486CEB800E47090 /* Frameworks */, + CECC02B709A36E8200CC0A94 /* CopyFiles */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = dupeguru; + productInstallPath = "$(HOME)/Applications"; + productName = dupeguru; + productReference = 8D1107320486CEB800E47090 /* dupeGuru PE.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 29B97313FDCFA39411CA2CEA /* Project object */ = { + isa = PBXProject; + buildConfigurationList = C01FCF4E08A954540054247B /* Build configuration list for PBXProject "dupeguru" */; + compatibilityVersion = "Xcode 2.4"; + hasScannedForEncodings = 1; + mainGroup = 29B97314FDCFA39411CA2CEA /* dupeguru */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 8D1107260486CEB800E47090 /* dupeguru */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 8D1107290486CEB800E47090 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 8D11072A0486CEB800E47090 /* MainMenu.nib in Resources */, + 8D11072B0486CEB800E47090 /* InfoPlist.strings in Resources */, + CE381D0509915304003581CE /* dg_cocoa.plugin in Resources */, + CE073F6309CAE1A3005C1D2F /* dupeguru_pe_help in Resources */, + CEEB135209C837A2004D2330 /* dupeguru.icns in Resources */, + CEFC294609C89E3D00D9F998 /* folder32.png in Resources */, + CEFC295509C89FF200D9F998 /* details32.png in Resources */, + CEFC295609C89FF200D9F998 /* preferences32.png in Resources */, + CEF7823809C8AA0200EF38FF /* gear.png in Resources */, + CECA899909DB12CA00A3D774 /* Details.nib in Resources */, + CE3AA46709DB207900DB3A21 /* Directories.nib in Resources */, + CEFCDE2D0AB0418600C33A93 /* dgpe_logo_32.png in Resources */, + CEDA432E0B07C6E600B3091A /* dg.xsl in Resources */, + CEDA432F0B07C6E600B3091A /* hardcoded.css in Resources */, + CE80DB760FC194760086DCA6 /* ErrorReportWindow.xib in Resources */, + CE80DB770FC194760086DCA6 /* progress.nib in Resources */, + CE80DB780FC194760086DCA6 /* registration.nib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + CEABE1A60FCC00E3005F8031 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "cd help\npython gen.py\n/Developer/Applications/Utilities/Help\\ Indexer.app/Contents/MacOS/Help\\ Indexer dupeguru_pe_help"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 8D11072C0486CEB800E47090 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 8D11072D0486CEB800E47090 /* main.m in Sources */, + CE381C9609914ACE003581CE /* AppDelegate.m in Sources */, + CE381C9C09914ADF003581CE /* ResultWindow.m in Sources */, + CE68EE6809ABC48000971085 /* DirectoryPanel.m in Sources */, + CECA899D09DB132E00A3D774 /* DetailsPanel.m in Sources */, + CEF4112B0A11069600E7F110 /* Consts.m in Sources */, + CE0C46AA0FA0647E000BE99B /* PictureBlocks.m in Sources */, + CE80DB2E0FC192D60086DCA6 /* Dialogs.m in Sources */, + CE80DB2F0FC192D60086DCA6 /* HSErrorReportWindow.m in Sources */, + CE80DB300FC192D60086DCA6 /* Outline.m in Sources */, + CE80DB310FC192D60086DCA6 /* ProgressController.m in Sources */, + CE80DB320FC192D60086DCA6 /* RecentDirectories.m in Sources */, + CE80DB330FC192D60086DCA6 /* RegistrationInterface.m in Sources */, + CE80DB340FC192D60086DCA6 /* Table.m in Sources */, + CE80DB350FC192D60086DCA6 /* Utils.m in Sources */, + CE80DB360FC192D60086DCA6 /* ValueTransformers.m in Sources */, + CE80DB470FC193650086DCA6 /* NSNotificationAdditions.m in Sources */, + CE80DB4A0FC193770086DCA6 /* NSImageAdditions.m in Sources */, + CE80DB8A0FC1951C0086DCA6 /* AppDelegate.m in Sources */, + CE80DB8B0FC1951C0086DCA6 /* DirectoryPanel.m in Sources */, + CE80DB8C0FC1951C0086DCA6 /* ResultWindow.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 089C165CFE840E0CC02AAC07 /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + 089C165DFE840E0CC02AAC07 /* English */, + ); + name = InfoPlist.strings; + sourceTree = ""; + }; + 29B97318FDCFA39411CA2CEA /* MainMenu.nib */ = { + isa = PBXVariantGroup; + children = ( + 29B97319FDCFA39411CA2CEA /* English */, + ); + name = MainMenu.nib; + sourceTree = SOURCE_ROOT; + }; + CE3AA46509DB207900DB3A21 /* Directories.nib */ = { + isa = PBXVariantGroup; + children = ( + CE3AA46609DB207900DB3A21 /* English */, + ); + name = Directories.nib; + sourceTree = ""; + }; + CE80DB700FC194760086DCA6 /* ErrorReportWindow.xib */ = { + isa = PBXVariantGroup; + children = ( + CE80DB710FC194760086DCA6 /* English */, + ); + name = ErrorReportWindow.xib; + sourceTree = SOURCE_ROOT; + }; + CE80DB720FC194760086DCA6 /* progress.nib */ = { + isa = PBXVariantGroup; + children = ( + CE80DB730FC194760086DCA6 /* English */, + ); + name = progress.nib; + sourceTree = SOURCE_ROOT; + }; + CE80DB740FC194760086DCA6 /* registration.nib */ = { + isa = PBXVariantGroup; + children = ( + CE80DB750FC194760086DCA6 /* English */, + ); + name = registration.nib; + sourceTree = SOURCE_ROOT; + }; + CECA899709DB12CA00A3D774 /* Details.nib */ = { + isa = PBXVariantGroup; + children = ( + CECA899809DB12CA00A3D774 /* English */, + ); + name = Details.nib; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + C01FCF4B08A954540054247B /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + COPY_PHASE_STRIP = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(FRAMEWORK_SEARCH_PATHS)", + "$(SRCROOT)/cocoalib/build/Release", + "$(FRAMEWORK_SEARCH_PATHS_QUOTED_FOR_TARGET_1)", + ); + FRAMEWORK_SEARCH_PATHS_QUOTED_FOR_TARGET_1 = "\"$(SRCROOT)/dgbase/build/Release\""; + GCC_DYNAMIC_NO_PIC = NO; + GCC_ENABLE_FIX_AND_CONTINUE = YES; + GCC_MODEL_TUNING = G5; + GCC_OPTIMIZATION_LEVEL = 0; + INFOPLIST_FILE = Info.plist; + INSTALL_PATH = "$(HOME)/Applications"; + PRODUCT_NAME = dupeGuru; + WRAPPER_EXTENSION = app; + ZERO_LINK = YES; + }; + name = Debug; + }; + C01FCF4C08A954540054247B /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = ( + ppc, + i386, + ); + FRAMEWORK_SEARCH_PATHS = ( + "$(FRAMEWORK_SEARCH_PATHS)", + "$(FRAMEWORK_SEARCH_PATHS_QUOTED_FOR_TARGET_1)", + ); + GCC_GENERATE_DEBUGGING_SYMBOLS = NO; + GCC_MODEL_TUNING = G5; + INFOPLIST_FILE = Info.plist; + INSTALL_PATH = "$(HOME)/Applications"; + PRODUCT_NAME = "dupeGuru PE"; + WRAPPER_EXTENSION = app; + }; + name = Release; + }; + C01FCF4F08A954540054247B /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + GCC_C_LANGUAGE_STANDARD = c99; + GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.4; + PREBINDING = NO; + SDKROOT = /Developer/SDKs/MacOSX10.4u.sdk; + }; + name = Debug; + }; + C01FCF5008A954540054247B /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = ( + ppc, + i386, + ); + FRAMEWORK_SEARCH_PATHS = ""; + GCC_C_LANGUAGE_STANDARD = c99; + GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.4; + PREBINDING = NO; + SDKROOT = /Developer/SDKs/MacOSX10.4u.sdk; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + C01FCF4A08A954540054247B /* Build configuration list for PBXNativeTarget "dupeguru" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + C01FCF4B08A954540054247B /* Debug */, + C01FCF4C08A954540054247B /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + C01FCF4E08A954540054247B /* Build configuration list for PBXProject "dupeguru" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + C01FCF4F08A954540054247B /* Debug */, + C01FCF5008A954540054247B /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 29B97313FDCFA39411CA2CEA /* Project object */; +} diff --git a/pe/cocoa/main.m b/pe/cocoa/main.m new file mode 100644 index 00000000..c5f30658 --- /dev/null +++ b/pe/cocoa/main.m @@ -0,0 +1,21 @@ +// +// main.m +// dupeguru +// +// Created by Virgil Dupras on 2006/02/01. +// Copyright __MyCompanyName__ 2006. All rights reserved. +// + +#import + +int main(int argc, char *argv[]) +{ + NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; + NSString *pluginPath = [[NSBundle mainBundle] + pathForResource:@"dg_cocoa" + ofType:@"plugin"]; + NSBundle *pluginBundle = [NSBundle bundleWithPath:pluginPath]; + [pluginBundle load]; + [pool release]; + return NSApplicationMain(argc, (const char **) argv); +} diff --git a/pe/cocoa/py/build_py.sh b/pe/cocoa/py/build_py.sh new file mode 100755 index 00000000..c9ac2993 --- /dev/null +++ b/pe/cocoa/py/build_py.sh @@ -0,0 +1,2 @@ +#!/bin/bash +python setup.py py2app \ No newline at end of file diff --git a/pe/cocoa/py/dg_cocoa.py b/pe/cocoa/py/dg_cocoa.py new file mode 100644 index 00000000..76126ec1 --- /dev/null +++ b/pe/cocoa/py/dg_cocoa.py @@ -0,0 +1,199 @@ +#!/usr/bin/env python +import objc +from AppKit import * +from PyObjCTools import NibClassBuilder + +NibClassBuilder.extractClasses("MainMenu", bundle=NSBundle.mainBundle()) + +from dupeguru import app_pe_cocoa, scanner + +class PyApp(NibClassBuilder.AutoBaseClass): + pass #fake class + +class PyDupeGuru(PyApp): + def init(self): + self = super(PyDupeGuru,self).init() + self.app = app_pe_cocoa.DupeGuruPE() + return self + + #---Directories + def addDirectory_(self,directory): + return self.app.AddDirectory(directory) + + def removeDirectory_(self,index): + self.app.RemoveDirectory(index) + + def setDirectory_state_(self,node_path,state): + self.app.SetDirectoryState(node_path,state) + + #---Results + def clearIgnoreList(self): + self.app.scanner.ignore_list.Clear() + + def clearPictureCache(self): + self.app.scanner.match_factory.cached_blocks.clear() + + def doScan(self): + return self.app.start_scanning() + + def exportToXHTMLwithColumns_xslt_css_(self,column_ids,xslt_path,css_path): + return self.app.ExportToXHTML(column_ids,xslt_path,css_path) + + def loadIgnoreList(self): + self.app.LoadIgnoreList() + + def loadResults(self): + self.app.load() + + def markAll(self): + self.app.results.mark_all() + + def markNone(self): + self.app.results.mark_none() + + def markInvert(self): + self.app.results.mark_invert() + + def purgeIgnoreList(self): + self.app.PurgeIgnoreList() + + def toggleSelectedMark(self): + self.app.ToggleSelectedMarkState() + + def saveIgnoreList(self): + self.app.SaveIgnoreList() + + def saveResults(self): + self.app.Save() + + def refreshDetailsWithSelected(self): + self.app.RefreshDetailsWithSelected() + + def selectResultNodePaths_(self,node_paths): + self.app.SelectResultNodePaths(node_paths) + + def selectPowerMarkerNodePaths_(self,node_paths): + self.app.SelectPowerMarkerNodePaths(node_paths) + + #---Actions + def addSelectedToIgnoreList(self): + self.app.AddSelectedToIgnoreList() + + def deleteMarked(self): + self.app.delete_marked() + + def applyFilter_(self, filter): + self.app.ApplyFilter(filter) + + def makeSelectedReference(self): + self.app.MakeSelectedReference() + + def copyOrMove_markedTo_recreatePath_(self,copy,destination,recreate_path): + self.app.copy_or_move_marked(copy, destination, recreate_path) + + def openSelected(self): + self.app.OpenSelected() + + def removeMarked(self): + self.app.results.perform_on_marked(lambda x:True,True) + + def removeSelected(self): + self.app.RemoveSelected() + + def renameSelected_(self,newname): + return self.app.RenameSelected(newname) + + def revealSelected(self): + self.app.RevealSelected() + + #---Misc + def sortDupesBy_ascending_(self,key,asc): + self.app.sort_dupes(key,asc) + + def sortGroupsBy_ascending_(self,key,asc): + self.app.sort_groups(key,asc) + + #---Information + def getIgnoreListCount(self): + return len(self.app.scanner.ignore_list) + + def getMarkCount(self): + return self.app.results.mark_count + + def getStatLine(self): + return self.app.stat_line + + def getOperationalErrorCount(self): + return self.app.last_op_error_count + + def getSelectedDupePath(self): + return unicode(self.app.selected_dupe_path()) + + def getSelectedDupeRefPath(self): + return unicode(self.app.selected_dupe_ref_path()) + + #---Data + @objc.signature('i@:i') + def getOutlineViewMaxLevel_(self, tag): + return self.app.GetOutlineViewMaxLevel(tag) + + @objc.signature('@@:i@') + def getOutlineView_childCountsForPath_(self, tag, node_path): + return self.app.GetOutlineViewChildCounts(tag, node_path) + + def getOutlineView_valuesForIndexes_(self,tag,node_path): + return self.app.GetOutlineViewValues(tag,node_path) + + def getOutlineView_markedAtIndexes_(self,tag,node_path): + return self.app.GetOutlineViewMarked(tag,node_path) + + def getTableViewCount_(self,tag): + return self.app.GetTableViewCount(tag) + + def getTableViewMarkedIndexes_(self,tag): + return self.app.GetTableViewMarkedIndexes(tag) + + def getTableView_valuesForRow_(self,tag,row): + return self.app.GetTableViewValues(tag,row) + + #---Properties + def setMatchScaled_(self,match_scaled): + self.app.scanner.match_factory.match_scaled = match_scaled + + def setMinMatchPercentage_(self,percentage): + self.app.scanner.match_factory.threshold = int(percentage) + + def setMixFileKind_(self,mix_file_kind): + self.app.scanner.mix_file_kind = mix_file_kind + + def setDisplayDeltaValues_(self,display_delta_values): + self.app.display_delta_values= display_delta_values + + def setEscapeFilterRegexp_(self, escape_filter_regexp): + self.app.options['escape_filter_regexp'] = escape_filter_regexp + + def setRemoveEmptyFolders_(self, remove_empty_folders): + self.app.options['clean_empty_dirs'] = remove_empty_folders + + #---Worker + def getJobProgress(self): + return self.app.progress.last_progress + + def getJobDesc(self): + return self.app.progress.last_desc + + def cancelJob(self): + self.app.progress.job_cancelled = True + + #---Registration + @objc.signature('i@:') + def isRegistered(self): + return self.app.registered + + @objc.signature('i@:@@') + def isCodeValid_withEmail_(self, code, email): + return self.app.is_code_valid(code, email) + + def setRegisteredCode_andEmail_(self, code, email): + self.app.set_registration(code, email) + diff --git a/pe/cocoa/py/setup.py b/pe/cocoa/py/setup.py new file mode 100644 index 00000000..8bf847db --- /dev/null +++ b/pe/cocoa/py/setup.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python +from setuptools import setup + +from hs.build import set_buildenv + +set_buildenv() + +setup( + plugin=['dg_cocoa.py'], + setup_requires=['py2app'], +) diff --git a/pe/cocoa/w3/dg.xsl b/pe/cocoa/w3/dg.xsl new file mode 100644 index 00000000..4f982fce --- /dev/null +++ b/pe/cocoa/w3/dg.xsl @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + indented + + + + + + + + + + + + + + + + + + + + + + + + + + + + dupeGuru Results + + + +

dupeGuru Results

+ + + + + +
+ + +
+ +
\ No newline at end of file diff --git a/pe/cocoa/w3/hardcoded.css b/pe/cocoa/w3/hardcoded.css new file mode 100644 index 00000000..ed243bcc --- /dev/null +++ b/pe/cocoa/w3/hardcoded.css @@ -0,0 +1,71 @@ +BODY +{ + background-color:white; +} + +BODY,A,P,UL,TABLE,TR,TD +{ + font-family:Tahoma,Arial,sans-serif; + font-size:10pt; + color: #4477AA; +} + +TABLE +{ + background-color: #225588; + margin-left: auto; + margin-right: auto; + width: 90%; +} + +TR +{ + background-color: white; +} + +TH +{ + font-weight: bold; + color: black; + background-color: #C8D6E5; +} + +TH TD +{ + color:black; +} + +TD +{ + padding-left: 2pt; +} + +TD.rightelem +{ + text-align:right; + /*padding-left:0pt;*/ + padding-right: 2pt; + width: 17%; +} + +TD.indented +{ + padding-left: 12pt; +} + +H1 +{ + font-family:"Courier New",monospace; + color:#6699CC; + font-size:18pt; + color:#6da500; + border-color: #70A0CF; + border-width: 1pt; + border-style: solid; + margin-top: 16pt; + margin-left: 5%; + margin-right: 5%; + padding-top: 2pt; + padding-bottom:2pt; + text-align: center; +} \ No newline at end of file diff --git a/pe/help/changelog.yaml b/pe/help/changelog.yaml new file mode 100644 index 00000000..f387c334 --- /dev/null +++ b/pe/help/changelog.yaml @@ -0,0 +1,174 @@ +- date: 2009-05-27 + version: 1.7.2 + description: | + * Fixed a bug causing '.jpeg' files not to be scanned. + * Fixed a bug causing a GUI freeze at the beginning of a scan with a lot of files. + * Fixed a bug that sometimes caused a crash when an action was cancelled, and then started again. + * Improved scanning speed. +- date: 2009-05-26 + version: 1.7.1 + description: | + * Fixed a bug causing the "Match Scaled" preference to be inverted. +- date: 2009-05-20 + version: 1.7.0 + description: | + * Fixed the bug from 1.6.0 preventing PowerPC macs from running the application. + * Converted the Windows GUI to Qt, thus enabling multiprocessing and making the scanning process + faster. +- date: 2009-03-24 + description: "* **Improved** scanning speed, mainly on OS X where all cores of the\ + \ CPU are now used.\r\n* **Fixed** an occasional crash caused by permission issues.\r\ + \n* **Fixed** a bug where the \"X discarded\" notice would show a too large number\ + \ of discarded duplicates." + version: 1.6.0 +- date: 2008-09-10 + description: "
    \n\t\t\t\t\t\t
  • Added a notice in the status bar when\ + \ matches were discarded during the scan.
  • \n\t\t\t\t\t\t
  • Improved\ + \ duplicate prioritization (smartly chooses which file you will keep).
  • \n\t\ + \t\t\t\t\t
  • Improved scan progress feedback.
  • \n\t\t\t\t\t\t
  • Improved\ + \ responsiveness of the user interface for certain actions.
  • \n\t\t \ + \
" + version: 1.5.0 +- date: 2008-07-28 + description: "
    \n\t\t\t\t\t\t
  • Improved iPhoto compatibility on Mac\ + \ OS X.
  • \n\t\t\t\t\t\t
  • Improved the speed of results loading and\ + \ saving.
  • \n\t\t\t\t\t\t
  • Fixed a crash sometimes occurring during\ + \ duplicate deletion.
  • \n\t\t
" + version: 1.4.2 +- date: 2008-04-12 + description: "
    \n\t\t\t\t\t\t
  • Improved iPhoto Library loading feedback\ + \ on Mac OS X.
  • \n\t\t\t\t\t\t
  • Fixed the directory selection dialog.\ + \ Bundles can be selected again on Mac OS X.
  • \n\t\t\t\t\t\t
  • Fixed\ + \ \"Clear Ignore List\" crash in Windows.
  • \n\t\t
" + version: 1.4.1 +- date: 2008-02-20 + description: "
    \n\t\t\t\t\t\t
  • Added iPhoto Library support on Mac OS\ + \ X.
  • \n\t\t\t\t\t\t
  • Fixed occasional crashes when scanning corrupted\ + \ pictures.
  • \n\t\t
" + version: 1.4.0 +- date: 2008-02-20 + description: "
    \n\t\t\t\t\t\t
  • Added iPhoto Library support on Mac OS\ + \ X.
  • \n\t\t\t\t\t\t
  • Fixed occasional crashes when scanning corrupted\ + \ pictures.
  • \n\t\t
" + version: 1.4.0 +- date: 2008-01-12 + description: "
    \n\t\t\t\t\t\t
  • Improved scan, delete and move speed\ + \ in situations where there were a lot of duplicates.
  • \n\t\t\t\t\t\t
  • Fixed\ + \ occasional crashes when moving a lot of files at once.
  • \n\t\t\t\t\t\t
  • Fixed\ + \ an issue sometimes preventing the application from starting at all.
  • \n\t\ + \t
" + version: 1.3.4 +- date: 2007-12-03 + description: "
    \n\t\t\t\t\t\t
  • Improved the handling of low memory situations.
  • \n\ + \t\t\t\t\t\t
  • Improved the directory panel. The \"Remove\" button changes\ + \ to \"Put Back\" when an excluded directory is selected.
  • \n\t\t\t\t\t\t
  • Fixed\ + \ the directory selection dialog. iPhoto '08 library files can now be selected.
  • \n\ + \t\t
" + version: 1.3.3 +- date: 2007-11-24 + description: "
    \n\t\t\t\t\t\t
  • Added the \"Remove empty folders\" option.
  • \n\ + \t\t\t\t\t\t
  • Fixed results load/save issues.
  • \n\t\t\t\t\t\t
  • Fixed\ + \ occasional status bar inaccuracies when the results are filtered.
  • \n\t\t\ + \
" + version: 1.3.2 +- date: 2007-10-21 + description: "
    \n\t\t\t\t\t\t
  • Improved results loading speed.
  • \n\ + \t\t\t\t\t\t
  • Improved details panel's picture loading (made it asynchronous).
  • \n\ + \t\t\t\t\t\t
  • Fixed a bug where the stats line at the bottom would sometimes\ + \ go confused while having a filter active.
  • \n\t\t\t\t\t\t
  • Fixed\ + \ a bug under Windows where some duplicate markings would be lost.
  • \n\t\t\ + \
" + version: 1.3.1 +- date: 2007-09-22 + description: "
    \n\t\t\t\t\t\t
  • Added post scan filtering.
  • \n\t\t\ + \t\t\t\t
  • Fixed issues with the rename feature under Windows
  • \n\t\ + \t\t\t\t\t
  • Fixed some user interface annoyances under Windows
  • \n\ + \t\t
" + version: 1.3.0 +- date: 2007-05-19 + description: "
    \n\t\t\t\t\t\t
  • Improved UI responsiveness (using threads)\ + \ under Mac OS X.
  • \n\t\t\t\t\t\t
  • Improved result load/save speed\ + \ and memory usage.
  • \n\t\t
" + version: 1.2.1 +- date: 2007-03-17 + description: "
    \n\t\t\t\t\t\t
  • Changed the picture decoding libraries\ + \ for both Mac OS X and Windows. The Mac OS X version uses the Core Graphics library\ + \ and the Windows version uses the .NET framework imaging capabilities. This results\ + \ in much faster scans. As a bonus, the Mac OS X version of dupeGuru PE now supports\ + \ RAW images.
  • \n\t\t
" + version: 1.2.0 +- date: 2007-02-11 + description: "
    \n\t\t\t\t\t\t
  • Added Re-orderable columns. In fact,\ + \ I re-added the feature which was lost in the C# conversion in 2.4.0 (Windows).
  • \n\ + \t\t\t\t\t\t
  • Fixed a bug with all the Delete/Move/Copy actions with\ + \ certain kinds of files.
  • \n\t\t
" + version: 1.1.6 +- date: 2007-01-11 + description: "
    \n\t\t\t\t\t\t
  • Fixed a bug with the Move action.
  • \n\ + \t\t
" + version: 1.1.5 +- date: 2007-01-09 + description: "
    \n\t\t\t\t\t\t
  • Fixed a \"ghosting\" bug. Dupes deleted\ + \ by dupeGuru would sometimes come back in subsequent scans (Windows).
  • \n\t\ + \t\t\t\t\t
  • Fixed bugs sometimes making dupeGuru crash when marking a\ + \ dupe (Windows).
  • \n\t\t\t\t\t\t
  • Fixed some minor visual glitches\ + \ (Windows).
  • \n\t\t
" + version: 1.1.4 +- date: 2006-12-23 + description: "
    \n\t\t\t\t\t\t
  • Improved the caching system. This makes\ + \ duplicate scans significantly faster.
  • \n\t\t\t\t\t\t
  • Improved\ + \ the rename file dialog to exclude the extension from the original selection\ + \ (so when you start typing your new filename, it doesn't overwrite it) (Windows).
  • \n\ + \t\t\t\t\t\t
  • Changed some menu key shortcuts that created conflicts\ + \ (Windows).
  • \n\t\t\t\t\t\t
  • Fixed a bug preventing files from \"\ + reference\" directories to be displayed in blue in the results (Windows).
  • \n\ + \t\t\t\t\t\t
  • Fixed a bug preventing some files to be sent to the recycle\ + \ bin (Windows).
  • \n\t\t\t\t\t\t
  • Fixed a bug with the \"Remove\"\ + \ button of the directories panel (Windows).
  • \n\t\t\t\t\t\t
  • Fixed\ + \ a bug in the packaging preventing certain Windows configurations to start dupeGuru\ + \ at all.
  • \n\t\t
" + version: 1.1.3 +- date: 2006-11-18 + description: "
    \n\t\t\t\t\t\t
  • Fixed a bug with directory states.
  • \n\ + \t\t
" + version: 1.1.2 +- date: 2006-11-17 + description: "
    \n\t\t\t\t\t\t
  • Fixed a bug causing the ignore list not\ + \ to be saved.
  • \n\t\t\t\t\t\t
  • Fixed a bug with selection under Power\ + \ Marker mode.
  • \n\t\t
" + version: 1.1.1 +- date: 2006-11-15 + description: "
    \n\t\t\t\t\t\t
  • Changed the Windows interface. It is\ + \ now .NET based.
  • \n\t\t\t\t\t\t
  • Added an auto-update feature to\ + \ the windows version.
  • \n\t\t\t\t\t\t
  • Changed the way power marking\ + \ works. It is now a mode instead of a separate window.
  • \n\t\t\t\t\t\t
  • Changed\ + \ the \"Size (MB)\" column for a \"Size (KB)\" column. The values are now \"ceiled\"\ + \ instead of rounded. Therefore, a size \"0\" is now really 0 bytes, not just\ + \ a value too small to be rounded up. It is also the case for delta values.
  • \n\ + \t\t\t\t\t\t
  • Fixed a bug sometimes making delete and move operations\ + \ stall.
  • \n\t\t
" + version: 1.1.0 +- date: 2006-10-12 + description: "
    \n\t\t\t\t\t\t
  • Added an auto-update feature in the Mac\ + \ OS X version (with Sparkle).
  • \n\t\t \t
  • Fixed a bug\ + \ sometimes causing inaccuracies of the Match %.
  • \n\t\t
" + version: 1.0.5 +- date: 2006-09-21 + description: "
    \n\t\t \t
  • Fixed a bug with the cache system.
  • \n\ + \t\t
" + version: 1.0.4 +- date: 2006-09-15 + description: "
    \n\t\t\t\t\t\t
  • Added the ability to search for scaled\ + \ duplicates.
  • \n\t\t\t\t\t\t
  • Added a cache system for faster scans.
  • \n\ + \t\t \t
  • Improved speed of the scanning engine.
  • \n\t\t\ + \
" + version: 1.0.3 +- date: 2006-09-11 + description: "
    \n\t\t \t
  • Improved speed of the scanning\ + \ engine.
  • \n\t\t\t\t\t\t
  • Improved the display of pictures in the\ + \ details panel (Windows).
  • \n\t\t
" + version: 1.0.2 +- date: 2006-09-08 + description: "
    \n\t\t \t
  • Initial release.
  • \n\t\t \ + \
" + version: 1.0.0 diff --git a/pe/help/gen.py b/pe/help/gen.py new file mode 100644 index 00000000..8ed33e4e --- /dev/null +++ b/pe/help/gen.py @@ -0,0 +1,12 @@ +#!/usr/bin/env python +# Unit Name: +# Created By: Virgil Dupras +# Created On: 2009-05-24 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +import os + +from web import generate_help + +generate_help.main('.', 'dupeguru_pe_help', force_render=True) diff --git a/pe/help/skeleton/hardcoded.css b/pe/help/skeleton/hardcoded.css new file mode 100644 index 00000000..a3b17c5b --- /dev/null +++ b/pe/help/skeleton/hardcoded.css @@ -0,0 +1,409 @@ +/***************************************************** + General settings +*****************************************************/ + +BODY +{ + background-color:white; +} + +BODY,A,P,UL,TABLE,TR,TD +{ + font-family:Tahoma,Arial,sans-serif; + font-size:10pt; + color: #4477AA;/*darker than 5588bb for the sake of the eyes*/ +} + +/***************************************************** + "A" settings +*****************************************************/ + +A +{ + color: #ae322b; + text-decoration:underline; + font-weight:bold; +} + +A.glossaryword {color:#A0A0A0;} + +A.noline +{ + text-decoration: none; +} + + +/***************************************************** + Menu and mainframe settings +*****************************************************/ + +.maincontainer +{ + display:block; + margin-left:7%; + margin-right:7%; + padding-left:5px; + padding-right:0px; + border-color:#CCCCCC; + border-style:solid; + border-width:2px; + border-right-width:0px; + border-bottom-width:0px; + border-top-color:#ae322b; + vertical-align:top; +} + +TD.menuframe +{ + width:30%; +} + +.menu +{ + margin:4px 4px 4px 4px; + margin-top: 16pt; + border-color:gray; + border-width:1px; + border-style:dotted; + padding-top:10pt; + padding-bottom:10pt; + padding-right:6pt; +} + +.submenu +{ + list-style-type: none; + margin-left:26pt; + margin-top:0pt; + margin-bottom:0pt; + padding-left:0pt; +} + +A.menuitem,A.menuitem_selected +{ + font-size:14pt; + font-family:Tahoma,Arial,sans-serif; + font-weight:normal; + padding-left:10pt; + color:#5588bb; + margin-right:2pt; + margin-left:4pt; + text-decoration:none; +} + +A.menuitem_selected +{ + font-weight:bold; +} + +A.submenuitem +{ + font-family:Tahoma,Arial,sans-serif; + font-weight:normal; + color:#5588bb; + text-decoration:none; +} + +.titleline +{ + border-width:3px; + border-style:solid; + border-left-width:0px; + border-right-width:0px; + border-top-width:0px; + border-color:#CCCCCC; + margin-left:28pt; + margin-right:2pt; + line-height:1px; + padding-top:0px; + margin-top:0px; + display:block; +} + +.titledescrip +{ + text-align:left; + display:block; + margin-left:26pt; + color:#ae322b; +} + +.mainlogo +{ + display:block; + margin-left:8%; + margin-top:4pt; + margin-bottom:4pt; +} + +/***************************************************** + IMG settings +*****************************************************/ + +IMG +{ + border-style:none; +} + +IMG.smallbutton +{ + margin-right: 20px; + float:none; +} + +IMG.floating +{ + float:left; + margin-right: 4pt; + margin-bottom: 4pt; +} + +IMG.lefticon +{ + vertical-align: middle; + padding-right: 2pt; +} + +IMG.righticon +{ + vertical-align: middle; + padding-left: 2pt; +} + +/***************************************************** + TABLE settings +*****************************************************/ + +TABLE +{ + border-style:none; +} + +TABLE.box +{ + width: 90%; + margin-left:5%; +} + +TABLE.centered +{ + margin-left: auto; + margin-right: auto; +} + +TABLE.hardcoded +{ + background-color: #225588; + margin-left: auto; + margin-right: auto; + width: 90%; +} + +TR { background-color: transparent; } + +TABLE.hardcoded TR { background-color: white } + +TABLE.hardcoded TR.header +{ + font-weight: bold; + color: black; + background-color: #C8D6E5; +} + +TABLE.hardcoded TR.header TD {color:black;} + +TABLE.hardcoded TD { padding-left: 2pt; } + +TD.minimelem { + padding-right:0px; + padding-left:0px; + text-align:center; +} + +TD.rightelem +{ + text-align:right; + /*padding-left:0pt;*/ + padding-right: 2pt; + width: 17%; +} + +/***************************************************** + P settings +*****************************************************/ + +p,.sub{text-align:justify;} +.centered{text-align:center;} +.sub +{ + padding-left: 16pt; + padding-right:16pt; +} + +.Note, .ContactInfo +{ + border-color: #ae322b; + border-width: 1pt; + border-style: dashed; + text-align:justify; + padding: 2pt 2pt 2pt 2pt; + margin-bottom:4pt; + margin-top:8pt; + list-style-position:inside; +} + +.ContactInfo +{ + width:60%; + margin-left:5%; +} + +.NewsItem +{ + border-color:#ae322b; + border-style: solid; + border-right:none; + border-top:none; + border-left:none; + border-bottom-width:1px; + text-align:justify; + padding-left:4pt; + padding-right:4pt; + padding-bottom:8pt; +} + +/***************************************************** + Lists settings +*****************************************************/ +UL.plain +{ + list-style-type: none; + padding-left:0px; + margin-left:0px; +} + +LI.plain +{ + list-style-type: none; +} + +LI.section +{ + padding-top: 6pt; +} + +UL.longtext LI +{ + border-color: #ae322b; + border-width:0px; + border-top-width:1px; + border-style:solid; + margin-top:12px; +} + +/* + with UL.longtext LI, there can be anything between + the UL and the LI, and it will still make the + lontext thing, I must break it with this hack +*/ +UL.longtext UL LI +{ + border-style:none; + margin-top:2px; +} + + +/***************************************************** + Titles settings +*****************************************************/ + +H1,H2,H3 +{ + font-family:"Courier New",monospace; + color:#5588bb; +} + +H1 +{ + font-size:18pt; + color: #ae322b; + border-color: #70A0CF; + border-width: 1pt; + border-style: solid; + margin-top: 16pt; + margin-left: 5%; + margin-right: 5%; + padding-top: 2pt; + padding-bottom:2pt; + text-align: center; +} + +H2 +{ + border-color: #ae322b; + border-bottom-width: 2px; + border-top-width: 0pt; + border-left-width: 2px; + border-right-width: 0pt; + border-bottom-color: #cccccc; + border-style: solid; + margin-top: 16pt; + margin-left: 0pt; + margin-right: 0pt; + padding-bottom:3pt; + padding-left:5pt; + text-align: left; + font-size:16pt; +} + +H3 +{ + display:block; + color:#ae322b; + border-color: #70A0CF; + border-bottom-width: 2px; + border-top-width: 0pt; + border-left-width: 0pt; + border-right-width: 0pt; + border-style: dashed; + margin-top: 12pt; + margin-left: 0pt; + margin-bottom: 4pt; + width:auto; + padding-bottom:3pt; + padding-right:2pt; + padding-left:2pt; + text-align: left; + font-weight:bold; +} + + +/***************************************************** + Misc. classes +*****************************************************/ +.longtext:first-letter {font-size: 150%} + +.price, .loweredprice, .specialprice {font-weight:bold;} + +.loweredprice {text-decoration:line-through} + +.specialprice {color:red} + +form +{ + margin:0px; +} + +.program_summary +{ + float:right; + margin: 32pt; + margin-top:0pt; + margin-bottom:0pt; +} + +.screenshot +{ + float:left; + margin: 8pt; +} \ No newline at end of file diff --git a/pe/help/skeleton/images/hs_title.png b/pe/help/skeleton/images/hs_title.png new file mode 100644 index 0000000000000000000000000000000000000000..07bd89c69dd50a3967b46a441741f274b8854de8 GIT binary patch literal 1817 zcmWkt2~Y|q^t%aD27PTfWQbSo7y0Xrdp{OF7oMwX4{+byFr3&y7B zlLpel1d+*^<>$!E@c2A9sOuxRq}jS+G}rcy>y2h&rngp=tT$NjxX)#-@ zR;$fwvyo;C3xs)cj4wl35`-y%c{0>!qGG~a8Nvb)0K&L3RHVdNQtA}p%Q4Dz$v^^f z%s`q=W-AZEN|l62NeGp=(QGM^zcEYpnJq53Zg_psX$pDf}jksh9!YZ6*y0d6t`*06htZIC4pHW%9Epf zh*AWzsT7ofh)_XrrKsL$;mT2tj52IVPjY1#SB?oFlp&##5=^Q9qlF{I7FJ9G2rF*a z2s?-pFR&PpFjtBRA?yV{E9}sUAgt)Ih9}1>x(SvT zsqP+UieWa0F{DUM&ub2ZbHs4dvq=VsEE&xV>e|H!OM+4TQTc&M3CEU=q(F(^fAYG# zOS_;aD|@ud29hH~gpd|cpbwr+aO8+kiBoq^eznVg<_$ zP1%d1$UV?R~TV5+i6w>mO|X i&$iNccmR(ItgDH2&`ti^@h8+@ro}}kMz!wGy!C(b`7gZy literal 0 HcmV?d00001 diff --git a/pe/help/templates/base_dg.mako b/pe/help/templates/base_dg.mako new file mode 100644 index 00000000..7767c49f --- /dev/null +++ b/pe/help/templates/base_dg.mako @@ -0,0 +1,14 @@ +<%inherit file="/base_help.mako"/> +${next.body()} + +<%def name="menu()"><% +self.menuitem('intro.htm', 'Introduction', 'Introduction to dupeGuru') +self.menuitem('quick_start.htm', 'Quick Start', 'Quickly get into the action') +self.menuitem('directories.htm', 'Directories', 'Managing dupeGuru directories') +self.menuitem('preferences.htm', 'Preferences', 'Setting dupeGuru preferences') +self.menuitem('results.htm', 'Results', 'Time to delete these duplicates!') +self.menuitem('power_marker.htm', 'Power Marker', 'Take control of your duplicates') +self.menuitem('faq.htm', 'F.A.Q.', 'Frequently Asked Questions') +self.menuitem('versions.htm', 'Version History', 'Changes dupeGuru went through') +self.menuitem('credits.htm', 'Credits', 'People who contributed to dupeGuru') +%> \ No newline at end of file diff --git a/pe/help/templates/credits.mako b/pe/help/templates/credits.mako new file mode 100644 index 00000000..9de91bd2 --- /dev/null +++ b/pe/help/templates/credits.mako @@ -0,0 +1,25 @@ +## -*- coding: utf-8 -*- +<%! + title = 'Credits' + selected_menu_item = 'Credits' +%> +<%inherit file="/base_dg.mako"/> +Below is the list of people who contributed, directly or indirectly to dupeGuru. + +${self.credit('Virgil Dupras', 'Developer', "That's me, Hardcoded Software founder", 'www.hardcoded.net', 'hsoft@hardcoded.net')} + +${self.credit(u'Jérôme Cantin', u'Icon designer', u"Icons in dupeGuru are from him")} + +${self.credit('Python', 'Programming language', "The bestest of the bests", 'www.python.org')} + +${self.credit('PyObjC', 'Python-to-Cocoa bridge', "Used for the Mac OS X version", 'pyobjc.sourceforge.net')} + +${self.credit('PyQt', 'Python-to-Qt bridge', "Used for the Windows version", 'www.riverbankcomputing.co.uk')} + +${self.credit('Qt', 'GUI Toolkit', "Used for the Windows version", 'www.qtsoftware.com')} + +${self.credit('Sparkle', 'Auto-update library', "Used for the Mac OS X version", 'andymatuschak.org/pages/sparkle')} + +${self.credit('Python Imaging Library', 'Picture analyzer', "Used for the Windows version", 'www.pythonware.com/products/pil/')} + +${self.credit('You', 'dupeGuru user', "What would I do without you?")} diff --git a/pe/help/templates/directories.mako b/pe/help/templates/directories.mako new file mode 100644 index 00000000..e75b47bd --- /dev/null +++ b/pe/help/templates/directories.mako @@ -0,0 +1,24 @@ +<%! + title = 'Directories' + selected_menu_item = 'Directories' +%> +<%inherit file="/base_dg.mako"/> + +There is a panel in dupeGuru called **Directories**. You can open it by clicking on the **Directories** button. This directory contains the list of the directories that will be scanned when you click on **Start Scanning**. + +This panel is quite straightforward to use. If you want to add a directory, click on **Add**. If you added directories before, a popup menu with a list of recent directories you added will pop. You can click on one of them to add it directly to your list. If you click on the first item of the popup menu, **Add New Directory...**, you will be prompted for a directory to add. If you never added a directory, no menu will pop and you will directly be prompted for a new directory to add. + +To remove a directory, select the directory to remove and click on **Remove**. If a subdirectory is selected when you click remove, the selected directory will be set to **excluded** state (see below) instead of being removed. + +Directory states +----- + +Every directory can be in one of these 3 states: + +* **Normal:** Duplicates found in these directories can be deleted. +* **Reference:** Duplicates found in this directory **cannot** be deleted. Files in reference directories will be in a blue color in the results. +* **Excluded:** Files in this directory will not be included in the scan. + +The default state of a directory is, of course, **Normal**. You can use **Reference** state for a directory if you want to be sure that you won't delete any file from it. + +When you set the state of a directory, all subdirectories of this directory automatically inherit this state unless you explicitly set a subdirectory's state. diff --git a/pe/help/templates/faq.mako b/pe/help/templates/faq.mako new file mode 100644 index 00000000..1c4e998f --- /dev/null +++ b/pe/help/templates/faq.mako @@ -0,0 +1,64 @@ +<%! + title = 'dupeGuru F.A.Q.' + selected_menu_item = 'F.A.Q.' +%> +<%inherit file="/base_dg.mako"/> + +<%text filter="md"> +### What is dupeGuru PE? + +dupeGuru Picture Edition (PE for short) is a tool to find duplicate pictures on your computer. Not only can it find exact matches, but it can also find duplicates among pictures of different kind (PNG, JPG, GIF etc..) and quality. + +### What makes it better than other duplicate scanners? + +The scanning engine is extremely flexible. You can tweak it to really get the kind of results you want. You can read more about dupeGuru tweaking option at the [Preferences page](preferences.htm). + +### How safe is it to use dupeGuru PE? + +Very safe. dupeGuru has been designed to make sure you don't delete files you didn't mean to delete. First, there is the reference directory system that lets you define directories where you absolutely **don't** want dupeGuru to let you delete files there, and then there is the group reference system that makes sure that you will **always** keep at least one member of the duplicate group. + +### What are the demo limitations of dupeGuru PE? + +In demo mode, you can only perform actions (delete/copy/move) on 10 duplicates per session. + +### The mark box of a file I want to delete is disabled. What must I do? + +You cannot mark the reference (The first file) of a duplicate group. However, what you can do is to promote a duplicate file to reference. Thus, if a file you want to mark is reference, select a duplicate file in the group that you want to promote to reference, and click on **Actions-->Make Selected Reference**. If the reference file is from a reference directory (filename written in blue letters), you cannot remove it from the reference position. + +### I have a directory from which I really don't want to delete files. + +If you want to be sure that dupeGuru will never delete file from a particular directory, just open the **Directories panel**, select that directory, and set its state to **Reference**. + +### What is this '(X discarded)' notice in the status bar? + +In some cases, some matches are not included in the final results for security reasons. Let me use an example. We have 3 file: A, B and C. We scan them using a low filter hardness. The scanner determines that A matches with B, A matches with C, but B does **not** match with C. Here, dupeGuru has kind of a problem. It cannot create a duplicate group with A, B and C in it because not all files in the group would match together. It could create 2 groups: one A-B group and then one A-C group, but it will not, for security reasons. Lets think about it: If B doesn't match with C, it probably means that either B, C or both are not actually duplicates. If there would be 2 groups (A-B and A-C), you would end up delete both B and C. And if one of them is not a duplicate, that is really not what you want to do, right? So what dupeGuru does in a case like this is to discard the A-C match (and adds a notice in the status bar). Thus, if you delete B and re-run a scan, you will have a A-C match in your next results. + +### I want to mark all files from a specific directory. What can I do? + +Enable the [Power Marker](power_marker.htm) mode and click on the Directory column to sort your duplicates by Directory. It will then be easy for you to select all duplicates from the same directory, and then press Space to mark all selected duplicates. + +### I want to remove all files that are more than 300 KB away from their reference file. What can I do? + +* Enable the [Power Marker](power_marker.htm) mode. +* Enable the **Delta Values** mode. +* Click on the "Size" column to sort the results by size. +* Select all duplicates below -300. +* Click on **Remove Selected from Results**. +* Select all duplicates over 300. +* Click on **Remove Selected from Results**. + +### I want to make my latest modified files reference files. What can I do? + +* Enable the [Power Marker](power_marker.htm) mode. +* Enable the **Delta Values** mode. +* Click on the "Modification" column to sort the results by modification date. +* Click on the "Modification" column again to reverse the sort order (see Power Marker page to know why). +* Select all duplicates over 0. +* Click on **Make Selected Reference**. + +### I want to mark all duplicates containing the word "copy". How do I do that? + +* **Windows**: Click on **Actions --> Apply Filter**, then type "copy", then click OK. +* **Mac OS X**: Type "copy" in the "Filter" field in the toolbar. +* Click on **Mark --> Mark All**. + \ No newline at end of file diff --git a/pe/help/templates/intro.mako b/pe/help/templates/intro.mako new file mode 100644 index 00000000..51d058c9 --- /dev/null +++ b/pe/help/templates/intro.mako @@ -0,0 +1,13 @@ +<%! + title = 'Introduction to dupeGuru PE' + selected_menu_item = 'introduction' +%> +<%inherit file="/base_dg.mako"/> + +dupeGuru Picture Edition (PE for short) is a tool to find duplicate pictures on your computer. Not only can it find exact matches, but it can also find duplicates among pictures of different kind (PNG, JPG, GIF etc..) and quality. + +Although dupeGuru can easily be used without documentation, reading this file will help you to master it. If you are looking for guidance for your first duplicate scan, you can take a look at the [Quick Start](quick_start.htm) section. + +It is a good idea to keep dupeGuru PE updated. You can download the latest version on the [dupeGuru PE homepage](http://www.hardcoded.net/dupeguru_pe/). + +<%def name="meta()"> diff --git a/pe/help/templates/power_marker.mako b/pe/help/templates/power_marker.mako new file mode 100644 index 00000000..26078f3d --- /dev/null +++ b/pe/help/templates/power_marker.mako @@ -0,0 +1,33 @@ +<%! + title = 'Power Marker' + selected_menu_item = 'Power Marker' +%> +<%inherit file="/base_dg.mako"/> + +You will probably not use the Power Marker feature very often, but if you get into a situation where you need it, you will be pretty happy that this feature exists. + +What is it? +----- + +When the Power Marker mode is enabled, the duplicates are shown without their respective reference file. You can select, mark and sort this list, just like in normal mode. + +So, what is it for? +----- + +The dupeGuru results, when in normal mode, are sorted according to duplicate groups' **reference file**. This means that if you want, for example, to mark all duplicates with the "exe" extension, you cannot just sort the results by "Kind" to have all exe duplicates together because a group can be composed of more than one kind of files. That is where Power Marker comes into play. To mark all your "exe" duplicates, you just have to: + +* Enable the Power marker mode. +* Add the "Kind" column with the "Columns" menu. +* Click on that "Kind" column to sort the list by kind. +* Locate the first duplicate with a "exe" kind. +* Select it. +* Scroll down the list to locate the last duplicate with a "exe" kind. +* Hold Shift and click on it. +* Press Space to mark all selected duplicates. + +Power Marker and delta values +----- + +The Power Marker unveil its true power when you use it with the **Delta Values** switch turned on. When you turn it on, relative values will be displayed instead of absolute ones. So if, for example, you want to remove from your results all duplicates that are more than 300 KB away from their reference, you could sort the Power Marker by Size, select all duplicates under -300 in the Size column, delete them, and then do the same for duplicates over 300 at the bottom of the list. + +You could also use it to change the reference priority of your duplicate list. When you make a fresh scan, if there are no reference directories, the reference file of every group is the biggest file. If you want to change that, for example, to the latest modification time, you can sort the Power Marker by modification time in **descending** order, select all duplicates with a modification time delta value higher than 0 and click on **Make Selected Reference**. The reason why you must make the sort order descending is because if 2 files among the same duplicate group are selected when you click on **Make Selected Reference**, only the first of the list will be made reference, the other will be ignored. And since you want the last modified file to be reference, having the sort order descending assures you that the first item of the list will be the last modified. diff --git a/pe/help/templates/preferences.mako b/pe/help/templates/preferences.mako new file mode 100644 index 00000000..0ef6a2ba --- /dev/null +++ b/pe/help/templates/preferences.mako @@ -0,0 +1,23 @@ +<%! + title = 'Preferences' + selected_menu_item = 'Preferences' +%> +<%inherit file="/base_dg.mako"/> + +**Filter Hardness:** The higher is this setting, the "harder" is the filter (In other words, the less results you get). Most pictures of the same quality match at 100% even if the format is different (PNG and JPG for example.). However, if you want to make a PNG match with a lower quality JPG, you will have to set the filer hardness to lower than 100. The default, 95, is a sweet spot. + +**Match scaled pictures together:** If you check this box, pictures of different dimensions will be allowed in the same duplicate group. + +**Can mix file kind:** If you check this box, duplicate groups are allowed to have files with different extensions. If you don't check it, well, they aren't! + +**Use regular expressions when filtering:** If you check this box, the filtering feature will treat your filter query as a **regular expression**. Explaining them is beyond the scope of this document. A good place to start learning it is . + +**Remove empty folders after delete or move:** When this option is enabled, folders are deleted after a file is deleted or moved and the folder is empty. + +**Copy and Move:** Determines how the Copy and Move operations (in the Action menu) will behave. + +* **Right in destination:** All files will be sent directly in the selected destination, without trying to recreate the source path at all. +* **Recreate relative path:** The source file's path will be re-created in the destination directory up to the root selection in the Directories panel. For example, if you added "/Users/foobar/Picture" to your Directories panel and you move "/Users/foobar/Picture/2006/06/photo.jpg" to the destination "/Users/foobar/MyDestination", the final destination for the file will be "/Users/foobar/MyDestination/2006/06" ("/Users/foobar/Picture" has been trimmed from source's path in the final destination.). +* **Recreate absolute path:** The source file's path will be re-created in the destination directory in it's entirety. For example, if you move "/Users/foobar/Picture/2006/06/photo.jpg" to the destination "/Users/foobar/MyDestination", the final destination for the file will be "/Users/foobar/MyDestination/Users/foobar/Picture/2006/06". + +In all cases, dupeGuru PE nicely handles naming conflicts by prepending a number to the destination filename if the filename already exists in the destination. diff --git a/pe/help/templates/quick_start.mako b/pe/help/templates/quick_start.mako new file mode 100644 index 00000000..dde33c65 --- /dev/null +++ b/pe/help/templates/quick_start.mako @@ -0,0 +1,18 @@ +<%! + title = 'Quick Start' + selected_menu_item = 'Quick Start' +%> +<%inherit file="/base_dg.mako"/> + +To get you quickly started with dupeGuru, let's just make a standard scan using default preferences. + +* Click on **Directories**. +* Click on **Add**. +* Choose a directory you want to scan for duplicates. +* Click on **Start Scanning**. +* Wait until the scan process is over. +* Look at every duplicate (The files that are indented) and verify that it is indeed a duplicate to the group's reference (The file above the duplicate that is not indented and have a disabled mark box). +* If a file is a false duplicate, select it and click on **Actions-->Remove Selected from Results**. +* Once you are sure that there is no false duplicate in your results, click on **Edit-->Mark All**, and then **Actions-->Send Marked to Recycle bin**. + +That is only a basic scan. There are a lot of tweaking you can do to get different results and several methods of examining and modifying your results. To know about them, just read the rest of this help file. diff --git a/pe/help/templates/results.mako b/pe/help/templates/results.mako new file mode 100644 index 00000000..53aa176f --- /dev/null +++ b/pe/help/templates/results.mako @@ -0,0 +1,73 @@ +<%! + title = 'Results' + selected_menu_item = 'Results' +%> +<%inherit file="/base_dg.mako"/> + +When dupeGuru is finished scanning for duplicates, it will show its results in the form of duplicate group list. + +About duplicate groups +----- + +A duplicate group is a group of files that all match together. Every group has a **reference file** and one or more **duplicate files**. The reference file is the first file of the group. Its mark box is disabled. Below it, and indented, are the duplicate files. + +You can mark duplicate files, but you can never mark the reference file of a group. This is a security measure to prevent dupeGuru from deleting not only duplicate files, but their reference. You sure don't want that, do you? + +What determines which files are reference and which files are duplicates is first their directory state. A files from a reference directory will always be reference in a duplicate group. If all files are from a normal directory, the size determine which file will be the reference of a duplicate group. dupeGuru assumes that you always want to keep the biggest file, so the biggest files will take the reference position. + +You can change the reference file of a group manually. To do so, select the duplicate file you want to promote to reference, and click on **Actions-->Make Selected Reference**. + +Reviewing results +----- + +Although you can just click on **Edit-->Mark All** and then **Actions-->Send Marked to Recycle bin** to quickly delete all duplicate files in your results, it is always recommended to review all duplicates before deleting them. + +To help you reviewing the results, you can bring up the **Details panel**. This panel shows all the details of the currently selected file as well as its reference's details. This is very handy to quickly determine if a duplicate really is a duplicate. You can also double-click on a file to open it with its associated application. + +If you have more false duplicates than true duplicates (If your filter hardness is very low), the best way to proceed would be to review duplicates, mark true duplicates and then click on **Actions-->Send Marked to Recycle bin**. If you have more true duplicates than false duplicates, you can instead mark all files that are false duplicates, and use **Actions-->Remove Marked from Results**. + +Marking and Selecting +----- + +A **marked** duplicate is a duplicate with the little box next to it having a check-mark. A **selected** duplicate is a duplicate being highlighted. The multiple selection actions can be performed in dupeGuru in the standard way (Shift/Command/Control click). You can toggle all selected duplicates' mark state by pressing **space**. + +Delta Values +----- + +If you turn this switch on, some columns will display the value relative to the duplicate's reference instead of the absolute values. These delta values will also be displayed in a different color so you can spot them easily. For example, if a duplicate is 1.2 MB and its reference is 1.4 MB, the Size column will display -0.2 MB. This option is a killer feature when combined with the [Power Marker](power_marker.htm). + +Filtering +----- + +dupeGuru supports post-scan filtering. With it, you can narrow down your results so you can perform actions on a subset of it. For example, you could easily mark all duplicates with their filename containing "copy" from your results using the filter. + +**Windows:** To use the filtering feature, click on Actions --> Apply Filter, write down the filter you want to apply and click OK. To go back to unfiltered results, click on Actions --> Cancel Filter. + +**Mac OS X:** To use the filtering feature, type your filter in the "Filter" search field in the toolbar. To go back to unfiltered result, blank out the field, or click on the "X". + +In simple mode (the default mode), whatever you type as the filter is the string used to perform the actual filtering, with the exception of one wildcard: **\***. Thus, if you type "[*]" as your filter, it will match anything with [] brackets in it, whatever is in between those brackets. + +For more advanced filtering, you can turn "Use regular expressions when filtering" on. The filtering feature will then use **regular expressions**. A regular expression is a language for matching text. Explaining them is beyond the scope of this document. A good place to start learning it is . + +Matches are case insensitive in both simple and regexp mode. + +For the filter to match, your regular expression don't have to match the whole filename, it just have to contain a string matching the expression. + +You might notice that not all duplicates in the filtered results will match your filter. That is because as soon as one single duplicate in a group matches the filter, the whole group stays in the results so you can have a better view of the duplicate's context. However, non-matching duplicates are in "reference mode". Therefore, you can perform actions like Mark All and be sure to only mark filtered duplicates. + +Action Menu +----- + +* **Start Duplicate Scan:** Starts a new duplicate scan. +* **Clear Ignore List:** Remove all ignored matches you added. You have to start a new scan for the newly cleared ignore list to be effective. +* **Export Results to XHTML:** Take the current results, and create an XHTML file out of it. The columns that are visible when you click on this button will be the columns present in the XHTML file. The file will automatically be opened in your default browser. +* **Send Marked to Trash:** Send all marked duplicates to trash, obviously. +* **Move Marked to...:** Prompt you for a destination, and then move all marked files to that destination. Source file's path might be re-created in destination, depending on the "Copy and Move" preference. +* **Copy Marked to...:** Prompt you for a destination, and then copy all marked files to that destination. Source file's path might be re-created in destination, depending on the "Copy and Move" preference. +* **Remove Marked from Results:** Remove all marked duplicates from results. The actual files will not be touched and will stay where they are. +* **Remove Selected from Results:** Remove all selected duplicates from results. Note that all selected reference files will be ignored, only duplicates can be removed with this action. +* **Make Selected Reference:** Promote all selected duplicates to reference. If a duplicate is a part of a group having a reference file coming from a reference directory (in blue color), no action will be taken for this duplicate. If more than one duplicate among the same group are selected, only the first of each group will be promoted. +* **Add Selected to Ignore List:** This first removes all selected duplicates from results, and then add the match of that duplicate and the current reference in the ignore list. This match will not come up again in further scan. The duplicate itself might come back, but it will be matched with another reference file. You can clear the ignore list with the Clear Ignore List command. +* **Open Selected with Default Application:** Open the file with the application associated with selected file's type. +* **Reveal Selected in Finder:** Open the folder containing selected file. +* **Rename Selected:** Prompts you for a new name, and then rename the selected file. diff --git a/pe/help/templates/versions.mako b/pe/help/templates/versions.mako new file mode 100644 index 00000000..157c26ba --- /dev/null +++ b/pe/help/templates/versions.mako @@ -0,0 +1,6 @@ +<%! + title = 'dupeGuru PE version history' + selected_menu_item = 'Version History' +%> +<%inherit file="/base_dg.mako"/> +${self.output_changelogs(changelog)} \ No newline at end of file diff --git a/pe/qt/app.py b/pe/qt/app.py new file mode 100644 index 00000000..f40580ad --- /dev/null +++ b/pe/qt/app.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python +# Unit Name: app +# Created By: Virgil Dupras +# Created On: 2009-04-25 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +import os.path as op + +from PyQt4.QtGui import QImage +import PIL.Image + +from hs import fs +from hs.fs import phys +from hs.utils.str import get_file_ext + +from dupeguru import data_pe +from dupeguru.picture.cache import Cache +from dupeguru.picture.matchbase import AsyncMatchFactory + +from block import getblocks +from base.app import DupeGuru as DupeGuruBase +from details_dialog import DetailsDialog +from main_window import MainWindow +from preferences import Preferences +from preferences_dialog import PreferencesDialog + +class File(phys.File): + cls_info_map = { + 'size': fs.IT_ATTRS, + 'ctime': fs.IT_ATTRS, + 'mtime': fs.IT_ATTRS, + 'md5': fs.IT_MD5, + 'md5partial': fs.IT_MD5, + 'dimensions': fs.IT_EXTRA, + } + + def _initialize_info(self, section): + super(File, self)._initialize_info(section) + if section == fs.IT_EXTRA: + self._info.update({ + 'dimensions': (0,0), + }) + + def _read_info(self, section): + super(File, self)._read_info(section) + if section == fs.IT_EXTRA: + im = PIL.Image.open(unicode(self.path)) + self._info['dimensions'] = im.size + + def get_blocks(self, block_count_per_side): + image = QImage(unicode(self.path)) + image = image.convertToFormat(QImage.Format_RGB888) + return getblocks(image, block_count_per_side) + + +class Directory(phys.Directory): + cls_file_class = File + cls_supported_exts = ('png', 'jpg', 'jpeg', 'gif', 'bmp', 'tiff') + + def _fetch_subitems(self): + subdirs, subfiles = super(Directory, self)._fetch_subitems() + return subdirs, [name for name in subfiles if get_file_ext(name) in self.cls_supported_exts] + + +class DupeGuru(DupeGuruBase): + LOGO_NAME = 'logo_pe' + NAME = 'dupeGuru Picture Edition' + VERSION = '1.7.2' + DELTA_COLUMNS = frozenset([2, 5, 6]) + + def __init__(self): + DupeGuruBase.__init__(self, data_pe, appid=5) + + def _setup(self): + self.scanner.match_factory = AsyncMatchFactory() + self.directories.dirclass = Directory + self.scanner.match_factory.cached_blocks = Cache(op.join(self.appdata, 'cached_pictures.db')) + DupeGuruBase._setup(self) + + def _update_options(self): + DupeGuruBase._update_options(self) + self.scanner.match_factory.match_scaled = self.prefs.match_scaled + self.scanner.match_factory.threshold = self.prefs.filter_hardness + + def _create_details_dialog(self, parent): + return DetailsDialog(parent, self) + + def _create_main_window(self): + return MainWindow(app=self) + + def _create_preferences(self): + return Preferences() + + def _create_preferences_dialog(self, parent): + return PreferencesDialog(parent, self) + diff --git a/pe/qt/app_win.py b/pe/qt/app_win.py new file mode 100644 index 00000000..a18ce2e9 --- /dev/null +++ b/pe/qt/app_win.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python +# Unit Name: app_win +# Created By: Virgil Dupras +# Created On: 2009-05-02 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +import winshell + +import app + +class DupeGuru(app.DupeGuru): + @staticmethod + def _recycle_dupe(dupe): + winshell.delete_file(unicode(dupe.path), no_confirm=True) + diff --git a/pe/qt/base/__init__.py b/pe/qt/base/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pe/qt/base/about_box.py b/pe/qt/base/about_box.py new file mode 100644 index 00000000..55a36eb1 --- /dev/null +++ b/pe/qt/base/about_box.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python +# Unit Name: about_box +# Created By: Virgil Dupras +# Created On: 2009-05-09 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +from PyQt4.QtCore import Qt, QCoreApplication, SIGNAL +from PyQt4.QtGui import QDialog, QDialogButtonBox, QPixmap + +from about_box_ui import Ui_AboutBox + +class AboutBox(QDialog, Ui_AboutBox): + def __init__(self, parent, app): + flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint | Qt.MSWindowsFixedSizeDialogHint + QDialog.__init__(self, parent, flags) + self.app = app + self._setupUi() + + self.connect(self.buttonBox, SIGNAL('clicked(QAbstractButton*)'), self.buttonClicked) + + def _setupUi(self): + self.setupUi(self) + # Stuff that can't be done in the Designer + self.setWindowTitle(u"About %s" % QCoreApplication.instance().applicationName()) + self.nameLabel.setText(QCoreApplication.instance().applicationName()) + self.versionLabel.setText('Version ' + QCoreApplication.instance().applicationVersion()) + self.logoLabel.setPixmap(QPixmap(':/%s_big' % self.app.LOGO_NAME)) + self.registerButton = self.buttonBox.addButton("Register", QDialogButtonBox.ActionRole) + + #--- Events + def buttonClicked(self, button): + if button is self.registerButton: + self.app.ask_for_reg_code() + diff --git a/pe/qt/base/about_box.ui b/pe/qt/base/about_box.ui new file mode 100644 index 00000000..aa9c5ce5 --- /dev/null +++ b/pe/qt/base/about_box.ui @@ -0,0 +1,133 @@ + + + AboutBox + + + + 0 + 0 + 400 + 190 + + + + + 0 + 0 + + + + About dupeGuru + + + + + + + + + :/logo_me_big + + + + + + + + + + 75 + true + + + + dupeGuru + + + + + + + Version + + + + + + + Copyright Hardcoded Software 2009 + + + + + + + + 75 + true + + + + Registered To: + + + + + + + UNREGISTERED + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Ok + + + + + + + + + + + + + buttonBox + accepted() + AboutBox + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + AboutBox + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/pe/qt/base/app.py b/pe/qt/base/app.py new file mode 100644 index 00000000..3fa340bf --- /dev/null +++ b/pe/qt/base/app.py @@ -0,0 +1,269 @@ +#!/usr/bin/env python +# Unit Name: app +# Created By: Virgil Dupras +# Created On: 2009-04-25 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +import logging +import os.path as op +import traceback + +from PyQt4.QtCore import Qt, QTimer, QObject, QCoreApplication, QUrl, SIGNAL +from PyQt4.QtGui import QProgressDialog, QDesktopServices, QFileDialog, QDialog, QMessageBox + +from hsutil import job +from hsutil.reg import RegistrationRequired + +from dupeguru import data_pe +from dupeguru.app import (DupeGuru as DupeGuruBase, JOB_SCAN, JOB_LOAD, JOB_MOVE, JOB_COPY, + JOB_DELETE) + +from main_window import MainWindow +from directories_dialog import DirectoriesDialog +from about_box import AboutBox +from reg import Registration +from error_report_dialog import ErrorReportDialog + +JOBID2TITLE = { + JOB_SCAN: "Scanning for duplicates", + JOB_LOAD: "Loading", + JOB_MOVE: "Moving", + JOB_COPY: "Copying", + JOB_DELETE: "Sending files to the recycle bin", +} + +class Progress(QProgressDialog, job.ThreadedJobPerformer): + def __init__(self, parent): + flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint + QProgressDialog.__init__(self, '', u"Cancel", 0, 100, parent, flags) + self.setModal(True) + self.setAutoReset(False) + self.setAutoClose(False) + self._timer = QTimer() + self._jobid = '' + self.connect(self._timer, SIGNAL('timeout()'), self.updateProgress) + + def updateProgress(self): + # the values might change before setValue happens + last_progress = self.last_progress + last_desc = self.last_desc + if not self._job_running or last_progress is None: + self._timer.stop() + self.close() + self.emit(SIGNAL('finished(QString)'), self._jobid) + if self._last_error is not None: + s = ''.join(traceback.format_exception(*self._last_error)) + dialog = ErrorReportDialog(self.parent(), s) + dialog.exec_() + return + if self.wasCanceled(): + self.job_cancelled = True + return + if last_desc: + self.setLabelText(last_desc) + self.setValue(last_progress) + + def run(self, jobid, title, target, args=()): + self._jobid = jobid + self.reset() + self.setLabelText('') + self.run_threaded(target, args) + self.setWindowTitle(title) + self.show() + self._timer.start(500) + + +def demo_method(method): + def wrapper(self, *args, **kwargs): + try: + return method(self, *args, **kwargs) + except RegistrationRequired: + msg = "The demo version of dupeGuru only allows 10 actions (delete/move/copy) per session." + QMessageBox.information(self.main_window, 'Demo', msg) + + return wrapper + +class DupeGuru(DupeGuruBase, QObject): + LOGO_NAME = '' + NAME = '' + DELTA_COLUMNS = frozenset() + + def __init__(self, data_module, appid): + appdata = unicode(QDesktopServices.storageLocation(QDesktopServices.DataLocation)) + DupeGuruBase.__init__(self, data_module, appdata, appid) + QObject.__init__(self) + self._setup() + + #--- Private + def _setup(self): + self.selected_dupe = None + self.prefs = self._create_preferences() + self.prefs.load() + self._update_options() + self.main_window = self._create_main_window() + self._progress = Progress(self.main_window) + self.directories_dialog = DirectoriesDialog(self.main_window, self) + self.details_dialog = self._create_details_dialog(self.main_window) + self.preferences_dialog = self._create_preferences_dialog(self.main_window) + self.about_box = AboutBox(self.main_window, self) + + self.reg = Registration(self) + self.set_registration(self.prefs.registration_code, self.prefs.registration_email) + if not self.registered: + self.reg.show_nag() + self.main_window.show() + self.load() + + self.connect(QCoreApplication.instance(), SIGNAL('aboutToQuit()'), self.application_will_terminate) + self.connect(self._progress, SIGNAL('finished(QString)'), self.job_finished) + + def _setup_as_registered(self): + self.prefs.registration_code = self.registration_code + self.prefs.registration_email = self.registration_email + self.main_window.actionRegister.setVisible(False) + self.about_box.registerButton.hide() + self.about_box.registeredEmailLabel.setText(self.prefs.registration_email) + + def _update_options(self): + self.scanner.mix_file_kind = self.prefs.mix_file_kind + self.options['escape_filter_regexp'] = self.prefs.use_regexp + self.options['clean_empty_dirs'] = self.prefs.remove_empty_folders + + #--- Virtual + def _create_details_dialog(self, parent): + raise NotImplementedError() + + def _create_main_window(self): + return MainWindow(app=self) + + def _create_preferences(self): + raise NotImplementedError() + + def _create_preferences_dialog(self, parent): + raise NotImplementedError() + + #--- Override + def _start_job(self, jobid, func): + title = JOBID2TITLE[jobid] + try: + j = self._progress.create_job() + self._progress.run(jobid, title, func, args=(j, )) + except job.JobInProgressError: + msg = "A previous action is still hanging in there. You can't start a new one yet. Wait a few seconds, then try again." + QMessageBox.information(self.main_window, 'Action in progress', msg) + + #--- Public + def add_dupes_to_ignore_list(self, duplicates): + for dupe in duplicates: + self.AddToIgnoreList(dupe) + self.remove_duplicates(duplicates) + + def ApplyFilter(self, filter): + DupeGuruBase.ApplyFilter(self, filter) + self.emit(SIGNAL('resultsChanged()')) + + def ask_for_reg_code(self): + if self.reg.ask_for_code(): + self._setup_ui_as_registered() + + @demo_method + def copy_or_move_marked(self, copy): + opname = 'copy' if copy else 'move' + title = "Select a directory to {0} marked files to".format(opname) + flags = QFileDialog.ShowDirsOnly + destination = unicode(QFileDialog.getExistingDirectory(self.main_window, title, '', flags)) + if not destination: + return + recreate_path = self.prefs.destination_type + DupeGuruBase.copy_or_move_marked(self, copy, destination, recreate_path) + + delete_marked = demo_method(DupeGuruBase.delete_marked) + + def make_reference(self, duplicates): + DupeGuruBase.make_reference(self, duplicates) + self.emit(SIGNAL('resultsChanged()')) + + def mark_all(self): + self.results.mark_all() + self.emit(SIGNAL('dupeMarkingChanged()')) + + def mark_invert(self): + self.results.mark_invert() + self.emit(SIGNAL('dupeMarkingChanged()')) + + def mark_none(self): + self.results.mark_none() + self.emit(SIGNAL('dupeMarkingChanged()')) + + def open_selected(self): + if self.selected_dupe is None: + return + url = QUrl.fromLocalFile(unicode(self.selected_dupe.path)) + QDesktopServices.openUrl(url) + + def remove_duplicates(self, duplicates): + self.results.remove_duplicates(duplicates) + self.emit(SIGNAL('resultsChanged()')) + + def remove_marked_duplicates(self): + marked = [d for d in self.results.dupes if self.results.is_marked(d)] + self.remove_duplicates(marked) + + def rename_dupe(self, dupe, newname): + try: + dupe.move(dupe.parent, newname) + return True + except (IndexError, fs.FSError) as e: + logging.warning("dupeGuru Warning: %s" % unicode(e)) + return False + + def reveal_selected(self): + if self.selected_dupe is None: + return + url = QUrl.fromLocalFile(unicode(self.selected_dupe.path[:-1])) + QDesktopServices.openUrl(url) + + def select_duplicate(self, dupe): + self.selected_dupe = dupe + self.emit(SIGNAL('duplicateSelected()')) + + def show_about_box(self): + self.about_box.show() + + def show_details(self): + self.details_dialog.show() + + def show_directories(self): + self.directories_dialog.show() + + def show_help(self): + url = QUrl.fromLocalFile(op.abspath('help/intro.htm')) + QDesktopServices.openUrl(url) + + def show_preferences(self): + self.preferences_dialog.load() + result = self.preferences_dialog.exec_() + if result == QDialog.Accepted: + self.preferences_dialog.save() + self.prefs.save() + self._update_options() + + def toggle_marking_for_dupes(self, dupes): + for dupe in dupes: + self.results.mark_toggle(dupe) + self.emit(SIGNAL('dupeMarkingChanged()')) + + #--- Events + def application_will_terminate(self): + self.Save() + self.SaveIgnoreList() + + def job_finished(self, jobid): + self.emit(SIGNAL('resultsChanged()')) + if jobid == JOB_LOAD: + self.emit(SIGNAL('directoriesChanged()')) + if jobid in (JOB_MOVE, JOB_COPY, JOB_DELETE) and self.last_op_error_count > 0: + msg = "{0} files could not be processed.".format(self.results.mark_count) + QMessageBox.warning(self.main_window, 'Warning', msg) + diff --git a/pe/qt/base/details_table.py b/pe/qt/base/details_table.py new file mode 100644 index 00000000..1c45de1e --- /dev/null +++ b/pe/qt/base/details_table.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python +# Unit Name: details_table +# Created By: Virgil Dupras +# Created On: 2009-05-17 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +from PyQt4.QtCore import Qt, SIGNAL, QAbstractTableModel, QVariant +from PyQt4.QtGui import QHeaderView, QTableView + +HEADER = ['Attribute', 'Selected', 'Reference'] + +class DetailsModel(QAbstractTableModel): + def __init__(self, app): + QAbstractTableModel.__init__(self) + self._app = app + self._data = app.data + self._dupe_data = None + self._ref_data = None + self.connect(app, SIGNAL('duplicateSelected()'), self.duplicateSelected) + + def columnCount(self, parent): + return len(HEADER) + + def data(self, index, role): + if not index.isValid(): + return QVariant() + if role != Qt.DisplayRole: + return QVariant() + column = index.column() + row = index.row() + if column == 0: + return QVariant(self._data.COLUMNS[row]['display']) + elif column == 1 and self._dupe_data: + return QVariant(self._dupe_data[row]) + elif column == 2 and self._ref_data: + return QVariant(self._ref_data[row]) + return QVariant() + + def headerData(self, section, orientation, role): + if orientation == Qt.Horizontal and role == Qt.DisplayRole and section < len(HEADER): + return QVariant(HEADER[section]) + return QVariant() + + def rowCount(self, parent): + return len(self._data.COLUMNS) + + #--- Events + def duplicateSelected(self): + dupe = self._app.selected_dupe + group = self._app.results.get_group_of_duplicate(dupe) + ref = group.ref + self._dupe_data = self._data.GetDisplayInfo(dupe, group) + self._ref_data = self._data.GetDisplayInfo(ref, group) + self.reset() + + +class DetailsTable(QTableView): + def __init__(self, *args): + QTableView.__init__(self, *args) + self.setAlternatingRowColors(True) + self.setSelectionBehavior(QTableView.SelectRows) + self.setShowGrid(False) + + def setModel(self, model): + QTableView.setModel(self, model) + # The model needs to be set to set header stuff + hheader = self.horizontalHeader() + hheader.setHighlightSections(False) + hheader.setStretchLastSection(False) + hheader.resizeSection(0, 100) + hheader.setResizeMode(0, QHeaderView.Fixed) + hheader.setResizeMode(1, QHeaderView.Stretch) + hheader.setResizeMode(2, QHeaderView.Stretch) + vheader = self.verticalHeader() + vheader.setVisible(False) + vheader.setDefaultSectionSize(18) + diff --git a/pe/qt/base/dg.qrc b/pe/qt/base/dg.qrc new file mode 100644 index 00000000..f2f5e936 --- /dev/null +++ b/pe/qt/base/dg.qrc @@ -0,0 +1,17 @@ + + + images/details32.png + images/dgpe_logo_32.png + images/dgpe_logo_128.png + images/dgme_logo_32.png + images/dgme_logo_128.png + images/dgse_logo_32.png + images/dgse_logo_128.png + images/folderwin32.png + images/gear.png + images/preferences32.png + images/actions32.png + images/delta32.png + images/power_marker32.png + + \ No newline at end of file diff --git a/pe/qt/base/directories_dialog.py b/pe/qt/base/directories_dialog.py new file mode 100644 index 00000000..e2f3ddb3 --- /dev/null +++ b/pe/qt/base/directories_dialog.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python +# Unit Name: directories_dialog +# Created By: Virgil Dupras +# Created On: 2009-04-25 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +from PyQt4.QtCore import SIGNAL, Qt +from PyQt4.QtGui import QDialog, QFileDialog, QHeaderView + +from directories_dialog_ui import Ui_DirectoriesDialog +from directories_model import DirectoriesModel, DirectoriesDelegate + +class DirectoriesDialog(QDialog, Ui_DirectoriesDialog): + def __init__(self, parent, app): + flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint + QDialog.__init__(self, parent, flags) + self.app = app + self._setupUi() + self._updateRemoveButton() + + self.connect(self.doneButton, SIGNAL('clicked()'), self.doneButtonClicked) + self.connect(self.addButton, SIGNAL('clicked()'), self.addButtonClicked) + self.connect(self.removeButton, SIGNAL('clicked()'), self.removeButtonClicked) + self.connect(self.treeView.selectionModel(), SIGNAL('selectionChanged(QItemSelection,QItemSelection)'), self.selectionChanged) + self.connect(self.app, SIGNAL('directoriesChanged()'), self.directoriesChanged) + + def _setupUi(self): + self.setupUi(self) + # Stuff that can't be done in the Designer + self.directoriesModel = DirectoriesModel(self.app) + self.directoriesDelegate = DirectoriesDelegate() + self.treeView.setItemDelegate(self.directoriesDelegate) + self.treeView.setModel(self.directoriesModel) + + header = self.treeView.header() + header.setStretchLastSection(False) + header.setResizeMode(0, QHeaderView.Stretch) + header.setResizeMode(1, QHeaderView.Fixed) + header.resizeSection(1, 100) + + def _updateRemoveButton(self): + indexes = self.treeView.selectedIndexes() + if not indexes: + self.removeButton.setEnabled(False) + return + self.removeButton.setEnabled(True) + index = indexes[0] + node = index.internalPointer() + # label = 'Remove' if node.parent is None else 'Exclude' + + def addButtonClicked(self): + title = u"Select a directory to add to the scanning list" + flags = QFileDialog.ShowDirsOnly + dirpath = unicode(QFileDialog.getExistingDirectory(self, title, '', flags)) + if not dirpath: + return + self.app.AddDirectory(dirpath) + self.directoriesModel.reset() + + def directoriesChanged(self): + self.directoriesModel.reset() + + def doneButtonClicked(self): + self.hide() + + def removeButtonClicked(self): + indexes = self.treeView.selectedIndexes() + if not indexes: + return + index = indexes[0] + node = index.internalPointer() + if node.parent is None: + row = index.row() + del self.app.directories[row] + self.directoriesModel.reset() + + def selectionChanged(self, selected, deselected): + self._updateRemoveButton() + diff --git a/pe/qt/base/directories_dialog.ui b/pe/qt/base/directories_dialog.ui new file mode 100644 index 00000000..68bc8d84 --- /dev/null +++ b/pe/qt/base/directories_dialog.ui @@ -0,0 +1,133 @@ + + + DirectoriesDialog + + + + 0 + 0 + 420 + 338 + + + + Directories + + + + + + QAbstractItemView::DoubleClicked|QAbstractItemView::EditKeyPressed|QAbstractItemView::SelectedClicked + + + true + + + false + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 91 + 0 + + + + + 16777215 + 32 + + + + Remove + + + + + + + + 91 + 0 + + + + + 16777215 + 32 + + + + Add + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + + 91 + 0 + + + + + 16777215 + 32 + + + + Done + + + true + + + + + + + + + + diff --git a/pe/qt/base/directories_model.py b/pe/qt/base/directories_model.py new file mode 100644 index 00000000..cae88f39 --- /dev/null +++ b/pe/qt/base/directories_model.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python +# Unit Name: directories_model +# Created By: Virgil Dupras +# Created On: 2009-04-25 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +from PyQt4.QtCore import QVariant, QModelIndex, Qt, QRect, QEvent, QPoint +from PyQt4.QtGui import QComboBox, QStyledItemDelegate, QMouseEvent, QApplication, QBrush + +from tree_model import TreeNode, TreeModel + +HEADERS = ['Name', 'State'] +STATES = ['Normal', 'Reference', 'Excluded'] + +class DirectoriesDelegate(QStyledItemDelegate): + def createEditor(self, parent, option, index): + editor = QComboBox(parent); + editor.addItems(STATES) + return editor + + def setEditorData(self, editor, index): + value, ok = index.model().data(index, Qt.EditRole).toInt() + assert ok + editor.setCurrentIndex(value); + press = QMouseEvent(QEvent.MouseButtonPress, QPoint(0, 0), Qt.LeftButton, Qt.LeftButton, Qt.NoModifier) + release = QMouseEvent(QEvent.MouseButtonRelease, QPoint(0, 0), Qt.LeftButton, Qt.LeftButton, Qt.NoModifier) + QApplication.sendEvent(editor, press) + QApplication.sendEvent(editor, release) + # editor.showPopup() # this causes a weird glitch. the ugly workaround is above. + + def setModelData(self, editor, model, index): + value = QVariant(editor.currentIndex()) + model.setData(index, value, Qt.EditRole) + + def updateEditorGeometry(self, editor, option, index): + editor.setGeometry(option.rect) + + +class DirectoryNode(TreeNode): + def __init__(self, parent, ref, row): + TreeNode.__init__(self, parent, row) + self.ref = ref + + def _get_children(self): + children = [] + for index, directory in enumerate(self.ref.dirs): + node = DirectoryNode(self, directory, index) + children.append(node) + return children + + +class DirectoriesModel(TreeModel): + def __init__(self, app): + self._dirs = app.directories + TreeModel.__init__(self) + + def _root_nodes(self): + nodes = [] + for index, directory in enumerate(self._dirs): + nodes.append(DirectoryNode(None, directory, index)) + return nodes + + def columnCount(self, parent): + return 2 + + def data(self, index, role): + if not index.isValid(): + return QVariant() + node = index.internalPointer() + if role == Qt.DisplayRole: + if index.column() == 0: + return QVariant(node.ref.name) + else: + return QVariant(STATES[self._dirs.GetState(node.ref.path)]) + elif role == Qt.EditRole and index.column() == 1: + return QVariant(self._dirs.GetState(node.ref.path)) + elif role == Qt.ForegroundRole: + state = self._dirs.GetState(node.ref.path) + if state == 1: + return QVariant(QBrush(Qt.blue)) + elif state == 2: + return QVariant(QBrush(Qt.red)) + return QVariant() + + def flags(self, index): + if not index.isValid(): + return 0 + result = Qt.ItemIsEnabled | Qt.ItemIsSelectable + if index.column() == 1: + result |= Qt.ItemIsEditable + return result + + def headerData(self, section, orientation, role): + if orientation == Qt.Horizontal: + if role == Qt.DisplayRole and section < len(HEADERS): + return QVariant(HEADERS[section]) + return QVariant() + + def setData(self, index, value, role): + if not index.isValid() or role != Qt.EditRole or index.column() != 1: + return False + node = index.internalPointer() + state, ok = value.toInt() + assert ok + self._dirs.SetState(node.ref.path, state) + return True + diff --git a/pe/qt/base/error_report_dialog.py b/pe/qt/base/error_report_dialog.py new file mode 100644 index 00000000..4aa8f977 --- /dev/null +++ b/pe/qt/base/error_report_dialog.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python +# Unit Name: error_report_dialog +# Created By: Virgil Dupras +# Created On: 2009-05-23 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +from PyQt4.QtCore import Qt, QUrl +from PyQt4.QtGui import QDialog, QDesktopServices + +from error_report_dialog_ui import Ui_ErrorReportDialog + +class ErrorReportDialog(QDialog, Ui_ErrorReportDialog): + def __init__(self, parent, error): + flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint + QDialog.__init__(self, parent, flags) + self.setupUi(self) + self.errorTextEdit.setPlainText(error) + + def accept(self): + text = self.errorTextEdit.toPlainText() + url = QUrl("mailto:support@hardcoded.net?SUBJECT=Error Report&BODY=%s" % text) + QDesktopServices.openUrl(url) + QDialog.accept(self) + diff --git a/pe/qt/base/error_report_dialog.ui b/pe/qt/base/error_report_dialog.ui new file mode 100644 index 00000000..0974dd2f --- /dev/null +++ b/pe/qt/base/error_report_dialog.ui @@ -0,0 +1,117 @@ + + + ErrorReportDialog + + + + 0 + 0 + 553 + 349 + + + + Error Report + + + + + + Something went wrong. Would you like to send the error report to Hardcoded Software? + + + true + + + + + + + true + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 110 + 0 + + + + Don't Send + + + + + + + + 110 + 0 + + + + Send + + + true + + + + + + + + + + + sendButton + clicked() + ErrorReportDialog + accept() + + + 485 + 320 + + + 276 + 174 + + + + + dontSendButton + clicked() + ErrorReportDialog + reject() + + + 373 + 320 + + + 276 + 174 + + + + + diff --git a/pe/qt/base/gen.py b/pe/qt/base/gen.py new file mode 100644 index 00000000..3b0df2fa --- /dev/null +++ b/pe/qt/base/gen.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python +# Unit Name: gen +# Created By: Virgil Dupras +# Created On: 2009-05-22 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +import os + +def print_and_do(cmd): + print cmd + os.system(cmd) + +print_and_do("pyuic4 main_window.ui > main_window_ui.py") +print_and_do("pyuic4 directories_dialog.ui > directories_dialog_ui.py") +print_and_do("pyuic4 about_box.ui > about_box_ui.py") +print_and_do("pyuic4 reg_submit_dialog.ui > reg_submit_dialog_ui.py") +print_and_do("pyuic4 reg_demo_dialog.ui > reg_demo_dialog_ui.py") +print_and_do("pyuic4 error_report_dialog.ui > error_report_dialog_ui.py") +print_and_do("pyrcc4 dg.qrc > dg_rc.py") \ No newline at end of file diff --git a/pe/qt/base/images/actions32.png b/pe/qt/base/images/actions32.png new file mode 100755 index 0000000000000000000000000000000000000000..16d64fc1e439ab6ae90a1e11cc99831813b2416f GIT binary patch literal 3039 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 literal 0 HcmV?d00001 diff --git a/pe/qt/base/images/delta32.png b/pe/qt/base/images/delta32.png new file mode 100755 index 0000000000000000000000000000000000000000..f7d95da07919c34aa960d37448642e229e75f1e6 GIT binary patch literal 1941 zcmV;G2Wt3RCwCt zS8Gs|*BO4k%iebZcVR&!3IRdrf+*408YCDms3l`#Qytr+HZj&|%@Yh+mbXj@U6o(toJChVaZb-Lv_IXNj18GV4^IK0!*0jJZM_Wb%P^Xe6)u{Duc z66EDF}KQKfSUMGn@lO; zwOUzKU2W97_}#z81i>1WB*bJ1;yt;ewb8n(K{X}{I z^AyMolN5s?n8@_Gu^4j5{6?Q2Mz0tCYu6xs*$dg{d=(=3I=$>kdu_k%|@y8f>UXBhj%kD+@?;YuETVyevoDhY_WNL#`0Za6G!WILW`cvw7iF9pl!0(hmn2>ln|<5RJ>b?5;1JPgllEk`$85O2DqE!mFQN z!a!yUDCACl)J1U)xTn2vhe8ODC8CrKCrG^H{Xr5f-H$V>Zhxmy^XibphjZ6cCJriY zV_HxgwvR16R4-ilIY*jx8Vbn>&=1y)reziJV7 zWb^@EN&WO4l#;kZH}%kg0s2moBgPc?^xs9i%3iB8{ABz3oP86MZhSU~^h?z4oZIKR;IGDok1U7SN7!T z@hK0bV$IAPxw1nL{48mz(P&;@$?5yc*68091c&Ox0EX#?pFNC;}=J8tI38Z z6J*wkV&It?w0+czvyTo#a^-j;0YQm&dc*6H^Z$Q;be4bN*# zcNd);KwvtABkAJ1dkBX{rx4?3eW9TOIkv^v{YVPTl}oYZ&wqp%i_eoM2yyujTekel zH#LPW0`QlHytc^+4^CZ9mD-VX00`hM%8}<;2D@E_J#|Lnc6m(|;;OZDo_B&wb`579NW`!c+$I9=l z+j70$fI^GGyloY;v+sHky$*EJbUYgXimAJL@oqBtRfKG!W<{^Y(m4l7so=3$SGqJ;|)3(~3=Ka%lfbnWbcSI%|0K~V^;+EvLguF8i}K)1V?RNAx|xp&;4Y&h4=DG{!O<-q8_ z+Z;DX|Ctd$-$cdlMhtsCAbOKXV=Z38LuFJU8jU_Y;j6wf8fRdc>b1H=JQm`a|NGbR be+3u-4f`}H8YPeZ00000NkvXXu0mjf0Q9d* literal 0 HcmV?d00001 diff --git a/pe/qt/base/images/details32.png b/pe/qt/base/images/details32.png new file mode 100644 index 0000000000000000000000000000000000000000..8ab245b41ba6f367f19d01227df99b85872bac81 GIT binary patch literal 5090 zcmV<86CLb{P)oBo>e5z;bFg=!FZok4Fh==CO@PG=T)!6@)LqtRs6TP$WWnN4K1n#qi#&1NB! z$v}CoBHFfXJMG%_;VJFDblU(B(0?yq{=$>}2bPl&7iTxs^F|T`Blb@qp6AJ6&?9Ia znT&e%J4U=`i+!fYIlZ7Kf%IA!yteV#WA>$JOU81M-`~yvbbsk~0Zf@c|MAC`EO`ZQ z!#!>oh_zEB93`Dzz^C}bT|FnuG9^+X#S@D9J}pUW! zCfR=oe@zCAUa$L&CCi@EDbz(NQZI-r3KWcl=nw0*P$H33Gh#4cr}cUQMM%(Pvch&# z!DcG>E0vN+Oo*U}mE7(;N~P24cNpkg;F%YflM!IafVeFH3t#!s;*ljqr{@>0Ab-1`%*cQ&gEj(+ z*1*(9p8npi8zrMQz$7al)#DvWje4WL{d^u)22@X z7+$RtcI*uVrvXTaiQA%(5xoA_^WSzR0zx_Bs+Lhl|`!vJ_SUJ*r~xGfc$ zJ#XF<_l>R|g-|<53tq{-N%JYH8q%L;%$P>?=bNduK1?EZD#;2t8K)$QH7%vH1(@@a zXv9b#b#`=7hrg42kWy8F@aI}NMSdujic&NZyX6cxN=gf#Td{JHtWXqH#PQkSGZ_%5 z;X(uT?N>oQUjdytQ%CIxhAB<97bKwFNl2@zJdjGJJS>n&)FI^ z1FU(qc3z=9J3ppz)ATgwhaPHeN>fvPikdGb=}J=o#Vk`eAk(F0g-)ODd2tr_^DlfC z*#i)dTkRh`ZA4EJB_PjYrii~;2B6yu-u&q+c8ATCN>YHC93_`NJQ?ZmN1xG^wpLm^ z$4jH{Gf}Wp27MH@c|k>?M5F=;2PKL}pg?h%K0T12U%wf^t~;r`yadH-BL{2*>)JO| zQ8fecYzA_2edE{P%zSvt#2F7w38yFso6E^L&Z;{RtAF)7^&Kn@9p5jK$-z^80Tjo< zp%6q8vBeXSdv@YK_D%c_Tug=R(c2GYw(n1x(|GMUSFx@f~6 zSJ9pi57N#Z`{~@57wAHL6SbW0M8iqZXK9gkti9UwT-az&k%0P;GGLu{)#w>bMzvFa z4FDy99sm{MzAgjavhw2T%a%P0_QvGX=j#Nk#Vk}5SAasil=+7|7ZN*!dH=YGnlAZO z9v2kmsiv&FqL?gJi`p>*Fn3`zs0dQ;Z|k78D_zulE`gqqfUQsn*Ci)FI2=$QgW+ZX zluQkX*&5()R>b2LFMh^e;4Vk!5tTEi&KZ{e^Behn`jsk`mHlb_gfYs%zLj*j^|G>I z{dTfp?y9?Zl)BoqLC0VxSDr(a%>fv8+@5>@(^GZzP#8L14Ip{EOHQmTZ0aU@0;PPb5di%fOf>a@%1$(@%EOJSyxQZFIHG;H`F z0PrDu#VVDUQ>7aQpP^7Jj$w#RLm2?qQ3ySR8}iDlKP0o!0?QC#sw3-Q8J}4=C$w+( z$L67<_4N26JGW^VT063jAekCIOT@HGY^6yj*GHp=H%Wo;GXsHH0vX?W}^I} zS>r7w(XV5;~b@v z;9*kruCO0SOVVmJi%-n`c2G$hm0$j-lLCGWA_&?B&6lO#OO7=&W}o9xSv{t3)j&t) z{+;bzpyE69-kJ?`XKCutf_?qDLVSe z=j!^!&wd5vDy7PSeNh)xG-~v4s;a7_vWjBG<8XtG5_~L1Hz;5#saCM0#V2P0aHXZi z+~UOxk)K#X3p}DNW=>@AGmW$-=P&qcrxpQlhODQft#i7quZ(cayMR06_MaK$Bs+{o%7%EIU7N#n+kk_>{3 z{g)CNwUfCN;+}nOfl^jdfmo7R0Bl`v2zGbXjQHK%FwsxXR||K{@? zh`{LPE7eiWG&4Q_rkh^-t(T@hVMpJ%{yN3QJ{oiP$TaMR3d51ixYJiM>qM&Ce9UYxgp{FDXK4qzW z6~No8evX0K#z`cx4^kR{E{$uk>@!O+z=8qXI()*E=?K8E`zEw6vO2IJL<=rvwb?9@NB?fdv7Os$%MNbs zvd(}pyYp_NYL&D7k%=DbOGZ=XSyh;88%PAw%yF?y9~s z?6BkksccSD$|RF)(w>A_(CV!QgFZ5R#9;Bk>GxIq_TBgIdH2mAz47}3wVGH1)9E-e z!!pNXG9B3!r=7o#!vDurDa||`FtVp`3IbGz7&``em&>6PpdWzuGKyC&EG&@R-n^94 z;YgY-ri4z;xe*SSFZ~x{PJ@mMZWN3dUdc?j>aoWBnLZ^=< zx~YUf6m(s=xVTWs_#dn4gNjhBWo1PGRHXQPUUp7GHYALfDcp-$&xA}0F2q0xxfGJJ z8K8w)`*xN@8N$dSL&N>ZbZDeV?1& zilS381`Gbl?zAPa;|aGrU$SGjolXaN3*5}eDdniU^710Z>w_D0IXH{e%(0r_IiAZr z`c(tZx`Cutk7%k~Tx+hetmOcOEVOyULHclKtt?A&(BsKJ4h0BE>39m}&V?c@ZTELn z{^p%OOkMNt`NG(U?M!b1-MnaA%^ltSHgHe?@vgWnZaJ-!jLzL33H|M>Ig04D8>(pT>f?OSBI)L|I;Qe@f zT!Z)J%(*r;G);f|f8To$!Ja89E;t6N)B~&;^&u4cyvcq8Dq@2NS0)BkRVn~0mb{kjg`3!H-=#j?b!PS*cMJr7tsqkw|n%-MNc{;aUR#bH1dk z@XwIQPbW;Q=|I!dgZE|=rXQWaTM>W_fnyRWyKm>*xpM=3`t*^7+_vA^+Dhx!ucu%zNGn#Xpl6?bR!Jt442TFIm@$LQfkBG(EK^#IkZ?FIv4z-L@M7B4t^=KS3stlu^UD&-ExB3+(BcZ=7V9}q|L>e-W#zLNs=v>8k-00++I6+$F_ZC4VNw_EDlR+ zVNpSci8mo&1z;5Ms>*a{Agt{^yN4g&Yt@mHWuP4Z*apS{Kmvdag}h!b8jPKy(P)pK ze~ukH)(vF#?Af$r$r7cis!9S7@yeAe3`iFi+ob>kZf78{07fl1fFLvICLv2wM|+nC z>UGEVZF|d4{N*&S@Ja^^T$jygi@~B~r7|m%k)GUF{K!DZp+g5l`}WnUGXbcs^y}9z z1Hfvvs;)2>KyN_UK>pZck1-&anyTdX_Vz>|5QvJR7{~%aux|2%}jus}olE0KpWEy+jB^Cv0li-_{|WI(b%KckTjeEhSnV)(E6F0;!E4E1?`9 z5gpmY?c27djV23i+O$cr*=$5Pveo@!lKSeauhPnuE9v0Dg9NH*p2neCU`8jVSPTeD z;ei7OsJXfMTImbrfWVPjL8(R@o0ygZ4D+TzHs!(VcQ7!aa3m3ri%cwHydc1Na#5TM z^YA~&OajJS3Wme-@@20nX0tix8SOQ%mX;P8KYl!;2%%`Gt}gR%YTdeZ^!n?stDx8g zKp)z$VM8|fYad8ST0yfW6vWQ6pxI(|@)dxzfU}*7tOPMbgb--RYPEzMPDjjaHj3yo zEK6cE5*5?wbTTf+(^j(?W2J#giYt2k`TM#TtBY))0|ySI(W6JJg?{wtQMKq<&$xw` zpmkfdaRCHk*6Y~s0qjQ&U!f-rgD2fNyeeG@+u`-OQ|09)DJdhP7+gcApzNcI>VGx-V(eQl~s*LWia&KxR1eBd6kbg7`#aMY>lL=K-5^|;UU7D7p+%| zL{TDtbj| z!qP0uGMp)K49CcMC8B=)Vi{hF3CqV{+J2V$@ zhoA%RI7+;3OCqgel)Sn6@doa`N3Z%f47RynUwn#=xEIYl|Q z()*4D7thfkSM>s#>jngs2ox6&juHdV6g$L>g^+O}jxv9c$il!u98#O4R0Q68VOegt zx$Hlc^K$P_z9BO}D((k%*4@FzvOQR~hz1A8D3CG1Koab1GZvXl@xGJ_0n3(t(89t1 zEt(miIp%hDfgmOQfFij`=Cc1(E?0PW@C%y*GU*U-uH;YhR*PL2!+#yW(x*c}WGUhw4Pu9WxCZH9Yn24{_HA$vMB)7N zYj6)Y5XK;pIxm+HVJ6J4&%cR&{_*)&C=DF~TbBVvj01_|U~sh0LK^CV_}c<4k45~7 zwdVPElxUATN<-g(|Gz^&0{)<=ML?4iK|E6~p}0Y5{tmdGz} z**k`q_U-HQ|KGzO9K8e>Y9h$Qxgc>EfO{Yfq|(m7D&uPRFG;c*3F-gL);92eZ2o;a zgv2aE{8K;W zJp)MT1YCnr;AqzeSgt2<%2=c;c9?))B2~?hlHrK^KfLpQf&VC!z8%8i)&NJ(M*LHN zkmTVW7z7HZ{=h1to}*0ENx<(YbvrL1!hc0O{~P!X?}f*&1D-7gN+ztqB&4BHpz|07 ztUT-|l;-qEL(Vcf=((MJ(C^_NFO2n{ym+}!pH_*R5PvDqM4@R6G%*1@{U-p$c%PMX zY%`>tT!L+las$%-%X8P93+I=OO#tBD#=Xug{X4PKlq!^_^qDN{)`MkTQ(4x1m@z!> zc4*8tU}UAh5;z7)AvkM_!6#_E!21`VT-{Pi`dpV%tzVma^B4qqLWXjMSfcxxAoy7n zDNiR!c?Kbq_d=PjEzc>N{1xMpO5|-he#rC4gv}7%W;F=&9?@ne()u!B)ysj?tVH>9 z2?RxM0e7FiPZ^4cM49J(MiGy59Q;Q7!Wvjx`-6-Q!*xJejtT-tDbiYVl_0F1+YDUmUBJZe0q2hUAtvJh z$mJmiXi~;eY{+GbZ3`BS4bZfa7c_6`^BN7A)f7ieY?1TwBIC|x^%BN+{i8+byqCf>gqhp+}#Rz_Tda1(ZlWHRgGc$a&c@hc38 zSp-q-cYrYr`6H>_%CQV-8Uw3~2~1ymKVrl=;4+T`Iru#I&8>!rd0zwV64O<7J3ev4BJ=Fgf+#(Vfxh3&^0F)$I%*^H1IHup)Pg@vFihhWP82~-nw-d)>stQ zW+`|Zm*d%52qDp1AfVM;9D{Bf$Iz5%3_4?U(1b(x!{^-q-+A9a%;uj#-hR+tnWMCe zlWWTN1k(wkJC1_u$5+AW(o{HJ+#U8!>kO+Wb%&7?^MThTLSvC0tgQsv4@Vu$1Tlo>9~WZ zXh!IZCWw|amt1WKnT)c3Jo3swXo&P3o;V18yip0~R}O)b^LyYJvP?->hXXUZ!i<7` zV2qBN`7U_t)?wLzqSlEU5c>w8r6s_Uh?~SYrxyaJS_Yi=h8iB(=>TwjPD1?3+u*VI z8z8(wYjq})RL3y#=zkG(476-vL|%zJvzZ?(-TpEBcxoNadp4Xb>sdR7N*v4L(Vd}9 zO1DLIvHa0{RvlR!)p0BGm~D7oEJWFHv58hV=M}(ttOG818*uTvflWRLbe~h;JLej- z-ux5r-ShWIykf@_a=%hJ%bE5xo)ys&!f;QJ)dX3yNkb1H@%-9*^dNkHa3RuvPA&e^ zh=1j{E|8d-yy}l)HkW7h358Lad%)O!FEDz<>V@;{y9v0Ut-wX@1P*yS*XdGUBH(ow<@`ImOFPZ=fFQ3+b1h-I@4NIHT zI0%0H|`QJldpnP@ePRm@F6%2UUW&yt6O_a z+ktqQLrwM`f@%d~nXjPl3Uryfd9B~X%30rp^qU;^L z9;1!(2}S+qTs`~(Y)%|p_pgD&dRHLs*W|O!cYbWD((4<(Iwm$QXI50!389aZ(B08g-Zsb+H@=2rS<^2i{BRTx*{*zyI{7 z+qs($?Y`n35*lQF#_=jkaods0{EKeDdrQBAXgu#DkdJ6^4VXb^fg5oFIAPAme^rBh z);GWve+yj6Er{Cr0CZzE-k}`qoYY;%O^X(2PBI?tJYB)6CEBBqzH4llp-3m8K%m{t z+QthU9q7GiyE2^*W*p}H=#vK{b}h>X?}+eU$!Uk5J?Xk{=_nYJH|Rz)Ya1JLc}DAr z(s++td8?ke>ZtUQs;JbF9s`#ik6BO+>aiDr9sLqJ(mFSz3OJ-0y69WbFTD+sTYdyO zAYnd5)0T!_>#q5SuLB30K44=Nk8~3VwrGEqF+m`aI#m*+Y8*kRMo1;9QDR4SG}_Fa z%^2$D6}K*LSyeg)WKPaT^Yg<1_Gd30{ASCf6lmW&`?Ym3mow)wxwAvyv_rT2=2V;b z3s-OjxT#1tGpm7{a|0Mm;TwJi{rFuEX_5+}bVg%%$_~&tPe)sG3fe+iA%3)nBcBvY zG(V%R8(?m`zWh^w#ub$xU71rj?p~v2EyU)s$DTeJcwx(Am_IQKT!KQ9&Cl!0y+>|3 z5m5Rq(BqKiale}Im;Ndv;{Ud zB7USBndUm`ILi9^GQax_fIGT-;j_LY`X0e&roD^%!Sce53s)*ivtUYI@8@E<61`2o ze|eAET;X3Azd%2@cWE4p9(9q>+2?@L`ArpC(zBo2<;zee>IP#*=iEnQW3At&zBavc&G;_? zH{omG3NTIh3TWI9!LzP|XV#LZ6vZS8ZMf*u_IeqMOtfQd8I62Xum$=d-~VIykKMmB ze#^qKAa`}^BD8xDp!vD#b6;FqGaU9z=?o)A<=km*V`n-;g!lDbCj2ExKLYQ`LmI;K zip|CSJn#%eO}Y%4(CjLfBuw+_=AC`U)oT*k_dA(r|6TDPuR2%0uqYR-B!o)9cKY$X z!Y{T>fwN0;;G;>MVDQL3HygLGw5t!d`JInIKXY#aJqr2$=!+&k%Hmm|?0F1Y_d5!5 zr_fJ%mTAdQbVI#w->)^Uqrtjmd!+q{8ovJr=l=lGdBySVKNQRycPo4TBA4U$ekeV? zWfGi0+4Ky`)Y~R>f_|e0UTDy`NfR^1`f{Ig#K~wx}V#>?8VxW;g3JwQuy#_Sq@ar%P{FQ zr%<+DJ1Gsi4D4U)v#Afe@Xlw%#tQ%0$Ug_-8P*SFhaSg(N!$w_u^&NbEczu#d9F~7 zp;#MtL!a;D%6^DH0r8`JES$T4aQ+X!c%E|Y;3_y)lmf>Kli}3-8ht>}XZBC+3?*|$ zJ(qiU`3Zf#&E-98Lxulza@C`y6AVGH*+ClubuHL=A;}kis z;r}D^U-9Vv;wv8&L#3b(*U`m}7N)?4nc0w$JEH7uaGT5c3|Uv4?2cd5w;x zUIBsOo59Vi&l8%WVuZ3tKP|QTlq$JOz#ng-AJhIt+2L*R?*nLX;fLz8mp101F4@zh z&kH*J;aS~a`<&j8H*d~IRuY+|8Qa_BJ_A=*_~%~&x(mwAZFZaV7eQAKiP`|pu3he; zsl>FF=>M_Mx+hcD{vvyZ`^|WR`o`hYZ`DcT$j^a{+YO9y3()Kmpv1*!?^z8h=lI(!ZeXE4+F={0 zbsaCt)j43*JRWVyk%(QehYRO#m~eho zv{!T&ue4R)ho^3M65e*c8tV0BBT|ClUcXYmgsWo3E|;f3>hCSMfFZ)XkWS z@U_-%W)|My!p6XAp9ES3;7F?C``zoiA21RKgN(KA-^jUH52kGk*Kl9eUYeK zBx*S5hDf*?{Br1JKE+f%v>J1)A6kw53!}X8GXLgRxhS+;_>ZG1M|{7^xtjm^<~;P} zf3U@J`Jn}fL&&ega{h%yIZnMura=firZ7x@SDY3}IMWR4`aa9)(rDT#ou=iHxF>Kz z(eFX>Eo4$f0!^#a(Fc-FQ;Lqbw;Y6bf5k;nd{eci-F7G40Q3QMMt@Ws>VrNH8Jdd} z+LsZkOh<`7`W6DwRxBhxw52Pj{uO--ZE&2Mt==&BbCN*a$f`5gSjQoTFpMAKvW53D z(zV#e4)qPR69_Vky(7jJQO=u$cYnpj^X|4DzC$WWA{XNyx}$!UfH6toKvD93LigZEVM%#lXwmZ9V;lSJG6z$^UO_-5z9Q6w05-na2TZ zYjgj;FZ*}?%lemN|6cy1Dp`6G#u4?!7>X`H&~d`}`!fvFP%QPXwzL%F#lLU--T|X4 zIc7TcKNuYB(}3pVk;b?lm{_qi;ISpj8iJ4R@0owStxw?CN}el3|2f8yIQB*xbRzc8 zx0T9NZ5_q`@qhju{p0*w8G`ap%K1`|IilQ44M*Ln{S(xi+@z#SiJiUBzX4^IUyXm& zpP@UNQrDiP=-U<>LnmHP@(Y1*oDK@*WKe4pF4L%cN-5)cE2}_L-!=W$&>ulM?X25B z$NuN6ulxK>_Y|q}Rw&yp(YYnf^7PC4&=|Vv1&j87T3QNvpZTD43Rpl8@-7krbw%8B zW`B8&zc~Q+r{MuXYSu^zU6DjApTn~?cH1}gXMRqTOhZCOc@h%R)=8VZAtq@Lcn5p{ zyna2b}6(X+N;!fQt)h}c*`WDew6jJbjA?2wuv7~`p zyWE8VXsLAvDeH`t5{t~u~ygP zRtuXpuzJNZ_-5+})REKRY*{*Nnw1HWiRoZv=lOt;v2Q(YXWe^K0wY#o-vWK1PZE7& zv}!qcr5pvnp}QV%UZI(US4bm?+Su8}t=~Nv*%jAZ4-;pU!R_*5I8mH}zN2iYL_cuh zgx;X>^dJ8=XP8(p`CDT<1qAjX25yWvo}vnMxWLDId9XyCaA}>kd7aM%5xJi zE~|kvYy>U>eLbjWGrdoN--0{fId0<_KB502m*Cv%NDmn#K{SxcGz$g4urLQcAsKLN z|0cM$Z7iHE?GBr!^?+8LI;7UuKONS+p(cLFw2;1sL7Dg|(6Ju_m$bjeKhv)g?Q<8w zYuV4>JN4tsO!WKtfmt8l*61+)jr4njGIqMqHey)dfPA-cV;@kv$2}f9adcV5r9&Gw ztt)&;>s&nR>tEAh?DW78~aZ&zFd{wJwZ@c|{GuME? zrb2nrHOO9>SymD^|6A}HcNOSf6((CAH&XD6pl?C&=ghqYfmu%pl}C-^m{T{qd(wab42WAX8yNVPW9FM0V1 zrD7`b*C>o*5cag+LxOVKA(go=b99g|K>L5K{&@j)uVa^v9SsifoGkQp8K7lF#r7`; z=X5?}Vdq#Qr+P*-65a_H?c!taKe4=8J-G@P!M2ZfaeDR*@LO{a?Q26fa!OZ&f9&d~ zV!KS_tHCJK1PLXf|0Gv(orNcto<8n*WaSu$jB91W z^RifN=9)gGk?;6V&N@%~2K)aSShQ=)r&mKj(PiLW4XGMUWH%4PT;$_P*na@}_X2^U zHLGhIc;)6KJoQ^zHsO}s)m2me0vdmH<>>K`X7vO&U+-S86gBszn_3?{DsZ-EXxeJUjE?havrXv_B=>UwHAf z`=xCKuz5-N;p>|k zCwzL|Denp}XwPQSj)4LBj*DC3R^h$YEolkM_W{f1k*5Cb>**iurcF+r-FN821w;RR zbj9%Jr%Th|4E8@GKl`T4S;qW4l? zj050Z*Mo0B8SrxbD4~p#uFYba=~>f1`mqH1f7ARQdG_zv%b%3N>5>#US@b^aE6stv z!}~6_B3TpPG51+FHw;+Q)PLkBm(-~zfbzna1bQjjRA=M)<7_%RRGOGFn$5($FRXtc z_8%hjzol*)=KE8>{NLEmSB-=NC0X#n`k7aXw{7h29u`rrjorCjZex%1MV|#C4>W{r z0fYYf2hxmgAnLOj49)*3 zof=*=wt;7_CJ{cXlkSMDp-P0d5lqcQjqs4Qc-By4gEkHBt|%Xqh$|QInCu8Nw%|F} zRGudCRaS|{HNq5W%K0&ks`xP$FOv=Cp?CQ_(BNdqFbUt3{Z)1fns#yH6#)ed>p6|0 zoCcuXp(Wb9P)1PqBq`lAlGIFQ8ATv=`A(O!OXroGdbrOiB5rR)7jv$=;?CkwbF-}&f*Ak@5-gj-^%`H8+(T*da^8Na)C_G*Tif;xF7ej64KG|fg4q;`ZL zgA3nL{n_^Q-Z`szb~ftW1JQooPSCj{(cZAuvZX)rg}N|8}lUlzt8 zB|NTROm#HIWJj`$oT{%clAs8ggyh^j`y2?3-VDwdJ4g9!2CtZ%;N%=Q(NXNPuR%j2 z#$|cp`qsn~+hF{&gb2p(X0(;KH~2TTk37%C(>UgelP0Q+l1athkyAd?bguwS9>)FV zEIvob+=}(yDKD~8vmiXO3pBS3#Mmisj3)`iy`evg@ul*5+SYj)CtP#JxM*+V3Y?3m zB9h_lea3!x(698|&kC>jWfH2xFH1}2K=sCca2R#E-kCkX8u`AX*rk$`QS6)A<~gI* zCt%W5HO3NB4#mJLSAik@GjNX0nn(JE+p&iB<5jAR$4#1r!pg-9;QGcPaCSik9GaI2 ziAf#4mJ#fm+tx+9_y*6o>g>D*PEUTSHUpz$WMeza4I8ZB?J}lSrd$V z_6tdei~EW8*zucuk-D^?3yMD_3N9m`M26<69&2Y^x1yhxyLDt z<3b%H=OoJ7DCf_*3BF6NJy3P*GtJOt^S5H#?kHRNEzw2?1pX2E~eOif^yx9_@gCaMV!Pz zV)E0SLivBqmPIGko}SF{7td6CHqLzPYVb-B+OKgGQ(g$NhmEa2FpPRk-8#bkIrNo}9+hoc+o}h*ikIf6f&;Jo75y9Xb~N{yeBu&c zfIejOqv=u3*7&!dpw+}B^NJzJA46WY?>ynv_D|gXE;FltX3@6lYUeBaOYaqr==5__ zTZh_sAfdd^$o0()6OUcu$Kc%eKZ~*XyFn?}COBy#deKZa?*AZcKLGK+-u|JhXJ?ho zFF0|0XW^F@aW52%NWIX|(#E1L4xeFbn;Y^EU*ZO0>_NM7@D7=eHZWSRc5XG8Akz{5 zo7+Eo`RGSyQ4T&=l8k$~HzamVGTDmjwr?1`rnzBM`DLc-5#Zc6fv0yK>b6o+I9&W}<&URkCBeak{h|NRUd1hCbmO{ZgnNC@(q_JcKDq9a za1dNvH-fv@fFEf}+(NGO&!jozE!M43t_;RA%PGHZ-6JoaxmFz7@WtNp^&6HS*d1T@ zUEO=yJGOx`uv=$+Kvs#XzFR5Jd#4ERkrpXdZk$4MKBWYyo5s*`McsNwQ4rgE=i4{> zlcCDu2TbU%vM79+3->h%8+w818A?!(MUC#>6yM&HnD?tV?<0boFOhhDi8hvozt@1)wbLL{hTro!wt2xY z${0I)p9_MXDv@}gpW^rH>)g5ypsC5olR7@4Xl06xjqhEPzg^;C(qZfCA8pf(T->`1 z=kdD-p3kTzY3H$4Xm5R4{~XGQP0&~SoB9XZb{SWXjFs-jML#KgKEQ9`dVFYQ<90yQ z&==2AALNzBrxKaxX`GXYS21W@QfIij_c^ANd$)H>{jgd;{`3RRC1FR8uDzZO%S=UG zB?4NsFyQy4>OA~5u-4Wgw4>9d6gVjtfJ(InT;nDmmdAF9R7dsO=pcFT>7JD{;MRsL zm^CsTzpK@ceYIbgfc_J;o@;^8uEy_yJ_V1dUp!IA^e&~v^62I*Jim@l$c77B3Q+&* z39Z{F=*{)1;EqjPQc7lOyB`6j+hJgbUI3T5S0Cwn&)ufdk9#3@a6f@QjCK~yT`sqZ z?^v*I$Fh~YR>Li-Jk`*rb?YYbsGc2sN9_4pHT-j6#(xc}#lL{t&;=jpf+idh;)LuS z4L`~VO`&Z|8(Hq+B@L%7m_4=M>{*su4(=+ZR4N(m*S4AFy*~DC1r;~xX*WSvboH4_ zNVk!4e&8>H+=YBS&CW_<;(4c^KUMb~HE8&bGsoKH4^BR29)n@b)@JUL&Rl2mtHG_u z+H210&{UZ$4Jk-NxzyD+Wl*J|aU!PcezE{W4?X~qsZfxm3uA+)dKLf75 zL-#A>9!X+xXVbT`XhZ92E_dSlE5j-l^frA<5*6P%-Td4zbe*Ms(6Kv8|Gl8qblZx) zxLC1c7qoGsecg`eZZ3EF^Zg&5Tsq+4$kBZZT1d&-{iBFXZ|oA2JKN1`#K%g7Usr~9 zLJ>V;yHw`2gOJcJ=K6e<6R-XvF9j$W2}}h`@V+=LkM-Uhs#Ds_=ig YiNbjNhnHL=%16}s>3mJzOVs%P0VM`{F#rGn literal 0 HcmV?d00001 diff --git a/pe/qt/base/images/dgme_logo_128.png b/pe/qt/base/images/dgme_logo_128.png new file mode 100644 index 0000000000000000000000000000000000000000..187c21c8cfb4e7541a63ce36689f8cb444d17b68 GIT binary patch literal 18092 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 literal 0 HcmV?d00001 diff --git a/pe/qt/base/images/dgme_logo_32.png b/pe/qt/base/images/dgme_logo_32.png new file mode 100644 index 0000000000000000000000000000000000000000..88488462f4a694cc1156f7894a0ebf78aa235d88 GIT binary patch literal 6079 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 literal 0 HcmV?d00001 diff --git a/pe/qt/base/images/dgpe_logo_32.png b/pe/qt/base/images/dgpe_logo_32.png new file mode 100755 index 0000000000000000000000000000000000000000..5384a81c130023c54c59af9e3a10893998f0ad57 GIT binary patch literal 5818 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 NuboDV(okz!|9`pBT*UwY literal 0 HcmV?d00001 diff --git a/pe/qt/base/images/dgse_logo_128.png b/pe/qt/base/images/dgse_logo_128.png new file mode 100644 index 0000000000000000000000000000000000000000..f50221b216d9dcae2626bb246dab97898b7b4e75 GIT binary patch literal 14474 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 literal 0 HcmV?d00001 diff --git a/pe/qt/base/images/dgse_logo_32.png b/pe/qt/base/images/dgse_logo_32.png new file mode 100755 index 0000000000000000000000000000000000000000..29d53d2d12fdd9932032c2e60195edd76d7b5f63 GIT binary patch literal 1828 zcmV+<2iy3GP)c$e2xSyP&*6Mde7Eb{o!Olk-rYNg1oB}g zMT|VR>zUo1XP$ZHeV-x582B;^kA9?(Ffy_qX2FCSKxqwxfEBe+LIR7DxKCk&0mJdX z=+7`h!l-3FMJWZvVlOQ2UyNVCL606e3b);I3kWGe1BI)4$j#roy8G$IB+E8^1#eGH zzqj8D?nNN=0@06jbNc;;2&fl^;WXth7|VVR=E0=nIIwZkCV1r0EiDG3!Z-(xv-WX5 zYgTlw|7O2+!=cI))Tb@T6R7}s=hyZy4Ak_)b)o-9T50K;5mC=kX zMm0TS2A5mT2_VupLvHLeKu6v{X-NnJ-S$_d(!}P>OECHc1Bg1XhuZ+j4gw4EKCCJy zR)$JvDPvO|mId_x4FK6iQK6JobTvW5LJT5FvHGy)8hdGHnp_<)dZ;IrJS`r44c{U` zi#c*>0HRpXmDWqT(q^F8MV77TO|6!G5UY#e6NUz`yrjwMa%%k)5&vA8?|=-Dwu*JX z(duhP<24MIoln>o1Q6gHmiL6WZQIa0vg59S{Wo;Tb@kc|gcA380Ie7!8}O60U3VTA z?vcgitY5NHK#;Cv(=U}me@~C}J~Bc)tT8g0`2vW-?B7T2S6=+v$zR!lvb{c2pDLAKGSaT)oyXK0-x`Vv=+Sjk`g)D|1jZ9IcS)K7WOeQnPeeYo={NI|GyEy`g zmM)3Jy|K@GYRj(bkj(ahtlMyETw{f}HmU^b$0uR)w+nZ_Q=J~otxS)u9wR2^}Al>%$@y4DvaE_?%$U7~lV0 z@rPZf3U~JveG{d0!*Bjo)m)ka7GMoAD&(oaTZDZ!di3plZIF>dKu=Tl~}X6oW8r z5b~T%Tr_AzAVxhoyrVK{b0F96^W>d37jm0_ykOyzN8cKQS|NbG<;Cu`%errU{oTpo z6}ik7A9d2^;q-Da$Adx#3{GO2!M=^yEHBEyY{66vqf$L_K!iV7qSZa?dO9DU6hu3s z3&!5#@b>E`VYz4Dq&XX#qI9oqTX3dBiakKm#)wKG7Y6rguesF_!eKsPljBDi!ge^{ z{rpE0*Ge$<50@6VmBZBPYSlrFH=vlZ*G|kdU+7QpHE1H*$-axb}dO9nvNX*>cTFv$6_N!5{?%Z7yh*HC(Sj%Y3jA`>Gc zE8k`z@{HJ=z?6I>f!OW7ED$Rutv?^A3`FID>1hT0^0c2NwYZ4BHs?#XG_n`@GpO0^qtXNGVUBSS~tytnbV?zi+1PRSq7W zTs*AQ9QR#ehljCCj08QrYa-F$Gcz-=`^{arhU%#l17_%h2U0t^5%t1P6U Sm55~k00003552EP)Nkl$NWTs#~O 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 VY%2RVTYUfk002ovPDHLkV1oUj+W`Oo literal 0 HcmV?d00001 diff --git a/pe/qt/base/images/gear.png b/pe/qt/base/images/gear.png new file mode 100755 index 0000000000000000000000000000000000000000..41ff2dd6d78b9827ab401185d625f35184687699 GIT binary patch literal 394 zcmeAS@N?(olHy`uVBq!ia0vp^JRr=$1|-8uW1a&kwj^(N7l!{JxM1({$v_d#0*}aI z1_o|n5N2eUHAey{$X?><>&kwWhn-hLtURzvhk=1n+|$J|MB{w!#e-f~3I*6doPYCA zR5WthRol+0h;3G_;z!LtNbZuns_b!IL1bS;(uI}T0&liP-(8ZtU_&XVbw`}jtz0XM zq!#AGH9u?X57(scyW_h1l~P=1MeL7wztvg%{u?`4bxy}BwrGp#p)m@*T*{pZ$N2B8h+e&7oveb317H3V|%s+_Ln#?%p zU9;-$wvF+^YCmo!?As)JX4;fI(~`WMlkDyvOYc$2;i%;)=nLJWP$hV7LpwjO-bahe z28VRy9xl+cQ2d`#)xlnIf0cGlqGfRNdew!Y70YaH>?{2+eRlQvhZ65UJUPa8zOYGK m=FuVBjZOViwf|49XOukW9JuC4h#N4}7(8A5T-G@yGywqT51()V literal 0 HcmV?d00001 diff --git a/pe/qt/base/images/power_marker32.png b/pe/qt/base/images/power_marker32.png new file mode 100755 index 0000000000000000000000000000000000000000..48129026bc3e9d827169bb5e8d4c7160690c5444 GIT binary patch literal 2132 zcmV-a2&?yrP)Oc*tiKUB_?Sbf!w6DHHj1|DpjEhRip|PB~n$T zl~P4XTA`*&(@;u7fCi_$48)jJEx0(?u`%`rWBghcjJ#}q)?ecHg|Xa z%zSs|+;hKkzVA|oVc@Hb-riocw6uWddDPU@U}R(jBg3OuS-B!>-D3?xR%Vv^|K_s` z-86Iqkw5@KAV6DPj)xeA;b0=AZa<%o(}pX3GPitrFp)^?TD*9170a^VI1ZjXFG7(Z zP7LhIn+{Dqwe2S__`Wm%O_tF6?rvP#yJyK0+kf1xXtYg^MhuFgAYSw1`oCJ}(Z`>D zX6K&w4_G;C&F0OU>uyaXe3Qtk-Y?XA=h@i37+eqlMSccFBh#B(Js+935A_d2 z4u>gCmN85KTnLIcx7Dxz$Rwn1piAU48DQQC)gs`0_-&kg?*fXnwL_aXH~CWnkffPh z?kxA$LxFKj`+U&iafCDt-SIGVuXo?7va&fd?nbO1ANX3ZzIZ$O!Uxg%eh)URcoCPG zep*)Kd*%>*x6jioO{HaK)Wn213`K!K)0i`K%S@aGT5PuRSU3WMtXw8HU95o&8}|=yBy9AZ`)8#YXScfS zD=eGPdq~E*2ezW9qyVva)O-zpKL8fq0-^#V5v$O3q>0g3QUDCuaF3NWa$W9HZ+x_; zYxwH(j}a9Pja)hV$-WDMu3C-WL=R54oq`|;lshebfVcChGr-7+I$0(wk46a|11wR1 zg|btONYyom40&nonvcxb^78Um`h0y&IN_T0H2re)jyJUtt*Q^Fi<3{Ci?F)J##Na&q*XCGZwTr8NeRn^r8JI6*=eyP zw?>!~-1Teg-I64kCIHbgi3@5w&L0~Zf8wWQH&S9i=ekh(J_;u@cVKe#;msTps&WWisM$>t#sT185z46Wxcyc(`EW#FHDoJ(W@pTUXi*eXQ$g+4=bO2NacFeUUt!Nouh$q0W}*><4GsB><&cABNTM3aWX-25FslnV zG4?8MwB}&Tw_buPlgIs-&|Mkucsxc%niC??zbh-fIx-hUM4Si1pudo_b*j*LD!BoG zD2vWQX&wAWa!~)!_n{ghg25mZNlv3u^#M}_8u>VJL&o7ws|fgKkj`kxrY$G4t;(wd zig=&__|%M@3{L#vjW_m}Ivp;JPX}86OS<#oq755xT>nEj@H&zpPlE8ij^xdPhtlyUq#j;OVQBipu4i7N=OX= zaYKj8Q4T_aP8gwKvCJkCOS>X?y|I`R_1kgFmoMK#jOP)R)C+}NKYDH}24~J;SMx7$ z`0(Lc0-!5}Vnp27^x+IwRU$Hvc_D%u2^}4A8Fq?0=AfxtNk~`o6!$a%p*AJw;1!l(Cp;m`3_aTxH5Yjb7b$z&;<-3eq3QVTq&}6qEUl>QJ zwH{$-A3p4Q9W(9uSX8GK8AyJF?Ra61J8pk@{##~1Mwluy7E6)-oX;jMP*qj+r^~r?DZ*0iRpsY@=C_Xti z;br-bI~q4qAt$GA+8j~|rrQ@|P1R~#8S*88MI%u=P1C6XFbU4h$eLX3bP*9ykI1UT zhz3sQvs~2lh%XLBm0#HL^Iv7=Wak>0ya(MqJ@EDSQyVul3TCUGx(^T)kVXtH+VBWs z92~_Ragi0txbqxQQ(Nn4YHISBrTEb`Ymk|l3Et{J_7Zrzx~x)>XPE(U+(?e-iOUQs@W4TEc)k8;HB5Lq3!g?$G;)~3LNAICnWuB zKEs8y1!AsTxe_ZbF2&5u3@C%agtZ-iIr19(Z8qO;b~jy0-3OA5DFs1Oy zk`Pq%hfcz0*^NVO?;v+-J$}%*1v;%!GO^|q88DKqxUNW9gOW73L}y3UWcnG<$?3?D zm1M0jbD%KoYjh6F=7`_#FA>55Z?oD+yqsjkWXGB26v?T1^j-k|E5HDx{l9{o3w;Ct0000< KMNUMnLSTa2=m_or literal 0 HcmV?d00001 diff --git a/pe/qt/base/images/preferences32.png b/pe/qt/base/images/preferences32.png new file mode 100755 index 0000000000000000000000000000000000000000..bdbc4051bed43e22ea6057ac4844866396219458 GIT binary patch literal 2899 zcmWlbdpy(oAIHDD4_~Yl8$hqQgSWp0I3ayzVZ>Qtvr z*5#Db#Wtf0GC!#lHJeLH<<4Z=_qYD|eBO`GAD_?X@p^xrulMJj9~u&1Y-ni+0Dy5& zpnsS?Qhp9RRzDxj%9_##bdqndF96g%r(+5}>(`|8jp6A#QWMjU#~eujeB)DN6DUC` zF-Zww2{G}RhhHRk1AyURkiYNF-`>Y+V3;9eA|W6a9UDg(La25yHNS5jI)b?ENH7A% z4^Vsr!T(dra!CLZ@1I1Yxju=HP4q#=Z^NkUC~Hx%F*N5yj4GDmgKnyW)J6mbN_C9+ zHUCoig$oD@#Iz;6Y?rz!+NJr^&*TcQZm;+Jr0zq{Y^@Lbw~78|#Rr&kTSBv~^J-D! zF66Qs$niy#44Ex#mS=%Fyow-(43KDo5kah-omCt^dLY0&nKaF)i(apt|Ne62_hK8x z^#;yt#Nc3I(q4Iu&kx;n>%~0^G3UgXw&y`4tkXUMyN;vsM)^~y+YW?OMC_V!sh^)8 z$UFwx_+&!{w^4@;2}gm$s|kF2y}kj6uHU0w>gd>-l-%E>nUzOBrKj%Ic^%XAiQTT6 zxZ?2zce=gisMVgCfuH{;^V&})R~Mb$+Qwi!J~8?EThH56b~enOme-0cK@gUfSrLg; zJ3BjSUST2MdaO^15`2Dj)y+F|0zFi+kK$v{GzqD#m;jeNW_fLFq)x#ho!VGmQAGA= z?CROBh1*`pB*SxZ;gthZjODJy0W(NV-BYtW>s(;+~lZ7-EhaTQ|i$Q&ok#eAad;tuzMy_~hn^ zj6(w}Z|#E-_b$#U#NSwTS1w9wg!AI%=Hl8mAuU5SfF1)AwejZg z;w2vwyKf(}mdR@w#I*;zn?%P(%tfg0y$$Wulvtxz{Qm=GEk77Hwo60}});k$AVIvKa<00jDogh60Ws|;k%W>ZP!L=vfit!?QW z9Bdv_kH-nB=&7{qA5;0#r!Cde&dyTb9a{=xbecKVj=fK^P1>YeS@(rP3TnlQ6&}MJ zSZC}QPn2?>LRUod#=)l#{yZKCoMCw3y!cgn0GTTD6{*p01L?ijds0M1BtL^>KC+&L*pQY+ zr5;}6HZ?QDGv2-XHPZ5C>zL+UoZ!Hoc5XBCT}pGoVopbMI6v?+7>7uyA)y?l=g~OiU0B#1@WHmxc#nVC zw2rW~c?ryC-0VkxEQexJj@rq3t{x}qbh_rTY3<4ZXZ7MrBMDYt8QTzIFXwhGTef3Q&U#?0t{GzCJO6DEW;mCiM%9U390wTlWI zFj(z2fr8P1n(XHYTx=-YCE!g9Xeb}Jk&Z1#>8th-v`y%d`^$uKy&alqKo|f@Mq~a% z#^W6p916dir_?=(6sm~MCQc;o1#?+Nu$#mn^j(*A6j_7H`a0qjh=_Ns z-s3g`1g6M+%piQLPNUNr)#Ec~zprsOPUD%GKR+V$zDL4rPWuW<)}myZUrvI1Txv0e z9C!v3RRl@qzD-JPfxlO<5id2m&(>WCa^Dd_Ma3^9b6Y(CMb*5 z*pZvj%9384*;NGf*!%7Q6EMcK}43*}Jd!4t5a_nawfNOuKCo#iELk%%&eUyU|g%~FK zuz#R~V5WkvN3c zAvP-60BN*5bd1;S%`D9EZ<4;7Qq0OhPD2BZ{z9$BK%Ptimw(QxyWKr8bZ6LYjJ=V* zLGp8|oGR1+XGDru=L5?tHXpxUBRnZclnPQUg8|BnrE3APG$8z~IZp3dpf6 zCuc!-=h+1H)cCRv+sEK2gB_!u8pL$~p4iY$B^P$Y6VU5URpE9n0ebRC8=19tktd%U zZH1!1%})CzODoR!ZuMJqO>jn8d=HJXxv>D{MRmWhb0^8)e~wrW1+_MI7Q}rk$jJZ@ zO}z8ij}0g0EyGtnLH%GFhR=vmPg^;HEf-yokv?+npcwG*_hDzt(!Mn96?i>Zjoi>j z_ZTrSFrY 0: + msg = "Are you sure you want to start a new duplicate scan?" + if not self._confirm(title, msg): + return + try: + self.app.start_scanning() + except NoScannableFileError: + msg = "The selected directories contain no scannable file." + QMessageBox.warning(self, title, msg) + self.app.show_directories() + except AllFilesAreRefError: + msg = "You cannot make a duplicate scan with only reference directories." + QMessageBox.warning(self, title, msg) + + def showHelpTriggered(self): + self.app.show_help() + + #--- Events + def application_will_terminate(self): + self._save_columns() + + def columnToggled(self, action): + colid = action.column_index + if colid == -1: + self.app.prefs.reset_columns() + self._load_columns() + else: + h = self.resultsView.header() + h.setSectionHidden(colid, not h.isSectionHidden(colid)) + self._update_column_actions_status() + + def dupeMarkingChanged(self): + self._redraw_results() + self._update_status_line() + + def resultsChanged(self): + self.resultsView.model().reset() + + def resultsReset(self): + self.resultsView.expandAll() + self._update_status_line() + + def selectionChanged(self, selected, deselected): + index = self.resultsView.selectionModel().currentIndex() + dupe = index.internalPointer().dupe if index.isValid() else None + self.app.select_duplicate(dupe) + diff --git a/pe/qt/base/main_window.ui b/pe/qt/base/main_window.ui new file mode 100644 index 00000000..754f265c --- /dev/null +++ b/pe/qt/base/main_window.ui @@ -0,0 +1,911 @@ + + + MainWindow + + + + 0 + 0 + 630 + 514 + + + + dupeGuru + + + + + 0 + + + + + QAbstractItemView::ExtendedSelection + + + QAbstractItemView::SelectRows + + + false + + + true + + + false + + + true + + + false + + + false + + + + + + + + + 0 + 0 + 630 + 22 + + + + + Columns + + + + + Actions + + + + + + + + + + + + + + + + + + + + Mark + + + + + + + + + Modes + + + + + + + Windows + + + + + + + + Help + + + + + + + + + File + + + + + + + + + + + + + + + + + + toolBar + + + false + + + Qt::ToolButtonTextUnderIcon + + + false + + + TopToolBarArea + + + false + + + + + + + + + + + + true + + + + + + :/logo_pe:/logo_pe + + + Start Scan + + + Start scanning for duplicates + + + Ctrl+S + + + + + + :/folder:/folder + + + Directories + + + Ctrl+4 + + + + + + :/details:/details + + + Details + + + Ctrl+3 + + + + + + :/actions:/actions + + + Actions + + + + + + :/preferences:/preferences + + + Preferences + + + Ctrl+5 + + + + + true + + + + :/delta:/delta + + + Delta Values + + + Ctrl+2 + + + + + true + + + + :/power_marker:/power_marker + + + Power Marker + + + Ctrl+1 + + + + + Send Marked to Recycle Bin + + + Ctrl+D + + + + + Move Marked to... + + + Ctrl+M + + + + + Copy Marked to... + + + Ctrl+Shift+M + + + + + Remove Marked from Results + + + Ctrl+R + + + + + Remove Selected from Results + + + Ctrl+Del + + + + + Add Selected to Ignore List + + + Ctrl+Shift+Del + + + + + Make Selected Reference + + + Ctrl+Space + + + + + Open Selected with Default Application + + + Ctrl+O + + + + + Open Containing Folder of Selected + + + Ctrl+Shift+O + + + + + Rename Selected + + + F2 + + + + + Mark All + + + Ctrl+A + + + + + Mark None + + + Ctrl+Shift+A + + + + + Invert Marking + + + Ctrl+Alt+A + + + + + Mark Selected + + + + + Clear Ignore List + + + + + Quit + + + Ctrl+Q + + + + + Apply Filter + + + Ctrl+F + + + + + Cancel Filter + + + Ctrl+Shift+F + + + + + dupeGuru Help + + + F1 + + + + + About dupeGuru + + + + + Register dupeGuru + + + + + Check for Update + + + + + + ResultsView + QTreeView +

results_model
+ + + + + + + + actionDirectories + triggered() + MainWindow + directoriesTriggered() + + + -1 + -1 + + + 314 + 256 + + + + + actionActions + triggered() + MainWindow + actionsTriggered() + + + -1 + -1 + + + 314 + 256 + + + + + actionCopyMarked + triggered() + MainWindow + copyTriggered() + + + -1 + -1 + + + 314 + 256 + + + + + actionDeleteMarked + triggered() + MainWindow + deleteTriggered() + + + -1 + -1 + + + 314 + 256 + + + + + actionDelta + triggered() + MainWindow + deltaTriggered() + + + -1 + -1 + + + 314 + 256 + + + + + actionDetails + triggered() + MainWindow + detailsTriggered() + + + -1 + -1 + + + 314 + 256 + + + + + actionIgnoreSelected + triggered() + MainWindow + addToIgnoreListTriggered() + + + -1 + -1 + + + 314 + 256 + + + + + actionMakeSelectedReference + triggered() + MainWindow + makeReferenceTriggered() + + + -1 + -1 + + + 314 + 256 + + + + + actionMoveMarked + triggered() + MainWindow + moveTriggered() + + + -1 + -1 + + + 314 + 256 + + + + + actionOpenSelected + triggered() + MainWindow + openTriggered() + + + -1 + -1 + + + 314 + 256 + + + + + actionPowerMarker + triggered() + MainWindow + powerMarkerTriggered() + + + -1 + -1 + + + 314 + 256 + + + + + actionPreferences + triggered() + MainWindow + preferencesTriggered() + + + -1 + -1 + + + 314 + 256 + + + + + actionRemoveMarked + triggered() + MainWindow + removeMarkedTriggered() + + + -1 + -1 + + + 314 + 256 + + + + + actionRemoveSelected + triggered() + MainWindow + removeSelectedTriggered() + + + -1 + -1 + + + 314 + 256 + + + + + actionRevealSelected + triggered() + MainWindow + revealTriggered() + + + -1 + -1 + + + 314 + 256 + + + + + actionRenameSelected + triggered() + MainWindow + renameTriggered() + + + -1 + -1 + + + 314 + 256 + + + + + actionScan + triggered() + MainWindow + scanTriggered() + + + -1 + -1 + + + 314 + 256 + + + + + actionClearIgnoreList + triggered() + MainWindow + clearIgnoreListTriggered() + + + -1 + -1 + + + 314 + 256 + + + + + actionMarkAll + triggered() + MainWindow + markAllTriggered() + + + -1 + -1 + + + 314 + 256 + + + + + actionMarkNone + triggered() + MainWindow + markNoneTriggered() + + + -1 + -1 + + + 314 + 256 + + + + + actionMarkSelected + triggered() + MainWindow + markSelectedTriggered() + + + -1 + -1 + + + 314 + 256 + + + + + actionInvertMarking + triggered() + MainWindow + markInvertTriggered() + + + -1 + -1 + + + 314 + 256 + + + + + actionApplyFilter + triggered() + MainWindow + applyFilterTriggered() + + + -1 + -1 + + + 314 + 256 + + + + + actionCancelFilter + triggered() + MainWindow + cancelFilterTriggered() + + + -1 + -1 + + + 314 + 256 + + + + + actionShowHelp + triggered() + MainWindow + showHelpTriggered() + + + -1 + -1 + + + 314 + 256 + + + + + actionAbout + triggered() + MainWindow + aboutTriggered() + + + -1 + -1 + + + 314 + 256 + + + + + actionRegister + triggered() + MainWindow + registerTrigerred() + + + -1 + -1 + + + 314 + 256 + + + + + actionCheckForUpdate + triggered() + MainWindow + checkForUpdateTriggered() + + + -1 + -1 + + + 314 + 256 + + + + + + directoriesTriggered() + scanTriggered() + actionsTriggered() + detailsTriggered() + preferencesTriggered() + deltaTriggered() + powerMarkerTriggered() + deleteTriggered() + moveTriggered() + copyTriggered() + removeMarkedTriggered() + removeSelectedTriggered() + addToIgnoreListTriggered() + makeReferenceTriggered() + openTriggered() + revealTriggered() + renameTriggered() + clearIgnoreListTriggered() + clearPictureCacheTriggered() + markAllTriggered() + markNoneTriggered() + markInvertTriggered() + markSelectedTriggered() + applyFilterTriggered() + cancelFilterTriggered() + showHelpTriggered() + aboutTriggered() + registerTrigerred() + checkForUpdateTriggered() + + diff --git a/pe/qt/base/preferences.py b/pe/qt/base/preferences.py new file mode 100644 index 00000000..64ad64d5 --- /dev/null +++ b/pe/qt/base/preferences.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python +# Unit Name: preferences +# Created By: Virgil Dupras +# Created On: 2009-05-03 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +from PyQt4.QtCore import QSettings, QVariant + +from hsutil.misc import tryint + +def variant_to_py(v): + value = None + ok = False + t = v.type() + if t == QVariant.String: + value = unicode(v.toString()) + ok = True # anyway + # might be bool or int, try them + if v == 'true': + value = True + elif value == 'false': + value = False + else: + value = tryint(value, value) + elif t == QVariant.Int: + value, ok = v.toInt() + elif t == QVariant.Bool: + value, ok = v.toBool(), True + elif t in (QVariant.List, QVariant.StringList): + value, ok = map(variant_to_py, v.toList()), True + if not ok: + raise TypeError() + return value + +class Preferences(object): + # (width, is_visible) + COLUMNS_DEFAULT_ATTRS = [] + + def __init__(self): + self.reset() + self.reset_columns() + + def _load_specific(self, settings, get): + # load prefs specific to the dg edition + pass + + def load(self): + self.reset() + settings = QSettings() + def get(name, default): + if settings.contains(name): + return variant_to_py(settings.value(name)) + else: + return default + + self.filter_hardness = get('FilterHardness', self.filter_hardness) + self.mix_file_kind = get('MixFileKind', self.mix_file_kind) + self.use_regexp = get('UseRegexp', self.use_regexp) + self.remove_empty_folders = get('RemoveEmptyFolders', self.remove_empty_folders) + self.destination_type = get('DestinationType', self.destination_type) + widths = get('ColumnsWidth', self.columns_width) + # only set nonzero values + for index, width in enumerate(widths[:len(self.columns_width)]): + if width > 0: + self.columns_width[index] = width + self.columns_visible = get('ColumnsVisible', self.columns_visible) + self.registration_code = get('RegistrationCode', self.registration_code) + self.registration_email = get('RegistrationEmail', self.registration_email) + self._load_specific(settings, get) + + def _reset_specific(self): + # reset prefs specific to the dg edition + pass + + def reset(self): + self.filter_hardness = 95 + self.mix_file_kind = True + self.use_regexp = False + self.remove_empty_folders = False + self.destination_type = 1 + self.registration_code = '' + self.registration_email = '' + self._reset_specific() + + def reset_columns(self): + self.columns_width = [width for width, _ in self.COLUMNS_DEFAULT_ATTRS] + self.columns_visible = [visible for _, visible in self.COLUMNS_DEFAULT_ATTRS] + + def _save_specific(self, settings, set_): + # save prefs specific to the dg edition + pass + + def save(self): + settings = QSettings() + def set_(name, value): + settings.setValue(name, QVariant(value)) + + set_('FilterHardness', self.filter_hardness) + set_('MixFileKind', self.mix_file_kind) + set_('UseRegexp', self.use_regexp) + set_('RemoveEmptyFolders', self.remove_empty_folders) + set_('DestinationType', self.destination_type) + set_('ColumnsWidth', self.columns_width) + set_('ColumnsVisible', self.columns_visible) + set_('RegistrationCode', self.registration_code) + set_('RegistrationEmail', self.registration_email) + self._save_specific(settings, set_) + diff --git a/pe/qt/base/reg.py b/pe/qt/base/reg.py new file mode 100644 index 00000000..59fd0bc3 --- /dev/null +++ b/pe/qt/base/reg.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python +# Unit Name: reg +# Created By: Virgil Dupras +# Created On: 2009-05-09 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +from hashlib import md5 + +from PyQt4.QtGui import QDialog + +from reg_submit_dialog import RegSubmitDialog +from reg_demo_dialog import RegDemoDialog + +class Registration(object): + def __init__(self, app): + self.app = app + + def ask_for_code(self): + dialog = RegSubmitDialog(self.app.main_window, self.app.is_code_valid) + result = dialog.exec_() + code = unicode(dialog.codeEdit.text()) + email = unicode(dialog.emailEdit.text()) + dialog.setParent(None) # free it + if result == QDialog.Accepted and self.app.is_code_valid(code, email): + self.app.set_registration(code, email) + return True + return False + + def show_nag(self): + dialog = RegDemoDialog(self.app.main_window, self) + dialog.exec_() + dialog.setParent(None) # free it + diff --git a/pe/qt/base/reg_demo_dialog.py b/pe/qt/base/reg_demo_dialog.py new file mode 100644 index 00000000..95280314 --- /dev/null +++ b/pe/qt/base/reg_demo_dialog.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python +# Unit Name: reg_demo_dialog +# Created By: Virgil Dupras +# Created On: 2009-05-10 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +from PyQt4.QtCore import SIGNAL, Qt, QUrl, QCoreApplication +from PyQt4.QtGui import QDialog, QMessageBox, QDesktopServices + +from reg_demo_dialog_ui import Ui_RegDemoDialog + +class RegDemoDialog(QDialog, Ui_RegDemoDialog): + def __init__(self, parent, reg): + flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint + QDialog.__init__(self, parent, flags) + self.reg = reg + self._setupUi() + + self.connect(self.enterCodeButton, SIGNAL('clicked()'), self.enterCodeClicked) + self.connect(self.purchaseButton, SIGNAL('clicked()'), self.purchaseClicked) + + def _setupUi(self): + self.setupUi(self) + # Stuff that can't be setup in the Designer + appname = QCoreApplication.instance().applicationName() + title = self.windowTitle() + title = title.replace('$appname', appname) + self.setWindowTitle(title) + title = self.titleLabel.text() + title = title.replace('$appname', appname) + self.titleLabel.setText(title) + desc = self.demoDescLabel.text() + desc = desc.replace('$appname', appname) + self.demoDescLabel.setText(desc) + + #--- Events + def enterCodeClicked(self): + if self.reg.ask_for_code(): + self.accept() + + def purchaseClicked(self): + url = QUrl('http://www.hardcoded.net/purchase.htm') + QDesktopServices.openUrl(url) + diff --git a/pe/qt/base/reg_demo_dialog.ui b/pe/qt/base/reg_demo_dialog.ui new file mode 100644 index 00000000..ef918225 --- /dev/null +++ b/pe/qt/base/reg_demo_dialog.ui @@ -0,0 +1,140 @@ + + + RegDemoDialog + + + + 0 + 0 + 387 + 161 + + + + $appname Demo Version + + + + + + + 75 + true + + + + $appname Demo Version + + + + + + + You are currently running a demo version of $appname. This version has limited functionalities, and you need to buy it to have access to these functionalities. + + + true + + + + + + + In the demo version, only 10 duplicates per session can be sent to the recycle bin, moved or copied. + + + true + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 110 + 0 + + + + Try Demo + + + + + + + + 110 + 0 + + + + Enter Code + + + + + + + + 110 + 0 + + + + Purchase + + + + + + + + + + + tryButton + clicked() + RegDemoDialog + accept() + + + 112 + 161 + + + 201 + 94 + + + + + diff --git a/pe/qt/base/reg_submit_dialog.py b/pe/qt/base/reg_submit_dialog.py new file mode 100644 index 00000000..4ba680b6 --- /dev/null +++ b/pe/qt/base/reg_submit_dialog.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python +# Unit Name: reg_submit_dialog +# Created By: Virgil Dupras +# Created On: 2009-05-09 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +from PyQt4.QtCore import SIGNAL, Qt, QUrl, QCoreApplication +from PyQt4.QtGui import QDialog, QMessageBox, QDesktopServices + +from reg_submit_dialog_ui import Ui_RegSubmitDialog + +class RegSubmitDialog(QDialog, Ui_RegSubmitDialog): + def __init__(self, parent, is_valid_func): + flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint + QDialog.__init__(self, parent, flags) + self._setupUi() + self.is_valid_func = is_valid_func + + self.connect(self.submitButton, SIGNAL('clicked()'), self.submitClicked) + self.connect(self.purchaseButton, SIGNAL('clicked()'), self.purchaseClicked) + + def _setupUi(self): + self.setupUi(self) + # Stuff that can't be setup in the Designer + appname = QCoreApplication.instance().applicationName() + prompt = self.promptLabel.text() + prompt = prompt.replace('$appname', appname) + self.promptLabel.setText(prompt) + + #--- Events + def purchaseClicked(self): + url = QUrl('http://www.hardcoded.net/purchase.htm') + QDesktopServices.openUrl(url) + + def submitClicked(self): + code = unicode(self.codeEdit.text()) + email = unicode(self.emailEdit.text()) + title = "Registration" + if self.is_valid_func(code, email): + msg = "This code is valid. Thanks!" + QMessageBox.information(self, title, msg) + self.accept() + else: + msg = "This code is invalid" + QMessageBox.warning(self, title, msg) + diff --git a/pe/qt/base/reg_submit_dialog.ui b/pe/qt/base/reg_submit_dialog.ui new file mode 100644 index 00000000..06de4191 --- /dev/null +++ b/pe/qt/base/reg_submit_dialog.ui @@ -0,0 +1,149 @@ + + + RegSubmitDialog + + + + 0 + 0 + 365 + 134 + + + + Enter your registration code + + + + + + Please enter your $appname registration code and registered e-mail (the e-mail you used for the purchase), then press "Submit". + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + + + + + QLayout::SetNoConstraint + + + QFormLayout::ExpandingFieldsGrow + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + + + Registration code: + + + + + + + Registered e-mail: + + + + + + + + + + + + + + + + + Purchase + + + false + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + Cancel + + + false + + + + + + + + 0 + 0 + + + + Submit + + + false + + + true + + + + + + + + + + + cancelButton + clicked() + RegSubmitDialog + reject() + + + 260 + 159 + + + 198 + 97 + + + + + diff --git a/pe/qt/base/results_model.py b/pe/qt/base/results_model.py new file mode 100644 index 00000000..d28d6da3 --- /dev/null +++ b/pe/qt/base/results_model.py @@ -0,0 +1,175 @@ +#!/usr/bin/env python +# Unit Name: +# Created By: Virgil Dupras +# Created On: 2009-04-23 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +from PyQt4.QtCore import SIGNAL, Qt, QAbstractItemModel, QVariant, QModelIndex, QRect +from PyQt4.QtGui import QBrush, QStyledItemDelegate, QFont, QTreeView, QColor + +from tree_model import TreeNode, TreeModel + +class ResultNode(TreeNode): + def __init__(self, model, parent, row, dupe, group): + TreeNode.__init__(self, parent, row) + self.model = model + self.dupe = dupe + self.group = group + self._normalData = None + self._deltaData = None + + def _get_children(self): + children = [] + if self.dupe is self.group.ref: + for index, dupe in enumerate(self.group.dupes): + children.append(ResultNode(self.model, self, index, dupe, self.group)) + return children + + def reset(self): + self._normalData = None + self._deltaData = None + + @property + def normalData(self): + if self._normalData is None: + self._normalData = self.model._data.GetDisplayInfo(self.dupe, self.group, delta=False) + return self._normalData + + @property + def deltaData(self): + if self._deltaData is None: + self._deltaData = self.model._data.GetDisplayInfo(self.dupe, self.group, delta=True) + return self._deltaData + + +class ResultsDelegate(QStyledItemDelegate): + def initStyleOption(self, option, index): + QStyledItemDelegate.initStyleOption(self, option, index) + node = index.internalPointer() + if node.group.ref is node.dupe: + newfont = QFont(option.font) + newfont.setBold(True) + option.font = newfont + + +class ResultsModel(TreeModel): + def __init__(self, app): + self._app = app + self._results = app.results + self._data = app.data + self._delta_columns = app.DELTA_COLUMNS + self.delta = False + self._power_marker = False + TreeModel.__init__(self) + + def _root_nodes(self): + nodes = [] + if self.power_marker: + for index, dupe in enumerate(self._results.dupes): + group = self._results.get_group_of_duplicate(dupe) + nodes.append(ResultNode(self, None, index, dupe, group)) + else: + for index, group in enumerate(self._results.groups): + nodes.append(ResultNode(self, None, index, group.ref, group)) + return nodes + + def columnCount(self, parent): + return len(self._data.COLUMNS) + + def data(self, index, role): + if not index.isValid(): + return QVariant() + node = index.internalPointer() + if role == Qt.DisplayRole: + data = node.deltaData if self.delta else node.normalData + return QVariant(data[index.column()]) + elif role == Qt.CheckStateRole: + if index.column() == 0 and node.dupe is not node.group.ref: + state = Qt.Checked if self._results.is_marked(node.dupe) else Qt.Unchecked + return QVariant(state) + elif role == Qt.ForegroundRole: + if node.dupe is node.group.ref or node.dupe.is_ref: + return QVariant(QBrush(Qt.blue)) + elif self.delta and index.column() in self._delta_columns: + return QVariant(QBrush(QColor(255, 142, 40))) # orange + elif role == Qt.EditRole: + if index.column() == 0: + return QVariant(node.normalData[index.column()]) + return QVariant() + + def dupesForIndexes(self, indexes): + nodes = [index.internalPointer() for index in indexes] + return [node.dupe for node in nodes] + + def flags(self, index): + if not index.isValid(): + return Qt.ItemIsEnabled + flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable + if index.column() == 0: + flags |= Qt.ItemIsUserCheckable | Qt.ItemIsEditable + return flags + + def headerData(self, section, orientation, role): + if orientation == Qt.Horizontal and role == Qt.DisplayRole and section < len(self._data.COLUMNS): + return QVariant(self._data.COLUMNS[section]['display']) + + return QVariant() + + def setData(self, index, value, role): + if not index.isValid(): + return False + node = index.internalPointer() + if role == Qt.CheckStateRole: + if index.column() == 0: + self._app.toggle_marking_for_dupes([node.dupe]) + return True + if role == Qt.EditRole: + if index.column() == 0: + value = unicode(value.toString()) + if self._app.rename_dupe(node.dupe, value): + node.reset() + return True + return False + + def sort(self, column, order): + if self.power_marker: + self._results.sort_dupes(column, order == Qt.AscendingOrder, self.delta) + else: + self._results.sort_groups(column, order == Qt.AscendingOrder) + self.reset() + + def toggleMarked(self, indexes): + assert indexes + dupes = self.dupesForIndexes(indexes) + self._app.toggle_marking_for_dupes(dupes) + + #--- Properties + @property + def power_marker(self): + return self._power_marker + + @power_marker.setter + def power_marker(self, value): + if value == self._power_marker: + return + self._power_marker = value + self.reset() + + +class ResultsView(QTreeView): + #--- Override + def keyPressEvent(self, event): + if event.text() == ' ': + self.model().toggleMarked(self.selectionModel().selectedRows()) + return + QTreeView.keyPressEvent(self, event) + + def setModel(self, model): + assert isinstance(model, ResultsModel) + QTreeView.setModel(self, model) + + #--- Public + def selectedDupes(self): + return self.model().dupesForIndexes(self.selectionModel().selectedRows()) + diff --git a/pe/qt/base/tree_model.py b/pe/qt/base/tree_model.py new file mode 100644 index 00000000..b3a994b3 --- /dev/null +++ b/pe/qt/base/tree_model.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python +# Unit Name: tree_model +# Created By: Virgil Dupras +# Created On: 2009-05-04 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +from PyQt4.QtCore import Qt, QAbstractItemModel, QVariant, QModelIndex + +class TreeNode(object): + def __init__(self, parent, row): + self.parent = parent + self.row = row + self._children = None + + def _get_children(self): + raise NotImplementedError() + + @property + def children(self): + if self._children is None: + self._children = self._get_children() + return self._children + + +class TreeModel(QAbstractItemModel): + def __init__(self): + QAbstractItemModel.__init__(self) + self._nodes = None + + def _root_nodes(self): + raise NotImplementedError() + + def index(self, row, column, parent): + if not self.nodes: + return QModelIndex() + if not parent.isValid(): + return self.createIndex(row, column, self.nodes[row]) + node = parent.internalPointer() + return self.createIndex(row, column, node.children[row]) + + def parent(self, index): + if not index.isValid(): + return QModelIndex() + node = index.internalPointer() + if node.parent is None: + return QModelIndex() + else: + return self.createIndex(node.parent.row, 0, node.parent) + + def reset(self): + self._nodes = None + QAbstractItemModel.reset(self) + + def rowCount(self, parent): + if not parent.isValid(): + return len(self.nodes) + node = parent.internalPointer() + return len(node.children) + + @property + def nodes(self): + if self._nodes is None: + self._nodes = self._root_nodes() + return self._nodes + diff --git a/pe/qt/block.py b/pe/qt/block.py new file mode 100644 index 00000000..0270aba1 --- /dev/null +++ b/pe/qt/block.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python +# Unit Name: block +# Created By: Virgil Dupras +# Created On: 2009-05-10 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +from _block import getblocks + +# Converted to Cython +# def getblock(image): +# width = image.width() +# height = image.height() +# if width: +# pixel_count = width * height +# red = green = blue = 0 +# s = image.bits().asstring(image.numBytes()) +# for i in xrange(pixel_count): +# offset = i * 3 +# red += ord(s[offset]) +# green += ord(s[offset + 1]) +# blue += ord(s[offset + 2]) +# return (red // pixel_count, green // pixel_count, blue // pixel_count) +# else: +# return (0, 0, 0) +# +# def getblocks(image, block_count_per_side): +# width = image.width() +# height = image.height() +# if not width: +# return [] +# block_width = max(width // block_count_per_side, 1) +# block_height = max(height // block_count_per_side, 1) +# result = [] +# for ih in xrange(block_count_per_side): +# top = min(ih * block_height, height - block_height) +# for iw in range(block_count_per_side): +# left = min(iw * block_width, width - block_width) +# crop = image.copy(left, top, block_width, block_height) +# result.append(getblock(crop)) +# return result diff --git a/pe/qt/build.py b/pe/qt/build.py new file mode 100644 index 00000000..6e454952 --- /dev/null +++ b/pe/qt/build.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python +# Unit Name: build +# Created By: Virgil Dupras +# Created On: 2009-05-22 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +# On Windows, PyInstaller is used to build an exe (py2exe creates a very bad looking icon +# The release version is outdated. Use at least r672 on http://svn.pyinstaller.org/trunk + +import os +import shutil +from app import DupeGuru + +def print_and_do(cmd): + print cmd + os.system(cmd) + +# Removing build and dist +shutil.rmtree('build') +shutil.rmtree('dist') + +version = DupeGuru.VERSION +versioncomma = version.replace('.', ', ') + ', 0' +verinfo = open('verinfo').read() +verinfo = verinfo.replace('$versioncomma', versioncomma).replace('$version', version) +fp = open('verinfo_tmp', 'w') +fp.write(verinfo) +fp.close() +print_and_do("python C:\\Python26\\pyinstaller\\Build.py dgpe.spec") +os.remove('verinfo_tmp') + +print_and_do("xcopy /Y C:\\src\\vs_comp\\msvcrt dist") +print_and_do("xcopy /Y /S /I help\\dupeguru_pe_help dist\\help") + +aicom = '"\\Program Files\\Caphyon\\Advanced Installer\\AdvancedInstaller.com"' +shutil.copy('installer.aip', 'installer_tmp.aip') # this is so we don'a have to re-commit installer.aip at every version change +print_and_do('%s /edit installer_tmp.aip /SetVersion %s' % (aicom, version)) +print_and_do('%s /build installer_tmp.aip -force' % aicom) +os.remove('installer_tmp.aip') \ No newline at end of file diff --git a/pe/qt/details_dialog.py b/pe/qt/details_dialog.py new file mode 100644 index 00000000..0c7503a6 --- /dev/null +++ b/pe/qt/details_dialog.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python +# Unit Name: details_dialog +# Created By: Virgil Dupras +# Created On: 2009-04-27 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +from PyQt4.QtCore import Qt, SIGNAL, QAbstractTableModel, QVariant +from PyQt4.QtGui import QDialog, QHeaderView, QPixmap + +from base.details_table import DetailsModel +from details_dialog_ui import Ui_DetailsDialog + +class DetailsDialog(QDialog, Ui_DetailsDialog): + def __init__(self, parent, app): + QDialog.__init__(self, parent, Qt.Tool) + self.app = app + self.selectedPixmap = None + self.referencePixmap = None + self.setupUi(self) + self.model = DetailsModel(app) + self.tableView.setModel(self.model) + self.connect(app, SIGNAL('duplicateSelected()'), self.duplicateSelected) + + def _update(self): + dupe = self.app.selected_dupe + if dupe is None: + return + group = self.app.results.get_group_of_duplicate(dupe) + ref = group.ref + + self.selectedPixmap = QPixmap(unicode(dupe.path)) + if ref is dupe: + self.referencePixmap = self.selectedPixmap + else: + self.referencePixmap = QPixmap(unicode(ref.path)) + self._updateImages() + + def _updateImages(self): + if self.selectedPixmap is not None: + target_size = self.selectedImage.size() + scaledPixmap = self.selectedPixmap.scaled(target_size, Qt.KeepAspectRatio, Qt.SmoothTransformation) + self.selectedImage.setPixmap(scaledPixmap) + if self.referencePixmap is not None: + target_size = self.referenceImage.size() + scaledPixmap = self.referencePixmap.scaled(target_size, Qt.KeepAspectRatio, Qt.SmoothTransformation) + self.referenceImage.setPixmap(scaledPixmap) + + #--- Override + def resizeEvent(self, event): + self._updateImages() + + def show(self): + QDialog.show(self) + self._update() + + #--- Events + def duplicateSelected(self): + if self.isVisible(): + self._update() + diff --git a/pe/qt/details_dialog.ui b/pe/qt/details_dialog.ui new file mode 100644 index 00000000..cee1adb1 --- /dev/null +++ b/pe/qt/details_dialog.ui @@ -0,0 +1,113 @@ + + + DetailsDialog + + + + 0 + 0 + 502 + 295 + + + + + 250 + 250 + + + + Details + + + + 0 + + + 0 + + + + + 4 + + + + + + 0 + 0 + + + + + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + + + + false + + + Qt::AlignCenter + + + + + + + + + + 0 + 0 + + + + + 0 + 188 + + + + + 16777215 + 188 + + + + true + + + QAbstractItemView::SelectRows + + + false + + + + + + + + DetailsTable + QTableView +
base.details_table
+
+
+ + +
diff --git a/pe/qt/dgpe.spec b/pe/qt/dgpe.spec new file mode 100644 index 00000000..06e92f4e --- /dev/null +++ b/pe/qt/dgpe.spec @@ -0,0 +1,19 @@ +# -*- mode: python -*- +a = Analysis([os.path.join(HOMEPATH,'support\\_mountzlib.py'), os.path.join(HOMEPATH,'support\\useUnicode.py'), 'start.py'], + pathex=['C:\\src\\dupeguru\\pe\\qt']) +pyz = PYZ(a.pure) +exe = EXE(pyz, + a.scripts, + exclude_binaries=1, + name=os.path.join('build\\pyi.win32\\dupeGuru PE', 'dupeGuru PE.exe'), + debug=False, + strip=False, + upx=True, + console=False , icon='base\\images\\dgpe_logo.ico', version='verinfo_tmp') +coll = COLLECT( exe, + a.binaries, + a.zipfiles, + a.datas, + strip=False, + upx=True, + name=os.path.join('dist')) diff --git a/pe/qt/dupeguru/__init__.py b/pe/qt/dupeguru/__init__.py new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/pe/qt/dupeguru/__init__.py @@ -0,0 +1 @@ + diff --git a/pe/qt/dupeguru/app.py b/pe/qt/dupeguru/app.py new file mode 100644 index 00000000..0e03603d --- /dev/null +++ b/pe/qt/dupeguru/app.py @@ -0,0 +1,229 @@ +#!/usr/bin/env python +""" +Unit Name: dupeguru.app +Created By: Virgil Dupras +Created On: 2006/11/11 +Last modified by:$Author: virgil $ +Last modified on:$Date: 2009-05-28 16:02:48 +0200 (Thu, 28 May 2009) $ + $Revision: 4388 $ +Copyright 2006 Hardcoded Software (http://www.hardcoded.net) +""" +import os +import os.path as op +import logging + +from hsfs import IT_ATTRS, IT_EXTRA +from hsutil import job, io, files +from hsutil.path import Path +from hsutil.reg import RegistrableApplication, RegistrationRequired +from hsutil.misc import flatten, first +from hsutil.str import escape + +import directories +import results +import scanner + +JOB_SCAN = 'job_scan' +JOB_LOAD = 'job_load' +JOB_MOVE = 'job_move' +JOB_COPY = 'job_copy' +JOB_DELETE = 'job_delete' + +class NoScannableFileError(Exception): + pass + +class AllFilesAreRefError(Exception): + pass + +class DupeGuru(RegistrableApplication): + def __init__(self, data_module, appdata, appid): + RegistrableApplication.__init__(self, appid) + self.appdata = appdata + if not op.exists(self.appdata): + os.makedirs(self.appdata) + self.data = data_module + self.directories = directories.Directories() + self.results = results.Results(data_module) + self.scanner = scanner.Scanner() + self.action_count = 0 + self.last_op_error_count = 0 + self.options = { + 'escape_filter_regexp': True, + 'clean_empty_dirs': False, + } + + def _demo_check(self): + if self.registered: + return + count = self.results.mark_count + if count + self.action_count > 10: + raise RegistrationRequired() + else: + self.action_count += count + + def _do_delete(self, j): + def op(dupe): + j.add_progress() + return self._do_delete_dupe(dupe) + + j.start_job(self.results.mark_count) + self.last_op_error_count = self.results.perform_on_marked(op, True) + + def _do_delete_dupe(self, dupe): + if not io.exists(dupe.path): + dupe.parent = None + return True + self._recycle_dupe(dupe) + self.clean_empty_dirs(dupe.path[:-1]) + if not io.exists(dupe.path): + dupe.parent = None + return True + logging.warning(u"Could not send {0} to trash.".format(unicode(dupe.path))) + return False + + def _do_load(self, j): + self.directories.LoadFromFile(op.join(self.appdata, 'last_directories.xml')) + j = j.start_subjob([1, 9]) + self.results.load_from_xml(op.join(self.appdata, 'last_results.xml'), self._get_file, j) + files = flatten(g[:] for g in self.results.groups) + for file in j.iter_with_progress(files, 'Reading metadata %d/%d'): + file._read_all_info(sections=[IT_ATTRS, IT_EXTRA]) + + def _get_file(self, str_path): + p = Path(str_path) + for d in self.directories: + if p not in d.path: + continue + result = d.find_path(p[d.path:]) + if result is not None: + return result + + @staticmethod + def _recycle_dupe(dupe): + raise NotImplementedError() + + def _start_job(self, jobid, func): + # func(j) + raise NotImplementedError() + + def AddDirectory(self, d): + try: + self.directories.add_path(Path(d)) + return 0 + except directories.AlreadyThereError: + return 1 + except directories.InvalidPathError: + return 2 + + def AddToIgnoreList(self, dupe): + g = self.results.get_group_of_duplicate(dupe) + for other in g: + if other is not dupe: + self.scanner.ignore_list.Ignore(unicode(other.path), unicode(dupe.path)) + + def ApplyFilter(self, filter): + self.results.apply_filter(None) + if self.options['escape_filter_regexp']: + filter = escape(filter, '()[]\\.|+?^') + filter = escape(filter, '*', '.') + self.results.apply_filter(filter) + + def clean_empty_dirs(self, path): + if self.options['clean_empty_dirs']: + while files.delete_if_empty(path, ['.DS_Store']): + path = path[:-1] + + def CopyOrMove(self, dupe, copy, destination, dest_type): + """ + copy: True = Copy False = Move + destination: string. + dest_type: 0 = right in destination. + 1 = relative re-creation. + 2 = absolute re-creation. + """ + source_path = dupe.path + location_path = dupe.root.path + dest_path = Path(destination) + if dest_type == 2: + dest_path = dest_path + source_path[1:-1] #Remove drive letter and filename + elif dest_type == 1: + dest_path = dest_path + source_path[location_path:-1] + if not io.exists(dest_path): + io.makedirs(dest_path) + try: + if copy: + files.copy(source_path, dest_path) + else: + files.move(source_path, dest_path) + self.clean_empty_dirs(source_path[:-1]) + except (IOError, OSError) as e: + operation = 'Copy' if copy else 'Move' + logging.warning('%s operation failed on %s. Error: %s' % (operation, unicode(dupe.path), unicode(e))) + return False + return True + + def copy_or_move_marked(self, copy, destination, recreate_path): + def do(j): + def op(dupe): + j.add_progress() + return self.CopyOrMove(dupe, copy, destination, recreate_path) + + j.start_job(self.results.mark_count) + self.last_op_error_count = self.results.perform_on_marked(op, not copy) + + self._demo_check() + jobid = JOB_COPY if copy else JOB_MOVE + self._start_job(jobid, do) + + def delete_marked(self): + self._demo_check() + self._start_job(JOB_DELETE, self._do_delete) + + def load(self): + self._start_job(JOB_LOAD, self._do_load) + self.LoadIgnoreList() + + def LoadIgnoreList(self): + p = op.join(self.appdata, 'ignore_list.xml') + self.scanner.ignore_list.load_from_xml(p) + + def make_reference(self, duplicates): + changed_groups = set() + for dupe in duplicates: + g = self.results.get_group_of_duplicate(dupe) + if g not in changed_groups: + self.results.make_ref(dupe) + changed_groups.add(g) + + def Save(self): + self.directories.SaveToFile(op.join(self.appdata, 'last_directories.xml')) + self.results.save_to_xml(op.join(self.appdata, 'last_results.xml')) + + def SaveIgnoreList(self): + p = op.join(self.appdata, 'ignore_list.xml') + self.scanner.ignore_list.save_to_xml(p) + + def start_scanning(self): + def do(j): + j.set_progress(0, 'Collecting files to scan') + files = list(self.directories.get_files()) + logging.info('Scanning %d files' % len(files)) + self.results.groups = self.scanner.GetDupeGroups(files, j) + + files = self.directories.get_files() + first_file = first(files) + if first_file is None: + raise NoScannableFileError() + if first_file.is_ref and all(f.is_ref for f in files): + raise AllFilesAreRefError() + self.results.groups = [] + self._start_job(JOB_SCAN, do) + + #--- Properties + @property + def stat_line(self): + result = self.results.stat_line + if self.scanner.discarded_file_count: + result = '%s (%d discarded)' % (result, self.scanner.discarded_file_count) + return result + diff --git a/pe/qt/dupeguru/app_cocoa.py b/pe/qt/dupeguru/app_cocoa.py new file mode 100644 index 00000000..4974d700 --- /dev/null +++ b/pe/qt/dupeguru/app_cocoa.py @@ -0,0 +1,304 @@ +#!/usr/bin/env python +""" +Unit Name: dupeguru.app_cocoa +Created By: Virgil Dupras +Created On: 2006/11/11 +Last modified by:$Author: virgil $ +Last modified on:$Date: 2009-05-28 16:33:32 +0200 (Thu, 28 May 2009) $ + $Revision: 4392 $ +Copyright 2006 Hardcoded Software (http://www.hardcoded.net) +""" +from AppKit import * +import logging +import os.path as op + +import hsfs as fs +from hsfs.phys.bundle import Bundle +from hsutil.cocoa import install_exception_hook +from hsutil.str import get_file_ext +from hsutil import io, cocoa, job +from hsutil.reg import RegistrationRequired + +import export, app, data + +JOBID2TITLE = { + app.JOB_SCAN: "Scanning for duplicates", + app.JOB_LOAD: "Loading", + app.JOB_MOVE: "Moving", + app.JOB_COPY: "Copying", + app.JOB_DELETE: "Sending to Trash", +} + +class DGDirectory(fs.phys.Directory): + def _create_sub_dir(self,name,with_parent = True): + ext = get_file_ext(name) + if ext == 'app': + if with_parent: + parent = self + else: + parent = None + return Bundle(parent,name) + else: + return super(DGDirectory,self)._create_sub_dir(name,with_parent) + + +def demo_method(method): + def wrapper(self, *args, **kwargs): + try: + return method(self, *args, **kwargs) + except RegistrationRequired: + NSNotificationCenter.defaultCenter().postNotificationName_object_('RegistrationRequired', self) + + return wrapper + +class DupeGuru(app.DupeGuru): + def __init__(self, data_module, appdata_subdir, appid): + LOGGING_LEVEL = logging.DEBUG if NSUserDefaults.standardUserDefaults().boolForKey_('debug') else logging.WARNING + logging.basicConfig(level=LOGGING_LEVEL, format='%(levelname)s %(message)s') + logging.debug('started in debug mode') + install_exception_hook() + if data_module is None: + data_module = data + appdata = op.expanduser(op.join('~', '.hsoftdata', appdata_subdir)) + app.DupeGuru.__init__(self, data_module, appdata, appid) + self.progress = cocoa.ThreadedJobPerformer() + self.directories.dirclass = DGDirectory + self.display_delta_values = False + self.selected_dupes = [] + self.RefreshDetailsTable(None,None) + + #--- Override + @staticmethod + def _recycle_dupe(dupe): + if not io.exists(dupe.path): + dupe.parent = None + return True + directory = unicode(dupe.parent.path) + filename = dupe.name + result, tag = NSWorkspace.sharedWorkspace().performFileOperation_source_destination_files_tag_( + NSWorkspaceRecycleOperation, directory, '', [filename]) + if not io.exists(dupe.path): + dupe.parent = None + return True + logging.warning('Could not send %s to trash. tag: %d' % (unicode(dupe.path), tag)) + return False + + def _start_job(self, jobid, func): + try: + j = self.progress.create_job() + self.progress.run_threaded(func, args=(j, )) + except job.JobInProgressError: + NSNotificationCenter.defaultCenter().postNotificationName_object_('JobInProgress', self) + else: + ud = {'desc': JOBID2TITLE[jobid], 'jobid':jobid} + NSNotificationCenter.defaultCenter().postNotificationName_object_userInfo_('JobStarted', self, ud) + + #---Helpers + def GetObjects(self,node_path): + #returns a tuple g,d + try: + g = self.results.groups[node_path[0]] + if len(node_path) == 2: + return (g,self.results.groups[node_path[0]].dupes[node_path[1]]) + else: + return (g,None) + except IndexError: + return (None,None) + + def GetDirectory(self,node_path,curr_dir=None): + if not node_path: + return curr_dir + if curr_dir is not None: + l = curr_dir.dirs + else: + l = self.directories + d = l[node_path[0]] + return self.GetDirectory(node_path[1:],d) + + def RefreshDetailsTable(self,dupe,group): + l1 = self.data.GetDisplayInfo(dupe,group,False) + if group is not None: + l2 = self.data.GetDisplayInfo(group.ref,group,False) + else: + l2 = l1 #To have a list of empty '---' values + names = [c['display'] for c in self.data.COLUMNS] + self.details_table = zip(names,l1,l2) + + #---Public + def AddSelectedToIgnoreList(self): + for dupe in self.selected_dupes: + self.AddToIgnoreList(dupe) + + copy_or_move_marked = demo_method(app.DupeGuru.copy_or_move_marked) + delete_marked = demo_method(app.DupeGuru.delete_marked) + + def ExportToXHTML(self,column_ids,xslt_path,css_path): + columns = [] + for index,column in enumerate(self.data.COLUMNS): + display = column['display'] + enabled = str(index) in column_ids + columns.append((display,enabled)) + xml_path = op.join(self.appdata,'results_export.xml') + self.results.save_to_xml(xml_path,self.data.GetDisplayInfo) + return export.export_to_xhtml(xml_path,xslt_path,css_path,columns) + + def MakeSelectedReference(self): + self.make_reference(self.selected_dupes) + + def OpenSelected(self): + if self.selected_dupes: + path = unicode(self.selected_dupes[0].path) + NSWorkspace.sharedWorkspace().openFile_(path) + + def PurgeIgnoreList(self): + self.scanner.ignore_list.Filter(lambda f,s:op.exists(f) and op.exists(s)) + + def RefreshDetailsWithSelected(self): + if self.selected_dupes: + self.RefreshDetailsTable( + self.selected_dupes[0], + self.results.get_group_of_duplicate(self.selected_dupes[0]) + ) + else: + self.RefreshDetailsTable(None,None) + + def RemoveDirectory(self,index): + try: + del self.directories[index] + except IndexError: + pass + + def RemoveSelected(self): + self.results.remove_duplicates(self.selected_dupes) + + def RenameSelected(self,newname): + try: + d = self.selected_dupes[0] + d = d.move(d.parent,newname) + return True + except (IndexError,fs.FSError),e: + logging.warning("dupeGuru Warning: %s" % str(e)) + return False + + def RevealSelected(self): + if self.selected_dupes: + path = unicode(self.selected_dupes[0].path) + NSWorkspace.sharedWorkspace().selectFile_inFileViewerRootedAtPath_(path,'') + + def start_scanning(self): + self.RefreshDetailsTable(None, None) + try: + app.DupeGuru.start_scanning(self) + return 0 + except app.NoScannableFileError: + return 3 + except app.AllFilesAreRefError: + return 1 + + def SelectResultNodePaths(self,node_paths): + def extract_dupe(t): + g,d = t + if d is not None: + return d + else: + if g is not None: + return g.ref + + selected = [extract_dupe(self.GetObjects(p)) for p in node_paths] + self.selected_dupes = [dupe for dupe in selected if dupe is not None] + + def SelectPowerMarkerNodePaths(self,node_paths): + rows = [p[0] for p in node_paths] + self.selected_dupes = [ + self.results.dupes[row] for row in rows if row in xrange(len(self.results.dupes)) + ] + + def SetDirectoryState(self,node_path,state): + d = self.GetDirectory(node_path) + self.directories.SetState(d.path,state) + + def sort_dupes(self,key,asc): + self.results.sort_dupes(key,asc,self.display_delta_values) + + def sort_groups(self,key,asc): + self.results.sort_groups(key,asc) + + def ToggleSelectedMarkState(self): + for dupe in self.selected_dupes: + self.results.mark_toggle(dupe) + + #---Data + def GetOutlineViewMaxLevel(self, tag): + if tag == 0: + return 2 + elif tag == 1: + return 0 + elif tag == 2: + return 1 + + def GetOutlineViewChildCounts(self, tag, node_path): + if self.progress._job_running: + return [] + if tag == 0: #Normal results + assert not node_path # no other value is possible + return [len(g.dupes) for g in self.results.groups] + elif tag == 1: #Directories + dirs = self.GetDirectory(node_path).dirs if node_path else self.directories + return [d.dircount for d in dirs] + else: #Power Marker + assert not node_path # no other value is possible + return [0 for d in self.results.dupes] + + def GetOutlineViewValues(self, tag, node_path): + if self.progress._job_running: + return + if not node_path: + return + if tag in (0,2): #Normal results / Power Marker + if tag == 0: + g, d = self.GetObjects(node_path) + if d is None: + d = g.ref + else: + d = self.results.dupes[node_path[0]] + g = self.results.get_group_of_duplicate(d) + result = self.data.GetDisplayInfo(d, g, self.display_delta_values) + return result + elif tag == 1: #Directories + d = self.GetDirectory(node_path) + return [ + d.name, + self.directories.GetState(d.path) + ] + + def GetOutlineViewMarked(self, tag, node_path): + # 0=unmarked 1=marked 2=unmarkable + if self.progress._job_running: + return + if not node_path: + return 2 + if tag == 1: #Directories + return 2 + if tag == 0: #Normal results + g, d = self.GetObjects(node_path) + else: #Power Marker + d = self.results.dupes[node_path[0]] + if (d is None) or (not self.results.is_markable(d)): + return 2 + elif self.results.is_marked(d): + return 1 + else: + return 0 + + def GetTableViewCount(self, tag): + if self.progress._job_running: + return 0 + return len(self.details_table) + + def GetTableViewMarkedIndexes(self,tag): + return [] + + def GetTableViewValues(self,tag,row): + return self.details_table[row] + + diff --git a/pe/qt/dupeguru/app_cocoa_test.py b/pe/qt/dupeguru/app_cocoa_test.py new file mode 100644 index 00000000..ad8b937a --- /dev/null +++ b/pe/qt/dupeguru/app_cocoa_test.py @@ -0,0 +1,320 @@ +#!/usr/bin/env python +""" +Unit Name: dupeguru.tests.app_cocoa +Created By: Virgil Dupras +Created On: 2006/11/11 +Last modified by:$Author: virgil $ +Last modified on:$Date: 2009-05-29 17:51:41 +0200 (Fri, 29 May 2009) $ + $Revision: 4409 $ +Copyright 2006 Hardcoded Software (http://www.hardcoded.net) +""" +import tempfile +import shutil +import logging + +from hsutil.path import Path +from hsutil.testcase import TestCase +from hsutil.decorators import log_calls +import hsfs.phys +import os.path as op + +from . import engine, data +try: + from .app_cocoa import DupeGuru as DupeGuruBase, DGDirectory +except ImportError: + from nose.plugins.skip import SkipTest + raise SkipTest("These tests can only be run on OS X") +from .results_test import GetTestGroups + +class DupeGuru(DupeGuruBase): + def __init__(self): + DupeGuruBase.__init__(self, data, '/tmp', appid=4) + + def _start_job(self, jobid, func): + func(nulljob) + + +def r2np(rows): + #Transforms a list of rows [1,2,3] into a list of node paths [[1],[2],[3]] + return [[i] for i in rows] + +class TCDupeGuru(TestCase): + def setUp(self): + self.app = DupeGuru() + self.objects,self.matches,self.groups = GetTestGroups() + self.app.results.groups = self.groups + + def test_GetObjects(self): + app = self.app + objects = self.objects + groups = self.groups + g,d = app.GetObjects([0]) + self.assert_(g is groups[0]) + self.assert_(d is None) + g,d = app.GetObjects([0,0]) + self.assert_(g is groups[0]) + self.assert_(d is objects[1]) + g,d = app.GetObjects([1,0]) + self.assert_(g is groups[1]) + self.assert_(d is objects[4]) + + def test_GetObjects_after_sort(self): + app = self.app + objects = self.objects + groups = self.groups[:] #To keep the old order in memory + app.sort_groups(0,False) #0 = Filename + #Now, the group order is supposed to be reversed + g,d = app.GetObjects([0,0]) + self.assert_(g is groups[1]) + self.assert_(d is objects[4]) + + def test_GetObjects_out_of_range(self): + app = self.app + self.assertEqual((None,None),app.GetObjects([2])) + self.assertEqual((None,None),app.GetObjects([])) + self.assertEqual((None,None),app.GetObjects([1,2])) + + def test_selectResultNodePaths(self): + app = self.app + objects = self.objects + app.SelectResultNodePaths([[0,0],[0,1]]) + self.assertEqual(2,len(app.selected_dupes)) + self.assert_(app.selected_dupes[0] is objects[1]) + self.assert_(app.selected_dupes[1] is objects[2]) + + def test_selectResultNodePaths_with_ref(self): + app = self.app + objects = self.objects + app.SelectResultNodePaths([[0,0],[0,1],[1]]) + self.assertEqual(3,len(app.selected_dupes)) + self.assert_(app.selected_dupes[0] is objects[1]) + self.assert_(app.selected_dupes[1] is objects[2]) + self.assert_(app.selected_dupes[2] is self.groups[1].ref) + + def test_selectResultNodePaths_empty(self): + self.app.SelectResultNodePaths([]) + self.assertEqual(0,len(self.app.selected_dupes)) + + def test_selectResultNodePaths_after_sort(self): + app = self.app + objects = self.objects + groups = self.groups[:] #To keep the old order in memory + app.sort_groups(0,False) #0 = Filename + #Now, the group order is supposed to be reversed + app.SelectResultNodePaths([[0,0],[1],[1,0]]) + self.assertEqual(3,len(app.selected_dupes)) + self.assert_(app.selected_dupes[0] is objects[4]) + self.assert_(app.selected_dupes[1] is groups[0].ref) + self.assert_(app.selected_dupes[2] is objects[1]) + + def test_selectResultNodePaths_out_of_range(self): + app = self.app + app.SelectResultNodePaths([[0,0],[0,1],[1],[1,1],[2]]) + self.assertEqual(3,len(app.selected_dupes)) + + def test_selectPowerMarkerRows(self): + app = self.app + objects = self.objects + app.SelectPowerMarkerNodePaths(r2np([0,1,2])) + self.assertEqual(3,len(app.selected_dupes)) + self.assert_(app.selected_dupes[0] is objects[1]) + self.assert_(app.selected_dupes[1] is objects[2]) + self.assert_(app.selected_dupes[2] is objects[4]) + + def test_selectPowerMarkerRows_empty(self): + self.app.SelectPowerMarkerNodePaths([]) + self.assertEqual(0,len(self.app.selected_dupes)) + + def test_selectPowerMarkerRows_after_sort(self): + app = self.app + objects = self.objects + app.sort_dupes(0,False) #0 = Filename + app.SelectPowerMarkerNodePaths(r2np([0,1,2])) + self.assertEqual(3,len(app.selected_dupes)) + self.assert_(app.selected_dupes[0] is objects[4]) + self.assert_(app.selected_dupes[1] is objects[2]) + self.assert_(app.selected_dupes[2] is objects[1]) + + def test_selectPowerMarkerRows_out_of_range(self): + app = self.app + app.SelectPowerMarkerNodePaths(r2np([0,1,2,3])) + self.assertEqual(3,len(app.selected_dupes)) + + def test_toggleSelectedMark(self): + app = self.app + objects = self.objects + app.ToggleSelectedMarkState() + self.assertEqual(0,app.results.mark_count) + app.SelectPowerMarkerNodePaths(r2np([0,2])) + app.ToggleSelectedMarkState() + self.assertEqual(2,app.results.mark_count) + self.assert_(not app.results.is_marked(objects[0])) + self.assert_(app.results.is_marked(objects[1])) + self.assert_(not app.results.is_marked(objects[2])) + self.assert_(not app.results.is_marked(objects[3])) + self.assert_(app.results.is_marked(objects[4])) + + def test_refreshDetailsWithSelected(self): + def mock_refresh(dupe,group): + self.called = True + if self.app.selected_dupes: + self.assert_(dupe is self.app.selected_dupes[0]) + self.assert_(group is self.app.results.get_group_of_duplicate(dupe)) + else: + self.assert_(dupe is None) + self.assert_(group is None) + + self.app.RefreshDetailsTable = mock_refresh + self.called = False + self.app.SelectPowerMarkerNodePaths(r2np([0,2])) + self.app.RefreshDetailsWithSelected() + self.assert_(self.called) + self.called = False + self.app.SelectPowerMarkerNodePaths([]) + self.app.RefreshDetailsWithSelected() + self.assert_(self.called) + + def test_makeSelectedReference(self): + app = self.app + objects = self.objects + groups = self.groups + app.SelectPowerMarkerNodePaths(r2np([0,2])) + app.MakeSelectedReference() + self.assert_(groups[0].ref is objects[1]) + self.assert_(groups[1].ref is objects[4]) + + def test_makeSelectedReference_by_selecting_two_dupes_in_the_same_group(self): + app = self.app + objects = self.objects + groups = self.groups + app.SelectPowerMarkerNodePaths(r2np([0,1,2])) + #Only 0 and 2 must go ref, not 1 because it is a part of the same group + app.MakeSelectedReference() + self.assert_(groups[0].ref is objects[1]) + self.assert_(groups[1].ref is objects[4]) + + def test_removeSelected(self): + app = self.app + app.SelectPowerMarkerNodePaths(r2np([0,2])) + app.RemoveSelected() + self.assertEqual(1,len(app.results.dupes)) + app.RemoveSelected() + self.assertEqual(1,len(app.results.dupes)) + app.SelectPowerMarkerNodePaths(r2np([0,2])) + app.RemoveSelected() + self.assertEqual(0,len(app.results.dupes)) + + def test_addDirectory_simple(self): + app = self.app + self.assertEqual(0,app.AddDirectory(self.datadirpath())) + self.assertEqual(1,len(app.directories)) + + def test_addDirectory_already_there(self): + app = self.app + self.assertEqual(0,app.AddDirectory(self.datadirpath())) + self.assertEqual(1,app.AddDirectory(self.datadirpath())) + + def test_addDirectory_does_not_exist(self): + app = self.app + self.assertEqual(2,app.AddDirectory('/does_not_exist')) + + def test_ignore(self): + app = self.app + app.SelectPowerMarkerNodePaths(r2np([2])) #The dupe of the second, 2 sized group + app.AddSelectedToIgnoreList() + self.assertEqual(1,len(app.scanner.ignore_list)) + app.SelectPowerMarkerNodePaths(r2np([0])) #first dupe of the 3 dupes group + app.AddSelectedToIgnoreList() + #BOTH the ref and the other dupe should have been added + self.assertEqual(3,len(app.scanner.ignore_list)) + + def test_purgeIgnoreList(self): + app = self.app + p1 = self.filepath('zerofile') + p2 = self.filepath('zerofill') + dne = '/does_not_exist' + app.scanner.ignore_list.Ignore(dne,p1) + app.scanner.ignore_list.Ignore(p2,dne) + app.scanner.ignore_list.Ignore(p1,p2) + app.PurgeIgnoreList() + self.assertEqual(1,len(app.scanner.ignore_list)) + self.assert_(app.scanner.ignore_list.AreIgnored(p1,p2)) + self.assert_(not app.scanner.ignore_list.AreIgnored(dne,p1)) + + def test_only_unicode_is_added_to_ignore_list(self): + def FakeIgnore(first,second): + if not isinstance(first,unicode): + self.fail() + if not isinstance(second,unicode): + self.fail() + + app = self.app + app.scanner.ignore_list.Ignore = FakeIgnore + app.SelectPowerMarkerNodePaths(r2np([2])) #The dupe of the second, 2 sized group + app.AddSelectedToIgnoreList() + + def test_dirclass(self): + self.assert_(self.app.directories.dirclass is DGDirectory) + + +class TCDupeGuru_renameSelected(TestCase): + def setUp(self): + p = Path(tempfile.mkdtemp()) + fp = open(str(p + 'foo bar 1'),mode='w') + fp.close() + fp = open(str(p + 'foo bar 2'),mode='w') + fp.close() + fp = open(str(p + 'foo bar 3'),mode='w') + fp.close() + refdir = hsfs.phys.Directory(None,str(p)) + matches = engine.MatchFactory().getmatches(refdir.files) + groups = engine.get_groups(matches) + g = groups[0] + g.prioritize(lambda x:x.name) + app = DupeGuru() + app.results.groups = groups + self.app = app + self.groups = groups + self.p = p + self.refdir = refdir + + def tearDown(self): + shutil.rmtree(str(self.p)) + + def test_simple(self): + app = self.app + refdir = self.refdir + g = self.groups[0] + app.SelectPowerMarkerNodePaths(r2np([0])) + self.assert_(app.RenameSelected('renamed')) + self.assert_('renamed' in refdir) + self.assert_('foo bar 2' not in refdir) + self.assert_(g.dupes[0] is refdir['renamed']) + self.assert_(g.dupes[0] in refdir) + + def test_none_selected(self): + app = self.app + refdir = self.refdir + g = self.groups[0] + app.SelectPowerMarkerNodePaths([]) + self.mock(logging, 'warning', log_calls(lambda msg: None)) + self.assert_(not app.RenameSelected('renamed')) + msg = logging.warning.calls[0]['msg'] + self.assertEqual('dupeGuru Warning: list index out of range', msg) + self.assert_('renamed' not in refdir) + self.assert_('foo bar 2' in refdir) + self.assert_(g.dupes[0] is refdir['foo bar 2']) + + def test_name_already_exists(self): + app = self.app + refdir = self.refdir + g = self.groups[0] + app.SelectPowerMarkerNodePaths(r2np([0])) + self.mock(logging, 'warning', log_calls(lambda msg: None)) + self.assert_(not app.RenameSelected('foo bar 1')) + msg = logging.warning.calls[0]['msg'] + self.assert_(msg.startswith('dupeGuru Warning: \'foo bar 2\' already exists in')) + self.assert_('foo bar 1' in refdir) + self.assert_('foo bar 2' in refdir) + self.assert_(g.dupes[0] is refdir['foo bar 2']) + diff --git a/pe/qt/dupeguru/app_me_cocoa.py b/pe/qt/dupeguru/app_me_cocoa.py new file mode 100644 index 00000000..51a61767 --- /dev/null +++ b/pe/qt/dupeguru/app_me_cocoa.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python +""" +Unit Name: dupeguru.app_me_cocoa +Created By: Virgil Dupras +Created On: 2006/11/16 +Last modified by:$Author: virgil $ +Last modified on:$Date: 2009-05-28 16:33:32 +0200 (Thu, 28 May 2009) $ + $Revision: 4392 $ +Copyright 2006 Hardcoded Software (http://www.hardcoded.net) +""" +import os.path as op +import logging +from appscript import app, k, CommandError +import time + +from hsutil.cocoa import as_fetch +import hsfs.phys.music + +import app_cocoa, data_me, scanner + +JOB_REMOVE_DEAD_TRACKS = 'jobRemoveDeadTracks' +JOB_SCAN_DEAD_TRACKS = 'jobScanDeadTracks' + +app_cocoa.JOBID2TITLE.update({ + JOB_REMOVE_DEAD_TRACKS: "Removing dead tracks from your iTunes Library", + JOB_SCAN_DEAD_TRACKS: "Scanning the iTunes Library", +}) + +class DupeGuruME(app_cocoa.DupeGuru): + def __init__(self): + app_cocoa.DupeGuru.__init__(self, data_me, 'dupeguru_me', appid=1) + self.scanner = scanner.ScannerME() + self.directories.dirclass = hsfs.phys.music.Directory + self.dead_tracks = [] + + def remove_dead_tracks(self): + def do(j): + a = app('iTunes') + for index, track in enumerate(j.iter_with_progress(self.dead_tracks)): + if index % 100 == 0: + time.sleep(.1) + try: + track.delete() + except CommandError as e: + logging.warning('Error while trying to remove a track from iTunes: %s' % unicode(e)) + + self._start_job(JOB_REMOVE_DEAD_TRACKS, do) + + def scan_dead_tracks(self): + def do(j): + a = app('iTunes') + try: + [source] = [s for s in a.sources() if s.kind() == k.library] + [library] = source.library_playlists() + except ValueError: + logging.warning('Some unexpected iTunes configuration encountered') + 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() == k.missing_value: + self.dead_tracks.append(track) + logging.info('Found %d dead tracks' % len(self.dead_tracks)) + + self._start_job(JOB_SCAN_DEAD_TRACKS, do) + diff --git a/pe/qt/dupeguru/app_pe_cocoa.py b/pe/qt/dupeguru/app_pe_cocoa.py new file mode 100644 index 00000000..5969d1c3 --- /dev/null +++ b/pe/qt/dupeguru/app_pe_cocoa.py @@ -0,0 +1,212 @@ +#!/usr/bin/env python +""" +Unit Name: dupeguru.app_pe_cocoa +Created By: Virgil Dupras +Created On: 2006/11/13 +Last modified by:$Author: virgil $ +Last modified on:$Date: 2009-05-28 16:33:32 +0200 (Thu, 28 May 2009) $ + $Revision: 4392 $ +Copyright 2006 Hardcoded Software (http://www.hardcoded.net) +""" +import os +import os.path as op +import logging +import plistlib + +import objc +from Foundation import * +from AppKit import * +from appscript import app, k + +from hsutil import job, io +import hsfs as fs +from hsfs import phys +from hsutil import files +from hsutil.str import get_file_ext +from hsutil.path import Path +from hsutil.cocoa import as_fetch + +import app_cocoa, data_pe, directories, picture.matchbase +from picture.cache import string_to_colors, Cache + +mainBundle = NSBundle.mainBundle() +PictureBlocks = mainBundle.classNamed_('PictureBlocks') +assert PictureBlocks is not None + +class Photo(phys.File): + cls_info_map = { + 'size': fs.IT_ATTRS, + 'ctime': fs.IT_ATTRS, + 'mtime': fs.IT_ATTRS, + 'md5': fs.IT_MD5, + 'md5partial': fs.IT_MD5, + 'dimensions': fs.IT_EXTRA, + } + + def _initialize_info(self,section): + super(Photo, self)._initialize_info(section) + if section == fs.IT_EXTRA: + self._info.update({ + 'dimensions': (0,0), + }) + + def _read_info(self,section): + super(Photo, self)._read_info(section) + if section == fs.IT_EXTRA: + size = PictureBlocks.getImageSize_(unicode(self.path)) + self._info['dimensions'] = (size.width, size.height) + + def get_blocks(self, block_count_per_side): + try: + blocks = PictureBlocks.getBlocksFromImagePath_blockCount_scanArea_(unicode(self.path), block_count_per_side, 0) + except Exception, e: + raise IOError('The reading of "%s" failed with "%s"' % (unicode(self.path), unicode(e))) + if not blocks: + raise IOError('The picture %s could not be read' % unicode(self.path)) + return string_to_colors(blocks) + + +class IPhoto(Photo): + def __init__(self, parent, whole_path): + super(IPhoto, self).__init__(parent, whole_path[-1]) + self.whole_path = whole_path + + def _build_path(self): + return self.whole_path + + @property + def display_path(self): + return super(IPhoto, self)._build_path() + + +class Directory(phys.Directory): + cls_file_class = Photo + cls_supported_exts = ('png', 'jpg', 'jpeg', 'gif', 'psd', 'bmp', 'tiff', 'nef', 'cr2') + + def _fetch_subitems(self): + subdirs, subfiles = super(Directory,self)._fetch_subitems() + return subdirs, [name for name in subfiles if get_file_ext(name) in self.cls_supported_exts] + + +class IPhotoLibrary(fs.Directory): + def __init__(self, plistpath): + self.plistpath = plistpath + self.refpath = plistpath[:-1] + # the AlbumData.xml file lives right in the library path + super(IPhotoLibrary, self).__init__(None, 'iPhoto Library') + + def _update_photo(self, photo_data): + if photo_data['MediaType'] != 'Image': + return + photo_path = Path(photo_data['ImagePath']) + subpath = photo_path[len(self.refpath):-1] + subdir = self + for element in subpath: + try: + subdir = subdir[element] + except KeyError: + subdir = fs.Directory(subdir, element) + IPhoto(subdir, photo_path) + + def update(self): + self.clear() + s = open(unicode(self.plistpath)).read() + # There was a case where a guy had 0x10 chars in his plist, causing expat errors on loading + s = s.replace('\x10', '') + plist = plistlib.readPlistFromString(s) + for photo_data in plist['Master Image List'].values(): + self._update_photo(photo_data) + + def force_update(self): # Don't update + pass + + +class DupeGuruPE(app_cocoa.DupeGuru): + def __init__(self): + app_cocoa.DupeGuru.__init__(self, data_pe, 'dupeguru_pe', appid=5) + self.scanner.match_factory = picture.matchbase.AsyncMatchFactory() + self.directories.dirclass = Directory + self.directories.special_dirclasses[Path('iPhoto Library')] = lambda _, __: self._create_iphoto_library() + p = op.join(self.appdata, 'cached_pictures.db') + self.scanner.match_factory.cached_blocks = Cache(p) + + def _create_iphoto_library(self): + ud = NSUserDefaults.standardUserDefaults() + prefs = ud.persistentDomainForName_('com.apple.iApps') + plisturl = NSURL.URLWithString_(prefs['iPhotoRecentDatabases'][0]) + plistpath = Path(plisturl.path()) + return IPhotoLibrary(plistpath) + + def _do_delete(self, j): + def op(dupe): + j.add_progress() + return self._do_delete_dupe(dupe) + + marked = [dupe for dupe in self.results.dupes if self.results.is_marked(dupe)] + self.path2iphoto = {} + if any(isinstance(dupe, IPhoto) for dupe in marked): + a = app('iPhoto') + a.select(a.photo_library_album()) + photos = as_fetch(a.photo_library_album().photos, k.item) + for photo in photos: + self.path2iphoto[photo.image_path()] = photo + self.last_op_error_count = self.results.perform_on_marked(op, True) + del self.path2iphoto + + def _do_delete_dupe(self, dupe): + if isinstance(dupe, IPhoto): + photo = self.path2iphoto[unicode(dupe.path)] + app('iPhoto').remove(photo) + return True + else: + return app_cocoa.DupeGuru._do_delete_dupe(self, dupe) + + def _do_load(self, j): + self.directories.LoadFromFile(op.join(self.appdata, 'last_directories.xml')) + for d in self.directories: + if isinstance(d, IPhotoLibrary): + d.update() + self.results.load_from_xml(op.join(self.appdata, 'last_results.xml'), self._get_file, j) + + def _get_file(self, str_path): + p = Path(str_path) + for d in self.directories: + result = None + if p in d.path: + result = d.find_path(p[d.path:]) + if isinstance(d, IPhotoLibrary) and p in d.refpath: + result = d.find_path(p[d.refpath:]) + if result is not None: + return result + + def AddDirectory(self, d): + try: + added = self.directories.add_path(Path(d)) + if d == 'iPhoto Library': + added.update() + return 0 + except directories.AlreadyThereError: + return 1 + + def CopyOrMove(self, dupe, copy, destination, dest_type): + if isinstance(dupe, IPhoto): + copy = True + return app_cocoa.DupeGuru.CopyOrMove(self, dupe, copy, destination, dest_type) + + def start_scanning(self): + for directory in self.directories: + if isinstance(directory, IPhotoLibrary): + self.directories.SetState(directory.refpath, directories.STATE_EXCLUDED) + return app_cocoa.DupeGuru.start_scanning(self) + + 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 + return ref.path + diff --git a/pe/qt/dupeguru/app_se_cocoa.py b/pe/qt/dupeguru/app_se_cocoa.py new file mode 100644 index 00000000..3d8c62b2 --- /dev/null +++ b/pe/qt/dupeguru/app_se_cocoa.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python +# Unit Name: app_se_cocoa +# Created By: Virgil Dupras +# Created On: 2009-05-24 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +import app_cocoa, data + +class DupeGuru(app_cocoa.DupeGuru): + def __init__(self): + app_cocoa.DupeGuru.__init__(self, data, 'dupeguru', appid=4) + diff --git a/pe/qt/dupeguru/app_test.py b/pe/qt/dupeguru/app_test.py new file mode 100644 index 00000000..af47067f --- /dev/null +++ b/pe/qt/dupeguru/app_test.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python +""" +Unit Name: dupeguru.tests.app +Created By: Virgil Dupras +Created On: 2007-06-23 +Last modified by:$Author: virgil $ +Last modified on:$Date: 2009-05-28 16:02:48 +0200 (Thu, 28 May 2009) $ + $Revision: 4388 $ +Copyright 2007 Hardcoded Software (http://www.hardcoded.net) +""" +import unittest +import os + +from hsutil.testcase import TestCase +from hsutil import io +from hsutil.path import Path +from hsutil.decorators import log_calls +import hsfs as fs +import hsfs.phys +import hsutil.files +from hsutil.job import nulljob + +from . import data, app +from .app import DupeGuru as DupeGuruBase + +class DupeGuru(DupeGuruBase): + def __init__(self): + DupeGuruBase.__init__(self, data, '/tmp', appid=4) + + def _start_job(self, jobid, func): + func(nulljob) + + +class TCDupeGuru(TestCase): + cls_tested_module = app + def test_ApplyFilter_calls_results_apply_filter(self): + app = DupeGuru() + self.mock(app.results, 'apply_filter', log_calls(app.results.apply_filter)) + app.ApplyFilter('foo') + self.assertEqual(2, len(app.results.apply_filter.calls)) + call = app.results.apply_filter.calls[0] + self.assert_(call['filter_str'] is None) + call = app.results.apply_filter.calls[1] + self.assertEqual('foo', call['filter_str']) + + def test_ApplyFilter_escapes_regexp(self): + app = DupeGuru() + self.mock(app.results, 'apply_filter', log_calls(app.results.apply_filter)) + app.ApplyFilter('()[]\\.|+?^abc') + call = app.results.apply_filter.calls[1] + self.assertEqual('\\(\\)\\[\\]\\\\\\.\\|\\+\\?\\^abc', call['filter_str']) + app.ApplyFilter('(*)') # In "simple mode", we want the * to behave as a wilcard + call = app.results.apply_filter.calls[3] + self.assertEqual('\(.*\)', call['filter_str']) + app.options['escape_filter_regexp'] = False + app.ApplyFilter('(abc)') + call = app.results.apply_filter.calls[5] + self.assertEqual('(abc)', call['filter_str']) + + def test_CopyOrMove(self): + # The goal here is just to have a test for a previous blowup I had. I know my test coverage + # for this unit is pathetic. What's done is done. My approach now is to add tests for + # every change I want to make. The blowup was caused by a missing import. + dupe_parent = fs.Directory(None, 'foo') + dupe = fs.File(dupe_parent, 'bar') + dupe.copy = log_calls(lambda dest, newname: None) + self.mock(hsutil.files, 'copy', log_calls(lambda source_path, dest_path: None)) + self.mock(os, 'makedirs', lambda path: None) # We don't want the test to create that fake directory + self.mock(fs.phys, 'Directory', fs.Directory) # We don't want an error because makedirs didn't work + app = DupeGuru() + app.CopyOrMove(dupe, True, 'some_destination', 0) + self.assertEqual(1, len(hsutil.files.copy.calls)) + call = hsutil.files.copy.calls[0] + self.assertEqual('some_destination', call['dest_path']) + self.assertEqual(dupe.path, call['source_path']) + + def test_CopyOrMove_clean_empty_dirs(self): + tmppath = Path(self.tmpdir()) + sourcepath = tmppath + 'source' + io.mkdir(sourcepath) + io.open(sourcepath + 'myfile', 'w') + tmpdir = hsfs.phys.Directory(None, unicode(tmppath)) + myfile = tmpdir['source']['myfile'] + app = DupeGuru() + self.mock(app, 'clean_empty_dirs', log_calls(lambda path: None)) + app.CopyOrMove(myfile, False, tmppath + 'dest', 0) + calls = app.clean_empty_dirs.calls + self.assertEqual(1, len(calls)) + self.assertEqual(sourcepath, calls[0]['path']) + + def test_Scan_with_objects_evaluating_to_false(self): + # At some point, any() was used in a wrong way that made Scan() wrongly return 1 + app = DupeGuru() + f1, f2 = [fs.File(None, 'foo') for i in range(2)] + f1.is_ref, f2.is_ref = (False, False) + assert not (bool(f1) and bool(f2)) + app.directories.get_files = lambda: [f1, f2] + app.directories._dirs.append('this is just so Scan() doesnt return 3') + app.start_scanning() # no exception + + +class TCDupeGuru_clean_empty_dirs(TestCase): + cls_tested_module = app + def setUp(self): + self.mock(hsutil.files, 'delete_if_empty', log_calls(lambda path, files_to_delete=[]: None)) + self.app = DupeGuru() + + def test_option_off(self): + self.app.clean_empty_dirs(Path('/foo/bar')) + self.assertEqual(0, len(hsutil.files.delete_if_empty.calls)) + + def test_option_on(self): + self.app.options['clean_empty_dirs'] = True + self.app.clean_empty_dirs(Path('/foo/bar')) + calls = hsutil.files.delete_if_empty.calls + self.assertEqual(1, len(calls)) + self.assertEqual(Path('/foo/bar'), calls[0]['path']) + self.assertEqual(['.DS_Store'], calls[0]['files_to_delete']) + + def test_recurse_up(self): + # delete_if_empty must be recursively called up in the path until it returns False + @log_calls + def mock_delete_if_empty(path, files_to_delete=[]): + return len(path) > 1 + + self.mock(hsutil.files, 'delete_if_empty', mock_delete_if_empty) + self.app.options['clean_empty_dirs'] = True + self.app.clean_empty_dirs(Path('not-empty/empty/empty')) + calls = hsutil.files.delete_if_empty.calls + self.assertEqual(3, len(calls)) + self.assertEqual(Path('not-empty/empty/empty'), calls[0]['path']) + self.assertEqual(Path('not-empty/empty'), calls[1]['path']) + self.assertEqual(Path('not-empty'), calls[2]['path']) + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/pe/qt/dupeguru/data.py b/pe/qt/dupeguru/data.py new file mode 100644 index 00000000..568a3400 --- /dev/null +++ b/pe/qt/dupeguru/data.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python +""" +Unit Name: dupeguru.data +Created By: Virgil Dupras +Created On: 2006/03/15 +Last modified by:$Author: virgil $ +Last modified on:$Date: 2009-05-28 15:22:39 +0200 (Thu, 28 May 2009) $ + $Revision: 4385 $ +Copyright 2004-2006 Hardcoded Software (http://www.hardcoded.net) +""" + +from hsutil.str import format_time, FT_DECIMAL, format_size + +import time + +def format_path(p): + return unicode(p[:-1]) + +def format_timestamp(t, delta): + if delta: + return format_time(t, FT_DECIMAL) + else: + if t > 0: + return time.strftime('%Y/%m/%d %H:%M:%S', time.localtime(t)) + else: + return '---' + +def format_words(w): + def do_format(w): + if isinstance(w, list): + return '(%s)' % ', '.join(do_format(item) for item in w) + else: + return w.replace('\n', ' ') + + return ', '.join(do_format(item) for item in w) + +def format_perc(p): + return "%0.0f" % p + +def format_dupe_count(c): + return str(c) if c else '---' + +def cmp_value(value): + return value.lower() if isinstance(value, basestring) else value + +COLUMNS = [ + {'attr':'name','display':'Filename'}, + {'attr':'path','display':'Directory'}, + {'attr':'size','display':'Size (KB)'}, + {'attr':'extension','display':'Kind'}, + {'attr':'ctime','display':'Creation'}, + {'attr':'mtime','display':'Modification'}, + {'attr':'percentage','display':'Match %'}, + {'attr':'words','display':'Words Used'}, + {'attr':'dupe_count','display':'Dupe Count'}, +] + +def GetDisplayInfo(dupe, group, delta=False): + if (dupe is None) or (group is None): + return ['---'] * len(COLUMNS) + size = dupe.size + ctime = dupe.ctime + mtime = dupe.mtime + m = group.get_match_of(dupe) + if m: + percentage = m.percentage + dupe_count = 0 + if delta: + r = group.ref + size -= r.size + ctime -= r.ctime + mtime -= r.mtime + else: + percentage = group.percentage + dupe_count = len(group.dupes) + return [ + dupe.name, + format_path(dupe.path), + format_size(size, 0, 1, False), + dupe.extension, + format_timestamp(ctime, delta and m), + format_timestamp(mtime, delta and m), + format_perc(percentage), + format_words(dupe.words), + format_dupe_count(dupe_count) + ] + +def GetDupeSortKey(dupe, get_group, key, delta): + if key == 6: + m = get_group().get_match_of(dupe) + return m.percentage + if key == 8: + return 0 + r = cmp_value(getattr(dupe, COLUMNS[key]['attr'])) + if delta and (key in (2, 4, 5)): + r -= cmp_value(getattr(get_group().ref, COLUMNS[key]['attr'])) + return r + +def GetGroupSortKey(group, key): + if key == 6: + return group.percentage + if key == 8: + return len(group) + return cmp_value(getattr(group.ref, COLUMNS[key]['attr'])) + diff --git a/pe/qt/dupeguru/data_me.py b/pe/qt/dupeguru/data_me.py new file mode 100644 index 00000000..70d3ae66 --- /dev/null +++ b/pe/qt/dupeguru/data_me.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python +""" +Unit Name: dupeguru.data +Created By: Virgil Dupras +Created On: 2006/03/15 +Last modified by:$Author: virgil $ +Last modified on:$Date: 2009-05-28 15:22:39 +0200 (Thu, 28 May 2009) $ + $Revision: 4385 $ +Copyright 2004-2006 Hardcoded Software (http://www.hardcoded.net) +""" + +from hsutil.str import format_time, FT_MINUTES, format_size +from .data import (format_path, format_timestamp, format_words, format_perc, + format_dupe_count, cmp_value) + +COLUMNS = [ + {'attr':'name','display':'Filename'}, + {'attr':'path','display':'Directory'}, + {'attr':'size','display':'Size (MB)'}, + {'attr':'duration','display':'Time'}, + {'attr':'bitrate','display':'Bitrate'}, + {'attr':'samplerate','display':'Sample Rate'}, + {'attr':'extension','display':'Kind'}, + {'attr':'ctime','display':'Creation'}, + {'attr':'mtime','display':'Modification'}, + {'attr':'title','display':'Title'}, + {'attr':'artist','display':'Artist'}, + {'attr':'album','display':'Album'}, + {'attr':'genre','display':'Genre'}, + {'attr':'year','display':'Year'}, + {'attr':'track','display':'Track Number'}, + {'attr':'comment','display':'Comment'}, + {'attr':'percentage','display':'Match %'}, + {'attr':'words','display':'Words Used'}, + {'attr':'dupe_count','display':'Dupe Count'}, +] + +def GetDisplayInfo(dupe, group, delta=False): + if (dupe is None) or (group is None): + return ['---'] * len(COLUMNS) + size = dupe.size + duration = dupe.duration + bitrate = dupe.bitrate + samplerate = dupe.samplerate + ctime = dupe.ctime + mtime = dupe.mtime + m = group.get_match_of(dupe) + if m: + percentage = m.percentage + dupe_count = 0 + if delta: + r = group.ref + size -= r.size + duration -= r.duration + bitrate -= r.bitrate + samplerate -= r.samplerate + ctime -= r.ctime + mtime -= r.mtime + else: + percentage = group.percentage + dupe_count = len(group.dupes) + return [ + dupe.name, + format_path(dupe.path), + format_size(size, 2, 2, False), + format_time(duration, FT_MINUTES), + str(bitrate), + str(samplerate), + dupe.extension, + format_timestamp(ctime,delta and m), + format_timestamp(mtime,delta and m), + dupe.title, + dupe.artist, + dupe.album, + dupe.genre, + dupe.year, + str(dupe.track), + dupe.comment, + format_perc(percentage), + format_words(dupe.words), + format_dupe_count(dupe_count) + ] + +def GetDupeSortKey(dupe, get_group, key, delta): + if key == 16: + m = get_group().get_match_of(dupe) + return m.percentage + if key == 18: + return 0 + r = cmp_value(getattr(dupe, COLUMNS[key]['attr'])) + if delta and (key in (2, 3, 4, 7, 8)): + r -= cmp_value(getattr(get_group().ref, COLUMNS[key]['attr'])) + return r + +def GetGroupSortKey(group, key): + if key == 16: + return group.percentage + if key == 18: + return len(group) + return cmp_value(getattr(group.ref, COLUMNS[key]['attr'])) diff --git a/pe/qt/dupeguru/data_pe.py b/pe/qt/dupeguru/data_pe.py new file mode 100644 index 00000000..94bdd99d --- /dev/null +++ b/pe/qt/dupeguru/data_pe.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python +""" +Unit Name: dupeguru.data +Created By: Virgil Dupras +Created On: 2006/03/15 +Last modified by:$Author: virgil $ +Last modified on:$Date: 2009-05-28 15:22:39 +0200 (Thu, 28 May 2009) $ + $Revision: 4385 $ +Copyright 2004-2006 Hardcoded Software (http://www.hardcoded.net) +""" +from hsutil.str import format_size +from .data import format_path, format_timestamp, format_perc, format_dupe_count, cmp_value + +def format_dimensions(dimensions): + return '%d x %d' % (dimensions[0], dimensions[1]) + +COLUMNS = [ + {'attr':'name','display':'Filename'}, + {'attr':'path','display':'Directory'}, + {'attr':'size','display':'Size (KB)'}, + {'attr':'extension','display':'Kind'}, + {'attr':'dimensions','display':'Dimensions'}, + {'attr':'ctime','display':'Creation'}, + {'attr':'mtime','display':'Modification'}, + {'attr':'percentage','display':'Match %'}, + {'attr':'dupe_count','display':'Dupe Count'}, +] + +def GetDisplayInfo(dupe,group,delta=False): + if (dupe is None) or (group is None): + return ['---'] * len(COLUMNS) + size = dupe.size + ctime = dupe.ctime + mtime = dupe.mtime + m = group.get_match_of(dupe) + if m: + percentage = m.percentage + dupe_count = 0 + if delta: + r = group.ref + size -= r.size + ctime -= r.ctime + mtime -= r.mtime + else: + percentage = group.percentage + dupe_count = len(group.dupes) + dupe_path = getattr(dupe, 'display_path', dupe.path) + return [ + dupe.name, + format_path(dupe_path), + format_size(size, 0, 1, False), + dupe.extension, + format_dimensions(dupe.dimensions), + format_timestamp(ctime, delta and m), + format_timestamp(mtime, delta and m), + format_perc(percentage), + format_dupe_count(dupe_count) + ] + +def GetDupeSortKey(dupe, get_group, key, delta): + if key == 7: + m = get_group().get_match_of(dupe) + return m.percentage + if key == 8: + return 0 + r = cmp_value(getattr(dupe, COLUMNS[key]['attr'])) + if delta and (key in (2, 5, 6)): + r -= cmp_value(getattr(get_group().ref, COLUMNS[key]['attr'])) + return r + +def GetGroupSortKey(group, key): + if key == 7: + return group.percentage + if key == 8: + return len(group) + return cmp_value(getattr(group.ref, COLUMNS[key]['attr'])) + diff --git a/pe/qt/dupeguru/directories.py b/pe/qt/dupeguru/directories.py new file mode 100644 index 00000000..3d73b5c5 --- /dev/null +++ b/pe/qt/dupeguru/directories.py @@ -0,0 +1,161 @@ +#!/usr/bin/env python +""" +Unit Name: dupeguru.directories +Created By: Virgil Dupras +Created On: 2006/02/27 +Last modified by:$Author: virgil $ +Last modified on:$Date: 2009-05-28 16:02:48 +0200 (Thu, 28 May 2009) $ + $Revision: 4388 $ +Copyright 2004-2006 Hardcoded Software (http://www.hardcoded.net) +""" +import xml.dom.minidom + +from hsfs import phys +import hsfs as fs +from hsutil.files import FileOrPath +from hsutil.path import Path + +(STATE_NORMAL, +STATE_REFERENCE, +STATE_EXCLUDED) = range(3) + +class AlreadyThereError(Exception): + """The path being added is already in the directory list""" + +class InvalidPathError(Exception): + """The path being added is invalid""" + +class Directories(object): + #---Override + def __init__(self): + self._dirs = [] + self.states = {} + self.dirclass = phys.Directory + self.special_dirclasses = {} + + def __contains__(self,path): + for d in self._dirs: + if path in d.path: + return True + return False + + def __delitem__(self,key): + self._dirs.__delitem__(key) + + def __getitem__(self,key): + return self._dirs.__getitem__(key) + + def __len__(self): + return len(self._dirs) + + #---Private + def _get_files(self, from_dir, state=STATE_NORMAL): + state = self.states.get(from_dir.path, state) + result = [] + for subdir in from_dir.dirs: + for file in self._get_files(subdir, state): + yield file + if state != STATE_EXCLUDED: + for file in from_dir.files: + file.is_ref = state == STATE_REFERENCE + yield file + + #---Public + def add_path(self, path): + """Adds 'path' to self, if not already there. + + Raises AlreadyThereError if 'path' is already in self. If path is a directory containing + some of the directories already present in self, 'path' will be added, but all directories + under it will be removed. Can also raise InvalidPathError if 'path' does not exist. + """ + if path in self: + raise AlreadyThereError + self._dirs = [d for d in self._dirs if d.path not in path] + try: + dirclass = self.special_dirclasses.get(path, self.dirclass) + d = dirclass(None, unicode(path)) + d[:] #If an InvalidPath exception has to be raised, it will be raised here + self._dirs.append(d) + return d + except fs.InvalidPath: + raise InvalidPathError + + def get_files(self): + """Returns a list of all files that are not excluded. + + Returned files also have their 'is_ref' attr set. + """ + for d in self._dirs: + d.force_update() + try: + for file in self._get_files(d): + yield file + except fs.InvalidPath: + pass + + def GetState(self, path): + """Returns the state of 'path' (One of the STATE_* const.) + + Raises LookupError if 'path' is not in self. + """ + if path not in self: + raise LookupError("The path '%s' is not in the directory list." % str(path)) + try: + return self.states[path] + except KeyError: + if path[-1].startswith('.'): # hidden + return STATE_EXCLUDED + parent = path[:-1] + if parent in self: + return self.GetState(parent) + else: + return STATE_NORMAL + + def LoadFromFile(self,infile): + try: + doc = xml.dom.minidom.parse(infile) + except: + return + root_dir_nodes = doc.getElementsByTagName('root_directory') + for rdn in root_dir_nodes: + if not rdn.getAttributeNode('path'): + continue + path = rdn.getAttributeNode('path').nodeValue + try: + self.add_path(Path(path)) + except (AlreadyThereError,InvalidPathError): + pass + state_nodes = doc.getElementsByTagName('state') + for sn in state_nodes: + if not (sn.getAttributeNode('path') and sn.getAttributeNode('value')): + continue + path = sn.getAttributeNode('path').nodeValue + state = sn.getAttributeNode('value').nodeValue + self.SetState(Path(path), int(state)) + + def Remove(self,directory): + self._dirs.remove(directory) + + def SaveToFile(self,outfile): + with FileOrPath(outfile, 'wb') as fp: + doc = xml.dom.minidom.Document() + root = doc.appendChild(doc.createElement('directories')) + for root_dir in self: + root_dir_node = root.appendChild(doc.createElement('root_directory')) + root_dir_node.setAttribute('path', unicode(root_dir.path).encode('utf-8')) + for path,state in self.states.iteritems(): + state_node = root.appendChild(doc.createElement('state')) + state_node.setAttribute('path', unicode(path).encode('utf-8')) + state_node.setAttribute('value', str(state)) + doc.writexml(fp,'\t','\t','\n',encoding='utf-8') + + def SetState(self,path,state): + try: + if self.GetState(path) == state: + return + self.states[path] = state + if (self.GetState(path[:-1]) == state) and (not path[-1].startswith('.')): + del self.states[path] + except LookupError: + pass + diff --git a/pe/qt/dupeguru/directories_test.py b/pe/qt/dupeguru/directories_test.py new file mode 100644 index 00000000..7d34c343 --- /dev/null +++ b/pe/qt/dupeguru/directories_test.py @@ -0,0 +1,280 @@ +#!/usr/bin/env python +""" +Unit Name: dupeguru.tests.directories +Created By: Virgil Dupras +Created On: 2006/02/27 +Last modified by:$Author: virgil $ +Last modified on:$Date: 2009-05-29 08:51:14 +0200 (Fri, 29 May 2009) $ + $Revision: 4398 $ +Copyright 2004-2006 Hardcoded Software (http://www.hardcoded.net) +""" +import unittest +import os.path as op +import os +import time +import shutil + +from hsutil import job, io +from hsutil.path import Path +from hsutil.testcase import TestCase +import hsfs.phys +from hsfs.phys import phys_test + +from directories import * + +testpath = Path(TestCase.datadirpath()) + +class TCDirectories(TestCase): + def test_empty(self): + d = Directories() + self.assertEqual(0,len(d)) + self.assert_('foobar' not in d) + + def test_add_path(self): + d = Directories() + p = testpath + 'utils' + added = d.add_path(p) + self.assertEqual(1,len(d)) + self.assert_(p in d) + self.assert_((p + 'foobar') in d) + self.assert_(p[:-1] not in d) + self.assertEqual(p,added.path) + self.assert_(d[0] is added) + p = self.tmppath() + d.add_path(p) + self.assertEqual(2,len(d)) + self.assert_(p in d) + + def test_AddPath_when_path_is_already_there(self): + d = Directories() + p = testpath + 'utils' + d.add_path(p) + self.assertRaises(AlreadyThereError, d.add_path, p) + self.assertRaises(AlreadyThereError, d.add_path, p + 'foobar') + self.assertEqual(1, len(d)) + + def test_AddPath_containing_paths_already_there(self): + d = Directories() + d.add_path(testpath + 'utils') + self.assertEqual(1, len(d)) + added = d.add_path(testpath) + self.assertEqual(1, len(d)) + self.assert_(added is d[0]) + + def test_AddPath_non_latin(self): + p = Path(self.tmpdir()) + to_add = p + u'unicode\u201a' + os.mkdir(unicode(to_add)) + d = Directories() + try: + d.add_path(to_add) + except UnicodeDecodeError: + self.fail() + + def test_del(self): + d = Directories() + d.add_path(testpath + 'utils') + try: + del d[1] + self.fail() + except IndexError: + pass + d.add_path(self.tmppath()) + del d[1] + self.assertEqual(1, len(d)) + + def test_states(self): + d = Directories() + p = testpath + 'utils' + d.add_path(p) + self.assertEqual(STATE_NORMAL,d.GetState(p)) + d.SetState(p,STATE_REFERENCE) + self.assertEqual(STATE_REFERENCE,d.GetState(p)) + self.assertEqual(STATE_REFERENCE,d.GetState(p + 'dir1')) + self.assertEqual(1,len(d.states)) + self.assertEqual(p,d.states.keys()[0]) + self.assertEqual(STATE_REFERENCE,d.states[p]) + + def test_GetState_with_path_not_there(self): + d = Directories() + d.add_path(testpath + 'utils') + self.assertRaises(LookupError,d.GetState,testpath) + + def test_states_remain_when_larger_directory_eat_smaller_ones(self): + d = Directories() + p = testpath + 'utils' + d.add_path(p) + d.SetState(p,STATE_EXCLUDED) + d.add_path(testpath) + d.SetState(testpath,STATE_REFERENCE) + self.assertEqual(STATE_EXCLUDED,d.GetState(p)) + self.assertEqual(STATE_EXCLUDED,d.GetState(p + 'dir1')) + self.assertEqual(STATE_REFERENCE,d.GetState(testpath)) + + def test_SetState_keep_state_dict_size_to_minimum(self): + d = Directories() + p = Path(phys_test.create_fake_fs(self.tmpdir())) + d.add_path(p) + d.SetState(p,STATE_REFERENCE) + d.SetState(p + 'dir1',STATE_REFERENCE) + self.assertEqual(1,len(d.states)) + self.assertEqual(STATE_REFERENCE,d.GetState(p + 'dir1')) + d.SetState(p + 'dir1',STATE_NORMAL) + self.assertEqual(2,len(d.states)) + self.assertEqual(STATE_NORMAL,d.GetState(p + 'dir1')) + d.SetState(p + 'dir1',STATE_REFERENCE) + self.assertEqual(1,len(d.states)) + self.assertEqual(STATE_REFERENCE,d.GetState(p + 'dir1')) + + def test_get_files(self): + d = Directories() + p = Path(phys_test.create_fake_fs(self.tmpdir())) + d.add_path(p) + d.SetState(p + 'dir1',STATE_REFERENCE) + d.SetState(p + 'dir2',STATE_EXCLUDED) + files = d.get_files() + self.assertEqual(5, len(list(files))) + for f in files: + if f.parent.path == p + 'dir1': + self.assert_(f.is_ref) + else: + self.assert_(not f.is_ref) + + def test_get_files_with_inherited_exclusion(self): + d = Directories() + p = testpath + 'utils' + d.add_path(p) + d.SetState(p,STATE_EXCLUDED) + self.assertEqual([], list(d.get_files())) + + def test_save_and_load(self): + d1 = Directories() + d2 = Directories() + p1 = self.tmppath() + p2 = self.tmppath() + d1.add_path(p1) + d1.add_path(p2) + d1.SetState(p1, STATE_REFERENCE) + d1.SetState(p1 + 'dir1',STATE_EXCLUDED) + tmpxml = op.join(self.tmpdir(), 'directories_testunit.xml') + d1.SaveToFile(tmpxml) + d2.LoadFromFile(tmpxml) + self.assertEqual(2, len(d2)) + self.assertEqual(STATE_REFERENCE,d2.GetState(p1)) + self.assertEqual(STATE_EXCLUDED,d2.GetState(p1 + 'dir1')) + + def test_invalid_path(self): + d = Directories() + p = Path('does_not_exist') + self.assertRaises(InvalidPathError, d.add_path, p) + self.assertEqual(0, len(d)) + + def test_SetState_on_invalid_path(self): + d = Directories() + try: + d.SetState(Path('foobar',),STATE_NORMAL) + except LookupError: + self.fail() + + def test_default_dirclass(self): + self.assert_(Directories().dirclass is hsfs.phys.Directory) + + def test_dirclass(self): + class MySpecialDirclass(hsfs.phys.Directory): pass + d = Directories() + d.dirclass = MySpecialDirclass + d.add_path(testpath) + self.assert_(isinstance(d[0], MySpecialDirclass)) + + def test_LoadFromFile_with_invalid_path(self): + #This test simulates a load from file resulting in a + #InvalidPath raise. Other directories must be loaded. + d1 = Directories() + d1.add_path(testpath + 'utils') + #Will raise InvalidPath upon loading + d1.add_path(self.tmppath()).name = 'does_not_exist' + tmpxml = op.join(self.tmpdir(), 'directories_testunit.xml') + d1.SaveToFile(tmpxml) + d2 = Directories() + d2.LoadFromFile(tmpxml) + self.assertEqual(1, len(d2)) + + def test_LoadFromFile_with_same_paths(self): + #This test simulates a load from file resulting in a + #AlreadyExists raise. Other directories must be loaded. + d1 = Directories() + p1 = self.tmppath() + p2 = self.tmppath() + d1.add_path(p1) + d1.add_path(p2) + #Will raise AlreadyExists upon loading + d1.add_path(self.tmppath()).name = unicode(p1) + tmpxml = op.join(self.tmpdir(), 'directories_testunit.xml') + d1.SaveToFile(tmpxml) + d2 = Directories() + d2.LoadFromFile(tmpxml) + self.assertEqual(2, len(d2)) + + def test_Remove(self): + d = Directories() + d1 = d.add_path(self.tmppath()) + d2 = d.add_path(self.tmppath()) + d.Remove(d1) + self.assertEqual(1, len(d)) + self.assert_(d[0] is d2) + + def test_unicode_save(self): + d = Directories() + p1 = self.tmppath() + u'hello\xe9' + io.mkdir(p1) + io.mkdir(p1 + u'foo\xe9') + d.add_path(p1) + d.SetState(d[0][0].path, STATE_EXCLUDED) + tmpxml = op.join(self.tmpdir(), 'directories_testunit.xml') + try: + d.SaveToFile(tmpxml) + except UnicodeDecodeError: + self.fail() + + def test_get_files_refreshes_its_directories(self): + d = Directories() + p = Path(phys_test.create_fake_fs(self.tmpdir())) + d.add_path(p) + files = d.get_files() + self.assertEqual(6, len(list(files))) + time.sleep(1) + os.remove(str(p + ('dir1','file1.test'))) + files = d.get_files() + self.assertEqual(5, len(list(files))) + + def test_get_files_does_not_choke_on_non_existing_directories(self): + d = Directories() + p = Path(self.tmpdir()) + d.add_path(p) + io.rmtree(p) + self.assertEqual([], list(d.get_files())) + + def test_GetState_returns_excluded_by_default_for_hidden_directories(self): + d = Directories() + p = Path(self.tmpdir()) + hidden_dir_path = p + '.foo' + io.mkdir(p + '.foo') + d.add_path(p) + self.assertEqual(d.GetState(hidden_dir_path), STATE_EXCLUDED) + # But it can be overriden + d.SetState(hidden_dir_path, STATE_NORMAL) + self.assertEqual(d.GetState(hidden_dir_path), STATE_NORMAL) + + def test_special_dirclasses(self): + # if a path is in special_dirclasses, use this class instead + class MySpecialDirclass(hsfs.phys.Directory): pass + d = Directories() + p1 = self.tmppath() + p2 = self.tmppath() + d.special_dirclasses[p1] = MySpecialDirclass + self.assert_(isinstance(d.add_path(p2), hsfs.phys.Directory)) + self.assert_(isinstance(d.add_path(p1), MySpecialDirclass)) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/pe/qt/dupeguru/engine.py b/pe/qt/dupeguru/engine.py new file mode 100644 index 00000000..a826902d --- /dev/null +++ b/pe/qt/dupeguru/engine.py @@ -0,0 +1,360 @@ +#!/usr/bin/env python +""" +Unit Name: dupeguru.engine +Created By: Virgil Dupras +Created On: 2006/01/29 +Last modified by:$Author: virgil $ +Last modified on:$Date: $ + $Revision: $ +Copyright 2007 Hardcoded Software (http://www.hardcoded.net) +""" +from __future__ import division +import difflib +import logging +import string +from collections import defaultdict, namedtuple +from unicodedata import normalize + +from hsutil.str import multi_replace +from hsutil import job + +(WEIGHT_WORDS, +MATCH_SIMILAR_WORDS, +NO_FIELD_ORDER) = range(3) + +JOB_REFRESH_RATE = 100 + +def getwords(s): + if isinstance(s, unicode): + s = normalize('NFD', s) + s = multi_replace(s, "-_&+():;\\[]{}.,<>/?~!@#$*", ' ').lower() + s = ''.join(c for c in s if c in string.ascii_letters + string.digits + string.whitespace) + return filter(None, s.split(' ')) # filter() is to remove empty elements + +def getfields(s): + fields = [getwords(field) for field in s.split(' - ')] + return filter(None, fields) + +def unpack_fields(fields): + result = [] + for field in fields: + if isinstance(field, list): + result += field + else: + result.append(field) + return result + +def compare(first, second, flags=()): + """Returns the % of words that match between first and second + + The result is a int in the range 0..100. + First and second can be either a string or a list. + """ + if not (first and second): + return 0 + if any(isinstance(element, list) for element in first): + return compare_fields(first, second, flags) + second = second[:] #We must use a copy of second because we remove items from it + match_similar = MATCH_SIMILAR_WORDS in flags + weight_words = WEIGHT_WORDS in flags + joined = first + second + total_count = (sum(len(word) for word in joined) if weight_words else len(joined)) + match_count = 0 + in_order = True + for word in first: + if match_similar and (word not in second): + similar = difflib.get_close_matches(word, second, 1, 0.8) + if similar: + word = similar[0] + if word in second: + if second[0] != word: + in_order = False + second.remove(word) + match_count += (len(word) if weight_words else 1) + result = round(((match_count * 2) / total_count) * 100) + if (result == 100) and (not in_order): + result = 99 # We cannot consider a match exact unless the ordering is the same + return result + +def compare_fields(first, second, flags=()): + """Returns the score for the lowest matching fields. + + first and second must be lists of lists of string. + """ + if len(first) != len(second): + return 0 + if NO_FIELD_ORDER in flags: + results = [] + #We don't want to remove field directly in the list. We must work on a copy. + second = second[:] + for field1 in first: + max = 0 + matched_field = None + for field2 in second: + r = compare(field1, field2, flags) + if r > max: + max = r + matched_field = field2 + results.append(max) + if matched_field: + second.remove(matched_field) + else: + results = [compare(word1, word2, flags) for word1, word2 in zip(first, second)] + return min(results) if results else 0 + +def build_word_dict(objects, j=job.nulljob): + """Returns a dict of objects mapped by their words. + + objects must have a 'words' attribute being a list of strings or a list of lists of strings. + + The result will be a dict with words as keys, lists of objects as values. + """ + result = defaultdict(set) + for object in j.iter_with_progress(objects, 'Prepared %d/%d files', JOB_REFRESH_RATE): + for word in unpack_fields(object.words): + result[word].add(object) + return result + +def merge_similar_words(word_dict): + """Take all keys in word_dict that are similar, and merge them together. + """ + keys = word_dict.keys() + keys.sort(key=len)# we want the shortest word to stay + while keys: + key = keys.pop(0) + similars = difflib.get_close_matches(key, keys, 100, 0.8) + if not similars: + continue + objects = word_dict[key] + for similar in similars: + objects |= word_dict[similar] + del word_dict[similar] + keys.remove(similar) + +def reduce_common_words(word_dict, threshold): + """Remove all objects from word_dict values where the object count >= threshold + + The exception to this removal are the objects where all the words of the object are common. + Because if we remove them, we will miss some duplicates! + """ + uncommon_words = set(word for word, objects in word_dict.items() if len(objects) < threshold) + for word, objects in word_dict.items(): + if len(objects) < threshold: + continue + reduced = set() + for o in objects: + if not any(w in uncommon_words for w in unpack_fields(o.words)): + reduced.add(o) + if reduced: + word_dict[word] = reduced + else: + del word_dict[word] + +Match = namedtuple('Match', 'first second percentage') +def get_match(first, second, flags=()): + #it is assumed here that first and second both have a "words" attribute + percentage = compare(first.words, second.words, flags) + return Match(first, second, percentage) + +class MatchFactory(object): + common_word_threshold = 50 + match_similar_words = False + min_match_percentage = 0 + weight_words = False + no_field_order = False + limit = 5000000 + + def getmatches(self, objects, j=job.nulljob): + j = j.start_subjob(2) + sj = j.start_subjob(2) + for o in objects: + if not hasattr(o, 'words'): + o.words = getwords(o.name) + word_dict = build_word_dict(objects, sj) + reduce_common_words(word_dict, self.common_word_threshold) + if self.match_similar_words: + merge_similar_words(word_dict) + match_flags = [] + if self.weight_words: + match_flags.append(WEIGHT_WORDS) + if self.match_similar_words: + match_flags.append(MATCH_SIMILAR_WORDS) + if self.no_field_order: + match_flags.append(NO_FIELD_ORDER) + j.start_job(len(word_dict), '0 matches found') + compared = defaultdict(set) + result = [] + try: + # This whole 'popping' thing is there to avoid taking too much memory at the same time. + while word_dict: + items = word_dict.popitem()[1] + while items: + ref = items.pop() + compared_already = compared[ref] + to_compare = items - compared_already + compared_already |= to_compare + for other in to_compare: + m = get_match(ref, other, match_flags) + if m.percentage >= self.min_match_percentage: + result.append(m) + if len(result) >= self.limit: + return result + j.add_progress(desc='%d matches found' % len(result)) + except MemoryError: + # This is the place where the memory usage is at its peak during the scan. + # Just continue the process with an incomplete list of matches. + del compared # This should give us enough room to call logging. + logging.warning('Memory Overflow. Matches: %d. Word dict: %d' % (len(result), len(word_dict))) + return result + return result + + +class Group(object): + #---Override + def __init__(self): + self._clear() + + def __contains__(self, item): + return item in self.unordered + + def __getitem__(self, key): + return self.ordered.__getitem__(key) + + def __iter__(self): + return iter(self.ordered) + + def __len__(self): + return len(self.ordered) + + #---Private + def _clear(self): + self._percentage = None + self._matches_for_ref = None + self.matches = set() + self.candidates = defaultdict(set) + self.ordered = [] + self.unordered = set() + + def _get_matches_for_ref(self): + if self._matches_for_ref is None: + ref = self.ref + self._matches_for_ref = [match for match in self.matches if ref in match] + return self._matches_for_ref + + #---Public + def add_match(self, match): + def add_candidate(item, match): + matches = self.candidates[item] + matches.add(match) + if self.unordered <= matches: + self.ordered.append(item) + self.unordered.add(item) + + if match in self.matches: + return + self.matches.add(match) + first, second, _ = match + if first not in self.unordered: + add_candidate(first, second) + if second not in self.unordered: + add_candidate(second, first) + self._percentage = None + self._matches_for_ref = None + + def clean_matches(self): + self.matches = set(m for m in self.matches if (m.first in self.unordered) and (m.second in self.unordered)) + self.candidates = defaultdict(set) + + def get_match_of(self, item): + if item is self.ref: + return + for m in self._get_matches_for_ref(): + if item in m: + return m + + def prioritize(self, key_func, tie_breaker=None): + # tie_breaker(ref, dupe) --> True if dupe should be ref + self.ordered.sort(key=key_func) + if tie_breaker is None: + return + ref = self.ref + key_value = key_func(ref) + for dupe in self.dupes: + if key_func(dupe) != key_value: + break + if tie_breaker(ref, dupe): + ref = dupe + if ref is not self.ref: + self.switch_ref(ref) + + def remove_dupe(self, item, clean_matches=True): + try: + self.ordered.remove(item) + self.unordered.remove(item) + self._percentage = None + self._matches_for_ref = None + if (len(self) > 1) and any(not getattr(item, 'is_ref', False) for item in self): + if clean_matches: + self.matches = set(m for m in self.matches if item not in m) + else: + self._clear() + except ValueError: + pass + + def switch_ref(self, with_dupe): + try: + self.ordered.remove(with_dupe) + self.ordered.insert(0, with_dupe) + self._percentage = None + self._matches_for_ref = None + except ValueError: + pass + + dupes = property(lambda self: self[1:]) + + @property + def percentage(self): + if self._percentage is None: + if self.dupes: + matches = self._get_matches_for_ref() + self._percentage = sum(match.percentage for match in matches) // len(matches) + else: + self._percentage = 0 + return self._percentage + + @property + def ref(self): + if self: + return self[0] + + +def get_groups(matches, j=job.nulljob): + matches.sort(key=lambda match: -match.percentage) + dupe2group = {} + groups = [] + for match in j.iter_with_progress(matches, 'Grouped %d/%d matches', JOB_REFRESH_RATE): + first, second, _ = match + first_group = dupe2group.get(first) + second_group = dupe2group.get(second) + if first_group: + if second_group: + if first_group is second_group: + target_group = first_group + else: + continue + else: + target_group = first_group + dupe2group[second] = target_group + else: + if second_group: + target_group = second_group + dupe2group[first] = target_group + else: + target_group = Group() + groups.append(target_group) + dupe2group[first] = target_group + dupe2group[second] = target_group + target_group.add_match(match) + for group in groups: + group.clean_matches() + return groups diff --git a/pe/qt/dupeguru/engine_test.py b/pe/qt/dupeguru/engine_test.py new file mode 100644 index 00000000..8e9706d9 --- /dev/null +++ b/pe/qt/dupeguru/engine_test.py @@ -0,0 +1,822 @@ +#!/usr/bin/env python +""" +Unit Name: dupeguru.engine_test +Created By: Virgil Dupras +Created On: 2006/01/29 +Last modified by:$Author: virgil $ +Last modified on:$Date: $ + $Revision: $ +Copyright 2004-2008 Hardcoded Software (http://www.hardcoded.net) +""" +import unittest +import sys + +from hsutil import job +from hsutil.decorators import log_calls +from hsutil.testcase import TestCase + +from . import engine +from .engine import * + +class NamedObject(object): + def __init__(self, name="foobar", with_words=False): + self.name = name + if with_words: + self.words = getwords(name) + + +def get_match_triangle(): + o1 = NamedObject(with_words=True) + o2 = NamedObject(with_words=True) + o3 = NamedObject(with_words=True) + m1 = get_match(o1,o2) + m2 = get_match(o1,o3) + m3 = get_match(o2,o3) + return [m1, m2, m3] + +def get_test_group(): + m1, m2, m3 = get_match_triangle() + result = Group() + result.add_match(m1) + result.add_match(m2) + result.add_match(m3) + return result + +class TCgetwords(TestCase): + def test_spaces(self): + self.assertEqual(['a', 'b', 'c', 'd'], getwords("a b c d")) + self.assertEqual(['a', 'b', 'c', 'd'], getwords(" a b c d ")) + + def test_splitter_chars(self): + self.assertEqual( + [chr(i) for i in xrange(ord('a'),ord('z')+1)], + getwords("a-b_c&d+e(f)g;h\\i[j]k{l}m:n.o,pr/s?t~u!v@w#x$y*z") + ) + + def test_joiner_chars(self): + self.assertEqual(["aec"], getwords(u"a'e\u0301c")) + + def test_empty(self): + self.assertEqual([], getwords('')) + + def test_returns_lowercase(self): + self.assertEqual(['foo', 'bar'], getwords('FOO BAR')) + + def test_decompose_unicode(self): + self.assertEqual(getwords(u'foo\xe9bar'), ['fooebar']) + + +class TCgetfields(TestCase): + def test_simple(self): + self.assertEqual([['a', 'b'], ['c', 'd', 'e']], getfields('a b - c d e')) + + def test_empty(self): + self.assertEqual([], getfields('')) + + def test_cleans_empty_fields(self): + expected = [['a', 'bc', 'def']] + actual = getfields(' - a bc def') + self.assertEqual(expected, actual) + expected = [['bc', 'def']] + + +class TCunpack_fields(TestCase): + def test_with_fields(self): + expected = ['a', 'b', 'c', 'd', 'e', 'f'] + actual = unpack_fields([['a'], ['b', 'c'], ['d', 'e', 'f']]) + self.assertEqual(expected, actual) + + def test_without_fields(self): + expected = ['a', 'b', 'c', 'd', 'e', 'f'] + actual = unpack_fields(['a', 'b', 'c', 'd', 'e', 'f']) + self.assertEqual(expected, actual) + + def test_empty(self): + self.assertEqual([], unpack_fields([])) + + +class TCWordCompare(TestCase): + def test_list(self): + self.assertEqual(100, compare(['a', 'b', 'c', 'd'],['a', 'b', 'c', 'd'])) + self.assertEqual(86, compare(['a', 'b', 'c', 'd'],['a', 'b', 'c'])) + + def test_unordered(self): + #Sometimes, users don't want fuzzy matching too much When they set the slider + #to 100, they don't expect a filename with the same words, but not the same order, to match. + #Thus, we want to return 99 in that case. + self.assertEqual(99, compare(['a', 'b', 'c', 'd'], ['d', 'b', 'c', 'a'])) + + def test_word_occurs_twice(self): + #if a word occurs twice in first, but once in second, we want the word to be only counted once + self.assertEqual(89, compare(['a', 'b', 'c', 'd', 'a'], ['d', 'b', 'c', 'a'])) + + def test_uses_copy_of_lists(self): + first = ['foo', 'bar'] + second = ['bar', 'bleh'] + compare(first, second) + self.assertEqual(['foo', 'bar'], first) + self.assertEqual(['bar', 'bleh'], second) + + def test_word_weight(self): + self.assertEqual(int((6.0 / 13.0) * 100), compare(['foo', 'bar'], ['bar', 'bleh'], (WEIGHT_WORDS, ))) + + def test_similar_words(self): + self.assertEqual(100, compare(['the', 'white', 'stripes'],['the', 'whites', 'stripe'], (MATCH_SIMILAR_WORDS, ))) + + def test_empty(self): + self.assertEqual(0, compare([], [])) + + def test_with_fields(self): + self.assertEqual(67, compare([['a', 'b'], ['c', 'd', 'e']], [['a', 'b'], ['c', 'd', 'f']])) + + def test_propagate_flags_with_fields(self): + def mock_compare(first, second, flags): + self.assertEqual((0, 1, 2, 3, 5), flags) + + self.mock(engine, 'compare_fields', mock_compare) + compare([['a']], [['a']], (0, 1, 2, 3, 5)) + + +class TCWordCompareWithFields(TestCase): + def test_simple(self): + self.assertEqual(67, compare_fields([['a', 'b'], ['c', 'd', 'e']], [['a', 'b'], ['c', 'd', 'f']])) + + def test_empty(self): + self.assertEqual(0, compare_fields([], [])) + + def test_different_length(self): + self.assertEqual(0, compare_fields([['a'], ['b']], [['a'], ['b'], ['c']])) + + def test_propagates_flags(self): + def mock_compare(first, second, flags): + self.assertEqual((0, 1, 2, 3, 5), flags) + + self.mock(engine, 'compare_fields', mock_compare) + compare_fields([['a']], [['a']],(0, 1, 2, 3, 5)) + + def test_order(self): + first = [['a', 'b'], ['c', 'd', 'e']] + second = [['c', 'd', 'f'], ['a', 'b']] + self.assertEqual(0, compare_fields(first, second)) + + def test_no_order(self): + first = [['a','b'],['c','d','e']] + second = [['c','d','f'],['a','b']] + self.assertEqual(67, compare_fields(first, second, (NO_FIELD_ORDER, ))) + first = [['a','b'],['a','b']] #a field can only be matched once. + second = [['c','d','f'],['a','b']] + self.assertEqual(0, compare_fields(first, second, (NO_FIELD_ORDER, ))) + first = [['a','b'],['a','b','c']] + second = [['c','d','f'],['a','b']] + self.assertEqual(33, compare_fields(first, second, (NO_FIELD_ORDER, ))) + + def test_compare_fields_without_order_doesnt_alter_fields(self): + #The NO_ORDER comp type altered the fields! + first = [['a','b'],['c','d','e']] + second = [['c','d','f'],['a','b']] + self.assertEqual(67, compare_fields(first, second, (NO_FIELD_ORDER, ))) + self.assertEqual([['a','b'],['c','d','e']],first) + self.assertEqual([['c','d','f'],['a','b']],second) + + +class TCbuild_word_dict(TestCase): + def test_with_standard_words(self): + l = [NamedObject('foo bar',True)] + l.append(NamedObject('bar baz',True)) + l.append(NamedObject('baz bleh foo',True)) + d = build_word_dict(l) + self.assertEqual(4,len(d)) + self.assertEqual(2,len(d['foo'])) + self.assert_(l[0] in d['foo']) + self.assert_(l[2] in d['foo']) + self.assertEqual(2,len(d['bar'])) + self.assert_(l[0] in d['bar']) + self.assert_(l[1] in d['bar']) + self.assertEqual(2,len(d['baz'])) + self.assert_(l[1] in d['baz']) + self.assert_(l[2] in d['baz']) + self.assertEqual(1,len(d['bleh'])) + self.assert_(l[2] in d['bleh']) + + def test_unpack_fields(self): + o = NamedObject('') + o.words = [['foo','bar'],['baz']] + d = build_word_dict([o]) + self.assertEqual(3,len(d)) + self.assertEqual(1,len(d['foo'])) + + def test_words_are_unaltered(self): + o = NamedObject('') + o.words = [['foo','bar'],['baz']] + d = build_word_dict([o]) + self.assertEqual([['foo','bar'],['baz']],o.words) + + def test_object_instances_can_only_be_once_in_words_object_list(self): + o = NamedObject('foo foo',True) + d = build_word_dict([o]) + self.assertEqual(1,len(d['foo'])) + + def test_job(self): + def do_progress(p,d=''): + self.log.append(p) + return True + + j = job.Job(1,do_progress) + self.log = [] + s = "foo bar" + build_word_dict([NamedObject(s, True), NamedObject(s, True), NamedObject(s, True)], j) + self.assertEqual(0,self.log[0]) + self.assertEqual(33,self.log[1]) + self.assertEqual(66,self.log[2]) + self.assertEqual(100,self.log[3]) + + +class TCmerge_similar_words(TestCase): + def test_some_similar_words(self): + d = { + 'foobar':set([1]), + 'foobar1':set([2]), + 'foobar2':set([3]), + } + merge_similar_words(d) + self.assertEqual(1,len(d)) + self.assertEqual(3,len(d['foobar'])) + + + +class TCreduce_common_words(TestCase): + def test_typical(self): + d = { + 'foo': set([NamedObject('foo bar',True) for i in range(50)]), + 'bar': set([NamedObject('foo bar',True) for i in range(49)]) + } + reduce_common_words(d, 50) + self.assert_('foo' not in d) + self.assertEqual(49,len(d['bar'])) + + def test_dont_remove_objects_with_only_common_words(self): + d = { + 'common': set([NamedObject("common uncommon",True) for i in range(50)] + [NamedObject("common",True)]), + 'uncommon': set([NamedObject("common uncommon",True)]) + } + reduce_common_words(d, 50) + self.assertEqual(1,len(d['common'])) + self.assertEqual(1,len(d['uncommon'])) + + def test_values_still_are_set_instances(self): + d = { + 'common': set([NamedObject("common uncommon",True) for i in range(50)] + [NamedObject("common",True)]), + 'uncommon': set([NamedObject("common uncommon",True)]) + } + reduce_common_words(d, 50) + self.assert_(isinstance(d['common'],set)) + self.assert_(isinstance(d['uncommon'],set)) + + def test_dont_raise_KeyError_when_a_word_has_been_removed(self): + #If a word has been removed by the reduce, an object in a subsequent common word that + #contains the word that has been removed would cause a KeyError. + d = { + 'foo': set([NamedObject('foo bar baz',True) for i in range(50)]), + 'bar': set([NamedObject('foo bar baz',True) for i in range(50)]), + 'baz': set([NamedObject('foo bar baz',True) for i in range(49)]) + } + try: + reduce_common_words(d, 50) + except KeyError: + self.fail() + + def test_unpack_fields(self): + #object.words may be fields. + def create_it(): + o = NamedObject('') + o.words = [['foo','bar'],['baz']] + return o + + d = { + 'foo': set([create_it() for i in range(50)]) + } + try: + reduce_common_words(d, 50) + except TypeError: + self.fail("must support fields.") + + def test_consider_a_reduced_common_word_common_even_after_reduction(self): + #There was a bug in the code that causeda word that has already been reduced not to + #be counted as a common word for subsequent words. For example, if 'foo' is processed + #as a common word, keeping a "foo bar" file in it, and the 'bar' is processed, "foo bar" + #would not stay in 'bar' because 'foo' is not a common word anymore. + only_common = NamedObject('foo bar',True) + d = { + 'foo': set([NamedObject('foo bar baz',True) for i in range(49)] + [only_common]), + 'bar': set([NamedObject('foo bar baz',True) for i in range(49)] + [only_common]), + 'baz': set([NamedObject('foo bar baz',True) for i in range(49)]) + } + reduce_common_words(d, 50) + self.assertEqual(1,len(d['foo'])) + self.assertEqual(1,len(d['bar'])) + self.assertEqual(49,len(d['baz'])) + + +class TCget_match(TestCase): + def test_simple(self): + o1 = NamedObject("foo bar",True) + o2 = NamedObject("bar bleh",True) + m = get_match(o1,o2) + self.assertEqual(50,m.percentage) + self.assertEqual(['foo','bar'],m.first.words) + self.assertEqual(['bar','bleh'],m.second.words) + self.assert_(m.first is o1) + self.assert_(m.second is o2) + + def test_in(self): + o1 = NamedObject("foo",True) + o2 = NamedObject("bar",True) + m = get_match(o1,o2) + self.assert_(o1 in m) + self.assert_(o2 in m) + self.assert_(object() not in m) + + def test_word_weight(self): + self.assertEqual(int((6.0 / 13.0) * 100),get_match(NamedObject("foo bar",True),NamedObject("bar bleh",True),(WEIGHT_WORDS,)).percentage) + + +class TCMatchFactory(TestCase): + def test_empty(self): + self.assertEqual([],MatchFactory().getmatches([])) + + def test_defaults(self): + mf = MatchFactory() + self.assertEqual(50,mf.common_word_threshold) + self.assertEqual(False,mf.weight_words) + self.assertEqual(False,mf.match_similar_words) + self.assertEqual(False,mf.no_field_order) + self.assertEqual(0,mf.min_match_percentage) + + def test_simple(self): + l = [NamedObject("foo bar"),NamedObject("bar bleh"),NamedObject("a b c foo")] + r = MatchFactory().getmatches(l) + self.assertEqual(2,len(r)) + seek = [m for m in r if m.percentage == 50] #"foo bar" and "bar bleh" + m = seek[0] + self.assertEqual(['foo','bar'],m.first.words) + self.assertEqual(['bar','bleh'],m.second.words) + seek = [m for m in r if m.percentage == 33] #"foo bar" and "a b c foo" + m = seek[0] + self.assertEqual(['foo','bar'],m.first.words) + self.assertEqual(['a','b','c','foo'],m.second.words) + + def test_null_and_unrelated_objects(self): + l = [NamedObject("foo bar"),NamedObject("bar bleh"),NamedObject(""),NamedObject("unrelated object")] + r = MatchFactory().getmatches(l) + self.assertEqual(1,len(r)) + m = r[0] + self.assertEqual(50,m.percentage) + self.assertEqual(['foo','bar'],m.first.words) + self.assertEqual(['bar','bleh'],m.second.words) + + def test_twice_the_same_word(self): + l = [NamedObject("foo foo bar"),NamedObject("bar bleh")] + r = MatchFactory().getmatches(l) + self.assertEqual(1,len(r)) + + def test_twice_the_same_word_when_preworded(self): + l = [NamedObject("foo foo bar",True),NamedObject("bar bleh",True)] + r = MatchFactory().getmatches(l) + self.assertEqual(1,len(r)) + + def test_two_words_match(self): + l = [NamedObject("foo bar"),NamedObject("foo bar bleh")] + r = MatchFactory().getmatches(l) + self.assertEqual(1,len(r)) + + def test_match_files_with_only_common_words(self): + #If a word occurs more than 50 times, it is excluded from the matching process + #The problem with the common_word_threshold is that the files containing only common + #words will never be matched together. We *should* match them. + mf = MatchFactory() + mf.common_word_threshold = 50 + l = [NamedObject("foo") for i in range(50)] + r = mf.getmatches(l) + self.assertEqual(1225,len(r)) + + def test_use_words_already_there_if_there(self): + o1 = NamedObject('foo') + o2 = NamedObject('bar') + o2.words = ['foo'] + self.assertEqual(1,len(MatchFactory().getmatches([o1,o2]))) + + def test_job(self): + def do_progress(p,d=''): + self.log.append(p) + return True + + j = job.Job(1,do_progress) + self.log = [] + s = "foo bar" + MatchFactory().getmatches([NamedObject(s),NamedObject(s),NamedObject(s)],j) + self.assert_(len(self.log) > 2) + self.assertEqual(0,self.log[0]) + self.assertEqual(100,self.log[-1]) + + def test_weight_words(self): + mf = MatchFactory() + mf.weight_words = True + l = [NamedObject("foo bar"),NamedObject("bar bleh")] + m = mf.getmatches(l)[0] + self.assertEqual(int((6.0 / 13.0) * 100),m.percentage) + + def test_similar_word(self): + mf = MatchFactory() + mf.match_similar_words = True + l = [NamedObject("foobar"),NamedObject("foobars")] + self.assertEqual(1,len(mf.getmatches(l))) + self.assertEqual(100,mf.getmatches(l)[0].percentage) + l = [NamedObject("foobar"),NamedObject("foo")] + self.assertEqual(0,len(mf.getmatches(l))) #too far + l = [NamedObject("bizkit"),NamedObject("bizket")] + self.assertEqual(1,len(mf.getmatches(l))) + l = [NamedObject("foobar"),NamedObject("foosbar")] + self.assertEqual(1,len(mf.getmatches(l))) + + def test_single_object_with_similar_words(self): + mf = MatchFactory() + mf.match_similar_words = True + l = [NamedObject("foo foos")] + self.assertEqual(0,len(mf.getmatches(l))) + + def test_double_words_get_counted_only_once(self): + mf = MatchFactory() + l = [NamedObject("foo bar foo bleh"),NamedObject("foo bar bleh bar")] + m = mf.getmatches(l)[0] + self.assertEqual(75,m.percentage) + + def test_with_fields(self): + mf = MatchFactory() + o1 = NamedObject("foo bar - foo bleh") + o2 = NamedObject("foo bar - bleh bar") + o1.words = getfields(o1.name) + o2.words = getfields(o2.name) + m = mf.getmatches([o1, o2])[0] + self.assertEqual(50, m.percentage) + + def test_with_fields_no_order(self): + mf = MatchFactory() + mf.no_field_order = True + o1 = NamedObject("foo bar - foo bleh") + o2 = NamedObject("bleh bang - foo bar") + o1.words = getfields(o1.name) + o2.words = getfields(o2.name) + m = mf.getmatches([o1, o2])[0] + self.assertEqual(50 ,m.percentage) + + def test_only_match_similar_when_the_option_is_set(self): + mf = MatchFactory() + mf.match_similar_words = False + l = [NamedObject("foobar"),NamedObject("foobars")] + self.assertEqual(0,len(mf.getmatches(l))) + + def test_dont_recurse_do_match(self): + # with nosetests, the stack is increased. The number has to be high enough not to be failing falsely + sys.setrecursionlimit(100) + mf = MatchFactory() + files = [NamedObject('foo bar') for i in range(101)] + try: + mf.getmatches(files) + except RuntimeError: + self.fail() + finally: + sys.setrecursionlimit(1000) + + def test_min_match_percentage(self): + l = [NamedObject("foo bar"),NamedObject("bar bleh"),NamedObject("a b c foo")] + mf = MatchFactory() + mf.min_match_percentage = 50 + r = mf.getmatches(l) + self.assertEqual(1,len(r)) #Only "foo bar" / "bar bleh" should match + + def test_limit(self): + l = [NamedObject(),NamedObject(),NamedObject()] + mf = MatchFactory() + mf.limit = 2 + r = mf.getmatches(l) + self.assertEqual(2,len(r)) + + def test_MemoryError(self): + @log_calls + def mocked_match(first, second, flags): + if len(mocked_match.calls) > 42: + raise MemoryError() + return Match(first, second, 0) + + objects = [NamedObject() for i in range(10)] # results in 45 matches + self.mock(engine, 'get_match', mocked_match) + mf = MatchFactory() + try: + r = mf.getmatches(objects) + except MemoryError: + self.fail('MemorryError must be handled') + self.assertEqual(42, len(r)) + + +class TCGroup(TestCase): + def test_empy(self): + g = Group() + self.assertEqual(None,g.ref) + self.assertEqual([],g.dupes) + self.assertEqual(0,len(g.matches)) + + def test_add_match(self): + g = Group() + m = get_match(NamedObject("foo",True),NamedObject("bar",True)) + g.add_match(m) + self.assert_(g.ref is m.first) + self.assertEqual([m.second],g.dupes) + self.assertEqual(1,len(g.matches)) + self.assert_(m in g.matches) + + def test_multiple_add_match(self): + g = Group() + o1 = NamedObject("a",True) + o2 = NamedObject("b",True) + o3 = NamedObject("c",True) + o4 = NamedObject("d",True) + g.add_match(get_match(o1,o2)) + self.assert_(g.ref is o1) + self.assertEqual([o2],g.dupes) + self.assertEqual(1,len(g.matches)) + g.add_match(get_match(o1,o3)) + self.assertEqual([o2],g.dupes) + self.assertEqual(2,len(g.matches)) + g.add_match(get_match(o2,o3)) + self.assertEqual([o2,o3],g.dupes) + self.assertEqual(3,len(g.matches)) + g.add_match(get_match(o1,o4)) + self.assertEqual([o2,o3],g.dupes) + self.assertEqual(4,len(g.matches)) + g.add_match(get_match(o2,o4)) + self.assertEqual([o2,o3],g.dupes) + self.assertEqual(5,len(g.matches)) + g.add_match(get_match(o3,o4)) + self.assertEqual([o2,o3,o4],g.dupes) + self.assertEqual(6,len(g.matches)) + + def test_len(self): + g = Group() + self.assertEqual(0,len(g)) + g.add_match(get_match(NamedObject("foo",True),NamedObject("bar",True))) + self.assertEqual(2,len(g)) + + def test_add_same_match_twice(self): + g = Group() + m = get_match(NamedObject("foo",True),NamedObject("foo",True)) + g.add_match(m) + self.assertEqual(2,len(g)) + self.assertEqual(1,len(g.matches)) + g.add_match(m) + self.assertEqual(2,len(g)) + self.assertEqual(1,len(g.matches)) + + def test_in(self): + g = Group() + o1 = NamedObject("foo",True) + o2 = NamedObject("bar",True) + self.assert_(o1 not in g) + g.add_match(get_match(o1,o2)) + self.assert_(o1 in g) + self.assert_(o2 in g) + + def test_remove(self): + g = Group() + o1 = NamedObject("foo",True) + o2 = NamedObject("bar",True) + o3 = NamedObject("bleh",True) + g.add_match(get_match(o1,o2)) + g.add_match(get_match(o1,o3)) + g.add_match(get_match(o2,o3)) + self.assertEqual(3,len(g.matches)) + self.assertEqual(3,len(g)) + g.remove_dupe(o3) + self.assertEqual(1,len(g.matches)) + self.assertEqual(2,len(g)) + g.remove_dupe(o1) + self.assertEqual(0,len(g.matches)) + self.assertEqual(0,len(g)) + + def test_remove_with_ref_dupes(self): + g = Group() + o1 = NamedObject("foo",True) + o2 = NamedObject("bar",True) + o3 = NamedObject("bleh",True) + g.add_match(get_match(o1,o2)) + g.add_match(get_match(o1,o3)) + g.add_match(get_match(o2,o3)) + o1.is_ref = True + o2.is_ref = True + g.remove_dupe(o3) + self.assertEqual(0,len(g)) + + def test_switch_ref(self): + o1 = NamedObject(with_words=True) + o2 = NamedObject(with_words=True) + g = Group() + g.add_match(get_match(o1,o2)) + self.assert_(o1 is g.ref) + g.switch_ref(o2) + self.assert_(o2 is g.ref) + self.assertEqual([o1],g.dupes) + g.switch_ref(o2) + self.assert_(o2 is g.ref) + g.switch_ref(NamedObject('',True)) + self.assert_(o2 is g.ref) + + def test_get_match_of(self): + g = Group() + for m in get_match_triangle(): + g.add_match(m) + o = g.dupes[0] + m = g.get_match_of(o) + self.assert_(g.ref in m) + self.assert_(o in m) + self.assert_(g.get_match_of(NamedObject('',True)) is None) + self.assert_(g.get_match_of(g.ref) is None) + + def test_percentage(self): + #percentage should return the avg percentage in relation to the ref + m1,m2,m3 = get_match_triangle() + m1 = Match(m1[0], m1[1], 100) + m2 = Match(m2[0], m2[1], 50) + m3 = Match(m3[0], m3[1], 33) + g = Group() + g.add_match(m1) + g.add_match(m2) + g.add_match(m3) + self.assertEqual(75,g.percentage) + g.switch_ref(g.dupes[0]) + self.assertEqual(66,g.percentage) + g.remove_dupe(g.dupes[0]) + self.assertEqual(33,g.percentage) + g.add_match(m1) + g.add_match(m2) + self.assertEqual(66,g.percentage) + + def test_percentage_on_empty_group(self): + g = Group() + self.assertEqual(0,g.percentage) + + def test_prioritize(self): + m1,m2,m3 = get_match_triangle() + o1 = m1.first + o2 = m1.second + o3 = m2.second + o1.name = 'c' + o2.name = 'b' + o3.name = 'a' + g = Group() + g.add_match(m1) + g.add_match(m2) + g.add_match(m3) + self.assert_(o1 is g.ref) + g.prioritize(lambda x:x.name) + self.assert_(o3 is g.ref) + + def test_prioritize_with_tie_breaker(self): + # if the ref has the same key as one or more of the dupe, run the tie_breaker func among them + g = get_test_group() + o1, o2, o3 = g.ordered + tie_breaker = lambda ref, dupe: dupe is o3 + g.prioritize(lambda x:0, tie_breaker) + self.assertTrue(g.ref is o3) + + def test_prioritize_with_tie_breaker_runs_on_all_dupes(self): + # Even if a dupe is chosen to switch with ref with a tie breaker, we still run the tie breaker + # with other dupes and the newly chosen ref + g = get_test_group() + o1, o2, o3 = g.ordered + o1.foo = 1 + o2.foo = 2 + o3.foo = 3 + tie_breaker = lambda ref, dupe: dupe.foo > ref.foo + g.prioritize(lambda x:0, tie_breaker) + self.assertTrue(g.ref is o3) + + def test_prioritize_with_tie_breaker_runs_only_on_tie_dupes(self): + # The tie breaker only runs on dupes that had the same value for the key_func + g = get_test_group() + o1, o2, o3 = g.ordered + o1.foo = 2 + o2.foo = 2 + o3.foo = 1 + o1.bar = 1 + o2.bar = 2 + o3.bar = 3 + key_func = lambda x: -x.foo + tie_breaker = lambda ref, dupe: dupe.bar > ref.bar + g.prioritize(key_func, tie_breaker) + self.assertTrue(g.ref is o2) + + def test_list_like(self): + g = Group() + o1,o2 = (NamedObject("foo",True),NamedObject("bar",True)) + g.add_match(get_match(o1,o2)) + self.assert_(g[0] is o1) + self.assert_(g[1] is o2) + + def test_clean_matches(self): + g = Group() + o1,o2,o3 = (NamedObject("foo",True),NamedObject("bar",True),NamedObject("baz",True)) + g.add_match(get_match(o1,o2)) + g.add_match(get_match(o1,o3)) + g.clean_matches() + self.assertEqual(1,len(g.matches)) + self.assertEqual(0,len(g.candidates)) + + +class TCget_groups(TestCase): + def test_empty(self): + r = get_groups([]) + self.assertEqual([],r) + + def test_simple(self): + l = [NamedObject("foo bar"),NamedObject("bar bleh")] + matches = MatchFactory().getmatches(l) + m = matches[0] + r = get_groups(matches) + self.assertEqual(1,len(r)) + g = r[0] + self.assert_(g.ref is m.first) + self.assertEqual([m.second],g.dupes) + + def test_group_with_multiple_matches(self): + #This results in 3 matches + l = [NamedObject("foo"),NamedObject("foo"),NamedObject("foo")] + matches = MatchFactory().getmatches(l) + r = get_groups(matches) + self.assertEqual(1,len(r)) + g = r[0] + self.assertEqual(3,len(g)) + + def test_must_choose_a_group(self): + l = [NamedObject("a b"),NamedObject("a b"),NamedObject("b c"),NamedObject("c d"),NamedObject("c d")] + #There will be 2 groups here: group "a b" and group "c d" + #"b c" can go either of them, but not both. + matches = MatchFactory().getmatches(l) + r = get_groups(matches) + self.assertEqual(2,len(r)) + self.assertEqual(5,len(r[0])+len(r[1])) + + def test_should_all_go_in_the_same_group(self): + l = [NamedObject("a b"),NamedObject("a b"),NamedObject("a b"),NamedObject("a b")] + #There will be 2 groups here: group "a b" and group "c d" + #"b c" can fit in both, but it must be in only one of them + matches = MatchFactory().getmatches(l) + r = get_groups(matches) + self.assertEqual(1,len(r)) + + def test_give_priority_to_matches_with_higher_percentage(self): + o1 = NamedObject(with_words=True) + o2 = NamedObject(with_words=True) + o3 = NamedObject(with_words=True) + m1 = Match(o1, o2, 1) + m2 = Match(o2, o3, 2) + r = get_groups([m1,m2]) + self.assertEqual(1,len(r)) + g = r[0] + self.assertEqual(2,len(g)) + self.assert_(o1 not in g) + self.assert_(o2 in g) + self.assert_(o3 in g) + + def test_four_sized_group(self): + l = [NamedObject("foobar") for i in xrange(4)] + m = MatchFactory().getmatches(l) + r = get_groups(m) + self.assertEqual(1,len(r)) + self.assertEqual(4,len(r[0])) + + def test_referenced_by_ref2(self): + o1 = NamedObject(with_words=True) + o2 = NamedObject(with_words=True) + o3 = NamedObject(with_words=True) + m1 = get_match(o1,o2) + m2 = get_match(o3,o1) + m3 = get_match(o3,o2) + r = get_groups([m1,m2,m3]) + self.assertEqual(3,len(r[0])) + + def test_job(self): + def do_progress(p,d=''): + self.log.append(p) + return True + + self.log = [] + j = job.Job(1,do_progress) + m1,m2,m3 = get_match_triangle() + #101%: To make sure it is processed first so the job test works correctly + m4 = Match(NamedObject('a',True), NamedObject('a',True), 101) + get_groups([m1,m2,m3,m4],j) + self.assertEqual(0,self.log[0]) + self.assertEqual(100,self.log[-1]) + + +if __name__ == "__main__": + unittest.main() diff --git a/pe/qt/dupeguru/export.py b/pe/qt/dupeguru/export.py new file mode 100644 index 00000000..c6293a5d --- /dev/null +++ b/pe/qt/dupeguru/export.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python +""" +Unit Name: dupeguru.export +Created By: Virgil Dupras +Created On: 2006/09/16 +Last modified by:$Author: virgil $ +Last modified on:$Date: 2009-05-28 15:22:39 +0200 (Thu, 28 May 2009) $ + $Revision: 4385 $ +Copyright 2004-2006 Hardcoded Software (http://www.hardcoded.net) +""" +from xml.dom import minidom +import tempfile +import os.path as op +import os +from StringIO import StringIO + +from hsutil.files import FileOrPath + +def output_column_xml(outfile, columns): + """Creates a xml file outfile with the supplied columns. + + outfile can be a filename or a file object. + columns is a list of 2 sized tuples (display,enabled) + """ + doc = minidom.Document() + root = doc.appendChild(doc.createElement('columns')) + for display,enabled in columns: + col_node = root.appendChild(doc.createElement('column')) + col_node.setAttribute('display', display) + col_node.setAttribute('enabled', {True:'y',False:'n'}[enabled]) + with FileOrPath(outfile, 'wb') as fp: + doc.writexml(fp, '\t','\t','\n', encoding='utf-8') + +def merge_css_into_xhtml(xhtml, css): + with FileOrPath(xhtml, 'r+') as xhtml: + with FileOrPath(css) as css: + try: + doc = minidom.parse(xhtml) + except Exception: + return False + head = doc.getElementsByTagName('head')[0] + links = head.getElementsByTagName('link') + for link in links: + if link.getAttribute('rel') == 'stylesheet': + head.removeChild(link) + style = head.appendChild(doc.createElement('style')) + style.setAttribute('type','text/css') + style.appendChild(doc.createTextNode(css.read())) + xhtml.truncate(0) + doc.writexml(xhtml, '\t','\t','\n', encoding='utf-8') + xhtml.seek(0) + return True + +def export_to_xhtml(xml, xslt, css, columns, cmd='xsltproc --path "%(folder)s" "%(xslt)s" "%(xml)s"'): + folder = op.split(xml)[0] + output_column_xml(op.join(folder,'columns.xml'),columns) + html = StringIO() + cmd = cmd % {'folder': folder, 'xslt': xslt, 'xml': xml} + html.write(os.popen(cmd).read()) + html.seek(0) + merge_css_into_xhtml(html,css) + html.seek(0) + html_path = op.join(folder,'export.htm') + html_file = open(html_path,'w') + html_file.write(html.read().encode('utf-8')) + html_file.close() + return html_path diff --git a/pe/qt/dupeguru/export_test.py b/pe/qt/dupeguru/export_test.py new file mode 100644 index 00000000..5c4a6d87 --- /dev/null +++ b/pe/qt/dupeguru/export_test.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python +""" +Unit Name: dupeguru.tests.export +Created By: Virgil Dupras +Created On: 2006/09/16 +Last modified by:$Author: virgil $ +Last modified on:$Date: 2009-05-28 15:22:39 +0200 (Thu, 28 May 2009) $ + $Revision: 4385 $ +Copyright 2004-2006 Hardcoded Software (http://www.hardcoded.net) +""" +import unittest +from xml.dom import minidom +from StringIO import StringIO + +from hsutil.testcase import TestCase + +from .export import * +from . import export + +class TCoutput_columns_xml(TestCase): + def test_empty_columns(self): + f = StringIO() + output_column_xml(f,[]) + f.seek(0) + doc = minidom.parse(f) + root = doc.documentElement + self.assertEqual('columns',root.nodeName) + self.assertEqual(0,len(root.childNodes)) + + def test_some_columns(self): + f = StringIO() + output_column_xml(f,[('foo',True),('bar',False),('baz',True)]) + f.seek(0) + doc = minidom.parse(f) + columns = doc.getElementsByTagName('column') + self.assertEqual(3,len(columns)) + c1,c2,c3 = columns + self.assertEqual('foo',c1.getAttribute('display')) + self.assertEqual('bar',c2.getAttribute('display')) + self.assertEqual('baz',c3.getAttribute('display')) + self.assertEqual('y',c1.getAttribute('enabled')) + self.assertEqual('n',c2.getAttribute('enabled')) + self.assertEqual('y',c3.getAttribute('enabled')) + + +class TCmerge_css_into_xhtml(TestCase): + def test_main(self): + css = StringIO() + css.write('foobar') + css.seek(0) + xhtml = StringIO() + xhtml.write(""" + + + + + dupeGuru - Duplicate file scanner + + + + + + """) + xhtml.seek(0) + self.assert_(merge_css_into_xhtml(xhtml,css)) + xhtml.seek(0) + doc = minidom.parse(xhtml) + head = doc.getElementsByTagName('head')[0] + #A style node should have been added in head. + styles = head.getElementsByTagName('style') + self.assertEqual(1,len(styles)) + style = styles[0] + self.assertEqual('text/css',style.getAttribute('type')) + self.assertEqual('foobar',style.firstChild.nodeValue.strip()) + #all should be removed + self.assertEqual(1,len(head.getElementsByTagName('link'))) + + def test_empty(self): + self.assert_(not merge_css_into_xhtml(StringIO(),StringIO())) + + def test_malformed(self): + xhtml = StringIO() + xhtml.write(""" + + """) + xhtml.seek(0) + self.assert_(not merge_css_into_xhtml(xhtml,StringIO())) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/pe/qt/dupeguru/gen.py b/pe/qt/dupeguru/gen.py new file mode 100644 index 00000000..0a842372 --- /dev/null +++ b/pe/qt/dupeguru/gen.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python +# Unit Name: gen +# Created By: Virgil Dupras +# Created On: 2009-05-26 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +import os +import os.path as op + +def move(src, dst): + if not op.exists(src): + return + if op.exists(dst): + os.remove(dst) + print 'Moving %s --> %s' % (src, dst) + os.rename(src, dst) + + +os.chdir(op.join('modules', 'block')) +os.system('python setup.py build_ext --inplace') +os.chdir(op.join('..', 'cache')) +os.system('python setup.py build_ext --inplace') +os.chdir(op.join('..', '..')) +move(op.join('modules', 'block', '_block.so'), op.join('picture', '_block.so')) +move(op.join('modules', 'block', '_block.pyd'), op.join('picture', '_block.pyd')) +move(op.join('modules', 'cache', '_cache.so'), op.join('picture', '_cache.so')) +move(op.join('modules', 'cache', '_cache.pyd'), op.join('picture', '_cache.pyd')) diff --git a/pe/qt/dupeguru/ignore.py b/pe/qt/dupeguru/ignore.py new file mode 100644 index 00000000..97060786 --- /dev/null +++ b/pe/qt/dupeguru/ignore.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python +""" +Unit Name: ignore +Created By: Virgil Dupras +Created On: 2006/05/02 +Last modified by:$Author: virgil $ +Last modified on:$Date: 2009-05-28 15:22:39 +0200 (Thu, 28 May 2009) $ + $Revision: 4385 $ +Copyright 2004-2006 Hardcoded Software (http://www.hardcoded.net) +""" +from hsutil.files import FileOrPath + +import xml.dom.minidom + +class IgnoreList(object): + """An ignore list implementation that is iterable, filterable and exportable to XML. + + Call Ignore to add an ignore list entry, and AreIgnore to check if 2 items are in the list. + When iterated, 2 sized tuples will be returned, the tuples containing 2 items ignored together. + """ + #---Override + def __init__(self): + self._ignored = {} + self._count = 0 + + def __iter__(self): + for first,seconds in self._ignored.iteritems(): + for second in seconds: + yield (first,second) + + def __len__(self): + return self._count + + #---Public + def AreIgnored(self,first,second): + def do_check(first,second): + try: + matches = self._ignored[first] + return second in matches + except KeyError: + return False + + return do_check(first,second) or do_check(second,first) + + def Clear(self): + self._ignored = {} + self._count = 0 + + def Filter(self,func): + """Applies a filter on all ignored items, and remove all matches where func(first,second) + doesn't return True. + """ + filtered = IgnoreList() + for first,second in self: + if func(first,second): + filtered.Ignore(first,second) + self._ignored = filtered._ignored + self._count = filtered._count + + def Ignore(self,first,second): + if self.AreIgnored(first,second): + return + try: + matches = self._ignored[first] + matches.add(second) + except KeyError: + try: + matches = self._ignored[second] + matches.add(first) + except KeyError: + matches = set() + matches.add(second) + self._ignored[first] = matches + self._count += 1 + + def load_from_xml(self,infile): + """Loads the ignore list from a XML created with save_to_xml. + + infile can be a file object or a filename. + """ + try: + doc = xml.dom.minidom.parse(infile) + except Exception: + return + file_nodes = doc.getElementsByTagName('file') + for fn in file_nodes: + if not fn.getAttributeNode('path'): + continue + file_path = fn.getAttributeNode('path').nodeValue + subfile_nodes = fn.getElementsByTagName('file') + for sfn in subfile_nodes: + if not sfn.getAttributeNode('path'): + continue + subfile_path = sfn.getAttributeNode('path').nodeValue + self.Ignore(file_path,subfile_path) + + def save_to_xml(self,outfile): + """Create a XML file that can be used by load_from_xml. + + outfile can be a file object or a filename. + """ + doc = xml.dom.minidom.Document() + root = doc.appendChild(doc.createElement('ignore_list')) + for file,subfiles in self._ignored.items(): + file_node = root.appendChild(doc.createElement('file')) + if isinstance(file,unicode): + file = file.encode('utf-8') + file_node.setAttribute('path',file) + for subfile in subfiles: + subfile_node = file_node.appendChild(doc.createElement('file')) + if isinstance(subfile,unicode): + subfile = subfile.encode('utf-8') + subfile_node.setAttribute('path',subfile) + with FileOrPath(outfile, 'wb') as fp: + doc.writexml(fp,'\t','\t','\n',encoding='utf-8') + + diff --git a/pe/qt/dupeguru/ignore_test.py b/pe/qt/dupeguru/ignore_test.py new file mode 100644 index 00000000..8ff91f52 --- /dev/null +++ b/pe/qt/dupeguru/ignore_test.py @@ -0,0 +1,158 @@ +#!/usr/bin/env python +""" +Unit Name: ignore +Created By: Virgil Dupras +Created On: 2006/05/02 +Last modified by:$Author: virgil $ +Last modified on:$Date: 2009-05-28 15:22:39 +0200 (Thu, 28 May 2009) $ + $Revision: 4385 $ +Copyright 2004-2006 Hardcoded Software (http://www.hardcoded.net) +""" +import unittest +import cStringIO +import xml.dom.minidom + +from .ignore import * + +class TCIgnoreList(unittest.TestCase): + def test_empty(self): + il = IgnoreList() + self.assertEqual(0,len(il)) + self.assert_(not il.AreIgnored('foo','bar')) + + def test_simple(self): + il = IgnoreList() + il.Ignore('foo','bar') + self.assert_(il.AreIgnored('foo','bar')) + self.assert_(il.AreIgnored('bar','foo')) + self.assert_(not il.AreIgnored('foo','bleh')) + self.assert_(not il.AreIgnored('bleh','bar')) + self.assertEqual(1,len(il)) + + def test_multiple(self): + il = IgnoreList() + il.Ignore('foo','bar') + il.Ignore('foo','bleh') + il.Ignore('bleh','bar') + il.Ignore('aybabtu','bleh') + self.assert_(il.AreIgnored('foo','bar')) + self.assert_(il.AreIgnored('bar','foo')) + self.assert_(il.AreIgnored('foo','bleh')) + self.assert_(il.AreIgnored('bleh','bar')) + self.assert_(not il.AreIgnored('aybabtu','bar')) + self.assertEqual(4,len(il)) + + def test_clear(self): + il = IgnoreList() + il.Ignore('foo','bar') + il.Clear() + self.assert_(not il.AreIgnored('foo','bar')) + self.assert_(not il.AreIgnored('bar','foo')) + self.assertEqual(0,len(il)) + + def test_add_same_twice(self): + il = IgnoreList() + il.Ignore('foo','bar') + il.Ignore('bar','foo') + self.assertEqual(1,len(il)) + + def test_save_to_xml(self): + il = IgnoreList() + il.Ignore('foo','bar') + il.Ignore('foo','bleh') + il.Ignore('bleh','bar') + f = cStringIO.StringIO() + il.save_to_xml(f) + f.seek(0) + doc = xml.dom.minidom.parse(f) + root = doc.documentElement + self.assertEqual('ignore_list',root.nodeName) + children = [c for c in root.childNodes if c.localName] + self.assertEqual(2,len(children)) + self.assertEqual(2,len([c for c in children if c.nodeName == 'file'])) + f1,f2 = children + subchildren = [c for c in f1.childNodes if c.localName == 'file'] +\ + [c for c in f2.childNodes if c.localName == 'file'] + self.assertEqual(3,len(subchildren)) + + def test_SaveThenLoad(self): + il = IgnoreList() + il.Ignore('foo','bar') + il.Ignore('foo','bleh') + il.Ignore('bleh','bar') + il.Ignore(u'\u00e9','bar') + f = cStringIO.StringIO() + il.save_to_xml(f) + f.seek(0) + il = IgnoreList() + il.load_from_xml(f) + self.assertEqual(4,len(il)) + self.assert_(il.AreIgnored(u'\u00e9','bar')) + + def test_LoadXML_with_empty_file_tags(self): + f = cStringIO.StringIO() + f.write('') + f.seek(0) + il = IgnoreList() + il.load_from_xml(f) + self.assertEqual(0,len(il)) + + def test_AreIgnore_works_when_a_child_is_a_key_somewhere_else(self): + il = IgnoreList() + il.Ignore('foo','bar') + il.Ignore('bar','baz') + self.assert_(il.AreIgnored('bar','foo')) + + + def test_no_dupes_when_a_child_is_a_key_somewhere_else(self): + il = IgnoreList() + il.Ignore('foo','bar') + il.Ignore('bar','baz') + il.Ignore('bar','foo') + self.assertEqual(2,len(il)) + + def test_iterate(self): + #It must be possible to iterate through ignore list + il = IgnoreList() + expected = [('foo','bar'),('bar','baz'),('foo','baz')] + for i in expected: + il.Ignore(i[0],i[1]) + for i in il: + expected.remove(i) #No exception should be raised + self.assert_(not expected) #expected should be empty + + def test_filter(self): + il = IgnoreList() + il.Ignore('foo','bar') + il.Ignore('bar','baz') + il.Ignore('foo','baz') + il.Filter(lambda f,s: f == 'bar') + self.assertEqual(1,len(il)) + self.assert_(not il.AreIgnored('foo','bar')) + self.assert_(il.AreIgnored('bar','baz')) + + def test_save_with_non_ascii_non_unicode_items(self): + il = IgnoreList() + il.Ignore('\xac','\xbf') + f = cStringIO.StringIO() + try: + il.save_to_xml(f) + except Exception,e: + self.fail(str(e)) + + def test_len(self): + il = IgnoreList() + self.assertEqual(0,len(il)) + il.Ignore('foo','bar') + self.assertEqual(1,len(il)) + + def test_nonzero(self): + il = IgnoreList() + self.assert_(not il) + il.Ignore('foo','bar') + self.assert_(il) + + +if __name__ == "__main__": + unittest.main() + diff --git a/pe/qt/dupeguru/modules/block/block.pyx b/pe/qt/dupeguru/modules/block/block.pyx new file mode 100644 index 00000000..db4c7500 --- /dev/null +++ b/pe/qt/dupeguru/modules/block/block.pyx @@ -0,0 +1,93 @@ +# Created By: Virgil Dupras +# Created On: 2009-04-23 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +cdef extern from "stdlib.h": + int abs(int n) # required so that abs() is applied on ints, not python objects + +class NoBlocksError(Exception): + """avgdiff/maxdiff has been called with empty lists""" + +class DifferentBlockCountError(Exception): + """avgdiff/maxdiff has been called with 2 block lists of different size.""" + + +cdef object getblock(object image): + """Returns a 3 sized tuple containing the mean color of 'image'. + + image: a PIL image or crop. + """ + cdef int pixel_count, red, green, blue, r, g, b + if image.size[0]: + pixel_count = image.size[0] * image.size[1] + red = green = blue = 0 + for r, g, b in image.getdata(): + red += r + green += g + blue += b + return (red // pixel_count, green // pixel_count, blue // pixel_count) + else: + return (0, 0, 0) + +def getblocks2(image, int block_count_per_side): + """Returns a list of blocks (3 sized tuples). + + image: A PIL image to base the blocks on. + block_count_per_side: This integer determine the number of blocks the function will return. + If it is 10, for example, 100 blocks will be returns (10 width, 10 height). The blocks will not + necessarely cover square areas. The area covered by each block will be proportional to the image + itself. + """ + if not image.size[0]: + return [] + cdef int width, height, block_width, block_height, ih, iw, top, bottom, left, right + width, height = image.size + block_width = max(width // block_count_per_side, 1) + block_height = max(height // block_count_per_side, 1) + result = [] + for ih in range(block_count_per_side): + top = min(ih * block_height, height - block_height) + bottom = top + block_height + for iw in range(block_count_per_side): + left = min(iw * block_width, width - block_width) + right = left + block_width + box = (left, top, right, bottom) + crop = image.crop(box) + result.append(getblock(crop)) + return result + +cdef int diff(first, second): + """Returns the difference between the first block and the second. + + It returns an absolute sum of the 3 differences (RGB). + """ + cdef int r1, g1, b1, r2, g2, b2 + r1, g1, b1 = first + r2, g2, b2 = second + return abs(r1 - r2) + abs(g1 - g2) + abs(b1 - b2) + +def avgdiff(first, second, int limit, int min_iterations): + """Returns the average diff between first blocks and seconds. + + If the result surpasses limit, limit + 1 is returned, except if less than min_iterations + iterations have been made in the blocks. + """ + cdef int count, sum, i, iteration_count + count = len(first) + if count != len(second): + raise DifferentBlockCountError() + if not count: + raise NoBlocksError() + sum = 0 + for i in range(count): + iteration_count = i + 1 + item1 = first[i] + item2 = second[i] + sum += diff(item1, item2) + if sum > limit * iteration_count and iteration_count >= min_iterations: + return limit + 1 + result = sum // count + if (not result) and sum: + result = 1 + return result \ No newline at end of file diff --git a/pe/qt/dupeguru/modules/block/setup.py b/pe/qt/dupeguru/modules/block/setup.py new file mode 100644 index 00000000..9d8f4cb5 --- /dev/null +++ b/pe/qt/dupeguru/modules/block/setup.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python +# Created By: Virgil Dupras +# Created On: 2009-04-23 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +from distutils.core import setup +from distutils.extension import Extension +from Cython.Distutils import build_ext + +setup( + cmdclass = {'build_ext': build_ext}, + ext_modules = [Extension("_block", ["block.pyx"])] +) \ No newline at end of file diff --git a/pe/qt/dupeguru/modules/cache/cache.pyx b/pe/qt/dupeguru/modules/cache/cache.pyx new file mode 100644 index 00000000..7bd2407d --- /dev/null +++ b/pe/qt/dupeguru/modules/cache/cache.pyx @@ -0,0 +1,34 @@ +#!/usr/bin/env python +# Created By: Virgil Dupras +# Created On: 2009-04-23 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +# ok, this is hacky and stuff, but I don't know C well enough to play with char buffers, copy +# them around and stuff +cdef int xchar_to_int(char c): + if 48 <= c <= 57: # 0-9 + return c - 48 + elif 65 <= c <= 70: # A-F + return c - 55 + elif 97 <= c <= 102: # a-f + return c - 87 + +def string_to_colors(s): + """Transform the string 's' in a list of 3 sized tuples. + """ + result = [] + cdef int i, char_count, r, g, b + cdef char* cs + char_count = len(s) + char_count = (char_count // 6) * 6 + cs = s + for i in range(0, char_count, 6): + r = xchar_to_int(cs[i]) << 4 + r += xchar_to_int(cs[i+1]) + g = xchar_to_int(cs[i+2]) << 4 + g += xchar_to_int(cs[i+3]) + b = xchar_to_int(cs[i+4]) << 4 + b += xchar_to_int(cs[i+5]) + result.append((r, g, b)) + return result diff --git a/pe/qt/dupeguru/modules/cache/setup.py b/pe/qt/dupeguru/modules/cache/setup.py new file mode 100644 index 00000000..2b6cd31b --- /dev/null +++ b/pe/qt/dupeguru/modules/cache/setup.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python +# Created By: Virgil Dupras +# Created On: 2009-04-23 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +from distutils.core import setup +from distutils.extension import Extension +from Cython.Distutils import build_ext + +setup( + cmdclass = {'build_ext': build_ext}, + ext_modules = [Extension("_cache", ["cache.pyx"])] +) \ No newline at end of file diff --git a/pe/qt/dupeguru/picture/__init__.py b/pe/qt/dupeguru/picture/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pe/qt/dupeguru/picture/block.py b/pe/qt/dupeguru/picture/block.py new file mode 100644 index 00000000..70015a50 --- /dev/null +++ b/pe/qt/dupeguru/picture/block.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python +""" +Unit Name: hs.picture.block +Created By: Virgil Dupras +Created On: 2006/09/01 +Last modified by:$Author: virgil $ +Last modified on:$Date: 2009-05-26 18:12:39 +0200 (Tue, 26 May 2009) $ + $Revision: 4365 $ +Copyright 2004-2006 Hardcoded Software (http://www.hardcoded.net) +""" +from _block import NoBlocksError, DifferentBlockCountError, avgdiff, getblocks2 + +# Converted to Cython +# def getblock(image): +# """Returns a 3 sized tuple containing the mean color of 'image'. +# +# image: a PIL image or crop. +# """ +# if image.size[0]: +# pixel_count = image.size[0] * image.size[1] +# red = green = blue = 0 +# for r,g,b in image.getdata(): +# red += r +# green += g +# blue += b +# return (red // pixel_count, green // pixel_count, blue // pixel_count) +# else: +# return (0,0,0) + +# This is not used anymore +# def getblocks(image,blocksize): +# """Returns a list of blocks (3 sized tuples). +# +# image: A PIL image to base the blocks on. +# blocksize: The size of the blocks to be create. This is a single integer, defining +# both width and height (blocks are square). +# """ +# if min(image.size) < blocksize: +# return () +# result = [] +# for i in xrange(image.size[1] // blocksize): +# for j in xrange(image.size[0] // blocksize): +# box = (blocksize * j, blocksize * i, blocksize * (j + 1), blocksize * (i + 1)) +# crop = image.crop(box) +# result.append(getblock(crop)) +# return result + +# Converted to Cython +# def getblocks2(image,block_count_per_side): +# """Returns a list of blocks (3 sized tuples). +# +# image: A PIL image to base the blocks on. +# block_count_per_side: This integer determine the number of blocks the function will return. +# If it is 10, for example, 100 blocks will be returns (10 width, 10 height). The blocks will not +# necessarely cover square areas. The area covered by each block will be proportional to the image +# itself. +# """ +# if not image.size[0]: +# return [] +# width,height = image.size +# block_width = max(width // block_count_per_side,1) +# block_height = max(height // block_count_per_side,1) +# result = [] +# for ih in range(block_count_per_side): +# top = min(ih * block_height, height - block_height) +# bottom = top + block_height +# for iw in range(block_count_per_side): +# left = min(iw * block_width, width - block_width) +# right = left + block_width +# box = (left,top,right,bottom) +# crop = image.crop(box) +# result.append(getblock(crop)) +# return result + +# Converted to Cython +# def diff(first, second): +# """Returns the difference between the first block and the second. +# +# It returns an absolute sum of the 3 differences (RGB). +# """ +# r1, g1, b1 = first +# r2, g2, b2 = second +# return abs(r1 - r2) + abs(g1 - g2) + abs(b1 - b2) + +# Converted to Cython +# def avgdiff(first, second, limit=768, min_iterations=1): +# """Returns the average diff between first blocks and seconds. +# +# If the result surpasses limit, limit + 1 is returned, except if less than min_iterations +# iterations have been made in the blocks. +# """ +# if len(first) != len(second): +# raise DifferentBlockCountError +# if not first: +# raise NoBlocksError +# count = len(first) +# sum = 0 +# zipped = izip(xrange(1, count + 1), first, second) +# for i, first, second in zipped: +# sum += diff(first, second) +# if sum > limit * i and i >= min_iterations: +# return limit + 1 +# result = sum // count +# if (not result) and sum: +# result = 1 +# return result + +# This is not used anymore +# def maxdiff(first,second,limit=768): +# """Returns the max diff between first blocks and seconds. +# +# If the result surpasses limit, the first max being over limit is returned. +# """ +# if len(first) != len(second): +# raise DifferentBlockCountError +# if not first: +# raise NoBlocksError +# result = 0 +# zipped = zip(first,second) +# for first,second in zipped: +# result = max(result,diff(first,second)) +# if result > limit: +# return result +# return result diff --git a/pe/qt/dupeguru/picture/block_test.py b/pe/qt/dupeguru/picture/block_test.py new file mode 100644 index 00000000..a06cf617 --- /dev/null +++ b/pe/qt/dupeguru/picture/block_test.py @@ -0,0 +1,313 @@ +#!/usr/bin/env python +""" +Unit Name: tests.picture.block +Created By: Virgil Dupras +Created On: 2006/09/01 +Last modified by:$Author: virgil $ +Last modified on:$Date: 2009-05-28 15:22:39 +0200 (Thu, 28 May 2009) $ + $Revision: 4385 $ +Copyright 2004-2006 Hardcoded Software (http://www.hardcoded.net) +""" +# The commented out tests are tests for function that have been converted to pure C for speed +import unittest + +from .block import * + +def my_avgdiff(first, second, limit=768, min_iter=3): # this is so I don't have to re-write every call + return avgdiff(first, second, limit, min_iter) + +BLACK = (0,0,0) +RED = (0xff,0,0) +GREEN = (0,0xff,0) +BLUE = (0,0,0xff) + +class FakeImage(object): + def __init__(self, size, data): + self.size = size + self.data = data + + def getdata(self): + return self.data + + def crop(self, box): + pixels = [] + for i in range(box[1], box[3]): + for j in range(box[0], box[2]): + pixel = self.data[i * self.size[0] + j] + pixels.append(pixel) + return FakeImage((box[2] - box[0], box[3] - box[1]), pixels) + +def empty(): + return FakeImage((0,0), []) + +def single_pixel(): #one red pixel + return FakeImage((1, 1), [(0xff,0,0)]) + +def four_pixels(): + pixels = [RED,(0,0x80,0xff),(0x80,0,0),(0,0x40,0x80)] + return FakeImage((2, 2), pixels) + +class TCgetblock(unittest.TestCase): + def test_single_pixel(self): + im = single_pixel() + [b] = getblocks2(im, 1) + self.assertEqual(RED,b) + + def test_no_pixel(self): + im = empty() + self.assertEqual([], getblocks2(im, 1)) + + def test_four_pixels(self): + im = four_pixels() + [b] = getblocks2(im, 1) + meanred = (0xff + 0x80) // 4 + meangreen = (0x80 + 0x40) // 4 + meanblue = (0xff + 0x80) // 4 + self.assertEqual((meanred,meangreen,meanblue),b) + + +# class TCdiff(unittest.TestCase): +# def test_diff(self): +# b1 = (10, 20, 30) +# b2 = (1, 2, 3) +# self.assertEqual(9 + 18 + 27,diff(b1,b2)) +# +# def test_diff_negative(self): +# b1 = (10, 20, 30) +# b2 = (1, 2, 3) +# self.assertEqual(9 + 18 + 27,diff(b2,b1)) +# +# def test_diff_mixed_positive_and_negative(self): +# b1 = (1, 5, 10) +# b2 = (10, 1, 15) +# self.assertEqual(9 + 4 + 5,diff(b1,b2)) +# + +# class TCgetblocks(unittest.TestCase): +# def test_empty_image(self): +# im = empty() +# blocks = getblocks(im,1) +# self.assertEqual(0,len(blocks)) +# +# def test_one_block_image(self): +# im = four_pixels() +# blocks = getblocks2(im, 1) +# self.assertEqual(1,len(blocks)) +# block = blocks[0] +# meanred = (0xff + 0x80) // 4 +# meangreen = (0x80 + 0x40) // 4 +# meanblue = (0xff + 0x80) // 4 +# self.assertEqual((meanred,meangreen,meanblue),block) +# +# def test_not_enough_height_to_fit_a_block(self): +# im = FakeImage((2,1), [BLACK, BLACK]) +# blocks = getblocks(im,2) +# self.assertEqual(0,len(blocks)) +# +# def xtest_dont_include_leftovers(self): +# # this test is disabled because getblocks is not used and getblock in cdeffed +# pixels = [ +# RED,(0,0x80,0xff),BLACK, +# (0x80,0,0),(0,0x40,0x80),BLACK, +# BLACK,BLACK,BLACK +# ] +# im = FakeImage((3,3), pixels) +# blocks = getblocks(im,2) +# block = blocks[0] +# #Because the block is smaller than the image, only blocksize must be considered. +# meanred = (0xff + 0x80) // 4 +# meangreen = (0x80 + 0x40) // 4 +# meanblue = (0xff + 0x80) // 4 +# self.assertEqual((meanred,meangreen,meanblue),block) +# +# def xtest_two_blocks(self): +# # this test is disabled because getblocks is not used and getblock in cdeffed +# pixels = [BLACK for i in xrange(4 * 2)] +# pixels[0] = RED +# pixels[1] = (0,0x80,0xff) +# pixels[4] = (0x80,0,0) +# pixels[5] = (0,0x40,0x80) +# im = FakeImage((4, 2), pixels) +# blocks = getblocks(im,2) +# self.assertEqual(2,len(blocks)) +# block = blocks[0] +# #Because the block is smaller than the image, only blocksize must be considered. +# meanred = (0xff + 0x80) // 4 +# meangreen = (0x80 + 0x40) // 4 +# meanblue = (0xff + 0x80) // 4 +# self.assertEqual((meanred,meangreen,meanblue),block) +# self.assertEqual(BLACK,blocks[1]) +# +# def test_four_blocks(self): +# pixels = [BLACK for i in xrange(4 * 4)] +# pixels[0] = RED +# pixels[1] = (0,0x80,0xff) +# pixels[4] = (0x80,0,0) +# pixels[5] = (0,0x40,0x80) +# im = FakeImage((4, 4), pixels) +# blocks = getblocks2(im, 2) +# self.assertEqual(4,len(blocks)) +# block = blocks[0] +# #Because the block is smaller than the image, only blocksize must be considered. +# meanred = (0xff + 0x80) // 4 +# meangreen = (0x80 + 0x40) // 4 +# meanblue = (0xff + 0x80) // 4 +# self.assertEqual((meanred,meangreen,meanblue),block) +# self.assertEqual(BLACK,blocks[1]) +# self.assertEqual(BLACK,blocks[2]) +# self.assertEqual(BLACK,blocks[3]) +# + +class TCgetblocks2(unittest.TestCase): + def test_empty_image(self): + im = empty() + blocks = getblocks2(im,1) + self.assertEqual(0,len(blocks)) + + def test_one_block_image(self): + im = four_pixels() + blocks = getblocks2(im,1) + self.assertEqual(1,len(blocks)) + block = blocks[0] + meanred = (0xff + 0x80) // 4 + meangreen = (0x80 + 0x40) // 4 + meanblue = (0xff + 0x80) // 4 + self.assertEqual((meanred,meangreen,meanblue),block) + + def test_four_blocks_all_black(self): + im = FakeImage((2, 2), [BLACK, BLACK, BLACK, BLACK]) + blocks = getblocks2(im,2) + self.assertEqual(4,len(blocks)) + for block in blocks: + self.assertEqual(BLACK,block) + + def test_two_pixels_image_horizontal(self): + pixels = [RED,BLUE] + im = FakeImage((2, 1), pixels) + blocks = getblocks2(im,2) + self.assertEqual(4,len(blocks)) + self.assertEqual(RED,blocks[0]) + self.assertEqual(BLUE,blocks[1]) + self.assertEqual(RED,blocks[2]) + self.assertEqual(BLUE,blocks[3]) + + def test_two_pixels_image_vertical(self): + pixels = [RED,BLUE] + im = FakeImage((1, 2), pixels) + blocks = getblocks2(im,2) + self.assertEqual(4,len(blocks)) + self.assertEqual(RED,blocks[0]) + self.assertEqual(RED,blocks[1]) + self.assertEqual(BLUE,blocks[2]) + self.assertEqual(BLUE,blocks[3]) + + +class TCavgdiff(unittest.TestCase): + def test_empty(self): + self.assertRaises(NoBlocksError, my_avgdiff, [], []) + + def test_two_blocks(self): + im = empty() + b1 = (5,10,15) + b2 = (255,250,245) + b3 = (0,0,0) + b4 = (255,0,255) + blocks1 = [b1,b2] + blocks2 = [b3,b4] + expected1 = 5 + 10 + 15 + expected2 = 0 + 250 + 10 + expected = (expected1 + expected2) // 2 + self.assertEqual(expected, my_avgdiff(blocks1, blocks2)) + + def test_blocks_not_the_same_size(self): + b = (0,0,0) + self.assertRaises(DifferentBlockCountError,my_avgdiff,[b,b],[b]) + + def test_first_arg_is_empty_but_not_second(self): + #Don't return 0 (as when the 2 lists are empty), raise! + b = (0,0,0) + self.assertRaises(DifferentBlockCountError,my_avgdiff,[],[b]) + + def test_limit(self): + ref = (0,0,0) + b1 = (10,10,10) #avg 30 + b2 = (20,20,20) #avg 45 + b3 = (30,30,30) #avg 60 + blocks1 = [ref,ref,ref] + blocks2 = [b1,b2,b3] + self.assertEqual(45,my_avgdiff(blocks1,blocks2,44)) + + def test_min_iterations(self): + ref = (0,0,0) + b1 = (10,10,10) #avg 30 + b2 = (20,20,20) #avg 45 + b3 = (10,10,10) #avg 40 + blocks1 = [ref,ref,ref] + blocks2 = [b1,b2,b3] + self.assertEqual(40,my_avgdiff(blocks1,blocks2,45 - 1,3)) + + # Bah, I don't know why this test fails, but I don't think it matters very much + # def test_just_over_the_limit(self): + # #A score just over the limit might return exactly the limit due to truncating. We should + # #ceil() the result in this case. + # ref = (0,0,0) + # b1 = (10,0,0) + # b2 = (11,0,0) + # blocks1 = [ref,ref] + # blocks2 = [b1,b2] + # self.assertEqual(11,my_avgdiff(blocks1,blocks2,10)) + # + def test_return_at_least_1_at_the_slightest_difference(self): + ref = (0,0,0) + b1 = (1,0,0) + blocks1 = [ref for i in xrange(250)] + blocks2 = [ref for i in xrange(250)] + blocks2[0] = b1 + self.assertEqual(1,my_avgdiff(blocks1,blocks2)) + + def test_return_0_if_there_is_no_difference(self): + ref = (0,0,0) + blocks1 = [ref,ref] + blocks2 = [ref,ref] + self.assertEqual(0,my_avgdiff(blocks1,blocks2)) + + +# class TCmaxdiff(unittest.TestCase): +# def test_empty(self): +# self.assertRaises(NoBlocksError,maxdiff,[],[]) +# +# def test_two_blocks(self): +# b1 = (5,10,15) +# b2 = (255,250,245) +# b3 = (0,0,0) +# b4 = (255,0,255) +# blocks1 = [b1,b2] +# blocks2 = [b3,b4] +# expected1 = 5 + 10 + 15 +# expected2 = 0 + 250 + 10 +# expected = max(expected1,expected2) +# self.assertEqual(expected,maxdiff(blocks1,blocks2)) +# +# def test_blocks_not_the_same_size(self): +# b = (0,0,0) +# self.assertRaises(DifferentBlockCountError,maxdiff,[b,b],[b]) +# +# def test_first_arg_is_empty_but_not_second(self): +# #Don't return 0 (as when the 2 lists are empty), raise! +# b = (0,0,0) +# self.assertRaises(DifferentBlockCountError,maxdiff,[],[b]) +# +# def test_limit(self): +# b1 = (5,10,15) +# b2 = (255,250,245) +# b3 = (0,0,0) +# b4 = (255,0,255) +# blocks1 = [b1,b2] +# blocks2 = [b3,b4] +# expected1 = 5 + 10 + 15 +# expected2 = 0 + 250 + 10 +# self.assertEqual(expected1,maxdiff(blocks1,blocks2,expected1 - 1)) +# + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/pe/qt/dupeguru/picture/cache.py b/pe/qt/dupeguru/picture/cache.py new file mode 100644 index 00000000..6ff0d2d1 --- /dev/null +++ b/pe/qt/dupeguru/picture/cache.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python +""" +Unit Name: hs.picture.cache +Created By: Virgil Dupras +Created On: 2006/09/14 +Last modified by:$Author: virgil $ +Last modified on:$Date: 2009-05-28 16:33:32 +0200 (Thu, 28 May 2009) $ + $Revision: 4392 $ +Copyright 2004-2006 Hardcoded Software (http://www.hardcoded.net) +""" +import os +import logging +import sqlite3 as sqlite + +import hsutil.sqlite + +from _cache import string_to_colors + +def colors_to_string(colors): + """Transform the 3 sized tuples 'colors' into a hex string. + + [(0,100,255)] --> 0064ff + [(1,2,3),(4,5,6)] --> 010203040506 + """ + return ''.join(['%02x%02x%02x' % (r,g,b) for r,g,b in colors]) + +# This function is an important bottleneck of dupeGuru PE. It has been converted to Cython. +# def string_to_colors(s): +# """Transform the string 's' in a list of 3 sized tuples. +# """ +# result = [] +# for i in xrange(0, len(s), 6): +# number = int(s[i:i+6], 16) +# result.append((number >> 16, (number >> 8) & 0xff, number & 0xff)) +# return result + +class Cache(object): + """A class to cache picture blocks. + """ + def __init__(self, db=':memory:', threaded=True): + def create_tables(): + sql = "create table pictures(path TEXT, blocks TEXT)" + self.con.execute(sql); + sql = "create index idx_path on pictures (path)" + self.con.execute(sql) + + self.dbname = db + if threaded: + self.con = hsutil.sqlite.ThreadedConn(db, True) + else: + self.con = sqlite.connect(db, isolation_level=None) + try: + self.con.execute("select * from pictures where 1=2") + except sqlite.OperationalError: # new db + create_tables() + except sqlite.DatabaseError, e: # corrupted db + logging.warning('Could not create picture cache because of an error: %s', str(e)) + self.con.close() + os.remove(db) + if threaded: + self.con = hsutil.sqlite.ThreadedConn(db, True) + else: + self.con = sqlite.connect(db, isolation_level=None) + create_tables() + + def __contains__(self, key): + sql = "select count(*) from pictures where path = ?" + result = self.con.execute(sql, [key]).fetchall() + return result[0][0] > 0 + + def __delitem__(self, key): + if key not in self: + raise KeyError(key) + sql = "delete from pictures where path = ?" + self.con.execute(sql, [key]) + + # Optimized + def __getitem__(self, key): + if isinstance(key, int): + sql = "select blocks from pictures where rowid = ?" + else: + sql = "select blocks from pictures where path = ?" + result = self.con.execute(sql, [key]).fetchone() + if result: + result = string_to_colors(result[0]) + return result + else: + raise KeyError(key) + + def __iter__(self): + sql = "select path from pictures" + result = self.con.execute(sql) + return (row[0] for row in result) + + def __len__(self): + sql = "select count(*) from pictures" + result = self.con.execute(sql).fetchall() + return result[0][0] + + def __setitem__(self, key, value): + value = colors_to_string(value) + if key in self: + sql = "update pictures set blocks = ? where path = ?" + else: + sql = "insert into pictures(blocks,path) values(?,?)" + try: + self.con.execute(sql, [value, key]) + except sqlite.OperationalError: + logging.warning('Picture cache could not set %r for key %r', value, key) + except sqlite.DatabaseError, e: + logging.warning('DatabaseError while setting %r for key %r: %s', value, key, str(e)) + + def clear(self): + sql = "delete from pictures" + self.con.execute(sql) + + def filter(self, func): + to_delete = [key for key in self if not func(key)] + for key in to_delete: + del self[key] + + def get_id(self, path): + sql = "select rowid from pictures where path = ?" + result = self.con.execute(sql, [path]).fetchone() + if result: + return result[0] + else: + raise ValueError(path) + + def get_multiple(self, rowids): + sql = "select rowid, blocks from pictures where rowid in (%s)" % ','.join(map(str, rowids)) + cur = self.con.execute(sql) + return ((rowid, string_to_colors(blocks)) for rowid, blocks in cur) + diff --git a/pe/qt/dupeguru/picture/cache_test.py b/pe/qt/dupeguru/picture/cache_test.py new file mode 100644 index 00000000..f453112f --- /dev/null +++ b/pe/qt/dupeguru/picture/cache_test.py @@ -0,0 +1,159 @@ +#!/usr/bin/env python +""" +Unit Name: tests.picture.cache +Created By: Virgil Dupras +Created On: 2006/09/14 +Last modified by:$Author: virgil $ +Last modified on:$Date: 2009-05-28 15:22:39 +0200 (Thu, 28 May 2009) $ + $Revision: 4385 $ +Copyright 2004-2006 Hardcoded Software (http://www.hardcoded.net) +""" +import unittest +from StringIO import StringIO +import os.path as op +import os +import threading + +from hsutil.testcase import TestCase +from .cache import * + +class TCcolors_to_string(unittest.TestCase): + def test_no_color(self): + self.assertEqual('',colors_to_string([])) + + def test_single_color(self): + self.assertEqual('000000',colors_to_string([(0,0,0)])) + self.assertEqual('010101',colors_to_string([(1,1,1)])) + self.assertEqual('0a141e',colors_to_string([(10,20,30)])) + + def test_two_colors(self): + self.assertEqual('000102030405',colors_to_string([(0,1,2),(3,4,5)])) + + +class TCstring_to_colors(unittest.TestCase): + def test_empty(self): + self.assertEqual([],string_to_colors('')) + + def test_single_color(self): + self.assertEqual([(0,0,0)],string_to_colors('000000')) + self.assertEqual([(2,3,4)],string_to_colors('020304')) + self.assertEqual([(10,20,30)],string_to_colors('0a141e')) + + def test_two_colors(self): + self.assertEqual([(10,20,30),(40,50,60)],string_to_colors('0a141e28323c')) + + def test_incomplete_color(self): + # don't return anything if it's not a complete color + self.assertEqual([],string_to_colors('102')) + + +class TCCache(TestCase): + def test_empty(self): + c = Cache() + self.assertEqual(0,len(c)) + self.assertRaises(KeyError,c.__getitem__,'foo') + + def test_set_then_retrieve_blocks(self): + c = Cache() + b = [(0,0,0),(1,2,3)] + c['foo'] = b + self.assertEqual(b,c['foo']) + + def test_delitem(self): + c = Cache() + c['foo'] = '' + del c['foo'] + self.assert_('foo' not in c) + self.assertRaises(KeyError,c.__delitem__,'foo') + + def test_persistance(self): + DBNAME = op.join(self.tmpdir(), 'hstest.db') + c = Cache(DBNAME) + c['foo'] = [(1,2,3)] + del c + c = Cache(DBNAME) + self.assertEqual([(1,2,3)],c['foo']) + del c + os.remove(DBNAME) + + def test_filter(self): + c = Cache() + c['foo'] = '' + c['bar'] = '' + c['baz'] = '' + c.filter(lambda p:p != 'bar') #only 'bar' is removed + self.assertEqual(2,len(c)) + self.assert_('foo' in c) + self.assert_('baz' in c) + self.assert_('bar' not in c) + + def test_clear(self): + c = Cache() + c['foo'] = '' + c['bar'] = '' + c['baz'] = '' + c.clear() + self.assertEqual(0,len(c)) + self.assert_('foo' not in c) + self.assert_('baz' not in c) + self.assert_('bar' not in c) + + def test_corrupted_db(self): + dbname = op.join(self.tmpdir(), 'foo.db') + fp = open(dbname, 'w') + fp.write('invalid sqlite content') + fp.close() + c = Cache(dbname) # should not raise a DatabaseError + c['foo'] = [(1, 2, 3)] + del c + c = Cache(dbname) + self.assertEqual(c['foo'], [(1, 2, 3)]) + + def test_by_id(self): + # it's possible to use the cache by referring to the files by their row_id + c = Cache() + b = [(0,0,0),(1,2,3)] + c['foo'] = b + foo_id = c.get_id('foo') + self.assertEqual(c[foo_id], b) + + +class TCCacheSQLEscape(unittest.TestCase): + def test_contains(self): + c = Cache() + self.assert_("foo'bar" not in c) + + def test_getitem(self): + c = Cache() + self.assertRaises(KeyError, c.__getitem__, "foo'bar") + + def test_setitem(self): + c = Cache() + c["foo'bar"] = [] + + def test_delitem(self): + c = Cache() + c["foo'bar"] = [] + try: + del c["foo'bar"] + except KeyError: + self.fail() + + +class TCCacheThreaded(unittest.TestCase): + def test_access_cache(self): + def thread_run(): + try: + c['foo'] = [(1,2,3)] + except sqlite.ProgrammingError: + self.fail() + + c = Cache() + t = threading.Thread(target=thread_run) + t.start() + t.join() + self.assertEqual([(1,2,3)], c['foo']) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/pe/qt/dupeguru/picture/matchbase.py b/pe/qt/dupeguru/picture/matchbase.py new file mode 100644 index 00000000..cf0d1e89 --- /dev/null +++ b/pe/qt/dupeguru/picture/matchbase.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python +""" +Unit Name: hs.picture._match +Created By: Virgil Dupras +Created On: 2007/02/25 +Last modified by:$Author: virgil $ +Last modified on:$Date: 2009-05-28 16:02:48 +0200 (Thu, 28 May 2009) $ + $Revision: 4388 $ +Copyright 2007 Hardcoded Software (http://www.hardcoded.net) +""" +import logging +import multiprocessing +from Queue import Empty +from collections import defaultdict + +from hsutil import job +from hs.utils.misc import dedupe + +from dupeguru.engine import Match +from block import avgdiff, DifferentBlockCountError, NoBlocksError +from cache import Cache + +MIN_ITERATIONS = 3 + +def get_match(first,second,percentage): + if percentage < 0: + percentage = 0 + return Match(first,second,percentage) + +class MatchFactory(object): + cached_blocks = None + block_count_per_side = 15 + threshold = 75 + match_scaled = False + + def _do_getmatches(self, files, j): + raise NotImplementedError() + + def getmatches(self, files, j=job.nulljob): + # The MemoryError handlers in there use logging without first caring about whether or not + # there is enough memory left to carry on the operation because it is assumed that the + # MemoryError happens when trying to read an image file, which is freed from memory by the + # time that MemoryError is raised. + j = j.start_subjob([2, 8]) + logging.info('Preparing %d files' % len(files)) + prepared = self.prepare_files(files, j) + logging.info('Finished preparing %d files' % len(prepared)) + return self._do_getmatches(prepared, j) + + def prepare_files(self, files, j=job.nulljob): + prepared = [] # only files for which there was no error getting blocks + try: + for picture in j.iter_with_progress(files, 'Analyzed %d/%d pictures'): + picture.dimensions + picture.unicode_path = unicode(picture.path) + try: + if picture.unicode_path not in self.cached_blocks: + blocks = picture.get_blocks(self.block_count_per_side) + self.cached_blocks[picture.unicode_path] = blocks + prepared.append(picture) + except IOError as e: + logging.warning(unicode(e)) + except MemoryError: + logging.warning(u'Ran out of memory while reading %s of size %d' % (picture.unicode_path, picture.size)) + if picture.size < 10 * 1024 * 1024: # We're really running out of memory + raise + except MemoryError: + logging.warning('Ran out of memory while preparing files') + return prepared + + +def async_compare(ref_id, other_ids, dbname, threshold): + cache = Cache(dbname, threaded=False) + limit = 100 - threshold + ref_blocks = cache[ref_id] + pairs = cache.get_multiple(other_ids) + results = [] + for other_id, other_blocks in pairs: + try: + diff = avgdiff(ref_blocks, other_blocks, limit, MIN_ITERATIONS) + percentage = 100 - diff + except (DifferentBlockCountError, NoBlocksError): + percentage = 0 + if percentage >= threshold: + results.append((ref_id, other_id, percentage)) + cache.con.close() + return results + +class AsyncMatchFactory(MatchFactory): + def _do_getmatches(self, pictures, j): + def empty_out_queue(queue, into): + try: + while True: + into.append(queue.get(block=False)) + except Empty: + pass + + j = j.start_subjob([1, 8, 1], 'Preparing for matching') + cache = self.cached_blocks + id2picture = {} + dimensions2pictures = defaultdict(set) + for picture in pictures[:]: + try: + picture.cache_id = cache.get_id(picture.unicode_path) + id2picture[picture.cache_id] = picture + except ValueError: + pictures.remove(picture) + if not self.match_scaled: + dimensions2pictures[picture.dimensions].add(picture) + pool = multiprocessing.Pool() + async_results = [] + pictures_copy = set(pictures) + for ref in j.iter_with_progress(pictures): + others = pictures_copy if self.match_scaled else dimensions2pictures[ref.dimensions] + others.remove(ref) + if others: + cache_ids = [f.cache_id for f in others] + args = (ref.cache_id, cache_ids, self.cached_blocks.dbname, self.threshold) + async_results.append(pool.apply_async(async_compare, args)) + + matches = [] + for result in j.iter_with_progress(async_results, 'Matched %d/%d pictures'): + matches.extend(result.get()) + + result = [] + for ref_id, other_id, percentage in j.iter_with_progress(matches, 'Verified %d/%d matches', every=10): + ref = id2picture[ref_id] + other = id2picture[other_id] + if percentage == 100 and ref.md5 != other.md5: + percentage = 99 + if percentage >= self.threshold: + result.append(get_match(ref, other, percentage)) + return result + + +multiprocessing.freeze_support() \ No newline at end of file diff --git a/pe/qt/dupeguru/results.py b/pe/qt/dupeguru/results.py new file mode 100644 index 00000000..a7ded5c0 --- /dev/null +++ b/pe/qt/dupeguru/results.py @@ -0,0 +1,359 @@ +#!/usr/bin/env python +""" +Unit Name: dupeguru.results +Created By: Virgil Dupras +Created On: 2006/02/23 +Last modified by:$Author: virgil $ +Last modified on:$Date: 2009-05-28 16:33:32 +0200 (Thu, 28 May 2009) $ + $Revision: 4392 $ +Copyright 2004-2006 Hardcoded Software (http://www.hardcoded.net) +""" +import re +from xml.sax import handler, make_parser, SAXException +from xml.sax.saxutils import XMLGenerator +from xml.sax.xmlreader import AttributesImpl + +from . import engine +from hsutil.job import nulljob +from hsutil.markable import Markable +from hsutil.misc import flatten, cond, nonone +from hsutil.str import format_size +from hsutil.files import open_if_filename + +class Results(Markable): + #---Override + def __init__(self, data_module): + super(Results, self).__init__() + self.__groups = [] + self.__group_of_duplicate = {} + self.__groups_sort_descriptor = None # This is a tuple (key, asc) + self.__dupes = None + self.__dupes_sort_descriptor = None # This is a tuple (key, asc, delta) + self.__filters = None + self.__filtered_dupes = None + self.__filtered_groups = None + self.__recalculate_stats() + self.__marked_size = 0 + self.data = data_module + + def _did_mark(self, dupe): + self.__marked_size += dupe.size + + def _did_unmark(self, dupe): + self.__marked_size -= dupe.size + + def _get_markable_count(self): + return self.__total_count + + def _is_markable(self, dupe): + if dupe.is_ref: + return False + g = self.get_group_of_duplicate(dupe) + if not g: + return False + if dupe is g.ref: + return False + if self.__filtered_dupes and dupe not in self.__filtered_dupes: + return False + return True + + #---Private + def __get_dupe_list(self): + if self.__dupes is None: + self.__dupes = flatten(group.dupes for group in self.groups) + if self.__filtered_dupes: + self.__dupes = [dupe for dupe in self.__dupes if dupe in self.__filtered_dupes] + sd = self.__dupes_sort_descriptor + if sd: + self.sort_dupes(sd[0], sd[1], sd[2]) + return self.__dupes + + def __get_groups(self): + if self.__filtered_groups is None: + return self.__groups + else: + return self.__filtered_groups + + def __get_stat_line(self): + if self.__filtered_dupes is None: + mark_count = self.mark_count + marked_size = self.__marked_size + total_count = self.__total_count + total_size = self.__total_size + else: + mark_count = len([dupe for dupe in self.__filtered_dupes if self.is_marked(dupe)]) + marked_size = sum(dupe.size for dupe in self.__filtered_dupes if self.is_marked(dupe)) + total_count = len([dupe for dupe in self.__filtered_dupes if self.is_markable(dupe)]) + total_size = sum(dupe.size for dupe in self.__filtered_dupes if self.is_markable(dupe)) + if self.mark_inverted: + marked_size = self.__total_size - marked_size + result = '%d / %d (%s / %s) duplicates marked.' % ( + mark_count, + total_count, + format_size(marked_size, 2), + format_size(total_size, 2), + ) + if self.__filters: + result += ' filter: %s' % ' --> '.join(self.__filters) + return result + + def __recalculate_stats(self): + self.__total_size = 0 + self.__total_count = 0 + for group in self.groups: + markable = [dupe for dupe in group.dupes if self._is_markable(dupe)] + self.__total_count += len(markable) + self.__total_size += sum(dupe.size for dupe in markable) + + def __set_groups(self, new_groups): + self.mark_none() + self.__groups = new_groups + self.__group_of_duplicate = {} + for g in self.__groups: + for dupe in g: + self.__group_of_duplicate[dupe] = g + if not hasattr(dupe, 'is_ref'): + dupe.is_ref = False + old_filters = nonone(self.__filters, []) + self.apply_filter(None) + for filter_str in old_filters: + self.apply_filter(filter_str) + + #---Public + def apply_filter(self, filter_str): + ''' Applies a filter 'filter_str' to self.groups + + When you apply the filter, only dupes with the filename matching 'filter_str' will be in + in the results. To cancel the filter, just call apply_filter with 'filter_str' to None, + and the results will go back to normal. + + If call apply_filter on a filtered results, the filter will be applied + *on the filtered results*. + + 'filter_str' is a string containing a regexp to filter dupes with. + ''' + if not filter_str: + self.__filtered_dupes = None + self.__filtered_groups = None + self.__filters = None + else: + if not self.__filters: + self.__filters = [] + self.__filters.append(filter_str) + filter_re = re.compile(filter_str, re.IGNORECASE) + if self.__filtered_dupes is None: + self.__filtered_dupes = flatten(g[:] for g in self.groups) + self.__filtered_dupes = set(dupe for dupe in self.__filtered_dupes if filter_re.search(dupe.name)) + filtered_groups = set() + for dupe in self.__filtered_dupes: + filtered_groups.add(self.get_group_of_duplicate(dupe)) + self.__filtered_groups = list(filtered_groups) + self.__recalculate_stats() + sd = self.__groups_sort_descriptor + if sd: + self.sort_groups(sd[0], sd[1]) + self.__dupes = None + + def get_group_of_duplicate(self, dupe): + try: + return self.__group_of_duplicate[dupe] + except (TypeError, KeyError): + return None + + is_markable = _is_markable + + def load_from_xml(self, infile, get_file, j=nulljob): + self.apply_filter(None) + handler = _ResultsHandler(get_file) + parser = make_parser() + parser.setContentHandler(handler) + try: + infile, must_close = open_if_filename(infile) + except IOError: + return + BUFSIZE = 1024 * 1024 # 1mb buffer + infile.seek(0, 2) + j.start_job(infile.tell() // BUFSIZE) + infile.seek(0, 0) + try: + while True: + data = infile.read(BUFSIZE) + if not data: + break + parser.feed(data) + j.add_progress() + except SAXException: + return + self.groups = handler.groups + for dupe_file in handler.marked: + self.mark(dupe_file) + + def make_ref(self, dupe): + g = self.get_group_of_duplicate(dupe) + r = g.ref + self._remove_mark_flag(dupe) + g.switch_ref(dupe); + if not r.is_ref: + self.__total_count += 1 + self.__total_size += r.size + if not dupe.is_ref: + self.__total_count -= 1 + self.__total_size -= dupe.size + self.__dupes = None + + def perform_on_marked(self, func, remove_from_results): + problems = [] + for d in self.dupes: + if self.is_marked(d) and (not func(d)): + problems.append(d) + if remove_from_results: + to_remove = [d for d in self.dupes if self.is_marked(d) and (d not in problems)] + self.remove_duplicates(to_remove) + self.mark_none() + for d in problems: + self.mark(d) + return len(problems) + + def remove_duplicates(self, dupes): + '''Remove 'dupes' from their respective group, and remove the group is it ends up empty. + ''' + affected_groups = set() + for dupe in dupes: + group = self.get_group_of_duplicate(dupe) + if dupe not in group.dupes: + return + group.remove_dupe(dupe, False) + self._remove_mark_flag(dupe) + self.__total_count -= 1 + self.__total_size -= dupe.size + if not group: + self.__groups.remove(group) + if self.__filtered_groups: + self.__filtered_groups.remove(group) + else: + affected_groups.add(group) + for group in affected_groups: + group.clean_matches() + self.__dupes = None + + def save_to_xml(self, outfile, with_data=False): + self.apply_filter(None) + outfile, must_close = open_if_filename(outfile, 'wb') + writer = XMLGenerator(outfile, 'utf-8') + writer.startDocument() + empty_attrs = AttributesImpl({}) + writer.startElement('results', empty_attrs) + for g in self.groups: + writer.startElement('group', empty_attrs) + dupe2index = {} + for index, d in enumerate(g): + dupe2index[d] = index + try: + words = engine.unpack_fields(d.words) + except AttributeError: + words = () + attrs = AttributesImpl({ + 'path': unicode(d.path), + 'is_ref': cond(d.is_ref, 'y', 'n'), + 'words': ','.join(words), + 'marked': cond(self.is_marked(d), 'y', 'n') + }) + writer.startElement('file', attrs) + if with_data: + data_list = self.data.GetDisplayInfo(d, g) + for data in data_list: + attrs = AttributesImpl({ + 'value': data, + }) + writer.startElement('data', attrs) + writer.endElement('data') + writer.endElement('file') + for match in g.matches: + attrs = AttributesImpl({ + 'first': str(dupe2index[match.first]), + 'second': str(dupe2index[match.second]), + 'percentage': str(int(match.percentage)), + }) + writer.startElement('match', attrs) + writer.endElement('match') + writer.endElement('group') + writer.endElement('results') + writer.endDocument() + if must_close: + outfile.close() + + def sort_dupes(self, key, asc=True, delta=False): + if not self.__dupes: + self.__get_dupe_list() + self.__dupes.sort(key=lambda d: self.data.GetDupeSortKey(d, lambda: self.get_group_of_duplicate(d), key, delta)) + if not asc: + self.__dupes.reverse() + self.__dupes_sort_descriptor = (key,asc,delta) + + def sort_groups(self,key,asc=True): + self.groups.sort(key=lambda g: self.data.GetGroupSortKey(g, key)) + if not asc: + self.groups.reverse() + self.__groups_sort_descriptor = (key,asc) + + #---Properties + dupes = property(__get_dupe_list) + groups = property(__get_groups, __set_groups) + stat_line = property(__get_stat_line) + +class _ResultsHandler(handler.ContentHandler): + def __init__(self, get_file): + self.group = None + self.dupes = None + self.marked = set() + self.groups = [] + self.get_file = get_file + + def startElement(self, name, attrs): + if name == 'group': + self.group = engine.Group() + self.dupes = [] + return + if (name == 'file') and (self.group is not None): + if not (('path' in attrs) and ('words' in attrs)): + return + path = attrs['path'] + file = self.get_file(path) + if file is None: + return + file.words = attrs['words'].split(',') + file.is_ref = attrs.get('is_ref') == 'y' + self.dupes.append(file) + if attrs.get('marked') == 'y': + self.marked.add(file) + if (name == 'match') and (self.group is not None): + try: + first_file = self.dupes[int(attrs['first'])] + second_file = self.dupes[int(attrs['second'])] + percentage = int(attrs['percentage']) + self.group.add_match(engine.Match(first_file, second_file, percentage)) + except (IndexError, KeyError, ValueError): # Covers missing attr, non-int values and indexes out of bounds + pass + + def endElement(self, name): + def do_match(ref_file, other_files, group): + if not other_files: + return + for other_file in other_files: + group.add_match(engine.get_match(ref_file, other_file)) + do_match(other_files[0], other_files[1:], group) + + if name == 'group': + group = self.group + self.group = None + dupes = self.dupes + self.dupes = [] + if group is None: + return + if len(dupes) < 2: + return + if not group.matches: # elements not present, do it manually, without % + do_match(dupes[0], dupes[1:], group) + group.prioritize(lambda x: dupes.index(x)) + self.groups.append(group) + diff --git a/pe/qt/dupeguru/results_test.py b/pe/qt/dupeguru/results_test.py new file mode 100644 index 00000000..1e74efc6 --- /dev/null +++ b/pe/qt/dupeguru/results_test.py @@ -0,0 +1,742 @@ +#!/usr/bin/env python +""" +Unit Name: dupeguru.tests.results +Created By: Virgil Dupras +Created On: 2006/02/23 +Last modified by:$Author: virgil $ +Last modified on:$Date: 2009-05-28 15:22:39 +0200 (Thu, 28 May 2009) $ + $Revision: 4385 $ +Copyright 2004-2006 Hardcoded Software (http://www.hardcoded.net) +""" +import unittest +import StringIO +import xml.dom.minidom +import os.path as op + +from hsutil.path import Path +from hsutil.testcase import TestCase +from hsutil.misc import first + +from . import engine_test +from . import data +from . import engine +from .results import * + +class NamedObject(engine_test.NamedObject): + size = 1 + path = property(lambda x:Path('basepath') + x.name) + is_ref = False + + def __nonzero__(self): + return False #Make sure that operations are made correctly when the bool value of files is false. + +# Returns a group set that looks like that: +# "foo bar" (1) +# "bar bleh" (1024) +# "foo bleh" (1) +# "ibabtu" (1) +# "ibabtu" (1) +def GetTestGroups(): + objects = [NamedObject("foo bar"),NamedObject("bar bleh"),NamedObject("foo bleh"),NamedObject("ibabtu"),NamedObject("ibabtu")] + objects[1].size = 1024 + matches = engine.MatchFactory().getmatches(objects) #we should have 5 matches + groups = engine.get_groups(matches) #We should have 2 groups + for g in groups: + g.prioritize(lambda x:objects.index(x)) #We want the dupes to be in the same order as the list is + groups.sort(key=len, reverse=True) # We want the group with 3 members to be first. + return (objects,matches,groups) + +class TCResultsEmpty(TestCase): + def setUp(self): + self.results = Results(data) + + def test_stat_line(self): + self.assertEqual("0 / 0 (0.00 B / 0.00 B) duplicates marked.",self.results.stat_line) + + def test_groups(self): + self.assertEqual(0,len(self.results.groups)) + + def test_get_group_of_duplicate(self): + self.assert_(self.results.get_group_of_duplicate('foo') is None) + + def test_save_to_xml(self): + f = StringIO.StringIO() + self.results.save_to_xml(f) + f.seek(0) + doc = xml.dom.minidom.parse(f) + root = doc.documentElement + self.assertEqual('results',root.nodeName) + + +class TCResultsWithSomeGroups(TestCase): + def setUp(self): + self.results = Results(data) + self.objects,self.matches,self.groups = GetTestGroups() + self.results.groups = self.groups + + def test_stat_line(self): + self.assertEqual("0 / 3 (0.00 B / 1.01 KB) duplicates marked.",self.results.stat_line) + + def test_groups(self): + self.assertEqual(2,len(self.results.groups)) + + def test_get_group_of_duplicate(self): + for o in self.objects: + g = self.results.get_group_of_duplicate(o) + self.assert_(isinstance(g, engine.Group)) + self.assert_(o in g) + self.assert_(self.results.get_group_of_duplicate(self.groups[0]) is None) + + def test_remove_duplicates(self): + g1,g2 = self.results.groups + self.results.remove_duplicates([g1.dupes[0]]) + self.assertEqual(2,len(g1)) + self.assert_(g1 in self.results.groups) + self.results.remove_duplicates([g1.ref]) + self.assertEqual(2,len(g1)) + self.assert_(g1 in self.results.groups) + self.results.remove_duplicates([g1.dupes[0]]) + self.assertEqual(0,len(g1)) + self.assert_(g1 not in self.results.groups) + self.results.remove_duplicates([g2.dupes[0]]) + self.assertEqual(0,len(g2)) + self.assert_(g2 not in self.results.groups) + self.assertEqual(0,len(self.results.groups)) + + def test_remove_duplicates_with_ref_files(self): + g1,g2 = self.results.groups + self.objects[0].is_ref = True + self.objects[1].is_ref = True + self.results.remove_duplicates([self.objects[2]]) + self.assertEqual(0,len(g1)) + self.assert_(g1 not in self.results.groups) + + def test_make_ref(self): + g = self.results.groups[0] + d = g.dupes[0] + self.results.make_ref(d) + self.assert_(d is g.ref) + + def test_sort_groups(self): + self.results.make_ref(self.objects[1]) #We want to make the 1024 sized object to go ref. + g1,g2 = self.groups + self.results.sort_groups(2) #2 is the key for size + self.assert_(self.results.groups[0] is g2) + self.assert_(self.results.groups[1] is g1) + self.results.sort_groups(2,False) + self.assert_(self.results.groups[0] is g1) + self.assert_(self.results.groups[1] is g2) + + def test_set_groups_when_sorted(self): + self.results.make_ref(self.objects[1]) #We want to make the 1024 sized object to go ref. + self.results.sort_groups(2) + objects,matches,groups = GetTestGroups() + g1,g2 = groups + g1.switch_ref(objects[1]) + self.results.groups = groups + self.assert_(self.results.groups[0] is g2) + self.assert_(self.results.groups[1] is g1) + + def test_get_dupe_list(self): + self.assertEqual([self.objects[1],self.objects[2],self.objects[4]],self.results.dupes) + + def test_dupe_list_is_cached(self): + self.assert_(self.results.dupes is self.results.dupes) + + def test_dupe_list_cache_is_invalidated_when_needed(self): + o1,o2,o3,o4,o5 = self.objects + self.assertEqual([o2,o3,o5],self.results.dupes) + self.results.make_ref(o2) + self.assertEqual([o1,o3,o5],self.results.dupes) + objects,matches,groups = GetTestGroups() + o1,o2,o3,o4,o5 = objects + self.results.groups = groups + self.assertEqual([o2,o3,o5],self.results.dupes) + + def test_dupe_list_sort(self): + o1,o2,o3,o4,o5 = self.objects + o1.size = 5 + o2.size = 4 + o3.size = 3 + o4.size = 2 + o5.size = 1 + self.results.sort_dupes(2) + self.assertEqual([o5,o3,o2],self.results.dupes) + self.results.sort_dupes(2,False) + self.assertEqual([o2,o3,o5],self.results.dupes) + + def test_dupe_list_remember_sort(self): + o1,o2,o3,o4,o5 = self.objects + o1.size = 5 + o2.size = 4 + o3.size = 3 + o4.size = 2 + o5.size = 1 + self.results.sort_dupes(2) + self.results.make_ref(o2) + self.assertEqual([o5,o3,o1],self.results.dupes) + + def test_dupe_list_sort_delta_values(self): + o1,o2,o3,o4,o5 = self.objects + o1.size = 10 + o2.size = 2 #-8 + o3.size = 3 #-7 + o4.size = 20 + o5.size = 1 #-19 + self.results.sort_dupes(2,delta=True) + self.assertEqual([o5,o2,o3],self.results.dupes) + + def test_sort_empty_list(self): + #There was an infinite loop when sorting an empty list. + r = Results(data) + r.sort_dupes(0) + self.assertEqual([],r.dupes) + + def test_dupe_list_update_on_remove_duplicates(self): + o1,o2,o3,o4,o5 = self.objects + self.assertEqual(3,len(self.results.dupes)) + self.results.remove_duplicates([o2]) + self.assertEqual(2,len(self.results.dupes)) + + +class TCResultsMarkings(TestCase): + def setUp(self): + self.results = Results(data) + self.objects,self.matches,self.groups = GetTestGroups() + self.results.groups = self.groups + + def test_stat_line(self): + self.assertEqual("0 / 3 (0.00 B / 1.01 KB) duplicates marked.",self.results.stat_line) + self.results.mark(self.objects[1]) + self.assertEqual("1 / 3 (1.00 KB / 1.01 KB) duplicates marked.",self.results.stat_line) + self.results.mark_invert() + self.assertEqual("2 / 3 (2.00 B / 1.01 KB) duplicates marked.",self.results.stat_line) + self.results.mark_invert() + self.results.unmark(self.objects[1]) + self.results.mark(self.objects[2]) + self.results.mark(self.objects[4]) + self.assertEqual("2 / 3 (2.00 B / 1.01 KB) duplicates marked.",self.results.stat_line) + self.results.mark(self.objects[0]) #this is a ref, it can't be counted + self.assertEqual("2 / 3 (2.00 B / 1.01 KB) duplicates marked.",self.results.stat_line) + self.results.groups = self.groups + self.assertEqual("0 / 3 (0.00 B / 1.01 KB) duplicates marked.",self.results.stat_line) + + def test_with_ref_duplicate(self): + self.objects[1].is_ref = True + self.results.groups = self.groups + self.assert_(not self.results.mark(self.objects[1])) + self.results.mark(self.objects[2]) + self.assertEqual("1 / 2 (1.00 B / 2.00 B) duplicates marked.",self.results.stat_line) + + def test_perform_on_marked(self): + def log_object(o): + log.append(o) + return True + + log = [] + self.results.mark_all() + self.results.perform_on_marked(log_object,False) + self.assert_(self.objects[1] in log) + self.assert_(self.objects[2] in log) + self.assert_(self.objects[4] in log) + self.assertEqual(3,len(log)) + log = [] + self.results.mark_none() + self.results.mark(self.objects[4]) + self.results.perform_on_marked(log_object,True) + self.assertEqual(1,len(log)) + self.assert_(self.objects[4] in log) + self.assertEqual(1,len(self.results.groups)) + + def test_perform_on_marked_with_problems(self): + def log_object(o): + log.append(o) + return o is not self.objects[1] + + log = [] + self.results.mark_all() + self.assert_(self.results.is_marked(self.objects[1])) + self.assertEqual(1,self.results.perform_on_marked(log_object, True)) + self.assertEqual(3,len(log)) + self.assertEqual(1,len(self.results.groups)) + self.assertEqual(2,len(self.results.groups[0])) + self.assert_(self.objects[1] in self.results.groups[0]) + self.assert_(not self.results.is_marked(self.objects[2])) + self.assert_(self.results.is_marked(self.objects[1])) + + def test_perform_on_marked_with_ref(self): + def log_object(o): + log.append(o) + return True + + log = [] + self.objects[0].is_ref = True + self.objects[1].is_ref = True + self.results.mark_all() + self.results.perform_on_marked(log_object,True) + self.assert_(self.objects[1] not in log) + self.assert_(self.objects[2] in log) + self.assert_(self.objects[4] in log) + self.assertEqual(2,len(log)) + self.assertEqual(0,len(self.results.groups)) + + def test_perform_on_marked_remove_objects_only_at_the_end(self): + def check_groups(o): + self.assertEqual(3,len(g1)) + self.assertEqual(2,len(g2)) + return True + + g1,g2 = self.results.groups + self.results.mark_all() + self.results.perform_on_marked(check_groups,True) + self.assertEqual(0,len(g1)) + self.assertEqual(0,len(g2)) + self.assertEqual(0,len(self.results.groups)) + + def test_remove_duplicates(self): + g1 = self.results.groups[0] + g2 = self.results.groups[1] + self.results.mark(g1.dupes[0]) + self.assertEqual("1 / 3 (1.00 KB / 1.01 KB) duplicates marked.",self.results.stat_line) + self.results.remove_duplicates([g1.dupes[1]]) + self.assertEqual("1 / 2 (1.00 KB / 1.01 KB) duplicates marked.",self.results.stat_line) + self.results.remove_duplicates([g1.dupes[0]]) + self.assertEqual("0 / 1 (0.00 B / 1.00 B) duplicates marked.",self.results.stat_line) + + def test_make_ref(self): + g = self.results.groups[0] + d = g.dupes[0] + self.results.mark(d) + self.assertEqual("1 / 3 (1.00 KB / 1.01 KB) duplicates marked.",self.results.stat_line) + self.results.make_ref(d) + self.assertEqual("0 / 3 (0.00 B / 3.00 B) duplicates marked.",self.results.stat_line) + self.results.make_ref(d) + self.assertEqual("0 / 3 (0.00 B / 3.00 B) duplicates marked.",self.results.stat_line) + + def test_SaveXML(self): + self.results.mark(self.objects[1]) + self.results.mark_invert() + f = StringIO.StringIO() + self.results.save_to_xml(f) + f.seek(0) + doc = xml.dom.minidom.parse(f) + root = doc.documentElement + g1,g2 = root.getElementsByTagName('group') + d1,d2,d3 = g1.getElementsByTagName('file') + self.assertEqual('n',d1.getAttributeNode('marked').nodeValue) + self.assertEqual('n',d2.getAttributeNode('marked').nodeValue) + self.assertEqual('y',d3.getAttributeNode('marked').nodeValue) + d1,d2 = g2.getElementsByTagName('file') + self.assertEqual('n',d1.getAttributeNode('marked').nodeValue) + self.assertEqual('y',d2.getAttributeNode('marked').nodeValue) + + def test_LoadXML(self): + def get_file(path): + return [f for f in self.objects if str(f.path) == path][0] + + self.objects[4].name = 'ibabtu 2' #we can't have 2 files with the same path + self.results.mark(self.objects[1]) + self.results.mark_invert() + f = StringIO.StringIO() + self.results.save_to_xml(f) + f.seek(0) + r = Results(data) + r.load_from_xml(f,get_file) + self.assert_(not r.is_marked(self.objects[0])) + self.assert_(not r.is_marked(self.objects[1])) + self.assert_(r.is_marked(self.objects[2])) + self.assert_(not r.is_marked(self.objects[3])) + self.assert_(r.is_marked(self.objects[4])) + + +class TCResultsXML(TestCase): + def setUp(self): + self.results = Results(data) + self.objects, self.matches, self.groups = GetTestGroups() + self.results.groups = self.groups + + def get_file(self, path): # use this as a callback for load_from_xml + return [o for o in self.objects if o.path == path][0] + + def test_save_to_xml(self): + self.objects[0].is_ref = True + self.objects[0].words = [['foo','bar']] + f = StringIO.StringIO() + self.results.save_to_xml(f) + f.seek(0) + doc = xml.dom.minidom.parse(f) + root = doc.documentElement + self.assertEqual('results',root.nodeName) + children = [c for c in root.childNodes if c.localName] + self.assertEqual(2,len(children)) + self.assertEqual(2,len([c for c in children if c.nodeName == 'group'])) + g1,g2 = children + children = [c for c in g1.childNodes if c.localName] + self.assertEqual(6,len(children)) + self.assertEqual(3,len([c for c in children if c.nodeName == 'file'])) + self.assertEqual(3,len([c for c in children if c.nodeName == 'match'])) + d1,d2,d3 = [c for c in children if c.nodeName == 'file'] + self.assertEqual(op.join('basepath','foo bar'),d1.getAttributeNode('path').nodeValue) + self.assertEqual(op.join('basepath','bar bleh'),d2.getAttributeNode('path').nodeValue) + self.assertEqual(op.join('basepath','foo bleh'),d3.getAttributeNode('path').nodeValue) + self.assertEqual('y',d1.getAttributeNode('is_ref').nodeValue) + self.assertEqual('n',d2.getAttributeNode('is_ref').nodeValue) + self.assertEqual('n',d3.getAttributeNode('is_ref').nodeValue) + self.assertEqual('foo,bar',d1.getAttributeNode('words').nodeValue) + self.assertEqual('bar,bleh',d2.getAttributeNode('words').nodeValue) + self.assertEqual('foo,bleh',d3.getAttributeNode('words').nodeValue) + children = [c for c in g2.childNodes if c.localName] + self.assertEqual(3,len(children)) + self.assertEqual(2,len([c for c in children if c.nodeName == 'file'])) + self.assertEqual(1,len([c for c in children if c.nodeName == 'match'])) + d1,d2 = [c for c in children if c.nodeName == 'file'] + self.assertEqual(op.join('basepath','ibabtu'),d1.getAttributeNode('path').nodeValue) + self.assertEqual(op.join('basepath','ibabtu'),d2.getAttributeNode('path').nodeValue) + self.assertEqual('n',d1.getAttributeNode('is_ref').nodeValue) + self.assertEqual('n',d2.getAttributeNode('is_ref').nodeValue) + self.assertEqual('ibabtu',d1.getAttributeNode('words').nodeValue) + self.assertEqual('ibabtu',d2.getAttributeNode('words').nodeValue) + + def test_save_to_xml_with_columns(self): + class FakeDataModule: + def GetDisplayInfo(self,dupe,group): + return [str(dupe.size),dupe.foo.upper()] + + for i,object in enumerate(self.objects): + object.size = i + object.foo = u'bar\u00e9' + f = StringIO.StringIO() + self.results.data = FakeDataModule() + self.results.save_to_xml(f,True) + f.seek(0) + doc = xml.dom.minidom.parse(f) + root = doc.documentElement + g1,g2 = root.getElementsByTagName('group') + d1,d2,d3 = g1.getElementsByTagName('file') + d4,d5 = g2.getElementsByTagName('file') + self.assertEqual('0',d1.getElementsByTagName('data')[0].getAttribute('value')) + self.assertEqual(u'BAR\u00c9',d1.getElementsByTagName('data')[1].getAttribute('value')) #\u00c9 is upper of \u00e9 + self.assertEqual('1',d2.getElementsByTagName('data')[0].getAttribute('value')) + self.assertEqual('2',d3.getElementsByTagName('data')[0].getAttribute('value')) + self.assertEqual('3',d4.getElementsByTagName('data')[0].getAttribute('value')) + self.assertEqual('4',d5.getElementsByTagName('data')[0].getAttribute('value')) + + def test_LoadXML(self): + def get_file(path): + return [f for f in self.objects if str(f.path) == path][0] + + self.objects[0].is_ref = True + self.objects[4].name = 'ibabtu 2' #we can't have 2 files with the same path + f = StringIO.StringIO() + self.results.save_to_xml(f) + f.seek(0) + r = Results(data) + r.load_from_xml(f,get_file) + self.assertEqual(2,len(r.groups)) + g1,g2 = r.groups + self.assertEqual(3,len(g1)) + self.assert_(g1[0].is_ref) + self.assert_(not g1[1].is_ref) + self.assert_(not g1[2].is_ref) + self.assert_(g1[0] is self.objects[0]) + self.assert_(g1[1] is self.objects[1]) + self.assert_(g1[2] is self.objects[2]) + self.assertEqual(['foo','bar'],g1[0].words) + self.assertEqual(['bar','bleh'],g1[1].words) + self.assertEqual(['foo','bleh'],g1[2].words) + self.assertEqual(2,len(g2)) + self.assert_(not g2[0].is_ref) + self.assert_(not g2[1].is_ref) + self.assert_(g2[0] is self.objects[3]) + self.assert_(g2[1] is self.objects[4]) + self.assertEqual(['ibabtu'],g2[0].words) + self.assertEqual(['ibabtu'],g2[1].words) + + def test_LoadXML_with_filename(self): + def get_file(path): + return [f for f in self.objects if str(f.path) == path][0] + + filename = op.join(self.tmpdir(), 'dupeguru_results.xml') + self.objects[4].name = 'ibabtu 2' #we can't have 2 files with the same path + self.results.save_to_xml(filename) + r = Results(data) + r.load_from_xml(filename,get_file) + self.assertEqual(2,len(r.groups)) + + def test_LoadXML_with_some_files_that_dont_exist_anymore(self): + def get_file(path): + if path.endswith('ibabtu 2'): + return None + return [f for f in self.objects if str(f.path) == path][0] + + self.objects[4].name = 'ibabtu 2' #we can't have 2 files with the same path + f = StringIO.StringIO() + self.results.save_to_xml(f) + f.seek(0) + r = Results(data) + r.load_from_xml(f,get_file) + self.assertEqual(1,len(r.groups)) + self.assertEqual(3,len(r.groups[0])) + + def test_LoadXML_missing_attributes_and_bogus_elements(self): + def get_file(path): + return [f for f in self.objects if str(f.path) == path][0] + + doc = xml.dom.minidom.Document() + root = doc.appendChild(doc.createElement('foobar')) #The root element shouldn't matter, really. + group_node = root.appendChild(doc.createElement('group')) + dupe_node = group_node.appendChild(doc.createElement('file')) #Perfectly correct file + dupe_node.setAttribute('path',op.join('basepath','foo bar')) + dupe_node.setAttribute('is_ref','y') + dupe_node.setAttribute('words','foo,bar') + dupe_node = group_node.appendChild(doc.createElement('file')) #is_ref missing, default to 'n' + dupe_node.setAttribute('path',op.join('basepath','foo bleh')) + dupe_node.setAttribute('words','foo,bleh') + dupe_node = group_node.appendChild(doc.createElement('file')) #words are missing, invalid. + dupe_node.setAttribute('path',op.join('basepath','bar bleh')) + dupe_node = group_node.appendChild(doc.createElement('file')) #path is missing, invalid. + dupe_node.setAttribute('words','foo,bleh') + dupe_node = group_node.appendChild(doc.createElement('foobar')) #Invalid element name + dupe_node.setAttribute('path',op.join('basepath','bar bleh')) + dupe_node.setAttribute('is_ref','y') + dupe_node.setAttribute('words','bar,bleh') + match_node = group_node.appendChild(doc.createElement('match')) # match pointing to a bad index + match_node.setAttribute('first', '42') + match_node.setAttribute('second', '45') + match_node = group_node.appendChild(doc.createElement('match')) # match with missing attrs + match_node = group_node.appendChild(doc.createElement('match')) # match with non-int values + match_node.setAttribute('first', 'foo') + match_node.setAttribute('second', 'bar') + match_node.setAttribute('percentage', 'baz') + group_node = root.appendChild(doc.createElement('foobar')) #invalid group + group_node = root.appendChild(doc.createElement('group')) #empty group + f = StringIO.StringIO() + doc.writexml(f,'\t','\t','\n',encoding='utf-8') + f.seek(0) + r = Results(data) + r.load_from_xml(f,get_file) + self.assertEqual(1,len(r.groups)) + self.assertEqual(2,len(r.groups[0])) + + def test_xml_non_ascii(self): + def get_file(path): + if path == op.join('basepath',u'\xe9foo bar'): + return objects[0] + if path == op.join('basepath',u'bar bleh'): + return objects[1] + + objects = [NamedObject(u"\xe9foo bar",True),NamedObject("bar bleh",True)] + matches = engine.MatchFactory().getmatches(objects) #we should have 5 matches + groups = engine.get_groups(matches) #We should have 2 groups + for g in groups: + g.prioritize(lambda x:objects.index(x)) #We want the dupes to be in the same order as the list is + results = Results(data) + results.groups = groups + f = StringIO.StringIO() + results.save_to_xml(f) + f.seek(0) + r = Results(data) + r.load_from_xml(f,get_file) + g = r.groups[0] + self.assertEqual(u"\xe9foo bar",g[0].name) + self.assertEqual(['efoo','bar'],g[0].words) + + def test_load_invalid_xml(self): + f = StringIO.StringIO() + f.write(' len(ref.path) + + def GetDupeGroups(self, files, j=job.nulljob): + j = j.start_subjob([8, 2]) + for f in [f for f in files if not hasattr(f, 'is_ref')]: + f.is_ref = False + if self.size_threshold: + files = [f for f in files if f.size >= self.size_threshold] + logging.info('Getting matches') + if self.match_factory is None: + matches = self._getmatches(files, j) + else: + matches = self.match_factory.getmatches(files, j) + logging.info('Found %d matches' % len(matches)) + if not self.mix_file_kind: + j.set_progress(100, 'Removing false matches') + matches = [m for m in matches if get_file_ext(m.first.name) == get_file_ext(m.second.name)] + if self.ignore_list: + j = j.start_subjob(2) + iter_matches = j.iter_with_progress(matches, 'Processed %d/%d matches against the ignore list') + matches = [m for m in iter_matches + if not self.ignore_list.AreIgnored(unicode(m.first.path), unicode(m.second.path))] + matched_files = dedupe([m.first for m in matches] + [m.second for m in matches]) + if self.scan_type in (SCAN_TYPE_CONTENT, SCAN_TYPE_CONTENT_AUDIO): + md5attrname = 'md5partial' if self.scan_type == SCAN_TYPE_CONTENT_AUDIO else 'md5' + md5 = lambda f: getattr(f, md5attrname) + j = j.start_subjob(2) + for matched_file in j.iter_with_progress(matched_files, 'Analyzed %d/%d matching files'): + md5(matched_file) + j.set_progress(100, 'Removing false matches') + matches = [m for m in matches if md5(m.first) == md5(m.second)] + words_for_content = ['--'] # We compared md5. No words were involved. + for m in matches: + m.first.words = words_for_content + m.second.words = words_for_content + logging.info('Grouping matches') + groups = engine.get_groups(matches, j) + groups = [g for g in groups if any(not f.is_ref for f in g)] + logging.info('Created %d groups' % len(groups)) + j.set_progress(100, 'Doing group prioritization') + for g in groups: + g.prioritize(self._key_func, self._tie_breaker) + matched_files = dedupe([m.first for m in matches] + [m.second for m in matches]) + self.discarded_file_count = len(matched_files) - sum(len(g) for g in groups) + return groups + + match_factory = None + match_similar_words = False + min_match_percentage = 80 + mix_file_kind = True + scan_type = SCAN_TYPE_FILENAME + scanned_tags = set(['artist', 'title']) + size_threshold = 0 + word_weighting = False + +class ScannerME(Scanner): # Scanner for Music Edition + @staticmethod + def _key_func(dupe): + return (not dupe.is_ref, -dupe.bitrate, -dupe.size) + diff --git a/pe/qt/dupeguru/scanner_test.py b/pe/qt/dupeguru/scanner_test.py new file mode 100644 index 00000000..89ad1417 --- /dev/null +++ b/pe/qt/dupeguru/scanner_test.py @@ -0,0 +1,468 @@ +#!/usr/bin/env python +""" +Unit Name: dupeguru.tests.scanner +Created By: Virgil Dupras +Created On: 2006/03/03 +Last modified by:$Author: virgil $ +Last modified on:$Date: 2009-05-28 15:22:39 +0200 (Thu, 28 May 2009) $ + $Revision: 4385 $ +Copyright 2004-2006 Hardcoded Software (http://www.hardcoded.net) +""" +import unittest + +from hsutil import job +from hsutil.path import Path +from hsutil.testcase import TestCase + +from .engine import getwords, Match +from .ignore import IgnoreList +from .scanner import * + +class NamedObject(object): + def __init__(self, name="foobar", size=1): + self.name = name + self.size = size + self.path = Path('') + self.words = getwords(name) + + +no = NamedObject + +class TCScanner(TestCase): + def test_empty(self): + s = Scanner() + r = s.GetDupeGroups([]) + self.assertEqual([],r) + + def test_default_settings(self): + s = Scanner() + self.assertEqual(80,s.min_match_percentage) + self.assertEqual(SCAN_TYPE_FILENAME,s.scan_type) + self.assertEqual(True,s.mix_file_kind) + self.assertEqual(False,s.word_weighting) + self.assertEqual(False,s.match_similar_words) + self.assert_(isinstance(s.ignore_list,IgnoreList)) + + def test_simple_with_default_settings(self): + s = Scanner() + f = [no('foo bar'),no('foo bar'),no('foo bleh')] + r = s.GetDupeGroups(f) + self.assertEqual(1,len(r)) + g = r[0] + #'foo bleh' cannot be in the group because the default min match % is 80 + self.assertEqual(2,len(g)) + self.assert_(g.ref in f[:2]) + self.assert_(g.dupes[0] in f[:2]) + + def test_simple_with_lower_min_match(self): + s = Scanner() + s.min_match_percentage = 50 + f = [no('foo bar'),no('foo bar'),no('foo bleh')] + r = s.GetDupeGroups(f) + self.assertEqual(1,len(r)) + g = r[0] + self.assertEqual(3,len(g)) + + def test_trim_all_ref_groups(self): + s = Scanner() + f = [no('foo'),no('foo'),no('bar'),no('bar')] + f[2].is_ref = True + f[3].is_ref = True + r = s.GetDupeGroups(f) + self.assertEqual(1,len(r)) + + def test_priorize(self): + s = Scanner() + f = [no('foo'),no('foo'),no('bar'),no('bar')] + f[1].size = 2 + f[2].size = 3 + f[3].is_ref = True + r = s.GetDupeGroups(f) + g1,g2 = r + self.assert_(f[1] in (g1.ref,g2.ref)) + self.assert_(f[0] in (g1.dupes[0],g2.dupes[0])) + self.assert_(f[3] in (g1.ref,g2.ref)) + self.assert_(f[2] in (g1.dupes[0],g2.dupes[0])) + + def test_content_scan(self): + s = Scanner() + s.scan_type = SCAN_TYPE_CONTENT + f = [no('foo'), no('bar'), no('bleh')] + f[0].md5 = 'foobar' + f[1].md5 = 'foobar' + f[2].md5 = 'bleh' + r = s.GetDupeGroups(f) + self.assertEqual(len(r), 1) + self.assertEqual(len(r[0]), 2) + self.assertEqual(s.discarded_file_count, 0) # don't count the different md5 as discarded! + + def test_content_scan_compare_sizes_first(self): + class MyFile(no): + def get_md5(file): + self.fail() + md5 = property(get_md5) + + s = Scanner() + s.scan_type = SCAN_TYPE_CONTENT + f = [MyFile('foo',1),MyFile('bar',2)] + self.assertEqual(0,len(s.GetDupeGroups(f))) + + def test_min_match_perc_doesnt_matter_for_content_scan(self): + s = Scanner() + s.scan_type = SCAN_TYPE_CONTENT + f = [no('foo'),no('bar'),no('bleh')] + f[0].md5 = 'foobar' + f[1].md5 = 'foobar' + f[2].md5 = 'bleh' + s.min_match_percentage = 101 + r = s.GetDupeGroups(f) + self.assertEqual(1,len(r)) + self.assertEqual(2,len(r[0])) + s.min_match_percentage = 0 + r = s.GetDupeGroups(f) + self.assertEqual(1,len(r)) + self.assertEqual(2,len(r[0])) + + def test_content_scan_puts_md5_in_words_at_the_end(self): + s = Scanner() + s.scan_type = SCAN_TYPE_CONTENT + f = [no('foo'),no('bar')] + f[0].md5 = '\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f' + f[1].md5 = '\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f' + r = s.GetDupeGroups(f) + g = r[0] + self.assertEqual(['--'],g.ref.words) + self.assertEqual(['--'],g.dupes[0].words) + + def test_extension_is_not_counted_in_filename_scan(self): + s = Scanner() + s.min_match_percentage = 100 + f = [no('foo.bar'),no('foo.bleh')] + r = s.GetDupeGroups(f) + self.assertEqual(1,len(r)) + self.assertEqual(2,len(r[0])) + + def test_job(self): + def do_progress(progress,desc=''): + log.append(progress) + return True + s = Scanner() + log = [] + f = [no('foo bar'),no('foo bar'),no('foo bleh')] + r = s.GetDupeGroups(f, job.Job(1,do_progress)) + self.assertEqual(0,log[0]) + self.assertEqual(100,log[-1]) + + def test_mix_file_kind(self): + s = Scanner() + s.mix_file_kind = False + f = [no('foo.1'),no('foo.2')] + r = s.GetDupeGroups(f) + self.assertEqual(0,len(r)) + + def test_word_weighting(self): + s = Scanner() + s.min_match_percentage = 75 + s.word_weighting = True + f = [no('foo bar'),no('foo bar bleh')] + r = s.GetDupeGroups(f) + self.assertEqual(1,len(r)) + g = r[0] + m = g.get_match_of(g.dupes[0]) + self.assertEqual(75,m.percentage) # 16 letters, 12 matching + + def test_similar_words(self): + s = Scanner() + s.match_similar_words = True + f = [no('The White Stripes'),no('The Whites Stripe'),no('Limp Bizkit'),no('Limp Bizkitt')] + r = s.GetDupeGroups(f) + self.assertEqual(2,len(r)) + + def test_fields(self): + s = Scanner() + s.scan_type = SCAN_TYPE_FIELDS + f = [no('The White Stripes - Little Ghost'),no('The White Stripes - Little Acorn')] + r = s.GetDupeGroups(f) + self.assertEqual(0,len(r)) + + def test_fields_no_order(self): + s = Scanner() + s.scan_type = SCAN_TYPE_FIELDS_NO_ORDER + f = [no('The White Stripes - Little Ghost'),no('Little Ghost - The White Stripes')] + r = s.GetDupeGroups(f) + self.assertEqual(1,len(r)) + + def test_tag_scan(self): + s = Scanner() + s.scan_type = SCAN_TYPE_TAG + o1 = no('foo') + o2 = no('bar') + o1.artist = 'The White Stripes' + o1.title = 'The Air Near My Fingers' + o2.artist = 'The White Stripes' + o2.title = 'The Air Near My Fingers' + r = s.GetDupeGroups([o1,o2]) + self.assertEqual(1,len(r)) + + def test_tag_with_album_scan(self): + s = Scanner() + s.scan_type = SCAN_TYPE_TAG_WITH_ALBUM + o1 = no('foo') + o2 = no('bar') + o3 = no('bleh') + o1.artist = 'The White Stripes' + o1.title = 'The Air Near My Fingers' + o1.album = 'Elephant' + o2.artist = 'The White Stripes' + o2.title = 'The Air Near My Fingers' + o2.album = 'Elephant' + o3.artist = 'The White Stripes' + o3.title = 'The Air Near My Fingers' + o3.album = 'foobar' + r = s.GetDupeGroups([o1,o2,o3]) + self.assertEqual(1,len(r)) + + def test_that_dash_in_tags_dont_create_new_fields(self): + s = Scanner() + s.scan_type = SCAN_TYPE_TAG_WITH_ALBUM + s.min_match_percentage = 50 + o1 = no('foo') + o2 = no('bar') + o1.artist = 'The White Stripes - a' + o1.title = 'The Air Near My Fingers - a' + o1.album = 'Elephant - a' + o2.artist = 'The White Stripes - b' + o2.title = 'The Air Near My Fingers - b' + o2.album = 'Elephant - b' + r = s.GetDupeGroups([o1,o2]) + self.assertEqual(1,len(r)) + + def test_tag_scan_with_different_scanned(self): + s = Scanner() + s.scan_type = SCAN_TYPE_TAG + s.scanned_tags = set(['track', 'year']) + o1 = no('foo') + o2 = no('bar') + o1.artist = 'The White Stripes' + o1.title = 'some title' + o1.track = 'foo' + o1.year = 'bar' + o2.artist = 'The White Stripes' + o2.title = 'another title' + o2.track = 'foo' + o2.year = 'bar' + r = s.GetDupeGroups([o1, o2]) + self.assertEqual(1, len(r)) + + def test_tag_scan_only_scans_existing_tags(self): + s = Scanner() + s.scan_type = SCAN_TYPE_TAG + s.scanned_tags = set(['artist', 'foo']) + o1 = no('foo') + o2 = no('bar') + o1.artist = 'The White Stripes' + o1.foo = 'foo' + o2.artist = 'The White Stripes' + o2.foo = 'bar' + r = s.GetDupeGroups([o1, o2]) + self.assertEqual(1, len(r)) # Because 'foo' is not scanned, they match + + def test_tag_scan_converts_to_str(self): + s = Scanner() + s.scan_type = SCAN_TYPE_TAG + s.scanned_tags = set(['track']) + o1 = no('foo') + o2 = no('bar') + o1.track = 42 + o2.track = 42 + try: + r = s.GetDupeGroups([o1, o2]) + except TypeError: + self.fail() + self.assertEqual(1, len(r)) + + def test_tag_scan_non_ascii(self): + s = Scanner() + s.scan_type = SCAN_TYPE_TAG + s.scanned_tags = set(['title']) + o1 = no('foo') + o2 = no('bar') + o1.title = u'foobar\u00e9' + o2.title = u'foobar\u00e9' + try: + r = s.GetDupeGroups([o1, o2]) + except UnicodeEncodeError: + self.fail() + self.assertEqual(1, len(r)) + + def test_audio_content_scan(self): + s = Scanner() + s.scan_type = SCAN_TYPE_CONTENT_AUDIO + f = [no('foo'),no('bar'),no('bleh')] + f[0].md5 = 'foo' + f[1].md5 = 'bar' + f[2].md5 = 'bleh' + f[0].md5partial = 'foo' + f[1].md5partial = 'foo' + f[2].md5partial = 'bleh' + f[0].audiosize = 1 + f[1].audiosize = 1 + f[2].audiosize = 1 + r = s.GetDupeGroups(f) + self.assertEqual(1,len(r)) + self.assertEqual(2,len(r[0])) + + def test_audio_content_scan_compare_sizes_first(self): + class MyFile(no): + def get_md5(file): + self.fail() + md5partial = property(get_md5) + + s = Scanner() + s.scan_type = SCAN_TYPE_CONTENT_AUDIO + f = [MyFile('foo'),MyFile('bar')] + f[0].audiosize = 1 + f[1].audiosize = 2 + self.assertEqual(0,len(s.GetDupeGroups(f))) + + def test_ignore_list(self): + s = Scanner() + f1 = no('foobar') + f2 = no('foobar') + f3 = no('foobar') + f1.path = Path('dir1/foobar') + f2.path = Path('dir2/foobar') + f3.path = Path('dir3/foobar') + s.ignore_list.Ignore(str(f1.path),str(f2.path)) + s.ignore_list.Ignore(str(f1.path),str(f3.path)) + r = s.GetDupeGroups([f1,f2,f3]) + self.assertEqual(1,len(r)) + g = r[0] + self.assertEqual(1,len(g.dupes)) + self.assert_(f1 not in g) + self.assert_(f2 in g) + self.assert_(f3 in g) + # Ignored matches are not counted as discarded + self.assertEqual(s.discarded_file_count, 0) + + def test_ignore_list_checks_for_unicode(self): + #scanner was calling path_str for ignore list checks. Since the Path changes, it must + #be unicode(path) + s = Scanner() + f1 = no('foobar') + f2 = no('foobar') + f3 = no('foobar') + f1.path = Path(u'foo1\u00e9') + f2.path = Path(u'foo2\u00e9') + f3.path = Path(u'foo3\u00e9') + s.ignore_list.Ignore(unicode(f1.path),unicode(f2.path)) + s.ignore_list.Ignore(unicode(f1.path),unicode(f3.path)) + r = s.GetDupeGroups([f1,f2,f3]) + self.assertEqual(1,len(r)) + g = r[0] + self.assertEqual(1,len(g.dupes)) + self.assert_(f1 not in g) + self.assert_(f2 in g) + self.assert_(f3 in g) + + def test_custom_match_factory(self): + class MatchFactory(object): + def getmatches(self,objects,j=None): + return [Match(objects[0], objects[1], 420)] + + + s = Scanner() + s.match_factory = MatchFactory() + o1,o2 = no('foo'),no('bar') + groups = s.GetDupeGroups([o1,o2]) + self.assertEqual(1,len(groups)) + g = groups[0] + self.assertEqual(2,len(g)) + g.switch_ref(o1) + m = g.get_match_of(o2) + self.assertEqual((o1,o2,420),m) + + def test_file_evaluates_to_false(self): + # A very wrong way to use any() was added at some point, causing resulting group list + # to be empty. + class FalseNamedObject(NamedObject): + def __nonzero__(self): + return False + + + s = Scanner() + f1 = FalseNamedObject('foobar') + f2 = FalseNamedObject('foobar') + r = s.GetDupeGroups([f1,f2]) + self.assertEqual(1,len(r)) + + def test_size_threshold(self): + # Only file equal or higher than the size_threshold in size are scanned + s = Scanner() + f1 = no('foo', 1) + f2 = no('foo', 2) + f3 = no('foo', 3) + s.size_threshold = 2 + groups = s.GetDupeGroups([f1,f2,f3]) + self.assertEqual(len(groups), 1) + [group] = groups + self.assertEqual(len(group), 2) + self.assertTrue(f1 not in group) + self.assertTrue(f2 in group) + self.assertTrue(f3 in group) + + def test_tie_breaker_path_deepness(self): + # If there is a tie in prioritization, path deepness is used as a tie breaker + s = Scanner() + o1, o2 = no('foo'), no('foo') + o1.path = Path('foo') + o2.path = Path('foo/bar') + [group] = s.GetDupeGroups([o1, o2]) + self.assertTrue(group.ref is o2) + + def test_tie_breaker_copy(self): + # if copy is in the words used (even if it has a deeper path), it becomes a dupe + s = Scanner() + o1, o2 = no('foo bar Copy'), no('foo bar') + o1.path = Path('deeper/path') + o2.path = Path('foo') + [group] = s.GetDupeGroups([o1, o2]) + self.assertTrue(group.ref is o2) + + def test_tie_breaker_same_name_plus_digit(self): + # if ref has the same words as dupe, but has some just one extra word which is a digit, it + # becomes a dupe + s = Scanner() + o1, o2 = no('foo bar 42'), no('foo bar') + o1.path = Path('deeper/path') + o2.path = Path('foo') + [group] = s.GetDupeGroups([o1, o2]) + self.assertTrue(group.ref is o2) + + def test_partial_group_match(self): + # Count the number od discarded matches (when a file doesn't match all other dupes of the + # group) in Scanner.discarded_file_count + s = Scanner() + o1, o2, o3 = no('a b'), no('a'), no('b') + s.min_match_percentage = 50 + [group] = s.GetDupeGroups([o1, o2, o3]) + self.assertEqual(len(group), 2) + self.assertTrue(o1 in group) + self.assertTrue(o2 in group) + self.assertTrue(o3 not in group) + self.assertEqual(s.discarded_file_count, 1) + + +class TCScannerME(TestCase): + def test_priorize(self): + # in ScannerME, bitrate goes first (right after is_ref) in priorization + s = ScannerME() + o1, o2 = no('foo'), no('foo') + o1.bitrate = 1 + o2.bitrate = 2 + [group] = s.GetDupeGroups([o1, o2]) + self.assertTrue(group.ref is o2) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/pe/qt/gen.py b/pe/qt/gen.py new file mode 100644 index 00000000..8bec8d5b --- /dev/null +++ b/pe/qt/gen.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python +# Unit Name: gen +# Created By: Virgil Dupras +# Created On: 2009-05-22 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +import os +import os.path as op + +def print_and_do(cmd): + print cmd + os.system(cmd) + +def move(src, dst): + if not op.exists(src): + return + if op.exists(dst): + os.remove(dst) + print 'Moving %s --> %s' % (src, dst) + os.rename(src, dst) + +os.chdir('dupeguru') +print_and_do('python gen.py') +os.chdir('..') + +os.chdir('base') +print_and_do('python gen.py') +os.chdir('..') + +os.chdir(op.join('modules', 'block')) +os.system('python setup.py build_ext --inplace') +move(op.join('modules', 'block', '_block.so'), op.join('picture', '_block.so')) +move(op.join('modules', 'block', '_block.pyd'), op.join('picture', '_block.pyd')) +os.chdir(op.join('..', '..')) + +print_and_do("pyuic4 details_dialog.ui > details_dialog_ui.py") +print_and_do("pyuic4 preferences_dialog.ui > preferences_dialog_ui.py") + +os.chdir('help') +print_and_do('python gen.py') +os.chdir('..') \ No newline at end of file diff --git a/pe/qt/help/changelog.yaml b/pe/qt/help/changelog.yaml new file mode 100644 index 00000000..f387c334 --- /dev/null +++ b/pe/qt/help/changelog.yaml @@ -0,0 +1,174 @@ +- date: 2009-05-27 + version: 1.7.2 + description: | + * Fixed a bug causing '.jpeg' files not to be scanned. + * Fixed a bug causing a GUI freeze at the beginning of a scan with a lot of files. + * Fixed a bug that sometimes caused a crash when an action was cancelled, and then started again. + * Improved scanning speed. +- date: 2009-05-26 + version: 1.7.1 + description: | + * Fixed a bug causing the "Match Scaled" preference to be inverted. +- date: 2009-05-20 + version: 1.7.0 + description: | + * Fixed the bug from 1.6.0 preventing PowerPC macs from running the application. + * Converted the Windows GUI to Qt, thus enabling multiprocessing and making the scanning process + faster. +- date: 2009-03-24 + description: "* **Improved** scanning speed, mainly on OS X where all cores of the\ + \ CPU are now used.\r\n* **Fixed** an occasional crash caused by permission issues.\r\ + \n* **Fixed** a bug where the \"X discarded\" notice would show a too large number\ + \ of discarded duplicates." + version: 1.6.0 +- date: 2008-09-10 + description: "
    \n\t\t\t\t\t\t
  • Added a notice in the status bar when\ + \ matches were discarded during the scan.
  • \n\t\t\t\t\t\t
  • Improved\ + \ duplicate prioritization (smartly chooses which file you will keep).
  • \n\t\ + \t\t\t\t\t
  • Improved scan progress feedback.
  • \n\t\t\t\t\t\t
  • Improved\ + \ responsiveness of the user interface for certain actions.
  • \n\t\t \ + \
" + version: 1.5.0 +- date: 2008-07-28 + description: "
    \n\t\t\t\t\t\t
  • Improved iPhoto compatibility on Mac\ + \ OS X.
  • \n\t\t\t\t\t\t
  • Improved the speed of results loading and\ + \ saving.
  • \n\t\t\t\t\t\t
  • Fixed a crash sometimes occurring during\ + \ duplicate deletion.
  • \n\t\t
" + version: 1.4.2 +- date: 2008-04-12 + description: "
    \n\t\t\t\t\t\t
  • Improved iPhoto Library loading feedback\ + \ on Mac OS X.
  • \n\t\t\t\t\t\t
  • Fixed the directory selection dialog.\ + \ Bundles can be selected again on Mac OS X.
  • \n\t\t\t\t\t\t
  • Fixed\ + \ \"Clear Ignore List\" crash in Windows.
  • \n\t\t
" + version: 1.4.1 +- date: 2008-02-20 + description: "
    \n\t\t\t\t\t\t
  • Added iPhoto Library support on Mac OS\ + \ X.
  • \n\t\t\t\t\t\t
  • Fixed occasional crashes when scanning corrupted\ + \ pictures.
  • \n\t\t
" + version: 1.4.0 +- date: 2008-02-20 + description: "
    \n\t\t\t\t\t\t
  • Added iPhoto Library support on Mac OS\ + \ X.
  • \n\t\t\t\t\t\t
  • Fixed occasional crashes when scanning corrupted\ + \ pictures.
  • \n\t\t
" + version: 1.4.0 +- date: 2008-01-12 + description: "
    \n\t\t\t\t\t\t
  • Improved scan, delete and move speed\ + \ in situations where there were a lot of duplicates.
  • \n\t\t\t\t\t\t
  • Fixed\ + \ occasional crashes when moving a lot of files at once.
  • \n\t\t\t\t\t\t
  • Fixed\ + \ an issue sometimes preventing the application from starting at all.
  • \n\t\ + \t
" + version: 1.3.4 +- date: 2007-12-03 + description: "
    \n\t\t\t\t\t\t
  • Improved the handling of low memory situations.
  • \n\ + \t\t\t\t\t\t
  • Improved the directory panel. The \"Remove\" button changes\ + \ to \"Put Back\" when an excluded directory is selected.
  • \n\t\t\t\t\t\t
  • Fixed\ + \ the directory selection dialog. iPhoto '08 library files can now be selected.
  • \n\ + \t\t
" + version: 1.3.3 +- date: 2007-11-24 + description: "
    \n\t\t\t\t\t\t
  • Added the \"Remove empty folders\" option.
  • \n\ + \t\t\t\t\t\t
  • Fixed results load/save issues.
  • \n\t\t\t\t\t\t
  • Fixed\ + \ occasional status bar inaccuracies when the results are filtered.
  • \n\t\t\ + \
" + version: 1.3.2 +- date: 2007-10-21 + description: "
    \n\t\t\t\t\t\t
  • Improved results loading speed.
  • \n\ + \t\t\t\t\t\t
  • Improved details panel's picture loading (made it asynchronous).
  • \n\ + \t\t\t\t\t\t
  • Fixed a bug where the stats line at the bottom would sometimes\ + \ go confused while having a filter active.
  • \n\t\t\t\t\t\t
  • Fixed\ + \ a bug under Windows where some duplicate markings would be lost.
  • \n\t\t\ + \
" + version: 1.3.1 +- date: 2007-09-22 + description: "
    \n\t\t\t\t\t\t
  • Added post scan filtering.
  • \n\t\t\ + \t\t\t\t
  • Fixed issues with the rename feature under Windows
  • \n\t\ + \t\t\t\t\t
  • Fixed some user interface annoyances under Windows
  • \n\ + \t\t
" + version: 1.3.0 +- date: 2007-05-19 + description: "
    \n\t\t\t\t\t\t
  • Improved UI responsiveness (using threads)\ + \ under Mac OS X.
  • \n\t\t\t\t\t\t
  • Improved result load/save speed\ + \ and memory usage.
  • \n\t\t
" + version: 1.2.1 +- date: 2007-03-17 + description: "
    \n\t\t\t\t\t\t
  • Changed the picture decoding libraries\ + \ for both Mac OS X and Windows. The Mac OS X version uses the Core Graphics library\ + \ and the Windows version uses the .NET framework imaging capabilities. This results\ + \ in much faster scans. As a bonus, the Mac OS X version of dupeGuru PE now supports\ + \ RAW images.
  • \n\t\t
" + version: 1.2.0 +- date: 2007-02-11 + description: "
    \n\t\t\t\t\t\t
  • Added Re-orderable columns. In fact,\ + \ I re-added the feature which was lost in the C# conversion in 2.4.0 (Windows).
  • \n\ + \t\t\t\t\t\t
  • Fixed a bug with all the Delete/Move/Copy actions with\ + \ certain kinds of files.
  • \n\t\t
" + version: 1.1.6 +- date: 2007-01-11 + description: "
    \n\t\t\t\t\t\t
  • Fixed a bug with the Move action.
  • \n\ + \t\t
" + version: 1.1.5 +- date: 2007-01-09 + description: "
    \n\t\t\t\t\t\t
  • Fixed a \"ghosting\" bug. Dupes deleted\ + \ by dupeGuru would sometimes come back in subsequent scans (Windows).
  • \n\t\ + \t\t\t\t\t
  • Fixed bugs sometimes making dupeGuru crash when marking a\ + \ dupe (Windows).
  • \n\t\t\t\t\t\t
  • Fixed some minor visual glitches\ + \ (Windows).
  • \n\t\t
" + version: 1.1.4 +- date: 2006-12-23 + description: "
    \n\t\t\t\t\t\t
  • Improved the caching system. This makes\ + \ duplicate scans significantly faster.
  • \n\t\t\t\t\t\t
  • Improved\ + \ the rename file dialog to exclude the extension from the original selection\ + \ (so when you start typing your new filename, it doesn't overwrite it) (Windows).
  • \n\ + \t\t\t\t\t\t
  • Changed some menu key shortcuts that created conflicts\ + \ (Windows).
  • \n\t\t\t\t\t\t
  • Fixed a bug preventing files from \"\ + reference\" directories to be displayed in blue in the results (Windows).
  • \n\ + \t\t\t\t\t\t
  • Fixed a bug preventing some files to be sent to the recycle\ + \ bin (Windows).
  • \n\t\t\t\t\t\t
  • Fixed a bug with the \"Remove\"\ + \ button of the directories panel (Windows).
  • \n\t\t\t\t\t\t
  • Fixed\ + \ a bug in the packaging preventing certain Windows configurations to start dupeGuru\ + \ at all.
  • \n\t\t
" + version: 1.1.3 +- date: 2006-11-18 + description: "
    \n\t\t\t\t\t\t
  • Fixed a bug with directory states.
  • \n\ + \t\t
" + version: 1.1.2 +- date: 2006-11-17 + description: "
    \n\t\t\t\t\t\t
  • Fixed a bug causing the ignore list not\ + \ to be saved.
  • \n\t\t\t\t\t\t
  • Fixed a bug with selection under Power\ + \ Marker mode.
  • \n\t\t
" + version: 1.1.1 +- date: 2006-11-15 + description: "
    \n\t\t\t\t\t\t
  • Changed the Windows interface. It is\ + \ now .NET based.
  • \n\t\t\t\t\t\t
  • Added an auto-update feature to\ + \ the windows version.
  • \n\t\t\t\t\t\t
  • Changed the way power marking\ + \ works. It is now a mode instead of a separate window.
  • \n\t\t\t\t\t\t
  • Changed\ + \ the \"Size (MB)\" column for a \"Size (KB)\" column. The values are now \"ceiled\"\ + \ instead of rounded. Therefore, a size \"0\" is now really 0 bytes, not just\ + \ a value too small to be rounded up. It is also the case for delta values.
  • \n\ + \t\t\t\t\t\t
  • Fixed a bug sometimes making delete and move operations\ + \ stall.
  • \n\t\t
" + version: 1.1.0 +- date: 2006-10-12 + description: "
    \n\t\t\t\t\t\t
  • Added an auto-update feature in the Mac\ + \ OS X version (with Sparkle).
  • \n\t\t \t
  • Fixed a bug\ + \ sometimes causing inaccuracies of the Match %.
  • \n\t\t
" + version: 1.0.5 +- date: 2006-09-21 + description: "
    \n\t\t \t
  • Fixed a bug with the cache system.
  • \n\ + \t\t
" + version: 1.0.4 +- date: 2006-09-15 + description: "
    \n\t\t\t\t\t\t
  • Added the ability to search for scaled\ + \ duplicates.
  • \n\t\t\t\t\t\t
  • Added a cache system for faster scans.
  • \n\ + \t\t \t
  • Improved speed of the scanning engine.
  • \n\t\t\ + \
" + version: 1.0.3 +- date: 2006-09-11 + description: "
    \n\t\t \t
  • Improved speed of the scanning\ + \ engine.
  • \n\t\t\t\t\t\t
  • Improved the display of pictures in the\ + \ details panel (Windows).
  • \n\t\t
" + version: 1.0.2 +- date: 2006-09-08 + description: "
    \n\t\t \t
  • Initial release.
  • \n\t\t \ + \
" + version: 1.0.0 diff --git a/pe/qt/help/gen.py b/pe/qt/help/gen.py new file mode 100644 index 00000000..8ed33e4e --- /dev/null +++ b/pe/qt/help/gen.py @@ -0,0 +1,12 @@ +#!/usr/bin/env python +# Unit Name: +# Created By: Virgil Dupras +# Created On: 2009-05-24 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +import os + +from web import generate_help + +generate_help.main('.', 'dupeguru_pe_help', force_render=True) diff --git a/pe/qt/help/skeleton/hardcoded.css b/pe/qt/help/skeleton/hardcoded.css new file mode 100644 index 00000000..a3b17c5b --- /dev/null +++ b/pe/qt/help/skeleton/hardcoded.css @@ -0,0 +1,409 @@ +/***************************************************** + General settings +*****************************************************/ + +BODY +{ + background-color:white; +} + +BODY,A,P,UL,TABLE,TR,TD +{ + font-family:Tahoma,Arial,sans-serif; + font-size:10pt; + color: #4477AA;/*darker than 5588bb for the sake of the eyes*/ +} + +/***************************************************** + "A" settings +*****************************************************/ + +A +{ + color: #ae322b; + text-decoration:underline; + font-weight:bold; +} + +A.glossaryword {color:#A0A0A0;} + +A.noline +{ + text-decoration: none; +} + + +/***************************************************** + Menu and mainframe settings +*****************************************************/ + +.maincontainer +{ + display:block; + margin-left:7%; + margin-right:7%; + padding-left:5px; + padding-right:0px; + border-color:#CCCCCC; + border-style:solid; + border-width:2px; + border-right-width:0px; + border-bottom-width:0px; + border-top-color:#ae322b; + vertical-align:top; +} + +TD.menuframe +{ + width:30%; +} + +.menu +{ + margin:4px 4px 4px 4px; + margin-top: 16pt; + border-color:gray; + border-width:1px; + border-style:dotted; + padding-top:10pt; + padding-bottom:10pt; + padding-right:6pt; +} + +.submenu +{ + list-style-type: none; + margin-left:26pt; + margin-top:0pt; + margin-bottom:0pt; + padding-left:0pt; +} + +A.menuitem,A.menuitem_selected +{ + font-size:14pt; + font-family:Tahoma,Arial,sans-serif; + font-weight:normal; + padding-left:10pt; + color:#5588bb; + margin-right:2pt; + margin-left:4pt; + text-decoration:none; +} + +A.menuitem_selected +{ + font-weight:bold; +} + +A.submenuitem +{ + font-family:Tahoma,Arial,sans-serif; + font-weight:normal; + color:#5588bb; + text-decoration:none; +} + +.titleline +{ + border-width:3px; + border-style:solid; + border-left-width:0px; + border-right-width:0px; + border-top-width:0px; + border-color:#CCCCCC; + margin-left:28pt; + margin-right:2pt; + line-height:1px; + padding-top:0px; + margin-top:0px; + display:block; +} + +.titledescrip +{ + text-align:left; + display:block; + margin-left:26pt; + color:#ae322b; +} + +.mainlogo +{ + display:block; + margin-left:8%; + margin-top:4pt; + margin-bottom:4pt; +} + +/***************************************************** + IMG settings +*****************************************************/ + +IMG +{ + border-style:none; +} + +IMG.smallbutton +{ + margin-right: 20px; + float:none; +} + +IMG.floating +{ + float:left; + margin-right: 4pt; + margin-bottom: 4pt; +} + +IMG.lefticon +{ + vertical-align: middle; + padding-right: 2pt; +} + +IMG.righticon +{ + vertical-align: middle; + padding-left: 2pt; +} + +/***************************************************** + TABLE settings +*****************************************************/ + +TABLE +{ + border-style:none; +} + +TABLE.box +{ + width: 90%; + margin-left:5%; +} + +TABLE.centered +{ + margin-left: auto; + margin-right: auto; +} + +TABLE.hardcoded +{ + background-color: #225588; + margin-left: auto; + margin-right: auto; + width: 90%; +} + +TR { background-color: transparent; } + +TABLE.hardcoded TR { background-color: white } + +TABLE.hardcoded TR.header +{ + font-weight: bold; + color: black; + background-color: #C8D6E5; +} + +TABLE.hardcoded TR.header TD {color:black;} + +TABLE.hardcoded TD { padding-left: 2pt; } + +TD.minimelem { + padding-right:0px; + padding-left:0px; + text-align:center; +} + +TD.rightelem +{ + text-align:right; + /*padding-left:0pt;*/ + padding-right: 2pt; + width: 17%; +} + +/***************************************************** + P settings +*****************************************************/ + +p,.sub{text-align:justify;} +.centered{text-align:center;} +.sub +{ + padding-left: 16pt; + padding-right:16pt; +} + +.Note, .ContactInfo +{ + border-color: #ae322b; + border-width: 1pt; + border-style: dashed; + text-align:justify; + padding: 2pt 2pt 2pt 2pt; + margin-bottom:4pt; + margin-top:8pt; + list-style-position:inside; +} + +.ContactInfo +{ + width:60%; + margin-left:5%; +} + +.NewsItem +{ + border-color:#ae322b; + border-style: solid; + border-right:none; + border-top:none; + border-left:none; + border-bottom-width:1px; + text-align:justify; + padding-left:4pt; + padding-right:4pt; + padding-bottom:8pt; +} + +/***************************************************** + Lists settings +*****************************************************/ +UL.plain +{ + list-style-type: none; + padding-left:0px; + margin-left:0px; +} + +LI.plain +{ + list-style-type: none; +} + +LI.section +{ + padding-top: 6pt; +} + +UL.longtext LI +{ + border-color: #ae322b; + border-width:0px; + border-top-width:1px; + border-style:solid; + margin-top:12px; +} + +/* + with UL.longtext LI, there can be anything between + the UL and the LI, and it will still make the + lontext thing, I must break it with this hack +*/ +UL.longtext UL LI +{ + border-style:none; + margin-top:2px; +} + + +/***************************************************** + Titles settings +*****************************************************/ + +H1,H2,H3 +{ + font-family:"Courier New",monospace; + color:#5588bb; +} + +H1 +{ + font-size:18pt; + color: #ae322b; + border-color: #70A0CF; + border-width: 1pt; + border-style: solid; + margin-top: 16pt; + margin-left: 5%; + margin-right: 5%; + padding-top: 2pt; + padding-bottom:2pt; + text-align: center; +} + +H2 +{ + border-color: #ae322b; + border-bottom-width: 2px; + border-top-width: 0pt; + border-left-width: 2px; + border-right-width: 0pt; + border-bottom-color: #cccccc; + border-style: solid; + margin-top: 16pt; + margin-left: 0pt; + margin-right: 0pt; + padding-bottom:3pt; + padding-left:5pt; + text-align: left; + font-size:16pt; +} + +H3 +{ + display:block; + color:#ae322b; + border-color: #70A0CF; + border-bottom-width: 2px; + border-top-width: 0pt; + border-left-width: 0pt; + border-right-width: 0pt; + border-style: dashed; + margin-top: 12pt; + margin-left: 0pt; + margin-bottom: 4pt; + width:auto; + padding-bottom:3pt; + padding-right:2pt; + padding-left:2pt; + text-align: left; + font-weight:bold; +} + + +/***************************************************** + Misc. classes +*****************************************************/ +.longtext:first-letter {font-size: 150%} + +.price, .loweredprice, .specialprice {font-weight:bold;} + +.loweredprice {text-decoration:line-through} + +.specialprice {color:red} + +form +{ + margin:0px; +} + +.program_summary +{ + float:right; + margin: 32pt; + margin-top:0pt; + margin-bottom:0pt; +} + +.screenshot +{ + float:left; + margin: 8pt; +} \ No newline at end of file diff --git a/pe/qt/help/skeleton/images/hs_title.png b/pe/qt/help/skeleton/images/hs_title.png new file mode 100644 index 0000000000000000000000000000000000000000..07bd89c69dd50a3967b46a441741f274b8854de8 GIT binary patch literal 1817 zcmWkt2~Y|q^t%aD27PTfWQbSo7y0Xrdp{OF7oMwX4{+byFr3&y7B zlLpel1d+*^<>$!E@c2A9sOuxRq}jS+G}rcy>y2h&rngp=tT$NjxX)#-@ zR;$fwvyo;C3xs)cj4wl35`-y%c{0>!qGG~a8Nvb)0K&L3RHVdNQtA}p%Q4Dz$v^^f z%s`q=W-AZEN|l62NeGp=(QGM^zcEYpnJq53Zg_psX$pDf}jksh9!YZ6*y0d6t`*06htZIC4pHW%9Epf zh*AWzsT7ofh)_XrrKsL$;mT2tj52IVPjY1#SB?oFlp&##5=^Q9qlF{I7FJ9G2rF*a z2s?-pFR&PpFjtBRA?yV{E9}sUAgt)Ih9}1>x(SvT zsqP+UieWa0F{DUM&ub2ZbHs4dvq=VsEE&xV>e|H!OM+4TQTc&M3CEU=q(F(^fAYG# zOS_;aD|@ud29hH~gpd|cpbwr+aO8+kiBoq^eznVg<_$ zP1%d1$UV?R~TV5+i6w>mO|X i&$iNccmR(ItgDH2&`ti^@h8+@ro}}kMz!wGy!C(b`7gZy literal 0 HcmV?d00001 diff --git a/pe/qt/help/templates/base_dg.mako b/pe/qt/help/templates/base_dg.mako new file mode 100644 index 00000000..7767c49f --- /dev/null +++ b/pe/qt/help/templates/base_dg.mako @@ -0,0 +1,14 @@ +<%inherit file="/base_help.mako"/> +${next.body()} + +<%def name="menu()"><% +self.menuitem('intro.htm', 'Introduction', 'Introduction to dupeGuru') +self.menuitem('quick_start.htm', 'Quick Start', 'Quickly get into the action') +self.menuitem('directories.htm', 'Directories', 'Managing dupeGuru directories') +self.menuitem('preferences.htm', 'Preferences', 'Setting dupeGuru preferences') +self.menuitem('results.htm', 'Results', 'Time to delete these duplicates!') +self.menuitem('power_marker.htm', 'Power Marker', 'Take control of your duplicates') +self.menuitem('faq.htm', 'F.A.Q.', 'Frequently Asked Questions') +self.menuitem('versions.htm', 'Version History', 'Changes dupeGuru went through') +self.menuitem('credits.htm', 'Credits', 'People who contributed to dupeGuru') +%> \ No newline at end of file diff --git a/pe/qt/help/templates/credits.mako b/pe/qt/help/templates/credits.mako new file mode 100644 index 00000000..9de91bd2 --- /dev/null +++ b/pe/qt/help/templates/credits.mako @@ -0,0 +1,25 @@ +## -*- coding: utf-8 -*- +<%! + title = 'Credits' + selected_menu_item = 'Credits' +%> +<%inherit file="/base_dg.mako"/> +Below is the list of people who contributed, directly or indirectly to dupeGuru. + +${self.credit('Virgil Dupras', 'Developer', "That's me, Hardcoded Software founder", 'www.hardcoded.net', 'hsoft@hardcoded.net')} + +${self.credit(u'Jérôme Cantin', u'Icon designer', u"Icons in dupeGuru are from him")} + +${self.credit('Python', 'Programming language', "The bestest of the bests", 'www.python.org')} + +${self.credit('PyObjC', 'Python-to-Cocoa bridge', "Used for the Mac OS X version", 'pyobjc.sourceforge.net')} + +${self.credit('PyQt', 'Python-to-Qt bridge', "Used for the Windows version", 'www.riverbankcomputing.co.uk')} + +${self.credit('Qt', 'GUI Toolkit', "Used for the Windows version", 'www.qtsoftware.com')} + +${self.credit('Sparkle', 'Auto-update library', "Used for the Mac OS X version", 'andymatuschak.org/pages/sparkle')} + +${self.credit('Python Imaging Library', 'Picture analyzer', "Used for the Windows version", 'www.pythonware.com/products/pil/')} + +${self.credit('You', 'dupeGuru user', "What would I do without you?")} diff --git a/pe/qt/help/templates/directories.mako b/pe/qt/help/templates/directories.mako new file mode 100644 index 00000000..e75b47bd --- /dev/null +++ b/pe/qt/help/templates/directories.mako @@ -0,0 +1,24 @@ +<%! + title = 'Directories' + selected_menu_item = 'Directories' +%> +<%inherit file="/base_dg.mako"/> + +There is a panel in dupeGuru called **Directories**. You can open it by clicking on the **Directories** button. This directory contains the list of the directories that will be scanned when you click on **Start Scanning**. + +This panel is quite straightforward to use. If you want to add a directory, click on **Add**. If you added directories before, a popup menu with a list of recent directories you added will pop. You can click on one of them to add it directly to your list. If you click on the first item of the popup menu, **Add New Directory...**, you will be prompted for a directory to add. If you never added a directory, no menu will pop and you will directly be prompted for a new directory to add. + +To remove a directory, select the directory to remove and click on **Remove**. If a subdirectory is selected when you click remove, the selected directory will be set to **excluded** state (see below) instead of being removed. + +Directory states +----- + +Every directory can be in one of these 3 states: + +* **Normal:** Duplicates found in these directories can be deleted. +* **Reference:** Duplicates found in this directory **cannot** be deleted. Files in reference directories will be in a blue color in the results. +* **Excluded:** Files in this directory will not be included in the scan. + +The default state of a directory is, of course, **Normal**. You can use **Reference** state for a directory if you want to be sure that you won't delete any file from it. + +When you set the state of a directory, all subdirectories of this directory automatically inherit this state unless you explicitly set a subdirectory's state. diff --git a/pe/qt/help/templates/faq.mako b/pe/qt/help/templates/faq.mako new file mode 100644 index 00000000..1c4e998f --- /dev/null +++ b/pe/qt/help/templates/faq.mako @@ -0,0 +1,64 @@ +<%! + title = 'dupeGuru F.A.Q.' + selected_menu_item = 'F.A.Q.' +%> +<%inherit file="/base_dg.mako"/> + +<%text filter="md"> +### What is dupeGuru PE? + +dupeGuru Picture Edition (PE for short) is a tool to find duplicate pictures on your computer. Not only can it find exact matches, but it can also find duplicates among pictures of different kind (PNG, JPG, GIF etc..) and quality. + +### What makes it better than other duplicate scanners? + +The scanning engine is extremely flexible. You can tweak it to really get the kind of results you want. You can read more about dupeGuru tweaking option at the [Preferences page](preferences.htm). + +### How safe is it to use dupeGuru PE? + +Very safe. dupeGuru has been designed to make sure you don't delete files you didn't mean to delete. First, there is the reference directory system that lets you define directories where you absolutely **don't** want dupeGuru to let you delete files there, and then there is the group reference system that makes sure that you will **always** keep at least one member of the duplicate group. + +### What are the demo limitations of dupeGuru PE? + +In demo mode, you can only perform actions (delete/copy/move) on 10 duplicates per session. + +### The mark box of a file I want to delete is disabled. What must I do? + +You cannot mark the reference (The first file) of a duplicate group. However, what you can do is to promote a duplicate file to reference. Thus, if a file you want to mark is reference, select a duplicate file in the group that you want to promote to reference, and click on **Actions-->Make Selected Reference**. If the reference file is from a reference directory (filename written in blue letters), you cannot remove it from the reference position. + +### I have a directory from which I really don't want to delete files. + +If you want to be sure that dupeGuru will never delete file from a particular directory, just open the **Directories panel**, select that directory, and set its state to **Reference**. + +### What is this '(X discarded)' notice in the status bar? + +In some cases, some matches are not included in the final results for security reasons. Let me use an example. We have 3 file: A, B and C. We scan them using a low filter hardness. The scanner determines that A matches with B, A matches with C, but B does **not** match with C. Here, dupeGuru has kind of a problem. It cannot create a duplicate group with A, B and C in it because not all files in the group would match together. It could create 2 groups: one A-B group and then one A-C group, but it will not, for security reasons. Lets think about it: If B doesn't match with C, it probably means that either B, C or both are not actually duplicates. If there would be 2 groups (A-B and A-C), you would end up delete both B and C. And if one of them is not a duplicate, that is really not what you want to do, right? So what dupeGuru does in a case like this is to discard the A-C match (and adds a notice in the status bar). Thus, if you delete B and re-run a scan, you will have a A-C match in your next results. + +### I want to mark all files from a specific directory. What can I do? + +Enable the [Power Marker](power_marker.htm) mode and click on the Directory column to sort your duplicates by Directory. It will then be easy for you to select all duplicates from the same directory, and then press Space to mark all selected duplicates. + +### I want to remove all files that are more than 300 KB away from their reference file. What can I do? + +* Enable the [Power Marker](power_marker.htm) mode. +* Enable the **Delta Values** mode. +* Click on the "Size" column to sort the results by size. +* Select all duplicates below -300. +* Click on **Remove Selected from Results**. +* Select all duplicates over 300. +* Click on **Remove Selected from Results**. + +### I want to make my latest modified files reference files. What can I do? + +* Enable the [Power Marker](power_marker.htm) mode. +* Enable the **Delta Values** mode. +* Click on the "Modification" column to sort the results by modification date. +* Click on the "Modification" column again to reverse the sort order (see Power Marker page to know why). +* Select all duplicates over 0. +* Click on **Make Selected Reference**. + +### I want to mark all duplicates containing the word "copy". How do I do that? + +* **Windows**: Click on **Actions --> Apply Filter**, then type "copy", then click OK. +* **Mac OS X**: Type "copy" in the "Filter" field in the toolbar. +* Click on **Mark --> Mark All**. + \ No newline at end of file diff --git a/pe/qt/help/templates/intro.mako b/pe/qt/help/templates/intro.mako new file mode 100644 index 00000000..51d058c9 --- /dev/null +++ b/pe/qt/help/templates/intro.mako @@ -0,0 +1,13 @@ +<%! + title = 'Introduction to dupeGuru PE' + selected_menu_item = 'introduction' +%> +<%inherit file="/base_dg.mako"/> + +dupeGuru Picture Edition (PE for short) is a tool to find duplicate pictures on your computer. Not only can it find exact matches, but it can also find duplicates among pictures of different kind (PNG, JPG, GIF etc..) and quality. + +Although dupeGuru can easily be used without documentation, reading this file will help you to master it. If you are looking for guidance for your first duplicate scan, you can take a look at the [Quick Start](quick_start.htm) section. + +It is a good idea to keep dupeGuru PE updated. You can download the latest version on the [dupeGuru PE homepage](http://www.hardcoded.net/dupeguru_pe/). + +<%def name="meta()"> diff --git a/pe/qt/help/templates/power_marker.mako b/pe/qt/help/templates/power_marker.mako new file mode 100644 index 00000000..26078f3d --- /dev/null +++ b/pe/qt/help/templates/power_marker.mako @@ -0,0 +1,33 @@ +<%! + title = 'Power Marker' + selected_menu_item = 'Power Marker' +%> +<%inherit file="/base_dg.mako"/> + +You will probably not use the Power Marker feature very often, but if you get into a situation where you need it, you will be pretty happy that this feature exists. + +What is it? +----- + +When the Power Marker mode is enabled, the duplicates are shown without their respective reference file. You can select, mark and sort this list, just like in normal mode. + +So, what is it for? +----- + +The dupeGuru results, when in normal mode, are sorted according to duplicate groups' **reference file**. This means that if you want, for example, to mark all duplicates with the "exe" extension, you cannot just sort the results by "Kind" to have all exe duplicates together because a group can be composed of more than one kind of files. That is where Power Marker comes into play. To mark all your "exe" duplicates, you just have to: + +* Enable the Power marker mode. +* Add the "Kind" column with the "Columns" menu. +* Click on that "Kind" column to sort the list by kind. +* Locate the first duplicate with a "exe" kind. +* Select it. +* Scroll down the list to locate the last duplicate with a "exe" kind. +* Hold Shift and click on it. +* Press Space to mark all selected duplicates. + +Power Marker and delta values +----- + +The Power Marker unveil its true power when you use it with the **Delta Values** switch turned on. When you turn it on, relative values will be displayed instead of absolute ones. So if, for example, you want to remove from your results all duplicates that are more than 300 KB away from their reference, you could sort the Power Marker by Size, select all duplicates under -300 in the Size column, delete them, and then do the same for duplicates over 300 at the bottom of the list. + +You could also use it to change the reference priority of your duplicate list. When you make a fresh scan, if there are no reference directories, the reference file of every group is the biggest file. If you want to change that, for example, to the latest modification time, you can sort the Power Marker by modification time in **descending** order, select all duplicates with a modification time delta value higher than 0 and click on **Make Selected Reference**. The reason why you must make the sort order descending is because if 2 files among the same duplicate group are selected when you click on **Make Selected Reference**, only the first of the list will be made reference, the other will be ignored. And since you want the last modified file to be reference, having the sort order descending assures you that the first item of the list will be the last modified. diff --git a/pe/qt/help/templates/preferences.mako b/pe/qt/help/templates/preferences.mako new file mode 100644 index 00000000..0ef6a2ba --- /dev/null +++ b/pe/qt/help/templates/preferences.mako @@ -0,0 +1,23 @@ +<%! + title = 'Preferences' + selected_menu_item = 'Preferences' +%> +<%inherit file="/base_dg.mako"/> + +**Filter Hardness:** The higher is this setting, the "harder" is the filter (In other words, the less results you get). Most pictures of the same quality match at 100% even if the format is different (PNG and JPG for example.). However, if you want to make a PNG match with a lower quality JPG, you will have to set the filer hardness to lower than 100. The default, 95, is a sweet spot. + +**Match scaled pictures together:** If you check this box, pictures of different dimensions will be allowed in the same duplicate group. + +**Can mix file kind:** If you check this box, duplicate groups are allowed to have files with different extensions. If you don't check it, well, they aren't! + +**Use regular expressions when filtering:** If you check this box, the filtering feature will treat your filter query as a **regular expression**. Explaining them is beyond the scope of this document. A good place to start learning it is . + +**Remove empty folders after delete or move:** When this option is enabled, folders are deleted after a file is deleted or moved and the folder is empty. + +**Copy and Move:** Determines how the Copy and Move operations (in the Action menu) will behave. + +* **Right in destination:** All files will be sent directly in the selected destination, without trying to recreate the source path at all. +* **Recreate relative path:** The source file's path will be re-created in the destination directory up to the root selection in the Directories panel. For example, if you added "/Users/foobar/Picture" to your Directories panel and you move "/Users/foobar/Picture/2006/06/photo.jpg" to the destination "/Users/foobar/MyDestination", the final destination for the file will be "/Users/foobar/MyDestination/2006/06" ("/Users/foobar/Picture" has been trimmed from source's path in the final destination.). +* **Recreate absolute path:** The source file's path will be re-created in the destination directory in it's entirety. For example, if you move "/Users/foobar/Picture/2006/06/photo.jpg" to the destination "/Users/foobar/MyDestination", the final destination for the file will be "/Users/foobar/MyDestination/Users/foobar/Picture/2006/06". + +In all cases, dupeGuru PE nicely handles naming conflicts by prepending a number to the destination filename if the filename already exists in the destination. diff --git a/pe/qt/help/templates/quick_start.mako b/pe/qt/help/templates/quick_start.mako new file mode 100644 index 00000000..dde33c65 --- /dev/null +++ b/pe/qt/help/templates/quick_start.mako @@ -0,0 +1,18 @@ +<%! + title = 'Quick Start' + selected_menu_item = 'Quick Start' +%> +<%inherit file="/base_dg.mako"/> + +To get you quickly started with dupeGuru, let's just make a standard scan using default preferences. + +* Click on **Directories**. +* Click on **Add**. +* Choose a directory you want to scan for duplicates. +* Click on **Start Scanning**. +* Wait until the scan process is over. +* Look at every duplicate (The files that are indented) and verify that it is indeed a duplicate to the group's reference (The file above the duplicate that is not indented and have a disabled mark box). +* If a file is a false duplicate, select it and click on **Actions-->Remove Selected from Results**. +* Once you are sure that there is no false duplicate in your results, click on **Edit-->Mark All**, and then **Actions-->Send Marked to Recycle bin**. + +That is only a basic scan. There are a lot of tweaking you can do to get different results and several methods of examining and modifying your results. To know about them, just read the rest of this help file. diff --git a/pe/qt/help/templates/results.mako b/pe/qt/help/templates/results.mako new file mode 100644 index 00000000..53aa176f --- /dev/null +++ b/pe/qt/help/templates/results.mako @@ -0,0 +1,73 @@ +<%! + title = 'Results' + selected_menu_item = 'Results' +%> +<%inherit file="/base_dg.mako"/> + +When dupeGuru is finished scanning for duplicates, it will show its results in the form of duplicate group list. + +About duplicate groups +----- + +A duplicate group is a group of files that all match together. Every group has a **reference file** and one or more **duplicate files**. The reference file is the first file of the group. Its mark box is disabled. Below it, and indented, are the duplicate files. + +You can mark duplicate files, but you can never mark the reference file of a group. This is a security measure to prevent dupeGuru from deleting not only duplicate files, but their reference. You sure don't want that, do you? + +What determines which files are reference and which files are duplicates is first their directory state. A files from a reference directory will always be reference in a duplicate group. If all files are from a normal directory, the size determine which file will be the reference of a duplicate group. dupeGuru assumes that you always want to keep the biggest file, so the biggest files will take the reference position. + +You can change the reference file of a group manually. To do so, select the duplicate file you want to promote to reference, and click on **Actions-->Make Selected Reference**. + +Reviewing results +----- + +Although you can just click on **Edit-->Mark All** and then **Actions-->Send Marked to Recycle bin** to quickly delete all duplicate files in your results, it is always recommended to review all duplicates before deleting them. + +To help you reviewing the results, you can bring up the **Details panel**. This panel shows all the details of the currently selected file as well as its reference's details. This is very handy to quickly determine if a duplicate really is a duplicate. You can also double-click on a file to open it with its associated application. + +If you have more false duplicates than true duplicates (If your filter hardness is very low), the best way to proceed would be to review duplicates, mark true duplicates and then click on **Actions-->Send Marked to Recycle bin**. If you have more true duplicates than false duplicates, you can instead mark all files that are false duplicates, and use **Actions-->Remove Marked from Results**. + +Marking and Selecting +----- + +A **marked** duplicate is a duplicate with the little box next to it having a check-mark. A **selected** duplicate is a duplicate being highlighted. The multiple selection actions can be performed in dupeGuru in the standard way (Shift/Command/Control click). You can toggle all selected duplicates' mark state by pressing **space**. + +Delta Values +----- + +If you turn this switch on, some columns will display the value relative to the duplicate's reference instead of the absolute values. These delta values will also be displayed in a different color so you can spot them easily. For example, if a duplicate is 1.2 MB and its reference is 1.4 MB, the Size column will display -0.2 MB. This option is a killer feature when combined with the [Power Marker](power_marker.htm). + +Filtering +----- + +dupeGuru supports post-scan filtering. With it, you can narrow down your results so you can perform actions on a subset of it. For example, you could easily mark all duplicates with their filename containing "copy" from your results using the filter. + +**Windows:** To use the filtering feature, click on Actions --> Apply Filter, write down the filter you want to apply and click OK. To go back to unfiltered results, click on Actions --> Cancel Filter. + +**Mac OS X:** To use the filtering feature, type your filter in the "Filter" search field in the toolbar. To go back to unfiltered result, blank out the field, or click on the "X". + +In simple mode (the default mode), whatever you type as the filter is the string used to perform the actual filtering, with the exception of one wildcard: **\***. Thus, if you type "[*]" as your filter, it will match anything with [] brackets in it, whatever is in between those brackets. + +For more advanced filtering, you can turn "Use regular expressions when filtering" on. The filtering feature will then use **regular expressions**. A regular expression is a language for matching text. Explaining them is beyond the scope of this document. A good place to start learning it is . + +Matches are case insensitive in both simple and regexp mode. + +For the filter to match, your regular expression don't have to match the whole filename, it just have to contain a string matching the expression. + +You might notice that not all duplicates in the filtered results will match your filter. That is because as soon as one single duplicate in a group matches the filter, the whole group stays in the results so you can have a better view of the duplicate's context. However, non-matching duplicates are in "reference mode". Therefore, you can perform actions like Mark All and be sure to only mark filtered duplicates. + +Action Menu +----- + +* **Start Duplicate Scan:** Starts a new duplicate scan. +* **Clear Ignore List:** Remove all ignored matches you added. You have to start a new scan for the newly cleared ignore list to be effective. +* **Export Results to XHTML:** Take the current results, and create an XHTML file out of it. The columns that are visible when you click on this button will be the columns present in the XHTML file. The file will automatically be opened in your default browser. +* **Send Marked to Trash:** Send all marked duplicates to trash, obviously. +* **Move Marked to...:** Prompt you for a destination, and then move all marked files to that destination. Source file's path might be re-created in destination, depending on the "Copy and Move" preference. +* **Copy Marked to...:** Prompt you for a destination, and then copy all marked files to that destination. Source file's path might be re-created in destination, depending on the "Copy and Move" preference. +* **Remove Marked from Results:** Remove all marked duplicates from results. The actual files will not be touched and will stay where they are. +* **Remove Selected from Results:** Remove all selected duplicates from results. Note that all selected reference files will be ignored, only duplicates can be removed with this action. +* **Make Selected Reference:** Promote all selected duplicates to reference. If a duplicate is a part of a group having a reference file coming from a reference directory (in blue color), no action will be taken for this duplicate. If more than one duplicate among the same group are selected, only the first of each group will be promoted. +* **Add Selected to Ignore List:** This first removes all selected duplicates from results, and then add the match of that duplicate and the current reference in the ignore list. This match will not come up again in further scan. The duplicate itself might come back, but it will be matched with another reference file. You can clear the ignore list with the Clear Ignore List command. +* **Open Selected with Default Application:** Open the file with the application associated with selected file's type. +* **Reveal Selected in Finder:** Open the folder containing selected file. +* **Rename Selected:** Prompts you for a new name, and then rename the selected file. diff --git a/pe/qt/help/templates/versions.mako b/pe/qt/help/templates/versions.mako new file mode 100644 index 00000000..157c26ba --- /dev/null +++ b/pe/qt/help/templates/versions.mako @@ -0,0 +1,6 @@ +<%! + title = 'dupeGuru PE version history' + selected_menu_item = 'Version History' +%> +<%inherit file="/base_dg.mako"/> +${self.output_changelogs(changelog)} \ No newline at end of file diff --git a/pe/qt/installer.aip b/pe/qt/installer.aip new file mode 100644 index 00000000..d7f3ed6c --- /dev/null +++ b/pe/qt/installer.aip @@ -0,0 +1,249 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pe/qt/main_window.py b/pe/qt/main_window.py new file mode 100644 index 00000000..c1ba71bd --- /dev/null +++ b/pe/qt/main_window.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python +# Unit Name: main_window +# Created By: Virgil Dupras +# Created On: 2009-05-23 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +from PyQt4.QtCore import SIGNAL +from PyQt4.QtGui import QMessageBox, QAction + +from base.main_window import MainWindow as MainWindowBase + +class MainWindow(MainWindowBase): + def _setupUi(self): + MainWindowBase._setupUi(self) + self.actionClearPictureCache = QAction("Clear Picture Cache", self) + self.menuFile.insertAction(self.actionClearIgnoreList, self.actionClearPictureCache) + self.connect(self.actionClearPictureCache, SIGNAL("triggered()"), self.clearPictureCacheTriggered) + + def clearPictureCacheTriggered(self): + title = "Clear Picture Cache" + msg = "Do you really want to remove all your cached picture analysis?" + if self._confirm(title, msg, QMessageBox.No): + self.app.scanner.match_factory.cached_blocks.clear() + QMessageBox.information(self, title, "Picture cache cleared.") + \ No newline at end of file diff --git a/pe/qt/modules/block/block.pyx b/pe/qt/modules/block/block.pyx new file mode 100644 index 00000000..777ff723 --- /dev/null +++ b/pe/qt/modules/block/block.pyx @@ -0,0 +1,39 @@ +cdef object getblock(object image): + cdef int width, height, pixel_count, red, green, blue, i, offset + cdef char *s + cdef unsigned char r, g, b + width = image.width() + height = image.height() + if width: + pixel_count = width * height + red = green = blue = 0 + tmp = image.bits().asstring(image.numBytes()) + s = tmp + for i in range(pixel_count): + offset = i * 3 + r = s[offset] + g = s[offset + 1] + b = s[offset + 2] + red += r + green += g + blue += b + return (red // pixel_count, green // pixel_count, blue // pixel_count) + else: + return (0, 0, 0) + +def getblocks(image, int block_count_per_side): + cdef int width, height, block_width, block_height, ih, iw, top, left + width = image.width() + height = image.height() + if not width: + return [] + block_width = max(width // block_count_per_side, 1) + block_height = max(height // block_count_per_side, 1) + result = [] + for ih in range(block_count_per_side): + top = min(ih * block_height, height - block_height) + for iw in range(block_count_per_side): + left = min(iw * block_width, width - block_width) + crop = image.copy(left, top, block_width, block_height) + result.append(getblock(crop)) + return result diff --git a/pe/qt/modules/block/setup.py b/pe/qt/modules/block/setup.py new file mode 100644 index 00000000..e37aee94 --- /dev/null +++ b/pe/qt/modules/block/setup.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python + +from distutils.core import setup +from distutils.extension import Extension +from Cython.Distutils import build_ext + +setup( + cmdclass = {'build_ext': build_ext}, + ext_modules = [Extension("_block", ["block.pyx"])] +) \ No newline at end of file diff --git a/pe/qt/preferences.py b/pe/qt/preferences.py new file mode 100644 index 00000000..4dd748fd --- /dev/null +++ b/pe/qt/preferences.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python +# Unit Name: preferences +# Created By: Virgil Dupras +# Created On: 2009-05-17 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +from PyQt4.QtCore import QSettings, QVariant + +from base.preferences import Preferences as PreferencesBase + +class Preferences(PreferencesBase): + # (width, is_visible) + COLUMNS_DEFAULT_ATTRS = [ + (200, True), # name + (180, True), # path + (60, True), # size + (40, False), # kind + (100, True), # dimensions + (120, False), # creation + (120, False), # modification + (60, True), # match % + (80, False), # dupe count + ] + + def _load_specific(self, settings, get): + self.match_scaled = get('MatchScaled', self.match_scaled) + + def _reset_specific(self): + self.filter_hardness = 95 + self.match_scaled = False + + def _save_specific(self, settings, set_): + set_('MatchScaled', self.match_scaled) + diff --git a/pe/qt/preferences_dialog.py b/pe/qt/preferences_dialog.py new file mode 100644 index 00000000..12505f95 --- /dev/null +++ b/pe/qt/preferences_dialog.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python +# Unit Name: preferences_dialog +# Created By: Virgil Dupras +# Created On: 2009-04-29 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +from PyQt4.QtCore import SIGNAL, Qt +from PyQt4.QtGui import QDialog, QDialogButtonBox + +from preferences_dialog_ui import Ui_PreferencesDialog +import preferences + +class PreferencesDialog(QDialog, Ui_PreferencesDialog): + def __init__(self, parent, app): + flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint + QDialog.__init__(self, parent, flags) + self.app = app + self._setupUi() + + self.connect(self.buttonBox, SIGNAL('clicked(QAbstractButton*)'), self.buttonClicked) + + def _setupUi(self): + self.setupUi(self) + + def load(self, prefs=None): + if prefs is None: + prefs = self.app.prefs + self.filterHardnessSlider.setValue(prefs.filter_hardness) + self.filterHardnessLabel.setNum(prefs.filter_hardness) + setchecked = lambda cb, b: cb.setCheckState(Qt.Checked if b else Qt.Unchecked) + setchecked(self.matchScaledBox, prefs.match_scaled) + setchecked(self.mixFileKindBox, prefs.mix_file_kind) + setchecked(self.useRegexpBox, prefs.use_regexp) + setchecked(self.removeEmptyFoldersBox, prefs.remove_empty_folders) + self.copyMoveDestinationComboBox.setCurrentIndex(prefs.destination_type) + + def save(self): + prefs = self.app.prefs + prefs.filter_hardness = self.filterHardnessSlider.value() + ischecked = lambda cb: cb.checkState() == Qt.Checked + prefs.match_scaled = ischecked(self.matchScaledBox) + prefs.mix_file_kind = ischecked(self.mixFileKindBox) + prefs.use_regexp = ischecked(self.useRegexpBox) + prefs.remove_empty_folders = ischecked(self.removeEmptyFoldersBox) + prefs.destination_type = self.copyMoveDestinationComboBox.currentIndex() + + def resetToDefaults(self): + self.load(preferences.Preferences()) + + #--- Events + def buttonClicked(self, button): + role = self.buttonBox.buttonRole(button) + if role == QDialogButtonBox.ResetRole: + self.resetToDefaults() + diff --git a/pe/qt/preferences_dialog.ui b/pe/qt/preferences_dialog.ui new file mode 100644 index 00000000..f91b24cb --- /dev/null +++ b/pe/qt/preferences_dialog.ui @@ -0,0 +1,257 @@ + + + PreferencesDialog + + + + 0 + 0 + 366 + 249 + + + + Preferences + + + false + + + true + + + + + + + + + + + 0 + 0 + + + + Filter Hardness: + + + + + + + 0 + + + + + 12 + + + + + + 0 + 0 + + + + 1 + + + 100 + + + true + + + Qt::Horizontal + + + + + + + + 21 + 0 + + + + 100 + + + + + + + + + 0 + + + + + More Results + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Less Results + + + + + + + + + + + + + Match scaled pictures together + + + + + + + Can mix file kind + + + + + + + Use regular expressions when filtering + + + + + + + Remove empty folders on delete or move + + + + + + + + + + 0 + 0 + + + + Copy and Move: + + + + + + + + 0 + 0 + + + + + Right in destination + + + + + Recreate relative path + + + + + Recreate absolute path + + + + + + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok|QDialogButtonBox::RestoreDefaults + + + + + + + + + filterHardnessSlider + valueChanged(int) + filterHardnessLabel + setNum(int) + + + 182 + 26 + + + 271 + 26 + + + + + buttonBox + accepted() + PreferencesDialog + accept() + + + 182 + 228 + + + 182 + 124 + + + + + buttonBox + rejected() + PreferencesDialog + reject() + + + 182 + 228 + + + 182 + 124 + + + + + diff --git a/pe/qt/profile.py b/pe/qt/profile.py new file mode 100644 index 00000000..e285fafe --- /dev/null +++ b/pe/qt/profile.py @@ -0,0 +1,20 @@ +import sys +import cProfile +import pstats + +from PyQt4.QtCore import QCoreApplication +from PyQt4.QtGui import QApplication + +if sys.platform == 'win32': + from app_win import DupeGuru +else: + from app import DupeGuru + +if __name__ == "__main__": + app = QApplication(sys.argv) + QCoreApplication.setOrganizationName('Hardcoded Software') + QCoreApplication.setApplicationName('dupeGuru Picture Edition') + dgapp = DupeGuru() + cProfile.run('app.exec_()', '/tmp/prof') + p = pstats.Stats('/tmp/prof') + p.sort_stats('time').print_stats() \ No newline at end of file diff --git a/pe/qt/start.py b/pe/qt/start.py new file mode 100644 index 00000000..6de46e8e --- /dev/null +++ b/pe/qt/start.py @@ -0,0 +1,20 @@ +import sys + +from PyQt4.QtCore import QCoreApplication +from PyQt4.QtGui import QApplication, QIcon, QPixmap + +import base.dg_rc + +if sys.platform == 'win32': + from app_win import DupeGuru +else: + from app import DupeGuru + +if __name__ == "__main__": + app = QApplication(sys.argv) + app.setWindowIcon(QIcon(QPixmap(":/logo_pe"))) + QCoreApplication.setOrganizationName('Hardcoded Software') + QCoreApplication.setApplicationName(DupeGuru.NAME) + QCoreApplication.setApplicationVersion(DupeGuru.VERSION) + dgapp = DupeGuru() + sys.exit(app.exec_()) \ No newline at end of file diff --git a/pe/qt/verinfo b/pe/qt/verinfo new file mode 100644 index 00000000..d70ee2a0 --- /dev/null +++ b/pe/qt/verinfo @@ -0,0 +1,28 @@ +VSVersionInfo( + ffi=FixedFileInfo( + filevers=($versioncomma), + prodvers=($versioncomma), + mask=0x17, + flags=0x0, + OS=0x4, + fileType=0x1, + subtype=0x0, + date=(0, 0) + ), + kids=[ + StringFileInfo( + [ + StringTable( + '040904b0', + [StringStruct('CompanyName', 'Hardcoded Software'), + StringStruct('FileDescription', 'dupeGuru Picture Edition'), + StringStruct('FileVersion', '$version'), + StringStruct('InternalName', 'dupeGuru PE.exe'), + StringStruct('LegalCopyright', '(c) Hardcoded Software. All rights reserved.'), + StringStruct('OriginalFilename', 'dupeGuru PE.exe'), + StringStruct('ProductName', 'dupeGuru Picture Edition'), + StringStruct('ProductVersion', '$versioncomma')]) + ]), + VarFileInfo([VarStruct('Translation', [1033])]) + ] +) diff --git a/py/__init__.py b/py/__init__.py new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/py/__init__.py @@ -0,0 +1 @@ + diff --git a/py/app.py b/py/app.py new file mode 100644 index 00000000..0e03603d --- /dev/null +++ b/py/app.py @@ -0,0 +1,229 @@ +#!/usr/bin/env python +""" +Unit Name: dupeguru.app +Created By: Virgil Dupras +Created On: 2006/11/11 +Last modified by:$Author: virgil $ +Last modified on:$Date: 2009-05-28 16:02:48 +0200 (Thu, 28 May 2009) $ + $Revision: 4388 $ +Copyright 2006 Hardcoded Software (http://www.hardcoded.net) +""" +import os +import os.path as op +import logging + +from hsfs import IT_ATTRS, IT_EXTRA +from hsutil import job, io, files +from hsutil.path import Path +from hsutil.reg import RegistrableApplication, RegistrationRequired +from hsutil.misc import flatten, first +from hsutil.str import escape + +import directories +import results +import scanner + +JOB_SCAN = 'job_scan' +JOB_LOAD = 'job_load' +JOB_MOVE = 'job_move' +JOB_COPY = 'job_copy' +JOB_DELETE = 'job_delete' + +class NoScannableFileError(Exception): + pass + +class AllFilesAreRefError(Exception): + pass + +class DupeGuru(RegistrableApplication): + def __init__(self, data_module, appdata, appid): + RegistrableApplication.__init__(self, appid) + self.appdata = appdata + if not op.exists(self.appdata): + os.makedirs(self.appdata) + self.data = data_module + self.directories = directories.Directories() + self.results = results.Results(data_module) + self.scanner = scanner.Scanner() + self.action_count = 0 + self.last_op_error_count = 0 + self.options = { + 'escape_filter_regexp': True, + 'clean_empty_dirs': False, + } + + def _demo_check(self): + if self.registered: + return + count = self.results.mark_count + if count + self.action_count > 10: + raise RegistrationRequired() + else: + self.action_count += count + + def _do_delete(self, j): + def op(dupe): + j.add_progress() + return self._do_delete_dupe(dupe) + + j.start_job(self.results.mark_count) + self.last_op_error_count = self.results.perform_on_marked(op, True) + + def _do_delete_dupe(self, dupe): + if not io.exists(dupe.path): + dupe.parent = None + return True + self._recycle_dupe(dupe) + self.clean_empty_dirs(dupe.path[:-1]) + if not io.exists(dupe.path): + dupe.parent = None + return True + logging.warning(u"Could not send {0} to trash.".format(unicode(dupe.path))) + return False + + def _do_load(self, j): + self.directories.LoadFromFile(op.join(self.appdata, 'last_directories.xml')) + j = j.start_subjob([1, 9]) + self.results.load_from_xml(op.join(self.appdata, 'last_results.xml'), self._get_file, j) + files = flatten(g[:] for g in self.results.groups) + for file in j.iter_with_progress(files, 'Reading metadata %d/%d'): + file._read_all_info(sections=[IT_ATTRS, IT_EXTRA]) + + def _get_file(self, str_path): + p = Path(str_path) + for d in self.directories: + if p not in d.path: + continue + result = d.find_path(p[d.path:]) + if result is not None: + return result + + @staticmethod + def _recycle_dupe(dupe): + raise NotImplementedError() + + def _start_job(self, jobid, func): + # func(j) + raise NotImplementedError() + + def AddDirectory(self, d): + try: + self.directories.add_path(Path(d)) + return 0 + except directories.AlreadyThereError: + return 1 + except directories.InvalidPathError: + return 2 + + def AddToIgnoreList(self, dupe): + g = self.results.get_group_of_duplicate(dupe) + for other in g: + if other is not dupe: + self.scanner.ignore_list.Ignore(unicode(other.path), unicode(dupe.path)) + + def ApplyFilter(self, filter): + self.results.apply_filter(None) + if self.options['escape_filter_regexp']: + filter = escape(filter, '()[]\\.|+?^') + filter = escape(filter, '*', '.') + self.results.apply_filter(filter) + + def clean_empty_dirs(self, path): + if self.options['clean_empty_dirs']: + while files.delete_if_empty(path, ['.DS_Store']): + path = path[:-1] + + def CopyOrMove(self, dupe, copy, destination, dest_type): + """ + copy: True = Copy False = Move + destination: string. + dest_type: 0 = right in destination. + 1 = relative re-creation. + 2 = absolute re-creation. + """ + source_path = dupe.path + location_path = dupe.root.path + dest_path = Path(destination) + if dest_type == 2: + dest_path = dest_path + source_path[1:-1] #Remove drive letter and filename + elif dest_type == 1: + dest_path = dest_path + source_path[location_path:-1] + if not io.exists(dest_path): + io.makedirs(dest_path) + try: + if copy: + files.copy(source_path, dest_path) + else: + files.move(source_path, dest_path) + self.clean_empty_dirs(source_path[:-1]) + except (IOError, OSError) as e: + operation = 'Copy' if copy else 'Move' + logging.warning('%s operation failed on %s. Error: %s' % (operation, unicode(dupe.path), unicode(e))) + return False + return True + + def copy_or_move_marked(self, copy, destination, recreate_path): + def do(j): + def op(dupe): + j.add_progress() + return self.CopyOrMove(dupe, copy, destination, recreate_path) + + j.start_job(self.results.mark_count) + self.last_op_error_count = self.results.perform_on_marked(op, not copy) + + self._demo_check() + jobid = JOB_COPY if copy else JOB_MOVE + self._start_job(jobid, do) + + def delete_marked(self): + self._demo_check() + self._start_job(JOB_DELETE, self._do_delete) + + def load(self): + self._start_job(JOB_LOAD, self._do_load) + self.LoadIgnoreList() + + def LoadIgnoreList(self): + p = op.join(self.appdata, 'ignore_list.xml') + self.scanner.ignore_list.load_from_xml(p) + + def make_reference(self, duplicates): + changed_groups = set() + for dupe in duplicates: + g = self.results.get_group_of_duplicate(dupe) + if g not in changed_groups: + self.results.make_ref(dupe) + changed_groups.add(g) + + def Save(self): + self.directories.SaveToFile(op.join(self.appdata, 'last_directories.xml')) + self.results.save_to_xml(op.join(self.appdata, 'last_results.xml')) + + def SaveIgnoreList(self): + p = op.join(self.appdata, 'ignore_list.xml') + self.scanner.ignore_list.save_to_xml(p) + + def start_scanning(self): + def do(j): + j.set_progress(0, 'Collecting files to scan') + files = list(self.directories.get_files()) + logging.info('Scanning %d files' % len(files)) + self.results.groups = self.scanner.GetDupeGroups(files, j) + + files = self.directories.get_files() + first_file = first(files) + if first_file is None: + raise NoScannableFileError() + if first_file.is_ref and all(f.is_ref for f in files): + raise AllFilesAreRefError() + self.results.groups = [] + self._start_job(JOB_SCAN, do) + + #--- Properties + @property + def stat_line(self): + result = self.results.stat_line + if self.scanner.discarded_file_count: + result = '%s (%d discarded)' % (result, self.scanner.discarded_file_count) + return result + diff --git a/py/app_cocoa.py b/py/app_cocoa.py new file mode 100644 index 00000000..4974d700 --- /dev/null +++ b/py/app_cocoa.py @@ -0,0 +1,304 @@ +#!/usr/bin/env python +""" +Unit Name: dupeguru.app_cocoa +Created By: Virgil Dupras +Created On: 2006/11/11 +Last modified by:$Author: virgil $ +Last modified on:$Date: 2009-05-28 16:33:32 +0200 (Thu, 28 May 2009) $ + $Revision: 4392 $ +Copyright 2006 Hardcoded Software (http://www.hardcoded.net) +""" +from AppKit import * +import logging +import os.path as op + +import hsfs as fs +from hsfs.phys.bundle import Bundle +from hsutil.cocoa import install_exception_hook +from hsutil.str import get_file_ext +from hsutil import io, cocoa, job +from hsutil.reg import RegistrationRequired + +import export, app, data + +JOBID2TITLE = { + app.JOB_SCAN: "Scanning for duplicates", + app.JOB_LOAD: "Loading", + app.JOB_MOVE: "Moving", + app.JOB_COPY: "Copying", + app.JOB_DELETE: "Sending to Trash", +} + +class DGDirectory(fs.phys.Directory): + def _create_sub_dir(self,name,with_parent = True): + ext = get_file_ext(name) + if ext == 'app': + if with_parent: + parent = self + else: + parent = None + return Bundle(parent,name) + else: + return super(DGDirectory,self)._create_sub_dir(name,with_parent) + + +def demo_method(method): + def wrapper(self, *args, **kwargs): + try: + return method(self, *args, **kwargs) + except RegistrationRequired: + NSNotificationCenter.defaultCenter().postNotificationName_object_('RegistrationRequired', self) + + return wrapper + +class DupeGuru(app.DupeGuru): + def __init__(self, data_module, appdata_subdir, appid): + LOGGING_LEVEL = logging.DEBUG if NSUserDefaults.standardUserDefaults().boolForKey_('debug') else logging.WARNING + logging.basicConfig(level=LOGGING_LEVEL, format='%(levelname)s %(message)s') + logging.debug('started in debug mode') + install_exception_hook() + if data_module is None: + data_module = data + appdata = op.expanduser(op.join('~', '.hsoftdata', appdata_subdir)) + app.DupeGuru.__init__(self, data_module, appdata, appid) + self.progress = cocoa.ThreadedJobPerformer() + self.directories.dirclass = DGDirectory + self.display_delta_values = False + self.selected_dupes = [] + self.RefreshDetailsTable(None,None) + + #--- Override + @staticmethod + def _recycle_dupe(dupe): + if not io.exists(dupe.path): + dupe.parent = None + return True + directory = unicode(dupe.parent.path) + filename = dupe.name + result, tag = NSWorkspace.sharedWorkspace().performFileOperation_source_destination_files_tag_( + NSWorkspaceRecycleOperation, directory, '', [filename]) + if not io.exists(dupe.path): + dupe.parent = None + return True + logging.warning('Could not send %s to trash. tag: %d' % (unicode(dupe.path), tag)) + return False + + def _start_job(self, jobid, func): + try: + j = self.progress.create_job() + self.progress.run_threaded(func, args=(j, )) + except job.JobInProgressError: + NSNotificationCenter.defaultCenter().postNotificationName_object_('JobInProgress', self) + else: + ud = {'desc': JOBID2TITLE[jobid], 'jobid':jobid} + NSNotificationCenter.defaultCenter().postNotificationName_object_userInfo_('JobStarted', self, ud) + + #---Helpers + def GetObjects(self,node_path): + #returns a tuple g,d + try: + g = self.results.groups[node_path[0]] + if len(node_path) == 2: + return (g,self.results.groups[node_path[0]].dupes[node_path[1]]) + else: + return (g,None) + except IndexError: + return (None,None) + + def GetDirectory(self,node_path,curr_dir=None): + if not node_path: + return curr_dir + if curr_dir is not None: + l = curr_dir.dirs + else: + l = self.directories + d = l[node_path[0]] + return self.GetDirectory(node_path[1:],d) + + def RefreshDetailsTable(self,dupe,group): + l1 = self.data.GetDisplayInfo(dupe,group,False) + if group is not None: + l2 = self.data.GetDisplayInfo(group.ref,group,False) + else: + l2 = l1 #To have a list of empty '---' values + names = [c['display'] for c in self.data.COLUMNS] + self.details_table = zip(names,l1,l2) + + #---Public + def AddSelectedToIgnoreList(self): + for dupe in self.selected_dupes: + self.AddToIgnoreList(dupe) + + copy_or_move_marked = demo_method(app.DupeGuru.copy_or_move_marked) + delete_marked = demo_method(app.DupeGuru.delete_marked) + + def ExportToXHTML(self,column_ids,xslt_path,css_path): + columns = [] + for index,column in enumerate(self.data.COLUMNS): + display = column['display'] + enabled = str(index) in column_ids + columns.append((display,enabled)) + xml_path = op.join(self.appdata,'results_export.xml') + self.results.save_to_xml(xml_path,self.data.GetDisplayInfo) + return export.export_to_xhtml(xml_path,xslt_path,css_path,columns) + + def MakeSelectedReference(self): + self.make_reference(self.selected_dupes) + + def OpenSelected(self): + if self.selected_dupes: + path = unicode(self.selected_dupes[0].path) + NSWorkspace.sharedWorkspace().openFile_(path) + + def PurgeIgnoreList(self): + self.scanner.ignore_list.Filter(lambda f,s:op.exists(f) and op.exists(s)) + + def RefreshDetailsWithSelected(self): + if self.selected_dupes: + self.RefreshDetailsTable( + self.selected_dupes[0], + self.results.get_group_of_duplicate(self.selected_dupes[0]) + ) + else: + self.RefreshDetailsTable(None,None) + + def RemoveDirectory(self,index): + try: + del self.directories[index] + except IndexError: + pass + + def RemoveSelected(self): + self.results.remove_duplicates(self.selected_dupes) + + def RenameSelected(self,newname): + try: + d = self.selected_dupes[0] + d = d.move(d.parent,newname) + return True + except (IndexError,fs.FSError),e: + logging.warning("dupeGuru Warning: %s" % str(e)) + return False + + def RevealSelected(self): + if self.selected_dupes: + path = unicode(self.selected_dupes[0].path) + NSWorkspace.sharedWorkspace().selectFile_inFileViewerRootedAtPath_(path,'') + + def start_scanning(self): + self.RefreshDetailsTable(None, None) + try: + app.DupeGuru.start_scanning(self) + return 0 + except app.NoScannableFileError: + return 3 + except app.AllFilesAreRefError: + return 1 + + def SelectResultNodePaths(self,node_paths): + def extract_dupe(t): + g,d = t + if d is not None: + return d + else: + if g is not None: + return g.ref + + selected = [extract_dupe(self.GetObjects(p)) for p in node_paths] + self.selected_dupes = [dupe for dupe in selected if dupe is not None] + + def SelectPowerMarkerNodePaths(self,node_paths): + rows = [p[0] for p in node_paths] + self.selected_dupes = [ + self.results.dupes[row] for row in rows if row in xrange(len(self.results.dupes)) + ] + + def SetDirectoryState(self,node_path,state): + d = self.GetDirectory(node_path) + self.directories.SetState(d.path,state) + + def sort_dupes(self,key,asc): + self.results.sort_dupes(key,asc,self.display_delta_values) + + def sort_groups(self,key,asc): + self.results.sort_groups(key,asc) + + def ToggleSelectedMarkState(self): + for dupe in self.selected_dupes: + self.results.mark_toggle(dupe) + + #---Data + def GetOutlineViewMaxLevel(self, tag): + if tag == 0: + return 2 + elif tag == 1: + return 0 + elif tag == 2: + return 1 + + def GetOutlineViewChildCounts(self, tag, node_path): + if self.progress._job_running: + return [] + if tag == 0: #Normal results + assert not node_path # no other value is possible + return [len(g.dupes) for g in self.results.groups] + elif tag == 1: #Directories + dirs = self.GetDirectory(node_path).dirs if node_path else self.directories + return [d.dircount for d in dirs] + else: #Power Marker + assert not node_path # no other value is possible + return [0 for d in self.results.dupes] + + def GetOutlineViewValues(self, tag, node_path): + if self.progress._job_running: + return + if not node_path: + return + if tag in (0,2): #Normal results / Power Marker + if tag == 0: + g, d = self.GetObjects(node_path) + if d is None: + d = g.ref + else: + d = self.results.dupes[node_path[0]] + g = self.results.get_group_of_duplicate(d) + result = self.data.GetDisplayInfo(d, g, self.display_delta_values) + return result + elif tag == 1: #Directories + d = self.GetDirectory(node_path) + return [ + d.name, + self.directories.GetState(d.path) + ] + + def GetOutlineViewMarked(self, tag, node_path): + # 0=unmarked 1=marked 2=unmarkable + if self.progress._job_running: + return + if not node_path: + return 2 + if tag == 1: #Directories + return 2 + if tag == 0: #Normal results + g, d = self.GetObjects(node_path) + else: #Power Marker + d = self.results.dupes[node_path[0]] + if (d is None) or (not self.results.is_markable(d)): + return 2 + elif self.results.is_marked(d): + return 1 + else: + return 0 + + def GetTableViewCount(self, tag): + if self.progress._job_running: + return 0 + return len(self.details_table) + + def GetTableViewMarkedIndexes(self,tag): + return [] + + def GetTableViewValues(self,tag,row): + return self.details_table[row] + + diff --git a/py/app_cocoa_test.py b/py/app_cocoa_test.py new file mode 100644 index 00000000..ad8b937a --- /dev/null +++ b/py/app_cocoa_test.py @@ -0,0 +1,320 @@ +#!/usr/bin/env python +""" +Unit Name: dupeguru.tests.app_cocoa +Created By: Virgil Dupras +Created On: 2006/11/11 +Last modified by:$Author: virgil $ +Last modified on:$Date: 2009-05-29 17:51:41 +0200 (Fri, 29 May 2009) $ + $Revision: 4409 $ +Copyright 2006 Hardcoded Software (http://www.hardcoded.net) +""" +import tempfile +import shutil +import logging + +from hsutil.path import Path +from hsutil.testcase import TestCase +from hsutil.decorators import log_calls +import hsfs.phys +import os.path as op + +from . import engine, data +try: + from .app_cocoa import DupeGuru as DupeGuruBase, DGDirectory +except ImportError: + from nose.plugins.skip import SkipTest + raise SkipTest("These tests can only be run on OS X") +from .results_test import GetTestGroups + +class DupeGuru(DupeGuruBase): + def __init__(self): + DupeGuruBase.__init__(self, data, '/tmp', appid=4) + + def _start_job(self, jobid, func): + func(nulljob) + + +def r2np(rows): + #Transforms a list of rows [1,2,3] into a list of node paths [[1],[2],[3]] + return [[i] for i in rows] + +class TCDupeGuru(TestCase): + def setUp(self): + self.app = DupeGuru() + self.objects,self.matches,self.groups = GetTestGroups() + self.app.results.groups = self.groups + + def test_GetObjects(self): + app = self.app + objects = self.objects + groups = self.groups + g,d = app.GetObjects([0]) + self.assert_(g is groups[0]) + self.assert_(d is None) + g,d = app.GetObjects([0,0]) + self.assert_(g is groups[0]) + self.assert_(d is objects[1]) + g,d = app.GetObjects([1,0]) + self.assert_(g is groups[1]) + self.assert_(d is objects[4]) + + def test_GetObjects_after_sort(self): + app = self.app + objects = self.objects + groups = self.groups[:] #To keep the old order in memory + app.sort_groups(0,False) #0 = Filename + #Now, the group order is supposed to be reversed + g,d = app.GetObjects([0,0]) + self.assert_(g is groups[1]) + self.assert_(d is objects[4]) + + def test_GetObjects_out_of_range(self): + app = self.app + self.assertEqual((None,None),app.GetObjects([2])) + self.assertEqual((None,None),app.GetObjects([])) + self.assertEqual((None,None),app.GetObjects([1,2])) + + def test_selectResultNodePaths(self): + app = self.app + objects = self.objects + app.SelectResultNodePaths([[0,0],[0,1]]) + self.assertEqual(2,len(app.selected_dupes)) + self.assert_(app.selected_dupes[0] is objects[1]) + self.assert_(app.selected_dupes[1] is objects[2]) + + def test_selectResultNodePaths_with_ref(self): + app = self.app + objects = self.objects + app.SelectResultNodePaths([[0,0],[0,1],[1]]) + self.assertEqual(3,len(app.selected_dupes)) + self.assert_(app.selected_dupes[0] is objects[1]) + self.assert_(app.selected_dupes[1] is objects[2]) + self.assert_(app.selected_dupes[2] is self.groups[1].ref) + + def test_selectResultNodePaths_empty(self): + self.app.SelectResultNodePaths([]) + self.assertEqual(0,len(self.app.selected_dupes)) + + def test_selectResultNodePaths_after_sort(self): + app = self.app + objects = self.objects + groups = self.groups[:] #To keep the old order in memory + app.sort_groups(0,False) #0 = Filename + #Now, the group order is supposed to be reversed + app.SelectResultNodePaths([[0,0],[1],[1,0]]) + self.assertEqual(3,len(app.selected_dupes)) + self.assert_(app.selected_dupes[0] is objects[4]) + self.assert_(app.selected_dupes[1] is groups[0].ref) + self.assert_(app.selected_dupes[2] is objects[1]) + + def test_selectResultNodePaths_out_of_range(self): + app = self.app + app.SelectResultNodePaths([[0,0],[0,1],[1],[1,1],[2]]) + self.assertEqual(3,len(app.selected_dupes)) + + def test_selectPowerMarkerRows(self): + app = self.app + objects = self.objects + app.SelectPowerMarkerNodePaths(r2np([0,1,2])) + self.assertEqual(3,len(app.selected_dupes)) + self.assert_(app.selected_dupes[0] is objects[1]) + self.assert_(app.selected_dupes[1] is objects[2]) + self.assert_(app.selected_dupes[2] is objects[4]) + + def test_selectPowerMarkerRows_empty(self): + self.app.SelectPowerMarkerNodePaths([]) + self.assertEqual(0,len(self.app.selected_dupes)) + + def test_selectPowerMarkerRows_after_sort(self): + app = self.app + objects = self.objects + app.sort_dupes(0,False) #0 = Filename + app.SelectPowerMarkerNodePaths(r2np([0,1,2])) + self.assertEqual(3,len(app.selected_dupes)) + self.assert_(app.selected_dupes[0] is objects[4]) + self.assert_(app.selected_dupes[1] is objects[2]) + self.assert_(app.selected_dupes[2] is objects[1]) + + def test_selectPowerMarkerRows_out_of_range(self): + app = self.app + app.SelectPowerMarkerNodePaths(r2np([0,1,2,3])) + self.assertEqual(3,len(app.selected_dupes)) + + def test_toggleSelectedMark(self): + app = self.app + objects = self.objects + app.ToggleSelectedMarkState() + self.assertEqual(0,app.results.mark_count) + app.SelectPowerMarkerNodePaths(r2np([0,2])) + app.ToggleSelectedMarkState() + self.assertEqual(2,app.results.mark_count) + self.assert_(not app.results.is_marked(objects[0])) + self.assert_(app.results.is_marked(objects[1])) + self.assert_(not app.results.is_marked(objects[2])) + self.assert_(not app.results.is_marked(objects[3])) + self.assert_(app.results.is_marked(objects[4])) + + def test_refreshDetailsWithSelected(self): + def mock_refresh(dupe,group): + self.called = True + if self.app.selected_dupes: + self.assert_(dupe is self.app.selected_dupes[0]) + self.assert_(group is self.app.results.get_group_of_duplicate(dupe)) + else: + self.assert_(dupe is None) + self.assert_(group is None) + + self.app.RefreshDetailsTable = mock_refresh + self.called = False + self.app.SelectPowerMarkerNodePaths(r2np([0,2])) + self.app.RefreshDetailsWithSelected() + self.assert_(self.called) + self.called = False + self.app.SelectPowerMarkerNodePaths([]) + self.app.RefreshDetailsWithSelected() + self.assert_(self.called) + + def test_makeSelectedReference(self): + app = self.app + objects = self.objects + groups = self.groups + app.SelectPowerMarkerNodePaths(r2np([0,2])) + app.MakeSelectedReference() + self.assert_(groups[0].ref is objects[1]) + self.assert_(groups[1].ref is objects[4]) + + def test_makeSelectedReference_by_selecting_two_dupes_in_the_same_group(self): + app = self.app + objects = self.objects + groups = self.groups + app.SelectPowerMarkerNodePaths(r2np([0,1,2])) + #Only 0 and 2 must go ref, not 1 because it is a part of the same group + app.MakeSelectedReference() + self.assert_(groups[0].ref is objects[1]) + self.assert_(groups[1].ref is objects[4]) + + def test_removeSelected(self): + app = self.app + app.SelectPowerMarkerNodePaths(r2np([0,2])) + app.RemoveSelected() + self.assertEqual(1,len(app.results.dupes)) + app.RemoveSelected() + self.assertEqual(1,len(app.results.dupes)) + app.SelectPowerMarkerNodePaths(r2np([0,2])) + app.RemoveSelected() + self.assertEqual(0,len(app.results.dupes)) + + def test_addDirectory_simple(self): + app = self.app + self.assertEqual(0,app.AddDirectory(self.datadirpath())) + self.assertEqual(1,len(app.directories)) + + def test_addDirectory_already_there(self): + app = self.app + self.assertEqual(0,app.AddDirectory(self.datadirpath())) + self.assertEqual(1,app.AddDirectory(self.datadirpath())) + + def test_addDirectory_does_not_exist(self): + app = self.app + self.assertEqual(2,app.AddDirectory('/does_not_exist')) + + def test_ignore(self): + app = self.app + app.SelectPowerMarkerNodePaths(r2np([2])) #The dupe of the second, 2 sized group + app.AddSelectedToIgnoreList() + self.assertEqual(1,len(app.scanner.ignore_list)) + app.SelectPowerMarkerNodePaths(r2np([0])) #first dupe of the 3 dupes group + app.AddSelectedToIgnoreList() + #BOTH the ref and the other dupe should have been added + self.assertEqual(3,len(app.scanner.ignore_list)) + + def test_purgeIgnoreList(self): + app = self.app + p1 = self.filepath('zerofile') + p2 = self.filepath('zerofill') + dne = '/does_not_exist' + app.scanner.ignore_list.Ignore(dne,p1) + app.scanner.ignore_list.Ignore(p2,dne) + app.scanner.ignore_list.Ignore(p1,p2) + app.PurgeIgnoreList() + self.assertEqual(1,len(app.scanner.ignore_list)) + self.assert_(app.scanner.ignore_list.AreIgnored(p1,p2)) + self.assert_(not app.scanner.ignore_list.AreIgnored(dne,p1)) + + def test_only_unicode_is_added_to_ignore_list(self): + def FakeIgnore(first,second): + if not isinstance(first,unicode): + self.fail() + if not isinstance(second,unicode): + self.fail() + + app = self.app + app.scanner.ignore_list.Ignore = FakeIgnore + app.SelectPowerMarkerNodePaths(r2np([2])) #The dupe of the second, 2 sized group + app.AddSelectedToIgnoreList() + + def test_dirclass(self): + self.assert_(self.app.directories.dirclass is DGDirectory) + + +class TCDupeGuru_renameSelected(TestCase): + def setUp(self): + p = Path(tempfile.mkdtemp()) + fp = open(str(p + 'foo bar 1'),mode='w') + fp.close() + fp = open(str(p + 'foo bar 2'),mode='w') + fp.close() + fp = open(str(p + 'foo bar 3'),mode='w') + fp.close() + refdir = hsfs.phys.Directory(None,str(p)) + matches = engine.MatchFactory().getmatches(refdir.files) + groups = engine.get_groups(matches) + g = groups[0] + g.prioritize(lambda x:x.name) + app = DupeGuru() + app.results.groups = groups + self.app = app + self.groups = groups + self.p = p + self.refdir = refdir + + def tearDown(self): + shutil.rmtree(str(self.p)) + + def test_simple(self): + app = self.app + refdir = self.refdir + g = self.groups[0] + app.SelectPowerMarkerNodePaths(r2np([0])) + self.assert_(app.RenameSelected('renamed')) + self.assert_('renamed' in refdir) + self.assert_('foo bar 2' not in refdir) + self.assert_(g.dupes[0] is refdir['renamed']) + self.assert_(g.dupes[0] in refdir) + + def test_none_selected(self): + app = self.app + refdir = self.refdir + g = self.groups[0] + app.SelectPowerMarkerNodePaths([]) + self.mock(logging, 'warning', log_calls(lambda msg: None)) + self.assert_(not app.RenameSelected('renamed')) + msg = logging.warning.calls[0]['msg'] + self.assertEqual('dupeGuru Warning: list index out of range', msg) + self.assert_('renamed' not in refdir) + self.assert_('foo bar 2' in refdir) + self.assert_(g.dupes[0] is refdir['foo bar 2']) + + def test_name_already_exists(self): + app = self.app + refdir = self.refdir + g = self.groups[0] + app.SelectPowerMarkerNodePaths(r2np([0])) + self.mock(logging, 'warning', log_calls(lambda msg: None)) + self.assert_(not app.RenameSelected('foo bar 1')) + msg = logging.warning.calls[0]['msg'] + self.assert_(msg.startswith('dupeGuru Warning: \'foo bar 2\' already exists in')) + self.assert_('foo bar 1' in refdir) + self.assert_('foo bar 2' in refdir) + self.assert_(g.dupes[0] is refdir['foo bar 2']) + diff --git a/py/app_me_cocoa.py b/py/app_me_cocoa.py new file mode 100644 index 00000000..51a61767 --- /dev/null +++ b/py/app_me_cocoa.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python +""" +Unit Name: dupeguru.app_me_cocoa +Created By: Virgil Dupras +Created On: 2006/11/16 +Last modified by:$Author: virgil $ +Last modified on:$Date: 2009-05-28 16:33:32 +0200 (Thu, 28 May 2009) $ + $Revision: 4392 $ +Copyright 2006 Hardcoded Software (http://www.hardcoded.net) +""" +import os.path as op +import logging +from appscript import app, k, CommandError +import time + +from hsutil.cocoa import as_fetch +import hsfs.phys.music + +import app_cocoa, data_me, scanner + +JOB_REMOVE_DEAD_TRACKS = 'jobRemoveDeadTracks' +JOB_SCAN_DEAD_TRACKS = 'jobScanDeadTracks' + +app_cocoa.JOBID2TITLE.update({ + JOB_REMOVE_DEAD_TRACKS: "Removing dead tracks from your iTunes Library", + JOB_SCAN_DEAD_TRACKS: "Scanning the iTunes Library", +}) + +class DupeGuruME(app_cocoa.DupeGuru): + def __init__(self): + app_cocoa.DupeGuru.__init__(self, data_me, 'dupeguru_me', appid=1) + self.scanner = scanner.ScannerME() + self.directories.dirclass = hsfs.phys.music.Directory + self.dead_tracks = [] + + def remove_dead_tracks(self): + def do(j): + a = app('iTunes') + for index, track in enumerate(j.iter_with_progress(self.dead_tracks)): + if index % 100 == 0: + time.sleep(.1) + try: + track.delete() + except CommandError as e: + logging.warning('Error while trying to remove a track from iTunes: %s' % unicode(e)) + + self._start_job(JOB_REMOVE_DEAD_TRACKS, do) + + def scan_dead_tracks(self): + def do(j): + a = app('iTunes') + try: + [source] = [s for s in a.sources() if s.kind() == k.library] + [library] = source.library_playlists() + except ValueError: + logging.warning('Some unexpected iTunes configuration encountered') + 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() == k.missing_value: + self.dead_tracks.append(track) + logging.info('Found %d dead tracks' % len(self.dead_tracks)) + + self._start_job(JOB_SCAN_DEAD_TRACKS, do) + diff --git a/py/app_pe_cocoa.py b/py/app_pe_cocoa.py new file mode 100644 index 00000000..5969d1c3 --- /dev/null +++ b/py/app_pe_cocoa.py @@ -0,0 +1,212 @@ +#!/usr/bin/env python +""" +Unit Name: dupeguru.app_pe_cocoa +Created By: Virgil Dupras +Created On: 2006/11/13 +Last modified by:$Author: virgil $ +Last modified on:$Date: 2009-05-28 16:33:32 +0200 (Thu, 28 May 2009) $ + $Revision: 4392 $ +Copyright 2006 Hardcoded Software (http://www.hardcoded.net) +""" +import os +import os.path as op +import logging +import plistlib + +import objc +from Foundation import * +from AppKit import * +from appscript import app, k + +from hsutil import job, io +import hsfs as fs +from hsfs import phys +from hsutil import files +from hsutil.str import get_file_ext +from hsutil.path import Path +from hsutil.cocoa import as_fetch + +import app_cocoa, data_pe, directories, picture.matchbase +from picture.cache import string_to_colors, Cache + +mainBundle = NSBundle.mainBundle() +PictureBlocks = mainBundle.classNamed_('PictureBlocks') +assert PictureBlocks is not None + +class Photo(phys.File): + cls_info_map = { + 'size': fs.IT_ATTRS, + 'ctime': fs.IT_ATTRS, + 'mtime': fs.IT_ATTRS, + 'md5': fs.IT_MD5, + 'md5partial': fs.IT_MD5, + 'dimensions': fs.IT_EXTRA, + } + + def _initialize_info(self,section): + super(Photo, self)._initialize_info(section) + if section == fs.IT_EXTRA: + self._info.update({ + 'dimensions': (0,0), + }) + + def _read_info(self,section): + super(Photo, self)._read_info(section) + if section == fs.IT_EXTRA: + size = PictureBlocks.getImageSize_(unicode(self.path)) + self._info['dimensions'] = (size.width, size.height) + + def get_blocks(self, block_count_per_side): + try: + blocks = PictureBlocks.getBlocksFromImagePath_blockCount_scanArea_(unicode(self.path), block_count_per_side, 0) + except Exception, e: + raise IOError('The reading of "%s" failed with "%s"' % (unicode(self.path), unicode(e))) + if not blocks: + raise IOError('The picture %s could not be read' % unicode(self.path)) + return string_to_colors(blocks) + + +class IPhoto(Photo): + def __init__(self, parent, whole_path): + super(IPhoto, self).__init__(parent, whole_path[-1]) + self.whole_path = whole_path + + def _build_path(self): + return self.whole_path + + @property + def display_path(self): + return super(IPhoto, self)._build_path() + + +class Directory(phys.Directory): + cls_file_class = Photo + cls_supported_exts = ('png', 'jpg', 'jpeg', 'gif', 'psd', 'bmp', 'tiff', 'nef', 'cr2') + + def _fetch_subitems(self): + subdirs, subfiles = super(Directory,self)._fetch_subitems() + return subdirs, [name for name in subfiles if get_file_ext(name) in self.cls_supported_exts] + + +class IPhotoLibrary(fs.Directory): + def __init__(self, plistpath): + self.plistpath = plistpath + self.refpath = plistpath[:-1] + # the AlbumData.xml file lives right in the library path + super(IPhotoLibrary, self).__init__(None, 'iPhoto Library') + + def _update_photo(self, photo_data): + if photo_data['MediaType'] != 'Image': + return + photo_path = Path(photo_data['ImagePath']) + subpath = photo_path[len(self.refpath):-1] + subdir = self + for element in subpath: + try: + subdir = subdir[element] + except KeyError: + subdir = fs.Directory(subdir, element) + IPhoto(subdir, photo_path) + + def update(self): + self.clear() + s = open(unicode(self.plistpath)).read() + # There was a case where a guy had 0x10 chars in his plist, causing expat errors on loading + s = s.replace('\x10', '') + plist = plistlib.readPlistFromString(s) + for photo_data in plist['Master Image List'].values(): + self._update_photo(photo_data) + + def force_update(self): # Don't update + pass + + +class DupeGuruPE(app_cocoa.DupeGuru): + def __init__(self): + app_cocoa.DupeGuru.__init__(self, data_pe, 'dupeguru_pe', appid=5) + self.scanner.match_factory = picture.matchbase.AsyncMatchFactory() + self.directories.dirclass = Directory + self.directories.special_dirclasses[Path('iPhoto Library')] = lambda _, __: self._create_iphoto_library() + p = op.join(self.appdata, 'cached_pictures.db') + self.scanner.match_factory.cached_blocks = Cache(p) + + def _create_iphoto_library(self): + ud = NSUserDefaults.standardUserDefaults() + prefs = ud.persistentDomainForName_('com.apple.iApps') + plisturl = NSURL.URLWithString_(prefs['iPhotoRecentDatabases'][0]) + plistpath = Path(plisturl.path()) + return IPhotoLibrary(plistpath) + + def _do_delete(self, j): + def op(dupe): + j.add_progress() + return self._do_delete_dupe(dupe) + + marked = [dupe for dupe in self.results.dupes if self.results.is_marked(dupe)] + self.path2iphoto = {} + if any(isinstance(dupe, IPhoto) for dupe in marked): + a = app('iPhoto') + a.select(a.photo_library_album()) + photos = as_fetch(a.photo_library_album().photos, k.item) + for photo in photos: + self.path2iphoto[photo.image_path()] = photo + self.last_op_error_count = self.results.perform_on_marked(op, True) + del self.path2iphoto + + def _do_delete_dupe(self, dupe): + if isinstance(dupe, IPhoto): + photo = self.path2iphoto[unicode(dupe.path)] + app('iPhoto').remove(photo) + return True + else: + return app_cocoa.DupeGuru._do_delete_dupe(self, dupe) + + def _do_load(self, j): + self.directories.LoadFromFile(op.join(self.appdata, 'last_directories.xml')) + for d in self.directories: + if isinstance(d, IPhotoLibrary): + d.update() + self.results.load_from_xml(op.join(self.appdata, 'last_results.xml'), self._get_file, j) + + def _get_file(self, str_path): + p = Path(str_path) + for d in self.directories: + result = None + if p in d.path: + result = d.find_path(p[d.path:]) + if isinstance(d, IPhotoLibrary) and p in d.refpath: + result = d.find_path(p[d.refpath:]) + if result is not None: + return result + + def AddDirectory(self, d): + try: + added = self.directories.add_path(Path(d)) + if d == 'iPhoto Library': + added.update() + return 0 + except directories.AlreadyThereError: + return 1 + + def CopyOrMove(self, dupe, copy, destination, dest_type): + if isinstance(dupe, IPhoto): + copy = True + return app_cocoa.DupeGuru.CopyOrMove(self, dupe, copy, destination, dest_type) + + def start_scanning(self): + for directory in self.directories: + if isinstance(directory, IPhotoLibrary): + self.directories.SetState(directory.refpath, directories.STATE_EXCLUDED) + return app_cocoa.DupeGuru.start_scanning(self) + + 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 + return ref.path + diff --git a/py/app_se_cocoa.py b/py/app_se_cocoa.py new file mode 100644 index 00000000..3d8c62b2 --- /dev/null +++ b/py/app_se_cocoa.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python +# Unit Name: app_se_cocoa +# Created By: Virgil Dupras +# Created On: 2009-05-24 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +import app_cocoa, data + +class DupeGuru(app_cocoa.DupeGuru): + def __init__(self): + app_cocoa.DupeGuru.__init__(self, data, 'dupeguru', appid=4) + diff --git a/py/app_test.py b/py/app_test.py new file mode 100644 index 00000000..af47067f --- /dev/null +++ b/py/app_test.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python +""" +Unit Name: dupeguru.tests.app +Created By: Virgil Dupras +Created On: 2007-06-23 +Last modified by:$Author: virgil $ +Last modified on:$Date: 2009-05-28 16:02:48 +0200 (Thu, 28 May 2009) $ + $Revision: 4388 $ +Copyright 2007 Hardcoded Software (http://www.hardcoded.net) +""" +import unittest +import os + +from hsutil.testcase import TestCase +from hsutil import io +from hsutil.path import Path +from hsutil.decorators import log_calls +import hsfs as fs +import hsfs.phys +import hsutil.files +from hsutil.job import nulljob + +from . import data, app +from .app import DupeGuru as DupeGuruBase + +class DupeGuru(DupeGuruBase): + def __init__(self): + DupeGuruBase.__init__(self, data, '/tmp', appid=4) + + def _start_job(self, jobid, func): + func(nulljob) + + +class TCDupeGuru(TestCase): + cls_tested_module = app + def test_ApplyFilter_calls_results_apply_filter(self): + app = DupeGuru() + self.mock(app.results, 'apply_filter', log_calls(app.results.apply_filter)) + app.ApplyFilter('foo') + self.assertEqual(2, len(app.results.apply_filter.calls)) + call = app.results.apply_filter.calls[0] + self.assert_(call['filter_str'] is None) + call = app.results.apply_filter.calls[1] + self.assertEqual('foo', call['filter_str']) + + def test_ApplyFilter_escapes_regexp(self): + app = DupeGuru() + self.mock(app.results, 'apply_filter', log_calls(app.results.apply_filter)) + app.ApplyFilter('()[]\\.|+?^abc') + call = app.results.apply_filter.calls[1] + self.assertEqual('\\(\\)\\[\\]\\\\\\.\\|\\+\\?\\^abc', call['filter_str']) + app.ApplyFilter('(*)') # In "simple mode", we want the * to behave as a wilcard + call = app.results.apply_filter.calls[3] + self.assertEqual('\(.*\)', call['filter_str']) + app.options['escape_filter_regexp'] = False + app.ApplyFilter('(abc)') + call = app.results.apply_filter.calls[5] + self.assertEqual('(abc)', call['filter_str']) + + def test_CopyOrMove(self): + # The goal here is just to have a test for a previous blowup I had. I know my test coverage + # for this unit is pathetic. What's done is done. My approach now is to add tests for + # every change I want to make. The blowup was caused by a missing import. + dupe_parent = fs.Directory(None, 'foo') + dupe = fs.File(dupe_parent, 'bar') + dupe.copy = log_calls(lambda dest, newname: None) + self.mock(hsutil.files, 'copy', log_calls(lambda source_path, dest_path: None)) + self.mock(os, 'makedirs', lambda path: None) # We don't want the test to create that fake directory + self.mock(fs.phys, 'Directory', fs.Directory) # We don't want an error because makedirs didn't work + app = DupeGuru() + app.CopyOrMove(dupe, True, 'some_destination', 0) + self.assertEqual(1, len(hsutil.files.copy.calls)) + call = hsutil.files.copy.calls[0] + self.assertEqual('some_destination', call['dest_path']) + self.assertEqual(dupe.path, call['source_path']) + + def test_CopyOrMove_clean_empty_dirs(self): + tmppath = Path(self.tmpdir()) + sourcepath = tmppath + 'source' + io.mkdir(sourcepath) + io.open(sourcepath + 'myfile', 'w') + tmpdir = hsfs.phys.Directory(None, unicode(tmppath)) + myfile = tmpdir['source']['myfile'] + app = DupeGuru() + self.mock(app, 'clean_empty_dirs', log_calls(lambda path: None)) + app.CopyOrMove(myfile, False, tmppath + 'dest', 0) + calls = app.clean_empty_dirs.calls + self.assertEqual(1, len(calls)) + self.assertEqual(sourcepath, calls[0]['path']) + + def test_Scan_with_objects_evaluating_to_false(self): + # At some point, any() was used in a wrong way that made Scan() wrongly return 1 + app = DupeGuru() + f1, f2 = [fs.File(None, 'foo') for i in range(2)] + f1.is_ref, f2.is_ref = (False, False) + assert not (bool(f1) and bool(f2)) + app.directories.get_files = lambda: [f1, f2] + app.directories._dirs.append('this is just so Scan() doesnt return 3') + app.start_scanning() # no exception + + +class TCDupeGuru_clean_empty_dirs(TestCase): + cls_tested_module = app + def setUp(self): + self.mock(hsutil.files, 'delete_if_empty', log_calls(lambda path, files_to_delete=[]: None)) + self.app = DupeGuru() + + def test_option_off(self): + self.app.clean_empty_dirs(Path('/foo/bar')) + self.assertEqual(0, len(hsutil.files.delete_if_empty.calls)) + + def test_option_on(self): + self.app.options['clean_empty_dirs'] = True + self.app.clean_empty_dirs(Path('/foo/bar')) + calls = hsutil.files.delete_if_empty.calls + self.assertEqual(1, len(calls)) + self.assertEqual(Path('/foo/bar'), calls[0]['path']) + self.assertEqual(['.DS_Store'], calls[0]['files_to_delete']) + + def test_recurse_up(self): + # delete_if_empty must be recursively called up in the path until it returns False + @log_calls + def mock_delete_if_empty(path, files_to_delete=[]): + return len(path) > 1 + + self.mock(hsutil.files, 'delete_if_empty', mock_delete_if_empty) + self.app.options['clean_empty_dirs'] = True + self.app.clean_empty_dirs(Path('not-empty/empty/empty')) + calls = hsutil.files.delete_if_empty.calls + self.assertEqual(3, len(calls)) + self.assertEqual(Path('not-empty/empty/empty'), calls[0]['path']) + self.assertEqual(Path('not-empty/empty'), calls[1]['path']) + self.assertEqual(Path('not-empty'), calls[2]['path']) + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/py/data.py b/py/data.py new file mode 100644 index 00000000..568a3400 --- /dev/null +++ b/py/data.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python +""" +Unit Name: dupeguru.data +Created By: Virgil Dupras +Created On: 2006/03/15 +Last modified by:$Author: virgil $ +Last modified on:$Date: 2009-05-28 15:22:39 +0200 (Thu, 28 May 2009) $ + $Revision: 4385 $ +Copyright 2004-2006 Hardcoded Software (http://www.hardcoded.net) +""" + +from hsutil.str import format_time, FT_DECIMAL, format_size + +import time + +def format_path(p): + return unicode(p[:-1]) + +def format_timestamp(t, delta): + if delta: + return format_time(t, FT_DECIMAL) + else: + if t > 0: + return time.strftime('%Y/%m/%d %H:%M:%S', time.localtime(t)) + else: + return '---' + +def format_words(w): + def do_format(w): + if isinstance(w, list): + return '(%s)' % ', '.join(do_format(item) for item in w) + else: + return w.replace('\n', ' ') + + return ', '.join(do_format(item) for item in w) + +def format_perc(p): + return "%0.0f" % p + +def format_dupe_count(c): + return str(c) if c else '---' + +def cmp_value(value): + return value.lower() if isinstance(value, basestring) else value + +COLUMNS = [ + {'attr':'name','display':'Filename'}, + {'attr':'path','display':'Directory'}, + {'attr':'size','display':'Size (KB)'}, + {'attr':'extension','display':'Kind'}, + {'attr':'ctime','display':'Creation'}, + {'attr':'mtime','display':'Modification'}, + {'attr':'percentage','display':'Match %'}, + {'attr':'words','display':'Words Used'}, + {'attr':'dupe_count','display':'Dupe Count'}, +] + +def GetDisplayInfo(dupe, group, delta=False): + if (dupe is None) or (group is None): + return ['---'] * len(COLUMNS) + size = dupe.size + ctime = dupe.ctime + mtime = dupe.mtime + m = group.get_match_of(dupe) + if m: + percentage = m.percentage + dupe_count = 0 + if delta: + r = group.ref + size -= r.size + ctime -= r.ctime + mtime -= r.mtime + else: + percentage = group.percentage + dupe_count = len(group.dupes) + return [ + dupe.name, + format_path(dupe.path), + format_size(size, 0, 1, False), + dupe.extension, + format_timestamp(ctime, delta and m), + format_timestamp(mtime, delta and m), + format_perc(percentage), + format_words(dupe.words), + format_dupe_count(dupe_count) + ] + +def GetDupeSortKey(dupe, get_group, key, delta): + if key == 6: + m = get_group().get_match_of(dupe) + return m.percentage + if key == 8: + return 0 + r = cmp_value(getattr(dupe, COLUMNS[key]['attr'])) + if delta and (key in (2, 4, 5)): + r -= cmp_value(getattr(get_group().ref, COLUMNS[key]['attr'])) + return r + +def GetGroupSortKey(group, key): + if key == 6: + return group.percentage + if key == 8: + return len(group) + return cmp_value(getattr(group.ref, COLUMNS[key]['attr'])) + diff --git a/py/data_me.py b/py/data_me.py new file mode 100644 index 00000000..70d3ae66 --- /dev/null +++ b/py/data_me.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python +""" +Unit Name: dupeguru.data +Created By: Virgil Dupras +Created On: 2006/03/15 +Last modified by:$Author: virgil $ +Last modified on:$Date: 2009-05-28 15:22:39 +0200 (Thu, 28 May 2009) $ + $Revision: 4385 $ +Copyright 2004-2006 Hardcoded Software (http://www.hardcoded.net) +""" + +from hsutil.str import format_time, FT_MINUTES, format_size +from .data import (format_path, format_timestamp, format_words, format_perc, + format_dupe_count, cmp_value) + +COLUMNS = [ + {'attr':'name','display':'Filename'}, + {'attr':'path','display':'Directory'}, + {'attr':'size','display':'Size (MB)'}, + {'attr':'duration','display':'Time'}, + {'attr':'bitrate','display':'Bitrate'}, + {'attr':'samplerate','display':'Sample Rate'}, + {'attr':'extension','display':'Kind'}, + {'attr':'ctime','display':'Creation'}, + {'attr':'mtime','display':'Modification'}, + {'attr':'title','display':'Title'}, + {'attr':'artist','display':'Artist'}, + {'attr':'album','display':'Album'}, + {'attr':'genre','display':'Genre'}, + {'attr':'year','display':'Year'}, + {'attr':'track','display':'Track Number'}, + {'attr':'comment','display':'Comment'}, + {'attr':'percentage','display':'Match %'}, + {'attr':'words','display':'Words Used'}, + {'attr':'dupe_count','display':'Dupe Count'}, +] + +def GetDisplayInfo(dupe, group, delta=False): + if (dupe is None) or (group is None): + return ['---'] * len(COLUMNS) + size = dupe.size + duration = dupe.duration + bitrate = dupe.bitrate + samplerate = dupe.samplerate + ctime = dupe.ctime + mtime = dupe.mtime + m = group.get_match_of(dupe) + if m: + percentage = m.percentage + dupe_count = 0 + if delta: + r = group.ref + size -= r.size + duration -= r.duration + bitrate -= r.bitrate + samplerate -= r.samplerate + ctime -= r.ctime + mtime -= r.mtime + else: + percentage = group.percentage + dupe_count = len(group.dupes) + return [ + dupe.name, + format_path(dupe.path), + format_size(size, 2, 2, False), + format_time(duration, FT_MINUTES), + str(bitrate), + str(samplerate), + dupe.extension, + format_timestamp(ctime,delta and m), + format_timestamp(mtime,delta and m), + dupe.title, + dupe.artist, + dupe.album, + dupe.genre, + dupe.year, + str(dupe.track), + dupe.comment, + format_perc(percentage), + format_words(dupe.words), + format_dupe_count(dupe_count) + ] + +def GetDupeSortKey(dupe, get_group, key, delta): + if key == 16: + m = get_group().get_match_of(dupe) + return m.percentage + if key == 18: + return 0 + r = cmp_value(getattr(dupe, COLUMNS[key]['attr'])) + if delta and (key in (2, 3, 4, 7, 8)): + r -= cmp_value(getattr(get_group().ref, COLUMNS[key]['attr'])) + return r + +def GetGroupSortKey(group, key): + if key == 16: + return group.percentage + if key == 18: + return len(group) + return cmp_value(getattr(group.ref, COLUMNS[key]['attr'])) diff --git a/py/data_pe.py b/py/data_pe.py new file mode 100644 index 00000000..94bdd99d --- /dev/null +++ b/py/data_pe.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python +""" +Unit Name: dupeguru.data +Created By: Virgil Dupras +Created On: 2006/03/15 +Last modified by:$Author: virgil $ +Last modified on:$Date: 2009-05-28 15:22:39 +0200 (Thu, 28 May 2009) $ + $Revision: 4385 $ +Copyright 2004-2006 Hardcoded Software (http://www.hardcoded.net) +""" +from hsutil.str import format_size +from .data import format_path, format_timestamp, format_perc, format_dupe_count, cmp_value + +def format_dimensions(dimensions): + return '%d x %d' % (dimensions[0], dimensions[1]) + +COLUMNS = [ + {'attr':'name','display':'Filename'}, + {'attr':'path','display':'Directory'}, + {'attr':'size','display':'Size (KB)'}, + {'attr':'extension','display':'Kind'}, + {'attr':'dimensions','display':'Dimensions'}, + {'attr':'ctime','display':'Creation'}, + {'attr':'mtime','display':'Modification'}, + {'attr':'percentage','display':'Match %'}, + {'attr':'dupe_count','display':'Dupe Count'}, +] + +def GetDisplayInfo(dupe,group,delta=False): + if (dupe is None) or (group is None): + return ['---'] * len(COLUMNS) + size = dupe.size + ctime = dupe.ctime + mtime = dupe.mtime + m = group.get_match_of(dupe) + if m: + percentage = m.percentage + dupe_count = 0 + if delta: + r = group.ref + size -= r.size + ctime -= r.ctime + mtime -= r.mtime + else: + percentage = group.percentage + dupe_count = len(group.dupes) + dupe_path = getattr(dupe, 'display_path', dupe.path) + return [ + dupe.name, + format_path(dupe_path), + format_size(size, 0, 1, False), + dupe.extension, + format_dimensions(dupe.dimensions), + format_timestamp(ctime, delta and m), + format_timestamp(mtime, delta and m), + format_perc(percentage), + format_dupe_count(dupe_count) + ] + +def GetDupeSortKey(dupe, get_group, key, delta): + if key == 7: + m = get_group().get_match_of(dupe) + return m.percentage + if key == 8: + return 0 + r = cmp_value(getattr(dupe, COLUMNS[key]['attr'])) + if delta and (key in (2, 5, 6)): + r -= cmp_value(getattr(get_group().ref, COLUMNS[key]['attr'])) + return r + +def GetGroupSortKey(group, key): + if key == 7: + return group.percentage + if key == 8: + return len(group) + return cmp_value(getattr(group.ref, COLUMNS[key]['attr'])) + diff --git a/py/directories.py b/py/directories.py new file mode 100644 index 00000000..3d73b5c5 --- /dev/null +++ b/py/directories.py @@ -0,0 +1,161 @@ +#!/usr/bin/env python +""" +Unit Name: dupeguru.directories +Created By: Virgil Dupras +Created On: 2006/02/27 +Last modified by:$Author: virgil $ +Last modified on:$Date: 2009-05-28 16:02:48 +0200 (Thu, 28 May 2009) $ + $Revision: 4388 $ +Copyright 2004-2006 Hardcoded Software (http://www.hardcoded.net) +""" +import xml.dom.minidom + +from hsfs import phys +import hsfs as fs +from hsutil.files import FileOrPath +from hsutil.path import Path + +(STATE_NORMAL, +STATE_REFERENCE, +STATE_EXCLUDED) = range(3) + +class AlreadyThereError(Exception): + """The path being added is already in the directory list""" + +class InvalidPathError(Exception): + """The path being added is invalid""" + +class Directories(object): + #---Override + def __init__(self): + self._dirs = [] + self.states = {} + self.dirclass = phys.Directory + self.special_dirclasses = {} + + def __contains__(self,path): + for d in self._dirs: + if path in d.path: + return True + return False + + def __delitem__(self,key): + self._dirs.__delitem__(key) + + def __getitem__(self,key): + return self._dirs.__getitem__(key) + + def __len__(self): + return len(self._dirs) + + #---Private + def _get_files(self, from_dir, state=STATE_NORMAL): + state = self.states.get(from_dir.path, state) + result = [] + for subdir in from_dir.dirs: + for file in self._get_files(subdir, state): + yield file + if state != STATE_EXCLUDED: + for file in from_dir.files: + file.is_ref = state == STATE_REFERENCE + yield file + + #---Public + def add_path(self, path): + """Adds 'path' to self, if not already there. + + Raises AlreadyThereError if 'path' is already in self. If path is a directory containing + some of the directories already present in self, 'path' will be added, but all directories + under it will be removed. Can also raise InvalidPathError if 'path' does not exist. + """ + if path in self: + raise AlreadyThereError + self._dirs = [d for d in self._dirs if d.path not in path] + try: + dirclass = self.special_dirclasses.get(path, self.dirclass) + d = dirclass(None, unicode(path)) + d[:] #If an InvalidPath exception has to be raised, it will be raised here + self._dirs.append(d) + return d + except fs.InvalidPath: + raise InvalidPathError + + def get_files(self): + """Returns a list of all files that are not excluded. + + Returned files also have their 'is_ref' attr set. + """ + for d in self._dirs: + d.force_update() + try: + for file in self._get_files(d): + yield file + except fs.InvalidPath: + pass + + def GetState(self, path): + """Returns the state of 'path' (One of the STATE_* const.) + + Raises LookupError if 'path' is not in self. + """ + if path not in self: + raise LookupError("The path '%s' is not in the directory list." % str(path)) + try: + return self.states[path] + except KeyError: + if path[-1].startswith('.'): # hidden + return STATE_EXCLUDED + parent = path[:-1] + if parent in self: + return self.GetState(parent) + else: + return STATE_NORMAL + + def LoadFromFile(self,infile): + try: + doc = xml.dom.minidom.parse(infile) + except: + return + root_dir_nodes = doc.getElementsByTagName('root_directory') + for rdn in root_dir_nodes: + if not rdn.getAttributeNode('path'): + continue + path = rdn.getAttributeNode('path').nodeValue + try: + self.add_path(Path(path)) + except (AlreadyThereError,InvalidPathError): + pass + state_nodes = doc.getElementsByTagName('state') + for sn in state_nodes: + if not (sn.getAttributeNode('path') and sn.getAttributeNode('value')): + continue + path = sn.getAttributeNode('path').nodeValue + state = sn.getAttributeNode('value').nodeValue + self.SetState(Path(path), int(state)) + + def Remove(self,directory): + self._dirs.remove(directory) + + def SaveToFile(self,outfile): + with FileOrPath(outfile, 'wb') as fp: + doc = xml.dom.minidom.Document() + root = doc.appendChild(doc.createElement('directories')) + for root_dir in self: + root_dir_node = root.appendChild(doc.createElement('root_directory')) + root_dir_node.setAttribute('path', unicode(root_dir.path).encode('utf-8')) + for path,state in self.states.iteritems(): + state_node = root.appendChild(doc.createElement('state')) + state_node.setAttribute('path', unicode(path).encode('utf-8')) + state_node.setAttribute('value', str(state)) + doc.writexml(fp,'\t','\t','\n',encoding='utf-8') + + def SetState(self,path,state): + try: + if self.GetState(path) == state: + return + self.states[path] = state + if (self.GetState(path[:-1]) == state) and (not path[-1].startswith('.')): + del self.states[path] + except LookupError: + pass + diff --git a/py/directories_test.py b/py/directories_test.py new file mode 100644 index 00000000..7d34c343 --- /dev/null +++ b/py/directories_test.py @@ -0,0 +1,280 @@ +#!/usr/bin/env python +""" +Unit Name: dupeguru.tests.directories +Created By: Virgil Dupras +Created On: 2006/02/27 +Last modified by:$Author: virgil $ +Last modified on:$Date: 2009-05-29 08:51:14 +0200 (Fri, 29 May 2009) $ + $Revision: 4398 $ +Copyright 2004-2006 Hardcoded Software (http://www.hardcoded.net) +""" +import unittest +import os.path as op +import os +import time +import shutil + +from hsutil import job, io +from hsutil.path import Path +from hsutil.testcase import TestCase +import hsfs.phys +from hsfs.phys import phys_test + +from directories import * + +testpath = Path(TestCase.datadirpath()) + +class TCDirectories(TestCase): + def test_empty(self): + d = Directories() + self.assertEqual(0,len(d)) + self.assert_('foobar' not in d) + + def test_add_path(self): + d = Directories() + p = testpath + 'utils' + added = d.add_path(p) + self.assertEqual(1,len(d)) + self.assert_(p in d) + self.assert_((p + 'foobar') in d) + self.assert_(p[:-1] not in d) + self.assertEqual(p,added.path) + self.assert_(d[0] is added) + p = self.tmppath() + d.add_path(p) + self.assertEqual(2,len(d)) + self.assert_(p in d) + + def test_AddPath_when_path_is_already_there(self): + d = Directories() + p = testpath + 'utils' + d.add_path(p) + self.assertRaises(AlreadyThereError, d.add_path, p) + self.assertRaises(AlreadyThereError, d.add_path, p + 'foobar') + self.assertEqual(1, len(d)) + + def test_AddPath_containing_paths_already_there(self): + d = Directories() + d.add_path(testpath + 'utils') + self.assertEqual(1, len(d)) + added = d.add_path(testpath) + self.assertEqual(1, len(d)) + self.assert_(added is d[0]) + + def test_AddPath_non_latin(self): + p = Path(self.tmpdir()) + to_add = p + u'unicode\u201a' + os.mkdir(unicode(to_add)) + d = Directories() + try: + d.add_path(to_add) + except UnicodeDecodeError: + self.fail() + + def test_del(self): + d = Directories() + d.add_path(testpath + 'utils') + try: + del d[1] + self.fail() + except IndexError: + pass + d.add_path(self.tmppath()) + del d[1] + self.assertEqual(1, len(d)) + + def test_states(self): + d = Directories() + p = testpath + 'utils' + d.add_path(p) + self.assertEqual(STATE_NORMAL,d.GetState(p)) + d.SetState(p,STATE_REFERENCE) + self.assertEqual(STATE_REFERENCE,d.GetState(p)) + self.assertEqual(STATE_REFERENCE,d.GetState(p + 'dir1')) + self.assertEqual(1,len(d.states)) + self.assertEqual(p,d.states.keys()[0]) + self.assertEqual(STATE_REFERENCE,d.states[p]) + + def test_GetState_with_path_not_there(self): + d = Directories() + d.add_path(testpath + 'utils') + self.assertRaises(LookupError,d.GetState,testpath) + + def test_states_remain_when_larger_directory_eat_smaller_ones(self): + d = Directories() + p = testpath + 'utils' + d.add_path(p) + d.SetState(p,STATE_EXCLUDED) + d.add_path(testpath) + d.SetState(testpath,STATE_REFERENCE) + self.assertEqual(STATE_EXCLUDED,d.GetState(p)) + self.assertEqual(STATE_EXCLUDED,d.GetState(p + 'dir1')) + self.assertEqual(STATE_REFERENCE,d.GetState(testpath)) + + def test_SetState_keep_state_dict_size_to_minimum(self): + d = Directories() + p = Path(phys_test.create_fake_fs(self.tmpdir())) + d.add_path(p) + d.SetState(p,STATE_REFERENCE) + d.SetState(p + 'dir1',STATE_REFERENCE) + self.assertEqual(1,len(d.states)) + self.assertEqual(STATE_REFERENCE,d.GetState(p + 'dir1')) + d.SetState(p + 'dir1',STATE_NORMAL) + self.assertEqual(2,len(d.states)) + self.assertEqual(STATE_NORMAL,d.GetState(p + 'dir1')) + d.SetState(p + 'dir1',STATE_REFERENCE) + self.assertEqual(1,len(d.states)) + self.assertEqual(STATE_REFERENCE,d.GetState(p + 'dir1')) + + def test_get_files(self): + d = Directories() + p = Path(phys_test.create_fake_fs(self.tmpdir())) + d.add_path(p) + d.SetState(p + 'dir1',STATE_REFERENCE) + d.SetState(p + 'dir2',STATE_EXCLUDED) + files = d.get_files() + self.assertEqual(5, len(list(files))) + for f in files: + if f.parent.path == p + 'dir1': + self.assert_(f.is_ref) + else: + self.assert_(not f.is_ref) + + def test_get_files_with_inherited_exclusion(self): + d = Directories() + p = testpath + 'utils' + d.add_path(p) + d.SetState(p,STATE_EXCLUDED) + self.assertEqual([], list(d.get_files())) + + def test_save_and_load(self): + d1 = Directories() + d2 = Directories() + p1 = self.tmppath() + p2 = self.tmppath() + d1.add_path(p1) + d1.add_path(p2) + d1.SetState(p1, STATE_REFERENCE) + d1.SetState(p1 + 'dir1',STATE_EXCLUDED) + tmpxml = op.join(self.tmpdir(), 'directories_testunit.xml') + d1.SaveToFile(tmpxml) + d2.LoadFromFile(tmpxml) + self.assertEqual(2, len(d2)) + self.assertEqual(STATE_REFERENCE,d2.GetState(p1)) + self.assertEqual(STATE_EXCLUDED,d2.GetState(p1 + 'dir1')) + + def test_invalid_path(self): + d = Directories() + p = Path('does_not_exist') + self.assertRaises(InvalidPathError, d.add_path, p) + self.assertEqual(0, len(d)) + + def test_SetState_on_invalid_path(self): + d = Directories() + try: + d.SetState(Path('foobar',),STATE_NORMAL) + except LookupError: + self.fail() + + def test_default_dirclass(self): + self.assert_(Directories().dirclass is hsfs.phys.Directory) + + def test_dirclass(self): + class MySpecialDirclass(hsfs.phys.Directory): pass + d = Directories() + d.dirclass = MySpecialDirclass + d.add_path(testpath) + self.assert_(isinstance(d[0], MySpecialDirclass)) + + def test_LoadFromFile_with_invalid_path(self): + #This test simulates a load from file resulting in a + #InvalidPath raise. Other directories must be loaded. + d1 = Directories() + d1.add_path(testpath + 'utils') + #Will raise InvalidPath upon loading + d1.add_path(self.tmppath()).name = 'does_not_exist' + tmpxml = op.join(self.tmpdir(), 'directories_testunit.xml') + d1.SaveToFile(tmpxml) + d2 = Directories() + d2.LoadFromFile(tmpxml) + self.assertEqual(1, len(d2)) + + def test_LoadFromFile_with_same_paths(self): + #This test simulates a load from file resulting in a + #AlreadyExists raise. Other directories must be loaded. + d1 = Directories() + p1 = self.tmppath() + p2 = self.tmppath() + d1.add_path(p1) + d1.add_path(p2) + #Will raise AlreadyExists upon loading + d1.add_path(self.tmppath()).name = unicode(p1) + tmpxml = op.join(self.tmpdir(), 'directories_testunit.xml') + d1.SaveToFile(tmpxml) + d2 = Directories() + d2.LoadFromFile(tmpxml) + self.assertEqual(2, len(d2)) + + def test_Remove(self): + d = Directories() + d1 = d.add_path(self.tmppath()) + d2 = d.add_path(self.tmppath()) + d.Remove(d1) + self.assertEqual(1, len(d)) + self.assert_(d[0] is d2) + + def test_unicode_save(self): + d = Directories() + p1 = self.tmppath() + u'hello\xe9' + io.mkdir(p1) + io.mkdir(p1 + u'foo\xe9') + d.add_path(p1) + d.SetState(d[0][0].path, STATE_EXCLUDED) + tmpxml = op.join(self.tmpdir(), 'directories_testunit.xml') + try: + d.SaveToFile(tmpxml) + except UnicodeDecodeError: + self.fail() + + def test_get_files_refreshes_its_directories(self): + d = Directories() + p = Path(phys_test.create_fake_fs(self.tmpdir())) + d.add_path(p) + files = d.get_files() + self.assertEqual(6, len(list(files))) + time.sleep(1) + os.remove(str(p + ('dir1','file1.test'))) + files = d.get_files() + self.assertEqual(5, len(list(files))) + + def test_get_files_does_not_choke_on_non_existing_directories(self): + d = Directories() + p = Path(self.tmpdir()) + d.add_path(p) + io.rmtree(p) + self.assertEqual([], list(d.get_files())) + + def test_GetState_returns_excluded_by_default_for_hidden_directories(self): + d = Directories() + p = Path(self.tmpdir()) + hidden_dir_path = p + '.foo' + io.mkdir(p + '.foo') + d.add_path(p) + self.assertEqual(d.GetState(hidden_dir_path), STATE_EXCLUDED) + # But it can be overriden + d.SetState(hidden_dir_path, STATE_NORMAL) + self.assertEqual(d.GetState(hidden_dir_path), STATE_NORMAL) + + def test_special_dirclasses(self): + # if a path is in special_dirclasses, use this class instead + class MySpecialDirclass(hsfs.phys.Directory): pass + d = Directories() + p1 = self.tmppath() + p2 = self.tmppath() + d.special_dirclasses[p1] = MySpecialDirclass + self.assert_(isinstance(d.add_path(p2), hsfs.phys.Directory)) + self.assert_(isinstance(d.add_path(p1), MySpecialDirclass)) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/py/engine.py b/py/engine.py new file mode 100644 index 00000000..a826902d --- /dev/null +++ b/py/engine.py @@ -0,0 +1,360 @@ +#!/usr/bin/env python +""" +Unit Name: dupeguru.engine +Created By: Virgil Dupras +Created On: 2006/01/29 +Last modified by:$Author: virgil $ +Last modified on:$Date: $ + $Revision: $ +Copyright 2007 Hardcoded Software (http://www.hardcoded.net) +""" +from __future__ import division +import difflib +import logging +import string +from collections import defaultdict, namedtuple +from unicodedata import normalize + +from hsutil.str import multi_replace +from hsutil import job + +(WEIGHT_WORDS, +MATCH_SIMILAR_WORDS, +NO_FIELD_ORDER) = range(3) + +JOB_REFRESH_RATE = 100 + +def getwords(s): + if isinstance(s, unicode): + s = normalize('NFD', s) + s = multi_replace(s, "-_&+():;\\[]{}.,<>/?~!@#$*", ' ').lower() + s = ''.join(c for c in s if c in string.ascii_letters + string.digits + string.whitespace) + return filter(None, s.split(' ')) # filter() is to remove empty elements + +def getfields(s): + fields = [getwords(field) for field in s.split(' - ')] + return filter(None, fields) + +def unpack_fields(fields): + result = [] + for field in fields: + if isinstance(field, list): + result += field + else: + result.append(field) + return result + +def compare(first, second, flags=()): + """Returns the % of words that match between first and second + + The result is a int in the range 0..100. + First and second can be either a string or a list. + """ + if not (first and second): + return 0 + if any(isinstance(element, list) for element in first): + return compare_fields(first, second, flags) + second = second[:] #We must use a copy of second because we remove items from it + match_similar = MATCH_SIMILAR_WORDS in flags + weight_words = WEIGHT_WORDS in flags + joined = first + second + total_count = (sum(len(word) for word in joined) if weight_words else len(joined)) + match_count = 0 + in_order = True + for word in first: + if match_similar and (word not in second): + similar = difflib.get_close_matches(word, second, 1, 0.8) + if similar: + word = similar[0] + if word in second: + if second[0] != word: + in_order = False + second.remove(word) + match_count += (len(word) if weight_words else 1) + result = round(((match_count * 2) / total_count) * 100) + if (result == 100) and (not in_order): + result = 99 # We cannot consider a match exact unless the ordering is the same + return result + +def compare_fields(first, second, flags=()): + """Returns the score for the lowest matching fields. + + first and second must be lists of lists of string. + """ + if len(first) != len(second): + return 0 + if NO_FIELD_ORDER in flags: + results = [] + #We don't want to remove field directly in the list. We must work on a copy. + second = second[:] + for field1 in first: + max = 0 + matched_field = None + for field2 in second: + r = compare(field1, field2, flags) + if r > max: + max = r + matched_field = field2 + results.append(max) + if matched_field: + second.remove(matched_field) + else: + results = [compare(word1, word2, flags) for word1, word2 in zip(first, second)] + return min(results) if results else 0 + +def build_word_dict(objects, j=job.nulljob): + """Returns a dict of objects mapped by their words. + + objects must have a 'words' attribute being a list of strings or a list of lists of strings. + + The result will be a dict with words as keys, lists of objects as values. + """ + result = defaultdict(set) + for object in j.iter_with_progress(objects, 'Prepared %d/%d files', JOB_REFRESH_RATE): + for word in unpack_fields(object.words): + result[word].add(object) + return result + +def merge_similar_words(word_dict): + """Take all keys in word_dict that are similar, and merge them together. + """ + keys = word_dict.keys() + keys.sort(key=len)# we want the shortest word to stay + while keys: + key = keys.pop(0) + similars = difflib.get_close_matches(key, keys, 100, 0.8) + if not similars: + continue + objects = word_dict[key] + for similar in similars: + objects |= word_dict[similar] + del word_dict[similar] + keys.remove(similar) + +def reduce_common_words(word_dict, threshold): + """Remove all objects from word_dict values where the object count >= threshold + + The exception to this removal are the objects where all the words of the object are common. + Because if we remove them, we will miss some duplicates! + """ + uncommon_words = set(word for word, objects in word_dict.items() if len(objects) < threshold) + for word, objects in word_dict.items(): + if len(objects) < threshold: + continue + reduced = set() + for o in objects: + if not any(w in uncommon_words for w in unpack_fields(o.words)): + reduced.add(o) + if reduced: + word_dict[word] = reduced + else: + del word_dict[word] + +Match = namedtuple('Match', 'first second percentage') +def get_match(first, second, flags=()): + #it is assumed here that first and second both have a "words" attribute + percentage = compare(first.words, second.words, flags) + return Match(first, second, percentage) + +class MatchFactory(object): + common_word_threshold = 50 + match_similar_words = False + min_match_percentage = 0 + weight_words = False + no_field_order = False + limit = 5000000 + + def getmatches(self, objects, j=job.nulljob): + j = j.start_subjob(2) + sj = j.start_subjob(2) + for o in objects: + if not hasattr(o, 'words'): + o.words = getwords(o.name) + word_dict = build_word_dict(objects, sj) + reduce_common_words(word_dict, self.common_word_threshold) + if self.match_similar_words: + merge_similar_words(word_dict) + match_flags = [] + if self.weight_words: + match_flags.append(WEIGHT_WORDS) + if self.match_similar_words: + match_flags.append(MATCH_SIMILAR_WORDS) + if self.no_field_order: + match_flags.append(NO_FIELD_ORDER) + j.start_job(len(word_dict), '0 matches found') + compared = defaultdict(set) + result = [] + try: + # This whole 'popping' thing is there to avoid taking too much memory at the same time. + while word_dict: + items = word_dict.popitem()[1] + while items: + ref = items.pop() + compared_already = compared[ref] + to_compare = items - compared_already + compared_already |= to_compare + for other in to_compare: + m = get_match(ref, other, match_flags) + if m.percentage >= self.min_match_percentage: + result.append(m) + if len(result) >= self.limit: + return result + j.add_progress(desc='%d matches found' % len(result)) + except MemoryError: + # This is the place where the memory usage is at its peak during the scan. + # Just continue the process with an incomplete list of matches. + del compared # This should give us enough room to call logging. + logging.warning('Memory Overflow. Matches: %d. Word dict: %d' % (len(result), len(word_dict))) + return result + return result + + +class Group(object): + #---Override + def __init__(self): + self._clear() + + def __contains__(self, item): + return item in self.unordered + + def __getitem__(self, key): + return self.ordered.__getitem__(key) + + def __iter__(self): + return iter(self.ordered) + + def __len__(self): + return len(self.ordered) + + #---Private + def _clear(self): + self._percentage = None + self._matches_for_ref = None + self.matches = set() + self.candidates = defaultdict(set) + self.ordered = [] + self.unordered = set() + + def _get_matches_for_ref(self): + if self._matches_for_ref is None: + ref = self.ref + self._matches_for_ref = [match for match in self.matches if ref in match] + return self._matches_for_ref + + #---Public + def add_match(self, match): + def add_candidate(item, match): + matches = self.candidates[item] + matches.add(match) + if self.unordered <= matches: + self.ordered.append(item) + self.unordered.add(item) + + if match in self.matches: + return + self.matches.add(match) + first, second, _ = match + if first not in self.unordered: + add_candidate(first, second) + if second not in self.unordered: + add_candidate(second, first) + self._percentage = None + self._matches_for_ref = None + + def clean_matches(self): + self.matches = set(m for m in self.matches if (m.first in self.unordered) and (m.second in self.unordered)) + self.candidates = defaultdict(set) + + def get_match_of(self, item): + if item is self.ref: + return + for m in self._get_matches_for_ref(): + if item in m: + return m + + def prioritize(self, key_func, tie_breaker=None): + # tie_breaker(ref, dupe) --> True if dupe should be ref + self.ordered.sort(key=key_func) + if tie_breaker is None: + return + ref = self.ref + key_value = key_func(ref) + for dupe in self.dupes: + if key_func(dupe) != key_value: + break + if tie_breaker(ref, dupe): + ref = dupe + if ref is not self.ref: + self.switch_ref(ref) + + def remove_dupe(self, item, clean_matches=True): + try: + self.ordered.remove(item) + self.unordered.remove(item) + self._percentage = None + self._matches_for_ref = None + if (len(self) > 1) and any(not getattr(item, 'is_ref', False) for item in self): + if clean_matches: + self.matches = set(m for m in self.matches if item not in m) + else: + self._clear() + except ValueError: + pass + + def switch_ref(self, with_dupe): + try: + self.ordered.remove(with_dupe) + self.ordered.insert(0, with_dupe) + self._percentage = None + self._matches_for_ref = None + except ValueError: + pass + + dupes = property(lambda self: self[1:]) + + @property + def percentage(self): + if self._percentage is None: + if self.dupes: + matches = self._get_matches_for_ref() + self._percentage = sum(match.percentage for match in matches) // len(matches) + else: + self._percentage = 0 + return self._percentage + + @property + def ref(self): + if self: + return self[0] + + +def get_groups(matches, j=job.nulljob): + matches.sort(key=lambda match: -match.percentage) + dupe2group = {} + groups = [] + for match in j.iter_with_progress(matches, 'Grouped %d/%d matches', JOB_REFRESH_RATE): + first, second, _ = match + first_group = dupe2group.get(first) + second_group = dupe2group.get(second) + if first_group: + if second_group: + if first_group is second_group: + target_group = first_group + else: + continue + else: + target_group = first_group + dupe2group[second] = target_group + else: + if second_group: + target_group = second_group + dupe2group[first] = target_group + else: + target_group = Group() + groups.append(target_group) + dupe2group[first] = target_group + dupe2group[second] = target_group + target_group.add_match(match) + for group in groups: + group.clean_matches() + return groups diff --git a/py/engine_test.py b/py/engine_test.py new file mode 100644 index 00000000..8e9706d9 --- /dev/null +++ b/py/engine_test.py @@ -0,0 +1,822 @@ +#!/usr/bin/env python +""" +Unit Name: dupeguru.engine_test +Created By: Virgil Dupras +Created On: 2006/01/29 +Last modified by:$Author: virgil $ +Last modified on:$Date: $ + $Revision: $ +Copyright 2004-2008 Hardcoded Software (http://www.hardcoded.net) +""" +import unittest +import sys + +from hsutil import job +from hsutil.decorators import log_calls +from hsutil.testcase import TestCase + +from . import engine +from .engine import * + +class NamedObject(object): + def __init__(self, name="foobar", with_words=False): + self.name = name + if with_words: + self.words = getwords(name) + + +def get_match_triangle(): + o1 = NamedObject(with_words=True) + o2 = NamedObject(with_words=True) + o3 = NamedObject(with_words=True) + m1 = get_match(o1,o2) + m2 = get_match(o1,o3) + m3 = get_match(o2,o3) + return [m1, m2, m3] + +def get_test_group(): + m1, m2, m3 = get_match_triangle() + result = Group() + result.add_match(m1) + result.add_match(m2) + result.add_match(m3) + return result + +class TCgetwords(TestCase): + def test_spaces(self): + self.assertEqual(['a', 'b', 'c', 'd'], getwords("a b c d")) + self.assertEqual(['a', 'b', 'c', 'd'], getwords(" a b c d ")) + + def test_splitter_chars(self): + self.assertEqual( + [chr(i) for i in xrange(ord('a'),ord('z')+1)], + getwords("a-b_c&d+e(f)g;h\\i[j]k{l}m:n.o,pr/s?t~u!v@w#x$y*z") + ) + + def test_joiner_chars(self): + self.assertEqual(["aec"], getwords(u"a'e\u0301c")) + + def test_empty(self): + self.assertEqual([], getwords('')) + + def test_returns_lowercase(self): + self.assertEqual(['foo', 'bar'], getwords('FOO BAR')) + + def test_decompose_unicode(self): + self.assertEqual(getwords(u'foo\xe9bar'), ['fooebar']) + + +class TCgetfields(TestCase): + def test_simple(self): + self.assertEqual([['a', 'b'], ['c', 'd', 'e']], getfields('a b - c d e')) + + def test_empty(self): + self.assertEqual([], getfields('')) + + def test_cleans_empty_fields(self): + expected = [['a', 'bc', 'def']] + actual = getfields(' - a bc def') + self.assertEqual(expected, actual) + expected = [['bc', 'def']] + + +class TCunpack_fields(TestCase): + def test_with_fields(self): + expected = ['a', 'b', 'c', 'd', 'e', 'f'] + actual = unpack_fields([['a'], ['b', 'c'], ['d', 'e', 'f']]) + self.assertEqual(expected, actual) + + def test_without_fields(self): + expected = ['a', 'b', 'c', 'd', 'e', 'f'] + actual = unpack_fields(['a', 'b', 'c', 'd', 'e', 'f']) + self.assertEqual(expected, actual) + + def test_empty(self): + self.assertEqual([], unpack_fields([])) + + +class TCWordCompare(TestCase): + def test_list(self): + self.assertEqual(100, compare(['a', 'b', 'c', 'd'],['a', 'b', 'c', 'd'])) + self.assertEqual(86, compare(['a', 'b', 'c', 'd'],['a', 'b', 'c'])) + + def test_unordered(self): + #Sometimes, users don't want fuzzy matching too much When they set the slider + #to 100, they don't expect a filename with the same words, but not the same order, to match. + #Thus, we want to return 99 in that case. + self.assertEqual(99, compare(['a', 'b', 'c', 'd'], ['d', 'b', 'c', 'a'])) + + def test_word_occurs_twice(self): + #if a word occurs twice in first, but once in second, we want the word to be only counted once + self.assertEqual(89, compare(['a', 'b', 'c', 'd', 'a'], ['d', 'b', 'c', 'a'])) + + def test_uses_copy_of_lists(self): + first = ['foo', 'bar'] + second = ['bar', 'bleh'] + compare(first, second) + self.assertEqual(['foo', 'bar'], first) + self.assertEqual(['bar', 'bleh'], second) + + def test_word_weight(self): + self.assertEqual(int((6.0 / 13.0) * 100), compare(['foo', 'bar'], ['bar', 'bleh'], (WEIGHT_WORDS, ))) + + def test_similar_words(self): + self.assertEqual(100, compare(['the', 'white', 'stripes'],['the', 'whites', 'stripe'], (MATCH_SIMILAR_WORDS, ))) + + def test_empty(self): + self.assertEqual(0, compare([], [])) + + def test_with_fields(self): + self.assertEqual(67, compare([['a', 'b'], ['c', 'd', 'e']], [['a', 'b'], ['c', 'd', 'f']])) + + def test_propagate_flags_with_fields(self): + def mock_compare(first, second, flags): + self.assertEqual((0, 1, 2, 3, 5), flags) + + self.mock(engine, 'compare_fields', mock_compare) + compare([['a']], [['a']], (0, 1, 2, 3, 5)) + + +class TCWordCompareWithFields(TestCase): + def test_simple(self): + self.assertEqual(67, compare_fields([['a', 'b'], ['c', 'd', 'e']], [['a', 'b'], ['c', 'd', 'f']])) + + def test_empty(self): + self.assertEqual(0, compare_fields([], [])) + + def test_different_length(self): + self.assertEqual(0, compare_fields([['a'], ['b']], [['a'], ['b'], ['c']])) + + def test_propagates_flags(self): + def mock_compare(first, second, flags): + self.assertEqual((0, 1, 2, 3, 5), flags) + + self.mock(engine, 'compare_fields', mock_compare) + compare_fields([['a']], [['a']],(0, 1, 2, 3, 5)) + + def test_order(self): + first = [['a', 'b'], ['c', 'd', 'e']] + second = [['c', 'd', 'f'], ['a', 'b']] + self.assertEqual(0, compare_fields(first, second)) + + def test_no_order(self): + first = [['a','b'],['c','d','e']] + second = [['c','d','f'],['a','b']] + self.assertEqual(67, compare_fields(first, second, (NO_FIELD_ORDER, ))) + first = [['a','b'],['a','b']] #a field can only be matched once. + second = [['c','d','f'],['a','b']] + self.assertEqual(0, compare_fields(first, second, (NO_FIELD_ORDER, ))) + first = [['a','b'],['a','b','c']] + second = [['c','d','f'],['a','b']] + self.assertEqual(33, compare_fields(first, second, (NO_FIELD_ORDER, ))) + + def test_compare_fields_without_order_doesnt_alter_fields(self): + #The NO_ORDER comp type altered the fields! + first = [['a','b'],['c','d','e']] + second = [['c','d','f'],['a','b']] + self.assertEqual(67, compare_fields(first, second, (NO_FIELD_ORDER, ))) + self.assertEqual([['a','b'],['c','d','e']],first) + self.assertEqual([['c','d','f'],['a','b']],second) + + +class TCbuild_word_dict(TestCase): + def test_with_standard_words(self): + l = [NamedObject('foo bar',True)] + l.append(NamedObject('bar baz',True)) + l.append(NamedObject('baz bleh foo',True)) + d = build_word_dict(l) + self.assertEqual(4,len(d)) + self.assertEqual(2,len(d['foo'])) + self.assert_(l[0] in d['foo']) + self.assert_(l[2] in d['foo']) + self.assertEqual(2,len(d['bar'])) + self.assert_(l[0] in d['bar']) + self.assert_(l[1] in d['bar']) + self.assertEqual(2,len(d['baz'])) + self.assert_(l[1] in d['baz']) + self.assert_(l[2] in d['baz']) + self.assertEqual(1,len(d['bleh'])) + self.assert_(l[2] in d['bleh']) + + def test_unpack_fields(self): + o = NamedObject('') + o.words = [['foo','bar'],['baz']] + d = build_word_dict([o]) + self.assertEqual(3,len(d)) + self.assertEqual(1,len(d['foo'])) + + def test_words_are_unaltered(self): + o = NamedObject('') + o.words = [['foo','bar'],['baz']] + d = build_word_dict([o]) + self.assertEqual([['foo','bar'],['baz']],o.words) + + def test_object_instances_can_only_be_once_in_words_object_list(self): + o = NamedObject('foo foo',True) + d = build_word_dict([o]) + self.assertEqual(1,len(d['foo'])) + + def test_job(self): + def do_progress(p,d=''): + self.log.append(p) + return True + + j = job.Job(1,do_progress) + self.log = [] + s = "foo bar" + build_word_dict([NamedObject(s, True), NamedObject(s, True), NamedObject(s, True)], j) + self.assertEqual(0,self.log[0]) + self.assertEqual(33,self.log[1]) + self.assertEqual(66,self.log[2]) + self.assertEqual(100,self.log[3]) + + +class TCmerge_similar_words(TestCase): + def test_some_similar_words(self): + d = { + 'foobar':set([1]), + 'foobar1':set([2]), + 'foobar2':set([3]), + } + merge_similar_words(d) + self.assertEqual(1,len(d)) + self.assertEqual(3,len(d['foobar'])) + + + +class TCreduce_common_words(TestCase): + def test_typical(self): + d = { + 'foo': set([NamedObject('foo bar',True) for i in range(50)]), + 'bar': set([NamedObject('foo bar',True) for i in range(49)]) + } + reduce_common_words(d, 50) + self.assert_('foo' not in d) + self.assertEqual(49,len(d['bar'])) + + def test_dont_remove_objects_with_only_common_words(self): + d = { + 'common': set([NamedObject("common uncommon",True) for i in range(50)] + [NamedObject("common",True)]), + 'uncommon': set([NamedObject("common uncommon",True)]) + } + reduce_common_words(d, 50) + self.assertEqual(1,len(d['common'])) + self.assertEqual(1,len(d['uncommon'])) + + def test_values_still_are_set_instances(self): + d = { + 'common': set([NamedObject("common uncommon",True) for i in range(50)] + [NamedObject("common",True)]), + 'uncommon': set([NamedObject("common uncommon",True)]) + } + reduce_common_words(d, 50) + self.assert_(isinstance(d['common'],set)) + self.assert_(isinstance(d['uncommon'],set)) + + def test_dont_raise_KeyError_when_a_word_has_been_removed(self): + #If a word has been removed by the reduce, an object in a subsequent common word that + #contains the word that has been removed would cause a KeyError. + d = { + 'foo': set([NamedObject('foo bar baz',True) for i in range(50)]), + 'bar': set([NamedObject('foo bar baz',True) for i in range(50)]), + 'baz': set([NamedObject('foo bar baz',True) for i in range(49)]) + } + try: + reduce_common_words(d, 50) + except KeyError: + self.fail() + + def test_unpack_fields(self): + #object.words may be fields. + def create_it(): + o = NamedObject('') + o.words = [['foo','bar'],['baz']] + return o + + d = { + 'foo': set([create_it() for i in range(50)]) + } + try: + reduce_common_words(d, 50) + except TypeError: + self.fail("must support fields.") + + def test_consider_a_reduced_common_word_common_even_after_reduction(self): + #There was a bug in the code that causeda word that has already been reduced not to + #be counted as a common word for subsequent words. For example, if 'foo' is processed + #as a common word, keeping a "foo bar" file in it, and the 'bar' is processed, "foo bar" + #would not stay in 'bar' because 'foo' is not a common word anymore. + only_common = NamedObject('foo bar',True) + d = { + 'foo': set([NamedObject('foo bar baz',True) for i in range(49)] + [only_common]), + 'bar': set([NamedObject('foo bar baz',True) for i in range(49)] + [only_common]), + 'baz': set([NamedObject('foo bar baz',True) for i in range(49)]) + } + reduce_common_words(d, 50) + self.assertEqual(1,len(d['foo'])) + self.assertEqual(1,len(d['bar'])) + self.assertEqual(49,len(d['baz'])) + + +class TCget_match(TestCase): + def test_simple(self): + o1 = NamedObject("foo bar",True) + o2 = NamedObject("bar bleh",True) + m = get_match(o1,o2) + self.assertEqual(50,m.percentage) + self.assertEqual(['foo','bar'],m.first.words) + self.assertEqual(['bar','bleh'],m.second.words) + self.assert_(m.first is o1) + self.assert_(m.second is o2) + + def test_in(self): + o1 = NamedObject("foo",True) + o2 = NamedObject("bar",True) + m = get_match(o1,o2) + self.assert_(o1 in m) + self.assert_(o2 in m) + self.assert_(object() not in m) + + def test_word_weight(self): + self.assertEqual(int((6.0 / 13.0) * 100),get_match(NamedObject("foo bar",True),NamedObject("bar bleh",True),(WEIGHT_WORDS,)).percentage) + + +class TCMatchFactory(TestCase): + def test_empty(self): + self.assertEqual([],MatchFactory().getmatches([])) + + def test_defaults(self): + mf = MatchFactory() + self.assertEqual(50,mf.common_word_threshold) + self.assertEqual(False,mf.weight_words) + self.assertEqual(False,mf.match_similar_words) + self.assertEqual(False,mf.no_field_order) + self.assertEqual(0,mf.min_match_percentage) + + def test_simple(self): + l = [NamedObject("foo bar"),NamedObject("bar bleh"),NamedObject("a b c foo")] + r = MatchFactory().getmatches(l) + self.assertEqual(2,len(r)) + seek = [m for m in r if m.percentage == 50] #"foo bar" and "bar bleh" + m = seek[0] + self.assertEqual(['foo','bar'],m.first.words) + self.assertEqual(['bar','bleh'],m.second.words) + seek = [m for m in r if m.percentage == 33] #"foo bar" and "a b c foo" + m = seek[0] + self.assertEqual(['foo','bar'],m.first.words) + self.assertEqual(['a','b','c','foo'],m.second.words) + + def test_null_and_unrelated_objects(self): + l = [NamedObject("foo bar"),NamedObject("bar bleh"),NamedObject(""),NamedObject("unrelated object")] + r = MatchFactory().getmatches(l) + self.assertEqual(1,len(r)) + m = r[0] + self.assertEqual(50,m.percentage) + self.assertEqual(['foo','bar'],m.first.words) + self.assertEqual(['bar','bleh'],m.second.words) + + def test_twice_the_same_word(self): + l = [NamedObject("foo foo bar"),NamedObject("bar bleh")] + r = MatchFactory().getmatches(l) + self.assertEqual(1,len(r)) + + def test_twice_the_same_word_when_preworded(self): + l = [NamedObject("foo foo bar",True),NamedObject("bar bleh",True)] + r = MatchFactory().getmatches(l) + self.assertEqual(1,len(r)) + + def test_two_words_match(self): + l = [NamedObject("foo bar"),NamedObject("foo bar bleh")] + r = MatchFactory().getmatches(l) + self.assertEqual(1,len(r)) + + def test_match_files_with_only_common_words(self): + #If a word occurs more than 50 times, it is excluded from the matching process + #The problem with the common_word_threshold is that the files containing only common + #words will never be matched together. We *should* match them. + mf = MatchFactory() + mf.common_word_threshold = 50 + l = [NamedObject("foo") for i in range(50)] + r = mf.getmatches(l) + self.assertEqual(1225,len(r)) + + def test_use_words_already_there_if_there(self): + o1 = NamedObject('foo') + o2 = NamedObject('bar') + o2.words = ['foo'] + self.assertEqual(1,len(MatchFactory().getmatches([o1,o2]))) + + def test_job(self): + def do_progress(p,d=''): + self.log.append(p) + return True + + j = job.Job(1,do_progress) + self.log = [] + s = "foo bar" + MatchFactory().getmatches([NamedObject(s),NamedObject(s),NamedObject(s)],j) + self.assert_(len(self.log) > 2) + self.assertEqual(0,self.log[0]) + self.assertEqual(100,self.log[-1]) + + def test_weight_words(self): + mf = MatchFactory() + mf.weight_words = True + l = [NamedObject("foo bar"),NamedObject("bar bleh")] + m = mf.getmatches(l)[0] + self.assertEqual(int((6.0 / 13.0) * 100),m.percentage) + + def test_similar_word(self): + mf = MatchFactory() + mf.match_similar_words = True + l = [NamedObject("foobar"),NamedObject("foobars")] + self.assertEqual(1,len(mf.getmatches(l))) + self.assertEqual(100,mf.getmatches(l)[0].percentage) + l = [NamedObject("foobar"),NamedObject("foo")] + self.assertEqual(0,len(mf.getmatches(l))) #too far + l = [NamedObject("bizkit"),NamedObject("bizket")] + self.assertEqual(1,len(mf.getmatches(l))) + l = [NamedObject("foobar"),NamedObject("foosbar")] + self.assertEqual(1,len(mf.getmatches(l))) + + def test_single_object_with_similar_words(self): + mf = MatchFactory() + mf.match_similar_words = True + l = [NamedObject("foo foos")] + self.assertEqual(0,len(mf.getmatches(l))) + + def test_double_words_get_counted_only_once(self): + mf = MatchFactory() + l = [NamedObject("foo bar foo bleh"),NamedObject("foo bar bleh bar")] + m = mf.getmatches(l)[0] + self.assertEqual(75,m.percentage) + + def test_with_fields(self): + mf = MatchFactory() + o1 = NamedObject("foo bar - foo bleh") + o2 = NamedObject("foo bar - bleh bar") + o1.words = getfields(o1.name) + o2.words = getfields(o2.name) + m = mf.getmatches([o1, o2])[0] + self.assertEqual(50, m.percentage) + + def test_with_fields_no_order(self): + mf = MatchFactory() + mf.no_field_order = True + o1 = NamedObject("foo bar - foo bleh") + o2 = NamedObject("bleh bang - foo bar") + o1.words = getfields(o1.name) + o2.words = getfields(o2.name) + m = mf.getmatches([o1, o2])[0] + self.assertEqual(50 ,m.percentage) + + def test_only_match_similar_when_the_option_is_set(self): + mf = MatchFactory() + mf.match_similar_words = False + l = [NamedObject("foobar"),NamedObject("foobars")] + self.assertEqual(0,len(mf.getmatches(l))) + + def test_dont_recurse_do_match(self): + # with nosetests, the stack is increased. The number has to be high enough not to be failing falsely + sys.setrecursionlimit(100) + mf = MatchFactory() + files = [NamedObject('foo bar') for i in range(101)] + try: + mf.getmatches(files) + except RuntimeError: + self.fail() + finally: + sys.setrecursionlimit(1000) + + def test_min_match_percentage(self): + l = [NamedObject("foo bar"),NamedObject("bar bleh"),NamedObject("a b c foo")] + mf = MatchFactory() + mf.min_match_percentage = 50 + r = mf.getmatches(l) + self.assertEqual(1,len(r)) #Only "foo bar" / "bar bleh" should match + + def test_limit(self): + l = [NamedObject(),NamedObject(),NamedObject()] + mf = MatchFactory() + mf.limit = 2 + r = mf.getmatches(l) + self.assertEqual(2,len(r)) + + def test_MemoryError(self): + @log_calls + def mocked_match(first, second, flags): + if len(mocked_match.calls) > 42: + raise MemoryError() + return Match(first, second, 0) + + objects = [NamedObject() for i in range(10)] # results in 45 matches + self.mock(engine, 'get_match', mocked_match) + mf = MatchFactory() + try: + r = mf.getmatches(objects) + except MemoryError: + self.fail('MemorryError must be handled') + self.assertEqual(42, len(r)) + + +class TCGroup(TestCase): + def test_empy(self): + g = Group() + self.assertEqual(None,g.ref) + self.assertEqual([],g.dupes) + self.assertEqual(0,len(g.matches)) + + def test_add_match(self): + g = Group() + m = get_match(NamedObject("foo",True),NamedObject("bar",True)) + g.add_match(m) + self.assert_(g.ref is m.first) + self.assertEqual([m.second],g.dupes) + self.assertEqual(1,len(g.matches)) + self.assert_(m in g.matches) + + def test_multiple_add_match(self): + g = Group() + o1 = NamedObject("a",True) + o2 = NamedObject("b",True) + o3 = NamedObject("c",True) + o4 = NamedObject("d",True) + g.add_match(get_match(o1,o2)) + self.assert_(g.ref is o1) + self.assertEqual([o2],g.dupes) + self.assertEqual(1,len(g.matches)) + g.add_match(get_match(o1,o3)) + self.assertEqual([o2],g.dupes) + self.assertEqual(2,len(g.matches)) + g.add_match(get_match(o2,o3)) + self.assertEqual([o2,o3],g.dupes) + self.assertEqual(3,len(g.matches)) + g.add_match(get_match(o1,o4)) + self.assertEqual([o2,o3],g.dupes) + self.assertEqual(4,len(g.matches)) + g.add_match(get_match(o2,o4)) + self.assertEqual([o2,o3],g.dupes) + self.assertEqual(5,len(g.matches)) + g.add_match(get_match(o3,o4)) + self.assertEqual([o2,o3,o4],g.dupes) + self.assertEqual(6,len(g.matches)) + + def test_len(self): + g = Group() + self.assertEqual(0,len(g)) + g.add_match(get_match(NamedObject("foo",True),NamedObject("bar",True))) + self.assertEqual(2,len(g)) + + def test_add_same_match_twice(self): + g = Group() + m = get_match(NamedObject("foo",True),NamedObject("foo",True)) + g.add_match(m) + self.assertEqual(2,len(g)) + self.assertEqual(1,len(g.matches)) + g.add_match(m) + self.assertEqual(2,len(g)) + self.assertEqual(1,len(g.matches)) + + def test_in(self): + g = Group() + o1 = NamedObject("foo",True) + o2 = NamedObject("bar",True) + self.assert_(o1 not in g) + g.add_match(get_match(o1,o2)) + self.assert_(o1 in g) + self.assert_(o2 in g) + + def test_remove(self): + g = Group() + o1 = NamedObject("foo",True) + o2 = NamedObject("bar",True) + o3 = NamedObject("bleh",True) + g.add_match(get_match(o1,o2)) + g.add_match(get_match(o1,o3)) + g.add_match(get_match(o2,o3)) + self.assertEqual(3,len(g.matches)) + self.assertEqual(3,len(g)) + g.remove_dupe(o3) + self.assertEqual(1,len(g.matches)) + self.assertEqual(2,len(g)) + g.remove_dupe(o1) + self.assertEqual(0,len(g.matches)) + self.assertEqual(0,len(g)) + + def test_remove_with_ref_dupes(self): + g = Group() + o1 = NamedObject("foo",True) + o2 = NamedObject("bar",True) + o3 = NamedObject("bleh",True) + g.add_match(get_match(o1,o2)) + g.add_match(get_match(o1,o3)) + g.add_match(get_match(o2,o3)) + o1.is_ref = True + o2.is_ref = True + g.remove_dupe(o3) + self.assertEqual(0,len(g)) + + def test_switch_ref(self): + o1 = NamedObject(with_words=True) + o2 = NamedObject(with_words=True) + g = Group() + g.add_match(get_match(o1,o2)) + self.assert_(o1 is g.ref) + g.switch_ref(o2) + self.assert_(o2 is g.ref) + self.assertEqual([o1],g.dupes) + g.switch_ref(o2) + self.assert_(o2 is g.ref) + g.switch_ref(NamedObject('',True)) + self.assert_(o2 is g.ref) + + def test_get_match_of(self): + g = Group() + for m in get_match_triangle(): + g.add_match(m) + o = g.dupes[0] + m = g.get_match_of(o) + self.assert_(g.ref in m) + self.assert_(o in m) + self.assert_(g.get_match_of(NamedObject('',True)) is None) + self.assert_(g.get_match_of(g.ref) is None) + + def test_percentage(self): + #percentage should return the avg percentage in relation to the ref + m1,m2,m3 = get_match_triangle() + m1 = Match(m1[0], m1[1], 100) + m2 = Match(m2[0], m2[1], 50) + m3 = Match(m3[0], m3[1], 33) + g = Group() + g.add_match(m1) + g.add_match(m2) + g.add_match(m3) + self.assertEqual(75,g.percentage) + g.switch_ref(g.dupes[0]) + self.assertEqual(66,g.percentage) + g.remove_dupe(g.dupes[0]) + self.assertEqual(33,g.percentage) + g.add_match(m1) + g.add_match(m2) + self.assertEqual(66,g.percentage) + + def test_percentage_on_empty_group(self): + g = Group() + self.assertEqual(0,g.percentage) + + def test_prioritize(self): + m1,m2,m3 = get_match_triangle() + o1 = m1.first + o2 = m1.second + o3 = m2.second + o1.name = 'c' + o2.name = 'b' + o3.name = 'a' + g = Group() + g.add_match(m1) + g.add_match(m2) + g.add_match(m3) + self.assert_(o1 is g.ref) + g.prioritize(lambda x:x.name) + self.assert_(o3 is g.ref) + + def test_prioritize_with_tie_breaker(self): + # if the ref has the same key as one or more of the dupe, run the tie_breaker func among them + g = get_test_group() + o1, o2, o3 = g.ordered + tie_breaker = lambda ref, dupe: dupe is o3 + g.prioritize(lambda x:0, tie_breaker) + self.assertTrue(g.ref is o3) + + def test_prioritize_with_tie_breaker_runs_on_all_dupes(self): + # Even if a dupe is chosen to switch with ref with a tie breaker, we still run the tie breaker + # with other dupes and the newly chosen ref + g = get_test_group() + o1, o2, o3 = g.ordered + o1.foo = 1 + o2.foo = 2 + o3.foo = 3 + tie_breaker = lambda ref, dupe: dupe.foo > ref.foo + g.prioritize(lambda x:0, tie_breaker) + self.assertTrue(g.ref is o3) + + def test_prioritize_with_tie_breaker_runs_only_on_tie_dupes(self): + # The tie breaker only runs on dupes that had the same value for the key_func + g = get_test_group() + o1, o2, o3 = g.ordered + o1.foo = 2 + o2.foo = 2 + o3.foo = 1 + o1.bar = 1 + o2.bar = 2 + o3.bar = 3 + key_func = lambda x: -x.foo + tie_breaker = lambda ref, dupe: dupe.bar > ref.bar + g.prioritize(key_func, tie_breaker) + self.assertTrue(g.ref is o2) + + def test_list_like(self): + g = Group() + o1,o2 = (NamedObject("foo",True),NamedObject("bar",True)) + g.add_match(get_match(o1,o2)) + self.assert_(g[0] is o1) + self.assert_(g[1] is o2) + + def test_clean_matches(self): + g = Group() + o1,o2,o3 = (NamedObject("foo",True),NamedObject("bar",True),NamedObject("baz",True)) + g.add_match(get_match(o1,o2)) + g.add_match(get_match(o1,o3)) + g.clean_matches() + self.assertEqual(1,len(g.matches)) + self.assertEqual(0,len(g.candidates)) + + +class TCget_groups(TestCase): + def test_empty(self): + r = get_groups([]) + self.assertEqual([],r) + + def test_simple(self): + l = [NamedObject("foo bar"),NamedObject("bar bleh")] + matches = MatchFactory().getmatches(l) + m = matches[0] + r = get_groups(matches) + self.assertEqual(1,len(r)) + g = r[0] + self.assert_(g.ref is m.first) + self.assertEqual([m.second],g.dupes) + + def test_group_with_multiple_matches(self): + #This results in 3 matches + l = [NamedObject("foo"),NamedObject("foo"),NamedObject("foo")] + matches = MatchFactory().getmatches(l) + r = get_groups(matches) + self.assertEqual(1,len(r)) + g = r[0] + self.assertEqual(3,len(g)) + + def test_must_choose_a_group(self): + l = [NamedObject("a b"),NamedObject("a b"),NamedObject("b c"),NamedObject("c d"),NamedObject("c d")] + #There will be 2 groups here: group "a b" and group "c d" + #"b c" can go either of them, but not both. + matches = MatchFactory().getmatches(l) + r = get_groups(matches) + self.assertEqual(2,len(r)) + self.assertEqual(5,len(r[0])+len(r[1])) + + def test_should_all_go_in_the_same_group(self): + l = [NamedObject("a b"),NamedObject("a b"),NamedObject("a b"),NamedObject("a b")] + #There will be 2 groups here: group "a b" and group "c d" + #"b c" can fit in both, but it must be in only one of them + matches = MatchFactory().getmatches(l) + r = get_groups(matches) + self.assertEqual(1,len(r)) + + def test_give_priority_to_matches_with_higher_percentage(self): + o1 = NamedObject(with_words=True) + o2 = NamedObject(with_words=True) + o3 = NamedObject(with_words=True) + m1 = Match(o1, o2, 1) + m2 = Match(o2, o3, 2) + r = get_groups([m1,m2]) + self.assertEqual(1,len(r)) + g = r[0] + self.assertEqual(2,len(g)) + self.assert_(o1 not in g) + self.assert_(o2 in g) + self.assert_(o3 in g) + + def test_four_sized_group(self): + l = [NamedObject("foobar") for i in xrange(4)] + m = MatchFactory().getmatches(l) + r = get_groups(m) + self.assertEqual(1,len(r)) + self.assertEqual(4,len(r[0])) + + def test_referenced_by_ref2(self): + o1 = NamedObject(with_words=True) + o2 = NamedObject(with_words=True) + o3 = NamedObject(with_words=True) + m1 = get_match(o1,o2) + m2 = get_match(o3,o1) + m3 = get_match(o3,o2) + r = get_groups([m1,m2,m3]) + self.assertEqual(3,len(r[0])) + + def test_job(self): + def do_progress(p,d=''): + self.log.append(p) + return True + + self.log = [] + j = job.Job(1,do_progress) + m1,m2,m3 = get_match_triangle() + #101%: To make sure it is processed first so the job test works correctly + m4 = Match(NamedObject('a',True), NamedObject('a',True), 101) + get_groups([m1,m2,m3,m4],j) + self.assertEqual(0,self.log[0]) + self.assertEqual(100,self.log[-1]) + + +if __name__ == "__main__": + unittest.main() diff --git a/py/export.py b/py/export.py new file mode 100644 index 00000000..c6293a5d --- /dev/null +++ b/py/export.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python +""" +Unit Name: dupeguru.export +Created By: Virgil Dupras +Created On: 2006/09/16 +Last modified by:$Author: virgil $ +Last modified on:$Date: 2009-05-28 15:22:39 +0200 (Thu, 28 May 2009) $ + $Revision: 4385 $ +Copyright 2004-2006 Hardcoded Software (http://www.hardcoded.net) +""" +from xml.dom import minidom +import tempfile +import os.path as op +import os +from StringIO import StringIO + +from hsutil.files import FileOrPath + +def output_column_xml(outfile, columns): + """Creates a xml file outfile with the supplied columns. + + outfile can be a filename or a file object. + columns is a list of 2 sized tuples (display,enabled) + """ + doc = minidom.Document() + root = doc.appendChild(doc.createElement('columns')) + for display,enabled in columns: + col_node = root.appendChild(doc.createElement('column')) + col_node.setAttribute('display', display) + col_node.setAttribute('enabled', {True:'y',False:'n'}[enabled]) + with FileOrPath(outfile, 'wb') as fp: + doc.writexml(fp, '\t','\t','\n', encoding='utf-8') + +def merge_css_into_xhtml(xhtml, css): + with FileOrPath(xhtml, 'r+') as xhtml: + with FileOrPath(css) as css: + try: + doc = minidom.parse(xhtml) + except Exception: + return False + head = doc.getElementsByTagName('head')[0] + links = head.getElementsByTagName('link') + for link in links: + if link.getAttribute('rel') == 'stylesheet': + head.removeChild(link) + style = head.appendChild(doc.createElement('style')) + style.setAttribute('type','text/css') + style.appendChild(doc.createTextNode(css.read())) + xhtml.truncate(0) + doc.writexml(xhtml, '\t','\t','\n', encoding='utf-8') + xhtml.seek(0) + return True + +def export_to_xhtml(xml, xslt, css, columns, cmd='xsltproc --path "%(folder)s" "%(xslt)s" "%(xml)s"'): + folder = op.split(xml)[0] + output_column_xml(op.join(folder,'columns.xml'),columns) + html = StringIO() + cmd = cmd % {'folder': folder, 'xslt': xslt, 'xml': xml} + html.write(os.popen(cmd).read()) + html.seek(0) + merge_css_into_xhtml(html,css) + html.seek(0) + html_path = op.join(folder,'export.htm') + html_file = open(html_path,'w') + html_file.write(html.read().encode('utf-8')) + html_file.close() + return html_path diff --git a/py/export_test.py b/py/export_test.py new file mode 100644 index 00000000..5c4a6d87 --- /dev/null +++ b/py/export_test.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python +""" +Unit Name: dupeguru.tests.export +Created By: Virgil Dupras +Created On: 2006/09/16 +Last modified by:$Author: virgil $ +Last modified on:$Date: 2009-05-28 15:22:39 +0200 (Thu, 28 May 2009) $ + $Revision: 4385 $ +Copyright 2004-2006 Hardcoded Software (http://www.hardcoded.net) +""" +import unittest +from xml.dom import minidom +from StringIO import StringIO + +from hsutil.testcase import TestCase + +from .export import * +from . import export + +class TCoutput_columns_xml(TestCase): + def test_empty_columns(self): + f = StringIO() + output_column_xml(f,[]) + f.seek(0) + doc = minidom.parse(f) + root = doc.documentElement + self.assertEqual('columns',root.nodeName) + self.assertEqual(0,len(root.childNodes)) + + def test_some_columns(self): + f = StringIO() + output_column_xml(f,[('foo',True),('bar',False),('baz',True)]) + f.seek(0) + doc = minidom.parse(f) + columns = doc.getElementsByTagName('column') + self.assertEqual(3,len(columns)) + c1,c2,c3 = columns + self.assertEqual('foo',c1.getAttribute('display')) + self.assertEqual('bar',c2.getAttribute('display')) + self.assertEqual('baz',c3.getAttribute('display')) + self.assertEqual('y',c1.getAttribute('enabled')) + self.assertEqual('n',c2.getAttribute('enabled')) + self.assertEqual('y',c3.getAttribute('enabled')) + + +class TCmerge_css_into_xhtml(TestCase): + def test_main(self): + css = StringIO() + css.write('foobar') + css.seek(0) + xhtml = StringIO() + xhtml.write(""" + + + + + dupeGuru - Duplicate file scanner + + + + + + """) + xhtml.seek(0) + self.assert_(merge_css_into_xhtml(xhtml,css)) + xhtml.seek(0) + doc = minidom.parse(xhtml) + head = doc.getElementsByTagName('head')[0] + #A style node should have been added in head. + styles = head.getElementsByTagName('style') + self.assertEqual(1,len(styles)) + style = styles[0] + self.assertEqual('text/css',style.getAttribute('type')) + self.assertEqual('foobar',style.firstChild.nodeValue.strip()) + #all should be removed + self.assertEqual(1,len(head.getElementsByTagName('link'))) + + def test_empty(self): + self.assert_(not merge_css_into_xhtml(StringIO(),StringIO())) + + def test_malformed(self): + xhtml = StringIO() + xhtml.write(""" + + """) + xhtml.seek(0) + self.assert_(not merge_css_into_xhtml(xhtml,StringIO())) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/py/gen.py b/py/gen.py new file mode 100644 index 00000000..0a842372 --- /dev/null +++ b/py/gen.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python +# Unit Name: gen +# Created By: Virgil Dupras +# Created On: 2009-05-26 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +import os +import os.path as op + +def move(src, dst): + if not op.exists(src): + return + if op.exists(dst): + os.remove(dst) + print 'Moving %s --> %s' % (src, dst) + os.rename(src, dst) + + +os.chdir(op.join('modules', 'block')) +os.system('python setup.py build_ext --inplace') +os.chdir(op.join('..', 'cache')) +os.system('python setup.py build_ext --inplace') +os.chdir(op.join('..', '..')) +move(op.join('modules', 'block', '_block.so'), op.join('picture', '_block.so')) +move(op.join('modules', 'block', '_block.pyd'), op.join('picture', '_block.pyd')) +move(op.join('modules', 'cache', '_cache.so'), op.join('picture', '_cache.so')) +move(op.join('modules', 'cache', '_cache.pyd'), op.join('picture', '_cache.pyd')) diff --git a/py/ignore.py b/py/ignore.py new file mode 100644 index 00000000..97060786 --- /dev/null +++ b/py/ignore.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python +""" +Unit Name: ignore +Created By: Virgil Dupras +Created On: 2006/05/02 +Last modified by:$Author: virgil $ +Last modified on:$Date: 2009-05-28 15:22:39 +0200 (Thu, 28 May 2009) $ + $Revision: 4385 $ +Copyright 2004-2006 Hardcoded Software (http://www.hardcoded.net) +""" +from hsutil.files import FileOrPath + +import xml.dom.minidom + +class IgnoreList(object): + """An ignore list implementation that is iterable, filterable and exportable to XML. + + Call Ignore to add an ignore list entry, and AreIgnore to check if 2 items are in the list. + When iterated, 2 sized tuples will be returned, the tuples containing 2 items ignored together. + """ + #---Override + def __init__(self): + self._ignored = {} + self._count = 0 + + def __iter__(self): + for first,seconds in self._ignored.iteritems(): + for second in seconds: + yield (first,second) + + def __len__(self): + return self._count + + #---Public + def AreIgnored(self,first,second): + def do_check(first,second): + try: + matches = self._ignored[first] + return second in matches + except KeyError: + return False + + return do_check(first,second) or do_check(second,first) + + def Clear(self): + self._ignored = {} + self._count = 0 + + def Filter(self,func): + """Applies a filter on all ignored items, and remove all matches where func(first,second) + doesn't return True. + """ + filtered = IgnoreList() + for first,second in self: + if func(first,second): + filtered.Ignore(first,second) + self._ignored = filtered._ignored + self._count = filtered._count + + def Ignore(self,first,second): + if self.AreIgnored(first,second): + return + try: + matches = self._ignored[first] + matches.add(second) + except KeyError: + try: + matches = self._ignored[second] + matches.add(first) + except KeyError: + matches = set() + matches.add(second) + self._ignored[first] = matches + self._count += 1 + + def load_from_xml(self,infile): + """Loads the ignore list from a XML created with save_to_xml. + + infile can be a file object or a filename. + """ + try: + doc = xml.dom.minidom.parse(infile) + except Exception: + return + file_nodes = doc.getElementsByTagName('file') + for fn in file_nodes: + if not fn.getAttributeNode('path'): + continue + file_path = fn.getAttributeNode('path').nodeValue + subfile_nodes = fn.getElementsByTagName('file') + for sfn in subfile_nodes: + if not sfn.getAttributeNode('path'): + continue + subfile_path = sfn.getAttributeNode('path').nodeValue + self.Ignore(file_path,subfile_path) + + def save_to_xml(self,outfile): + """Create a XML file that can be used by load_from_xml. + + outfile can be a file object or a filename. + """ + doc = xml.dom.minidom.Document() + root = doc.appendChild(doc.createElement('ignore_list')) + for file,subfiles in self._ignored.items(): + file_node = root.appendChild(doc.createElement('file')) + if isinstance(file,unicode): + file = file.encode('utf-8') + file_node.setAttribute('path',file) + for subfile in subfiles: + subfile_node = file_node.appendChild(doc.createElement('file')) + if isinstance(subfile,unicode): + subfile = subfile.encode('utf-8') + subfile_node.setAttribute('path',subfile) + with FileOrPath(outfile, 'wb') as fp: + doc.writexml(fp,'\t','\t','\n',encoding='utf-8') + + diff --git a/py/ignore_test.py b/py/ignore_test.py new file mode 100644 index 00000000..8ff91f52 --- /dev/null +++ b/py/ignore_test.py @@ -0,0 +1,158 @@ +#!/usr/bin/env python +""" +Unit Name: ignore +Created By: Virgil Dupras +Created On: 2006/05/02 +Last modified by:$Author: virgil $ +Last modified on:$Date: 2009-05-28 15:22:39 +0200 (Thu, 28 May 2009) $ + $Revision: 4385 $ +Copyright 2004-2006 Hardcoded Software (http://www.hardcoded.net) +""" +import unittest +import cStringIO +import xml.dom.minidom + +from .ignore import * + +class TCIgnoreList(unittest.TestCase): + def test_empty(self): + il = IgnoreList() + self.assertEqual(0,len(il)) + self.assert_(not il.AreIgnored('foo','bar')) + + def test_simple(self): + il = IgnoreList() + il.Ignore('foo','bar') + self.assert_(il.AreIgnored('foo','bar')) + self.assert_(il.AreIgnored('bar','foo')) + self.assert_(not il.AreIgnored('foo','bleh')) + self.assert_(not il.AreIgnored('bleh','bar')) + self.assertEqual(1,len(il)) + + def test_multiple(self): + il = IgnoreList() + il.Ignore('foo','bar') + il.Ignore('foo','bleh') + il.Ignore('bleh','bar') + il.Ignore('aybabtu','bleh') + self.assert_(il.AreIgnored('foo','bar')) + self.assert_(il.AreIgnored('bar','foo')) + self.assert_(il.AreIgnored('foo','bleh')) + self.assert_(il.AreIgnored('bleh','bar')) + self.assert_(not il.AreIgnored('aybabtu','bar')) + self.assertEqual(4,len(il)) + + def test_clear(self): + il = IgnoreList() + il.Ignore('foo','bar') + il.Clear() + self.assert_(not il.AreIgnored('foo','bar')) + self.assert_(not il.AreIgnored('bar','foo')) + self.assertEqual(0,len(il)) + + def test_add_same_twice(self): + il = IgnoreList() + il.Ignore('foo','bar') + il.Ignore('bar','foo') + self.assertEqual(1,len(il)) + + def test_save_to_xml(self): + il = IgnoreList() + il.Ignore('foo','bar') + il.Ignore('foo','bleh') + il.Ignore('bleh','bar') + f = cStringIO.StringIO() + il.save_to_xml(f) + f.seek(0) + doc = xml.dom.minidom.parse(f) + root = doc.documentElement + self.assertEqual('ignore_list',root.nodeName) + children = [c for c in root.childNodes if c.localName] + self.assertEqual(2,len(children)) + self.assertEqual(2,len([c for c in children if c.nodeName == 'file'])) + f1,f2 = children + subchildren = [c for c in f1.childNodes if c.localName == 'file'] +\ + [c for c in f2.childNodes if c.localName == 'file'] + self.assertEqual(3,len(subchildren)) + + def test_SaveThenLoad(self): + il = IgnoreList() + il.Ignore('foo','bar') + il.Ignore('foo','bleh') + il.Ignore('bleh','bar') + il.Ignore(u'\u00e9','bar') + f = cStringIO.StringIO() + il.save_to_xml(f) + f.seek(0) + il = IgnoreList() + il.load_from_xml(f) + self.assertEqual(4,len(il)) + self.assert_(il.AreIgnored(u'\u00e9','bar')) + + def test_LoadXML_with_empty_file_tags(self): + f = cStringIO.StringIO() + f.write('') + f.seek(0) + il = IgnoreList() + il.load_from_xml(f) + self.assertEqual(0,len(il)) + + def test_AreIgnore_works_when_a_child_is_a_key_somewhere_else(self): + il = IgnoreList() + il.Ignore('foo','bar') + il.Ignore('bar','baz') + self.assert_(il.AreIgnored('bar','foo')) + + + def test_no_dupes_when_a_child_is_a_key_somewhere_else(self): + il = IgnoreList() + il.Ignore('foo','bar') + il.Ignore('bar','baz') + il.Ignore('bar','foo') + self.assertEqual(2,len(il)) + + def test_iterate(self): + #It must be possible to iterate through ignore list + il = IgnoreList() + expected = [('foo','bar'),('bar','baz'),('foo','baz')] + for i in expected: + il.Ignore(i[0],i[1]) + for i in il: + expected.remove(i) #No exception should be raised + self.assert_(not expected) #expected should be empty + + def test_filter(self): + il = IgnoreList() + il.Ignore('foo','bar') + il.Ignore('bar','baz') + il.Ignore('foo','baz') + il.Filter(lambda f,s: f == 'bar') + self.assertEqual(1,len(il)) + self.assert_(not il.AreIgnored('foo','bar')) + self.assert_(il.AreIgnored('bar','baz')) + + def test_save_with_non_ascii_non_unicode_items(self): + il = IgnoreList() + il.Ignore('\xac','\xbf') + f = cStringIO.StringIO() + try: + il.save_to_xml(f) + except Exception,e: + self.fail(str(e)) + + def test_len(self): + il = IgnoreList() + self.assertEqual(0,len(il)) + il.Ignore('foo','bar') + self.assertEqual(1,len(il)) + + def test_nonzero(self): + il = IgnoreList() + self.assert_(not il) + il.Ignore('foo','bar') + self.assert_(il) + + +if __name__ == "__main__": + unittest.main() + diff --git a/py/modules/block/block.pyx b/py/modules/block/block.pyx new file mode 100644 index 00000000..db4c7500 --- /dev/null +++ b/py/modules/block/block.pyx @@ -0,0 +1,93 @@ +# Created By: Virgil Dupras +# Created On: 2009-04-23 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +cdef extern from "stdlib.h": + int abs(int n) # required so that abs() is applied on ints, not python objects + +class NoBlocksError(Exception): + """avgdiff/maxdiff has been called with empty lists""" + +class DifferentBlockCountError(Exception): + """avgdiff/maxdiff has been called with 2 block lists of different size.""" + + +cdef object getblock(object image): + """Returns a 3 sized tuple containing the mean color of 'image'. + + image: a PIL image or crop. + """ + cdef int pixel_count, red, green, blue, r, g, b + if image.size[0]: + pixel_count = image.size[0] * image.size[1] + red = green = blue = 0 + for r, g, b in image.getdata(): + red += r + green += g + blue += b + return (red // pixel_count, green // pixel_count, blue // pixel_count) + else: + return (0, 0, 0) + +def getblocks2(image, int block_count_per_side): + """Returns a list of blocks (3 sized tuples). + + image: A PIL image to base the blocks on. + block_count_per_side: This integer determine the number of blocks the function will return. + If it is 10, for example, 100 blocks will be returns (10 width, 10 height). The blocks will not + necessarely cover square areas. The area covered by each block will be proportional to the image + itself. + """ + if not image.size[0]: + return [] + cdef int width, height, block_width, block_height, ih, iw, top, bottom, left, right + width, height = image.size + block_width = max(width // block_count_per_side, 1) + block_height = max(height // block_count_per_side, 1) + result = [] + for ih in range(block_count_per_side): + top = min(ih * block_height, height - block_height) + bottom = top + block_height + for iw in range(block_count_per_side): + left = min(iw * block_width, width - block_width) + right = left + block_width + box = (left, top, right, bottom) + crop = image.crop(box) + result.append(getblock(crop)) + return result + +cdef int diff(first, second): + """Returns the difference between the first block and the second. + + It returns an absolute sum of the 3 differences (RGB). + """ + cdef int r1, g1, b1, r2, g2, b2 + r1, g1, b1 = first + r2, g2, b2 = second + return abs(r1 - r2) + abs(g1 - g2) + abs(b1 - b2) + +def avgdiff(first, second, int limit, int min_iterations): + """Returns the average diff between first blocks and seconds. + + If the result surpasses limit, limit + 1 is returned, except if less than min_iterations + iterations have been made in the blocks. + """ + cdef int count, sum, i, iteration_count + count = len(first) + if count != len(second): + raise DifferentBlockCountError() + if not count: + raise NoBlocksError() + sum = 0 + for i in range(count): + iteration_count = i + 1 + item1 = first[i] + item2 = second[i] + sum += diff(item1, item2) + if sum > limit * iteration_count and iteration_count >= min_iterations: + return limit + 1 + result = sum // count + if (not result) and sum: + result = 1 + return result \ No newline at end of file diff --git a/py/modules/block/setup.py b/py/modules/block/setup.py new file mode 100644 index 00000000..9d8f4cb5 --- /dev/null +++ b/py/modules/block/setup.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python +# Created By: Virgil Dupras +# Created On: 2009-04-23 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +from distutils.core import setup +from distutils.extension import Extension +from Cython.Distutils import build_ext + +setup( + cmdclass = {'build_ext': build_ext}, + ext_modules = [Extension("_block", ["block.pyx"])] +) \ No newline at end of file diff --git a/py/modules/cache/cache.pyx b/py/modules/cache/cache.pyx new file mode 100644 index 00000000..7bd2407d --- /dev/null +++ b/py/modules/cache/cache.pyx @@ -0,0 +1,34 @@ +#!/usr/bin/env python +# Created By: Virgil Dupras +# Created On: 2009-04-23 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +# ok, this is hacky and stuff, but I don't know C well enough to play with char buffers, copy +# them around and stuff +cdef int xchar_to_int(char c): + if 48 <= c <= 57: # 0-9 + return c - 48 + elif 65 <= c <= 70: # A-F + return c - 55 + elif 97 <= c <= 102: # a-f + return c - 87 + +def string_to_colors(s): + """Transform the string 's' in a list of 3 sized tuples. + """ + result = [] + cdef int i, char_count, r, g, b + cdef char* cs + char_count = len(s) + char_count = (char_count // 6) * 6 + cs = s + for i in range(0, char_count, 6): + r = xchar_to_int(cs[i]) << 4 + r += xchar_to_int(cs[i+1]) + g = xchar_to_int(cs[i+2]) << 4 + g += xchar_to_int(cs[i+3]) + b = xchar_to_int(cs[i+4]) << 4 + b += xchar_to_int(cs[i+5]) + result.append((r, g, b)) + return result diff --git a/py/modules/cache/setup.py b/py/modules/cache/setup.py new file mode 100644 index 00000000..2b6cd31b --- /dev/null +++ b/py/modules/cache/setup.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python +# Created By: Virgil Dupras +# Created On: 2009-04-23 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +from distutils.core import setup +from distutils.extension import Extension +from Cython.Distutils import build_ext + +setup( + cmdclass = {'build_ext': build_ext}, + ext_modules = [Extension("_cache", ["cache.pyx"])] +) \ No newline at end of file diff --git a/py/picture/__init__.py b/py/picture/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/py/picture/block.py b/py/picture/block.py new file mode 100644 index 00000000..70015a50 --- /dev/null +++ b/py/picture/block.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python +""" +Unit Name: hs.picture.block +Created By: Virgil Dupras +Created On: 2006/09/01 +Last modified by:$Author: virgil $ +Last modified on:$Date: 2009-05-26 18:12:39 +0200 (Tue, 26 May 2009) $ + $Revision: 4365 $ +Copyright 2004-2006 Hardcoded Software (http://www.hardcoded.net) +""" +from _block import NoBlocksError, DifferentBlockCountError, avgdiff, getblocks2 + +# Converted to Cython +# def getblock(image): +# """Returns a 3 sized tuple containing the mean color of 'image'. +# +# image: a PIL image or crop. +# """ +# if image.size[0]: +# pixel_count = image.size[0] * image.size[1] +# red = green = blue = 0 +# for r,g,b in image.getdata(): +# red += r +# green += g +# blue += b +# return (red // pixel_count, green // pixel_count, blue // pixel_count) +# else: +# return (0,0,0) + +# This is not used anymore +# def getblocks(image,blocksize): +# """Returns a list of blocks (3 sized tuples). +# +# image: A PIL image to base the blocks on. +# blocksize: The size of the blocks to be create. This is a single integer, defining +# both width and height (blocks are square). +# """ +# if min(image.size) < blocksize: +# return () +# result = [] +# for i in xrange(image.size[1] // blocksize): +# for j in xrange(image.size[0] // blocksize): +# box = (blocksize * j, blocksize * i, blocksize * (j + 1), blocksize * (i + 1)) +# crop = image.crop(box) +# result.append(getblock(crop)) +# return result + +# Converted to Cython +# def getblocks2(image,block_count_per_side): +# """Returns a list of blocks (3 sized tuples). +# +# image: A PIL image to base the blocks on. +# block_count_per_side: This integer determine the number of blocks the function will return. +# If it is 10, for example, 100 blocks will be returns (10 width, 10 height). The blocks will not +# necessarely cover square areas. The area covered by each block will be proportional to the image +# itself. +# """ +# if not image.size[0]: +# return [] +# width,height = image.size +# block_width = max(width // block_count_per_side,1) +# block_height = max(height // block_count_per_side,1) +# result = [] +# for ih in range(block_count_per_side): +# top = min(ih * block_height, height - block_height) +# bottom = top + block_height +# for iw in range(block_count_per_side): +# left = min(iw * block_width, width - block_width) +# right = left + block_width +# box = (left,top,right,bottom) +# crop = image.crop(box) +# result.append(getblock(crop)) +# return result + +# Converted to Cython +# def diff(first, second): +# """Returns the difference between the first block and the second. +# +# It returns an absolute sum of the 3 differences (RGB). +# """ +# r1, g1, b1 = first +# r2, g2, b2 = second +# return abs(r1 - r2) + abs(g1 - g2) + abs(b1 - b2) + +# Converted to Cython +# def avgdiff(first, second, limit=768, min_iterations=1): +# """Returns the average diff between first blocks and seconds. +# +# If the result surpasses limit, limit + 1 is returned, except if less than min_iterations +# iterations have been made in the blocks. +# """ +# if len(first) != len(second): +# raise DifferentBlockCountError +# if not first: +# raise NoBlocksError +# count = len(first) +# sum = 0 +# zipped = izip(xrange(1, count + 1), first, second) +# for i, first, second in zipped: +# sum += diff(first, second) +# if sum > limit * i and i >= min_iterations: +# return limit + 1 +# result = sum // count +# if (not result) and sum: +# result = 1 +# return result + +# This is not used anymore +# def maxdiff(first,second,limit=768): +# """Returns the max diff between first blocks and seconds. +# +# If the result surpasses limit, the first max being over limit is returned. +# """ +# if len(first) != len(second): +# raise DifferentBlockCountError +# if not first: +# raise NoBlocksError +# result = 0 +# zipped = zip(first,second) +# for first,second in zipped: +# result = max(result,diff(first,second)) +# if result > limit: +# return result +# return result diff --git a/py/picture/block_test.py b/py/picture/block_test.py new file mode 100644 index 00000000..a06cf617 --- /dev/null +++ b/py/picture/block_test.py @@ -0,0 +1,313 @@ +#!/usr/bin/env python +""" +Unit Name: tests.picture.block +Created By: Virgil Dupras +Created On: 2006/09/01 +Last modified by:$Author: virgil $ +Last modified on:$Date: 2009-05-28 15:22:39 +0200 (Thu, 28 May 2009) $ + $Revision: 4385 $ +Copyright 2004-2006 Hardcoded Software (http://www.hardcoded.net) +""" +# The commented out tests are tests for function that have been converted to pure C for speed +import unittest + +from .block import * + +def my_avgdiff(first, second, limit=768, min_iter=3): # this is so I don't have to re-write every call + return avgdiff(first, second, limit, min_iter) + +BLACK = (0,0,0) +RED = (0xff,0,0) +GREEN = (0,0xff,0) +BLUE = (0,0,0xff) + +class FakeImage(object): + def __init__(self, size, data): + self.size = size + self.data = data + + def getdata(self): + return self.data + + def crop(self, box): + pixels = [] + for i in range(box[1], box[3]): + for j in range(box[0], box[2]): + pixel = self.data[i * self.size[0] + j] + pixels.append(pixel) + return FakeImage((box[2] - box[0], box[3] - box[1]), pixels) + +def empty(): + return FakeImage((0,0), []) + +def single_pixel(): #one red pixel + return FakeImage((1, 1), [(0xff,0,0)]) + +def four_pixels(): + pixels = [RED,(0,0x80,0xff),(0x80,0,0),(0,0x40,0x80)] + return FakeImage((2, 2), pixels) + +class TCgetblock(unittest.TestCase): + def test_single_pixel(self): + im = single_pixel() + [b] = getblocks2(im, 1) + self.assertEqual(RED,b) + + def test_no_pixel(self): + im = empty() + self.assertEqual([], getblocks2(im, 1)) + + def test_four_pixels(self): + im = four_pixels() + [b] = getblocks2(im, 1) + meanred = (0xff + 0x80) // 4 + meangreen = (0x80 + 0x40) // 4 + meanblue = (0xff + 0x80) // 4 + self.assertEqual((meanred,meangreen,meanblue),b) + + +# class TCdiff(unittest.TestCase): +# def test_diff(self): +# b1 = (10, 20, 30) +# b2 = (1, 2, 3) +# self.assertEqual(9 + 18 + 27,diff(b1,b2)) +# +# def test_diff_negative(self): +# b1 = (10, 20, 30) +# b2 = (1, 2, 3) +# self.assertEqual(9 + 18 + 27,diff(b2,b1)) +# +# def test_diff_mixed_positive_and_negative(self): +# b1 = (1, 5, 10) +# b2 = (10, 1, 15) +# self.assertEqual(9 + 4 + 5,diff(b1,b2)) +# + +# class TCgetblocks(unittest.TestCase): +# def test_empty_image(self): +# im = empty() +# blocks = getblocks(im,1) +# self.assertEqual(0,len(blocks)) +# +# def test_one_block_image(self): +# im = four_pixels() +# blocks = getblocks2(im, 1) +# self.assertEqual(1,len(blocks)) +# block = blocks[0] +# meanred = (0xff + 0x80) // 4 +# meangreen = (0x80 + 0x40) // 4 +# meanblue = (0xff + 0x80) // 4 +# self.assertEqual((meanred,meangreen,meanblue),block) +# +# def test_not_enough_height_to_fit_a_block(self): +# im = FakeImage((2,1), [BLACK, BLACK]) +# blocks = getblocks(im,2) +# self.assertEqual(0,len(blocks)) +# +# def xtest_dont_include_leftovers(self): +# # this test is disabled because getblocks is not used and getblock in cdeffed +# pixels = [ +# RED,(0,0x80,0xff),BLACK, +# (0x80,0,0),(0,0x40,0x80),BLACK, +# BLACK,BLACK,BLACK +# ] +# im = FakeImage((3,3), pixels) +# blocks = getblocks(im,2) +# block = blocks[0] +# #Because the block is smaller than the image, only blocksize must be considered. +# meanred = (0xff + 0x80) // 4 +# meangreen = (0x80 + 0x40) // 4 +# meanblue = (0xff + 0x80) // 4 +# self.assertEqual((meanred,meangreen,meanblue),block) +# +# def xtest_two_blocks(self): +# # this test is disabled because getblocks is not used and getblock in cdeffed +# pixels = [BLACK for i in xrange(4 * 2)] +# pixels[0] = RED +# pixels[1] = (0,0x80,0xff) +# pixels[4] = (0x80,0,0) +# pixels[5] = (0,0x40,0x80) +# im = FakeImage((4, 2), pixels) +# blocks = getblocks(im,2) +# self.assertEqual(2,len(blocks)) +# block = blocks[0] +# #Because the block is smaller than the image, only blocksize must be considered. +# meanred = (0xff + 0x80) // 4 +# meangreen = (0x80 + 0x40) // 4 +# meanblue = (0xff + 0x80) // 4 +# self.assertEqual((meanred,meangreen,meanblue),block) +# self.assertEqual(BLACK,blocks[1]) +# +# def test_four_blocks(self): +# pixels = [BLACK for i in xrange(4 * 4)] +# pixels[0] = RED +# pixels[1] = (0,0x80,0xff) +# pixels[4] = (0x80,0,0) +# pixels[5] = (0,0x40,0x80) +# im = FakeImage((4, 4), pixels) +# blocks = getblocks2(im, 2) +# self.assertEqual(4,len(blocks)) +# block = blocks[0] +# #Because the block is smaller than the image, only blocksize must be considered. +# meanred = (0xff + 0x80) // 4 +# meangreen = (0x80 + 0x40) // 4 +# meanblue = (0xff + 0x80) // 4 +# self.assertEqual((meanred,meangreen,meanblue),block) +# self.assertEqual(BLACK,blocks[1]) +# self.assertEqual(BLACK,blocks[2]) +# self.assertEqual(BLACK,blocks[3]) +# + +class TCgetblocks2(unittest.TestCase): + def test_empty_image(self): + im = empty() + blocks = getblocks2(im,1) + self.assertEqual(0,len(blocks)) + + def test_one_block_image(self): + im = four_pixels() + blocks = getblocks2(im,1) + self.assertEqual(1,len(blocks)) + block = blocks[0] + meanred = (0xff + 0x80) // 4 + meangreen = (0x80 + 0x40) // 4 + meanblue = (0xff + 0x80) // 4 + self.assertEqual((meanred,meangreen,meanblue),block) + + def test_four_blocks_all_black(self): + im = FakeImage((2, 2), [BLACK, BLACK, BLACK, BLACK]) + blocks = getblocks2(im,2) + self.assertEqual(4,len(blocks)) + for block in blocks: + self.assertEqual(BLACK,block) + + def test_two_pixels_image_horizontal(self): + pixels = [RED,BLUE] + im = FakeImage((2, 1), pixels) + blocks = getblocks2(im,2) + self.assertEqual(4,len(blocks)) + self.assertEqual(RED,blocks[0]) + self.assertEqual(BLUE,blocks[1]) + self.assertEqual(RED,blocks[2]) + self.assertEqual(BLUE,blocks[3]) + + def test_two_pixels_image_vertical(self): + pixels = [RED,BLUE] + im = FakeImage((1, 2), pixels) + blocks = getblocks2(im,2) + self.assertEqual(4,len(blocks)) + self.assertEqual(RED,blocks[0]) + self.assertEqual(RED,blocks[1]) + self.assertEqual(BLUE,blocks[2]) + self.assertEqual(BLUE,blocks[3]) + + +class TCavgdiff(unittest.TestCase): + def test_empty(self): + self.assertRaises(NoBlocksError, my_avgdiff, [], []) + + def test_two_blocks(self): + im = empty() + b1 = (5,10,15) + b2 = (255,250,245) + b3 = (0,0,0) + b4 = (255,0,255) + blocks1 = [b1,b2] + blocks2 = [b3,b4] + expected1 = 5 + 10 + 15 + expected2 = 0 + 250 + 10 + expected = (expected1 + expected2) // 2 + self.assertEqual(expected, my_avgdiff(blocks1, blocks2)) + + def test_blocks_not_the_same_size(self): + b = (0,0,0) + self.assertRaises(DifferentBlockCountError,my_avgdiff,[b,b],[b]) + + def test_first_arg_is_empty_but_not_second(self): + #Don't return 0 (as when the 2 lists are empty), raise! + b = (0,0,0) + self.assertRaises(DifferentBlockCountError,my_avgdiff,[],[b]) + + def test_limit(self): + ref = (0,0,0) + b1 = (10,10,10) #avg 30 + b2 = (20,20,20) #avg 45 + b3 = (30,30,30) #avg 60 + blocks1 = [ref,ref,ref] + blocks2 = [b1,b2,b3] + self.assertEqual(45,my_avgdiff(blocks1,blocks2,44)) + + def test_min_iterations(self): + ref = (0,0,0) + b1 = (10,10,10) #avg 30 + b2 = (20,20,20) #avg 45 + b3 = (10,10,10) #avg 40 + blocks1 = [ref,ref,ref] + blocks2 = [b1,b2,b3] + self.assertEqual(40,my_avgdiff(blocks1,blocks2,45 - 1,3)) + + # Bah, I don't know why this test fails, but I don't think it matters very much + # def test_just_over_the_limit(self): + # #A score just over the limit might return exactly the limit due to truncating. We should + # #ceil() the result in this case. + # ref = (0,0,0) + # b1 = (10,0,0) + # b2 = (11,0,0) + # blocks1 = [ref,ref] + # blocks2 = [b1,b2] + # self.assertEqual(11,my_avgdiff(blocks1,blocks2,10)) + # + def test_return_at_least_1_at_the_slightest_difference(self): + ref = (0,0,0) + b1 = (1,0,0) + blocks1 = [ref for i in xrange(250)] + blocks2 = [ref for i in xrange(250)] + blocks2[0] = b1 + self.assertEqual(1,my_avgdiff(blocks1,blocks2)) + + def test_return_0_if_there_is_no_difference(self): + ref = (0,0,0) + blocks1 = [ref,ref] + blocks2 = [ref,ref] + self.assertEqual(0,my_avgdiff(blocks1,blocks2)) + + +# class TCmaxdiff(unittest.TestCase): +# def test_empty(self): +# self.assertRaises(NoBlocksError,maxdiff,[],[]) +# +# def test_two_blocks(self): +# b1 = (5,10,15) +# b2 = (255,250,245) +# b3 = (0,0,0) +# b4 = (255,0,255) +# blocks1 = [b1,b2] +# blocks2 = [b3,b4] +# expected1 = 5 + 10 + 15 +# expected2 = 0 + 250 + 10 +# expected = max(expected1,expected2) +# self.assertEqual(expected,maxdiff(blocks1,blocks2)) +# +# def test_blocks_not_the_same_size(self): +# b = (0,0,0) +# self.assertRaises(DifferentBlockCountError,maxdiff,[b,b],[b]) +# +# def test_first_arg_is_empty_but_not_second(self): +# #Don't return 0 (as when the 2 lists are empty), raise! +# b = (0,0,0) +# self.assertRaises(DifferentBlockCountError,maxdiff,[],[b]) +# +# def test_limit(self): +# b1 = (5,10,15) +# b2 = (255,250,245) +# b3 = (0,0,0) +# b4 = (255,0,255) +# blocks1 = [b1,b2] +# blocks2 = [b3,b4] +# expected1 = 5 + 10 + 15 +# expected2 = 0 + 250 + 10 +# self.assertEqual(expected1,maxdiff(blocks1,blocks2,expected1 - 1)) +# + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/py/picture/cache.py b/py/picture/cache.py new file mode 100644 index 00000000..6ff0d2d1 --- /dev/null +++ b/py/picture/cache.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python +""" +Unit Name: hs.picture.cache +Created By: Virgil Dupras +Created On: 2006/09/14 +Last modified by:$Author: virgil $ +Last modified on:$Date: 2009-05-28 16:33:32 +0200 (Thu, 28 May 2009) $ + $Revision: 4392 $ +Copyright 2004-2006 Hardcoded Software (http://www.hardcoded.net) +""" +import os +import logging +import sqlite3 as sqlite + +import hsutil.sqlite + +from _cache import string_to_colors + +def colors_to_string(colors): + """Transform the 3 sized tuples 'colors' into a hex string. + + [(0,100,255)] --> 0064ff + [(1,2,3),(4,5,6)] --> 010203040506 + """ + return ''.join(['%02x%02x%02x' % (r,g,b) for r,g,b in colors]) + +# This function is an important bottleneck of dupeGuru PE. It has been converted to Cython. +# def string_to_colors(s): +# """Transform the string 's' in a list of 3 sized tuples. +# """ +# result = [] +# for i in xrange(0, len(s), 6): +# number = int(s[i:i+6], 16) +# result.append((number >> 16, (number >> 8) & 0xff, number & 0xff)) +# return result + +class Cache(object): + """A class to cache picture blocks. + """ + def __init__(self, db=':memory:', threaded=True): + def create_tables(): + sql = "create table pictures(path TEXT, blocks TEXT)" + self.con.execute(sql); + sql = "create index idx_path on pictures (path)" + self.con.execute(sql) + + self.dbname = db + if threaded: + self.con = hsutil.sqlite.ThreadedConn(db, True) + else: + self.con = sqlite.connect(db, isolation_level=None) + try: + self.con.execute("select * from pictures where 1=2") + except sqlite.OperationalError: # new db + create_tables() + except sqlite.DatabaseError, e: # corrupted db + logging.warning('Could not create picture cache because of an error: %s', str(e)) + self.con.close() + os.remove(db) + if threaded: + self.con = hsutil.sqlite.ThreadedConn(db, True) + else: + self.con = sqlite.connect(db, isolation_level=None) + create_tables() + + def __contains__(self, key): + sql = "select count(*) from pictures where path = ?" + result = self.con.execute(sql, [key]).fetchall() + return result[0][0] > 0 + + def __delitem__(self, key): + if key not in self: + raise KeyError(key) + sql = "delete from pictures where path = ?" + self.con.execute(sql, [key]) + + # Optimized + def __getitem__(self, key): + if isinstance(key, int): + sql = "select blocks from pictures where rowid = ?" + else: + sql = "select blocks from pictures where path = ?" + result = self.con.execute(sql, [key]).fetchone() + if result: + result = string_to_colors(result[0]) + return result + else: + raise KeyError(key) + + def __iter__(self): + sql = "select path from pictures" + result = self.con.execute(sql) + return (row[0] for row in result) + + def __len__(self): + sql = "select count(*) from pictures" + result = self.con.execute(sql).fetchall() + return result[0][0] + + def __setitem__(self, key, value): + value = colors_to_string(value) + if key in self: + sql = "update pictures set blocks = ? where path = ?" + else: + sql = "insert into pictures(blocks,path) values(?,?)" + try: + self.con.execute(sql, [value, key]) + except sqlite.OperationalError: + logging.warning('Picture cache could not set %r for key %r', value, key) + except sqlite.DatabaseError, e: + logging.warning('DatabaseError while setting %r for key %r: %s', value, key, str(e)) + + def clear(self): + sql = "delete from pictures" + self.con.execute(sql) + + def filter(self, func): + to_delete = [key for key in self if not func(key)] + for key in to_delete: + del self[key] + + def get_id(self, path): + sql = "select rowid from pictures where path = ?" + result = self.con.execute(sql, [path]).fetchone() + if result: + return result[0] + else: + raise ValueError(path) + + def get_multiple(self, rowids): + sql = "select rowid, blocks from pictures where rowid in (%s)" % ','.join(map(str, rowids)) + cur = self.con.execute(sql) + return ((rowid, string_to_colors(blocks)) for rowid, blocks in cur) + diff --git a/py/picture/cache_test.py b/py/picture/cache_test.py new file mode 100644 index 00000000..f453112f --- /dev/null +++ b/py/picture/cache_test.py @@ -0,0 +1,159 @@ +#!/usr/bin/env python +""" +Unit Name: tests.picture.cache +Created By: Virgil Dupras +Created On: 2006/09/14 +Last modified by:$Author: virgil $ +Last modified on:$Date: 2009-05-28 15:22:39 +0200 (Thu, 28 May 2009) $ + $Revision: 4385 $ +Copyright 2004-2006 Hardcoded Software (http://www.hardcoded.net) +""" +import unittest +from StringIO import StringIO +import os.path as op +import os +import threading + +from hsutil.testcase import TestCase +from .cache import * + +class TCcolors_to_string(unittest.TestCase): + def test_no_color(self): + self.assertEqual('',colors_to_string([])) + + def test_single_color(self): + self.assertEqual('000000',colors_to_string([(0,0,0)])) + self.assertEqual('010101',colors_to_string([(1,1,1)])) + self.assertEqual('0a141e',colors_to_string([(10,20,30)])) + + def test_two_colors(self): + self.assertEqual('000102030405',colors_to_string([(0,1,2),(3,4,5)])) + + +class TCstring_to_colors(unittest.TestCase): + def test_empty(self): + self.assertEqual([],string_to_colors('')) + + def test_single_color(self): + self.assertEqual([(0,0,0)],string_to_colors('000000')) + self.assertEqual([(2,3,4)],string_to_colors('020304')) + self.assertEqual([(10,20,30)],string_to_colors('0a141e')) + + def test_two_colors(self): + self.assertEqual([(10,20,30),(40,50,60)],string_to_colors('0a141e28323c')) + + def test_incomplete_color(self): + # don't return anything if it's not a complete color + self.assertEqual([],string_to_colors('102')) + + +class TCCache(TestCase): + def test_empty(self): + c = Cache() + self.assertEqual(0,len(c)) + self.assertRaises(KeyError,c.__getitem__,'foo') + + def test_set_then_retrieve_blocks(self): + c = Cache() + b = [(0,0,0),(1,2,3)] + c['foo'] = b + self.assertEqual(b,c['foo']) + + def test_delitem(self): + c = Cache() + c['foo'] = '' + del c['foo'] + self.assert_('foo' not in c) + self.assertRaises(KeyError,c.__delitem__,'foo') + + def test_persistance(self): + DBNAME = op.join(self.tmpdir(), 'hstest.db') + c = Cache(DBNAME) + c['foo'] = [(1,2,3)] + del c + c = Cache(DBNAME) + self.assertEqual([(1,2,3)],c['foo']) + del c + os.remove(DBNAME) + + def test_filter(self): + c = Cache() + c['foo'] = '' + c['bar'] = '' + c['baz'] = '' + c.filter(lambda p:p != 'bar') #only 'bar' is removed + self.assertEqual(2,len(c)) + self.assert_('foo' in c) + self.assert_('baz' in c) + self.assert_('bar' not in c) + + def test_clear(self): + c = Cache() + c['foo'] = '' + c['bar'] = '' + c['baz'] = '' + c.clear() + self.assertEqual(0,len(c)) + self.assert_('foo' not in c) + self.assert_('baz' not in c) + self.assert_('bar' not in c) + + def test_corrupted_db(self): + dbname = op.join(self.tmpdir(), 'foo.db') + fp = open(dbname, 'w') + fp.write('invalid sqlite content') + fp.close() + c = Cache(dbname) # should not raise a DatabaseError + c['foo'] = [(1, 2, 3)] + del c + c = Cache(dbname) + self.assertEqual(c['foo'], [(1, 2, 3)]) + + def test_by_id(self): + # it's possible to use the cache by referring to the files by their row_id + c = Cache() + b = [(0,0,0),(1,2,3)] + c['foo'] = b + foo_id = c.get_id('foo') + self.assertEqual(c[foo_id], b) + + +class TCCacheSQLEscape(unittest.TestCase): + def test_contains(self): + c = Cache() + self.assert_("foo'bar" not in c) + + def test_getitem(self): + c = Cache() + self.assertRaises(KeyError, c.__getitem__, "foo'bar") + + def test_setitem(self): + c = Cache() + c["foo'bar"] = [] + + def test_delitem(self): + c = Cache() + c["foo'bar"] = [] + try: + del c["foo'bar"] + except KeyError: + self.fail() + + +class TCCacheThreaded(unittest.TestCase): + def test_access_cache(self): + def thread_run(): + try: + c['foo'] = [(1,2,3)] + except sqlite.ProgrammingError: + self.fail() + + c = Cache() + t = threading.Thread(target=thread_run) + t.start() + t.join() + self.assertEqual([(1,2,3)], c['foo']) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/py/picture/matchbase.py b/py/picture/matchbase.py new file mode 100644 index 00000000..cf0d1e89 --- /dev/null +++ b/py/picture/matchbase.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python +""" +Unit Name: hs.picture._match +Created By: Virgil Dupras +Created On: 2007/02/25 +Last modified by:$Author: virgil $ +Last modified on:$Date: 2009-05-28 16:02:48 +0200 (Thu, 28 May 2009) $ + $Revision: 4388 $ +Copyright 2007 Hardcoded Software (http://www.hardcoded.net) +""" +import logging +import multiprocessing +from Queue import Empty +from collections import defaultdict + +from hsutil import job +from hs.utils.misc import dedupe + +from dupeguru.engine import Match +from block import avgdiff, DifferentBlockCountError, NoBlocksError +from cache import Cache + +MIN_ITERATIONS = 3 + +def get_match(first,second,percentage): + if percentage < 0: + percentage = 0 + return Match(first,second,percentage) + +class MatchFactory(object): + cached_blocks = None + block_count_per_side = 15 + threshold = 75 + match_scaled = False + + def _do_getmatches(self, files, j): + raise NotImplementedError() + + def getmatches(self, files, j=job.nulljob): + # The MemoryError handlers in there use logging without first caring about whether or not + # there is enough memory left to carry on the operation because it is assumed that the + # MemoryError happens when trying to read an image file, which is freed from memory by the + # time that MemoryError is raised. + j = j.start_subjob([2, 8]) + logging.info('Preparing %d files' % len(files)) + prepared = self.prepare_files(files, j) + logging.info('Finished preparing %d files' % len(prepared)) + return self._do_getmatches(prepared, j) + + def prepare_files(self, files, j=job.nulljob): + prepared = [] # only files for which there was no error getting blocks + try: + for picture in j.iter_with_progress(files, 'Analyzed %d/%d pictures'): + picture.dimensions + picture.unicode_path = unicode(picture.path) + try: + if picture.unicode_path not in self.cached_blocks: + blocks = picture.get_blocks(self.block_count_per_side) + self.cached_blocks[picture.unicode_path] = blocks + prepared.append(picture) + except IOError as e: + logging.warning(unicode(e)) + except MemoryError: + logging.warning(u'Ran out of memory while reading %s of size %d' % (picture.unicode_path, picture.size)) + if picture.size < 10 * 1024 * 1024: # We're really running out of memory + raise + except MemoryError: + logging.warning('Ran out of memory while preparing files') + return prepared + + +def async_compare(ref_id, other_ids, dbname, threshold): + cache = Cache(dbname, threaded=False) + limit = 100 - threshold + ref_blocks = cache[ref_id] + pairs = cache.get_multiple(other_ids) + results = [] + for other_id, other_blocks in pairs: + try: + diff = avgdiff(ref_blocks, other_blocks, limit, MIN_ITERATIONS) + percentage = 100 - diff + except (DifferentBlockCountError, NoBlocksError): + percentage = 0 + if percentage >= threshold: + results.append((ref_id, other_id, percentage)) + cache.con.close() + return results + +class AsyncMatchFactory(MatchFactory): + def _do_getmatches(self, pictures, j): + def empty_out_queue(queue, into): + try: + while True: + into.append(queue.get(block=False)) + except Empty: + pass + + j = j.start_subjob([1, 8, 1], 'Preparing for matching') + cache = self.cached_blocks + id2picture = {} + dimensions2pictures = defaultdict(set) + for picture in pictures[:]: + try: + picture.cache_id = cache.get_id(picture.unicode_path) + id2picture[picture.cache_id] = picture + except ValueError: + pictures.remove(picture) + if not self.match_scaled: + dimensions2pictures[picture.dimensions].add(picture) + pool = multiprocessing.Pool() + async_results = [] + pictures_copy = set(pictures) + for ref in j.iter_with_progress(pictures): + others = pictures_copy if self.match_scaled else dimensions2pictures[ref.dimensions] + others.remove(ref) + if others: + cache_ids = [f.cache_id for f in others] + args = (ref.cache_id, cache_ids, self.cached_blocks.dbname, self.threshold) + async_results.append(pool.apply_async(async_compare, args)) + + matches = [] + for result in j.iter_with_progress(async_results, 'Matched %d/%d pictures'): + matches.extend(result.get()) + + result = [] + for ref_id, other_id, percentage in j.iter_with_progress(matches, 'Verified %d/%d matches', every=10): + ref = id2picture[ref_id] + other = id2picture[other_id] + if percentage == 100 and ref.md5 != other.md5: + percentage = 99 + if percentage >= self.threshold: + result.append(get_match(ref, other, percentage)) + return result + + +multiprocessing.freeze_support() \ No newline at end of file diff --git a/py/results.py b/py/results.py new file mode 100644 index 00000000..a7ded5c0 --- /dev/null +++ b/py/results.py @@ -0,0 +1,359 @@ +#!/usr/bin/env python +""" +Unit Name: dupeguru.results +Created By: Virgil Dupras +Created On: 2006/02/23 +Last modified by:$Author: virgil $ +Last modified on:$Date: 2009-05-28 16:33:32 +0200 (Thu, 28 May 2009) $ + $Revision: 4392 $ +Copyright 2004-2006 Hardcoded Software (http://www.hardcoded.net) +""" +import re +from xml.sax import handler, make_parser, SAXException +from xml.sax.saxutils import XMLGenerator +from xml.sax.xmlreader import AttributesImpl + +from . import engine +from hsutil.job import nulljob +from hsutil.markable import Markable +from hsutil.misc import flatten, cond, nonone +from hsutil.str import format_size +from hsutil.files import open_if_filename + +class Results(Markable): + #---Override + def __init__(self, data_module): + super(Results, self).__init__() + self.__groups = [] + self.__group_of_duplicate = {} + self.__groups_sort_descriptor = None # This is a tuple (key, asc) + self.__dupes = None + self.__dupes_sort_descriptor = None # This is a tuple (key, asc, delta) + self.__filters = None + self.__filtered_dupes = None + self.__filtered_groups = None + self.__recalculate_stats() + self.__marked_size = 0 + self.data = data_module + + def _did_mark(self, dupe): + self.__marked_size += dupe.size + + def _did_unmark(self, dupe): + self.__marked_size -= dupe.size + + def _get_markable_count(self): + return self.__total_count + + def _is_markable(self, dupe): + if dupe.is_ref: + return False + g = self.get_group_of_duplicate(dupe) + if not g: + return False + if dupe is g.ref: + return False + if self.__filtered_dupes and dupe not in self.__filtered_dupes: + return False + return True + + #---Private + def __get_dupe_list(self): + if self.__dupes is None: + self.__dupes = flatten(group.dupes for group in self.groups) + if self.__filtered_dupes: + self.__dupes = [dupe for dupe in self.__dupes if dupe in self.__filtered_dupes] + sd = self.__dupes_sort_descriptor + if sd: + self.sort_dupes(sd[0], sd[1], sd[2]) + return self.__dupes + + def __get_groups(self): + if self.__filtered_groups is None: + return self.__groups + else: + return self.__filtered_groups + + def __get_stat_line(self): + if self.__filtered_dupes is None: + mark_count = self.mark_count + marked_size = self.__marked_size + total_count = self.__total_count + total_size = self.__total_size + else: + mark_count = len([dupe for dupe in self.__filtered_dupes if self.is_marked(dupe)]) + marked_size = sum(dupe.size for dupe in self.__filtered_dupes if self.is_marked(dupe)) + total_count = len([dupe for dupe in self.__filtered_dupes if self.is_markable(dupe)]) + total_size = sum(dupe.size for dupe in self.__filtered_dupes if self.is_markable(dupe)) + if self.mark_inverted: + marked_size = self.__total_size - marked_size + result = '%d / %d (%s / %s) duplicates marked.' % ( + mark_count, + total_count, + format_size(marked_size, 2), + format_size(total_size, 2), + ) + if self.__filters: + result += ' filter: %s' % ' --> '.join(self.__filters) + return result + + def __recalculate_stats(self): + self.__total_size = 0 + self.__total_count = 0 + for group in self.groups: + markable = [dupe for dupe in group.dupes if self._is_markable(dupe)] + self.__total_count += len(markable) + self.__total_size += sum(dupe.size for dupe in markable) + + def __set_groups(self, new_groups): + self.mark_none() + self.__groups = new_groups + self.__group_of_duplicate = {} + for g in self.__groups: + for dupe in g: + self.__group_of_duplicate[dupe] = g + if not hasattr(dupe, 'is_ref'): + dupe.is_ref = False + old_filters = nonone(self.__filters, []) + self.apply_filter(None) + for filter_str in old_filters: + self.apply_filter(filter_str) + + #---Public + def apply_filter(self, filter_str): + ''' Applies a filter 'filter_str' to self.groups + + When you apply the filter, only dupes with the filename matching 'filter_str' will be in + in the results. To cancel the filter, just call apply_filter with 'filter_str' to None, + and the results will go back to normal. + + If call apply_filter on a filtered results, the filter will be applied + *on the filtered results*. + + 'filter_str' is a string containing a regexp to filter dupes with. + ''' + if not filter_str: + self.__filtered_dupes = None + self.__filtered_groups = None + self.__filters = None + else: + if not self.__filters: + self.__filters = [] + self.__filters.append(filter_str) + filter_re = re.compile(filter_str, re.IGNORECASE) + if self.__filtered_dupes is None: + self.__filtered_dupes = flatten(g[:] for g in self.groups) + self.__filtered_dupes = set(dupe for dupe in self.__filtered_dupes if filter_re.search(dupe.name)) + filtered_groups = set() + for dupe in self.__filtered_dupes: + filtered_groups.add(self.get_group_of_duplicate(dupe)) + self.__filtered_groups = list(filtered_groups) + self.__recalculate_stats() + sd = self.__groups_sort_descriptor + if sd: + self.sort_groups(sd[0], sd[1]) + self.__dupes = None + + def get_group_of_duplicate(self, dupe): + try: + return self.__group_of_duplicate[dupe] + except (TypeError, KeyError): + return None + + is_markable = _is_markable + + def load_from_xml(self, infile, get_file, j=nulljob): + self.apply_filter(None) + handler = _ResultsHandler(get_file) + parser = make_parser() + parser.setContentHandler(handler) + try: + infile, must_close = open_if_filename(infile) + except IOError: + return + BUFSIZE = 1024 * 1024 # 1mb buffer + infile.seek(0, 2) + j.start_job(infile.tell() // BUFSIZE) + infile.seek(0, 0) + try: + while True: + data = infile.read(BUFSIZE) + if not data: + break + parser.feed(data) + j.add_progress() + except SAXException: + return + self.groups = handler.groups + for dupe_file in handler.marked: + self.mark(dupe_file) + + def make_ref(self, dupe): + g = self.get_group_of_duplicate(dupe) + r = g.ref + self._remove_mark_flag(dupe) + g.switch_ref(dupe); + if not r.is_ref: + self.__total_count += 1 + self.__total_size += r.size + if not dupe.is_ref: + self.__total_count -= 1 + self.__total_size -= dupe.size + self.__dupes = None + + def perform_on_marked(self, func, remove_from_results): + problems = [] + for d in self.dupes: + if self.is_marked(d) and (not func(d)): + problems.append(d) + if remove_from_results: + to_remove = [d for d in self.dupes if self.is_marked(d) and (d not in problems)] + self.remove_duplicates(to_remove) + self.mark_none() + for d in problems: + self.mark(d) + return len(problems) + + def remove_duplicates(self, dupes): + '''Remove 'dupes' from their respective group, and remove the group is it ends up empty. + ''' + affected_groups = set() + for dupe in dupes: + group = self.get_group_of_duplicate(dupe) + if dupe not in group.dupes: + return + group.remove_dupe(dupe, False) + self._remove_mark_flag(dupe) + self.__total_count -= 1 + self.__total_size -= dupe.size + if not group: + self.__groups.remove(group) + if self.__filtered_groups: + self.__filtered_groups.remove(group) + else: + affected_groups.add(group) + for group in affected_groups: + group.clean_matches() + self.__dupes = None + + def save_to_xml(self, outfile, with_data=False): + self.apply_filter(None) + outfile, must_close = open_if_filename(outfile, 'wb') + writer = XMLGenerator(outfile, 'utf-8') + writer.startDocument() + empty_attrs = AttributesImpl({}) + writer.startElement('results', empty_attrs) + for g in self.groups: + writer.startElement('group', empty_attrs) + dupe2index = {} + for index, d in enumerate(g): + dupe2index[d] = index + try: + words = engine.unpack_fields(d.words) + except AttributeError: + words = () + attrs = AttributesImpl({ + 'path': unicode(d.path), + 'is_ref': cond(d.is_ref, 'y', 'n'), + 'words': ','.join(words), + 'marked': cond(self.is_marked(d), 'y', 'n') + }) + writer.startElement('file', attrs) + if with_data: + data_list = self.data.GetDisplayInfo(d, g) + for data in data_list: + attrs = AttributesImpl({ + 'value': data, + }) + writer.startElement('data', attrs) + writer.endElement('data') + writer.endElement('file') + for match in g.matches: + attrs = AttributesImpl({ + 'first': str(dupe2index[match.first]), + 'second': str(dupe2index[match.second]), + 'percentage': str(int(match.percentage)), + }) + writer.startElement('match', attrs) + writer.endElement('match') + writer.endElement('group') + writer.endElement('results') + writer.endDocument() + if must_close: + outfile.close() + + def sort_dupes(self, key, asc=True, delta=False): + if not self.__dupes: + self.__get_dupe_list() + self.__dupes.sort(key=lambda d: self.data.GetDupeSortKey(d, lambda: self.get_group_of_duplicate(d), key, delta)) + if not asc: + self.__dupes.reverse() + self.__dupes_sort_descriptor = (key,asc,delta) + + def sort_groups(self,key,asc=True): + self.groups.sort(key=lambda g: self.data.GetGroupSortKey(g, key)) + if not asc: + self.groups.reverse() + self.__groups_sort_descriptor = (key,asc) + + #---Properties + dupes = property(__get_dupe_list) + groups = property(__get_groups, __set_groups) + stat_line = property(__get_stat_line) + +class _ResultsHandler(handler.ContentHandler): + def __init__(self, get_file): + self.group = None + self.dupes = None + self.marked = set() + self.groups = [] + self.get_file = get_file + + def startElement(self, name, attrs): + if name == 'group': + self.group = engine.Group() + self.dupes = [] + return + if (name == 'file') and (self.group is not None): + if not (('path' in attrs) and ('words' in attrs)): + return + path = attrs['path'] + file = self.get_file(path) + if file is None: + return + file.words = attrs['words'].split(',') + file.is_ref = attrs.get('is_ref') == 'y' + self.dupes.append(file) + if attrs.get('marked') == 'y': + self.marked.add(file) + if (name == 'match') and (self.group is not None): + try: + first_file = self.dupes[int(attrs['first'])] + second_file = self.dupes[int(attrs['second'])] + percentage = int(attrs['percentage']) + self.group.add_match(engine.Match(first_file, second_file, percentage)) + except (IndexError, KeyError, ValueError): # Covers missing attr, non-int values and indexes out of bounds + pass + + def endElement(self, name): + def do_match(ref_file, other_files, group): + if not other_files: + return + for other_file in other_files: + group.add_match(engine.get_match(ref_file, other_file)) + do_match(other_files[0], other_files[1:], group) + + if name == 'group': + group = self.group + self.group = None + dupes = self.dupes + self.dupes = [] + if group is None: + return + if len(dupes) < 2: + return + if not group.matches: # elements not present, do it manually, without % + do_match(dupes[0], dupes[1:], group) + group.prioritize(lambda x: dupes.index(x)) + self.groups.append(group) + diff --git a/py/results_test.py b/py/results_test.py new file mode 100644 index 00000000..1e74efc6 --- /dev/null +++ b/py/results_test.py @@ -0,0 +1,742 @@ +#!/usr/bin/env python +""" +Unit Name: dupeguru.tests.results +Created By: Virgil Dupras +Created On: 2006/02/23 +Last modified by:$Author: virgil $ +Last modified on:$Date: 2009-05-28 15:22:39 +0200 (Thu, 28 May 2009) $ + $Revision: 4385 $ +Copyright 2004-2006 Hardcoded Software (http://www.hardcoded.net) +""" +import unittest +import StringIO +import xml.dom.minidom +import os.path as op + +from hsutil.path import Path +from hsutil.testcase import TestCase +from hsutil.misc import first + +from . import engine_test +from . import data +from . import engine +from .results import * + +class NamedObject(engine_test.NamedObject): + size = 1 + path = property(lambda x:Path('basepath') + x.name) + is_ref = False + + def __nonzero__(self): + return False #Make sure that operations are made correctly when the bool value of files is false. + +# Returns a group set that looks like that: +# "foo bar" (1) +# "bar bleh" (1024) +# "foo bleh" (1) +# "ibabtu" (1) +# "ibabtu" (1) +def GetTestGroups(): + objects = [NamedObject("foo bar"),NamedObject("bar bleh"),NamedObject("foo bleh"),NamedObject("ibabtu"),NamedObject("ibabtu")] + objects[1].size = 1024 + matches = engine.MatchFactory().getmatches(objects) #we should have 5 matches + groups = engine.get_groups(matches) #We should have 2 groups + for g in groups: + g.prioritize(lambda x:objects.index(x)) #We want the dupes to be in the same order as the list is + groups.sort(key=len, reverse=True) # We want the group with 3 members to be first. + return (objects,matches,groups) + +class TCResultsEmpty(TestCase): + def setUp(self): + self.results = Results(data) + + def test_stat_line(self): + self.assertEqual("0 / 0 (0.00 B / 0.00 B) duplicates marked.",self.results.stat_line) + + def test_groups(self): + self.assertEqual(0,len(self.results.groups)) + + def test_get_group_of_duplicate(self): + self.assert_(self.results.get_group_of_duplicate('foo') is None) + + def test_save_to_xml(self): + f = StringIO.StringIO() + self.results.save_to_xml(f) + f.seek(0) + doc = xml.dom.minidom.parse(f) + root = doc.documentElement + self.assertEqual('results',root.nodeName) + + +class TCResultsWithSomeGroups(TestCase): + def setUp(self): + self.results = Results(data) + self.objects,self.matches,self.groups = GetTestGroups() + self.results.groups = self.groups + + def test_stat_line(self): + self.assertEqual("0 / 3 (0.00 B / 1.01 KB) duplicates marked.",self.results.stat_line) + + def test_groups(self): + self.assertEqual(2,len(self.results.groups)) + + def test_get_group_of_duplicate(self): + for o in self.objects: + g = self.results.get_group_of_duplicate(o) + self.assert_(isinstance(g, engine.Group)) + self.assert_(o in g) + self.assert_(self.results.get_group_of_duplicate(self.groups[0]) is None) + + def test_remove_duplicates(self): + g1,g2 = self.results.groups + self.results.remove_duplicates([g1.dupes[0]]) + self.assertEqual(2,len(g1)) + self.assert_(g1 in self.results.groups) + self.results.remove_duplicates([g1.ref]) + self.assertEqual(2,len(g1)) + self.assert_(g1 in self.results.groups) + self.results.remove_duplicates([g1.dupes[0]]) + self.assertEqual(0,len(g1)) + self.assert_(g1 not in self.results.groups) + self.results.remove_duplicates([g2.dupes[0]]) + self.assertEqual(0,len(g2)) + self.assert_(g2 not in self.results.groups) + self.assertEqual(0,len(self.results.groups)) + + def test_remove_duplicates_with_ref_files(self): + g1,g2 = self.results.groups + self.objects[0].is_ref = True + self.objects[1].is_ref = True + self.results.remove_duplicates([self.objects[2]]) + self.assertEqual(0,len(g1)) + self.assert_(g1 not in self.results.groups) + + def test_make_ref(self): + g = self.results.groups[0] + d = g.dupes[0] + self.results.make_ref(d) + self.assert_(d is g.ref) + + def test_sort_groups(self): + self.results.make_ref(self.objects[1]) #We want to make the 1024 sized object to go ref. + g1,g2 = self.groups + self.results.sort_groups(2) #2 is the key for size + self.assert_(self.results.groups[0] is g2) + self.assert_(self.results.groups[1] is g1) + self.results.sort_groups(2,False) + self.assert_(self.results.groups[0] is g1) + self.assert_(self.results.groups[1] is g2) + + def test_set_groups_when_sorted(self): + self.results.make_ref(self.objects[1]) #We want to make the 1024 sized object to go ref. + self.results.sort_groups(2) + objects,matches,groups = GetTestGroups() + g1,g2 = groups + g1.switch_ref(objects[1]) + self.results.groups = groups + self.assert_(self.results.groups[0] is g2) + self.assert_(self.results.groups[1] is g1) + + def test_get_dupe_list(self): + self.assertEqual([self.objects[1],self.objects[2],self.objects[4]],self.results.dupes) + + def test_dupe_list_is_cached(self): + self.assert_(self.results.dupes is self.results.dupes) + + def test_dupe_list_cache_is_invalidated_when_needed(self): + o1,o2,o3,o4,o5 = self.objects + self.assertEqual([o2,o3,o5],self.results.dupes) + self.results.make_ref(o2) + self.assertEqual([o1,o3,o5],self.results.dupes) + objects,matches,groups = GetTestGroups() + o1,o2,o3,o4,o5 = objects + self.results.groups = groups + self.assertEqual([o2,o3,o5],self.results.dupes) + + def test_dupe_list_sort(self): + o1,o2,o3,o4,o5 = self.objects + o1.size = 5 + o2.size = 4 + o3.size = 3 + o4.size = 2 + o5.size = 1 + self.results.sort_dupes(2) + self.assertEqual([o5,o3,o2],self.results.dupes) + self.results.sort_dupes(2,False) + self.assertEqual([o2,o3,o5],self.results.dupes) + + def test_dupe_list_remember_sort(self): + o1,o2,o3,o4,o5 = self.objects + o1.size = 5 + o2.size = 4 + o3.size = 3 + o4.size = 2 + o5.size = 1 + self.results.sort_dupes(2) + self.results.make_ref(o2) + self.assertEqual([o5,o3,o1],self.results.dupes) + + def test_dupe_list_sort_delta_values(self): + o1,o2,o3,o4,o5 = self.objects + o1.size = 10 + o2.size = 2 #-8 + o3.size = 3 #-7 + o4.size = 20 + o5.size = 1 #-19 + self.results.sort_dupes(2,delta=True) + self.assertEqual([o5,o2,o3],self.results.dupes) + + def test_sort_empty_list(self): + #There was an infinite loop when sorting an empty list. + r = Results(data) + r.sort_dupes(0) + self.assertEqual([],r.dupes) + + def test_dupe_list_update_on_remove_duplicates(self): + o1,o2,o3,o4,o5 = self.objects + self.assertEqual(3,len(self.results.dupes)) + self.results.remove_duplicates([o2]) + self.assertEqual(2,len(self.results.dupes)) + + +class TCResultsMarkings(TestCase): + def setUp(self): + self.results = Results(data) + self.objects,self.matches,self.groups = GetTestGroups() + self.results.groups = self.groups + + def test_stat_line(self): + self.assertEqual("0 / 3 (0.00 B / 1.01 KB) duplicates marked.",self.results.stat_line) + self.results.mark(self.objects[1]) + self.assertEqual("1 / 3 (1.00 KB / 1.01 KB) duplicates marked.",self.results.stat_line) + self.results.mark_invert() + self.assertEqual("2 / 3 (2.00 B / 1.01 KB) duplicates marked.",self.results.stat_line) + self.results.mark_invert() + self.results.unmark(self.objects[1]) + self.results.mark(self.objects[2]) + self.results.mark(self.objects[4]) + self.assertEqual("2 / 3 (2.00 B / 1.01 KB) duplicates marked.",self.results.stat_line) + self.results.mark(self.objects[0]) #this is a ref, it can't be counted + self.assertEqual("2 / 3 (2.00 B / 1.01 KB) duplicates marked.",self.results.stat_line) + self.results.groups = self.groups + self.assertEqual("0 / 3 (0.00 B / 1.01 KB) duplicates marked.",self.results.stat_line) + + def test_with_ref_duplicate(self): + self.objects[1].is_ref = True + self.results.groups = self.groups + self.assert_(not self.results.mark(self.objects[1])) + self.results.mark(self.objects[2]) + self.assertEqual("1 / 2 (1.00 B / 2.00 B) duplicates marked.",self.results.stat_line) + + def test_perform_on_marked(self): + def log_object(o): + log.append(o) + return True + + log = [] + self.results.mark_all() + self.results.perform_on_marked(log_object,False) + self.assert_(self.objects[1] in log) + self.assert_(self.objects[2] in log) + self.assert_(self.objects[4] in log) + self.assertEqual(3,len(log)) + log = [] + self.results.mark_none() + self.results.mark(self.objects[4]) + self.results.perform_on_marked(log_object,True) + self.assertEqual(1,len(log)) + self.assert_(self.objects[4] in log) + self.assertEqual(1,len(self.results.groups)) + + def test_perform_on_marked_with_problems(self): + def log_object(o): + log.append(o) + return o is not self.objects[1] + + log = [] + self.results.mark_all() + self.assert_(self.results.is_marked(self.objects[1])) + self.assertEqual(1,self.results.perform_on_marked(log_object, True)) + self.assertEqual(3,len(log)) + self.assertEqual(1,len(self.results.groups)) + self.assertEqual(2,len(self.results.groups[0])) + self.assert_(self.objects[1] in self.results.groups[0]) + self.assert_(not self.results.is_marked(self.objects[2])) + self.assert_(self.results.is_marked(self.objects[1])) + + def test_perform_on_marked_with_ref(self): + def log_object(o): + log.append(o) + return True + + log = [] + self.objects[0].is_ref = True + self.objects[1].is_ref = True + self.results.mark_all() + self.results.perform_on_marked(log_object,True) + self.assert_(self.objects[1] not in log) + self.assert_(self.objects[2] in log) + self.assert_(self.objects[4] in log) + self.assertEqual(2,len(log)) + self.assertEqual(0,len(self.results.groups)) + + def test_perform_on_marked_remove_objects_only_at_the_end(self): + def check_groups(o): + self.assertEqual(3,len(g1)) + self.assertEqual(2,len(g2)) + return True + + g1,g2 = self.results.groups + self.results.mark_all() + self.results.perform_on_marked(check_groups,True) + self.assertEqual(0,len(g1)) + self.assertEqual(0,len(g2)) + self.assertEqual(0,len(self.results.groups)) + + def test_remove_duplicates(self): + g1 = self.results.groups[0] + g2 = self.results.groups[1] + self.results.mark(g1.dupes[0]) + self.assertEqual("1 / 3 (1.00 KB / 1.01 KB) duplicates marked.",self.results.stat_line) + self.results.remove_duplicates([g1.dupes[1]]) + self.assertEqual("1 / 2 (1.00 KB / 1.01 KB) duplicates marked.",self.results.stat_line) + self.results.remove_duplicates([g1.dupes[0]]) + self.assertEqual("0 / 1 (0.00 B / 1.00 B) duplicates marked.",self.results.stat_line) + + def test_make_ref(self): + g = self.results.groups[0] + d = g.dupes[0] + self.results.mark(d) + self.assertEqual("1 / 3 (1.00 KB / 1.01 KB) duplicates marked.",self.results.stat_line) + self.results.make_ref(d) + self.assertEqual("0 / 3 (0.00 B / 3.00 B) duplicates marked.",self.results.stat_line) + self.results.make_ref(d) + self.assertEqual("0 / 3 (0.00 B / 3.00 B) duplicates marked.",self.results.stat_line) + + def test_SaveXML(self): + self.results.mark(self.objects[1]) + self.results.mark_invert() + f = StringIO.StringIO() + self.results.save_to_xml(f) + f.seek(0) + doc = xml.dom.minidom.parse(f) + root = doc.documentElement + g1,g2 = root.getElementsByTagName('group') + d1,d2,d3 = g1.getElementsByTagName('file') + self.assertEqual('n',d1.getAttributeNode('marked').nodeValue) + self.assertEqual('n',d2.getAttributeNode('marked').nodeValue) + self.assertEqual('y',d3.getAttributeNode('marked').nodeValue) + d1,d2 = g2.getElementsByTagName('file') + self.assertEqual('n',d1.getAttributeNode('marked').nodeValue) + self.assertEqual('y',d2.getAttributeNode('marked').nodeValue) + + def test_LoadXML(self): + def get_file(path): + return [f for f in self.objects if str(f.path) == path][0] + + self.objects[4].name = 'ibabtu 2' #we can't have 2 files with the same path + self.results.mark(self.objects[1]) + self.results.mark_invert() + f = StringIO.StringIO() + self.results.save_to_xml(f) + f.seek(0) + r = Results(data) + r.load_from_xml(f,get_file) + self.assert_(not r.is_marked(self.objects[0])) + self.assert_(not r.is_marked(self.objects[1])) + self.assert_(r.is_marked(self.objects[2])) + self.assert_(not r.is_marked(self.objects[3])) + self.assert_(r.is_marked(self.objects[4])) + + +class TCResultsXML(TestCase): + def setUp(self): + self.results = Results(data) + self.objects, self.matches, self.groups = GetTestGroups() + self.results.groups = self.groups + + def get_file(self, path): # use this as a callback for load_from_xml + return [o for o in self.objects if o.path == path][0] + + def test_save_to_xml(self): + self.objects[0].is_ref = True + self.objects[0].words = [['foo','bar']] + f = StringIO.StringIO() + self.results.save_to_xml(f) + f.seek(0) + doc = xml.dom.minidom.parse(f) + root = doc.documentElement + self.assertEqual('results',root.nodeName) + children = [c for c in root.childNodes if c.localName] + self.assertEqual(2,len(children)) + self.assertEqual(2,len([c for c in children if c.nodeName == 'group'])) + g1,g2 = children + children = [c for c in g1.childNodes if c.localName] + self.assertEqual(6,len(children)) + self.assertEqual(3,len([c for c in children if c.nodeName == 'file'])) + self.assertEqual(3,len([c for c in children if c.nodeName == 'match'])) + d1,d2,d3 = [c for c in children if c.nodeName == 'file'] + self.assertEqual(op.join('basepath','foo bar'),d1.getAttributeNode('path').nodeValue) + self.assertEqual(op.join('basepath','bar bleh'),d2.getAttributeNode('path').nodeValue) + self.assertEqual(op.join('basepath','foo bleh'),d3.getAttributeNode('path').nodeValue) + self.assertEqual('y',d1.getAttributeNode('is_ref').nodeValue) + self.assertEqual('n',d2.getAttributeNode('is_ref').nodeValue) + self.assertEqual('n',d3.getAttributeNode('is_ref').nodeValue) + self.assertEqual('foo,bar',d1.getAttributeNode('words').nodeValue) + self.assertEqual('bar,bleh',d2.getAttributeNode('words').nodeValue) + self.assertEqual('foo,bleh',d3.getAttributeNode('words').nodeValue) + children = [c for c in g2.childNodes if c.localName] + self.assertEqual(3,len(children)) + self.assertEqual(2,len([c for c in children if c.nodeName == 'file'])) + self.assertEqual(1,len([c for c in children if c.nodeName == 'match'])) + d1,d2 = [c for c in children if c.nodeName == 'file'] + self.assertEqual(op.join('basepath','ibabtu'),d1.getAttributeNode('path').nodeValue) + self.assertEqual(op.join('basepath','ibabtu'),d2.getAttributeNode('path').nodeValue) + self.assertEqual('n',d1.getAttributeNode('is_ref').nodeValue) + self.assertEqual('n',d2.getAttributeNode('is_ref').nodeValue) + self.assertEqual('ibabtu',d1.getAttributeNode('words').nodeValue) + self.assertEqual('ibabtu',d2.getAttributeNode('words').nodeValue) + + def test_save_to_xml_with_columns(self): + class FakeDataModule: + def GetDisplayInfo(self,dupe,group): + return [str(dupe.size),dupe.foo.upper()] + + for i,object in enumerate(self.objects): + object.size = i + object.foo = u'bar\u00e9' + f = StringIO.StringIO() + self.results.data = FakeDataModule() + self.results.save_to_xml(f,True) + f.seek(0) + doc = xml.dom.minidom.parse(f) + root = doc.documentElement + g1,g2 = root.getElementsByTagName('group') + d1,d2,d3 = g1.getElementsByTagName('file') + d4,d5 = g2.getElementsByTagName('file') + self.assertEqual('0',d1.getElementsByTagName('data')[0].getAttribute('value')) + self.assertEqual(u'BAR\u00c9',d1.getElementsByTagName('data')[1].getAttribute('value')) #\u00c9 is upper of \u00e9 + self.assertEqual('1',d2.getElementsByTagName('data')[0].getAttribute('value')) + self.assertEqual('2',d3.getElementsByTagName('data')[0].getAttribute('value')) + self.assertEqual('3',d4.getElementsByTagName('data')[0].getAttribute('value')) + self.assertEqual('4',d5.getElementsByTagName('data')[0].getAttribute('value')) + + def test_LoadXML(self): + def get_file(path): + return [f for f in self.objects if str(f.path) == path][0] + + self.objects[0].is_ref = True + self.objects[4].name = 'ibabtu 2' #we can't have 2 files with the same path + f = StringIO.StringIO() + self.results.save_to_xml(f) + f.seek(0) + r = Results(data) + r.load_from_xml(f,get_file) + self.assertEqual(2,len(r.groups)) + g1,g2 = r.groups + self.assertEqual(3,len(g1)) + self.assert_(g1[0].is_ref) + self.assert_(not g1[1].is_ref) + self.assert_(not g1[2].is_ref) + self.assert_(g1[0] is self.objects[0]) + self.assert_(g1[1] is self.objects[1]) + self.assert_(g1[2] is self.objects[2]) + self.assertEqual(['foo','bar'],g1[0].words) + self.assertEqual(['bar','bleh'],g1[1].words) + self.assertEqual(['foo','bleh'],g1[2].words) + self.assertEqual(2,len(g2)) + self.assert_(not g2[0].is_ref) + self.assert_(not g2[1].is_ref) + self.assert_(g2[0] is self.objects[3]) + self.assert_(g2[1] is self.objects[4]) + self.assertEqual(['ibabtu'],g2[0].words) + self.assertEqual(['ibabtu'],g2[1].words) + + def test_LoadXML_with_filename(self): + def get_file(path): + return [f for f in self.objects if str(f.path) == path][0] + + filename = op.join(self.tmpdir(), 'dupeguru_results.xml') + self.objects[4].name = 'ibabtu 2' #we can't have 2 files with the same path + self.results.save_to_xml(filename) + r = Results(data) + r.load_from_xml(filename,get_file) + self.assertEqual(2,len(r.groups)) + + def test_LoadXML_with_some_files_that_dont_exist_anymore(self): + def get_file(path): + if path.endswith('ibabtu 2'): + return None + return [f for f in self.objects if str(f.path) == path][0] + + self.objects[4].name = 'ibabtu 2' #we can't have 2 files with the same path + f = StringIO.StringIO() + self.results.save_to_xml(f) + f.seek(0) + r = Results(data) + r.load_from_xml(f,get_file) + self.assertEqual(1,len(r.groups)) + self.assertEqual(3,len(r.groups[0])) + + def test_LoadXML_missing_attributes_and_bogus_elements(self): + def get_file(path): + return [f for f in self.objects if str(f.path) == path][0] + + doc = xml.dom.minidom.Document() + root = doc.appendChild(doc.createElement('foobar')) #The root element shouldn't matter, really. + group_node = root.appendChild(doc.createElement('group')) + dupe_node = group_node.appendChild(doc.createElement('file')) #Perfectly correct file + dupe_node.setAttribute('path',op.join('basepath','foo bar')) + dupe_node.setAttribute('is_ref','y') + dupe_node.setAttribute('words','foo,bar') + dupe_node = group_node.appendChild(doc.createElement('file')) #is_ref missing, default to 'n' + dupe_node.setAttribute('path',op.join('basepath','foo bleh')) + dupe_node.setAttribute('words','foo,bleh') + dupe_node = group_node.appendChild(doc.createElement('file')) #words are missing, invalid. + dupe_node.setAttribute('path',op.join('basepath','bar bleh')) + dupe_node = group_node.appendChild(doc.createElement('file')) #path is missing, invalid. + dupe_node.setAttribute('words','foo,bleh') + dupe_node = group_node.appendChild(doc.createElement('foobar')) #Invalid element name + dupe_node.setAttribute('path',op.join('basepath','bar bleh')) + dupe_node.setAttribute('is_ref','y') + dupe_node.setAttribute('words','bar,bleh') + match_node = group_node.appendChild(doc.createElement('match')) # match pointing to a bad index + match_node.setAttribute('first', '42') + match_node.setAttribute('second', '45') + match_node = group_node.appendChild(doc.createElement('match')) # match with missing attrs + match_node = group_node.appendChild(doc.createElement('match')) # match with non-int values + match_node.setAttribute('first', 'foo') + match_node.setAttribute('second', 'bar') + match_node.setAttribute('percentage', 'baz') + group_node = root.appendChild(doc.createElement('foobar')) #invalid group + group_node = root.appendChild(doc.createElement('group')) #empty group + f = StringIO.StringIO() + doc.writexml(f,'\t','\t','\n',encoding='utf-8') + f.seek(0) + r = Results(data) + r.load_from_xml(f,get_file) + self.assertEqual(1,len(r.groups)) + self.assertEqual(2,len(r.groups[0])) + + def test_xml_non_ascii(self): + def get_file(path): + if path == op.join('basepath',u'\xe9foo bar'): + return objects[0] + if path == op.join('basepath',u'bar bleh'): + return objects[1] + + objects = [NamedObject(u"\xe9foo bar",True),NamedObject("bar bleh",True)] + matches = engine.MatchFactory().getmatches(objects) #we should have 5 matches + groups = engine.get_groups(matches) #We should have 2 groups + for g in groups: + g.prioritize(lambda x:objects.index(x)) #We want the dupes to be in the same order as the list is + results = Results(data) + results.groups = groups + f = StringIO.StringIO() + results.save_to_xml(f) + f.seek(0) + r = Results(data) + r.load_from_xml(f,get_file) + g = r.groups[0] + self.assertEqual(u"\xe9foo bar",g[0].name) + self.assertEqual(['efoo','bar'],g[0].words) + + def test_load_invalid_xml(self): + f = StringIO.StringIO() + f.write(' len(ref.path) + + def GetDupeGroups(self, files, j=job.nulljob): + j = j.start_subjob([8, 2]) + for f in [f for f in files if not hasattr(f, 'is_ref')]: + f.is_ref = False + if self.size_threshold: + files = [f for f in files if f.size >= self.size_threshold] + logging.info('Getting matches') + if self.match_factory is None: + matches = self._getmatches(files, j) + else: + matches = self.match_factory.getmatches(files, j) + logging.info('Found %d matches' % len(matches)) + if not self.mix_file_kind: + j.set_progress(100, 'Removing false matches') + matches = [m for m in matches if get_file_ext(m.first.name) == get_file_ext(m.second.name)] + if self.ignore_list: + j = j.start_subjob(2) + iter_matches = j.iter_with_progress(matches, 'Processed %d/%d matches against the ignore list') + matches = [m for m in iter_matches + if not self.ignore_list.AreIgnored(unicode(m.first.path), unicode(m.second.path))] + matched_files = dedupe([m.first for m in matches] + [m.second for m in matches]) + if self.scan_type in (SCAN_TYPE_CONTENT, SCAN_TYPE_CONTENT_AUDIO): + md5attrname = 'md5partial' if self.scan_type == SCAN_TYPE_CONTENT_AUDIO else 'md5' + md5 = lambda f: getattr(f, md5attrname) + j = j.start_subjob(2) + for matched_file in j.iter_with_progress(matched_files, 'Analyzed %d/%d matching files'): + md5(matched_file) + j.set_progress(100, 'Removing false matches') + matches = [m for m in matches if md5(m.first) == md5(m.second)] + words_for_content = ['--'] # We compared md5. No words were involved. + for m in matches: + m.first.words = words_for_content + m.second.words = words_for_content + logging.info('Grouping matches') + groups = engine.get_groups(matches, j) + groups = [g for g in groups if any(not f.is_ref for f in g)] + logging.info('Created %d groups' % len(groups)) + j.set_progress(100, 'Doing group prioritization') + for g in groups: + g.prioritize(self._key_func, self._tie_breaker) + matched_files = dedupe([m.first for m in matches] + [m.second for m in matches]) + self.discarded_file_count = len(matched_files) - sum(len(g) for g in groups) + return groups + + match_factory = None + match_similar_words = False + min_match_percentage = 80 + mix_file_kind = True + scan_type = SCAN_TYPE_FILENAME + scanned_tags = set(['artist', 'title']) + size_threshold = 0 + word_weighting = False + +class ScannerME(Scanner): # Scanner for Music Edition + @staticmethod + def _key_func(dupe): + return (not dupe.is_ref, -dupe.bitrate, -dupe.size) + diff --git a/py/scanner_test.py b/py/scanner_test.py new file mode 100644 index 00000000..89ad1417 --- /dev/null +++ b/py/scanner_test.py @@ -0,0 +1,468 @@ +#!/usr/bin/env python +""" +Unit Name: dupeguru.tests.scanner +Created By: Virgil Dupras +Created On: 2006/03/03 +Last modified by:$Author: virgil $ +Last modified on:$Date: 2009-05-28 15:22:39 +0200 (Thu, 28 May 2009) $ + $Revision: 4385 $ +Copyright 2004-2006 Hardcoded Software (http://www.hardcoded.net) +""" +import unittest + +from hsutil import job +from hsutil.path import Path +from hsutil.testcase import TestCase + +from .engine import getwords, Match +from .ignore import IgnoreList +from .scanner import * + +class NamedObject(object): + def __init__(self, name="foobar", size=1): + self.name = name + self.size = size + self.path = Path('') + self.words = getwords(name) + + +no = NamedObject + +class TCScanner(TestCase): + def test_empty(self): + s = Scanner() + r = s.GetDupeGroups([]) + self.assertEqual([],r) + + def test_default_settings(self): + s = Scanner() + self.assertEqual(80,s.min_match_percentage) + self.assertEqual(SCAN_TYPE_FILENAME,s.scan_type) + self.assertEqual(True,s.mix_file_kind) + self.assertEqual(False,s.word_weighting) + self.assertEqual(False,s.match_similar_words) + self.assert_(isinstance(s.ignore_list,IgnoreList)) + + def test_simple_with_default_settings(self): + s = Scanner() + f = [no('foo bar'),no('foo bar'),no('foo bleh')] + r = s.GetDupeGroups(f) + self.assertEqual(1,len(r)) + g = r[0] + #'foo bleh' cannot be in the group because the default min match % is 80 + self.assertEqual(2,len(g)) + self.assert_(g.ref in f[:2]) + self.assert_(g.dupes[0] in f[:2]) + + def test_simple_with_lower_min_match(self): + s = Scanner() + s.min_match_percentage = 50 + f = [no('foo bar'),no('foo bar'),no('foo bleh')] + r = s.GetDupeGroups(f) + self.assertEqual(1,len(r)) + g = r[0] + self.assertEqual(3,len(g)) + + def test_trim_all_ref_groups(self): + s = Scanner() + f = [no('foo'),no('foo'),no('bar'),no('bar')] + f[2].is_ref = True + f[3].is_ref = True + r = s.GetDupeGroups(f) + self.assertEqual(1,len(r)) + + def test_priorize(self): + s = Scanner() + f = [no('foo'),no('foo'),no('bar'),no('bar')] + f[1].size = 2 + f[2].size = 3 + f[3].is_ref = True + r = s.GetDupeGroups(f) + g1,g2 = r + self.assert_(f[1] in (g1.ref,g2.ref)) + self.assert_(f[0] in (g1.dupes[0],g2.dupes[0])) + self.assert_(f[3] in (g1.ref,g2.ref)) + self.assert_(f[2] in (g1.dupes[0],g2.dupes[0])) + + def test_content_scan(self): + s = Scanner() + s.scan_type = SCAN_TYPE_CONTENT + f = [no('foo'), no('bar'), no('bleh')] + f[0].md5 = 'foobar' + f[1].md5 = 'foobar' + f[2].md5 = 'bleh' + r = s.GetDupeGroups(f) + self.assertEqual(len(r), 1) + self.assertEqual(len(r[0]), 2) + self.assertEqual(s.discarded_file_count, 0) # don't count the different md5 as discarded! + + def test_content_scan_compare_sizes_first(self): + class MyFile(no): + def get_md5(file): + self.fail() + md5 = property(get_md5) + + s = Scanner() + s.scan_type = SCAN_TYPE_CONTENT + f = [MyFile('foo',1),MyFile('bar',2)] + self.assertEqual(0,len(s.GetDupeGroups(f))) + + def test_min_match_perc_doesnt_matter_for_content_scan(self): + s = Scanner() + s.scan_type = SCAN_TYPE_CONTENT + f = [no('foo'),no('bar'),no('bleh')] + f[0].md5 = 'foobar' + f[1].md5 = 'foobar' + f[2].md5 = 'bleh' + s.min_match_percentage = 101 + r = s.GetDupeGroups(f) + self.assertEqual(1,len(r)) + self.assertEqual(2,len(r[0])) + s.min_match_percentage = 0 + r = s.GetDupeGroups(f) + self.assertEqual(1,len(r)) + self.assertEqual(2,len(r[0])) + + def test_content_scan_puts_md5_in_words_at_the_end(self): + s = Scanner() + s.scan_type = SCAN_TYPE_CONTENT + f = [no('foo'),no('bar')] + f[0].md5 = '\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f' + f[1].md5 = '\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f' + r = s.GetDupeGroups(f) + g = r[0] + self.assertEqual(['--'],g.ref.words) + self.assertEqual(['--'],g.dupes[0].words) + + def test_extension_is_not_counted_in_filename_scan(self): + s = Scanner() + s.min_match_percentage = 100 + f = [no('foo.bar'),no('foo.bleh')] + r = s.GetDupeGroups(f) + self.assertEqual(1,len(r)) + self.assertEqual(2,len(r[0])) + + def test_job(self): + def do_progress(progress,desc=''): + log.append(progress) + return True + s = Scanner() + log = [] + f = [no('foo bar'),no('foo bar'),no('foo bleh')] + r = s.GetDupeGroups(f, job.Job(1,do_progress)) + self.assertEqual(0,log[0]) + self.assertEqual(100,log[-1]) + + def test_mix_file_kind(self): + s = Scanner() + s.mix_file_kind = False + f = [no('foo.1'),no('foo.2')] + r = s.GetDupeGroups(f) + self.assertEqual(0,len(r)) + + def test_word_weighting(self): + s = Scanner() + s.min_match_percentage = 75 + s.word_weighting = True + f = [no('foo bar'),no('foo bar bleh')] + r = s.GetDupeGroups(f) + self.assertEqual(1,len(r)) + g = r[0] + m = g.get_match_of(g.dupes[0]) + self.assertEqual(75,m.percentage) # 16 letters, 12 matching + + def test_similar_words(self): + s = Scanner() + s.match_similar_words = True + f = [no('The White Stripes'),no('The Whites Stripe'),no('Limp Bizkit'),no('Limp Bizkitt')] + r = s.GetDupeGroups(f) + self.assertEqual(2,len(r)) + + def test_fields(self): + s = Scanner() + s.scan_type = SCAN_TYPE_FIELDS + f = [no('The White Stripes - Little Ghost'),no('The White Stripes - Little Acorn')] + r = s.GetDupeGroups(f) + self.assertEqual(0,len(r)) + + def test_fields_no_order(self): + s = Scanner() + s.scan_type = SCAN_TYPE_FIELDS_NO_ORDER + f = [no('The White Stripes - Little Ghost'),no('Little Ghost - The White Stripes')] + r = s.GetDupeGroups(f) + self.assertEqual(1,len(r)) + + def test_tag_scan(self): + s = Scanner() + s.scan_type = SCAN_TYPE_TAG + o1 = no('foo') + o2 = no('bar') + o1.artist = 'The White Stripes' + o1.title = 'The Air Near My Fingers' + o2.artist = 'The White Stripes' + o2.title = 'The Air Near My Fingers' + r = s.GetDupeGroups([o1,o2]) + self.assertEqual(1,len(r)) + + def test_tag_with_album_scan(self): + s = Scanner() + s.scan_type = SCAN_TYPE_TAG_WITH_ALBUM + o1 = no('foo') + o2 = no('bar') + o3 = no('bleh') + o1.artist = 'The White Stripes' + o1.title = 'The Air Near My Fingers' + o1.album = 'Elephant' + o2.artist = 'The White Stripes' + o2.title = 'The Air Near My Fingers' + o2.album = 'Elephant' + o3.artist = 'The White Stripes' + o3.title = 'The Air Near My Fingers' + o3.album = 'foobar' + r = s.GetDupeGroups([o1,o2,o3]) + self.assertEqual(1,len(r)) + + def test_that_dash_in_tags_dont_create_new_fields(self): + s = Scanner() + s.scan_type = SCAN_TYPE_TAG_WITH_ALBUM + s.min_match_percentage = 50 + o1 = no('foo') + o2 = no('bar') + o1.artist = 'The White Stripes - a' + o1.title = 'The Air Near My Fingers - a' + o1.album = 'Elephant - a' + o2.artist = 'The White Stripes - b' + o2.title = 'The Air Near My Fingers - b' + o2.album = 'Elephant - b' + r = s.GetDupeGroups([o1,o2]) + self.assertEqual(1,len(r)) + + def test_tag_scan_with_different_scanned(self): + s = Scanner() + s.scan_type = SCAN_TYPE_TAG + s.scanned_tags = set(['track', 'year']) + o1 = no('foo') + o2 = no('bar') + o1.artist = 'The White Stripes' + o1.title = 'some title' + o1.track = 'foo' + o1.year = 'bar' + o2.artist = 'The White Stripes' + o2.title = 'another title' + o2.track = 'foo' + o2.year = 'bar' + r = s.GetDupeGroups([o1, o2]) + self.assertEqual(1, len(r)) + + def test_tag_scan_only_scans_existing_tags(self): + s = Scanner() + s.scan_type = SCAN_TYPE_TAG + s.scanned_tags = set(['artist', 'foo']) + o1 = no('foo') + o2 = no('bar') + o1.artist = 'The White Stripes' + o1.foo = 'foo' + o2.artist = 'The White Stripes' + o2.foo = 'bar' + r = s.GetDupeGroups([o1, o2]) + self.assertEqual(1, len(r)) # Because 'foo' is not scanned, they match + + def test_tag_scan_converts_to_str(self): + s = Scanner() + s.scan_type = SCAN_TYPE_TAG + s.scanned_tags = set(['track']) + o1 = no('foo') + o2 = no('bar') + o1.track = 42 + o2.track = 42 + try: + r = s.GetDupeGroups([o1, o2]) + except TypeError: + self.fail() + self.assertEqual(1, len(r)) + + def test_tag_scan_non_ascii(self): + s = Scanner() + s.scan_type = SCAN_TYPE_TAG + s.scanned_tags = set(['title']) + o1 = no('foo') + o2 = no('bar') + o1.title = u'foobar\u00e9' + o2.title = u'foobar\u00e9' + try: + r = s.GetDupeGroups([o1, o2]) + except UnicodeEncodeError: + self.fail() + self.assertEqual(1, len(r)) + + def test_audio_content_scan(self): + s = Scanner() + s.scan_type = SCAN_TYPE_CONTENT_AUDIO + f = [no('foo'),no('bar'),no('bleh')] + f[0].md5 = 'foo' + f[1].md5 = 'bar' + f[2].md5 = 'bleh' + f[0].md5partial = 'foo' + f[1].md5partial = 'foo' + f[2].md5partial = 'bleh' + f[0].audiosize = 1 + f[1].audiosize = 1 + f[2].audiosize = 1 + r = s.GetDupeGroups(f) + self.assertEqual(1,len(r)) + self.assertEqual(2,len(r[0])) + + def test_audio_content_scan_compare_sizes_first(self): + class MyFile(no): + def get_md5(file): + self.fail() + md5partial = property(get_md5) + + s = Scanner() + s.scan_type = SCAN_TYPE_CONTENT_AUDIO + f = [MyFile('foo'),MyFile('bar')] + f[0].audiosize = 1 + f[1].audiosize = 2 + self.assertEqual(0,len(s.GetDupeGroups(f))) + + def test_ignore_list(self): + s = Scanner() + f1 = no('foobar') + f2 = no('foobar') + f3 = no('foobar') + f1.path = Path('dir1/foobar') + f2.path = Path('dir2/foobar') + f3.path = Path('dir3/foobar') + s.ignore_list.Ignore(str(f1.path),str(f2.path)) + s.ignore_list.Ignore(str(f1.path),str(f3.path)) + r = s.GetDupeGroups([f1,f2,f3]) + self.assertEqual(1,len(r)) + g = r[0] + self.assertEqual(1,len(g.dupes)) + self.assert_(f1 not in g) + self.assert_(f2 in g) + self.assert_(f3 in g) + # Ignored matches are not counted as discarded + self.assertEqual(s.discarded_file_count, 0) + + def test_ignore_list_checks_for_unicode(self): + #scanner was calling path_str for ignore list checks. Since the Path changes, it must + #be unicode(path) + s = Scanner() + f1 = no('foobar') + f2 = no('foobar') + f3 = no('foobar') + f1.path = Path(u'foo1\u00e9') + f2.path = Path(u'foo2\u00e9') + f3.path = Path(u'foo3\u00e9') + s.ignore_list.Ignore(unicode(f1.path),unicode(f2.path)) + s.ignore_list.Ignore(unicode(f1.path),unicode(f3.path)) + r = s.GetDupeGroups([f1,f2,f3]) + self.assertEqual(1,len(r)) + g = r[0] + self.assertEqual(1,len(g.dupes)) + self.assert_(f1 not in g) + self.assert_(f2 in g) + self.assert_(f3 in g) + + def test_custom_match_factory(self): + class MatchFactory(object): + def getmatches(self,objects,j=None): + return [Match(objects[0], objects[1], 420)] + + + s = Scanner() + s.match_factory = MatchFactory() + o1,o2 = no('foo'),no('bar') + groups = s.GetDupeGroups([o1,o2]) + self.assertEqual(1,len(groups)) + g = groups[0] + self.assertEqual(2,len(g)) + g.switch_ref(o1) + m = g.get_match_of(o2) + self.assertEqual((o1,o2,420),m) + + def test_file_evaluates_to_false(self): + # A very wrong way to use any() was added at some point, causing resulting group list + # to be empty. + class FalseNamedObject(NamedObject): + def __nonzero__(self): + return False + + + s = Scanner() + f1 = FalseNamedObject('foobar') + f2 = FalseNamedObject('foobar') + r = s.GetDupeGroups([f1,f2]) + self.assertEqual(1,len(r)) + + def test_size_threshold(self): + # Only file equal or higher than the size_threshold in size are scanned + s = Scanner() + f1 = no('foo', 1) + f2 = no('foo', 2) + f3 = no('foo', 3) + s.size_threshold = 2 + groups = s.GetDupeGroups([f1,f2,f3]) + self.assertEqual(len(groups), 1) + [group] = groups + self.assertEqual(len(group), 2) + self.assertTrue(f1 not in group) + self.assertTrue(f2 in group) + self.assertTrue(f3 in group) + + def test_tie_breaker_path_deepness(self): + # If there is a tie in prioritization, path deepness is used as a tie breaker + s = Scanner() + o1, o2 = no('foo'), no('foo') + o1.path = Path('foo') + o2.path = Path('foo/bar') + [group] = s.GetDupeGroups([o1, o2]) + self.assertTrue(group.ref is o2) + + def test_tie_breaker_copy(self): + # if copy is in the words used (even if it has a deeper path), it becomes a dupe + s = Scanner() + o1, o2 = no('foo bar Copy'), no('foo bar') + o1.path = Path('deeper/path') + o2.path = Path('foo') + [group] = s.GetDupeGroups([o1, o2]) + self.assertTrue(group.ref is o2) + + def test_tie_breaker_same_name_plus_digit(self): + # if ref has the same words as dupe, but has some just one extra word which is a digit, it + # becomes a dupe + s = Scanner() + o1, o2 = no('foo bar 42'), no('foo bar') + o1.path = Path('deeper/path') + o2.path = Path('foo') + [group] = s.GetDupeGroups([o1, o2]) + self.assertTrue(group.ref is o2) + + def test_partial_group_match(self): + # Count the number od discarded matches (when a file doesn't match all other dupes of the + # group) in Scanner.discarded_file_count + s = Scanner() + o1, o2, o3 = no('a b'), no('a'), no('b') + s.min_match_percentage = 50 + [group] = s.GetDupeGroups([o1, o2, o3]) + self.assertEqual(len(group), 2) + self.assertTrue(o1 in group) + self.assertTrue(o2 in group) + self.assertTrue(o3 not in group) + self.assertEqual(s.discarded_file_count, 1) + + +class TCScannerME(TestCase): + def test_priorize(self): + # in ScannerME, bitrate goes first (right after is_ref) in priorization + s = ScannerME() + o1, o2 = no('foo'), no('foo') + o1.bitrate = 1 + o2.bitrate = 2 + [group] = s.GetDupeGroups([o1, o2]) + self.assertTrue(group.ref is o2) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/se/cocoa/AppDelegate.h b/se/cocoa/AppDelegate.h new file mode 100644 index 00000000..77d3b57f --- /dev/null +++ b/se/cocoa/AppDelegate.h @@ -0,0 +1,18 @@ +#import +#import "dgbase/AppDelegate.h" +#import "ResultWindow.h" +#import "DirectoryPanel.h" +#import "PyDupeGuru.h" + +@interface AppDelegate : AppDelegateBase +{ + IBOutlet ResultWindow *result; + + DirectoryPanel *_directoryPanel; +} +- (IBAction)openWebsite:(id)sender; +- (IBAction)toggleDirectories:(id)sender; + +- (DirectoryPanel *)directoryPanel; +- (PyDupeGuru *)py; +@end diff --git a/se/cocoa/AppDelegate.m b/se/cocoa/AppDelegate.m new file mode 100644 index 00000000..c1378a8c --- /dev/null +++ b/se/cocoa/AppDelegate.m @@ -0,0 +1,108 @@ +#import "AppDelegate.h" +#import "cocoalib/ProgressController.h" +#import "cocoalib/RegistrationInterface.h" +#import "cocoalib/Utils.h" +#import "cocoalib/ValueTransformers.h" +#import "Consts.h" + +@implementation AppDelegate ++ (void)initialize +{ + NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; + NSMutableDictionary *d = [NSMutableDictionary dictionary]; + [d setObject:i2n(1) forKey:@"scanType"]; + [d setObject:i2n(80) forKey:@"minMatchPercentage"]; + [d setObject:i2n(30) forKey:@"smallFileThreshold"]; + [d setObject:i2n(1) forKey:@"recreatePathType"]; + [d setObject:b2n(YES) forKey:@"wordWeighting"]; + [d setObject:b2n(NO) forKey:@"matchSimilarWords"]; + [d setObject:b2n(YES) forKey:@"mixFileKind"]; + [d setObject:b2n(NO) forKey:@"useRegexpFilter"]; + [d setObject:b2n(NO) forKey:@"removeEmptyFolders"]; + [d setObject:b2n(YES) forKey:@"ignoreSmallFiles"]; + [d setObject:b2n(NO) forKey:@"debug"]; + [d setObject:[NSArray array] forKey:@"recentDirectories"]; + [d setObject:[NSArray array] forKey:@"columnsOrder"]; + [d setObject:[NSDictionary dictionary] forKey:@"columnsWidth"]; + [[NSUserDefaultsController sharedUserDefaultsController] setInitialValues:d]; + [ud registerDefaults:d]; +} + +- (id)init +{ + self = [super init]; + VTIsIntIn *vt = [[[VTIsIntIn alloc] initWithValues:[NSIndexSet indexSetWithIndex:1] reverse:YES] autorelease]; + [NSValueTransformer setValueTransformer:vt forName:@"vtScanTypeIsNotContent"]; + _directoryPanel = nil; + _appName = APPNAME; + return self; +} + +- (IBAction)openWebsite:(id)sender +{ + [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"http://www.hardcoded.net/dupeguru"]]; +} + +- (IBAction)toggleDirectories:(id)sender +{ + [[self directoryPanel] toggleVisible:sender]; +} + +- (DirectoryPanel *)directoryPanel +{ + if (!_directoryPanel) + _directoryPanel = [[DirectoryPanel alloc] initWithParentApp:self]; + return _directoryPanel; +} +- (PyDupeGuru *)py { return (PyDupeGuru *)py; } + +//Delegate +- (void)applicationDidFinishLaunching:(NSNotification *)aNotification +{ + [[ProgressController mainProgressController] setWorker:py]; + NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; + //Restore Columns + NSArray *columnsOrder = [ud arrayForKey:@"columnsOrder"]; + NSDictionary *columnsWidth = [ud dictionaryForKey:@"columnsWidth"]; + if ([columnsOrder count]) + [result restoreColumnsPosition:columnsOrder widths:columnsWidth]; + //Reg stuff + if ([RegistrationInterface showNagWithApp:[self py] name:APPNAME limitDescription:LIMIT_DESC]) + [unlockMenuItem setTitle:@"Thanks for buying dupeGuru!"]; + //Restore results + [py loadIgnoreList]; + [py loadResults]; +} + +- (void)applicationWillBecomeActive:(NSNotification *)aNotification +{ + if (![[result window] isVisible]) + [result showWindow:NSApp]; +} + +- (void)applicationWillTerminate:(NSNotification *)aNotification +{ + NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; + [ud setObject: [result getColumnsOrder] forKey:@"columnsOrder"]; + [ud setObject: [result getColumnsWidth] forKey:@"columnsWidth"]; + [py saveResults]; + int sc = [ud integerForKey:@"sessionCountSinceLastIgnorePurge"]; + if (sc >= 10) + { + sc = -1; + [py purgeIgnoreList]; + } + sc++; + [ud setInteger:sc forKey:@"sessionCountSinceLastIgnorePurge"]; + [py saveIgnoreList]; + // NSApplication does not release nib instances objects, we must do it manually + // Well, it isn't needed because the memory is freed anyway (we are quitting the application + // But I need to release RecentDirectories so it saves the user defaults + [recentDirectories release]; +} + +- (void)recentDirecoryClicked:(NSString *)directory +{ + [[self directoryPanel] addDirectory:directory]; +} +@end diff --git a/se/cocoa/Consts.h b/se/cocoa/Consts.h new file mode 100644 index 00000000..b27af158 --- /dev/null +++ b/se/cocoa/Consts.h @@ -0,0 +1,3 @@ +#import "dgbase/Consts.h" + +#define APPNAME @"dupeGuru" \ No newline at end of file diff --git a/se/cocoa/DetailsPanel.h b/se/cocoa/DetailsPanel.h new file mode 100644 index 00000000..0d4c025d --- /dev/null +++ b/se/cocoa/DetailsPanel.h @@ -0,0 +1,13 @@ +#import +#import "cocoalib/PyApp.h" +#import "cocoalib/Table.h" + + +@interface DetailsPanel : NSWindowController +{ + IBOutlet TableView *detailsTable; +} +- (id)initWithPy:(PyApp *)aPy; + +- (void)refresh; +@end \ No newline at end of file diff --git a/se/cocoa/DetailsPanel.m b/se/cocoa/DetailsPanel.m new file mode 100644 index 00000000..1baac387 --- /dev/null +++ b/se/cocoa/DetailsPanel.m @@ -0,0 +1,16 @@ +#import "DetailsPanel.h" + +@implementation DetailsPanel +- (id)initWithPy:(PyApp *)aPy +{ + self = [super initWithWindowNibName:@"Details"]; + [self window]; //So the detailsTable is initialized. + [detailsTable setPy:aPy]; + return self; +} + +- (void)refresh +{ + [detailsTable reloadData]; +} +@end diff --git a/se/cocoa/DirectoryPanel.h b/se/cocoa/DirectoryPanel.h new file mode 100644 index 00000000..6f8141b9 --- /dev/null +++ b/se/cocoa/DirectoryPanel.h @@ -0,0 +1,7 @@ +#import +#import "dgbase/DirectoryPanel.h" + +@interface DirectoryPanel : DirectoryPanelBase +{ +} +@end diff --git a/se/cocoa/DirectoryPanel.m b/se/cocoa/DirectoryPanel.m new file mode 100644 index 00000000..dd07462d --- /dev/null +++ b/se/cocoa/DirectoryPanel.m @@ -0,0 +1,4 @@ +#import "DirectoryPanel.h" + +@implementation DirectoryPanel +@end diff --git a/se/cocoa/English.lproj/Details.nib/classes.nib b/se/cocoa/English.lproj/Details.nib/classes.nib new file mode 100644 index 00000000..e1b7cb92 --- /dev/null +++ b/se/cocoa/English.lproj/Details.nib/classes.nib @@ -0,0 +1,18 @@ +{ + IBClasses = ( + { + CLASS = DetailsPanel; + LANGUAGE = ObjC; + OUTLETS = {detailsTable = NSTableView; }; + SUPERCLASS = NSWindowController; + }, + {CLASS = FirstResponder; LANGUAGE = ObjC; SUPERCLASS = NSObject; }, + { + CLASS = TableView; + LANGUAGE = ObjC; + OUTLETS = {py = PyApp; }; + SUPERCLASS = NSTableView; + } + ); + IBVersion = 1; +} \ No newline at end of file diff --git a/se/cocoa/English.lproj/Details.nib/info.nib b/se/cocoa/English.lproj/Details.nib/info.nib new file mode 100644 index 00000000..3f14ee77 --- /dev/null +++ b/se/cocoa/English.lproj/Details.nib/info.nib @@ -0,0 +1,16 @@ + + + + + IBDocumentLocation + 432 54 356 240 0 0 1024 746 + IBFramework Version + 443.0 + IBOpenObjects + + 5 + + IBSystem Version + 8I127 + + diff --git a/se/cocoa/English.lproj/Details.nib/keyedobjects.nib b/se/cocoa/English.lproj/Details.nib/keyedobjects.nib new file mode 100644 index 0000000000000000000000000000000000000000..d4df1a7889bd47136abada1956a01f83a9ab97d2 GIT binary patch literal 6122 zcmai2349b~mVdAMs_LVOm~L>G4kOs z3?m-vDvHa1kbuZ?i8qRl$BsBEKZi?1L>7;A&_$GS@P>U~Rd>MA`EBRdU0+pw@4f%K z-tbRdgu^yW>fPUhkXryXDBHA=-K(y`_y*?4I zRlJq{iTYAKSd`qT0j}C~2t<(PRTK|1w*>=<0K+}XZQueoWWxx^hcR#i6hRqGfa!1x zEQByLK>}J}Is6tjz*cwyo`Lt^B)ku&;3N27I0qNtTlfxsL;<^CI`+d%ydH<(FwDaO zya~r*5tiZv^y4h7!MPZ~It*eXMzIMOV+*$7D*Qd(jrZXFxE9ypMr_Az_$cnc-M9xI z#~1KL{1d*4Z_@QG{3{;9!*~qe!FTZlzK19A13Zl%;otBq{vH2;mjzK!;iRAmY1kli z5z>VoLT}-Ep+FcZ6bhq+(KtpJBita|2(Jq_3B^L0Fn-2>NOL%RTG7A*GI&7&6*NeL zE|3mgp&N9E9?%nd!8Onu`aoaEfPRn({b2wMgll0CTnB^UdKdy(FcgNtaPS$npJMu< zIuLHw$s0W?{YCZldORMg3xz|8r6tiwg!~kZnVu>M2jX!(Ztk94>7Nn`#>so}IhFp( zz#`pP&#Uw&xA8Z>WXWkv26IgYDcMS;zdA&M!}`onBp7XpR}ZM?@;E~i289p5GentG zbeGh?6xQvZ@LvV6JVppxqOpZUQyz^ZCI=#cMm<(T{PZOW+E`ubpB{}SSUp?D7O@Bm zvM{@qMVXzoF_Gykz*?D&fg&^EFj+hYav_ftHC>);}i0I+NPyi#L z5Jt^17186f%|(QEx5FqHJ)g*%^v+?)uI5&e2%Mx~#AP0DhpXoqnyo=L2hl^unNFp2n0hDw+M zQ^7|@Db?eNP$ZBbE6=R-PYTrOVFKijHpl99=4A8Od?vgPeyD;OPz^I-7SzCOm;-Z( z&pen9wQw^8pbqLG2s$)CBP@VWt)f=?O@I0mOT+r)KzyMQc;>U>K>fl{q>-F3ITZ1S zmg$7Ud{Y%lgsBe6l)Ab0y;q-weoW65Ubx3ca$1|(S!+}N~p!|Ws zM)?m#V6&N$X~@ zd(yob(S{7a9wy0pFrz#a)(^uXvO)x+Byw0h5hI<*hDH=H+4RwGAl3mfh{I?@|CE=E zz1+^m!sOEo(JBs?$wy%cEQMvnyOol*4Q?av`P>+#FBVL%w zfX!sHOwTi@*bFO}#>zf0xUGcW!7BJYtcE|p9k2%Ogu96U-Ea^2>|Xlb2lvBTcmN)R zb?^|~t%pC7`3%O~!nF1`H|a5sfxN{5Q0{TKWc!k6ETYHw4TW%}f4m+bSS8_56MsaM z6mf~AvvHP)47v#yGjO;9CZ6HVDq za`W$`S7oP_ba)o_!E^9DIdwn0058Iy$Qdt@#2k1TULmKfgjeCe;Ln8OHA<)h@H)YM z1KxzU;2^EuhQGog(&-4f=oq{M$KhQ#K>=IouOMqto`%Ezra(Q9zECi+fbzd8&`54E z{b0JK2e)mwc~PXOnd-V84=tnZMsAXSrbk=G>!HR4Jfo^i)o4`~;RPjFt6W2>#{!Lw zoWiIJJe|v6^~^gDPFpz7r-i5H`ZMN@_P?f z&ib(u6`{spnhej*_3YNXyh2L*yxg{TXoy%z{`#E}89U)Lh0t0!0Uwe}uc097#d`mG z5PfU}(E$pg0~AD`@gQP;BTRZynDk^-Y+_APOhKDjBffz1Ti`r=319IlK?)jxu6U1H zR1#@t&L1cqHOd;JnT%!BDqUJzhvwvGW%zha%cezkL0eMl_wd6O z_`#GqulDLBHY4~asOW%8aG4lovXSft${Mu5NwlI3?dU)eC3I2@*TPP8qX%X5!dpm* ziD~3CLTZE<50%bPETKrHQbtAJ2vjOu#+BYz1S#u74Iw>7v=woKTIYVJ?WHsarYHkT z3<`3>fXw0yfXw1M$;166=F2QyMyrBpzT@y`?26s6J9Nb!*b{rbU|-CDBNSRy zRJu|bO(m@<8cUFnWZ~g0gUd2%6XEJy=di(S2)mXIWWzP#R62ojUd1*9NLaU zVKkBPIpqirM;~#`#vIJ$hFTCx@HS^4=h?1L^bO19pPa%%P%P|vlg$XsZ^wKXO(e$F zW0;R4(T9b^YZTIei8o*_SCZE>j*k~zGH^KBXiPK1*igmJve@<2{-yDRzKFWk0;8OF znk|K_m?JCpK}vENmT$#!9Eanv;+JvE4QX^h&Zj)2hlBhHDO6O?M-P5On)6G_e-jBY z`44vCWb92bHWjDg^kj>mZo)Hy5-N{;(v{_FUHV-btBP>7x>Y#CRF~zl9IjHW(zTvU zOT_h0^UY5o>2@{4Q8+)za2Ok5GE|ZbJFR1Sp&rL=Nm;^tqGLllHo$07 z;HrA|s~Lqkq-}^Y>K0r`8MTOK6zMoBv!t8?s8C{KD9p1{X8L)UC(JNU2J2`xd{$~; zN$vktvtBXezecni8Lq(F3DL?VqMI^{1D`@>NfyDnQDUV8D<>r;1#7jDeQOBTU2udI z896kT=MYj3P1hUrm>#Luud*_{i}xn2T*8XEmFJiNXhdwvQx6!ymrV{z3H*QbGQIFH zZtB2IxY?M&le1V+EEZTw>0(^30)id5B?)9a8^?jvG^V;z3dVL0qtrKSL}wQ4#9i&U z%bfA~F~;8#Xri&HR!MJdmaM7=%;3o>q~*i z9zm;|!nU@#wZUj=1RG`A=_P!5E53}cbV5qKCqcYYQVN~83fvk>)hqB0P@8<60R9Ev zNP?KgK`j1bE1OOSLjsZQBNY6X8zZ+ckARHeZHdl0c#scH2dGh&(fDB4%-Cf%W@=v4 zqf?`~iS-B`-GWC=q*PB#q~_2=-H+S+=kvTtbsWCzFlh|e5FWEd`f>KK^8Opobl@5M zknik5ogq{=rJ0U8)QOTuZR&gUu=DoHQEexF!jEcH0dD4JvAIF>tZ7sc(`29Fg>4iX zJK-|T@t2^IIyN6lN7A^QNk_OFSe=RCYuwzPs*a#y@y&d^CIJ^B41n&y)CeelA6!gxB~uHd*x7n)VLCUn%5 z2qnj-7?TUvCrLAeXoM_bs4z?zF8G9OAxFp+@`MqriT#GfSezwTGh57B*b=ss)hQmu ztvD2yVpAl=sfZ@7u~4yzm6_`jys#<7yF9t3^S@9bOlTJ-7_CxLIdi!5TC}^@ngIwAepci{Y$7+m?_K`!oo6Pwa_l?7WNCT2?vF@ zg+sy-;h1n-I3b)AP6?-lGr~v0C&H(~S>bcxoN!+FO8DMlw}=*}rJE(gGT4%5DYT5Y zOtSOQy)~Bt{TA#D-x4vk7$@&-To7RKYPi=zDWlOj9 zw)M4LXB%!SvQ4o0ZL@5%Z3}ISZL4knX8U*B{k8{e>uj5CTWpWm9<`mZowI#sci6kw z2ibG&W9()2Y4%!s*dDPr*<<#EeX)IseVP4M`*Qp3_LcTk_SN?F_AT~3_PzEe>`&RB zvG21#Z~xf-h5cKH;IKOE4$j`?qrYRIW02!E$2!MG#}3B<#}UUV z$ETuQ>?an8HKHyi#MRh%{6hE@ex((g>+QDwIm3Nzx1{BsEF5O7}|lNo%DC zrA<=1v`>0PdQ&pj=|t`A(FxV~{)-43_p z*4(|_+3pheRQF7G!hO4YrF)fowfheDo$hx4-YwooypMW!dG~l<_a5_p;63a8+IvaquMAWMDT9?E%1|Xo$y25( zHOgFNzH+m&SXrUmq1>lzQl3#>QVuAmmCuyRs;GL@40Vv|Qw!8eb)Fhgo79+^P#3Ge zRqs^Ss~gmZ)y-;$x>en#ZdZ4xPpbRWSJXr55%rjQTs@(lR8Og=)idg6>No0l>JREq z>SYZzi)Pbgt(VqY>#OzC`fCHVLE2!gKr7Y!+8iyYHEJPkp_b5Ew53|Bwo-dYTd!@< z9@aK%9oklHo3>rsq3zQ4XwPZ~wYRlH+7a!Tc3eB5ozzZgr?oTMN7^Uar`lQVbM2gV tUi(V>yY{2@lXf{xOq0@FX{|C-a6WIU& literal 0 HcmV?d00001 diff --git a/se/cocoa/English.lproj/Directories.nib/classes.nib b/se/cocoa/English.lproj/Directories.nib/classes.nib new file mode 100644 index 00000000..3ebaa96a --- /dev/null +++ b/se/cocoa/English.lproj/Directories.nib/classes.nib @@ -0,0 +1,62 @@ + + + + + IBClasses + + + CLASS + FirstResponder + LANGUAGE + ObjC + SUPERCLASS + NSObject + + + ACTIONS + + askForDirectory + id + changeDirectoryState + id + popupAddDirectoryMenu + id + removeSelectedDirectory + id + toggleVisible + id + + CLASS + DirectoryPanel + LANGUAGE + ObjC + OUTLETS + + addButtonPopUp + NSPopUpButton + directories + NSOutlineView + removeButton + NSButton + + SUPERCLASS + DirectoryPanelBase + + + CLASS + OutlineView + LANGUAGE + ObjC + OUTLETS + + py + PyApp + + SUPERCLASS + NSOutlineView + + + IBVersion + 1 + + diff --git a/se/cocoa/English.lproj/Directories.nib/info.nib b/se/cocoa/English.lproj/Directories.nib/info.nib new file mode 100644 index 00000000..5c508e04 --- /dev/null +++ b/se/cocoa/English.lproj/Directories.nib/info.nib @@ -0,0 +1,20 @@ + + + + + IBFramework Version + 629 + IBLastKnownRelativeProjectPath + ../../dupeguru.xcodeproj + IBOldestOS + 5 + IBOpenObjects + + 6 + + IBSystem Version + 9B18 + targetFramework + IBCocoaFramework + + diff --git a/se/cocoa/English.lproj/Directories.nib/keyedobjects.nib b/se/cocoa/English.lproj/Directories.nib/keyedobjects.nib new file mode 100644 index 0000000000000000000000000000000000000000..2275d202d0e2f6eaaa97521edfef9371f56b2f95 GIT binary patch literal 9698 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 Baclqp literal 0 HcmV?d00001 diff --git a/se/cocoa/English.lproj/InfoPlist.strings b/se/cocoa/English.lproj/InfoPlist.strings new file mode 100644 index 0000000000000000000000000000000000000000..d224a14bd2062242e455ea370c921c67e7045c94 GIT binary patch literal 204 zcmW-bOA5k35Cv=PDT2!&D(*yFxrrby%n4#XD*i$e6}^#{RLh~Ed*=0fHS_s0A|_(R zm7I(d2VRsEYIkQtt8(SyjGUEy>8yVS6Mq literal 0 HcmV?d00001 diff --git a/se/cocoa/English.lproj/MainMenu.nib/classes.nib b/se/cocoa/English.lproj/MainMenu.nib/classes.nib new file mode 100644 index 00000000..3a85cf2a --- /dev/null +++ b/se/cocoa/English.lproj/MainMenu.nib/classes.nib @@ -0,0 +1,229 @@ + + + + + IBClasses + + + CLASS + NSSegmentedControl + LANGUAGE + ObjC + SUPERCLASS + NSControl + + + ACTIONS + + openWebsite + id + toggleDirectories + id + unlockApp + id + + CLASS + AppDelegate + LANGUAGE + ObjC + OUTLETS + + py + PyDupeGuru + recentDirectories + RecentDirectories + result + ResultWindow + unlockMenuItem + NSMenuItem + + SUPERCLASS + NSObject + + + CLASS + PyApp + LANGUAGE + ObjC + SUPERCLASS + NSObject + + + CLASS + MatchesView + LANGUAGE + ObjC + SUPERCLASS + OutlineView + + + CLASS + PyDupeGuru + LANGUAGE + ObjC + SUPERCLASS + PyApp + + + ACTIONS + + changeDelta + id + changePowerMarker + id + clearIgnoreList + id + collapseAll + id + copyMarked + id + deleteMarked + id + expandAll + id + exportToXHTML + id + filter + id + ignoreSelected + id + markAll + id + markInvert + id + markNone + id + markSelected + id + markToggle + id + moveMarked + id + openSelected + id + refresh + id + removeMarked + id + removeSelected + id + renameSelected + id + resetColumnsToDefault + id + revealSelected + id + showPreferencesPanel + id + startDuplicateScan + id + switchSelected + id + toggleColumn + id + toggleDelta + id + toggleDetailsPanel + id + togglePowerMarker + id + + CLASS + ResultWindow + LANGUAGE + ObjC + OUTLETS + + actionMenu + NSPopUpButton + actionMenuView + NSView + app + id + columnsMenu + NSMenu + deltaSwitch + NSSegmentedControl + deltaSwitchView + NSView + filterField + NSSearchField + filterFieldView + NSView + matches + MatchesView + pmSwitch + NSSegmentedControl + pmSwitchView + NSView + preferencesPanel + NSWindow + py + PyDupeGuru + stats + NSTextField + + SUPERCLASS + NSWindowController + + + CLASS + FirstResponder + LANGUAGE + ObjC + SUPERCLASS + NSObject + + + ACTIONS + + checkForUpdates + id + + CLASS + SUUpdater + LANGUAGE + ObjC + SUPERCLASS + NSObject + + + ACTIONS + + clearMenu + id + menuClick + id + + CLASS + RecentDirectories + LANGUAGE + ObjC + OUTLETS + + delegate + id + menu + NSMenu + + SUPERCLASS + NSObject + + + CLASS + OutlineView + LANGUAGE + ObjC + OUTLETS + + py + PyApp + + SUPERCLASS + NSOutlineView + + + IBVersion + 1 + + diff --git a/se/cocoa/English.lproj/MainMenu.nib/info.nib b/se/cocoa/English.lproj/MainMenu.nib/info.nib new file mode 100644 index 00000000..6799cea9 --- /dev/null +++ b/se/cocoa/English.lproj/MainMenu.nib/info.nib @@ -0,0 +1,20 @@ + + + + + IBFramework Version + 629 + IBLastKnownRelativeProjectPath + ../../dupeguru.xcodeproj + IBOldestOS + 5 + IBOpenObjects + + 524 + + IBSystem Version + 9E17 + targetFramework + IBCocoaFramework + + diff --git a/se/cocoa/English.lproj/MainMenu.nib/keyedobjects.nib b/se/cocoa/English.lproj/MainMenu.nib/keyedobjects.nib new file mode 100644 index 0000000000000000000000000000000000000000..7136407cef8a5712c05487ac7165ec08616e1cfd GIT binary patch literal 48638 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𝔗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 literal 0 HcmV?d00001 diff --git a/se/cocoa/Info.plist b/se/cocoa/Info.plist new file mode 100644 index 00000000..d24efa47 --- /dev/null +++ b/se/cocoa/Info.plist @@ -0,0 +1,34 @@ + + + + + CFBundleDevelopmentRegion + English + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleHelpBookFolder + dupeguru_help + CFBundleHelpBookName + dupeGuru Help + CFBundleIconFile + dupeguru + CFBundleIdentifier + com.hardcoded_software.dupeguru + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + APPL + CFBundleSignature + hsft + CFBundleVersion + 2.7.1 + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + SUFeedURL + http://www.hardcoded.net/updates/dupeguru.appcast + + diff --git a/se/cocoa/PyDupeGuru.h b/se/cocoa/PyDupeGuru.h new file mode 100644 index 00000000..85874157 --- /dev/null +++ b/se/cocoa/PyDupeGuru.h @@ -0,0 +1,9 @@ +#import +#import "dgbase/PyDupeGuru.h" + +@interface PyDupeGuru : PyDupeGuruBase +//Scanning options +- (void)setScanType:(NSNumber *)scan_type; +- (void)setWordWeighting:(NSNumber *)words_are_weighted; +- (void)setMatchSimilarWords:(NSNumber *)match_similar_words; +@end diff --git a/se/cocoa/ResultWindow.h b/se/cocoa/ResultWindow.h new file mode 100644 index 00000000..443d5afe --- /dev/null +++ b/se/cocoa/ResultWindow.h @@ -0,0 +1,55 @@ +#import +#import "cocoalib/Outline.h" +#import "dgbase/ResultWindow.h" +#import "DetailsPanel.h" +#import "DirectoryPanel.h" + +@interface ResultWindow : ResultWindowBase +{ + IBOutlet NSPopUpButton *actionMenu; + IBOutlet NSMenu *columnsMenu; + IBOutlet NSSearchField *filterField; + IBOutlet NSSegmentedControl *pmSwitch; + IBOutlet NSWindow *preferencesPanel; + IBOutlet NSTextField *stats; + + NSString *_lastAction; + DetailsPanel *_detailsPanel; + NSMutableArray *_resultColumns; + NSMutableIndexSet *_deltaColumns; +} +- (IBAction)changePowerMarker:(id)sender; +- (IBAction)clearIgnoreList:(id)sender; +- (IBAction)exportToXHTML:(id)sender; +- (IBAction)filter:(id)sender; +- (IBAction)ignoreSelected:(id)sender; +- (IBAction)markAll:(id)sender; +- (IBAction)markInvert:(id)sender; +- (IBAction)markNone:(id)sender; +- (IBAction)markSelected:(id)sender; +- (IBAction)markToggle:(id)sender; +- (IBAction)openSelected:(id)sender; +- (IBAction)refresh:(id)sender; +- (IBAction)removeMarked:(id)sender; +- (IBAction)removeSelected:(id)sender; +- (IBAction)renameSelected:(id)sender; +- (IBAction)resetColumnsToDefault:(id)sender; +- (IBAction)revealSelected:(id)sender; +- (IBAction)showPreferencesPanel:(id)sender; +- (IBAction)startDuplicateScan:(id)sender; +- (IBAction)switchSelected:(id)sender; +- (IBAction)toggleColumn:(id)sender; +- (IBAction)toggleDelta:(id)sender; +- (IBAction)toggleDetailsPanel:(id)sender; +- (IBAction)togglePowerMarker:(id)sender; + +- (NSTableColumn *)getColumnForIdentifier:(int)aIdentifier title:(NSString *)aTitle width:(int)aWidth refCol:(NSTableColumn *)aColumn; +- (NSArray *)getColumnsOrder; +- (NSDictionary *)getColumnsWidth; +- (NSArray *)getSelected:(BOOL)aDupesOnly; +- (NSArray *)getSelectedPaths:(BOOL)aDupesOnly; +- (void)initResultColumns; +- (void)performPySelection:(NSArray *)aIndexPaths; +- (void)refreshStats; +- (void)restoreColumnsPosition:(NSArray *)aColumnsOrder widths:(NSDictionary *)aColumnsWidth; +@end diff --git a/se/cocoa/ResultWindow.m b/se/cocoa/ResultWindow.m new file mode 100644 index 00000000..49b80b56 --- /dev/null +++ b/se/cocoa/ResultWindow.m @@ -0,0 +1,460 @@ +#import "ResultWindow.h" +#import "cocoalib/Dialogs.h" +#import "cocoalib/ProgressController.h" +#import "cocoalib/Utils.h" +#import "AppDelegate.h" +#import "Consts.h" + +@implementation ResultWindow +/* Override */ +- (void)awakeFromNib +{ + [super awakeFromNib]; + _detailsPanel = nil; + _displayDelta = NO; + _powerMode = NO; + _deltaColumns = [[NSMutableIndexSet indexSetWithIndexesInRange:NSMakeRange(2,4)] retain]; + [_deltaColumns removeIndex:3]; + [deltaSwitch setSelectedSegment:0]; + [pmSwitch setSelectedSegment:0]; + [py setDisplayDeltaValues:b2n(_displayDelta)]; + [matches setTarget:self]; + [matches setDoubleAction:@selector(openSelected:)]; + [[actionMenu itemAtIndex:0] setImage:[NSImage imageNamed: @"gear"]]; + [self initResultColumns]; + [self refreshStats]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(resultsMarkingChanged:) name:ResultsMarkingChangedNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(duplicateSelectionChanged:) name:DuplicateSelectionChangedNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(resultsChanged:) name:ResultsChangedNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(jobCompleted:) name:JobCompletedNotification object:nil]; + + NSToolbar *t = [[[NSToolbar alloc] initWithIdentifier:@"ResultWindowToolbar"] autorelease]; + [t setAllowsUserCustomization:YES]; + [t setAutosavesConfiguration:YES]; + [t setDisplayMode:NSToolbarDisplayModeIconAndLabel]; + [t setDelegate:self]; + [[self window] setToolbar:t]; +} + +/* Actions */ + +- (IBAction)changePowerMarker:(id)sender +{ + _powerMode = [pmSwitch selectedSegment] == 1; + if (_powerMode) + [matches setTag:2]; + else + [matches setTag:0]; + [self expandAll:nil]; + [self outlineView:matches didClickTableColumn:nil]; +} + +- (IBAction)clearIgnoreList:(id)sender +{ + int i = n2i([py getIgnoreListCount]); + if (!i) + return; + if ([Dialogs askYesNo:[NSString stringWithFormat:@"Do you really want to remove all %d items from the ignore list?",i]] == NSAlertSecondButtonReturn) // NO + return; + [py clearIgnoreList]; +} + +- (IBAction)exportToXHTML:(id)sender +{ + NSString *xsltPath = [[NSBundle mainBundle] pathForResource:@"dg" ofType:@"xsl"]; + NSString *cssPath = [[NSBundle mainBundle] pathForResource:@"hardcoded" ofType:@"css"]; + NSString *exported = [py exportToXHTMLwithColumns:[self getColumnsOrder] xslt:xsltPath css:cssPath]; + [[NSWorkspace sharedWorkspace] openFile:exported]; +} + +- (IBAction)filter:(id)sender +{ + NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; + [py setEscapeFilterRegexp:b2n(!n2b([ud objectForKey:@"useRegexpFilter"]))]; + [py applyFilter:[filterField stringValue]]; + [[NSNotificationCenter defaultCenter] postNotificationName:ResultsChangedNotification object:self]; +} + +- (IBAction)ignoreSelected:(id)sender +{ + NSArray *nodeList = [self getSelected:YES]; + if (![nodeList count]) + return; + if ([Dialogs askYesNo:[NSString stringWithFormat:@"All selected %d matches are going to be ignored in all subsequent scans. Continue?",[nodeList count]]] == NSAlertSecondButtonReturn) // NO + return; + [self performPySelection:[self getSelectedPaths:YES]]; + [py addSelectedToIgnoreList]; + [py removeSelected]; + [[NSNotificationCenter defaultCenter] postNotificationName:ResultsChangedNotification object:self]; +} + +- (IBAction)markAll:(id)sender +{ + [py markAll]; + [[NSNotificationCenter defaultCenter] postNotificationName:ResultsMarkingChangedNotification object:self]; +} + +- (IBAction)markInvert:(id)sender +{ + [py markInvert]; + [[NSNotificationCenter defaultCenter] postNotificationName:ResultsMarkingChangedNotification object:self]; +} + +- (IBAction)markNone:(id)sender +{ + [py markNone]; + [[NSNotificationCenter defaultCenter] postNotificationName:ResultsMarkingChangedNotification object:self]; +} + +- (IBAction)markSelected:(id)sender +{ + [self performPySelection:[self getSelectedPaths:YES]]; + [py toggleSelectedMark]; + [[NSNotificationCenter defaultCenter] postNotificationName:ResultsMarkingChangedNotification object:self]; +} + +- (IBAction)markToggle:(id)sender +{ + OVNode *node = [matches itemAtRow:[matches clickedRow]]; + [self performPySelection:[NSArray arrayWithObject:p2a([node indexPath])]]; + [py toggleSelectedMark]; + [[NSNotificationCenter defaultCenter] postNotificationName:ResultsMarkingChangedNotification object:self]; +} + +- (IBAction)openSelected:(id)sender +{ + [self performPySelection:[self getSelectedPaths:NO]]; + [py openSelected]; +} + +- (IBAction)refresh:(id)sender +{ + [[NSNotificationCenter defaultCenter] postNotificationName:ResultsChangedNotification object:self]; +} + +- (IBAction)removeMarked:(id)sender +{ + int mark_count = [[py getMarkCount] intValue]; + if (!mark_count) + return; + if ([Dialogs askYesNo:[NSString stringWithFormat:@"You are about to remove %d files from results. Continue?",mark_count]] == NSAlertSecondButtonReturn) // NO + return; + [py removeMarked]; + [[NSNotificationCenter defaultCenter] postNotificationName:ResultsChangedNotification object:self]; +} + +- (IBAction)removeSelected:(id)sender +{ + NSArray *nodeList = [self getSelected:YES]; + if (![nodeList count]) + return; + if ([Dialogs askYesNo:[NSString stringWithFormat:@"You are about to remove %d files from results. Continue?",[nodeList count]]] == NSAlertSecondButtonReturn) // NO + return; + [self performPySelection:[self getSelectedPaths:YES]]; + [py removeSelected]; + [[NSNotificationCenter defaultCenter] postNotificationName:ResultsChangedNotification object:self]; +} + +- (IBAction)renameSelected:(id)sender +{ + int col = [matches columnWithIdentifier:@"0"]; + int row = [matches selectedRow]; + [matches editColumn:col row:row withEvent:[NSApp currentEvent] select:YES]; +} + +- (IBAction)resetColumnsToDefault:(id)sender +{ + NSMutableArray *columnsOrder = [NSMutableArray array]; + [columnsOrder addObject:@"0"]; + [columnsOrder addObject:@"1"]; + [columnsOrder addObject:@"2"]; + [columnsOrder addObject:@"6"]; + NSMutableDictionary *columnsWidth = [NSMutableDictionary dictionary]; + [columnsWidth setObject:i2n(195) forKey:@"0"]; + [columnsWidth setObject:i2n(120) forKey:@"1"]; + [columnsWidth setObject:i2n(63) forKey:@"2"]; + [columnsWidth setObject:i2n(60) forKey:@"6"]; + [self restoreColumnsPosition:columnsOrder widths:columnsWidth]; +} + +- (IBAction)revealSelected:(id)sender +{ + [self performPySelection:[self getSelectedPaths:NO]]; + [py revealSelected]; +} + +- (IBAction)showPreferencesPanel:(id)sender +{ + [preferencesPanel makeKeyAndOrderFront:sender]; +} + +- (IBAction)startDuplicateScan:(id)sender +{ + if ([matches numberOfRows] > 0) + { + if ([Dialogs askYesNo:@"Are you sure you want to start a new duplicate scan?"] == NSAlertSecondButtonReturn) // NO + return; + } + NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; + PyDupeGuru *_py = (PyDupeGuru *)py; + [_py setScanType:[ud objectForKey:@"scanType"]]; + [_py setMinMatchPercentage:[ud objectForKey:@"minMatchPercentage"]]; + [_py setWordWeighting:[ud objectForKey:@"wordWeighting"]]; + [_py setMixFileKind:[ud objectForKey:@"mixFileKind"]]; + [_py setMatchSimilarWords:[ud objectForKey:@"matchSimilarWords"]]; + int smallFileThreshold = [ud integerForKey:@"smallFileThreshold"]; // In KB + int sizeThreshold = [ud boolForKey:@"ignoreSmallFiles"] ? smallFileThreshold * 1024 : 0; // The py side wants bytes + [_py setSizeThreshold:sizeThreshold]; + int r = n2i([py doScan]); + [matches reloadData]; + [self refreshStats]; + if (r != 0) + [[ProgressController mainProgressController] hide]; + if (r == 1) + [Dialogs showMessage:@"You cannot make a duplicate scan with only reference directories."]; + if (r == 3) + { + [Dialogs showMessage:@"The selected directories contain no scannable file."]; + [app toggleDirectories:nil]; + } +} + +- (IBAction)switchSelected:(id)sender +{ + [self performPySelection:[self getSelectedPaths:YES]]; + [py makeSelectedReference]; + [[NSNotificationCenter defaultCenter] postNotificationName:ResultsChangedNotification object:self]; +} + +- (IBAction)toggleColumn:(id)sender +{ + NSMenuItem *mi = sender; + NSString *colId = [NSString stringWithFormat:@"%d",[mi tag]]; + NSTableColumn *col = [matches tableColumnWithIdentifier:colId]; + if (col == nil) + { + //Add Column + col = [_resultColumns objectAtIndex:[mi tag]]; + [matches addTableColumn:col]; + [mi setState:NSOnState]; + } + else + { + //Remove column + [matches removeTableColumn:col]; + [mi setState:NSOffState]; + } +} + +- (IBAction)toggleDelta:(id)sender +{ + if ([deltaSwitch selectedSegment] == 1) + [deltaSwitch setSelectedSegment:0]; + else + [deltaSwitch setSelectedSegment:1]; + [self changeDelta:sender]; +} + +- (IBAction)toggleDetailsPanel:(id)sender +{ + if (!_detailsPanel) + _detailsPanel = [[DetailsPanel alloc] initWithPy:py]; + if ([[_detailsPanel window] isVisible]) + [[_detailsPanel window] close]; + else + [[_detailsPanel window] orderFront:nil]; +} + +- (IBAction)togglePowerMarker:(id)sender +{ + if ([pmSwitch selectedSegment] == 1) + [pmSwitch setSelectedSegment:0]; + else + [pmSwitch setSelectedSegment:1]; + [self changePowerMarker:sender]; +} + +/* Public */ +- (NSTableColumn *)getColumnForIdentifier:(int)aIdentifier title:(NSString *)aTitle width:(int)aWidth refCol:(NSTableColumn *)aColumn +{ + NSNumber *n = [NSNumber numberWithInt:aIdentifier]; + NSTableColumn *col = [[NSTableColumn alloc] initWithIdentifier:[n stringValue]]; + [col setWidth:aWidth]; + [col setEditable:NO]; + [[col dataCell] setFont:[[aColumn dataCell] font]]; + [[col headerCell] setStringValue:aTitle]; + [col setResizingMask:NSTableColumnUserResizingMask]; + [col setSortDescriptorPrototype:[[NSSortDescriptor alloc] initWithKey:[n stringValue] ascending:YES]]; + return col; +} + +//Returns an array of identifiers, in order. +- (NSArray *)getColumnsOrder +{ + NSTableColumn *col; + NSString *colId; + NSMutableArray *result = [NSMutableArray array]; + NSEnumerator *e = [[matches tableColumns] objectEnumerator]; + while (col = [e nextObject]) + { + colId = [col identifier]; + [result addObject:colId]; + } + return result; +} + +- (NSDictionary *)getColumnsWidth +{ + NSMutableDictionary *result = [NSMutableDictionary dictionary]; + NSTableColumn *col; + NSString *colId; + NSNumber *width; + NSEnumerator *e = [[matches tableColumns] objectEnumerator]; + while (col = [e nextObject]) + { + colId = [col identifier]; + width = [NSNumber numberWithFloat:[col width]]; + [result setObject:width forKey:colId]; + } + return result; +} + +- (NSArray *)getSelected:(BOOL)aDupesOnly +{ + if (_powerMode) + aDupesOnly = NO; + NSIndexSet *indexes = [matches selectedRowIndexes]; + NSMutableArray *nodeList = [NSMutableArray array]; + OVNode *node; + int i = [indexes firstIndex]; + while (i != NSNotFound) + { + node = [matches itemAtRow:i]; + if (!aDupesOnly || ([node level] > 1)) + [nodeList addObject:node]; + i = [indexes indexGreaterThanIndex:i]; + } + return nodeList; +} + +- (NSArray *)getSelectedPaths:(BOOL)aDupesOnly +{ + NSMutableArray *r = [NSMutableArray array]; + NSArray *selected = [self getSelected:aDupesOnly]; + NSEnumerator *e = [selected objectEnumerator]; + OVNode *node; + while (node = [e nextObject]) + [r addObject:p2a([node indexPath])]; + return r; +} + +- (void)performPySelection:(NSArray *)aIndexPaths +{ + if (_powerMode) + [py selectPowerMarkerNodePaths:aIndexPaths]; + else + [py selectResultNodePaths:aIndexPaths]; +} + +- (void)initResultColumns +{ + NSTableColumn *refCol = [matches tableColumnWithIdentifier:@"0"]; + _resultColumns = [[NSMutableArray alloc] init]; + [_resultColumns addObject:[matches tableColumnWithIdentifier:@"0"]]; // File Name + [_resultColumns addObject:[matches tableColumnWithIdentifier:@"1"]]; // Directory + [_resultColumns addObject:[matches tableColumnWithIdentifier:@"2"]]; // Size + [_resultColumns addObject:[self getColumnForIdentifier:3 title:@"Kind" width:40 refCol:refCol]]; + [_resultColumns addObject:[self getColumnForIdentifier:4 title:@"Creation" width:120 refCol:refCol]]; + [_resultColumns addObject:[self getColumnForIdentifier:5 title:@"Modification" width:120 refCol:refCol]]; + [_resultColumns addObject:[matches tableColumnWithIdentifier:@"6"]]; // Match % + [_resultColumns addObject:[self getColumnForIdentifier:7 title:@"Words Used" width:120 refCol:refCol]]; + [_resultColumns addObject:[self getColumnForIdentifier:8 title:@"Dupe Count" width:80 refCol:refCol]]; +} + +-(void)refreshStats +{ + [stats setStringValue:[py getStatLine]]; +} + +- (void)restoreColumnsPosition:(NSArray *)aColumnsOrder widths:(NSDictionary *)aColumnsWidth +{ + NSTableColumn *col; + NSString *colId; + NSNumber *width; + NSMenuItem *mi; + //Remove all columns + NSEnumerator *e = [[columnsMenu itemArray] objectEnumerator]; + while (mi = [e nextObject]) + { + if ([mi state] == NSOnState) + [self toggleColumn:mi]; + } + //Add columns and set widths + e = [aColumnsOrder objectEnumerator]; + while (colId = [e nextObject]) + { + if (![colId isEqual:@"mark"]) + { + col = [_resultColumns objectAtIndex:[colId intValue]]; + width = [aColumnsWidth objectForKey:[col identifier]]; + mi = [columnsMenu itemWithTag:[colId intValue]]; + if (width) + [col setWidth:[width floatValue]]; + [self toggleColumn:mi]; + } + } +} + +/* Delegate */ +- (void)outlineView:(NSOutlineView *)outlineView willDisplayCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn item:(id)item +{ + OVNode *node = item; + if ([[tableColumn identifier] isEqual:@"mark"]) + { + [cell setEnabled: [node isMarkable]]; + } + if ([cell isKindOfClass:[NSTextFieldCell class]]) + { + // Determine if the text color will be blue due to directory being reference. + NSTextFieldCell *textCell = cell; + if ([node isMarkable]) + [textCell setTextColor:[NSColor blackColor]]; + else + [textCell setTextColor:[NSColor blueColor]]; + if ((_displayDelta) && (_powerMode || ([node level] > 1))) + { + int i = [[tableColumn identifier] intValue]; + if ([_deltaColumns containsIndex:i]) + [textCell setTextColor:[NSColor orangeColor]]; + } + } +} + +/* Notifications */ +- (void)duplicateSelectionChanged:(NSNotification *)aNotification +{ + if (_detailsPanel) + [_detailsPanel refresh]; +} + +- (void)outlineViewSelectionDidChange:(NSNotification *)notification +{ + [self performPySelection:[self getSelectedPaths:NO]]; + [py refreshDetailsWithSelected]; + [[NSNotificationCenter defaultCenter] postNotificationName:DuplicateSelectionChangedNotification object:self]; +} + +- (void)resultsChanged:(NSNotification *)aNotification +{ + [matches reloadData]; + [self expandAll:nil]; + [self outlineViewSelectionDidChange:nil]; + [self refreshStats]; +} + +- (void)resultsMarkingChanged:(NSNotification *)aNotification +{ + [matches invalidateMarkings]; + [self refreshStats]; +} +@end diff --git a/se/cocoa/dupeguru.icns b/se/cocoa/dupeguru.icns new file mode 100755 index 0000000000000000000000000000000000000000..6641a6b4a15b02b3d6ddf1b421dcf0313824ff3b GIT binary patch literal 53620 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 P>MA$nJk&{X9)te}Eq7n5 literal 0 HcmV?d00001 diff --git a/se/cocoa/dupeguru.xcodeproj/hsoft.mode1 b/se/cocoa/dupeguru.xcodeproj/hsoft.mode1 new file mode 100644 index 00000000..6ffe0908 --- /dev/null +++ b/se/cocoa/dupeguru.xcodeproj/hsoft.mode1 @@ -0,0 +1,1369 @@ + + + + + ActivePerspectiveName + Project + AllowedModules + + + BundleLoadPath + + MaxInstances + n + Module + PBXSmartGroupTreeModule + Name + Groups and Files Outline View + + + BundleLoadPath + + MaxInstances + n + Module + PBXNavigatorGroup + Name + Editor + + + BundleLoadPath + + MaxInstances + n + Module + XCTaskListModule + Name + Task List + + + BundleLoadPath + + MaxInstances + n + Module + XCDetailModule + Name + File and Smart Group Detail Viewer + + + BundleLoadPath + + MaxInstances + 1 + Module + PBXBuildResultsModule + Name + Detailed Build Results Viewer + + + BundleLoadPath + + MaxInstances + 1 + Module + PBXProjectFindModule + Name + Project Batch Find Tool + + + BundleLoadPath + + MaxInstances + n + Module + PBXRunSessionModule + Name + Run Log + + + BundleLoadPath + + MaxInstances + n + Module + PBXBookmarksModule + Name + Bookmarks Tool + + + BundleLoadPath + + MaxInstances + n + Module + PBXClassBrowserModule + Name + Class Browser + + + BundleLoadPath + + MaxInstances + n + Module + PBXCVSModule + Name + Source Code Control Tool + + + BundleLoadPath + + MaxInstances + n + Module + PBXDebugBreakpointsModule + Name + Debug Breakpoints Tool + + + BundleLoadPath + + MaxInstances + n + Module + XCDockableInspector + Name + Inspector + + + BundleLoadPath + + MaxInstances + n + Module + PBXOpenQuicklyModule + Name + Open Quickly Tool + + + BundleLoadPath + + MaxInstances + 1 + Module + PBXDebugSessionModule + Name + Debugger + + + BundleLoadPath + + MaxInstances + 1 + Module + PBXDebugCLIModule + Name + Debug Console + + + Description + DefaultDescriptionKey + DockingSystemVisible + + Extension + mode1 + FavBarConfig + + PBXProjectModuleGUID + CE381CB409914B41003581CE + XCBarModuleItemNames + + XCBarModuleItems + + + FirstTimeWindowDisplayed + + Identifier + com.apple.perspectives.project.mode1 + MajorVersion + 31 + MinorVersion + 1 + Name + Default + Notifications + + OpenEditors + + PerspectiveWidths + + -1 + -1 + + Perspectives + + + ChosenToolbarItems + + active-executable-popup + action + active-buildstyle-popup + active-target-popup + buildOrClean + build-and-runOrDebug + com.apple.ide.PBXToolbarStopButton + get-info + toggle-editor + + ControllerClassBaseName + + IconName + WindowOfProjectWithEditor + Identifier + perspective.project + IsVertical + + Layout + + + BecomeActive + + ContentConfiguration + + PBXBottomSmartGroupGIDs + + 1C37FBAC04509CD000000102 + 1C37FAAC04509CD000000102 + 1C08E77C0454961000C914BD + 1C37FABC05509CD000000102 + 1C37FABC05539CD112110102 + E2644B35053B69B200211256 + 1C37FABC04509CD000100104 + + PBXProjectModuleGUID + 1CE0B1FE06471DED0097A5F4 + PBXProjectModuleLabel + Files + PBXProjectStructureProvided + yes + PBXSmartGroupTreeModuleColumnData + + PBXSmartGroupTreeModuleColumnWidthsKey + + 194 + + PBXSmartGroupTreeModuleColumnsKey_v4 + + MainColumn + + + PBXSmartGroupTreeModuleOutlineStateKey_v7 + + PBXSmartGroupTreeModuleOutlineStateExpansionKey + + 29B97314FDCFA39411CA2CEA + 080E96DDFE201D6D7F000001 + 29B97317FDCFA39411CA2CEA + 29B97323FDCFA39411CA2CEA + 1058C7A0FEA54F0111CA2CBB + 1058C7A2FEA54F0111CA2CBB + 19C28FACFE9D520D11CA2CBB + 1C37FBAC04509CD000000102 + 1C37FAAC04509CD000000102 + 1C37FABC05509CD000000102 + + PBXSmartGroupTreeModuleOutlineStateSelectionKey + + + 37 + + + PBXSmartGroupTreeModuleOutlineStateVisibleRectKey + {{0, 0}, {194, 764}} + + PBXTopSmartGroupGIDs + + XCIncludePerspectivesSwitch + + XCSharingToken + com.apple.Xcode.GFSharingToken + + GeometryConfiguration + + Frame + {{0, 0}, {211, 782}} + GroupTreeTableConfiguration + + MainColumn + 194 + + RubberWindowFrame + 1 55 1366 823 0 0 1440 878 + + Module + PBXSmartGroupTreeModule + Proportion + 211pt + + + Dock + + + ContentConfiguration + + PBXProjectModuleGUID + 1CE0B20306471E060097A5F4 + PBXProjectModuleLabel + Info.plist + PBXSplitModuleInNavigatorKey + + Split0 + + PBXProjectModuleGUID + 1CE0B20406471E060097A5F4 + PBXProjectModuleLabel + Info.plist + _historyCapacity + 10 + bookmark + CE6D39D50C9B15B600C7FE6C + history + + CEDA43440B07D11900B3091A + CECEEB580B0CAE8E00E6972C + CECEEBE40B0CB68A00E6972C + CECEEBE60B0CB68A00E6972C + CECEEBE70B0CB68A00E6972C + CED9635E0B0F954E00DDBB8C + CE24ED9F0B552A5D00DDF502 + CE6D394A0C9B111900C7FE6C + + prevStack + + CEF411790A110C7F00E7F110 + CE6E6AE60AA528B2002F29BE + CE6E6AE70AA528B2002F29BE + CE17C9AC0B04D16D0023E222 + CE17C9AD0B04D16D0023E222 + CE17CA230B04E15F0023E222 + CED963600B0F954E00DDBB8C + CE24EDA00B552A5D00DDF502 + + + SplitCount + 1 + + StatusBarVisibility + + + GeometryConfiguration + + Frame + {{0, 0}, {1150, 544}} + RubberWindowFrame + 1 55 1366 823 0 0 1440 878 + + Module + PBXNavigatorGroup + Proportion + 544pt + + + ContentConfiguration + + PBXProjectModuleGUID + 1CE0B20506471E060097A5F4 + PBXProjectModuleLabel + Detail + + GeometryConfiguration + + Frame + {{0, 549}, {1150, 233}} + RubberWindowFrame + 1 55 1366 823 0 0 1440 878 + + Module + XCDetailModule + Proportion + 233pt + + + Proportion + 1150pt + + + Name + Project + ServiceClasses + + XCModuleDock + PBXSmartGroupTreeModule + XCModuleDock + PBXNavigatorGroup + XCDetailModule + + TableOfContents + + CE6D39CF0C9B114700C7FE6C + 1CE0B1FE06471DED0097A5F4 + CE6D39D00C9B114700C7FE6C + 1CE0B20306471E060097A5F4 + 1CE0B20506471E060097A5F4 + + ToolbarConfiguration + xcode.toolbar.config.default + + + ControllerClassBaseName + + IconName + WindowOfProject + Identifier + perspective.morph + IsVertical + 0 + Layout + + + BecomeActive + 1 + ContentConfiguration + + PBXBottomSmartGroupGIDs + + 1C37FBAC04509CD000000102 + 1C37FAAC04509CD000000102 + 1C08E77C0454961000C914BD + 1C37FABC05509CD000000102 + 1C37FABC05539CD112110102 + E2644B35053B69B200211256 + 1C37FABC04509CD000100104 + 1CC0EA4004350EF90044410B + 1CC0EA4004350EF90041110B + + PBXProjectModuleGUID + 11E0B1FE06471DED0097A5F4 + PBXProjectModuleLabel + Files + PBXProjectStructureProvided + yes + PBXSmartGroupTreeModuleColumnData + + PBXSmartGroupTreeModuleColumnWidthsKey + + 186 + + PBXSmartGroupTreeModuleColumnsKey_v4 + + MainColumn + + + PBXSmartGroupTreeModuleOutlineStateKey_v7 + + PBXSmartGroupTreeModuleOutlineStateExpansionKey + + 29B97314FDCFA39411CA2CEA + 1C37FABC05509CD000000102 + + PBXSmartGroupTreeModuleOutlineStateSelectionKey + + + 0 + + + PBXSmartGroupTreeModuleOutlineStateVisibleRectKey + {{0, 0}, {186, 337}} + + PBXTopSmartGroupGIDs + + XCIncludePerspectivesSwitch + 1 + XCSharingToken + com.apple.Xcode.GFSharingToken + + GeometryConfiguration + + Frame + {{0, 0}, {203, 355}} + GroupTreeTableConfiguration + + MainColumn + 186 + + RubberWindowFrame + 373 269 690 397 0 0 1440 878 + + Module + PBXSmartGroupTreeModule + Proportion + 100% + + + Name + Morph + PreferredWidth + 300 + ServiceClasses + + XCModuleDock + PBXSmartGroupTreeModule + + TableOfContents + + 11E0B1FE06471DED0097A5F4 + + ToolbarConfiguration + xcode.toolbar.config.default.short + + + PerspectivesBarVisible + + ShelfIsVisible + + SourceDescription + file at '/System/Library/PrivateFrameworks/DevToolsInterface.framework/Versions/A/Resources/XCPerspectivesSpecificationMode1.xcperspec' + StatusbarIsVisible + + TimeStamp + 0.0 + ToolbarDisplayMode + 1 + ToolbarIsVisible + + ToolbarSizeMode + 1 + Type + Perspectives + UpdateMessage + The Default Workspace in this version of Xcode now includes support to hide and show the detail view (what has been referred to as the "Metro-Morph" feature). You must discard your current Default Workspace settings and update to the latest Default Workspace in order to gain this feature. Do you wish to update to the latest Workspace defaults for project '%@'? + WindowJustification + 5 + WindowOrderList + + 1C0AD2B3069F1EA900FABCE6 + CE381CCE09914BC8003581CE + /Users/hsoft/src/dupeguru_cocoa/dupeguru.xcodeproj + + WindowString + 1 55 1366 823 0 0 1440 878 + WindowTools + + + FirstTimeWindowDisplayed + + Identifier + windowTool.build + IsVertical + + Layout + + + Dock + + + ContentConfiguration + + PBXProjectModuleGUID + 1CD0528F0623707200166675 + PBXProjectModuleLabel + + StatusBarVisibility + + + GeometryConfiguration + + Frame + {{0, 0}, {1366, 540}} + RubberWindowFrame + 0 56 1366 822 0 0 1440 878 + + Module + PBXNavigatorGroup + Proportion + 540pt + + + ContentConfiguration + + PBXProjectModuleGUID + XCMainBuildResultsModuleGUID + PBXProjectModuleLabel + Build + XCBuildResultsTrigger_Collapse + 1021 + XCBuildResultsTrigger_Open + 1011 + + GeometryConfiguration + + Frame + {{0, 545}, {1366, 236}} + RubberWindowFrame + 0 56 1366 822 0 0 1440 878 + + Module + PBXBuildResultsModule + Proportion + 236pt + + + Proportion + 781pt + + + Name + Build Results + ServiceClasses + + PBXBuildResultsModule + + StatusbarIsVisible + + TableOfContents + + CE381CCE09914BC8003581CE + CE6D39D10C9B114700C7FE6C + 1CD0528F0623707200166675 + XCMainBuildResultsModuleGUID + + ToolbarConfiguration + xcode.toolbar.config.build + WindowString + 0 56 1366 822 0 0 1440 878 + WindowToolGUID + CE381CCE09914BC8003581CE + WindowToolIsVisible + + + + FirstTimeWindowDisplayed + + Identifier + windowTool.debugger + IsVertical + + Layout + + + Dock + + + ContentConfiguration + + Debugger + + HorizontalSplitView + + _collapsingFrameDimension + 0.0 + _indexOfCollapsedView + 0 + _percentageOfCollapsedView + 0.0 + isCollapsed + yes + sizes + + {{0, 0}, {0, 258}} + {{0, 0}, {1024, 258}} + + + VerticalSplitView + + _collapsingFrameDimension + 0.0 + _indexOfCollapsedView + 0 + _percentageOfCollapsedView + 0.0 + isCollapsed + yes + sizes + + {{0, 0}, {1024, 258}} + {{0, 258}, {1024, 387}} + + + + LauncherConfigVersion + 8 + PBXProjectModuleGUID + 1C162984064C10D400B95A72 + PBXProjectModuleLabel + Debug - GLUTExamples (Underwater) + + GeometryConfiguration + + DebugConsoleDrawerSize + {100, 120} + DebugConsoleVisible + None + DebugConsoleWindowFrame + {{200, 200}, {500, 300}} + DebugSTDIOWindowFrame + {{200, 200}, {500, 300}} + Frame + {{0, 0}, {1024, 645}} + RubberWindowFrame + 342 192 1024 686 0 0 1440 878 + + Module + PBXDebugSessionModule + Proportion + 645pt + + + Proportion + 645pt + + + Name + Debugger + ServiceClasses + + PBXDebugSessionModule + + StatusbarIsVisible + + TableOfContents + + 1CD10A99069EF8BA00B06720 + CE24EDA90B552A6300DDF502 + 1C162984064C10D400B95A72 + CE24EDAA0B552A6300DDF502 + CE24EDAB0B552A6300DDF502 + CE24EDAC0B552A6300DDF502 + CE24EDAD0B552A6300DDF502 + CE24EDAE0B552A6300DDF502 + CE24EDAF0B552A6300DDF502 + + ToolbarConfiguration + xcode.toolbar.config.debug + WindowString + 342 192 1024 686 0 0 1440 878 + WindowToolGUID + 1CD10A99069EF8BA00B06720 + WindowToolIsVisible + + + + FirstTimeWindowDisplayed + + Identifier + windowTool.find + IsVertical + + Layout + + + Dock + + + Dock + + + ContentConfiguration + + PBXProjectModuleGUID + 1CDD528C0622207200134675 + PBXProjectModuleLabel + AppDelegate.m + StatusBarVisibility + + + GeometryConfiguration + + Frame + {{0, 0}, {781, 212}} + RubberWindowFrame + 84 374 781 470 0 0 1440 878 + + Module + PBXNavigatorGroup + Proportion + 781pt + + + Proportion + 212pt + + + BecomeActive + + ContentConfiguration + + PBXProjectModuleGUID + 1CD0528E0623707200166675 + PBXProjectModuleLabel + Project Find + + GeometryConfiguration + + Frame + {{0, 217}, {781, 212}} + RubberWindowFrame + 84 374 781 470 0 0 1440 878 + + Module + PBXProjectFindModule + Proportion + 212pt + + + Proportion + 429pt + + + Name + Project Find + ServiceClasses + + PBXProjectFindModule + + StatusbarIsVisible + + TableOfContents + + 1C530D57069F1CE1000CFCEE + CECEEB510B0CAE8800E6972C + CECEEB520B0CAE8800E6972C + 1CDD528C0622207200134675 + 1CD0528E0623707200166675 + + WindowString + 84 374 781 470 0 0 1440 878 + WindowToolGUID + 1C530D57069F1CE1000CFCEE + WindowToolIsVisible + + + + Identifier + MENUSEPARATOR + + + FirstTimeWindowDisplayed + + Identifier + windowTool.debuggerConsole + IsVertical + + Layout + + + Dock + + + ContentConfiguration + + PBXProjectModuleGUID + 1C78EAAC065D492600B07095 + PBXProjectModuleLabel + Debugger Console + + GeometryConfiguration + + Frame + {{0, 0}, {440, 358}} + RubberWindowFrame + 72 414 440 400 0 0 1440 878 + + Module + PBXDebugCLIModule + Proportion + 358pt + + + Proportion + 359pt + + + Name + Debugger Console + ServiceClasses + + PBXDebugCLIModule + + StatusbarIsVisible + + TableOfContents + + CECD0ADE099294C1003DC359 + CE24EDB00B552A6300DDF502 + 1C78EAAC065D492600B07095 + + WindowString + 72 414 440 400 0 0 1440 878 + WindowToolGUID + CECD0ADE099294C1003DC359 + WindowToolIsVisible + + + + FirstTimeWindowDisplayed + + Identifier + windowTool.run + IsVertical + + Layout + + + Dock + + + ContentConfiguration + + LauncherConfigVersion + 3 + PBXProjectModuleGUID + 1CD0528B0623707200166675 + PBXProjectModuleLabel + Run + Runner + + HorizontalSplitView + + _collapsingFrameDimension + 0.0 + _indexOfCollapsedView + 0 + _percentageOfCollapsedView + 0.0 + isCollapsed + yes + sizes + + {{0, 0}, {367, 168}} + {{0, 173}, {367, 270}} + + + VerticalSplitView + + _collapsingFrameDimension + 0.0 + _indexOfCollapsedView + 0 + _percentageOfCollapsedView + 0.0 + isCollapsed + yes + sizes + + {{0, 0}, {406, 443}} + {{411, 0}, {517, 443}} + + + + + GeometryConfiguration + + Frame + {{0, 0}, {1024, 645}} + RubberWindowFrame + 343 192 1024 686 0 0 1440 878 + + Module + PBXRunSessionModule + Proportion + 645pt + + + Proportion + 645pt + + + Name + Run Log + ServiceClasses + + PBXRunSessionModule + + StatusbarIsVisible + + TableOfContents + + 1C0AD2B3069F1EA900FABCE6 + CE6D39D20C9B114700C7FE6C + 1CD0528B0623707200166675 + CE6D39D30C9B114700C7FE6C + + ToolbarConfiguration + xcode.toolbar.config.run + WindowString + 343 192 1024 686 0 0 1440 878 + WindowToolGUID + 1C0AD2B3069F1EA900FABCE6 + WindowToolIsVisible + + + + Identifier + windowTool.scm + Layout + + + Dock + + + ContentConfiguration + + PBXProjectModuleGUID + 1C78EAB2065D492600B07095 + PBXProjectModuleLabel + <No Editor> + PBXSplitModuleInNavigatorKey + + Split0 + + PBXProjectModuleGUID + 1C78EAB3065D492600B07095 + + SplitCount + 1 + + StatusBarVisibility + 1 + + GeometryConfiguration + + Frame + {{0, 0}, {452, 0}} + RubberWindowFrame + 743 379 452 308 0 0 1280 1002 + + Module + PBXNavigatorGroup + Proportion + 0pt + + + BecomeActive + 1 + ContentConfiguration + + PBXProjectModuleGUID + 1CD052920623707200166675 + PBXProjectModuleLabel + SCM + + GeometryConfiguration + + ConsoleFrame + {{0, 259}, {452, 0}} + Frame + {{0, 7}, {452, 259}} + RubberWindowFrame + 743 379 452 308 0 0 1280 1002 + TableConfiguration + + Status + 30 + FileName + 199 + Path + 197.09500122070312 + + TableFrame + {{0, 0}, {452, 250}} + + Module + PBXCVSModule + Proportion + 262pt + + + Proportion + 266pt + + + Name + SCM + ServiceClasses + + PBXCVSModule + + StatusbarIsVisible + 1 + TableOfContents + + 1C78EAB4065D492600B07095 + 1C78EAB5065D492600B07095 + 1C78EAB2065D492600B07095 + 1CD052920623707200166675 + + ToolbarConfiguration + xcode.toolbar.config.scm + WindowString + 743 379 452 308 0 0 1280 1002 + + + FirstTimeWindowDisplayed + + Identifier + windowTool.breakpoints + IsVertical + + Layout + + + Dock + + + ContentConfiguration + + PBXBottomSmartGroupGIDs + + 1C77FABC04509CD000000102 + + PBXProjectModuleGUID + 1CE0B1FE06471DED0097A5F4 + PBXProjectModuleLabel + Files + PBXProjectStructureProvided + no + PBXSmartGroupTreeModuleColumnData + + PBXSmartGroupTreeModuleColumnWidthsKey + + 168 + + PBXSmartGroupTreeModuleColumnsKey_v4 + + MainColumn + + + PBXSmartGroupTreeModuleOutlineStateKey_v7 + + PBXSmartGroupTreeModuleOutlineStateExpansionKey + + 1C77FABC04509CD000000102 + + PBXSmartGroupTreeModuleOutlineStateSelectionKey + + + 0 + + + PBXSmartGroupTreeModuleOutlineStateVisibleRectKey + {{0, 0}, {168, 350}} + + PBXTopSmartGroupGIDs + + XCIncludePerspectivesSwitch + + + GeometryConfiguration + + Frame + {{0, 0}, {185, 368}} + GroupTreeTableConfiguration + + MainColumn + 168 + + RubberWindowFrame + 21 314 744 409 0 0 1024 746 + + Module + PBXSmartGroupTreeModule + Proportion + 185pt + + + BecomeActive + + ContentConfiguration + + PBXProjectModuleGUID + 1CA1AED706398EBD00589147 + PBXProjectModuleLabel + Detail + + GeometryConfiguration + + Frame + {{190, 0}, {554, 368}} + RubberWindowFrame + 21 314 744 409 0 0 1024 746 + + Module + XCDetailModule + Proportion + 554pt + + + Proportion + 368pt + + + MajorVersion + 2 + MinorVersion + 0 + Name + Breakpoints + ServiceClasses + + PBXSmartGroupTreeModule + XCDetailModule + + StatusbarIsVisible + + TableOfContents + + CEDA9EAC09D2BBCE00741F3F + CEDA9EAD09D2BBCE00741F3F + 1CE0B1FE06471DED0097A5F4 + 1CA1AED706398EBD00589147 + + ToolbarConfiguration + xcode.toolbar.config.breakpoints + WindowString + 21 314 744 409 0 0 1024 746 + WindowToolGUID + CEDA9EAC09D2BBCE00741F3F + WindowToolIsVisible + + + + Identifier + windowTool.debugAnimator + Layout + + + Dock + + + Module + PBXNavigatorGroup + Proportion + 100% + + + Proportion + 100% + + + Name + Debug Visualizer + ServiceClasses + + PBXNavigatorGroup + + StatusbarIsVisible + 1 + ToolbarConfiguration + xcode.toolbar.config.debugAnimator + WindowString + 100 100 700 500 0 0 1280 1002 + + + Identifier + windowTool.bookmarks + Layout + + + Dock + + + Module + PBXBookmarksModule + Proportion + 100% + + + Proportion + 100% + + + Name + Bookmarks + ServiceClasses + + PBXBookmarksModule + + StatusbarIsVisible + 0 + WindowString + 538 42 401 187 0 0 1280 1002 + + + Identifier + windowTool.classBrowser + Layout + + + Dock + + + BecomeActive + 1 + ContentConfiguration + + OptionsSetName + Hierarchy, all classes + PBXProjectModuleGUID + 1CA6456E063B45B4001379D8 + PBXProjectModuleLabel + Class Browser - NSObject + + GeometryConfiguration + + ClassesFrame + {{0, 0}, {374, 96}} + ClassesTreeTableConfiguration + + PBXClassNameColumnIdentifier + 208 + PBXClassBookColumnIdentifier + 22 + + Frame + {{0, 0}, {630, 331}} + MembersFrame + {{0, 105}, {374, 395}} + MembersTreeTableConfiguration + + PBXMemberTypeIconColumnIdentifier + 22 + PBXMemberNameColumnIdentifier + 216 + PBXMemberTypeColumnIdentifier + 97 + PBXMemberBookColumnIdentifier + 22 + + PBXModuleWindowStatusBarHidden2 + 1 + RubberWindowFrame + 385 179 630 352 0 0 1440 878 + + Module + PBXClassBrowserModule + Proportion + 332pt + + + Proportion + 332pt + + + Name + Class Browser + ServiceClasses + + PBXClassBrowserModule + + StatusbarIsVisible + 0 + TableOfContents + + 1C0AD2AF069F1E9B00FABCE6 + 1C0AD2B0069F1E9B00FABCE6 + 1CA6456E063B45B4001379D8 + + ToolbarConfiguration + xcode.toolbar.config.classbrowser + WindowString + 385 179 630 352 0 0 1440 878 + WindowToolGUID + 1C0AD2AF069F1E9B00FABCE6 + WindowToolIsVisible + 0 + + + + diff --git a/se/cocoa/dupeguru.xcodeproj/project.pbxproj b/se/cocoa/dupeguru.xcodeproj/project.pbxproj new file mode 100644 index 00000000..3755a27e --- /dev/null +++ b/se/cocoa/dupeguru.xcodeproj/project.pbxproj @@ -0,0 +1,548 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 44; + objects = { + +/* Begin PBXBuildFile section */ + 8D11072A0486CEB800E47090 /* MainMenu.nib in Resources */ = {isa = PBXBuildFile; fileRef = 29B97318FDCFA39411CA2CEA /* MainMenu.nib */; }; + 8D11072B0486CEB800E47090 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 089C165CFE840E0CC02AAC07 /* InfoPlist.strings */; }; + 8D11072D0486CEB800E47090 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 29B97316FDCFA39411CA2CEA /* main.m */; settings = {ATTRIBUTES = (); }; }; + 8D11072F0486CEB800E47090 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */; }; + CE073F6309CAE1A3005C1D2F /* dupeguru_help in Resources */ = {isa = PBXBuildFile; fileRef = CE073F5409CAE1A3005C1D2F /* dupeguru_help */; }; + CE0D67640ABC2D3E00E2FFD9 /* dg.xsl in Resources */ = {isa = PBXBuildFile; fileRef = CE0D67620ABC2D3E00E2FFD9 /* dg.xsl */; }; + CE0D67650ABC2D3E00E2FFD9 /* hardcoded.css in Resources */ = {isa = PBXBuildFile; fileRef = CE0D67630ABC2D3E00E2FFD9 /* hardcoded.css */; }; + CE381C9609914ACE003581CE /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = CE381C9409914ACE003581CE /* AppDelegate.m */; }; + CE381C9C09914ADF003581CE /* ResultWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = CE381C9A09914ADF003581CE /* ResultWindow.m */; }; + CE381D0509915304003581CE /* dg_cocoa.plugin in Resources */ = {isa = PBXBuildFile; fileRef = CE381CF509915304003581CE /* dg_cocoa.plugin */; }; + CE3AA46709DB207900DB3A21 /* Directories.nib in Resources */ = {isa = PBXBuildFile; fileRef = CE3AA46509DB207900DB3A21 /* Directories.nib */; }; + CE45579B0AE3BC2B005A9546 /* Sparkle.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE45579A0AE3BC2B005A9546 /* Sparkle.framework */; }; + CE4557B40AE3BC50005A9546 /* Sparkle.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = CE45579A0AE3BC2B005A9546 /* Sparkle.framework */; }; + CE68EE6809ABC48000971085 /* DirectoryPanel.m in Sources */ = {isa = PBXBuildFile; fileRef = CE68EE6609ABC48000971085 /* DirectoryPanel.m */; }; + CE848A1909DD85810004CB44 /* Consts.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = CE848A1809DD85810004CB44 /* Consts.h */; }; + CECA899909DB12CA00A3D774 /* Details.nib in Resources */ = {isa = PBXBuildFile; fileRef = CECA899709DB12CA00A3D774 /* Details.nib */; }; + CECA899C09DB132E00A3D774 /* DetailsPanel.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = CECA899A09DB132E00A3D774 /* DetailsPanel.h */; }; + CECA899D09DB132E00A3D774 /* DetailsPanel.m in Sources */ = {isa = PBXBuildFile; fileRef = CECA899B09DB132E00A3D774 /* DetailsPanel.m */; }; + CEEB135209C837A2004D2330 /* dupeguru.icns in Resources */ = {isa = PBXBuildFile; fileRef = CEEB135109C837A2004D2330 /* dupeguru.icns */; }; + CEF7823809C8AA0200EF38FF /* gear.png in Resources */ = {isa = PBXBuildFile; fileRef = CEF7823709C8AA0200EF38FF /* gear.png */; }; + CEFC294609C89E3D00D9F998 /* folder32.png in Resources */ = {isa = PBXBuildFile; fileRef = CEFC294509C89E3D00D9F998 /* folder32.png */; }; + CEFC295509C89FF200D9F998 /* details32.png in Resources */ = {isa = PBXBuildFile; fileRef = CEFC295309C89FF200D9F998 /* details32.png */; }; + CEFC295609C89FF200D9F998 /* preferences32.png in Resources */ = {isa = PBXBuildFile; fileRef = CEFC295409C89FF200D9F998 /* preferences32.png */; }; + CEFC295E09C8A0B000D9F998 /* dg_logo32.png in Resources */ = {isa = PBXBuildFile; fileRef = CEFC295D09C8A0B000D9F998 /* dg_logo32.png */; }; + CEFC7F9E0FC9517500CD5728 /* Dialogs.m in Sources */ = {isa = PBXBuildFile; fileRef = CEFC7F8B0FC9517500CD5728 /* Dialogs.m */; }; + CEFC7F9F0FC9517500CD5728 /* HSErrorReportWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = CEFC7F8D0FC9517500CD5728 /* HSErrorReportWindow.m */; }; + CEFC7FA00FC9517500CD5728 /* Outline.m in Sources */ = {isa = PBXBuildFile; fileRef = CEFC7F8F0FC9517500CD5728 /* Outline.m */; }; + CEFC7FA10FC9517500CD5728 /* ProgressController.m in Sources */ = {isa = PBXBuildFile; fileRef = CEFC7F910FC9517500CD5728 /* ProgressController.m */; }; + CEFC7FA20FC9517500CD5728 /* RecentDirectories.m in Sources */ = {isa = PBXBuildFile; fileRef = CEFC7F950FC9517500CD5728 /* RecentDirectories.m */; }; + CEFC7FA30FC9517500CD5728 /* RegistrationInterface.m in Sources */ = {isa = PBXBuildFile; fileRef = CEFC7F970FC9517500CD5728 /* RegistrationInterface.m */; }; + CEFC7FA40FC9517500CD5728 /* Table.m in Sources */ = {isa = PBXBuildFile; fileRef = CEFC7F990FC9517500CD5728 /* Table.m */; }; + CEFC7FA50FC9517500CD5728 /* Utils.m in Sources */ = {isa = PBXBuildFile; fileRef = CEFC7F9B0FC9517500CD5728 /* Utils.m */; }; + CEFC7FA60FC9517500CD5728 /* ValueTransformers.m in Sources */ = {isa = PBXBuildFile; fileRef = CEFC7F9D0FC9517500CD5728 /* ValueTransformers.m */; }; + CEFC7FAD0FC9518A00CD5728 /* ErrorReportWindow.xib in Resources */ = {isa = PBXBuildFile; fileRef = CEFC7FA70FC9518A00CD5728 /* ErrorReportWindow.xib */; }; + CEFC7FAE0FC9518A00CD5728 /* progress.nib in Resources */ = {isa = PBXBuildFile; fileRef = CEFC7FA90FC9518A00CD5728 /* progress.nib */; }; + CEFC7FAF0FC9518A00CD5728 /* registration.nib in Resources */ = {isa = PBXBuildFile; fileRef = CEFC7FAB0FC9518A00CD5728 /* registration.nib */; }; + CEFC7FB90FC951A700CD5728 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = CEFC7FB20FC951A700CD5728 /* AppDelegate.m */; }; + CEFC7FBA0FC951A700CD5728 /* DirectoryPanel.m in Sources */ = {isa = PBXBuildFile; fileRef = CEFC7FB50FC951A700CD5728 /* DirectoryPanel.m */; }; + CEFC7FBB0FC951A700CD5728 /* ResultWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = CEFC7FB80FC951A700CD5728 /* ResultWindow.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + CECC02B709A36E8200CC0A94 /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + CE4557B40AE3BC50005A9546 /* Sparkle.framework in CopyFiles */, + CECA899C09DB132E00A3D774 /* DetailsPanel.h in CopyFiles */, + CE848A1909DD85810004CB44 /* Consts.h in CopyFiles */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 089C165DFE840E0CC02AAC07 /* English */ = {isa = PBXFileReference; fileEncoding = 10; lastKnownFileType = text.plist.strings; name = English; path = English.lproj/InfoPlist.strings; sourceTree = ""; }; + 1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = /System/Library/Frameworks/Cocoa.framework; sourceTree = ""; }; + 13E42FB307B3F0F600E4EEF1 /* CoreData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreData.framework; path = /System/Library/Frameworks/CoreData.framework; sourceTree = ""; }; + 29B97316FDCFA39411CA2CEA /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 29B97319FDCFA39411CA2CEA /* English */ = {isa = PBXFileReference; lastKnownFileType = wrapper.nib; name = English; path = English.lproj/MainMenu.nib; sourceTree = ""; }; + 29B97324FDCFA39411CA2CEA /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = /System/Library/Frameworks/AppKit.framework; sourceTree = ""; }; + 29B97325FDCFA39411CA2CEA /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = /System/Library/Frameworks/Foundation.framework; sourceTree = ""; }; + 8D1107310486CEB800E47090 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = SOURCE_ROOT; }; + 8D1107320486CEB800E47090 /* dupeGuru.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = dupeGuru.app; sourceTree = BUILT_PRODUCTS_DIR; }; + CE073F5409CAE1A3005C1D2F /* dupeguru_help */ = {isa = PBXFileReference; lastKnownFileType = folder; name = dupeguru_help; path = help/dupeguru_help; sourceTree = ""; }; + CE0D67620ABC2D3E00E2FFD9 /* dg.xsl */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = text.xml; name = dg.xsl; path = w3/dg.xsl; sourceTree = SOURCE_ROOT; }; + CE0D67630ABC2D3E00E2FFD9 /* hardcoded.css */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = text; name = hardcoded.css; path = w3/hardcoded.css; sourceTree = SOURCE_ROOT; }; + CE381C9409914ACE003581CE /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = SOURCE_ROOT; }; + CE381C9509914ACE003581CE /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = SOURCE_ROOT; }; + CE381C9A09914ADF003581CE /* ResultWindow.m */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.objc; path = ResultWindow.m; sourceTree = SOURCE_ROOT; }; + CE381C9B09914ADF003581CE /* ResultWindow.h */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.h; path = ResultWindow.h; sourceTree = SOURCE_ROOT; }; + CE381CF509915304003581CE /* dg_cocoa.plugin */ = {isa = PBXFileReference; lastKnownFileType = folder; name = dg_cocoa.plugin; path = py/dist/dg_cocoa.plugin; sourceTree = SOURCE_ROOT; }; + CE3AA46609DB207900DB3A21 /* English */ = {isa = PBXFileReference; lastKnownFileType = wrapper.nib; name = English; path = English.lproj/Directories.nib; sourceTree = ""; }; + CE45579A0AE3BC2B005A9546 /* Sparkle.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Sparkle.framework; path = /Library/Frameworks/Sparkle.framework; sourceTree = ""; }; + CE68EE6509ABC48000971085 /* DirectoryPanel.h */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.h; path = DirectoryPanel.h; sourceTree = SOURCE_ROOT; }; + CE68EE6609ABC48000971085 /* DirectoryPanel.m */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.objc; path = DirectoryPanel.m; sourceTree = SOURCE_ROOT; }; + CE848A1809DD85810004CB44 /* Consts.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Consts.h; sourceTree = ""; }; + CECA899809DB12CA00A3D774 /* English */ = {isa = PBXFileReference; lastKnownFileType = wrapper.nib; name = English; path = English.lproj/Details.nib; sourceTree = ""; }; + CECA899A09DB132E00A3D774 /* DetailsPanel.h */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.h; path = DetailsPanel.h; sourceTree = ""; }; + CECA899B09DB132E00A3D774 /* DetailsPanel.m */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.objc; path = DetailsPanel.m; sourceTree = ""; }; + CEEB135109C837A2004D2330 /* dupeguru.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; path = dupeguru.icns; sourceTree = ""; }; + CEF7823709C8AA0200EF38FF /* gear.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = gear.png; path = images/gear.png; sourceTree = ""; }; + CEFC294509C89E3D00D9F998 /* folder32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = folder32.png; path = images/folder32.png; sourceTree = SOURCE_ROOT; }; + CEFC295309C89FF200D9F998 /* details32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = details32.png; path = images/details32.png; sourceTree = SOURCE_ROOT; }; + CEFC295409C89FF200D9F998 /* preferences32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = preferences32.png; path = images/preferences32.png; sourceTree = SOURCE_ROOT; }; + CEFC295D09C8A0B000D9F998 /* dg_logo32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = dg_logo32.png; path = images/dg_logo32.png; sourceTree = SOURCE_ROOT; }; + CEFC7F8A0FC9517500CD5728 /* Dialogs.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Dialogs.h; path = cocoalib/Dialogs.h; sourceTree = SOURCE_ROOT; }; + CEFC7F8B0FC9517500CD5728 /* Dialogs.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Dialogs.m; path = cocoalib/Dialogs.m; sourceTree = SOURCE_ROOT; }; + CEFC7F8C0FC9517500CD5728 /* HSErrorReportWindow.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = HSErrorReportWindow.h; path = cocoalib/HSErrorReportWindow.h; sourceTree = SOURCE_ROOT; }; + CEFC7F8D0FC9517500CD5728 /* HSErrorReportWindow.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = HSErrorReportWindow.m; path = cocoalib/HSErrorReportWindow.m; sourceTree = SOURCE_ROOT; }; + CEFC7F8E0FC9517500CD5728 /* Outline.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Outline.h; path = cocoalib/Outline.h; sourceTree = SOURCE_ROOT; }; + CEFC7F8F0FC9517500CD5728 /* Outline.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Outline.m; path = cocoalib/Outline.m; sourceTree = SOURCE_ROOT; }; + CEFC7F900FC9517500CD5728 /* ProgressController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ProgressController.h; path = cocoalib/ProgressController.h; sourceTree = SOURCE_ROOT; }; + CEFC7F910FC9517500CD5728 /* ProgressController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ProgressController.m; path = cocoalib/ProgressController.m; sourceTree = SOURCE_ROOT; }; + CEFC7F920FC9517500CD5728 /* PyApp.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PyApp.h; path = cocoalib/PyApp.h; sourceTree = SOURCE_ROOT; }; + CEFC7F930FC9517500CD5728 /* PyRegistrable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PyRegistrable.h; path = cocoalib/PyRegistrable.h; sourceTree = SOURCE_ROOT; }; + CEFC7F940FC9517500CD5728 /* RecentDirectories.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RecentDirectories.h; path = cocoalib/RecentDirectories.h; sourceTree = SOURCE_ROOT; }; + CEFC7F950FC9517500CD5728 /* RecentDirectories.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RecentDirectories.m; path = cocoalib/RecentDirectories.m; sourceTree = SOURCE_ROOT; }; + CEFC7F960FC9517500CD5728 /* RegistrationInterface.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RegistrationInterface.h; path = cocoalib/RegistrationInterface.h; sourceTree = SOURCE_ROOT; }; + CEFC7F970FC9517500CD5728 /* RegistrationInterface.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RegistrationInterface.m; path = cocoalib/RegistrationInterface.m; sourceTree = SOURCE_ROOT; }; + CEFC7F980FC9517500CD5728 /* Table.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Table.h; path = cocoalib/Table.h; sourceTree = SOURCE_ROOT; }; + CEFC7F990FC9517500CD5728 /* Table.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Table.m; path = cocoalib/Table.m; sourceTree = SOURCE_ROOT; }; + CEFC7F9A0FC9517500CD5728 /* Utils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Utils.h; path = cocoalib/Utils.h; sourceTree = SOURCE_ROOT; }; + CEFC7F9B0FC9517500CD5728 /* Utils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Utils.m; path = cocoalib/Utils.m; sourceTree = SOURCE_ROOT; }; + CEFC7F9C0FC9517500CD5728 /* ValueTransformers.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ValueTransformers.h; path = cocoalib/ValueTransformers.h; sourceTree = SOURCE_ROOT; }; + CEFC7F9D0FC9517500CD5728 /* ValueTransformers.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ValueTransformers.m; path = cocoalib/ValueTransformers.m; sourceTree = SOURCE_ROOT; }; + CEFC7FA80FC9518A00CD5728 /* English */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = English; path = cocoalib/English.lproj/ErrorReportWindow.xib; sourceTree = ""; }; + CEFC7FAA0FC9518A00CD5728 /* English */ = {isa = PBXFileReference; lastKnownFileType = wrapper.nib; name = English; path = cocoalib/English.lproj/progress.nib; sourceTree = ""; }; + CEFC7FAC0FC9518A00CD5728 /* English */ = {isa = PBXFileReference; lastKnownFileType = wrapper.nib; name = English; path = cocoalib/English.lproj/registration.nib; sourceTree = ""; }; + CEFC7FB10FC951A700CD5728 /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = dgbase/AppDelegate.h; sourceTree = SOURCE_ROOT; }; + CEFC7FB20FC951A700CD5728 /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AppDelegate.m; path = dgbase/AppDelegate.m; sourceTree = SOURCE_ROOT; }; + CEFC7FB30FC951A700CD5728 /* Consts.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Consts.h; path = dgbase/Consts.h; sourceTree = SOURCE_ROOT; }; + CEFC7FB40FC951A700CD5728 /* DirectoryPanel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = DirectoryPanel.h; path = dgbase/DirectoryPanel.h; sourceTree = SOURCE_ROOT; }; + CEFC7FB50FC951A700CD5728 /* DirectoryPanel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = DirectoryPanel.m; path = dgbase/DirectoryPanel.m; sourceTree = SOURCE_ROOT; }; + CEFC7FB60FC951A700CD5728 /* PyDupeGuru.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PyDupeGuru.h; path = dgbase/PyDupeGuru.h; sourceTree = SOURCE_ROOT; }; + CEFC7FB70FC951A700CD5728 /* ResultWindow.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ResultWindow.h; path = dgbase/ResultWindow.h; sourceTree = SOURCE_ROOT; }; + CEFC7FB80FC951A700CD5728 /* ResultWindow.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ResultWindow.m; path = dgbase/ResultWindow.m; sourceTree = SOURCE_ROOT; }; + CEFF18A009A4D387005E6321 /* PyDupeGuru.h */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.h; path = PyDupeGuru.h; sourceTree = SOURCE_ROOT; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 8D11072E0486CEB800E47090 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 8D11072F0486CEB800E47090 /* Cocoa.framework in Frameworks */, + CE45579B0AE3BC2B005A9546 /* Sparkle.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 080E96DDFE201D6D7F000001 /* DGSE */ = { + isa = PBXGroup; + children = ( + CE381C9509914ACE003581CE /* AppDelegate.h */, + CE381C9409914ACE003581CE /* AppDelegate.m */, + CE848A1809DD85810004CB44 /* Consts.h */, + CECA899A09DB132E00A3D774 /* DetailsPanel.h */, + CECA899B09DB132E00A3D774 /* DetailsPanel.m */, + CE68EE6509ABC48000971085 /* DirectoryPanel.h */, + CE68EE6609ABC48000971085 /* DirectoryPanel.m */, + CEFF18A009A4D387005E6321 /* PyDupeGuru.h */, + CE381C9B09914ADF003581CE /* ResultWindow.h */, + CE381C9A09914ADF003581CE /* ResultWindow.m */, + 29B97316FDCFA39411CA2CEA /* main.m */, + ); + name = DGSE; + sourceTree = ""; + }; + 1058C7A0FEA54F0111CA2CBB /* Linked Frameworks */ = { + isa = PBXGroup; + children = ( + CE45579A0AE3BC2B005A9546 /* Sparkle.framework */, + 1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */, + ); + name = "Linked Frameworks"; + sourceTree = ""; + }; + 1058C7A2FEA54F0111CA2CBB /* Other Frameworks */ = { + isa = PBXGroup; + children = ( + 29B97324FDCFA39411CA2CEA /* AppKit.framework */, + 13E42FB307B3F0F600E4EEF1 /* CoreData.framework */, + 29B97325FDCFA39411CA2CEA /* Foundation.framework */, + ); + name = "Other Frameworks"; + sourceTree = ""; + }; + 19C28FACFE9D520D11CA2CBB /* Products */ = { + isa = PBXGroup; + children = ( + 8D1107320486CEB800E47090 /* dupeGuru.app */, + ); + name = Products; + sourceTree = ""; + }; + 29B97314FDCFA39411CA2CEA /* dupeguru */ = { + isa = PBXGroup; + children = ( + 080E96DDFE201D6D7F000001 /* DGSE */, + CEFC7FB00FC9518F00CD5728 /* dgbase */, + CEFC7F890FC9513600CD5728 /* cocoalib */, + 29B97317FDCFA39411CA2CEA /* Resources */, + 29B97323FDCFA39411CA2CEA /* Frameworks */, + 19C28FACFE9D520D11CA2CBB /* Products */, + ); + name = dupeguru; + sourceTree = ""; + }; + 29B97317FDCFA39411CA2CEA /* Resources */ = { + isa = PBXGroup; + children = ( + CE073F5409CAE1A3005C1D2F /* dupeguru_help */, + CE381CF509915304003581CE /* dg_cocoa.plugin */, + CEFC294309C89E0000D9F998 /* images */, + CE0D67610ABC2D3E00E2FFD9 /* w3 */, + CEEB135109C837A2004D2330 /* dupeguru.icns */, + 8D1107310486CEB800E47090 /* Info.plist */, + 089C165CFE840E0CC02AAC07 /* InfoPlist.strings */, + CECA899709DB12CA00A3D774 /* Details.nib */, + CE3AA46509DB207900DB3A21 /* Directories.nib */, + 29B97318FDCFA39411CA2CEA /* MainMenu.nib */, + ); + name = Resources; + sourceTree = ""; + }; + 29B97323FDCFA39411CA2CEA /* Frameworks */ = { + isa = PBXGroup; + children = ( + 1058C7A0FEA54F0111CA2CBB /* Linked Frameworks */, + 1058C7A2FEA54F0111CA2CBB /* Other Frameworks */, + ); + name = Frameworks; + sourceTree = ""; + }; + CE0D67610ABC2D3E00E2FFD9 /* w3 */ = { + isa = PBXGroup; + children = ( + CE0D67620ABC2D3E00E2FFD9 /* dg.xsl */, + CE0D67630ABC2D3E00E2FFD9 /* hardcoded.css */, + ); + path = w3; + sourceTree = ""; + }; + CEFC294309C89E0000D9F998 /* images */ = { + isa = PBXGroup; + children = ( + CEF7823709C8AA0200EF38FF /* gear.png */, + CEFC295D09C8A0B000D9F998 /* dg_logo32.png */, + CEFC295309C89FF200D9F998 /* details32.png */, + CEFC295409C89FF200D9F998 /* preferences32.png */, + CEFC294509C89E3D00D9F998 /* folder32.png */, + ); + name = images; + sourceTree = ""; + }; + CEFC7F890FC9513600CD5728 /* cocoalib */ = { + isa = PBXGroup; + children = ( + CEFC7FA70FC9518A00CD5728 /* ErrorReportWindow.xib */, + CEFC7FA90FC9518A00CD5728 /* progress.nib */, + CEFC7FAB0FC9518A00CD5728 /* registration.nib */, + CEFC7F8A0FC9517500CD5728 /* Dialogs.h */, + CEFC7F8B0FC9517500CD5728 /* Dialogs.m */, + CEFC7F8C0FC9517500CD5728 /* HSErrorReportWindow.h */, + CEFC7F8D0FC9517500CD5728 /* HSErrorReportWindow.m */, + CEFC7F8E0FC9517500CD5728 /* Outline.h */, + CEFC7F8F0FC9517500CD5728 /* Outline.m */, + CEFC7F900FC9517500CD5728 /* ProgressController.h */, + CEFC7F910FC9517500CD5728 /* ProgressController.m */, + CEFC7F920FC9517500CD5728 /* PyApp.h */, + CEFC7F930FC9517500CD5728 /* PyRegistrable.h */, + CEFC7F940FC9517500CD5728 /* RecentDirectories.h */, + CEFC7F950FC9517500CD5728 /* RecentDirectories.m */, + CEFC7F960FC9517500CD5728 /* RegistrationInterface.h */, + CEFC7F970FC9517500CD5728 /* RegistrationInterface.m */, + CEFC7F980FC9517500CD5728 /* Table.h */, + CEFC7F990FC9517500CD5728 /* Table.m */, + CEFC7F9A0FC9517500CD5728 /* Utils.h */, + CEFC7F9B0FC9517500CD5728 /* Utils.m */, + CEFC7F9C0FC9517500CD5728 /* ValueTransformers.h */, + CEFC7F9D0FC9517500CD5728 /* ValueTransformers.m */, + ); + name = cocoalib; + sourceTree = ""; + }; + CEFC7FB00FC9518F00CD5728 /* dgbase */ = { + isa = PBXGroup; + children = ( + CEFC7FB10FC951A700CD5728 /* AppDelegate.h */, + CEFC7FB20FC951A700CD5728 /* AppDelegate.m */, + CEFC7FB30FC951A700CD5728 /* Consts.h */, + CEFC7FB40FC951A700CD5728 /* DirectoryPanel.h */, + CEFC7FB50FC951A700CD5728 /* DirectoryPanel.m */, + CEFC7FB60FC951A700CD5728 /* PyDupeGuru.h */, + CEFC7FB70FC951A700CD5728 /* ResultWindow.h */, + CEFC7FB80FC951A700CD5728 /* ResultWindow.m */, + ); + name = dgbase; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 8D1107260486CEB800E47090 /* dupeguru */ = { + isa = PBXNativeTarget; + buildConfigurationList = C01FCF4A08A954540054247B /* Build configuration list for PBXNativeTarget "dupeguru" */; + buildPhases = ( + 8D1107290486CEB800E47090 /* Resources */, + 8D11072C0486CEB800E47090 /* Sources */, + 8D11072E0486CEB800E47090 /* Frameworks */, + CECC02B709A36E8200CC0A94 /* CopyFiles */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = dupeguru; + productInstallPath = "$(HOME)/Applications"; + productName = dupeguru; + productReference = 8D1107320486CEB800E47090 /* dupeGuru.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 29B97313FDCFA39411CA2CEA /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = NO; + }; + buildConfigurationList = C01FCF4E08A954540054247B /* Build configuration list for PBXProject "dupeguru" */; + compatibilityVersion = "Xcode 3.0"; + hasScannedForEncodings = 1; + mainGroup = 29B97314FDCFA39411CA2CEA /* dupeguru */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 8D1107260486CEB800E47090 /* dupeguru */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 8D1107290486CEB800E47090 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 8D11072A0486CEB800E47090 /* MainMenu.nib in Resources */, + 8D11072B0486CEB800E47090 /* InfoPlist.strings in Resources */, + CE381D0509915304003581CE /* dg_cocoa.plugin in Resources */, + CE073F6309CAE1A3005C1D2F /* dupeguru_help in Resources */, + CEEB135209C837A2004D2330 /* dupeguru.icns in Resources */, + CEFC294609C89E3D00D9F998 /* folder32.png in Resources */, + CEFC295509C89FF200D9F998 /* details32.png in Resources */, + CEFC295609C89FF200D9F998 /* preferences32.png in Resources */, + CEFC295E09C8A0B000D9F998 /* dg_logo32.png in Resources */, + CEF7823809C8AA0200EF38FF /* gear.png in Resources */, + CECA899909DB12CA00A3D774 /* Details.nib in Resources */, + CE3AA46709DB207900DB3A21 /* Directories.nib in Resources */, + CE0D67640ABC2D3E00E2FFD9 /* dg.xsl in Resources */, + CE0D67650ABC2D3E00E2FFD9 /* hardcoded.css in Resources */, + CEFC7FAD0FC9518A00CD5728 /* ErrorReportWindow.xib in Resources */, + CEFC7FAE0FC9518A00CD5728 /* progress.nib in Resources */, + CEFC7FAF0FC9518A00CD5728 /* registration.nib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 8D11072C0486CEB800E47090 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 8D11072D0486CEB800E47090 /* main.m in Sources */, + CE381C9609914ACE003581CE /* AppDelegate.m in Sources */, + CE381C9C09914ADF003581CE /* ResultWindow.m in Sources */, + CE68EE6809ABC48000971085 /* DirectoryPanel.m in Sources */, + CECA899D09DB132E00A3D774 /* DetailsPanel.m in Sources */, + CEFC7F9E0FC9517500CD5728 /* Dialogs.m in Sources */, + CEFC7F9F0FC9517500CD5728 /* HSErrorReportWindow.m in Sources */, + CEFC7FA00FC9517500CD5728 /* Outline.m in Sources */, + CEFC7FA10FC9517500CD5728 /* ProgressController.m in Sources */, + CEFC7FA20FC9517500CD5728 /* RecentDirectories.m in Sources */, + CEFC7FA30FC9517500CD5728 /* RegistrationInterface.m in Sources */, + CEFC7FA40FC9517500CD5728 /* Table.m in Sources */, + CEFC7FA50FC9517500CD5728 /* Utils.m in Sources */, + CEFC7FA60FC9517500CD5728 /* ValueTransformers.m in Sources */, + CEFC7FB90FC951A700CD5728 /* AppDelegate.m in Sources */, + CEFC7FBA0FC951A700CD5728 /* DirectoryPanel.m in Sources */, + CEFC7FBB0FC951A700CD5728 /* ResultWindow.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 089C165CFE840E0CC02AAC07 /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + 089C165DFE840E0CC02AAC07 /* English */, + ); + name = InfoPlist.strings; + sourceTree = ""; + }; + 29B97318FDCFA39411CA2CEA /* MainMenu.nib */ = { + isa = PBXVariantGroup; + children = ( + 29B97319FDCFA39411CA2CEA /* English */, + ); + name = MainMenu.nib; + sourceTree = SOURCE_ROOT; + }; + CE3AA46509DB207900DB3A21 /* Directories.nib */ = { + isa = PBXVariantGroup; + children = ( + CE3AA46609DB207900DB3A21 /* English */, + ); + name = Directories.nib; + sourceTree = ""; + }; + CECA899709DB12CA00A3D774 /* Details.nib */ = { + isa = PBXVariantGroup; + children = ( + CECA899809DB12CA00A3D774 /* English */, + ); + name = Details.nib; + sourceTree = ""; + }; + CEFC7FA70FC9518A00CD5728 /* ErrorReportWindow.xib */ = { + isa = PBXVariantGroup; + children = ( + CEFC7FA80FC9518A00CD5728 /* English */, + ); + name = ErrorReportWindow.xib; + sourceTree = SOURCE_ROOT; + }; + CEFC7FA90FC9518A00CD5728 /* progress.nib */ = { + isa = PBXVariantGroup; + children = ( + CEFC7FAA0FC9518A00CD5728 /* English */, + ); + name = progress.nib; + sourceTree = SOURCE_ROOT; + }; + CEFC7FAB0FC9518A00CD5728 /* registration.nib */ = { + isa = PBXVariantGroup; + children = ( + CEFC7FAC0FC9518A00CD5728 /* English */, + ); + name = registration.nib; + sourceTree = SOURCE_ROOT; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + C01FCF4B08A954540054247B /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + COPY_PHASE_STRIP = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(FRAMEWORK_SEARCH_PATHS)", + "$(SRCROOT)/../../../cocoalib/build/Release", + "$(FRAMEWORK_SEARCH_PATHS_QUOTED_FOR_TARGET_1)", + ); + FRAMEWORK_SEARCH_PATHS_QUOTED_FOR_TARGET_1 = "\"$(SRCROOT)/../../base/cocoa/build/Release\""; + GCC_DYNAMIC_NO_PIC = NO; + GCC_ENABLE_FIX_AND_CONTINUE = YES; + GCC_MODEL_TUNING = G5; + GCC_OPTIMIZATION_LEVEL = 0; + INFOPLIST_FILE = Info.plist; + INSTALL_PATH = "$(HOME)/Applications"; + PRODUCT_NAME = dupeGuru; + WRAPPER_EXTENSION = app; + ZERO_LINK = YES; + }; + name = Debug; + }; + C01FCF4C08A954540054247B /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = ( + ppc, + i386, + ); + FRAMEWORK_SEARCH_PATHS = ( + "$(FRAMEWORK_SEARCH_PATHS)", + "$(SRCROOT)/../../../cocoalib/build/Release", + "$(FRAMEWORK_SEARCH_PATHS_QUOTED_FOR_TARGET_1)", + ); + FRAMEWORK_SEARCH_PATHS_QUOTED_FOR_TARGET_1 = "\"$(SRCROOT)/../../base/cocoa/build/Release\""; + GCC_GENERATE_DEBUGGING_SYMBOLS = NO; + GCC_MODEL_TUNING = G5; + INFOPLIST_FILE = Info.plist; + INSTALL_PATH = "$(HOME)/Applications"; + PRODUCT_NAME = dupeGuru; + WRAPPER_EXTENSION = app; + }; + name = Release; + }; + C01FCF4F08A954540054247B /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + GCC_C_LANGUAGE_STANDARD = c99; + GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.4; + PREBINDING = NO; + SDKROOT = "$(DEVELOPER_SDK_DIR)/MacOSX10.4u.sdk"; + }; + name = Debug; + }; + C01FCF5008A954540054247B /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = ( + ppc, + i386, + ); + FRAMEWORK_SEARCH_PATHS = "@executable_path/../Frameworks"; + GCC_C_LANGUAGE_STANDARD = c99; + GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.4; + PREBINDING = NO; + SDKROOT = "$(DEVELOPER_SDK_DIR)/MacOSX10.4u.sdk"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + C01FCF4A08A954540054247B /* Build configuration list for PBXNativeTarget "dupeguru" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + C01FCF4B08A954540054247B /* Debug */, + C01FCF4C08A954540054247B /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + C01FCF4E08A954540054247B /* Build configuration list for PBXProject "dupeguru" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + C01FCF4F08A954540054247B /* Debug */, + C01FCF5008A954540054247B /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 29B97313FDCFA39411CA2CEA /* Project object */; +} diff --git a/se/cocoa/gen.py b/se/cocoa/gen.py new file mode 100644 index 00000000..4101b2a0 --- /dev/null +++ b/se/cocoa/gen.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python + +import os + +print "Generating help" +os.chdir('help') +os.system('python -u gen.py') +os.system('/Developer/Applications/Utilities/Help\\ Indexer.app/Contents/MacOS/Help\\ Indexer dupeguru_help') +os.chdir('..') + +print "Generating py plugin" +os.chdir('py') +os.system('python -u gen.py') +os.chdir('..') \ No newline at end of file diff --git a/se/cocoa/main.m b/se/cocoa/main.m new file mode 100644 index 00000000..c5f30658 --- /dev/null +++ b/se/cocoa/main.m @@ -0,0 +1,21 @@ +// +// main.m +// dupeguru +// +// Created by Virgil Dupras on 2006/02/01. +// Copyright __MyCompanyName__ 2006. All rights reserved. +// + +#import + +int main(int argc, char *argv[]) +{ + NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; + NSString *pluginPath = [[NSBundle mainBundle] + pathForResource:@"dg_cocoa" + ofType:@"plugin"]; + NSBundle *pluginBundle = [NSBundle bundleWithPath:pluginPath]; + [pluginBundle load]; + [pool release]; + return NSApplicationMain(argc, (const char **) argv); +} diff --git a/se/cocoa/py/dg_cocoa.py b/se/cocoa/py/dg_cocoa.py new file mode 100644 index 00000000..63a4df2c --- /dev/null +++ b/se/cocoa/py/dg_cocoa.py @@ -0,0 +1,208 @@ +#!/usr/bin/env python +import objc +from AppKit import * + +from dupeguru import app_se_cocoa, scanner + +# Fix py2app imports with chokes on relative imports +from dupeguru import app, app_cocoa, data, directories, engine, export, ignore, results, scanner +from hsfs import auto, manual, stats, tree, utils +from hsfs.phys import bundle + +class PyApp(NSObject): + pass #fake class + +class PyDupeGuru(PyApp): + def init(self): + self = super(PyDupeGuru,self).init() + self.app = app_se_cocoa.DupeGuru() + return self + + #---Directories + def addDirectory_(self,directory): + return self.app.AddDirectory(directory) + + def removeDirectory_(self,index): + self.app.RemoveDirectory(index) + + def setDirectory_state_(self,node_path,state): + self.app.SetDirectoryState(node_path,state) + + #---Results + def clearIgnoreList(self): + self.app.scanner.ignore_list.Clear() + + def doScan(self): + return self.app.start_scanning() + + def exportToXHTMLwithColumns_xslt_css_(self,column_ids,xslt_path,css_path): + return self.app.ExportToXHTML(column_ids,xslt_path,css_path) + + def loadIgnoreList(self): + self.app.LoadIgnoreList() + + def loadResults(self): + self.app.load() + + def markAll(self): + self.app.results.mark_all() + + def markNone(self): + self.app.results.mark_none() + + def markInvert(self): + self.app.results.mark_invert() + + def purgeIgnoreList(self): + self.app.PurgeIgnoreList() + + def toggleSelectedMark(self): + self.app.ToggleSelectedMarkState() + + def saveIgnoreList(self): + self.app.SaveIgnoreList() + + def saveResults(self): + self.app.Save() + + def refreshDetailsWithSelected(self): + self.app.RefreshDetailsWithSelected() + + def selectResultNodePaths_(self,node_paths): + self.app.SelectResultNodePaths(node_paths) + + def selectPowerMarkerNodePaths_(self,node_paths): + self.app.SelectPowerMarkerNodePaths(node_paths) + + #---Actions + def addSelectedToIgnoreList(self): + self.app.AddSelectedToIgnoreList() + + def deleteMarked(self): + self.app.delete_marked() + + def applyFilter_(self, filter): + self.app.ApplyFilter(filter) + + def makeSelectedReference(self): + self.app.MakeSelectedReference() + + def copyOrMove_markedTo_recreatePath_(self,copy,destination,recreate_path): + self.app.copy_or_move_marked(copy, destination, recreate_path) + + def openSelected(self): + self.app.OpenSelected() + + def removeMarked(self): + self.app.results.perform_on_marked(lambda x:True, True) + + def removeSelected(self): + self.app.RemoveSelected() + + def renameSelected_(self,newname): + return self.app.RenameSelected(newname) + + def revealSelected(self): + self.app.RevealSelected() + + #---Misc + def sortDupesBy_ascending_(self,key,asc): + self.app.sort_dupes(key,asc) + + def sortGroupsBy_ascending_(self,key,asc): + self.app.sort_groups(key,asc) + + #---Information + def getIgnoreListCount(self): + return len(self.app.scanner.ignore_list) + + def getMarkCount(self): + return self.app.results.mark_count + + def getStatLine(self): + return self.app.stat_line + + def getOperationalErrorCount(self): + return self.app.last_op_error_count + + #---Data + @objc.signature('i@:i') + def getOutlineViewMaxLevel_(self, tag): + return self.app.GetOutlineViewMaxLevel(tag) + + @objc.signature('@@:i@') + def getOutlineView_childCountsForPath_(self, tag, node_path): + return self.app.GetOutlineViewChildCounts(tag, node_path) + + def getOutlineView_valuesForIndexes_(self,tag,node_path): + return self.app.GetOutlineViewValues(tag,node_path) + + def getOutlineView_markedAtIndexes_(self,tag,node_path): + return self.app.GetOutlineViewMarked(tag,node_path) + + def getTableViewCount_(self,tag): + return self.app.GetTableViewCount(tag) + + def getTableViewMarkedIndexes_(self,tag): + return self.app.GetTableViewMarkedIndexes(tag) + + def getTableView_valuesForRow_(self,tag,row): + return self.app.GetTableViewValues(tag,row) + + #---Properties + def setMinMatchPercentage_(self,percentage): + self.app.scanner.min_match_percentage = int(percentage) + + def setScanType_(self,scan_type): + try: + self.app.scanner.scan_type = [ + scanner.SCAN_TYPE_FILENAME, + scanner.SCAN_TYPE_CONTENT + ][scan_type] + except IndexError: + pass + + def setWordWeighting_(self,words_are_weighted): + self.app.scanner.word_weighting = words_are_weighted + + def setMixFileKind_(self,mix_file_kind): + self.app.scanner.mix_file_kind = mix_file_kind + + def setDisplayDeltaValues_(self,display_delta_values): + self.app.display_delta_values= display_delta_values + + def setMatchSimilarWords_(self,match_similar_words): + self.app.scanner.match_similar_words = match_similar_words + + def setEscapeFilterRegexp_(self, escape_filter_regexp): + self.app.options['escape_filter_regexp'] = escape_filter_regexp + + def setRemoveEmptyFolders_(self, remove_empty_folders): + self.app.options['clean_empty_dirs'] = remove_empty_folders + + @objc.signature('v@:i') + def setSizeThreshold_(self, size_threshold): + self.app.scanner.size_threshold = size_threshold + + #---Worker + def getJobProgress(self): + return self.app.progress.last_progress + + def getJobDesc(self): + return self.app.progress.last_desc + + def cancelJob(self): + self.app.progress.job_cancelled = True + + #---Registration + @objc.signature('i@:') + def isRegistered(self): + return self.app.registered + + @objc.signature('i@:@@') + def isCodeValid_withEmail_(self, code, email): + return self.app.is_code_valid(code, email) + + def setRegisteredCode_andEmail_(self, code, email): + self.app.set_registration(code, email) + diff --git a/se/cocoa/py/gen.py b/se/cocoa/py/gen.py new file mode 100644 index 00000000..2279eb36 --- /dev/null +++ b/se/cocoa/py/gen.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python + +import os +import os.path as op +import shutil + +print "Cleaning build and dist" +if op.exists('build'): + shutil.rmtree('build') +if op.exists('dist'): + shutil.rmtree('dist') + +print "Buiding the py2app plugin" + +os.system('python -u setup.py py2app') \ No newline at end of file diff --git a/se/cocoa/py/setup.py b/se/cocoa/py/setup.py new file mode 100644 index 00000000..6d37e3c5 --- /dev/null +++ b/se/cocoa/py/setup.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python + +from distutils.core import setup +import py2app + +from hsutil.build import move_testdata_out, put_testdata_back + +move_log = move_testdata_out() +try: + setup( + plugin = ['dg_cocoa.py'], + ) +finally: + put_testdata_back(move_log) \ No newline at end of file diff --git a/se/cocoa/w3/dg.xsl b/se/cocoa/w3/dg.xsl new file mode 100644 index 00000000..4f982fce --- /dev/null +++ b/se/cocoa/w3/dg.xsl @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + indented + + + + + + + + + + + + + + + + + + + + + + + + + + + + dupeGuru Results + + + +

dupeGuru Results

+ + + + + +
+ + + + + \ No newline at end of file diff --git a/se/cocoa/w3/hardcoded.css b/se/cocoa/w3/hardcoded.css new file mode 100644 index 00000000..ed243bcc --- /dev/null +++ b/se/cocoa/w3/hardcoded.css @@ -0,0 +1,71 @@ +BODY +{ + background-color:white; +} + +BODY,A,P,UL,TABLE,TR,TD +{ + font-family:Tahoma,Arial,sans-serif; + font-size:10pt; + color: #4477AA; +} + +TABLE +{ + background-color: #225588; + margin-left: auto; + margin-right: auto; + width: 90%; +} + +TR +{ + background-color: white; +} + +TH +{ + font-weight: bold; + color: black; + background-color: #C8D6E5; +} + +TH TD +{ + color:black; +} + +TD +{ + padding-left: 2pt; +} + +TD.rightelem +{ + text-align:right; + /*padding-left:0pt;*/ + padding-right: 2pt; + width: 17%; +} + +TD.indented +{ + padding-left: 12pt; +} + +H1 +{ + font-family:"Courier New",monospace; + color:#6699CC; + font-size:18pt; + color:#6da500; + border-color: #70A0CF; + border-width: 1pt; + border-style: solid; + margin-top: 16pt; + margin-left: 5%; + margin-right: 5%; + padding-top: 2pt; + padding-bottom:2pt; + text-align: center; +} \ No newline at end of file diff --git a/se/help/changelog.yaml b/se/help/changelog.yaml new file mode 100644 index 00000000..8bb0a392 --- /dev/null +++ b/se/help/changelog.yaml @@ -0,0 +1,230 @@ +- date: 2009-05-29 + version: 2.7.1 + description: | + * Fixed a bug causing crashes when having application files in the results. + * Fixed a bug causing a GUI freeze at the beginning of a scan with a lot of files. + * Fixed a bug that sometimes caused a crash when an action was cancelled, and then started again. +- date: 2009-05-25 + version: 2.7.0 + description: | + * Converted the Windows GUI to Qt. + * Improved the reliability of the scanning process. +- date: 2009-03-27 + version: 2.6.1 + description: | + * **Fixed** an occasional crash caused by permission issues. + * **Fixed** a bug where the "X discarded" notice would show a too large number of discarded + duplicates. +- date: 2008-09-10 + description: "* **Added** a small file threshold preference.\r\n* **Added** a notice\ + \ in the status bar when matches were discarded during the scan.\r\n* **Improved**\ + \ duplicate prioritization (smartly chooses which file you will keep).\r\n* **Improved**\ + \ scan progress feedback.\r\n* **Improved** responsiveness of the user interface\ + \ for certain actions." + version: 2.6.0 +- date: 2008-08-10 + description: "
    \n\t\t\t\t\t\t
  • Improved the speed of results loading\ + \ and saving.
  • \n\t\t\t\t\t\t
  • Fixed a crash sometimes occurring during\ + \ duplicate deletion.
  • \n\t\t
" + version: 2.5.4 +- date: 2008-07-08 + description: "
    \n\t\t\t\t\t\t
  • Improved unicode handling for filenames.\ + \ dupeGuru will now find a lot more duplicates if your files have non-ascii characters\ + \ in it.
  • \n\t\t\t\t\t\t
  • Fixed \"Clear Ignore List\" crash in Windows.
  • \n\ + \t\t
" + version: 2.5.3 +- date: 2008-01-10 + description: "
    \n\t\t\t\t\t\t
  • Improved the handling of low memory situations.
  • \n\ + \t\t\t\t\t\t
  • Improved the directory panel. The \"Remove\" button changes\ + \ to \"Put Back\" when an excluded directory is selected.
  • \n\t\t\t\t\t\t
  • Improved\ + \ scan, delete and move speed in situations where there were a lot of duplicates.
  • \n\ + \t\t\t\t\t\t
  • Fixed occasional crashes when moving bundles (such as .app\ + \ files).
  • \n\t\t\t\t\t\t
  • Fixed occasional crashes when moving a\ + \ lot of files at once.
  • \n\t\t
" + version: 2.5.2 +- date: 2007-11-22 + description: "
    \n\t\t\t\t\t\t
  • Added the \"Remove empty folders\" option.
  • \n\ + \t\t\t\t\t\t
  • Fixed results load/save issues.
  • \n\t\t\t\t\t\t
  • Fixed\ + \ occasional status bar inaccuracies when the results are filtered.
  • \n\t\t\ + \
" + version: 2.5.1 +- date: 2007-09-15 + description: "
    \n\t\t\t\t\t\t
  • Added post scan filtering.
  • \n\t\t\ + \t\t\t\t
  • Fixed issues with the rename feature under Windows
  • \n\t\ + \t\t\t\t\t
  • Fixed some user interface annoyances under Windows
  • \n\ + \t\t
" + version: 2.5.0 +- date: 2007-04-14 + description: "
    \n\t\t\t\t\t\t
  • Improved UI responsiveness (using threads)\ + \ under Mac OS X.
  • \n\t\t\t\t\t\t
  • Improved result load/save speed\ + \ and memory usage.
  • \n\t\t
" + version: 2.4.8 +- date: 2007-03-10 + description: "
    \n\t\t\t\t\t\t
  • Fixed a \"bad file descriptor\" error\ + \ occasionally popping up.
  • \n\t\t\t\t\t\t
  • Fixed a bug with non-latin\ + \ directory names.
  • \n\t\t
" + version: 2.4.7 +- date: 2007-02-10 + description: "
    \n\t\t\t\t\t\t
  • Added Re-orderable columns. In fact,\ + \ I re-added the feature which was lost in the C# conversion in 2.4.0 (Windows).
  • \n\ + \t\t\t\t\t\t
  • Changed the behavior of the scanning engine when setting\ + \ the hardness to 100. It will now only match files that have their words in the\ + \ same order.
  • \n\t\t\t\t\t\t
  • Fixed a bug with all the Delete/Move/Copy\ + \ actions with certain kinds of files.
  • \n\t\t
" + version: 2.4.6 +- date: 2007-01-11 + description: "
    \n\t\t\t\t\t\t
  • Fixed a bug with the Move action.
  • \n\ + \t\t
" + version: 2.4.5 +- date: 2007-01-07 + description: "
    \n\t\t\t\t\t\t
  • Fixed a \"ghosting\" bug. Dupes deleted\ + \ by dupeGuru would sometimes come back in subsequent scans (Windows).
  • \n\t\ + \t\t\t\t\t
  • Fixed bugs sometimes making dupeGuru crash when marking a\ + \ dupe (Windows).
  • \n\t\t\t\t\t\t
  • Fixed some minor visual glitches\ + \ (Windows).
  • \n\t\t
" + version: 2.4.4 +- date: 2006-12-08 + description: "
    \n\t\t\t\t\t\t
  • Fixed a mishandling of \".app\" files\ + \ (OS X).
  • \n\t\t\t\t\t\t
  • Fixed a bug preventing files from \"reference\"\ + \ directories to be displayed in blue in the results (Windows).
  • \n\t\t\t\t\ + \t\t
  • Fixed a bug preventing some files to be sent to the recycle bin\ + \ (Windows).
  • \n\t\t\t\t\t\t
  • Fixed a bug in the packaging preventing\ + \ certain Windows configurations to start dupeGuru at all.
  • \n\t\t \ + \
" + version: 2.4.3 +- date: 2006-11-18 + description: "
    \n\t\t\t\t\t\t
  • Fixed a bug with directory states.
  • \n\ + \t\t
" + version: 2.4.2 +- date: 2006-11-15 + description: "
    \n\t\t\t\t\t\t
  • Fixed a bug causing the ignore list not\ + \ to be saved.
  • \n\t\t\t\t\t\t
  • Fixed a bug sometimes making delete\ + \ and move operations stall.
  • \n\t\t
" + version: 2.4.1 +- date: 2006-11-10 + description: "
    \n\t\t\t\t\t\t
  • Changed the Windows interface. It is\ + \ now .NET based.
  • \n\t\t\t\t\t\t
  • Added an auto-update feature to\ + \ the windows version.
  • \n\t\t\t\t\t\t
  • Changed the way power marking\ + \ works. It is now a mode instead of a separate window.
  • \n\t\t\t\t\t\t
  • Changed\ + \ the \"Size (MB)\" column for a \"Size (KB)\" column. The values are now \"ceiled\"\ + \ instead of rounded. Therefore, a size \"0\" is now really 0 bytes, not just\ + \ a value too small to be rounded up. It is also the case for delta values.
  • \n\ + \t\t\t\t\t\t
  • Removed the min word length/count options. These came from\ + \ Mp3 Filter, and just aren't used anymore. Word weighting does pretty much the\ + \ same job.
  • \n\t\t
" + version: 2.4.0 +- date: 2006-11-07 + description: "
    \n\t\t\t\t\t\t
  • Improved speed and memory usage of the\ + \ scanning engine, again. Does it mean there was a lot of improvements to be made?\ + \ Nah...
  • \n\t\t
" + version: 2.3.4 +- date: 2006-11-02 + description: "
    \n\t\t\t\t\t\t
  • Improved speed and memory usage of the\ + \ scanning engine, especially when the scan results in a lot of duplicates.
  • \n\ + \t\t\t\t\t\t
  • Now I wonder if Sparkle is going to work well...
  • \n\t\t \ + \
" + version: 2.3.3 +- date: 2006-10-16 + description: "
    \n\t\t\t\t\t\t
  • Added an auto-update feature in the Mac\ + \ OS X version (with Sparkle).
  • \n\t\t\t\t\t\t
  • Fixed a bug preventing\ + \ some duplicate reports to be created correctly under Windows.
  • \n\t\t \ + \
" + version: 2.3.2 +- date: 2006-10-02 + description: "
    \n\t\t\t\t\t\t
  • Fixed a bug preventing some duplicates\ + \ to be found, especially when scanning lots of files.
  • \n\t\t
" + version: 2.3.1 +- date: 2006-09-22 + description: "
    \n\t\t\t\t\t\t
  • Added XHTML export feature.
  • \n\t\t\ + \
" + version: 2.3.0 +- date: 2006-08-31 + description: "
    \n\t\t\t\t\t\t
  • Added sticky columns.
  • \n\t\t\t\t\t\ + \t
  • Fixed an issue with file caching between scans.
  • \n\t\t\t\t\t\t\ +
  • Fixed an issue preventing some duplicates from being deleted/moved/copied.
  • \n\ + \t\t
" + version: 2.2.10 +- date: 2006-08-27 + description: "
    \n\t\t\t\t\t\t
  • Fixed an issue with ignore list and unicode.
  • \n\ + \t\t\t\t\t\t
  • Fixed an issue with file attribute fetching sometimes causing\ + \ dupeGuru to crash.
  • \n\t\t\t\t\t\t
  • Fixed an issue in the directories\ + \ panel under Windows.
  • \n\t\t
" + version: 2.2.9 +- date: 2006-08-17 + description: "
    \n\t\t\t\t\t\t
  • Fixed an issue in the duplicate seeking\ + \ engine preventing some duplicates to be found.
  • \n\t\t
" + version: 2.2.8 +- date: 2006-08-12 + description: "
    \n\t\t\t\t\t\t
  • Improved unicode support.
  • \n\t\t\t\ + \t\t\t
  • Improved the \"Reveal in Finder\" (\"Open Containing Folder\"\ + \ in Windows) feature so it selects the file in the folder it opens.
  • \n\t\t\ + \
" + version: 2.2.7 +- date: 2006-08-07 + description: "
    \n\t\t\t\t\t\t
  • Improved the ignore list system.
  • \n\ + \t\t\t\t\t\t
  • dupeGuru is now a Universal application on Mac OS X.
  • \n\t\t\ + \
" + version: 2.2.6 +- date: 2006-07-26 + description: "
    \n\t\t\t\t\t\t
  • Improved application (.app) dupe detection\ + \ on Mac OS X.
  • \n\t\t\t\t\t\t
  • Fixed an issue that occasionally made\ + \ dupeGuru crash on startup.
  • \n\t\t
" + version: 2.2.5 +- date: 2006-06-27 + description: "
    \n\t\t\t\t\t\t
  • Fixed an issue with Move and Copy features.
  • \n\ + \t\t
" + version: 2.2.4 +- date: 2006-06-15 + description: "
    \n\t\t\t\t\t\t
  • Improved duplicate scanning speed.
  • \n\ + \t\t\t\t\t\t
  • Added a warning that a file couldn't be renamed if a file\ + \ with the same name already exists.
  • \n\t\t
" + version: 2.2.3 +- date: 2006-06-07 + description: "
    \n\t\t\t\t\t\t
  • Added \"Rename Selected\" feature.
  • \n\ + \t\t\t\t\t\t
  • Fixed some minor issues with \"Reload Last Results\" feature.
  • \n\ + \t\t\t\t\t\t
  • Fixed ignore list issues.
  • \n\t\t
" + version: 2.2.2 +- date: 2006-05-22 + description: "
    \n\t\t \t
  • Fixed occasional progress bar woes\ + \ under Windows.
  • \n\t\t\t\t\t\t
  • Fixed a bug in the registration\ + \ system under Windows.
  • \n\t\t\t\t\t\t
  • Nothing has been changed in the\ + \ Mac OS X version, but I want to keep version in sync.
  • \n\t\t \ + \
" + version: 2.2.1 +- date: 2006-05-10 + description: "
    \n\t\t \t
  • Added destination path re-creation\ + \ options.
  • \n\t\t\t\t\t\t
  • Added an ignore list.
  • \n\t\t\t\t\t\ + \t
  • Changed the main icon.
  • \n\t\t\t\t\t\t
  • Improved dramatically\ + \ the delta values feature.
  • \n\t\t
" + version: 2.2.0 +- date: 2006-04-18 + description: "
    \n\t\t \t
  • Added the \"Match similar words\"\ + \ option.
  • \n\t\t\t\t\t\t
  • Fixed Power marking issues under Mac.
  • \n\ + \t\t
" + version: 2.1.2 +- date: 2006-04-14 + description: "
    \n\t\t \t
  • Added the \"Display delta values\"\ + \ option.
  • \n\t\t\t\t\t\t
  • Improved Power marking sorting speed under\ + \ Mac.
  • \n\t\t\t\t\t\t
  • Fixed Power marking sorting issues.
  • \n\ + \t\t
" + version: 2.1.1 +- date: 2006-04-03 + description: "
    \n\t\t \t
  • Added the Power Marker feature.
  • \n\ + \t\t\t\t\t\t
  • Fixed a column sorting bug. The results would sometimes\ + \ lose their sort order.
  • \n\t\t\t\t\t\t
  • Fixed a bug with the Make\ + \ Reference feature. The results sometimes wasn't correctly refreshed after the\ + \ reference switch.
  • \n\t\t
" + version: 2.1.0 +- date: 2006-03-23 + description: "
    \n\t\t \t
  • Fixed an issue occasionally occurring\ + \ when trying to reload results from removable media that is no longer present.
  • \n\ + \t\t
" + version: 2.0.1 +- date: 2006-03-17 + description: "
    \n\t\t \t
  • Complete rewrite.
  • \n\t\t \ + \ \t
  • Now runs on Mac OS X.
  • \n\t\t
" + version: 2.0.0 +- date: 2004-09-24 + description: "
    \n\t\t \t
  • Initial release.
  • \n\t\t \ + \
" + version: 1.0.0 diff --git a/se/help/gen.py b/se/help/gen.py new file mode 100644 index 00000000..3f067c83 --- /dev/null +++ b/se/help/gen.py @@ -0,0 +1,12 @@ +#!/usr/bin/env python +# Unit Name: +# Created By: Virgil Dupras +# Created On: 2009-05-24 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +import os + +from web import generate_help + +generate_help.main('.', 'dupeguru_help', force_render=True) diff --git a/se/help/skeleton/hardcoded.css b/se/help/skeleton/hardcoded.css new file mode 100644 index 00000000..a3b17c5b --- /dev/null +++ b/se/help/skeleton/hardcoded.css @@ -0,0 +1,409 @@ +/***************************************************** + General settings +*****************************************************/ + +BODY +{ + background-color:white; +} + +BODY,A,P,UL,TABLE,TR,TD +{ + font-family:Tahoma,Arial,sans-serif; + font-size:10pt; + color: #4477AA;/*darker than 5588bb for the sake of the eyes*/ +} + +/***************************************************** + "A" settings +*****************************************************/ + +A +{ + color: #ae322b; + text-decoration:underline; + font-weight:bold; +} + +A.glossaryword {color:#A0A0A0;} + +A.noline +{ + text-decoration: none; +} + + +/***************************************************** + Menu and mainframe settings +*****************************************************/ + +.maincontainer +{ + display:block; + margin-left:7%; + margin-right:7%; + padding-left:5px; + padding-right:0px; + border-color:#CCCCCC; + border-style:solid; + border-width:2px; + border-right-width:0px; + border-bottom-width:0px; + border-top-color:#ae322b; + vertical-align:top; +} + +TD.menuframe +{ + width:30%; +} + +.menu +{ + margin:4px 4px 4px 4px; + margin-top: 16pt; + border-color:gray; + border-width:1px; + border-style:dotted; + padding-top:10pt; + padding-bottom:10pt; + padding-right:6pt; +} + +.submenu +{ + list-style-type: none; + margin-left:26pt; + margin-top:0pt; + margin-bottom:0pt; + padding-left:0pt; +} + +A.menuitem,A.menuitem_selected +{ + font-size:14pt; + font-family:Tahoma,Arial,sans-serif; + font-weight:normal; + padding-left:10pt; + color:#5588bb; + margin-right:2pt; + margin-left:4pt; + text-decoration:none; +} + +A.menuitem_selected +{ + font-weight:bold; +} + +A.submenuitem +{ + font-family:Tahoma,Arial,sans-serif; + font-weight:normal; + color:#5588bb; + text-decoration:none; +} + +.titleline +{ + border-width:3px; + border-style:solid; + border-left-width:0px; + border-right-width:0px; + border-top-width:0px; + border-color:#CCCCCC; + margin-left:28pt; + margin-right:2pt; + line-height:1px; + padding-top:0px; + margin-top:0px; + display:block; +} + +.titledescrip +{ + text-align:left; + display:block; + margin-left:26pt; + color:#ae322b; +} + +.mainlogo +{ + display:block; + margin-left:8%; + margin-top:4pt; + margin-bottom:4pt; +} + +/***************************************************** + IMG settings +*****************************************************/ + +IMG +{ + border-style:none; +} + +IMG.smallbutton +{ + margin-right: 20px; + float:none; +} + +IMG.floating +{ + float:left; + margin-right: 4pt; + margin-bottom: 4pt; +} + +IMG.lefticon +{ + vertical-align: middle; + padding-right: 2pt; +} + +IMG.righticon +{ + vertical-align: middle; + padding-left: 2pt; +} + +/***************************************************** + TABLE settings +*****************************************************/ + +TABLE +{ + border-style:none; +} + +TABLE.box +{ + width: 90%; + margin-left:5%; +} + +TABLE.centered +{ + margin-left: auto; + margin-right: auto; +} + +TABLE.hardcoded +{ + background-color: #225588; + margin-left: auto; + margin-right: auto; + width: 90%; +} + +TR { background-color: transparent; } + +TABLE.hardcoded TR { background-color: white } + +TABLE.hardcoded TR.header +{ + font-weight: bold; + color: black; + background-color: #C8D6E5; +} + +TABLE.hardcoded TR.header TD {color:black;} + +TABLE.hardcoded TD { padding-left: 2pt; } + +TD.minimelem { + padding-right:0px; + padding-left:0px; + text-align:center; +} + +TD.rightelem +{ + text-align:right; + /*padding-left:0pt;*/ + padding-right: 2pt; + width: 17%; +} + +/***************************************************** + P settings +*****************************************************/ + +p,.sub{text-align:justify;} +.centered{text-align:center;} +.sub +{ + padding-left: 16pt; + padding-right:16pt; +} + +.Note, .ContactInfo +{ + border-color: #ae322b; + border-width: 1pt; + border-style: dashed; + text-align:justify; + padding: 2pt 2pt 2pt 2pt; + margin-bottom:4pt; + margin-top:8pt; + list-style-position:inside; +} + +.ContactInfo +{ + width:60%; + margin-left:5%; +} + +.NewsItem +{ + border-color:#ae322b; + border-style: solid; + border-right:none; + border-top:none; + border-left:none; + border-bottom-width:1px; + text-align:justify; + padding-left:4pt; + padding-right:4pt; + padding-bottom:8pt; +} + +/***************************************************** + Lists settings +*****************************************************/ +UL.plain +{ + list-style-type: none; + padding-left:0px; + margin-left:0px; +} + +LI.plain +{ + list-style-type: none; +} + +LI.section +{ + padding-top: 6pt; +} + +UL.longtext LI +{ + border-color: #ae322b; + border-width:0px; + border-top-width:1px; + border-style:solid; + margin-top:12px; +} + +/* + with UL.longtext LI, there can be anything between + the UL and the LI, and it will still make the + lontext thing, I must break it with this hack +*/ +UL.longtext UL LI +{ + border-style:none; + margin-top:2px; +} + + +/***************************************************** + Titles settings +*****************************************************/ + +H1,H2,H3 +{ + font-family:"Courier New",monospace; + color:#5588bb; +} + +H1 +{ + font-size:18pt; + color: #ae322b; + border-color: #70A0CF; + border-width: 1pt; + border-style: solid; + margin-top: 16pt; + margin-left: 5%; + margin-right: 5%; + padding-top: 2pt; + padding-bottom:2pt; + text-align: center; +} + +H2 +{ + border-color: #ae322b; + border-bottom-width: 2px; + border-top-width: 0pt; + border-left-width: 2px; + border-right-width: 0pt; + border-bottom-color: #cccccc; + border-style: solid; + margin-top: 16pt; + margin-left: 0pt; + margin-right: 0pt; + padding-bottom:3pt; + padding-left:5pt; + text-align: left; + font-size:16pt; +} + +H3 +{ + display:block; + color:#ae322b; + border-color: #70A0CF; + border-bottom-width: 2px; + border-top-width: 0pt; + border-left-width: 0pt; + border-right-width: 0pt; + border-style: dashed; + margin-top: 12pt; + margin-left: 0pt; + margin-bottom: 4pt; + width:auto; + padding-bottom:3pt; + padding-right:2pt; + padding-left:2pt; + text-align: left; + font-weight:bold; +} + + +/***************************************************** + Misc. classes +*****************************************************/ +.longtext:first-letter {font-size: 150%} + +.price, .loweredprice, .specialprice {font-weight:bold;} + +.loweredprice {text-decoration:line-through} + +.specialprice {color:red} + +form +{ + margin:0px; +} + +.program_summary +{ + float:right; + margin: 32pt; + margin-top:0pt; + margin-bottom:0pt; +} + +.screenshot +{ + float:left; + margin: 8pt; +} \ No newline at end of file diff --git a/se/help/skeleton/images/hs_title.png b/se/help/skeleton/images/hs_title.png new file mode 100644 index 0000000000000000000000000000000000000000..07bd89c69dd50a3967b46a441741f274b8854de8 GIT binary patch literal 1817 zcmWkt2~Y|q^t%aD27PTfWQbSo7y0Xrdp{OF7oMwX4{+byFr3&y7B zlLpel1d+*^<>$!E@c2A9sOuxRq}jS+G}rcy>y2h&rngp=tT$NjxX)#-@ zR;$fwvyo;C3xs)cj4wl35`-y%c{0>!qGG~a8Nvb)0K&L3RHVdNQtA}p%Q4Dz$v^^f z%s`q=W-AZEN|l62NeGp=(QGM^zcEYpnJq53Zg_psX$pDf}jksh9!YZ6*y0d6t`*06htZIC4pHW%9Epf zh*AWzsT7ofh)_XrrKsL$;mT2tj52IVPjY1#SB?oFlp&##5=^Q9qlF{I7FJ9G2rF*a z2s?-pFR&PpFjtBRA?yV{E9}sUAgt)Ih9}1>x(SvT zsqP+UieWa0F{DUM&ub2ZbHs4dvq=VsEE&xV>e|H!OM+4TQTc&M3CEU=q(F(^fAYG# zOS_;aD|@ud29hH~gpd|cpbwr+aO8+kiBoq^eznVg<_$ zP1%d1$UV?R~TV5+i6w>mO|X i&$iNccmR(ItgDH2&`ti^@h8+@ro}}kMz!wGy!C(b`7gZy literal 0 HcmV?d00001 diff --git a/se/help/templates/base_dg.mako b/se/help/templates/base_dg.mako new file mode 100644 index 00000000..7767c49f --- /dev/null +++ b/se/help/templates/base_dg.mako @@ -0,0 +1,14 @@ +<%inherit file="/base_help.mako"/> +${next.body()} + +<%def name="menu()"><% +self.menuitem('intro.htm', 'Introduction', 'Introduction to dupeGuru') +self.menuitem('quick_start.htm', 'Quick Start', 'Quickly get into the action') +self.menuitem('directories.htm', 'Directories', 'Managing dupeGuru directories') +self.menuitem('preferences.htm', 'Preferences', 'Setting dupeGuru preferences') +self.menuitem('results.htm', 'Results', 'Time to delete these duplicates!') +self.menuitem('power_marker.htm', 'Power Marker', 'Take control of your duplicates') +self.menuitem('faq.htm', 'F.A.Q.', 'Frequently Asked Questions') +self.menuitem('versions.htm', 'Version History', 'Changes dupeGuru went through') +self.menuitem('credits.htm', 'Credits', 'People who contributed to dupeGuru') +%> \ No newline at end of file diff --git a/se/help/templates/credits.mako b/se/help/templates/credits.mako new file mode 100644 index 00000000..269d7463 --- /dev/null +++ b/se/help/templates/credits.mako @@ -0,0 +1,20 @@ +<%! + title = 'Credits' + selected_menu_item = 'Credits' +%> +<%inherit file="/base_dg.mako"/> +Below is the list of people who contributed, directly or indirectly to dupeGuru. + +${self.credit('Virgil Dupras', 'Developer', "That's me, Hardcoded Software founder", 'www.hardcoded.net', 'hsoft@hardcoded.net')} + +${self.credit('Jerome', 'Icon designer', "Icons in dupeGuru are from him")} + +${self.credit('Python', 'Programming language', "The bestest of the bests", 'www.python.org')} + +${self.credit('PyObjC', 'Python-to-Cocoa bridge', "Used for the Mac OS X version", 'pyobjc.sourceforge.net')} + +${self.credit('Python for .NET', 'Python-to-.NET bridge', "Used for the Windows version", 'sourceforge.net/projects/pythonnet/')} + +${self.credit('Sparkle', 'Auto-update library', "Used for the Mac OS X version", 'andymatuschak.org/pages/sparkle')} + +${self.credit('You', 'dupeGuru user', "What would I do without you?")} diff --git a/se/help/templates/directories.mako b/se/help/templates/directories.mako new file mode 100644 index 00000000..e75b47bd --- /dev/null +++ b/se/help/templates/directories.mako @@ -0,0 +1,24 @@ +<%! + title = 'Directories' + selected_menu_item = 'Directories' +%> +<%inherit file="/base_dg.mako"/> + +There is a panel in dupeGuru called **Directories**. You can open it by clicking on the **Directories** button. This directory contains the list of the directories that will be scanned when you click on **Start Scanning**. + +This panel is quite straightforward to use. If you want to add a directory, click on **Add**. If you added directories before, a popup menu with a list of recent directories you added will pop. You can click on one of them to add it directly to your list. If you click on the first item of the popup menu, **Add New Directory...**, you will be prompted for a directory to add. If you never added a directory, no menu will pop and you will directly be prompted for a new directory to add. + +To remove a directory, select the directory to remove and click on **Remove**. If a subdirectory is selected when you click remove, the selected directory will be set to **excluded** state (see below) instead of being removed. + +Directory states +----- + +Every directory can be in one of these 3 states: + +* **Normal:** Duplicates found in these directories can be deleted. +* **Reference:** Duplicates found in this directory **cannot** be deleted. Files in reference directories will be in a blue color in the results. +* **Excluded:** Files in this directory will not be included in the scan. + +The default state of a directory is, of course, **Normal**. You can use **Reference** state for a directory if you want to be sure that you won't delete any file from it. + +When you set the state of a directory, all subdirectories of this directory automatically inherit this state unless you explicitly set a subdirectory's state. diff --git a/se/help/templates/faq.mako b/se/help/templates/faq.mako new file mode 100644 index 00000000..659d8b14 --- /dev/null +++ b/se/help/templates/faq.mako @@ -0,0 +1,64 @@ +<%! + title = 'dupeGuru F.A.Q.' + selected_menu_item = 'F.A.Q.' +%> +<%inherit file="/base_dg.mako"/> + +<%text filter="md"> +### What is dupeGuru? + +dupeGuru is a tool to find duplicate files on your computer. It can scan either filenames or content. The filename scan features a fuzzy matching algorithm that can find duplicate filenames even when they are not exactly the same. + +### What makes it better than other duplicate scanners? + +The scanning engine is extremely flexible. You can tweak it to really get the kind of results you want. You can read more about dupeGuru tweaking option at the [Preferences page](preferences.htm). + +### How safe is it to use dupeGuru? + +Very safe. dupeGuru has been designed to make sure you don't delete files you didn't mean to delete. First, there is the reference directory system that lets you define directories where you absolutely **don't** want dupeGuru to let you delete files there, and then there is the group reference system that makes sure that you will **always** keep at least one member of the duplicate group. + +### What are the demo limitations of dupeGuru? + +In demo mode, you can only perform actions (delete/copy/move) on 10 duplicates per session. + +### The mark box of a file I want to delete is disabled. What must I do? + +You cannot mark the reference (The first file) of a duplicate group. However, what you can do is to promote a duplicate file to reference. Thus, if a file you want to mark is reference, select a duplicate file in the group that you want to promote to reference, and click on **Actions-->Make Selected Reference**. If the reference file is from a reference directory (filename written in blue letters), you cannot remove it from the reference position. + +### I have a directory from which I really don't want to delete files. + +If you want to be sure that dupeGuru will never delete file from a particular directory, just open the **Directories panel**, select that directory, and set its state to **Reference**. + +### What is this '(X discarded)' notice in the status bar? + +In some cases, some matches are not included in the final results for security reasons. Let me use an example. We have 3 file: A, B and C. We scan them using a low filter hardness. The scanner determines that A matches with B, A matches with C, but B does **not** match with C. Here, dupeGuru has kind of a problem. It cannot create a duplicate group with A, B and C in it because not all files in the group would match together. It could create 2 groups: one A-B group and then one A-C group, but it will not, for security reasons. Lets think about it: If B doesn't match with C, it probably means that either B, C or both are not actually duplicates. If there would be 2 groups (A-B and A-C), you would end up delete both B and C. And if one of them is not a duplicate, that is really not what you want to do, right? So what dupeGuru does in a case like this is to discard the A-C match (and adds a notice in the status bar). Thus, if you delete B and re-run a scan, you will have a A-C match in your next results. + +### I want to mark all files from a specific directory. What can I do? + +Enable the [Power Marker](power_marker.htm) mode and click on the Directory column to sort your duplicates by Directory. It will then be easy for you to select all duplicates from the same directory, and then press Space to mark all selected duplicates. + +### I want to remove all files that are more than 300 KB away from their reference file. What can I do? + +* Enable the [Power Marker](power_marker.htm) mode. +* Enable the **Delta Values** mode. +* Click on the "Size" column to sort the results by size. +* Select all duplicates below -300. +* Click on **Remove Selected from Results**. +* Select all duplicates over 300. +* Click on **Remove Selected from Results**. + +### I want to make my latest modified files reference files. What can I do? + +* Enable the [Power Marker](power_marker.htm) mode. +* Enable the **Delta Values** mode. +* Click on the "Modification" column to sort the results by modification date. +* Click on the "Modification" column again to reverse the sort order (see Power Marker page to know why). +* Select all duplicates over 0. +* Click on **Make Selected Reference**. + +### I want to mark all duplicates containing the word "copy". How do I do that? + +* **Windows**: Click on **Actions --> Apply Filter**, then type "copy", then click OK. +* **Mac OS X**: Type "copy" in the "Filter" field in the toolbar. +* Click on **Mark --> Mark All**. + \ No newline at end of file diff --git a/se/help/templates/intro.mako b/se/help/templates/intro.mako new file mode 100644 index 00000000..0fd3019b --- /dev/null +++ b/se/help/templates/intro.mako @@ -0,0 +1,13 @@ +<%! + title = 'Introduction to dupeGuru' + selected_menu_item = 'introduction' +%> +<%inherit file="/base_dg.mako"/> + +dupeGuru is a tool to find duplicate files on your computer. It can scan either filenames or contents. The filename scan features a fuzzy matching algorithm that can find duplicate filenames even when they are not exactly the same. + +Although dupeGuru can easily be used without documentation, reading this file will help you to master it. If you are looking for guidance for your first duplicate scan, you can take a look at the [Quick Start](quick_start.htm) section. + +It is a good idea to keep dupeGuru updated. You can download the latest version on the [dupeGuru homepage](http://www.hardcoded.net/dupeguru/). + +<%def name="meta()"> \ No newline at end of file diff --git a/se/help/templates/power_marker.mako b/se/help/templates/power_marker.mako new file mode 100644 index 00000000..26078f3d --- /dev/null +++ b/se/help/templates/power_marker.mako @@ -0,0 +1,33 @@ +<%! + title = 'Power Marker' + selected_menu_item = 'Power Marker' +%> +<%inherit file="/base_dg.mako"/> + +You will probably not use the Power Marker feature very often, but if you get into a situation where you need it, you will be pretty happy that this feature exists. + +What is it? +----- + +When the Power Marker mode is enabled, the duplicates are shown without their respective reference file. You can select, mark and sort this list, just like in normal mode. + +So, what is it for? +----- + +The dupeGuru results, when in normal mode, are sorted according to duplicate groups' **reference file**. This means that if you want, for example, to mark all duplicates with the "exe" extension, you cannot just sort the results by "Kind" to have all exe duplicates together because a group can be composed of more than one kind of files. That is where Power Marker comes into play. To mark all your "exe" duplicates, you just have to: + +* Enable the Power marker mode. +* Add the "Kind" column with the "Columns" menu. +* Click on that "Kind" column to sort the list by kind. +* Locate the first duplicate with a "exe" kind. +* Select it. +* Scroll down the list to locate the last duplicate with a "exe" kind. +* Hold Shift and click on it. +* Press Space to mark all selected duplicates. + +Power Marker and delta values +----- + +The Power Marker unveil its true power when you use it with the **Delta Values** switch turned on. When you turn it on, relative values will be displayed instead of absolute ones. So if, for example, you want to remove from your results all duplicates that are more than 300 KB away from their reference, you could sort the Power Marker by Size, select all duplicates under -300 in the Size column, delete them, and then do the same for duplicates over 300 at the bottom of the list. + +You could also use it to change the reference priority of your duplicate list. When you make a fresh scan, if there are no reference directories, the reference file of every group is the biggest file. If you want to change that, for example, to the latest modification time, you can sort the Power Marker by modification time in **descending** order, select all duplicates with a modification time delta value higher than 0 and click on **Make Selected Reference**. The reason why you must make the sort order descending is because if 2 files among the same duplicate group are selected when you click on **Make Selected Reference**, only the first of the list will be made reference, the other will be ignored. And since you want the last modified file to be reference, having the sort order descending assures you that the first item of the list will be the last modified. diff --git a/se/help/templates/preferences.mako b/se/help/templates/preferences.mako new file mode 100644 index 00000000..578fe8b2 --- /dev/null +++ b/se/help/templates/preferences.mako @@ -0,0 +1,27 @@ +<%! + title = 'Preferences' + selected_menu_item = 'Preferences' +%> +<%inherit file="/base_dg.mako"/> + +**Scan Type:** This option determines what aspect of the files will be compared in the duplicate scan. If you select **Filename**, dupeGuru will compare every filenames word-by-word and, depending on the other settings below, it will determine if enough words are matching to consider 2 files duplicates. If you select **Content**, only files with the exact same content will match. + +**Filter Hardness:** If you chose the **Filename** scan type, this option determines how similar two filenames must be for dupeGuru to consider them duplicates. If the filter hardness is, for example 80, it means that 80% of the words of two filenames must match. To determine the matching percentage, dupeGuru first counts the total number of words in **both** filenames, then count the number of words matching (every word matching count as 2), and then divide the number of words matching by the total number of words. If the result is higher or equal to the filter hardness, we have a duplicate match. For example, "a b c d" and "c d e" have a matching percentage of 57 (4 words matching, 7 total words). + +**Word weighting:** If you chose the **Filename** scan type, this option slightly changes how matching percentage is calculated. With word weighting, instead of having a value of 1 in the duplicate count and total word count, every word have a value equal to the number of characters they have. With word weighting, "ab cde fghi" and "ab cde fghij" would have a matching percentage of 53% (19 total characters, 10 characters matching (4 for "ab" and 6 for "cde")). + +**Match similar words:** If you turn this option on, similar words will be counted as matches. For example "The White Stripes" and "The White Stripe" would have a match % of 100 instead of 66 with that option turned on. **Warning:** Use this option with caution. It is likely that you will get a lot of false positives in your results when turning it on. However, it will help you to find duplicates that you wouldn't have found otherwise. The scan process also is significantly slower with this option turned on. + +**Can mix file kind:** If you check this box, duplicate groups are allowed to have files with different extensions. If you don't check it, well, they aren't! + +**Use regular expressions when filtering:** If you check this box, the filtering feature will treat your filter query as a **regular expression**. Explaining them is beyond the scope of this document. A good place to start learning it is . + +**Remove empty folders after delete or move:** When this option is enabled, folders are deleted after a file is deleted or moved and the folder is empty. + +**Copy and Move:** Determines how the Copy and Move operations (in the Action menu) will behave. + +* **Right in destination:** All files will be sent directly in the selected destination, without trying to recreate the source path at all. +* **Recreate relative path:** The source file's path will be re-created in the destination directory up to the root selection in the Directories panel. For example, if you added "/Users/foobar/Music" to your Directories panel and you move "/Users/foobar/Music/Artist/Album/the_song.mp3" to the destination "/Users/foobar/MyDestination", the final destination for the file will be "/Users/foobar/MyDestination/Artist/Album" ("/Users/foobar/Music" has been trimmed from source's path in the final destination.). +* **Recreate absolute path:** The source file's path will be re-created in the destination directory in it's entirety. For example, if you move "/Users/foobar/Music/Artist/Album/the_song.mp3" to the destination "/Users/foobar/MyDestination", the final destination for the file will be "/Users/foobar/MyDestination/Users/foobar/Music/Artist/Album". + +In all cases, dupeGuru nicely handles naming conflicts by prepending a number to the destination filename if the filename already exists in the destination. diff --git a/se/help/templates/quick_start.mako b/se/help/templates/quick_start.mako new file mode 100644 index 00000000..dde33c65 --- /dev/null +++ b/se/help/templates/quick_start.mako @@ -0,0 +1,18 @@ +<%! + title = 'Quick Start' + selected_menu_item = 'Quick Start' +%> +<%inherit file="/base_dg.mako"/> + +To get you quickly started with dupeGuru, let's just make a standard scan using default preferences. + +* Click on **Directories**. +* Click on **Add**. +* Choose a directory you want to scan for duplicates. +* Click on **Start Scanning**. +* Wait until the scan process is over. +* Look at every duplicate (The files that are indented) and verify that it is indeed a duplicate to the group's reference (The file above the duplicate that is not indented and have a disabled mark box). +* If a file is a false duplicate, select it and click on **Actions-->Remove Selected from Results**. +* Once you are sure that there is no false duplicate in your results, click on **Edit-->Mark All**, and then **Actions-->Send Marked to Recycle bin**. + +That is only a basic scan. There are a lot of tweaking you can do to get different results and several methods of examining and modifying your results. To know about them, just read the rest of this help file. diff --git a/se/help/templates/results.mako b/se/help/templates/results.mako new file mode 100644 index 00000000..53aa176f --- /dev/null +++ b/se/help/templates/results.mako @@ -0,0 +1,73 @@ +<%! + title = 'Results' + selected_menu_item = 'Results' +%> +<%inherit file="/base_dg.mako"/> + +When dupeGuru is finished scanning for duplicates, it will show its results in the form of duplicate group list. + +About duplicate groups +----- + +A duplicate group is a group of files that all match together. Every group has a **reference file** and one or more **duplicate files**. The reference file is the first file of the group. Its mark box is disabled. Below it, and indented, are the duplicate files. + +You can mark duplicate files, but you can never mark the reference file of a group. This is a security measure to prevent dupeGuru from deleting not only duplicate files, but their reference. You sure don't want that, do you? + +What determines which files are reference and which files are duplicates is first their directory state. A files from a reference directory will always be reference in a duplicate group. If all files are from a normal directory, the size determine which file will be the reference of a duplicate group. dupeGuru assumes that you always want to keep the biggest file, so the biggest files will take the reference position. + +You can change the reference file of a group manually. To do so, select the duplicate file you want to promote to reference, and click on **Actions-->Make Selected Reference**. + +Reviewing results +----- + +Although you can just click on **Edit-->Mark All** and then **Actions-->Send Marked to Recycle bin** to quickly delete all duplicate files in your results, it is always recommended to review all duplicates before deleting them. + +To help you reviewing the results, you can bring up the **Details panel**. This panel shows all the details of the currently selected file as well as its reference's details. This is very handy to quickly determine if a duplicate really is a duplicate. You can also double-click on a file to open it with its associated application. + +If you have more false duplicates than true duplicates (If your filter hardness is very low), the best way to proceed would be to review duplicates, mark true duplicates and then click on **Actions-->Send Marked to Recycle bin**. If you have more true duplicates than false duplicates, you can instead mark all files that are false duplicates, and use **Actions-->Remove Marked from Results**. + +Marking and Selecting +----- + +A **marked** duplicate is a duplicate with the little box next to it having a check-mark. A **selected** duplicate is a duplicate being highlighted. The multiple selection actions can be performed in dupeGuru in the standard way (Shift/Command/Control click). You can toggle all selected duplicates' mark state by pressing **space**. + +Delta Values +----- + +If you turn this switch on, some columns will display the value relative to the duplicate's reference instead of the absolute values. These delta values will also be displayed in a different color so you can spot them easily. For example, if a duplicate is 1.2 MB and its reference is 1.4 MB, the Size column will display -0.2 MB. This option is a killer feature when combined with the [Power Marker](power_marker.htm). + +Filtering +----- + +dupeGuru supports post-scan filtering. With it, you can narrow down your results so you can perform actions on a subset of it. For example, you could easily mark all duplicates with their filename containing "copy" from your results using the filter. + +**Windows:** To use the filtering feature, click on Actions --> Apply Filter, write down the filter you want to apply and click OK. To go back to unfiltered results, click on Actions --> Cancel Filter. + +**Mac OS X:** To use the filtering feature, type your filter in the "Filter" search field in the toolbar. To go back to unfiltered result, blank out the field, or click on the "X". + +In simple mode (the default mode), whatever you type as the filter is the string used to perform the actual filtering, with the exception of one wildcard: **\***. Thus, if you type "[*]" as your filter, it will match anything with [] brackets in it, whatever is in between those brackets. + +For more advanced filtering, you can turn "Use regular expressions when filtering" on. The filtering feature will then use **regular expressions**. A regular expression is a language for matching text. Explaining them is beyond the scope of this document. A good place to start learning it is . + +Matches are case insensitive in both simple and regexp mode. + +For the filter to match, your regular expression don't have to match the whole filename, it just have to contain a string matching the expression. + +You might notice that not all duplicates in the filtered results will match your filter. That is because as soon as one single duplicate in a group matches the filter, the whole group stays in the results so you can have a better view of the duplicate's context. However, non-matching duplicates are in "reference mode". Therefore, you can perform actions like Mark All and be sure to only mark filtered duplicates. + +Action Menu +----- + +* **Start Duplicate Scan:** Starts a new duplicate scan. +* **Clear Ignore List:** Remove all ignored matches you added. You have to start a new scan for the newly cleared ignore list to be effective. +* **Export Results to XHTML:** Take the current results, and create an XHTML file out of it. The columns that are visible when you click on this button will be the columns present in the XHTML file. The file will automatically be opened in your default browser. +* **Send Marked to Trash:** Send all marked duplicates to trash, obviously. +* **Move Marked to...:** Prompt you for a destination, and then move all marked files to that destination. Source file's path might be re-created in destination, depending on the "Copy and Move" preference. +* **Copy Marked to...:** Prompt you for a destination, and then copy all marked files to that destination. Source file's path might be re-created in destination, depending on the "Copy and Move" preference. +* **Remove Marked from Results:** Remove all marked duplicates from results. The actual files will not be touched and will stay where they are. +* **Remove Selected from Results:** Remove all selected duplicates from results. Note that all selected reference files will be ignored, only duplicates can be removed with this action. +* **Make Selected Reference:** Promote all selected duplicates to reference. If a duplicate is a part of a group having a reference file coming from a reference directory (in blue color), no action will be taken for this duplicate. If more than one duplicate among the same group are selected, only the first of each group will be promoted. +* **Add Selected to Ignore List:** This first removes all selected duplicates from results, and then add the match of that duplicate and the current reference in the ignore list. This match will not come up again in further scan. The duplicate itself might come back, but it will be matched with another reference file. You can clear the ignore list with the Clear Ignore List command. +* **Open Selected with Default Application:** Open the file with the application associated with selected file's type. +* **Reveal Selected in Finder:** Open the folder containing selected file. +* **Rename Selected:** Prompts you for a new name, and then rename the selected file. diff --git a/se/help/templates/versions.mako b/se/help/templates/versions.mako new file mode 100644 index 00000000..354a294d --- /dev/null +++ b/se/help/templates/versions.mako @@ -0,0 +1,6 @@ +<%! + title = 'dupeGuru version history' + selected_menu_item = 'Version History' +%> +<%inherit file="/base_dg.mako"/> +${self.output_changelogs(changelog)} \ No newline at end of file diff --git a/se/qt/app.py b/se/qt/app.py new file mode 100644 index 00000000..3859a5f8 --- /dev/null +++ b/se/qt/app.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python +# Unit Name: app +# Created By: Virgil Dupras +# Created On: 2009-05-24 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +from dupeguru import data + +from base.app import DupeGuru as DupeGuruBase +from details_dialog import DetailsDialog +from preferences import Preferences +from preferences_dialog import PreferencesDialog + +class DupeGuru(DupeGuruBase): + LOGO_NAME = 'logo_se' + NAME = 'dupeGuru' + VERSION = '2.7.1' + DELTA_COLUMNS = frozenset([2, 4, 5]) + + def __init__(self): + DupeGuruBase.__init__(self, data, appid=4) + + def _update_options(self): + DupeGuruBase._update_options(self) + self.scanner.min_match_percentage = self.prefs.filter_hardness + self.scanner.scan_type = self.prefs.scan_type + self.scanner.word_weighting = self.prefs.word_weighting + self.scanner.match_similar_words = self.prefs.match_similar + threshold = self.prefs.small_file_threshold if self.prefs.ignore_small_files else 0 + self.scanner.size_threshold = threshold * 1024 # threshold is in KB. the scanner wants bytes + + def _create_details_dialog(self, parent): + return DetailsDialog(parent, self) + + def _create_preferences(self): + return Preferences() + + def _create_preferences_dialog(self, parent): + return PreferencesDialog(parent, self) + diff --git a/se/qt/app_win.py b/se/qt/app_win.py new file mode 100644 index 00000000..8088bce6 --- /dev/null +++ b/se/qt/app_win.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python +# Unit Name: app_win +# Created By: Virgil Dupras +# Created On: 2009-05-24 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +import winshell + +import app + +class DupeGuru(app.DupeGuru): + @staticmethod + def _recycle_dupe(dupe): + winshell.delete_file(unicode(dupe.path), no_confirm=True) + diff --git a/se/qt/build.py b/se/qt/build.py new file mode 100644 index 00000000..ff2b3e69 --- /dev/null +++ b/se/qt/build.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python +# Unit Name: build +# Created By: Virgil Dupras +# Created On: 2009-05-24 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +# On Windows, PyInstaller is used to build an exe (py2exe creates a very bad looking icon +# The release version is outdated. Use at least r672 on http://svn.pyinstaller.org/trunk + +import os +import os.path as op +import shutil +from app import DupeGuru + +def print_and_do(cmd): + print cmd + os.system(cmd) + +# Removing build and dist +if op.exists('build'): + shutil.rmtree('build') +if op.exists('dist'): + shutil.rmtree('dist') + +version = DupeGuru.VERSION +versioncomma = version.replace('.', ', ') + ', 0' +verinfo = open('verinfo').read() +verinfo = verinfo.replace('$versioncomma', versioncomma).replace('$version', version) +fp = open('verinfo_tmp', 'w') +fp.write(verinfo) +fp.close() +print_and_do("python C:\\Python26\\pyinstaller\\Build.py dgse.spec") +os.remove('verinfo_tmp') + +print_and_do("xcopy /Y C:\\src\\vs_comp\\msvcrt dist") +print_and_do("xcopy /Y /S /I help\\dupeguru_help dist\\help") + +aicom = '"\\Program Files\\Caphyon\\Advanced Installer\\AdvancedInstaller.com"' +shutil.copy('installer.aip', 'installer_tmp.aip') # this is so we don'a have to re-commit installer.aip at every version change +print_and_do('%s /edit installer_tmp.aip /SetVersion %s' % (aicom, version)) +print_and_do('%s /build installer_tmp.aip -force' % aicom) +os.remove('installer_tmp.aip') \ No newline at end of file diff --git a/se/qt/details_dialog.py b/se/qt/details_dialog.py new file mode 100644 index 00000000..df72ee52 --- /dev/null +++ b/se/qt/details_dialog.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python +# Unit Name: details_dialog +# Created By: Virgil Dupras +# Created On: 2009-05-24 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +from PyQt4.QtCore import Qt +from PyQt4.QtGui import QDialog + +from base.details_table import DetailsModel +from details_dialog_ui import Ui_DetailsDialog + +class DetailsDialog(QDialog, Ui_DetailsDialog): + def __init__(self, parent, app): + QDialog.__init__(self, parent, Qt.Tool) + self.app = app + self.setupUi(self) + self.model = DetailsModel(app) + self.tableView.setModel(self.model) diff --git a/se/qt/details_dialog.ui b/se/qt/details_dialog.ui new file mode 100644 index 00000000..1dc063d0 --- /dev/null +++ b/se/qt/details_dialog.ui @@ -0,0 +1,53 @@ + + + DetailsDialog + + + + 0 + 0 + 502 + 186 + + + + + 200 + 0 + + + + Details + + + + 0 + + + 0 + + + + + true + + + QAbstractItemView::SelectRows + + + false + + + + + + + + DetailsTable + QTableView +
base.details_table
+
+
+ + +
diff --git a/se/qt/dgse.spec b/se/qt/dgse.spec new file mode 100644 index 00000000..e2229597 --- /dev/null +++ b/se/qt/dgse.spec @@ -0,0 +1,19 @@ +# -*- mode: python -*- +a = Analysis([os.path.join(HOMEPATH,'support\\_mountzlib.py'), os.path.join(HOMEPATH,'support\\useUnicode.py'), 'start.py'], + pathex=['C:\\src\\dupeguru\\se\\qt']) +pyz = PYZ(a.pure) +exe = EXE(pyz, + a.scripts, + exclude_binaries=1, + name=os.path.join('build\\pyi.win32\\dupeGuru', 'dupeGuru.exe'), + debug=False, + strip=False, + upx=True, + console=False , icon='base\\images\\dgse_logo.ico', version='verinfo_tmp') +coll = COLLECT( exe, + a.binaries, + a.zipfiles, + a.datas, + strip=False, + upx=True, + name=os.path.join('dist')) diff --git a/se/qt/gen.py b/se/qt/gen.py new file mode 100644 index 00000000..b712a5b6 --- /dev/null +++ b/se/qt/gen.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python +# Unit Name: gen +# Created By: Virgil Dupras +# Created On: 2009-05-24 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +import os + +def print_and_do(cmd): + print cmd + os.system(cmd) + +os.chdir('dupeguru') +print_and_do('python gen.py') +os.chdir('..') + +os.chdir('base') +print_and_do('python gen.py') +os.chdir('..') + +print_and_do("pyuic4 details_dialog.ui > details_dialog_ui.py") +print_and_do("pyuic4 preferences_dialog.ui > preferences_dialog_ui.py") + +os.chdir('help') +print_and_do('python gen.py') +os.chdir('..') diff --git a/se/qt/installer.aip b/se/qt/installer.aip new file mode 100644 index 00000000..c60f88ae --- /dev/null +++ b/se/qt/installer.aip @@ -0,0 +1,257 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/se/qt/preferences.py b/se/qt/preferences.py new file mode 100644 index 00000000..b978bc6d --- /dev/null +++ b/se/qt/preferences.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python +# Unit Name: preferences +# Created By: Virgil Dupras +# Created On: 2009-05-24 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +from dupeguru.scanner import SCAN_TYPE_FILENAME, SCAN_TYPE_CONTENT + +from base.preferences import Preferences as PreferencesBase + +class Preferences(PreferencesBase): + # (width, is_visible) + COLUMNS_DEFAULT_ATTRS = [ + (200, True), # name + (180, True), # path + (60, True), # size + (40, False), # Kind + (120, False), # creation + (120, False), # modification + (60, True), # match % + (120, False), # Words Used + (80, False), # dupe count + ] + + def _load_specific(self, settings, get): + self.scan_type = get('ScanType', self.scan_type) + self.word_weighting = get('WordWeighting', self.word_weighting) + self.match_similar = get('MatchSimilar', self.match_similar) + self.ignore_small_files = get('IgnoreSmallFiles', self.ignore_small_files) + self.small_file_threshold = get('SmallFileThreshold', self.small_file_threshold) + + def _reset_specific(self): + self.filter_hardness = 80 + self.scan_type = SCAN_TYPE_CONTENT + self.word_weighting = True + self.match_similar = False + self.ignore_small_files = True + self.small_file_threshold = 10 # KB + + def _save_specific(self, settings, set_): + set_('ScanType', self.scan_type) + set_('WordWeighting', self.word_weighting) + set_('MatchSimilar', self.match_similar) + set_('IgnoreSmallFiles', self.ignore_small_files) + set_('SmallFileThreshold', self.small_file_threshold) + diff --git a/se/qt/preferences_dialog.py b/se/qt/preferences_dialog.py new file mode 100644 index 00000000..80be1ac9 --- /dev/null +++ b/se/qt/preferences_dialog.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python +# Unit Name: preferences_dialog +# Created By: Virgil Dupras +# Created On: 2009-05-24 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +from PyQt4.QtCore import SIGNAL, Qt +from PyQt4.QtGui import QDialog, QDialogButtonBox + +from hsutil.misc import tryint + +from dupeguru.scanner import SCAN_TYPE_FILENAME, SCAN_TYPE_CONTENT + +from preferences_dialog_ui import Ui_PreferencesDialog +import preferences + +SCAN_TYPE_ORDER = [ + SCAN_TYPE_FILENAME, + SCAN_TYPE_CONTENT, +] + +class PreferencesDialog(QDialog, Ui_PreferencesDialog): + def __init__(self, parent, app): + flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint + QDialog.__init__(self, parent, flags) + self.app = app + self._setupUi() + + self.connect(self.buttonBox, SIGNAL('clicked(QAbstractButton*)'), self.buttonClicked) + self.connect(self.scanTypeComboBox, SIGNAL('currentIndexChanged(int)'), self.scanTypeChanged) + + def _setupUi(self): + self.setupUi(self) + + def load(self, prefs=None): + if prefs is None: + prefs = self.app.prefs + self.filterHardnessSlider.setValue(prefs.filter_hardness) + self.filterHardnessLabel.setNum(prefs.filter_hardness) + scan_type_index = SCAN_TYPE_ORDER.index(prefs.scan_type) + self.scanTypeComboBox.setCurrentIndex(scan_type_index) + setchecked = lambda cb, b: cb.setCheckState(Qt.Checked if b else Qt.Unchecked) + setchecked(self.matchSimilarBox, prefs.match_similar) + setchecked(self.wordWeightingBox, prefs.word_weighting) + setchecked(self.mixFileKindBox, prefs.mix_file_kind) + setchecked(self.useRegexpBox, prefs.use_regexp) + setchecked(self.removeEmptyFoldersBox, prefs.remove_empty_folders) + setchecked(self.ignoreSmallFilesBox, prefs.ignore_small_files) + self.sizeThresholdEdit.setText(unicode(prefs.small_file_threshold)) + self.copyMoveDestinationComboBox.setCurrentIndex(prefs.destination_type) + + def save(self): + prefs = self.app.prefs + prefs.filter_hardness = self.filterHardnessSlider.value() + prefs.scan_type = SCAN_TYPE_ORDER[self.scanTypeComboBox.currentIndex()] + ischecked = lambda cb: cb.checkState() == Qt.Checked + prefs.match_similar = ischecked(self.matchSimilarBox) + prefs.word_weighting = ischecked(self.wordWeightingBox) + prefs.mix_file_kind = ischecked(self.mixFileKindBox) + prefs.use_regexp = ischecked(self.useRegexpBox) + prefs.remove_empty_folders = ischecked(self.removeEmptyFoldersBox) + prefs.ignore_small_files = ischecked(self.ignoreSmallFilesBox) + prefs.small_file_threshold = tryint(self.sizeThresholdEdit.text()) + prefs.destination_type = self.copyMoveDestinationComboBox.currentIndex() + + def resetToDefaults(self): + self.load(preferences.Preferences()) + + #--- Events + def buttonClicked(self, button): + role = self.buttonBox.buttonRole(button) + if role == QDialogButtonBox.ResetRole: + self.resetToDefaults() + + def scanTypeChanged(self, index): + scan_type = SCAN_TYPE_ORDER[self.scanTypeComboBox.currentIndex()] + word_based = scan_type == SCAN_TYPE_FILENAME + self.filterHardnessSlider.setEnabled(word_based) + self.matchSimilarBox.setEnabled(word_based) + self.wordWeightingBox.setEnabled(word_based) + diff --git a/se/qt/preferences_dialog.ui b/se/qt/preferences_dialog.ui new file mode 100644 index 00000000..368228fa --- /dev/null +++ b/se/qt/preferences_dialog.ui @@ -0,0 +1,389 @@ + + + PreferencesDialog + + + + 0 + 0 + 294 + 296 + + + + Preferences + + + false + + + true + + + + + + + + + + + 100 + 0 + + + + + 100 + 16777215 + + + + Scan Type: + + + + + + + + Filename + + + + + Contents + + + + + + + + + + + + + 100 + 0 + + + + + 100 + 16777215 + + + + Filter Hardness: + + + + + + + 0 + + + + + 12 + + + + + + 0 + 0 + + + + 1 + + + 100 + + + true + + + Qt::Horizontal + + + + + + + + 21 + 0 + + + + 100 + + + + + + + + + 0 + + + + + More Results + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Less Results + + + + + + + + + + + + + + 0 + 134 + + + + + + 0 + 0 + 350 + 21 + + + + Word weighting + + + + + + 0 + 22 + 350 + 21 + + + + Match similar words + + + + + + 0 + 44 + 350 + 21 + + + + Can mix file kind + + + + + + 0 + 66 + 350 + 21 + + + + Use regular expressions when filtering + + + + + + 0 + 88 + 350 + 21 + + + + Remove empty folders on delete or move + + + + + + 0 + 110 + 171 + 21 + + + + Ignore files smaller than + + + + + + 170 + 110 + 51 + 22 + + + + + + + 230 + 110 + 21 + 17 + + + + KB + + + + + + + + + + + 100 + 0 + + + + + 100 + 16777215 + + + + Copy and Move: + + + + + + + + 0 + 0 + + + + + Right in destination + + + + + Recreate relative path + + + + + Recreate absolute path + + + + + + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok|QDialogButtonBox::RestoreDefaults + + + + + + + + + filterHardnessSlider + valueChanged(int) + filterHardnessLabel + setNum(int) + + + 182 + 26 + + + 271 + 26 + + + + + buttonBox + accepted() + PreferencesDialog + accept() + + + 182 + 228 + + + 182 + 124 + + + + + buttonBox + rejected() + PreferencesDialog + reject() + + + 182 + 228 + + + 182 + 124 + + + + + diff --git a/se/qt/profile.py b/se/qt/profile.py new file mode 100644 index 00000000..c1aefca9 --- /dev/null +++ b/se/qt/profile.py @@ -0,0 +1,20 @@ +import sys +import cProfile +import pstats + +from PyQt4.QtCore import QCoreApplication +from PyQt4.QtGui import QApplication + +if sys.platform == 'win32': + from app_win import DupeGuru +else: + from app import DupeGuru + +if __name__ == "__main__": + app = QApplication(sys.argv) + QCoreApplication.setOrganizationName('Hardcoded Software') + QCoreApplication.setApplicationName('dupeGuru') + dgapp = DupeGuru() + cProfile.run('app.exec_()', '/tmp/prof') + p = pstats.Stats('/tmp/prof') + p.sort_stats('time').print_stats() \ No newline at end of file diff --git a/se/qt/start.py b/se/qt/start.py new file mode 100644 index 00000000..41118430 --- /dev/null +++ b/se/qt/start.py @@ -0,0 +1,20 @@ +import sys + +from PyQt4.QtCore import QCoreApplication +from PyQt4.QtGui import QApplication, QIcon, QPixmap + +import base.dg_rc + +if sys.platform == 'win32': + from app_win import DupeGuru +else: + from app import DupeGuru + +if __name__ == "__main__": + app = QApplication(sys.argv) + app.setWindowIcon(QIcon(QPixmap(":/logo_se"))) + QCoreApplication.setOrganizationName('Hardcoded Software') + QCoreApplication.setApplicationName(DupeGuru.NAME) + QCoreApplication.setApplicationVersion(DupeGuru.VERSION) + dgapp = DupeGuru() + sys.exit(app.exec_()) \ No newline at end of file diff --git a/se/qt/verinfo b/se/qt/verinfo new file mode 100644 index 00000000..b32801d5 --- /dev/null +++ b/se/qt/verinfo @@ -0,0 +1,28 @@ +VSVersionInfo( + ffi=FixedFileInfo( + filevers=($versioncomma), + prodvers=($versioncomma), + mask=0x17, + flags=0x0, + OS=0x4, + fileType=0x1, + subtype=0x0, + date=(0, 0) + ), + kids=[ + StringFileInfo( + [ + StringTable( + '040904b0', + [StringStruct('CompanyName', 'Hardcoded Software'), + StringStruct('FileDescription', 'dupeGuru'), + StringStruct('FileVersion', '$version'), + StringStruct('InternalName', 'dupeGuru.exe'), + StringStruct('LegalCopyright', '(c) Hardcoded Software. All rights reserved.'), + StringStruct('OriginalFilename', 'dupeGuru.exe'), + StringStruct('ProductName', 'dupeGuru'), + StringStruct('ProductVersion', '$versioncomma')]) + ]), + VarFileInfo([VarStruct('Translation', [1033])]) + ] +) From a17ab6136c3fb2b09a5ebc7a92f46b03cb138f82 Mon Sep 17 00:00:00 2001 From: hsoft Date: Sat, 6 Jun 2009 12:09:02 +0000 Subject: [PATCH 003/275] py: ignore/import adjustments --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%403 --- py/picture/matchbase.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/py/picture/matchbase.py b/py/picture/matchbase.py index cf0d1e89..1bc4d04a 100644 --- a/py/picture/matchbase.py +++ b/py/picture/matchbase.py @@ -14,7 +14,7 @@ from Queue import Empty from collections import defaultdict from hsutil import job -from hs.utils.misc import dedupe +from hsutil.misc import dedupe from dupeguru.engine import Match from block import avgdiff, DifferentBlockCountError, NoBlocksError From dfbbce73d6d2c02e83c3064e8f09c2493a93db43 Mon Sep 17 00:00:00 2001 From: hsoft Date: Sat, 6 Jun 2009 12:11:11 +0000 Subject: [PATCH 004/275] pe cocoa: adapted to the latest structural updates. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%404 --- pe/cocoa/ResultWindow.h | 5 - pe/cocoa/ResultWindow.m | 118 +- pe/cocoa/dupeguru.xcodeproj/hsoft.mode1 | 1380 ------------------- pe/cocoa/dupeguru.xcodeproj/project.pbxproj | 17 - pe/cocoa/gen.py | 14 + pe/cocoa/py/build_py.sh | 2 - pe/cocoa/py/dg_cocoa.py | 10 +- pe/cocoa/py/gen.py | 18 + pe/cocoa/py/setup.py | 17 +- 9 files changed, 55 insertions(+), 1526 deletions(-) delete mode 100644 pe/cocoa/dupeguru.xcodeproj/hsoft.mode1 create mode 100644 pe/cocoa/gen.py delete mode 100755 pe/cocoa/py/build_py.sh create mode 100644 pe/cocoa/py/gen.py diff --git a/pe/cocoa/ResultWindow.h b/pe/cocoa/ResultWindow.h index dcc96079..5816d50a 100644 --- a/pe/cocoa/ResultWindow.h +++ b/pe/cocoa/ResultWindow.h @@ -6,14 +6,9 @@ @interface ResultWindow : ResultWindowBase { IBOutlet NSPopUpButton *actionMenu; - IBOutlet NSView *actionMenuView; - IBOutlet id app; IBOutlet NSMenu *columnsMenu; - IBOutlet NSView *deltaSwitchView; IBOutlet NSSearchField *filterField; - IBOutlet NSView *filterFieldView; IBOutlet NSSegmentedControl *pmSwitch; - IBOutlet NSView *pmSwitchView; IBOutlet NSWindow *preferencesPanel; IBOutlet NSTextField *stats; diff --git a/pe/cocoa/ResultWindow.m b/pe/cocoa/ResultWindow.m index e0679e4f..361dc876 100644 --- a/pe/cocoa/ResultWindow.m +++ b/pe/cocoa/ResultWindow.m @@ -6,15 +6,6 @@ #import "AppDelegate.h" #import "Consts.h" -static NSString* tbbDirectories = @"tbbDirectories"; -static NSString* tbbDetails = @"tbbDetail"; -static NSString* tbbPreferences = @"tbbPreferences"; -static NSString* tbbPowerMarker = @"tbbPowerMarker"; -static NSString* tbbScan = @"tbbScan"; -static NSString* tbbAction = @"tbbAction"; -static NSString* tbbDelta = @"tbbDelta"; -static NSString* tbbFilter = @"tbbFilter"; - @implementation ResultWindow /* Override */ - (void)awakeFromNib @@ -39,12 +30,18 @@ static NSString* tbbFilter = @"tbbFilter"; NSToolbar *t = [[[NSToolbar alloc] initWithIdentifier:@"ResultWindowToolbar"] autorelease]; [t setAllowsUserCustomization:YES]; - [t setAutosavesConfiguration:NO]; + [t setAutosavesConfiguration:YES]; [t setDisplayMode:NSToolbarDisplayModeIconAndLabel]; [t setDelegate:self]; [[self window] setToolbar:t]; } +/* Overrides */ +- (NSString *)logoImageName +{ + return @"dgpe_logo32"; +} + /* Actions */ - (IBAction)changePowerMarker:(id)sender @@ -465,105 +462,4 @@ static NSString* tbbFilter = @"tbbFilter"; [self refreshStats]; } -/* Toolbar */ - -- (NSToolbarItem *)toolbar:(NSToolbar *)toolbar itemForItemIdentifier:(NSString *)itemIdentifier willBeInsertedIntoToolbar:(BOOL)flag -{ - NSToolbarItem *tbi = [[[NSToolbarItem alloc] initWithItemIdentifier:itemIdentifier] autorelease]; - if (itemIdentifier == tbbDirectories) - { - [tbi setLabel: @"Directories"]; - [tbi setToolTip: @"Show/Hide the directories panel."]; - [tbi setImage: [NSImage imageNamed: @"folder32"]]; - [tbi setTarget: self]; - [tbi setAction: @selector(toggleDirectories:)]; - } - else if (itemIdentifier == tbbDetails) - { - [tbi setLabel: @"Details"]; - [tbi setToolTip: @"Show/Hide the details panel."]; - [tbi setImage: [NSImage imageNamed: @"details32"]]; - [tbi setTarget: self]; - [tbi setAction: @selector(toggleDetailsPanel:)]; - } - else if (itemIdentifier == tbbPreferences) - { - [tbi setLabel: @"Preferences"]; - [tbi setToolTip: @"Show the preferences panel."]; - [tbi setImage: [NSImage imageNamed: @"preferences32"]]; - [tbi setTarget: self]; - [tbi setAction: @selector(showPreferencesPanel:)]; - } - else if (itemIdentifier == tbbPowerMarker) - { - [tbi setLabel: @"Power Marker"]; - [tbi setToolTip: @"When enabled, only the duplicates are shown, not the references."]; - [tbi setView:pmSwitchView]; - [tbi setMinSize:[pmSwitchView frame].size]; - [tbi setMaxSize:[pmSwitchView frame].size]; - } - else if (itemIdentifier == tbbScan) - { - [tbi setLabel: @"Start Scanning"]; - [tbi setToolTip: @"Start scanning for duplicates in the selected diectories."]; - [tbi setImage: [NSImage imageNamed: @"dgpe_logo_32"]]; - [tbi setTarget: self]; - [tbi setAction: @selector(startDuplicateScan:)]; - } - else if (itemIdentifier == tbbAction) - { - [tbi setLabel: @"Action"]; - [tbi setView:actionMenuView]; - [tbi setMinSize:[actionMenuView frame].size]; - [tbi setMaxSize:[actionMenuView frame].size]; - } - else if (itemIdentifier == tbbDelta) - { - [tbi setLabel: @"Delta Values"]; - [tbi setToolTip: @"When enabled, this option makes dupeGuru display, where applicable, delta values instead of absolute values."]; - [tbi setView:deltaSwitchView]; - [tbi setMinSize:[deltaSwitchView frame].size]; - [tbi setMaxSize:[deltaSwitchView frame].size]; - } - else if (itemIdentifier == tbbFilter) - { - [tbi setLabel: @"Filter"]; - [tbi setToolTip: @"Filters the results using regular expression."]; - [tbi setView:filterFieldView]; - [tbi setMinSize:[filterFieldView frame].size]; - [tbi setMaxSize:NSMakeSize(1000, [filterFieldView frame].size.height)]; - } - [tbi setPaletteLabel: [tbi label]]; - return tbi; -} - -- (NSArray *)toolbarAllowedItemIdentifiers:(NSToolbar *)toolbar -{ - return [NSArray arrayWithObjects: - tbbDirectories, - tbbDetails, - tbbPreferences, - tbbPowerMarker, - tbbScan, - tbbAction, - tbbDelta, - tbbFilter, - NSToolbarSeparatorItemIdentifier, - NSToolbarSpaceItemIdentifier, - NSToolbarFlexibleSpaceItemIdentifier, - nil]; -} - -- (NSArray *)toolbarDefaultItemIdentifiers:(NSToolbar *)toolbar -{ - return [NSArray arrayWithObjects: - tbbScan, - tbbAction, - tbbDirectories, - tbbDetails, - tbbPowerMarker, - tbbDelta, - tbbFilter, - nil]; -} @end \ No newline at end of file diff --git a/pe/cocoa/dupeguru.xcodeproj/hsoft.mode1 b/pe/cocoa/dupeguru.xcodeproj/hsoft.mode1 deleted file mode 100644 index 699331ae..00000000 --- a/pe/cocoa/dupeguru.xcodeproj/hsoft.mode1 +++ /dev/null @@ -1,1380 +0,0 @@ - - - - - ActivePerspectiveName - Project - AllowedModules - - - BundleLoadPath - - MaxInstances - n - Module - PBXSmartGroupTreeModule - Name - Groups and Files Outline View - - - BundleLoadPath - - MaxInstances - n - Module - PBXNavigatorGroup - Name - Editor - - - BundleLoadPath - - MaxInstances - n - Module - XCTaskListModule - Name - Task List - - - BundleLoadPath - - MaxInstances - n - Module - XCDetailModule - Name - File and Smart Group Detail Viewer - - - BundleLoadPath - - MaxInstances - 1 - Module - PBXBuildResultsModule - Name - Detailed Build Results Viewer - - - BundleLoadPath - - MaxInstances - 1 - Module - PBXProjectFindModule - Name - Project Batch Find Tool - - - BundleLoadPath - - MaxInstances - n - Module - PBXRunSessionModule - Name - Run Log - - - BundleLoadPath - - MaxInstances - n - Module - PBXBookmarksModule - Name - Bookmarks Tool - - - BundleLoadPath - - MaxInstances - n - Module - PBXClassBrowserModule - Name - Class Browser - - - BundleLoadPath - - MaxInstances - n - Module - PBXCVSModule - Name - Source Code Control Tool - - - BundleLoadPath - - MaxInstances - n - Module - PBXDebugBreakpointsModule - Name - Debug Breakpoints Tool - - - BundleLoadPath - - MaxInstances - n - Module - XCDockableInspector - Name - Inspector - - - BundleLoadPath - - MaxInstances - n - Module - PBXOpenQuicklyModule - Name - Open Quickly Tool - - - BundleLoadPath - - MaxInstances - 1 - Module - PBXDebugSessionModule - Name - Debugger - - - BundleLoadPath - - MaxInstances - 1 - Module - PBXDebugCLIModule - Name - Debug Console - - - Description - DefaultDescriptionKey - DockingSystemVisible - - Extension - mode1 - FavBarConfig - - PBXProjectModuleGUID - CE381CB409914B41003581CE - XCBarModuleItemNames - - XCBarModuleItems - - - FirstTimeWindowDisplayed - - Identifier - com.apple.perspectives.project.mode1 - MajorVersion - 31 - MinorVersion - 1 - Name - Default - Notifications - - OpenEditors - - PerspectiveWidths - - -1 - -1 - - Perspectives - - - ChosenToolbarItems - - active-executable-popup - action - active-buildstyle-popup - active-target-popup - buildOrClean - build-and-runOrDebug - com.apple.ide.PBXToolbarStopButton - get-info - toggle-editor - - ControllerClassBaseName - - IconName - WindowOfProjectWithEditor - Identifier - perspective.project - IsVertical - - Layout - - - BecomeActive - - ContentConfiguration - - PBXBottomSmartGroupGIDs - - 1C37FBAC04509CD000000102 - 1C37FAAC04509CD000000102 - 1C08E77C0454961000C914BD - 1C37FABC05509CD000000102 - 1C37FABC05539CD112110102 - E2644B35053B69B200211256 - 1C37FABC04509CD000100104 - 1CC0EA4004350EF90044410B - 1CC0EA4004350EF90041110B - - PBXProjectModuleGUID - 1CE0B1FE06471DED0097A5F4 - PBXProjectModuleLabel - Files - PBXProjectStructureProvided - yes - PBXSmartGroupTreeModuleColumnData - - PBXSmartGroupTreeModuleColumnWidthsKey - - 194 - - PBXSmartGroupTreeModuleColumnsKey_v4 - - MainColumn - - - PBXSmartGroupTreeModuleOutlineStateKey_v7 - - PBXSmartGroupTreeModuleOutlineStateExpansionKey - - 29B97314FDCFA39411CA2CEA - 080E96DDFE201D6D7F000001 - 29B97315FDCFA39411CA2CEA - 29B97317FDCFA39411CA2CEA - 29B97323FDCFA39411CA2CEA - 1058C7A0FEA54F0111CA2CBB - 19C28FACFE9D520D11CA2CBB - 1C37FBAC04509CD000000102 - CE2ACA9A0CA214440012E1E8 - CE2ACA9B0CA214440012E1E8 - CE2ACA9C0CA214440012E1E8 - CE2ACA9D0CA214440012E1E8 - 1C37FAAC04509CD000000102 - 1C37FABC05509CD000000102 - - PBXSmartGroupTreeModuleOutlineStateSelectionKey - - - 17 - 15 - 0 - - - PBXSmartGroupTreeModuleOutlineStateVisibleRectKey - {{0, 0}, {194, 764}} - - PBXTopSmartGroupGIDs - - XCIncludePerspectivesSwitch - - XCSharingToken - com.apple.Xcode.GFSharingToken - - GeometryConfiguration - - Frame - {{0, 0}, {211, 782}} - GroupTreeTableConfiguration - - MainColumn - 194 - - RubberWindowFrame - 4 54 1366 823 0 0 1440 878 - - Module - PBXSmartGroupTreeModule - Proportion - 211pt - - - Dock - - - ContentConfiguration - - PBXProjectModuleGUID - 1CE0B20306471E060097A5F4 - PBXProjectModuleLabel - PyDupeGuru.h - PBXSplitModuleInNavigatorKey - - Split0 - - PBXProjectModuleGUID - 1CE0B20406471E060097A5F4 - PBXProjectModuleLabel - PyDupeGuru.h - _historyCapacity - 10 - bookmark - CE2ACAA20CA214B50012E1E8 - history - - CEE22C420B9A163B000D3096 - CEE584280BACAE4E004F9755 - CEE586070BACBCA6004F9755 - CEE301880BF7350900D6840C - CEE301890BF7350900D6840C - CEE3018A0BF7350900D6840C - CEE301DC0BF73A0300D6840C - CEE301DD0BF73A0300D6840C - CEE301F30BF73A9A00D6840C - CEB6A23E0C9EA80D00767CC9 - - prevStack - - CE2CB4DA09AE70AA0015538F - CEF411510A11093E00E7F110 - CE6E6AE70AA528B2002F29BE - CEFA86C20AAEE6DE00E0FAA1 - CEFA86C30AAEE6DE00E0FAA1 - CED527320AB0C9EB00D70726 - CEE2E1F30AB0E4A900D458B6 - CEE2E1F40AB0E4A900D458B6 - CEF650770ABABB44009F3C83 - CEE3018C0BF7350900D6840C - CEE3018F0BF7350900D6840C - - - SplitCount - 1 - - StatusBarVisibility - - - GeometryConfiguration - - Frame - {{0, 0}, {1150, 544}} - RubberWindowFrame - 4 54 1366 823 0 0 1440 878 - - Module - PBXNavigatorGroup - Proportion - 544pt - - - ContentConfiguration - - PBXProjectModuleGUID - 1CE0B20506471E060097A5F4 - PBXProjectModuleLabel - Detail - - GeometryConfiguration - - Frame - {{0, 549}, {1150, 233}} - RubberWindowFrame - 4 54 1366 823 0 0 1440 878 - - Module - XCDetailModule - Proportion - 233pt - - - Proportion - 1150pt - - - Name - Project - ServiceClasses - - XCModuleDock - PBXSmartGroupTreeModule - XCModuleDock - PBXNavigatorGroup - XCDetailModule - - TableOfContents - - CE2ACA9F0CA214440012E1E8 - 1CE0B1FE06471DED0097A5F4 - CE2ACAA00CA214440012E1E8 - 1CE0B20306471E060097A5F4 - 1CE0B20506471E060097A5F4 - - ToolbarConfiguration - xcode.toolbar.config.default - - - ControllerClassBaseName - - IconName - WindowOfProject - Identifier - perspective.morph - IsVertical - 0 - Layout - - - BecomeActive - 1 - ContentConfiguration - - PBXBottomSmartGroupGIDs - - 1C37FBAC04509CD000000102 - 1C37FAAC04509CD000000102 - 1C08E77C0454961000C914BD - 1C37FABC05509CD000000102 - 1C37FABC05539CD112110102 - E2644B35053B69B200211256 - 1C37FABC04509CD000100104 - 1CC0EA4004350EF90044410B - 1CC0EA4004350EF90041110B - - PBXProjectModuleGUID - 11E0B1FE06471DED0097A5F4 - PBXProjectModuleLabel - Files - PBXProjectStructureProvided - yes - PBXSmartGroupTreeModuleColumnData - - PBXSmartGroupTreeModuleColumnWidthsKey - - 186 - - PBXSmartGroupTreeModuleColumnsKey_v4 - - MainColumn - - - PBXSmartGroupTreeModuleOutlineStateKey_v7 - - PBXSmartGroupTreeModuleOutlineStateExpansionKey - - 29B97314FDCFA39411CA2CEA - 1C37FABC05509CD000000102 - - PBXSmartGroupTreeModuleOutlineStateSelectionKey - - - 0 - - - PBXSmartGroupTreeModuleOutlineStateVisibleRectKey - {{0, 0}, {186, 337}} - - PBXTopSmartGroupGIDs - - XCIncludePerspectivesSwitch - 1 - XCSharingToken - com.apple.Xcode.GFSharingToken - - GeometryConfiguration - - Frame - {{0, 0}, {203, 355}} - GroupTreeTableConfiguration - - MainColumn - 186 - - RubberWindowFrame - 373 269 690 397 0 0 1440 878 - - Module - PBXSmartGroupTreeModule - Proportion - 100% - - - Name - Morph - PreferredWidth - 300 - ServiceClasses - - XCModuleDock - PBXSmartGroupTreeModule - - TableOfContents - - 11E0B1FE06471DED0097A5F4 - - ToolbarConfiguration - xcode.toolbar.config.default.short - - - PerspectivesBarVisible - - ShelfIsVisible - - SourceDescription - file at '/System/Library/PrivateFrameworks/DevToolsInterface.framework/Versions/A/Resources/XCPerspectivesSpecificationMode1.xcperspec' - StatusbarIsVisible - - TimeStamp - 0.0 - ToolbarDisplayMode - 1 - ToolbarIsVisible - - ToolbarSizeMode - 1 - Type - Perspectives - UpdateMessage - The Default Workspace in this version of Xcode now includes support to hide and show the detail view (what has been referred to as the "Metro-Morph" feature). You must discard your current Default Workspace settings and update to the latest Default Workspace in order to gain this feature. Do you wish to update to the latest Workspace defaults for project '%@'? - WindowJustification - 5 - WindowOrderList - - /Users/hsoft/src/dupeguru_pe_cocoa/dupeguru.xcodeproj - - WindowString - 4 54 1366 823 0 0 1440 878 - WindowTools - - - FirstTimeWindowDisplayed - - Identifier - windowTool.build - IsVertical - - Layout - - - Dock - - - ContentConfiguration - - PBXProjectModuleGUID - 1CD0528F0623707200166675 - PBXProjectModuleLabel - - StatusBarVisibility - - - GeometryConfiguration - - Frame - {{0, 0}, {1366, 540}} - RubberWindowFrame - 0 56 1366 822 0 0 1440 878 - - Module - PBXNavigatorGroup - Proportion - 540pt - - - BecomeActive - - ContentConfiguration - - PBXProjectModuleGUID - XCMainBuildResultsModuleGUID - PBXProjectModuleLabel - Build - XCBuildResultsTrigger_Collapse - 1021 - XCBuildResultsTrigger_Open - 1011 - - GeometryConfiguration - - Frame - {{0, 545}, {1366, 236}} - RubberWindowFrame - 0 56 1366 822 0 0 1440 878 - - Module - PBXBuildResultsModule - Proportion - 236pt - - - Proportion - 781pt - - - Name - Build Results - ServiceClasses - - PBXBuildResultsModule - - StatusbarIsVisible - - TableOfContents - - CE381CCE09914BC8003581CE - CEB6A2360C9EA7D700767CC9 - 1CD0528F0623707200166675 - XCMainBuildResultsModuleGUID - - ToolbarConfiguration - xcode.toolbar.config.build - WindowString - 0 56 1366 822 0 0 1440 878 - WindowToolGUID - CE381CCE09914BC8003581CE - WindowToolIsVisible - - - - FirstTimeWindowDisplayed - - Identifier - windowTool.debugger - IsVertical - - Layout - - - Dock - - - ContentConfiguration - - Debugger - - HorizontalSplitView - - _collapsingFrameDimension - 0.0 - _indexOfCollapsedView - 0 - _percentageOfCollapsedView - 0.0 - isCollapsed - yes - sizes - - {{0, 0}, {150, 339}} - {{150, 0}, {874, 339}} - - - VerticalSplitView - - _collapsingFrameDimension - 0.0 - _indexOfCollapsedView - 0 - _percentageOfCollapsedView - 0.0 - isCollapsed - yes - sizes - - {{0, 0}, {1024, 339}} - {{0, 339}, {1024, 306}} - - - - LauncherConfigVersion - 8 - PBXProjectModuleGUID - 1C162984064C10D400B95A72 - PBXProjectModuleLabel - Debug - GLUTExamples (Underwater) - - GeometryConfiguration - - DebugConsoleDrawerSize - {100, 120} - DebugConsoleVisible - None - DebugConsoleWindowFrame - {{200, 200}, {500, 300}} - DebugSTDIOWindowFrame - {{200, 200}, {500, 300}} - Frame - {{0, 0}, {1024, 645}} - RubberWindowFrame - 328 62 1024 686 0 0 1440 878 - - Module - PBXDebugSessionModule - Proportion - 645pt - - - Proportion - 645pt - - - Name - Debugger - ServiceClasses - - PBXDebugSessionModule - - StatusbarIsVisible - - TableOfContents - - 1CD10A99069EF8BA00B06720 - CEC94B9A0BA3652F009F7CBD - 1C162984064C10D400B95A72 - CEC94B9B0BA3652F009F7CBD - CEC94B9C0BA3652F009F7CBD - CEC94B9D0BA3652F009F7CBD - CEC94B9E0BA3652F009F7CBD - CEC94B9F0BA3652F009F7CBD - CEC94BA00BA3652F009F7CBD - - ToolbarConfiguration - xcode.toolbar.config.debug - WindowString - 328 62 1024 686 0 0 1440 878 - WindowToolGUID - 1CD10A99069EF8BA00B06720 - WindowToolIsVisible - - - - FirstTimeWindowDisplayed - - Identifier - windowTool.find - IsVertical - - Layout - - - Dock - - - Dock - - - ContentConfiguration - - PBXProjectModuleGUID - 1CDD528C0622207200134675 - PBXProjectModuleLabel - - StatusBarVisibility - - - GeometryConfiguration - - Frame - {{0, 0}, {781, 212}} - RubberWindowFrame - 31 253 781 470 0 0 1024 746 - - Module - PBXNavigatorGroup - Proportion - 781pt - - - Proportion - 212pt - - - ContentConfiguration - - PBXProjectModuleGUID - 1CD0528E0623707200166675 - PBXProjectModuleLabel - Project Find - - GeometryConfiguration - - Frame - {{0, 217}, {781, 212}} - RubberWindowFrame - 31 253 781 470 0 0 1024 746 - - Module - PBXProjectFindModule - Proportion - 212pt - - - Proportion - 429pt - - - Name - Project Find - ServiceClasses - - PBXProjectFindModule - - StatusbarIsVisible - - TableOfContents - - 1C530D57069F1CE1000CFCEE - CE3755460A37628100022F3B - CE3755470A37628100022F3B - 1CDD528C0622207200134675 - 1CD0528E0623707200166675 - - WindowString - 31 253 781 470 0 0 1024 746 - WindowToolGUID - 1C530D57069F1CE1000CFCEE - WindowToolIsVisible - - - - Identifier - MENUSEPARATOR - - - FirstTimeWindowDisplayed - - Identifier - windowTool.debuggerConsole - IsVertical - - Layout - - - Dock - - - ContentConfiguration - - PBXProjectModuleGUID - 1C78EAAC065D492600B07095 - PBXProjectModuleLabel - Debugger Console - - GeometryConfiguration - - Frame - {{0, 0}, {440, 358}} - RubberWindowFrame - 72 414 440 400 0 0 1440 878 - - Module - PBXDebugCLIModule - Proportion - 358pt - - - Proportion - 359pt - - - Name - Debugger Console - ServiceClasses - - PBXDebugCLIModule - - StatusbarIsVisible - - TableOfContents - - CECD0ADE099294C1003DC359 - CEC94BA10BA3652F009F7CBD - 1C78EAAC065D492600B07095 - - WindowString - 72 414 440 400 0 0 1440 878 - WindowToolGUID - CECD0ADE099294C1003DC359 - WindowToolIsVisible - - - - FirstTimeWindowDisplayed - - Identifier - windowTool.run - IsVertical - - Layout - - - Dock - - - ContentConfiguration - - LauncherConfigVersion - 3 - PBXProjectModuleGUID - 1CD0528B0623707200166675 - PBXProjectModuleLabel - Run - Runner - - HorizontalSplitView - - _collapsingFrameDimension - 0.0 - _indexOfCollapsedView - 0 - _percentageOfCollapsedView - 0.0 - isCollapsed - yes - sizes - - {{0, 0}, {366, 168}} - {{0, 173}, {366, 270}} - - - VerticalSplitView - - _collapsingFrameDimension - 0.0 - _indexOfCollapsedView - 0 - _percentageOfCollapsedView - 0.0 - isCollapsed - yes - sizes - - {{0, 0}, {406, 443}} - {{411, 0}, {517, 443}} - - - - - GeometryConfiguration - - Frame - {{0, 0}, {1377, 781}} - RubberWindowFrame - 0 56 1377 822 0 0 1440 878 - - Module - PBXRunSessionModule - Proportion - 781pt - - - Proportion - 781pt - - - Name - Run Log - ServiceClasses - - PBXRunSessionModule - - StatusbarIsVisible - - TableOfContents - - 1C0AD2B3069F1EA900FABCE6 - CE89234E0BFF46580079C065 - 1CD0528B0623707200166675 - CE89234F0BFF46580079C065 - - ToolbarConfiguration - xcode.toolbar.config.run - WindowString - 0 56 1377 822 0 0 1440 878 - WindowToolGUID - 1C0AD2B3069F1EA900FABCE6 - WindowToolIsVisible - - - - Identifier - windowTool.scm - Layout - - - Dock - - - ContentConfiguration - - PBXProjectModuleGUID - 1C78EAB2065D492600B07095 - PBXProjectModuleLabel - <No Editor> - PBXSplitModuleInNavigatorKey - - Split0 - - PBXProjectModuleGUID - 1C78EAB3065D492600B07095 - - SplitCount - 1 - - StatusBarVisibility - 1 - - GeometryConfiguration - - Frame - {{0, 0}, {452, 0}} - RubberWindowFrame - 743 379 452 308 0 0 1280 1002 - - Module - PBXNavigatorGroup - Proportion - 0pt - - - BecomeActive - 1 - ContentConfiguration - - PBXProjectModuleGUID - 1CD052920623707200166675 - PBXProjectModuleLabel - SCM - - GeometryConfiguration - - ConsoleFrame - {{0, 259}, {452, 0}} - Frame - {{0, 7}, {452, 259}} - RubberWindowFrame - 743 379 452 308 0 0 1280 1002 - TableConfiguration - - Status - 30 - FileName - 199 - Path - 197.09500122070312 - - TableFrame - {{0, 0}, {452, 250}} - - Module - PBXCVSModule - Proportion - 262pt - - - Proportion - 266pt - - - Name - SCM - ServiceClasses - - PBXCVSModule - - StatusbarIsVisible - 1 - TableOfContents - - 1C78EAB4065D492600B07095 - 1C78EAB5065D492600B07095 - 1C78EAB2065D492600B07095 - 1CD052920623707200166675 - - ToolbarConfiguration - xcode.toolbar.config.scm - WindowString - 743 379 452 308 0 0 1280 1002 - - - FirstTimeWindowDisplayed - - Identifier - windowTool.breakpoints - IsVertical - - Layout - - - Dock - - - ContentConfiguration - - PBXBottomSmartGroupGIDs - - 1C77FABC04509CD000000102 - - PBXProjectModuleGUID - 1CE0B1FE06471DED0097A5F4 - PBXProjectModuleLabel - Files - PBXProjectStructureProvided - no - PBXSmartGroupTreeModuleColumnData - - PBXSmartGroupTreeModuleColumnWidthsKey - - 168 - - PBXSmartGroupTreeModuleColumnsKey_v4 - - MainColumn - - - PBXSmartGroupTreeModuleOutlineStateKey_v7 - - PBXSmartGroupTreeModuleOutlineStateExpansionKey - - 1C77FABC04509CD000000102 - - PBXSmartGroupTreeModuleOutlineStateSelectionKey - - - 0 - - - PBXSmartGroupTreeModuleOutlineStateVisibleRectKey - {{0, 0}, {168, 350}} - - PBXTopSmartGroupGIDs - - XCIncludePerspectivesSwitch - - - GeometryConfiguration - - Frame - {{0, 0}, {185, 368}} - GroupTreeTableConfiguration - - MainColumn - 168 - - RubberWindowFrame - 21 314 744 409 0 0 1024 746 - - Module - PBXSmartGroupTreeModule - Proportion - 185pt - - - BecomeActive - - ContentConfiguration - - PBXProjectModuleGUID - 1CA1AED706398EBD00589147 - PBXProjectModuleLabel - Detail - - GeometryConfiguration - - Frame - {{190, 0}, {554, 368}} - RubberWindowFrame - 21 314 744 409 0 0 1024 746 - - Module - XCDetailModule - Proportion - 554pt - - - Proportion - 368pt - - - MajorVersion - 2 - MinorVersion - 0 - Name - Breakpoints - ServiceClasses - - PBXSmartGroupTreeModule - XCDetailModule - - StatusbarIsVisible - - TableOfContents - - CEDA9EAC09D2BBCE00741F3F - CEDA9EAD09D2BBCE00741F3F - 1CE0B1FE06471DED0097A5F4 - 1CA1AED706398EBD00589147 - - ToolbarConfiguration - xcode.toolbar.config.breakpoints - WindowString - 21 314 744 409 0 0 1024 746 - WindowToolGUID - CEDA9EAC09D2BBCE00741F3F - WindowToolIsVisible - - - - Identifier - windowTool.debugAnimator - Layout - - - Dock - - - Module - PBXNavigatorGroup - Proportion - 100% - - - Proportion - 100% - - - Name - Debug Visualizer - ServiceClasses - - PBXNavigatorGroup - - StatusbarIsVisible - 1 - ToolbarConfiguration - xcode.toolbar.config.debugAnimator - WindowString - 100 100 700 500 0 0 1280 1002 - - - Identifier - windowTool.bookmarks - Layout - - - Dock - - - Module - PBXBookmarksModule - Proportion - 100% - - - Proportion - 100% - - - Name - Bookmarks - ServiceClasses - - PBXBookmarksModule - - StatusbarIsVisible - 0 - WindowString - 538 42 401 187 0 0 1280 1002 - - - Identifier - windowTool.classBrowser - Layout - - - Dock - - - BecomeActive - 1 - ContentConfiguration - - OptionsSetName - Hierarchy, all classes - PBXProjectModuleGUID - 1CA6456E063B45B4001379D8 - PBXProjectModuleLabel - Class Browser - NSObject - - GeometryConfiguration - - ClassesFrame - {{0, 0}, {374, 96}} - ClassesTreeTableConfiguration - - PBXClassNameColumnIdentifier - 208 - PBXClassBookColumnIdentifier - 22 - - Frame - {{0, 0}, {630, 331}} - MembersFrame - {{0, 105}, {374, 395}} - MembersTreeTableConfiguration - - PBXMemberTypeIconColumnIdentifier - 22 - PBXMemberNameColumnIdentifier - 216 - PBXMemberTypeColumnIdentifier - 97 - PBXMemberBookColumnIdentifier - 22 - - PBXModuleWindowStatusBarHidden2 - 1 - RubberWindowFrame - 385 179 630 352 0 0 1440 878 - - Module - PBXClassBrowserModule - Proportion - 332pt - - - Proportion - 332pt - - - Name - Class Browser - ServiceClasses - - PBXClassBrowserModule - - StatusbarIsVisible - 0 - TableOfContents - - 1C0AD2AF069F1E9B00FABCE6 - 1C0AD2B0069F1E9B00FABCE6 - 1CA6456E063B45B4001379D8 - - ToolbarConfiguration - xcode.toolbar.config.classbrowser - WindowString - 385 179 630 352 0 0 1440 878 - WindowToolGUID - 1C0AD2AF069F1E9B00FABCE6 - WindowToolIsVisible - 0 - - - - diff --git a/pe/cocoa/dupeguru.xcodeproj/project.pbxproj b/pe/cocoa/dupeguru.xcodeproj/project.pbxproj index 02043065..9d13aec4 100644 --- a/pe/cocoa/dupeguru.xcodeproj/project.pbxproj +++ b/pe/cocoa/dupeguru.xcodeproj/project.pbxproj @@ -323,7 +323,6 @@ isa = PBXNativeTarget; buildConfigurationList = C01FCF4A08A954540054247B /* Build configuration list for PBXNativeTarget "dupeguru" */; buildPhases = ( - CEABE1A60FCC00E3005F8031 /* ShellScript */, 8D1107290486CEB800E47090 /* Resources */, 8D11072C0486CEB800E47090 /* Sources */, 8D11072E0486CEB800E47090 /* Frameworks */, @@ -383,22 +382,6 @@ }; /* End PBXResourcesBuildPhase section */ -/* Begin PBXShellScriptBuildPhase section */ - CEABE1A60FCC00E3005F8031 /* ShellScript */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "cd help\npython gen.py\n/Developer/Applications/Utilities/Help\\ Indexer.app/Contents/MacOS/Help\\ Indexer dupeguru_pe_help"; - }; -/* End PBXShellScriptBuildPhase section */ - /* Begin PBXSourcesBuildPhase section */ 8D11072C0486CEB800E47090 /* Sources */ = { isa = PBXSourcesBuildPhase; diff --git a/pe/cocoa/gen.py b/pe/cocoa/gen.py new file mode 100644 index 00000000..69d7125d --- /dev/null +++ b/pe/cocoa/gen.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python + +import os + +print "Generating help" +os.chdir('help') +os.system('python -u gen.py') +os.system('/Developer/Applications/Utilities/Help\\ Indexer.app/Contents/MacOS/Help\\ Indexer dupeguru_pe_help') +os.chdir('..') + +print "Generating py plugin" +os.chdir('py') +os.system('python -u gen.py') +os.chdir('..') \ No newline at end of file diff --git a/pe/cocoa/py/build_py.sh b/pe/cocoa/py/build_py.sh deleted file mode 100755 index c9ac2993..00000000 --- a/pe/cocoa/py/build_py.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/bash -python setup.py py2app \ No newline at end of file diff --git a/pe/cocoa/py/dg_cocoa.py b/pe/cocoa/py/dg_cocoa.py index 76126ec1..b3aadcb6 100644 --- a/pe/cocoa/py/dg_cocoa.py +++ b/pe/cocoa/py/dg_cocoa.py @@ -1,13 +1,15 @@ #!/usr/bin/env python import objc from AppKit import * -from PyObjCTools import NibClassBuilder - -NibClassBuilder.extractClasses("MainMenu", bundle=NSBundle.mainBundle()) from dupeguru import app_pe_cocoa, scanner -class PyApp(NibClassBuilder.AutoBaseClass): +# Fix py2app imports which chokes on relative imports +from dupeguru import app, app_cocoa, data, directories, engine, export, ignore, results, scanner +from dupeguru.picture import block, cache, matchbase +from hsfs import auto, manual, stats, tree, utils + +class PyApp(NSObject): pass #fake class class PyDupeGuru(PyApp): diff --git a/pe/cocoa/py/gen.py b/pe/cocoa/py/gen.py new file mode 100644 index 00000000..6195927d --- /dev/null +++ b/pe/cocoa/py/gen.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python + +import os +import os.path as op +import shutil + +from hsutil.build import print_and_do + +os.chdir('dupeguru') +print_and_do('python gen.py') +os.chdir('..') + +if op.exists('build'): + shutil.rmtree('build') +if op.exists('dist'): + shutil.rmtree('dist') + +print_and_do('python -u setup.py py2app') \ No newline at end of file diff --git a/pe/cocoa/py/setup.py b/pe/cocoa/py/setup.py index 8bf847db..af81b3ed 100644 --- a/pe/cocoa/py/setup.py +++ b/pe/cocoa/py/setup.py @@ -1,11 +1,14 @@ #!/usr/bin/env python -from setuptools import setup -from hs.build import set_buildenv +from distutils.core import setup +import py2app -set_buildenv() +from hsutil.build import move_testdata_out, put_testdata_back -setup( - plugin=['dg_cocoa.py'], - setup_requires=['py2app'], -) +move_log = move_testdata_out() +try: + setup( + plugin = ['dg_cocoa.py'], + ) +finally: + put_testdata_back(move_log) From 05261c5053e1d52645bc25a997267ee2f4eba691 Mon Sep 17 00:00:00 2001 From: hsoft Date: Sat, 6 Jun 2009 12:35:50 +0000 Subject: [PATCH 005/275] [#18] enforce one liners in the reg code dialog --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%405 --- pe/cocoa/dupeguru.xcodeproj/project.pbxproj | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/pe/cocoa/dupeguru.xcodeproj/project.pbxproj b/pe/cocoa/dupeguru.xcodeproj/project.pbxproj index 9d13aec4..d8ffd079 100644 --- a/pe/cocoa/dupeguru.xcodeproj/project.pbxproj +++ b/pe/cocoa/dupeguru.xcodeproj/project.pbxproj @@ -38,6 +38,8 @@ CE80DB8B0FC1951C0086DCA6 /* DirectoryPanel.m in Sources */ = {isa = PBXBuildFile; fileRef = CE80DB860FC1951C0086DCA6 /* DirectoryPanel.m */; }; CE80DB8C0FC1951C0086DCA6 /* ResultWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = CE80DB890FC1951C0086DCA6 /* ResultWindow.m */; }; CE848A1909DD85810004CB44 /* Consts.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = CE848A1809DD85810004CB44 /* Consts.h */; }; + CEBAE4270FDA97E000B7887D /* BRSingleLineFormatter.m in Sources */ = {isa = PBXBuildFile; fileRef = CEBAE4240FDA97E000B7887D /* BRSingleLineFormatter.m */; }; + CEBAE4280FDA97E000B7887D /* NSCharacterSet_Extensions.m in Sources */ = {isa = PBXBuildFile; fileRef = CEBAE4260FDA97E000B7887D /* NSCharacterSet_Extensions.m */; }; CECA899909DB12CA00A3D774 /* Details.nib in Resources */ = {isa = PBXBuildFile; fileRef = CECA899709DB12CA00A3D774 /* Details.nib */; }; CECA899C09DB132E00A3D774 /* DetailsPanel.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = CECA899A09DB132E00A3D774 /* DetailsPanel.h */; }; CECA899D09DB132E00A3D774 /* DetailsPanel.m in Sources */ = {isa = PBXBuildFile; fileRef = CECA899B09DB132E00A3D774 /* DetailsPanel.m */; }; @@ -124,6 +126,10 @@ CE80DB880FC1951C0086DCA6 /* ResultWindow.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ResultWindow.h; path = dgbase/ResultWindow.h; sourceTree = SOURCE_ROOT; }; CE80DB890FC1951C0086DCA6 /* ResultWindow.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ResultWindow.m; path = dgbase/ResultWindow.m; sourceTree = SOURCE_ROOT; }; CE848A1809DD85810004CB44 /* Consts.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Consts.h; sourceTree = ""; }; + CEBAE4230FDA97E000B7887D /* BRSingleLineFormatter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BRSingleLineFormatter.h; path = cocoalib/brsinglelineformatter/BRSingleLineFormatter.h; sourceTree = SOURCE_ROOT; }; + CEBAE4240FDA97E000B7887D /* BRSingleLineFormatter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BRSingleLineFormatter.m; path = cocoalib/brsinglelineformatter/BRSingleLineFormatter.m; sourceTree = SOURCE_ROOT; }; + CEBAE4250FDA97E000B7887D /* NSCharacterSet_Extensions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = NSCharacterSet_Extensions.h; path = cocoalib/brsinglelineformatter/NSCharacterSet_Extensions.h; sourceTree = SOURCE_ROOT; }; + CEBAE4260FDA97E000B7887D /* NSCharacterSet_Extensions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = NSCharacterSet_Extensions.m; path = cocoalib/brsinglelineformatter/NSCharacterSet_Extensions.m; sourceTree = SOURCE_ROOT; }; CECA899809DB12CA00A3D774 /* English */ = {isa = PBXFileReference; lastKnownFileType = wrapper.nib; name = English; path = English.lproj/Details.nib; sourceTree = ""; }; CECA899A09DB132E00A3D774 /* DetailsPanel.h */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.h; path = DetailsPanel.h; sourceTree = ""; }; CECA899B09DB132E00A3D774 /* DetailsPanel.m */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.objc; path = DetailsPanel.m; sourceTree = ""; }; @@ -250,6 +256,7 @@ CE80DB1A0FC192AB0086DCA6 /* cocoalib */ = { isa = PBXGroup; children = ( + CEBAE4220FDA97E000B7887D /* brsinglelineformatter */, CE80DB700FC194760086DCA6 /* ErrorReportWindow.xib */, CE80DB720FC194760086DCA6 /* progress.nib */, CE80DB740FC194760086DCA6 /* registration.nib */, @@ -295,6 +302,18 @@ name = dgbase; sourceTree = ""; }; + CEBAE4220FDA97E000B7887D /* brsinglelineformatter */ = { + isa = PBXGroup; + children = ( + CEBAE4230FDA97E000B7887D /* BRSingleLineFormatter.h */, + CEBAE4240FDA97E000B7887D /* BRSingleLineFormatter.m */, + CEBAE4250FDA97E000B7887D /* NSCharacterSet_Extensions.h */, + CEBAE4260FDA97E000B7887D /* NSCharacterSet_Extensions.m */, + ); + name = brsinglelineformatter; + path = cocoalib/brsinglelineformatter; + sourceTree = SOURCE_ROOT; + }; CEDA432B0B07C6E600B3091A /* w3 */ = { isa = PBXGroup; children = ( @@ -408,6 +427,8 @@ CE80DB8A0FC1951C0086DCA6 /* AppDelegate.m in Sources */, CE80DB8B0FC1951C0086DCA6 /* DirectoryPanel.m in Sources */, CE80DB8C0FC1951C0086DCA6 /* ResultWindow.m in Sources */, + CEBAE4270FDA97E000B7887D /* BRSingleLineFormatter.m in Sources */, + CEBAE4280FDA97E000B7887D /* NSCharacterSet_Extensions.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; From c08fc8b911678a757c5f9afacb32b532cbf921b3 Mon Sep 17 00:00:00 2001 From: hsoft Date: Sat, 6 Jun 2009 13:45:26 +0000 Subject: [PATCH 006/275] pe cocoa: fixed main icon name --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%406 --- pe/cocoa/ResultWindow.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pe/cocoa/ResultWindow.m b/pe/cocoa/ResultWindow.m index 361dc876..d2e415bf 100644 --- a/pe/cocoa/ResultWindow.m +++ b/pe/cocoa/ResultWindow.m @@ -39,7 +39,7 @@ /* Overrides */ - (NSString *)logoImageName { - return @"dgpe_logo32"; + return @"dgpe_logo_32"; } /* Actions */ From 7f9895e806c59fc3da53945e5011b85820598e1c Mon Sep 17 00:00:00 2001 From: hsoft Date: Sat, 6 Jun 2009 13:49:45 +0000 Subject: [PATCH 007/275] [#22 state:fixed] Error-proofed the path2iphoto query and added more progress feedback to the iphoto deletion process. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%407 --- py/app_pe_cocoa.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/py/app_pe_cocoa.py b/py/app_pe_cocoa.py index 5969d1c3..75fc4868 100644 --- a/py/app_pe_cocoa.py +++ b/py/app_pe_cocoa.py @@ -145,19 +145,24 @@ class DupeGuruPE(app_cocoa.DupeGuru): marked = [dupe for dupe in self.results.dupes if self.results.is_marked(dupe)] self.path2iphoto = {} if any(isinstance(dupe, IPhoto) for dupe in marked): + j = j.start_subjob([6, 4], "Probing iPhoto. Don\'t touch it during the operation!") a = app('iPhoto') a.select(a.photo_library_album()) photos = as_fetch(a.photo_library_album().photos, k.item) - for photo in photos: - self.path2iphoto[photo.image_path()] = photo + for photo in j.iter_with_progress(photos): + self.path2iphoto[unicode(photo.image_path())] = photo + j.start_job(self.results.mark_count, "Sending dupes to the Trash") self.last_op_error_count = self.results.perform_on_marked(op, True) del self.path2iphoto def _do_delete_dupe(self, dupe): if isinstance(dupe, IPhoto): - photo = self.path2iphoto[unicode(dupe.path)] - app('iPhoto').remove(photo) - return True + if unicode(dupe.path) in self.path2iphoto: + photo = self.path2iphoto[unicode(dupe.path)] + app('iPhoto').remove(photo) + return True + else: + logging.warning("Could not find photo {0} in iPhoto Library", unicode(dupe.path)) else: return app_cocoa.DupeGuru._do_delete_dupe(self, dupe) From c5263f0817b460659d6031010d1f510543f6707c Mon Sep 17 00:00:00 2001 From: hsoft Date: Sat, 6 Jun 2009 15:21:20 +0000 Subject: [PATCH 008/275] pe help: updated ignores --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%408 From b05a46a398c3a93e3c0a5a9f4a13079b6f4f450a Mon Sep 17 00:00:00 2001 From: hsoft Date: Sat, 6 Jun 2009 15:22:12 +0000 Subject: [PATCH 009/275] [#19 state:hold] Added "timeout=0" args to appscript call. Let's try and see if it helps. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%409 --- py/app_pe_cocoa.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/py/app_pe_cocoa.py b/py/app_pe_cocoa.py index 75fc4868..eee96086 100644 --- a/py/app_pe_cocoa.py +++ b/py/app_pe_cocoa.py @@ -147,10 +147,10 @@ class DupeGuruPE(app_cocoa.DupeGuru): if any(isinstance(dupe, IPhoto) for dupe in marked): j = j.start_subjob([6, 4], "Probing iPhoto. Don\'t touch it during the operation!") a = app('iPhoto') - a.select(a.photo_library_album()) + a.select(a.photo_library_album(timeout=0), timeout=0) photos = as_fetch(a.photo_library_album().photos, k.item) for photo in j.iter_with_progress(photos): - self.path2iphoto[unicode(photo.image_path())] = photo + self.path2iphoto[unicode(photo.image_path(timeout=0))] = photo j.start_job(self.results.mark_count, "Sending dupes to the Trash") self.last_op_error_count = self.results.perform_on_marked(op, True) del self.path2iphoto From 97872fc086fdc66ab5482ce7db3e5f63ba4b91d7 Mon Sep 17 00:00:00 2001 From: hsoft Date: Sat, 6 Jun 2009 19:55:33 +0000 Subject: [PATCH 010/275] [#23 state:fixed] Wrapped KeyError in prefs for iPhotoDatabase into a InvalidPathError. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4010 --- py/app.py | 4 +--- py/app_pe_cocoa.py | 13 ++++++------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/py/app.py b/py/app.py index 0e03603d..5e0a5f07 100644 --- a/py/app.py +++ b/py/app.py @@ -19,9 +19,7 @@ from hsutil.reg import RegistrableApplication, RegistrationRequired from hsutil.misc import flatten, first from hsutil.str import escape -import directories -import results -import scanner +from . import directories, results, scanner JOB_SCAN = 'job_scan' JOB_LOAD = 'job_load' diff --git a/py/app_pe_cocoa.py b/py/app_pe_cocoa.py index eee96086..6d3b22e9 100644 --- a/py/app_pe_cocoa.py +++ b/py/app_pe_cocoa.py @@ -133,6 +133,8 @@ class DupeGuruPE(app_cocoa.DupeGuru): def _create_iphoto_library(self): ud = NSUserDefaults.standardUserDefaults() prefs = ud.persistentDomainForName_('com.apple.iApps') + if 'iPhotoRecentDatabases' not in prefs: + raise directories.InvalidPathError plisturl = NSURL.URLWithString_(prefs['iPhotoRecentDatabases'][0]) plistpath = Path(plisturl.path()) return IPhotoLibrary(plistpath) @@ -185,13 +187,10 @@ class DupeGuruPE(app_cocoa.DupeGuru): return result def AddDirectory(self, d): - try: - added = self.directories.add_path(Path(d)) - if d == 'iPhoto Library': - added.update() - return 0 - except directories.AlreadyThereError: - return 1 + result = app_cocoa.DupeGuru.AddDirectory(self, d) + if (result == 0) and (d == 'iPhoto Library'): + added.update() + return result def CopyOrMove(self, dupe, copy, destination, dest_type): if isinstance(dupe, IPhoto): From 65692ec2ef3f0b171ff20a17b8ad4c9a243e770a Mon Sep 17 00:00:00 2001 From: hsoft Date: Sun, 7 Jun 2009 06:55:17 +0000 Subject: [PATCH 011/275] pe cocoa: fixed a bug preventing iPhoto library from being added. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4011 --- py/app_pe_cocoa.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/py/app_pe_cocoa.py b/py/app_pe_cocoa.py index 6d3b22e9..30745606 100644 --- a/py/app_pe_cocoa.py +++ b/py/app_pe_cocoa.py @@ -189,7 +189,8 @@ class DupeGuruPE(app_cocoa.DupeGuru): def AddDirectory(self, d): result = app_cocoa.DupeGuru.AddDirectory(self, d) if (result == 0) and (d == 'iPhoto Library'): - added.update() + [iphotolib] = [dir for dir in self.directories if dir.path == d] + iphotolib.update() return result def CopyOrMove(self, dupe, copy, destination, dest_type): From 8b09eb822143c2faa137cd527fc16f9ece507182 Mon Sep 17 00:00:00 2001 From: hsoft Date: Sun, 7 Jun 2009 06:56:44 +0000 Subject: [PATCH 012/275] [#16 state:fixed] Moved the io.makedirs() call into the try..except in CopyOrMove --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4012 --- py/app.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/py/app.py b/py/app.py index 5e0a5f07..99041c7a 100644 --- a/py/app.py +++ b/py/app.py @@ -146,9 +146,9 @@ class DupeGuru(RegistrableApplication): dest_path = dest_path + source_path[1:-1] #Remove drive letter and filename elif dest_type == 1: dest_path = dest_path + source_path[location_path:-1] - if not io.exists(dest_path): - io.makedirs(dest_path) try: + if not io.exists(dest_path): + io.makedirs(dest_path) if copy: files.copy(source_path, dest_path) else: From a1ee45217b56f596faa91347d3a88ceabcbeb0d5 Mon Sep 17 00:00:00 2001 From: hsoft Date: Sun, 7 Jun 2009 07:06:47 +0000 Subject: [PATCH 013/275] pe qt: Removed duplicated code (supposed to be there through externals. I'm not sure how they got there) --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4013 --- pe/qt/base/__init__.py | 0 pe/qt/base/about_box.py | 35 - pe/qt/base/about_box.ui | 133 ---- pe/qt/base/app.py | 269 ------- pe/qt/base/details_table.py | 78 -- pe/qt/base/dg.qrc | 17 - pe/qt/base/directories_dialog.py | 80 --- pe/qt/base/directories_dialog.ui | 133 ---- pe/qt/base/directories_model.py | 108 --- pe/qt/base/error_report_dialog.py | 25 - pe/qt/base/error_report_dialog.ui | 117 --- pe/qt/base/gen.py | 20 - pe/qt/base/images/actions32.png | Bin 3039 -> 0 bytes pe/qt/base/images/delta32.png | Bin 1941 -> 0 bytes pe/qt/base/images/details32.png | Bin 5090 -> 0 bytes pe/qt/base/images/dgme_logo.ico | Bin 17542 -> 0 bytes pe/qt/base/images/dgme_logo_128.png | Bin 18092 -> 0 bytes pe/qt/base/images/dgme_logo_32.png | Bin 6079 -> 0 bytes pe/qt/base/images/dgpe_logo.ico | Bin 17542 -> 0 bytes pe/qt/base/images/dgpe_logo_128.png | Bin 19168 -> 0 bytes pe/qt/base/images/dgpe_logo_32.png | Bin 5818 -> 0 bytes pe/qt/base/images/dgse_logo.ico | Bin 17542 -> 0 bytes pe/qt/base/images/dgse_logo_128.png | Bin 14474 -> 0 bytes pe/qt/base/images/dgse_logo_32.png | Bin 1828 -> 0 bytes pe/qt/base/images/folder32.png | Bin 2427 -> 0 bytes pe/qt/base/images/folderwin32.png | Bin 1519 -> 0 bytes pe/qt/base/images/gear.png | Bin 394 -> 0 bytes pe/qt/base/images/power_marker32.png | Bin 2132 -> 0 bytes pe/qt/base/images/preferences32.png | Bin 2899 -> 0 bytes pe/qt/base/main_window.py | 304 -------- pe/qt/base/main_window.ui | 911 ------------------------ pe/qt/base/preferences.py | 109 --- pe/qt/base/reg.py | 34 - pe/qt/base/reg_demo_dialog.py | 45 -- pe/qt/base/reg_demo_dialog.ui | 140 ---- pe/qt/base/reg_submit_dialog.py | 47 -- pe/qt/base/reg_submit_dialog.ui | 149 ---- pe/qt/base/results_model.py | 175 ----- pe/qt/base/tree_model.py | 66 -- pe/qt/dupeguru/__init__.py | 1 - pe/qt/dupeguru/app.py | 229 ------ pe/qt/dupeguru/app_cocoa.py | 304 -------- pe/qt/dupeguru/app_cocoa_test.py | 320 --------- pe/qt/dupeguru/app_me_cocoa.py | 68 -- pe/qt/dupeguru/app_pe_cocoa.py | 212 ------ pe/qt/dupeguru/app_se_cocoa.py | 13 - pe/qt/dupeguru/app_test.py | 137 ---- pe/qt/dupeguru/data.py | 105 --- pe/qt/dupeguru/data_me.py | 100 --- pe/qt/dupeguru/data_pe.py | 77 -- pe/qt/dupeguru/directories.py | 161 ----- pe/qt/dupeguru/directories_test.py | 280 -------- pe/qt/dupeguru/engine.py | 360 ---------- pe/qt/dupeguru/engine_test.py | 822 --------------------- pe/qt/dupeguru/export.py | 67 -- pe/qt/dupeguru/export_test.py | 91 --- pe/qt/dupeguru/gen.py | 28 - pe/qt/dupeguru/ignore.py | 117 --- pe/qt/dupeguru/ignore_test.py | 158 ---- pe/qt/dupeguru/modules/block/block.pyx | 93 --- pe/qt/dupeguru/modules/block/setup.py | 14 - pe/qt/dupeguru/modules/cache/cache.pyx | 34 - pe/qt/dupeguru/modules/cache/setup.py | 14 - pe/qt/dupeguru/picture/__init__.py | 0 pe/qt/dupeguru/picture/block.py | 124 ---- pe/qt/dupeguru/picture/block_test.py | 313 -------- pe/qt/dupeguru/picture/cache.py | 134 ---- pe/qt/dupeguru/picture/cache_test.py | 159 ----- pe/qt/dupeguru/picture/matchbase.py | 136 ---- pe/qt/dupeguru/results.py | 359 ---------- pe/qt/dupeguru/results_test.py | 742 ------------------- pe/qt/dupeguru/scanner.py | 131 ---- pe/qt/dupeguru/scanner_test.py | 468 ------------ pe/qt/help/changelog.yaml | 174 ----- pe/qt/help/gen.py | 12 - pe/qt/help/skeleton/hardcoded.css | 409 ----------- pe/qt/help/skeleton/images/hs_title.png | Bin 1817 -> 0 bytes pe/qt/help/templates/base_dg.mako | 14 - pe/qt/help/templates/credits.mako | 25 - pe/qt/help/templates/directories.mako | 24 - pe/qt/help/templates/faq.mako | 64 -- pe/qt/help/templates/intro.mako | 13 - pe/qt/help/templates/power_marker.mako | 33 - pe/qt/help/templates/preferences.mako | 23 - pe/qt/help/templates/quick_start.mako | 18 - pe/qt/help/templates/results.mako | 73 -- pe/qt/help/templates/versions.mako | 6 - 87 files changed, 10254 deletions(-) delete mode 100644 pe/qt/base/__init__.py delete mode 100644 pe/qt/base/about_box.py delete mode 100644 pe/qt/base/about_box.ui delete mode 100644 pe/qt/base/app.py delete mode 100644 pe/qt/base/details_table.py delete mode 100644 pe/qt/base/dg.qrc delete mode 100644 pe/qt/base/directories_dialog.py delete mode 100644 pe/qt/base/directories_dialog.ui delete mode 100644 pe/qt/base/directories_model.py delete mode 100644 pe/qt/base/error_report_dialog.py delete mode 100644 pe/qt/base/error_report_dialog.ui delete mode 100644 pe/qt/base/gen.py delete mode 100755 pe/qt/base/images/actions32.png delete mode 100755 pe/qt/base/images/delta32.png delete mode 100644 pe/qt/base/images/details32.png delete mode 100644 pe/qt/base/images/dgme_logo.ico delete mode 100644 pe/qt/base/images/dgme_logo_128.png delete mode 100644 pe/qt/base/images/dgme_logo_32.png delete mode 100644 pe/qt/base/images/dgpe_logo.ico delete mode 100644 pe/qt/base/images/dgpe_logo_128.png delete mode 100755 pe/qt/base/images/dgpe_logo_32.png delete mode 100644 pe/qt/base/images/dgse_logo.ico delete mode 100644 pe/qt/base/images/dgse_logo_128.png delete mode 100755 pe/qt/base/images/dgse_logo_32.png delete mode 100755 pe/qt/base/images/folder32.png delete mode 100755 pe/qt/base/images/folderwin32.png delete mode 100755 pe/qt/base/images/gear.png delete mode 100755 pe/qt/base/images/power_marker32.png delete mode 100755 pe/qt/base/images/preferences32.png delete mode 100644 pe/qt/base/main_window.py delete mode 100644 pe/qt/base/main_window.ui delete mode 100644 pe/qt/base/preferences.py delete mode 100644 pe/qt/base/reg.py delete mode 100644 pe/qt/base/reg_demo_dialog.py delete mode 100644 pe/qt/base/reg_demo_dialog.ui delete mode 100644 pe/qt/base/reg_submit_dialog.py delete mode 100644 pe/qt/base/reg_submit_dialog.ui delete mode 100644 pe/qt/base/results_model.py delete mode 100644 pe/qt/base/tree_model.py delete mode 100644 pe/qt/dupeguru/__init__.py delete mode 100644 pe/qt/dupeguru/app.py delete mode 100644 pe/qt/dupeguru/app_cocoa.py delete mode 100644 pe/qt/dupeguru/app_cocoa_test.py delete mode 100644 pe/qt/dupeguru/app_me_cocoa.py delete mode 100644 pe/qt/dupeguru/app_pe_cocoa.py delete mode 100644 pe/qt/dupeguru/app_se_cocoa.py delete mode 100644 pe/qt/dupeguru/app_test.py delete mode 100644 pe/qt/dupeguru/data.py delete mode 100644 pe/qt/dupeguru/data_me.py delete mode 100644 pe/qt/dupeguru/data_pe.py delete mode 100644 pe/qt/dupeguru/directories.py delete mode 100644 pe/qt/dupeguru/directories_test.py delete mode 100644 pe/qt/dupeguru/engine.py delete mode 100644 pe/qt/dupeguru/engine_test.py delete mode 100644 pe/qt/dupeguru/export.py delete mode 100644 pe/qt/dupeguru/export_test.py delete mode 100644 pe/qt/dupeguru/gen.py delete mode 100644 pe/qt/dupeguru/ignore.py delete mode 100644 pe/qt/dupeguru/ignore_test.py delete mode 100644 pe/qt/dupeguru/modules/block/block.pyx delete mode 100644 pe/qt/dupeguru/modules/block/setup.py delete mode 100644 pe/qt/dupeguru/modules/cache/cache.pyx delete mode 100644 pe/qt/dupeguru/modules/cache/setup.py delete mode 100644 pe/qt/dupeguru/picture/__init__.py delete mode 100644 pe/qt/dupeguru/picture/block.py delete mode 100644 pe/qt/dupeguru/picture/block_test.py delete mode 100644 pe/qt/dupeguru/picture/cache.py delete mode 100644 pe/qt/dupeguru/picture/cache_test.py delete mode 100644 pe/qt/dupeguru/picture/matchbase.py delete mode 100644 pe/qt/dupeguru/results.py delete mode 100644 pe/qt/dupeguru/results_test.py delete mode 100644 pe/qt/dupeguru/scanner.py delete mode 100644 pe/qt/dupeguru/scanner_test.py delete mode 100644 pe/qt/help/changelog.yaml delete mode 100644 pe/qt/help/gen.py delete mode 100644 pe/qt/help/skeleton/hardcoded.css delete mode 100644 pe/qt/help/skeleton/images/hs_title.png delete mode 100644 pe/qt/help/templates/base_dg.mako delete mode 100644 pe/qt/help/templates/credits.mako delete mode 100644 pe/qt/help/templates/directories.mako delete mode 100644 pe/qt/help/templates/faq.mako delete mode 100644 pe/qt/help/templates/intro.mako delete mode 100644 pe/qt/help/templates/power_marker.mako delete mode 100644 pe/qt/help/templates/preferences.mako delete mode 100644 pe/qt/help/templates/quick_start.mako delete mode 100644 pe/qt/help/templates/results.mako delete mode 100644 pe/qt/help/templates/versions.mako diff --git a/pe/qt/base/__init__.py b/pe/qt/base/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/pe/qt/base/about_box.py b/pe/qt/base/about_box.py deleted file mode 100644 index 55a36eb1..00000000 --- a/pe/qt/base/about_box.py +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env python -# Unit Name: about_box -# Created By: Virgil Dupras -# Created On: 2009-05-09 -# $Id$ -# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) - -from PyQt4.QtCore import Qt, QCoreApplication, SIGNAL -from PyQt4.QtGui import QDialog, QDialogButtonBox, QPixmap - -from about_box_ui import Ui_AboutBox - -class AboutBox(QDialog, Ui_AboutBox): - def __init__(self, parent, app): - flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint | Qt.MSWindowsFixedSizeDialogHint - QDialog.__init__(self, parent, flags) - self.app = app - self._setupUi() - - self.connect(self.buttonBox, SIGNAL('clicked(QAbstractButton*)'), self.buttonClicked) - - def _setupUi(self): - self.setupUi(self) - # Stuff that can't be done in the Designer - self.setWindowTitle(u"About %s" % QCoreApplication.instance().applicationName()) - self.nameLabel.setText(QCoreApplication.instance().applicationName()) - self.versionLabel.setText('Version ' + QCoreApplication.instance().applicationVersion()) - self.logoLabel.setPixmap(QPixmap(':/%s_big' % self.app.LOGO_NAME)) - self.registerButton = self.buttonBox.addButton("Register", QDialogButtonBox.ActionRole) - - #--- Events - def buttonClicked(self, button): - if button is self.registerButton: - self.app.ask_for_reg_code() - diff --git a/pe/qt/base/about_box.ui b/pe/qt/base/about_box.ui deleted file mode 100644 index aa9c5ce5..00000000 --- a/pe/qt/base/about_box.ui +++ /dev/null @@ -1,133 +0,0 @@ - - - AboutBox - - - - 0 - 0 - 400 - 190 - - - - - 0 - 0 - - - - About dupeGuru - - - - - - - - - :/logo_me_big - - - - - - - - - - 75 - true - - - - dupeGuru - - - - - - - Version - - - - - - - Copyright Hardcoded Software 2009 - - - - - - - - 75 - true - - - - Registered To: - - - - - - - UNREGISTERED - - - - - - - Qt::Horizontal - - - QDialogButtonBox::Ok - - - - - - - - - - - - - buttonBox - accepted() - AboutBox - accept() - - - 248 - 254 - - - 157 - 274 - - - - - buttonBox - rejected() - AboutBox - reject() - - - 316 - 260 - - - 286 - 274 - - - - - diff --git a/pe/qt/base/app.py b/pe/qt/base/app.py deleted file mode 100644 index 3fa340bf..00000000 --- a/pe/qt/base/app.py +++ /dev/null @@ -1,269 +0,0 @@ -#!/usr/bin/env python -# Unit Name: app -# Created By: Virgil Dupras -# Created On: 2009-04-25 -# $Id$ -# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) - -import logging -import os.path as op -import traceback - -from PyQt4.QtCore import Qt, QTimer, QObject, QCoreApplication, QUrl, SIGNAL -from PyQt4.QtGui import QProgressDialog, QDesktopServices, QFileDialog, QDialog, QMessageBox - -from hsutil import job -from hsutil.reg import RegistrationRequired - -from dupeguru import data_pe -from dupeguru.app import (DupeGuru as DupeGuruBase, JOB_SCAN, JOB_LOAD, JOB_MOVE, JOB_COPY, - JOB_DELETE) - -from main_window import MainWindow -from directories_dialog import DirectoriesDialog -from about_box import AboutBox -from reg import Registration -from error_report_dialog import ErrorReportDialog - -JOBID2TITLE = { - JOB_SCAN: "Scanning for duplicates", - JOB_LOAD: "Loading", - JOB_MOVE: "Moving", - JOB_COPY: "Copying", - JOB_DELETE: "Sending files to the recycle bin", -} - -class Progress(QProgressDialog, job.ThreadedJobPerformer): - def __init__(self, parent): - flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint - QProgressDialog.__init__(self, '', u"Cancel", 0, 100, parent, flags) - self.setModal(True) - self.setAutoReset(False) - self.setAutoClose(False) - self._timer = QTimer() - self._jobid = '' - self.connect(self._timer, SIGNAL('timeout()'), self.updateProgress) - - def updateProgress(self): - # the values might change before setValue happens - last_progress = self.last_progress - last_desc = self.last_desc - if not self._job_running or last_progress is None: - self._timer.stop() - self.close() - self.emit(SIGNAL('finished(QString)'), self._jobid) - if self._last_error is not None: - s = ''.join(traceback.format_exception(*self._last_error)) - dialog = ErrorReportDialog(self.parent(), s) - dialog.exec_() - return - if self.wasCanceled(): - self.job_cancelled = True - return - if last_desc: - self.setLabelText(last_desc) - self.setValue(last_progress) - - def run(self, jobid, title, target, args=()): - self._jobid = jobid - self.reset() - self.setLabelText('') - self.run_threaded(target, args) - self.setWindowTitle(title) - self.show() - self._timer.start(500) - - -def demo_method(method): - def wrapper(self, *args, **kwargs): - try: - return method(self, *args, **kwargs) - except RegistrationRequired: - msg = "The demo version of dupeGuru only allows 10 actions (delete/move/copy) per session." - QMessageBox.information(self.main_window, 'Demo', msg) - - return wrapper - -class DupeGuru(DupeGuruBase, QObject): - LOGO_NAME = '' - NAME = '' - DELTA_COLUMNS = frozenset() - - def __init__(self, data_module, appid): - appdata = unicode(QDesktopServices.storageLocation(QDesktopServices.DataLocation)) - DupeGuruBase.__init__(self, data_module, appdata, appid) - QObject.__init__(self) - self._setup() - - #--- Private - def _setup(self): - self.selected_dupe = None - self.prefs = self._create_preferences() - self.prefs.load() - self._update_options() - self.main_window = self._create_main_window() - self._progress = Progress(self.main_window) - self.directories_dialog = DirectoriesDialog(self.main_window, self) - self.details_dialog = self._create_details_dialog(self.main_window) - self.preferences_dialog = self._create_preferences_dialog(self.main_window) - self.about_box = AboutBox(self.main_window, self) - - self.reg = Registration(self) - self.set_registration(self.prefs.registration_code, self.prefs.registration_email) - if not self.registered: - self.reg.show_nag() - self.main_window.show() - self.load() - - self.connect(QCoreApplication.instance(), SIGNAL('aboutToQuit()'), self.application_will_terminate) - self.connect(self._progress, SIGNAL('finished(QString)'), self.job_finished) - - def _setup_as_registered(self): - self.prefs.registration_code = self.registration_code - self.prefs.registration_email = self.registration_email - self.main_window.actionRegister.setVisible(False) - self.about_box.registerButton.hide() - self.about_box.registeredEmailLabel.setText(self.prefs.registration_email) - - def _update_options(self): - self.scanner.mix_file_kind = self.prefs.mix_file_kind - self.options['escape_filter_regexp'] = self.prefs.use_regexp - self.options['clean_empty_dirs'] = self.prefs.remove_empty_folders - - #--- Virtual - def _create_details_dialog(self, parent): - raise NotImplementedError() - - def _create_main_window(self): - return MainWindow(app=self) - - def _create_preferences(self): - raise NotImplementedError() - - def _create_preferences_dialog(self, parent): - raise NotImplementedError() - - #--- Override - def _start_job(self, jobid, func): - title = JOBID2TITLE[jobid] - try: - j = self._progress.create_job() - self._progress.run(jobid, title, func, args=(j, )) - except job.JobInProgressError: - msg = "A previous action is still hanging in there. You can't start a new one yet. Wait a few seconds, then try again." - QMessageBox.information(self.main_window, 'Action in progress', msg) - - #--- Public - def add_dupes_to_ignore_list(self, duplicates): - for dupe in duplicates: - self.AddToIgnoreList(dupe) - self.remove_duplicates(duplicates) - - def ApplyFilter(self, filter): - DupeGuruBase.ApplyFilter(self, filter) - self.emit(SIGNAL('resultsChanged()')) - - def ask_for_reg_code(self): - if self.reg.ask_for_code(): - self._setup_ui_as_registered() - - @demo_method - def copy_or_move_marked(self, copy): - opname = 'copy' if copy else 'move' - title = "Select a directory to {0} marked files to".format(opname) - flags = QFileDialog.ShowDirsOnly - destination = unicode(QFileDialog.getExistingDirectory(self.main_window, title, '', flags)) - if not destination: - return - recreate_path = self.prefs.destination_type - DupeGuruBase.copy_or_move_marked(self, copy, destination, recreate_path) - - delete_marked = demo_method(DupeGuruBase.delete_marked) - - def make_reference(self, duplicates): - DupeGuruBase.make_reference(self, duplicates) - self.emit(SIGNAL('resultsChanged()')) - - def mark_all(self): - self.results.mark_all() - self.emit(SIGNAL('dupeMarkingChanged()')) - - def mark_invert(self): - self.results.mark_invert() - self.emit(SIGNAL('dupeMarkingChanged()')) - - def mark_none(self): - self.results.mark_none() - self.emit(SIGNAL('dupeMarkingChanged()')) - - def open_selected(self): - if self.selected_dupe is None: - return - url = QUrl.fromLocalFile(unicode(self.selected_dupe.path)) - QDesktopServices.openUrl(url) - - def remove_duplicates(self, duplicates): - self.results.remove_duplicates(duplicates) - self.emit(SIGNAL('resultsChanged()')) - - def remove_marked_duplicates(self): - marked = [d for d in self.results.dupes if self.results.is_marked(d)] - self.remove_duplicates(marked) - - def rename_dupe(self, dupe, newname): - try: - dupe.move(dupe.parent, newname) - return True - except (IndexError, fs.FSError) as e: - logging.warning("dupeGuru Warning: %s" % unicode(e)) - return False - - def reveal_selected(self): - if self.selected_dupe is None: - return - url = QUrl.fromLocalFile(unicode(self.selected_dupe.path[:-1])) - QDesktopServices.openUrl(url) - - def select_duplicate(self, dupe): - self.selected_dupe = dupe - self.emit(SIGNAL('duplicateSelected()')) - - def show_about_box(self): - self.about_box.show() - - def show_details(self): - self.details_dialog.show() - - def show_directories(self): - self.directories_dialog.show() - - def show_help(self): - url = QUrl.fromLocalFile(op.abspath('help/intro.htm')) - QDesktopServices.openUrl(url) - - def show_preferences(self): - self.preferences_dialog.load() - result = self.preferences_dialog.exec_() - if result == QDialog.Accepted: - self.preferences_dialog.save() - self.prefs.save() - self._update_options() - - def toggle_marking_for_dupes(self, dupes): - for dupe in dupes: - self.results.mark_toggle(dupe) - self.emit(SIGNAL('dupeMarkingChanged()')) - - #--- Events - def application_will_terminate(self): - self.Save() - self.SaveIgnoreList() - - def job_finished(self, jobid): - self.emit(SIGNAL('resultsChanged()')) - if jobid == JOB_LOAD: - self.emit(SIGNAL('directoriesChanged()')) - if jobid in (JOB_MOVE, JOB_COPY, JOB_DELETE) and self.last_op_error_count > 0: - msg = "{0} files could not be processed.".format(self.results.mark_count) - QMessageBox.warning(self.main_window, 'Warning', msg) - diff --git a/pe/qt/base/details_table.py b/pe/qt/base/details_table.py deleted file mode 100644 index 1c45de1e..00000000 --- a/pe/qt/base/details_table.py +++ /dev/null @@ -1,78 +0,0 @@ -#!/usr/bin/env python -# Unit Name: details_table -# Created By: Virgil Dupras -# Created On: 2009-05-17 -# $Id$ -# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) - -from PyQt4.QtCore import Qt, SIGNAL, QAbstractTableModel, QVariant -from PyQt4.QtGui import QHeaderView, QTableView - -HEADER = ['Attribute', 'Selected', 'Reference'] - -class DetailsModel(QAbstractTableModel): - def __init__(self, app): - QAbstractTableModel.__init__(self) - self._app = app - self._data = app.data - self._dupe_data = None - self._ref_data = None - self.connect(app, SIGNAL('duplicateSelected()'), self.duplicateSelected) - - def columnCount(self, parent): - return len(HEADER) - - def data(self, index, role): - if not index.isValid(): - return QVariant() - if role != Qt.DisplayRole: - return QVariant() - column = index.column() - row = index.row() - if column == 0: - return QVariant(self._data.COLUMNS[row]['display']) - elif column == 1 and self._dupe_data: - return QVariant(self._dupe_data[row]) - elif column == 2 and self._ref_data: - return QVariant(self._ref_data[row]) - return QVariant() - - def headerData(self, section, orientation, role): - if orientation == Qt.Horizontal and role == Qt.DisplayRole and section < len(HEADER): - return QVariant(HEADER[section]) - return QVariant() - - def rowCount(self, parent): - return len(self._data.COLUMNS) - - #--- Events - def duplicateSelected(self): - dupe = self._app.selected_dupe - group = self._app.results.get_group_of_duplicate(dupe) - ref = group.ref - self._dupe_data = self._data.GetDisplayInfo(dupe, group) - self._ref_data = self._data.GetDisplayInfo(ref, group) - self.reset() - - -class DetailsTable(QTableView): - def __init__(self, *args): - QTableView.__init__(self, *args) - self.setAlternatingRowColors(True) - self.setSelectionBehavior(QTableView.SelectRows) - self.setShowGrid(False) - - def setModel(self, model): - QTableView.setModel(self, model) - # The model needs to be set to set header stuff - hheader = self.horizontalHeader() - hheader.setHighlightSections(False) - hheader.setStretchLastSection(False) - hheader.resizeSection(0, 100) - hheader.setResizeMode(0, QHeaderView.Fixed) - hheader.setResizeMode(1, QHeaderView.Stretch) - hheader.setResizeMode(2, QHeaderView.Stretch) - vheader = self.verticalHeader() - vheader.setVisible(False) - vheader.setDefaultSectionSize(18) - diff --git a/pe/qt/base/dg.qrc b/pe/qt/base/dg.qrc deleted file mode 100644 index f2f5e936..00000000 --- a/pe/qt/base/dg.qrc +++ /dev/null @@ -1,17 +0,0 @@ - - - images/details32.png - images/dgpe_logo_32.png - images/dgpe_logo_128.png - images/dgme_logo_32.png - images/dgme_logo_128.png - images/dgse_logo_32.png - images/dgse_logo_128.png - images/folderwin32.png - images/gear.png - images/preferences32.png - images/actions32.png - images/delta32.png - images/power_marker32.png - - \ No newline at end of file diff --git a/pe/qt/base/directories_dialog.py b/pe/qt/base/directories_dialog.py deleted file mode 100644 index e2f3ddb3..00000000 --- a/pe/qt/base/directories_dialog.py +++ /dev/null @@ -1,80 +0,0 @@ -#!/usr/bin/env python -# Unit Name: directories_dialog -# Created By: Virgil Dupras -# Created On: 2009-04-25 -# $Id$ -# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) - -from PyQt4.QtCore import SIGNAL, Qt -from PyQt4.QtGui import QDialog, QFileDialog, QHeaderView - -from directories_dialog_ui import Ui_DirectoriesDialog -from directories_model import DirectoriesModel, DirectoriesDelegate - -class DirectoriesDialog(QDialog, Ui_DirectoriesDialog): - def __init__(self, parent, app): - flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint - QDialog.__init__(self, parent, flags) - self.app = app - self._setupUi() - self._updateRemoveButton() - - self.connect(self.doneButton, SIGNAL('clicked()'), self.doneButtonClicked) - self.connect(self.addButton, SIGNAL('clicked()'), self.addButtonClicked) - self.connect(self.removeButton, SIGNAL('clicked()'), self.removeButtonClicked) - self.connect(self.treeView.selectionModel(), SIGNAL('selectionChanged(QItemSelection,QItemSelection)'), self.selectionChanged) - self.connect(self.app, SIGNAL('directoriesChanged()'), self.directoriesChanged) - - def _setupUi(self): - self.setupUi(self) - # Stuff that can't be done in the Designer - self.directoriesModel = DirectoriesModel(self.app) - self.directoriesDelegate = DirectoriesDelegate() - self.treeView.setItemDelegate(self.directoriesDelegate) - self.treeView.setModel(self.directoriesModel) - - header = self.treeView.header() - header.setStretchLastSection(False) - header.setResizeMode(0, QHeaderView.Stretch) - header.setResizeMode(1, QHeaderView.Fixed) - header.resizeSection(1, 100) - - def _updateRemoveButton(self): - indexes = self.treeView.selectedIndexes() - if not indexes: - self.removeButton.setEnabled(False) - return - self.removeButton.setEnabled(True) - index = indexes[0] - node = index.internalPointer() - # label = 'Remove' if node.parent is None else 'Exclude' - - def addButtonClicked(self): - title = u"Select a directory to add to the scanning list" - flags = QFileDialog.ShowDirsOnly - dirpath = unicode(QFileDialog.getExistingDirectory(self, title, '', flags)) - if not dirpath: - return - self.app.AddDirectory(dirpath) - self.directoriesModel.reset() - - def directoriesChanged(self): - self.directoriesModel.reset() - - def doneButtonClicked(self): - self.hide() - - def removeButtonClicked(self): - indexes = self.treeView.selectedIndexes() - if not indexes: - return - index = indexes[0] - node = index.internalPointer() - if node.parent is None: - row = index.row() - del self.app.directories[row] - self.directoriesModel.reset() - - def selectionChanged(self, selected, deselected): - self._updateRemoveButton() - diff --git a/pe/qt/base/directories_dialog.ui b/pe/qt/base/directories_dialog.ui deleted file mode 100644 index 68bc8d84..00000000 --- a/pe/qt/base/directories_dialog.ui +++ /dev/null @@ -1,133 +0,0 @@ - - - DirectoriesDialog - - - - 0 - 0 - 420 - 338 - - - - Directories - - - - - - QAbstractItemView::DoubleClicked|QAbstractItemView::EditKeyPressed|QAbstractItemView::SelectedClicked - - - true - - - false - - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - 91 - 0 - - - - - 16777215 - 32 - - - - Remove - - - - - - - - 91 - 0 - - - - - 16777215 - 32 - - - - Add - - - - - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 40 - 20 - - - - - - - - - 0 - 0 - - - - - 91 - 0 - - - - - 16777215 - 32 - - - - Done - - - true - - - - - - - - - - diff --git a/pe/qt/base/directories_model.py b/pe/qt/base/directories_model.py deleted file mode 100644 index cae88f39..00000000 --- a/pe/qt/base/directories_model.py +++ /dev/null @@ -1,108 +0,0 @@ -#!/usr/bin/env python -# Unit Name: directories_model -# Created By: Virgil Dupras -# Created On: 2009-04-25 -# $Id$ -# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) - -from PyQt4.QtCore import QVariant, QModelIndex, Qt, QRect, QEvent, QPoint -from PyQt4.QtGui import QComboBox, QStyledItemDelegate, QMouseEvent, QApplication, QBrush - -from tree_model import TreeNode, TreeModel - -HEADERS = ['Name', 'State'] -STATES = ['Normal', 'Reference', 'Excluded'] - -class DirectoriesDelegate(QStyledItemDelegate): - def createEditor(self, parent, option, index): - editor = QComboBox(parent); - editor.addItems(STATES) - return editor - - def setEditorData(self, editor, index): - value, ok = index.model().data(index, Qt.EditRole).toInt() - assert ok - editor.setCurrentIndex(value); - press = QMouseEvent(QEvent.MouseButtonPress, QPoint(0, 0), Qt.LeftButton, Qt.LeftButton, Qt.NoModifier) - release = QMouseEvent(QEvent.MouseButtonRelease, QPoint(0, 0), Qt.LeftButton, Qt.LeftButton, Qt.NoModifier) - QApplication.sendEvent(editor, press) - QApplication.sendEvent(editor, release) - # editor.showPopup() # this causes a weird glitch. the ugly workaround is above. - - def setModelData(self, editor, model, index): - value = QVariant(editor.currentIndex()) - model.setData(index, value, Qt.EditRole) - - def updateEditorGeometry(self, editor, option, index): - editor.setGeometry(option.rect) - - -class DirectoryNode(TreeNode): - def __init__(self, parent, ref, row): - TreeNode.__init__(self, parent, row) - self.ref = ref - - def _get_children(self): - children = [] - for index, directory in enumerate(self.ref.dirs): - node = DirectoryNode(self, directory, index) - children.append(node) - return children - - -class DirectoriesModel(TreeModel): - def __init__(self, app): - self._dirs = app.directories - TreeModel.__init__(self) - - def _root_nodes(self): - nodes = [] - for index, directory in enumerate(self._dirs): - nodes.append(DirectoryNode(None, directory, index)) - return nodes - - def columnCount(self, parent): - return 2 - - def data(self, index, role): - if not index.isValid(): - return QVariant() - node = index.internalPointer() - if role == Qt.DisplayRole: - if index.column() == 0: - return QVariant(node.ref.name) - else: - return QVariant(STATES[self._dirs.GetState(node.ref.path)]) - elif role == Qt.EditRole and index.column() == 1: - return QVariant(self._dirs.GetState(node.ref.path)) - elif role == Qt.ForegroundRole: - state = self._dirs.GetState(node.ref.path) - if state == 1: - return QVariant(QBrush(Qt.blue)) - elif state == 2: - return QVariant(QBrush(Qt.red)) - return QVariant() - - def flags(self, index): - if not index.isValid(): - return 0 - result = Qt.ItemIsEnabled | Qt.ItemIsSelectable - if index.column() == 1: - result |= Qt.ItemIsEditable - return result - - def headerData(self, section, orientation, role): - if orientation == Qt.Horizontal: - if role == Qt.DisplayRole and section < len(HEADERS): - return QVariant(HEADERS[section]) - return QVariant() - - def setData(self, index, value, role): - if not index.isValid() or role != Qt.EditRole or index.column() != 1: - return False - node = index.internalPointer() - state, ok = value.toInt() - assert ok - self._dirs.SetState(node.ref.path, state) - return True - diff --git a/pe/qt/base/error_report_dialog.py b/pe/qt/base/error_report_dialog.py deleted file mode 100644 index 4aa8f977..00000000 --- a/pe/qt/base/error_report_dialog.py +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env python -# Unit Name: error_report_dialog -# Created By: Virgil Dupras -# Created On: 2009-05-23 -# $Id$ -# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) - -from PyQt4.QtCore import Qt, QUrl -from PyQt4.QtGui import QDialog, QDesktopServices - -from error_report_dialog_ui import Ui_ErrorReportDialog - -class ErrorReportDialog(QDialog, Ui_ErrorReportDialog): - def __init__(self, parent, error): - flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint - QDialog.__init__(self, parent, flags) - self.setupUi(self) - self.errorTextEdit.setPlainText(error) - - def accept(self): - text = self.errorTextEdit.toPlainText() - url = QUrl("mailto:support@hardcoded.net?SUBJECT=Error Report&BODY=%s" % text) - QDesktopServices.openUrl(url) - QDialog.accept(self) - diff --git a/pe/qt/base/error_report_dialog.ui b/pe/qt/base/error_report_dialog.ui deleted file mode 100644 index 0974dd2f..00000000 --- a/pe/qt/base/error_report_dialog.ui +++ /dev/null @@ -1,117 +0,0 @@ - - - ErrorReportDialog - - - - 0 - 0 - 553 - 349 - - - - Error Report - - - - - - Something went wrong. Would you like to send the error report to Hardcoded Software? - - - true - - - - - - - true - - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - 110 - 0 - - - - Don't Send - - - - - - - - 110 - 0 - - - - Send - - - true - - - - - - - - - - - sendButton - clicked() - ErrorReportDialog - accept() - - - 485 - 320 - - - 276 - 174 - - - - - dontSendButton - clicked() - ErrorReportDialog - reject() - - - 373 - 320 - - - 276 - 174 - - - - - diff --git a/pe/qt/base/gen.py b/pe/qt/base/gen.py deleted file mode 100644 index 3b0df2fa..00000000 --- a/pe/qt/base/gen.py +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env python -# Unit Name: gen -# Created By: Virgil Dupras -# Created On: 2009-05-22 -# $Id$ -# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) - -import os - -def print_and_do(cmd): - print cmd - os.system(cmd) - -print_and_do("pyuic4 main_window.ui > main_window_ui.py") -print_and_do("pyuic4 directories_dialog.ui > directories_dialog_ui.py") -print_and_do("pyuic4 about_box.ui > about_box_ui.py") -print_and_do("pyuic4 reg_submit_dialog.ui > reg_submit_dialog_ui.py") -print_and_do("pyuic4 reg_demo_dialog.ui > reg_demo_dialog_ui.py") -print_and_do("pyuic4 error_report_dialog.ui > error_report_dialog_ui.py") -print_and_do("pyrcc4 dg.qrc > dg_rc.py") \ No newline at end of file diff --git a/pe/qt/base/images/actions32.png b/pe/qt/base/images/actions32.png deleted file mode 100755 index 16d64fc1e439ab6ae90a1e11cc99831813b2416f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3039 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 diff --git a/pe/qt/base/images/delta32.png b/pe/qt/base/images/delta32.png deleted file mode 100755 index f7d95da07919c34aa960d37448642e229e75f1e6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1941 zcmV;G2Wt3RCwCt zS8Gs|*BO4k%iebZcVR&!3IRdrf+*408YCDms3l`#Qytr+HZj&|%@Yh+mbXj@U6o(toJChVaZb-Lv_IXNj18GV4^IK0!*0jJZM_Wb%P^Xe6)u{Duc z66EDF}KQKfSUMGn@lO; zwOUzKU2W97_}#z81i>1WB*bJ1;yt;ewb8n(K{X}{I z^AyMolN5s?n8@_Gu^4j5{6?Q2Mz0tCYu6xs*$dg{d=(=3I=$>kdu_k%|@y8f>UXBhj%kD+@?;YuETVyevoDhY_WNL#`0Za6G!WILW`cvw7iF9pl!0(hmn2>ln|<5RJ>b?5;1JPgllEk`$85O2DqE!mFQN z!a!yUDCACl)J1U)xTn2vhe8ODC8CrKCrG^H{Xr5f-H$V>Zhxmy^XibphjZ6cCJriY zV_HxgwvR16R4-ilIY*jx8Vbn>&=1y)reziJV7 zWb^@EN&WO4l#;kZH}%kg0s2moBgPc?^xs9i%3iB8{ABz3oP86MZhSU~^h?z4oZIKR;IGDok1U7SN7!T z@hK0bV$IAPxw1nL{48mz(P&;@$?5yc*68091c&Ox0EX#?pFNC;}=J8tI38Z z6J*wkV&It?w0+czvyTo#a^-j;0YQm&dc*6H^Z$Q;be4bN*# zcNd);KwvtABkAJ1dkBX{rx4?3eW9TOIkv^v{YVPTl}oYZ&wqp%i_eoM2yyujTekel zH#LPW0`QlHytc^+4^CZ9mD-VX00`hM%8}<;2D@E_J#|Lnc6m(|;;OZDo_B&wb`579NW`!c+$I9=l z+j70$fI^GGyloY;v+sHky$*EJbUYgXimAJL@oqBtRfKG!W<{^Y(m4l7so=3$SGqJ;|)3(~3=Ka%lfbnWbcSI%|0K~V^;+EvLguF8i}K)1V?RNAx|xp&;4Y&h4=DG{!O<-q8_ z+Z;DX|Ctd$-$cdlMhtsCAbOKXV=Z38LuFJU8jU_Y;j6wf8fRdc>b1H=JQm`a|NGbR be+3u-4f`}H8YPeZ00000NkvXXu0mjf0Q9d* diff --git a/pe/qt/base/images/details32.png b/pe/qt/base/images/details32.png deleted file mode 100644 index 8ab245b41ba6f367f19d01227df99b85872bac81..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5090 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 YiNbjNhnHL=%16}s>3mJzOVs%P0VM`{F#rGn diff --git a/pe/qt/base/images/dgme_logo_128.png b/pe/qt/base/images/dgme_logo_128.png deleted file mode 100644 index 187c21c8cfb4e7541a63ce36689f8cb444d17b68..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18092 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 diff --git a/pe/qt/base/images/dgme_logo_32.png b/pe/qt/base/images/dgme_logo_32.png deleted file mode 100644 index 88488462f4a694cc1156f7894a0ebf78aa235d88..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6079 zcmV;w7eMHVP)4Tx0C=30S9vtmUH9MLd#`K0=6Sk?h$zWCX1G%3ITEhxny2nHhr|<^hiH%? zQz1oC5*3o9jA@WWC8>}nGKKs8K|R0qzUy7@`t3i?`kc?%`|Quz=d5+s2H+4ThJ*y5 z03a|Zlwxzh0DsuY8BhNju)qx5AOey^5;a8M%E}D>t1Z0*1OVW<0x={cfI-#w^&Cl3Ig zo>WpG04^Hp&s2~4_Td)BH01)1Os@Fax002N(3;^if(P#_O0Q9*48#y%EuQD2KqZ|PJ z7N9MF6h;Z(t`~yz0{pM})|s~rjR2qkKoor84dRBZqXy8Uba?D@hUd)MI1GCUml#hF zU#UQeP^^fa=mefBPLqt0`XwDEGbL*;*C8*fkiC0V(PQuPeJ0B7s%mP*>KmF4T9>uw zbyf7j_c!UU8R{4%5;~1nOe7AP9`Z9iWmafjZ_#NvVl{8WY%6PLV;|#C>p1Gf(NU3Mi%k1^FHm<;k)L)C%_{xE2t*;Oo;a}Bg!tSU?@izM>t=E zSme&AJ<-a?wPOrp55yUtFo;(_DVHFW$db7J*W9TONiUP1oNi9JoSK~$l}^ks%~a3Y znJtvVo=cazcxF7WJHPg9*15p*rWbYQW!Og-TS*NyKDZ*TTBw%@vYyRND7PC;|p-Qz8u zt$O$9?ls@1K2UzJ-uAS;yd(ahd#C;*i7s^4$H!gWuB}pSS<&wTy>OC{BET-|&I@ zQR(CN$;+Rdr&y-$e)gH>o$mY+_EqNV*tfzNyIJ&X!Q76y((nA=lfJLc7yj+>gRsD| zkhRFMnEsRLXa17h(#z%QmFugU>+Uof0PukoTtNhoT9hx^2@^**N}s|Q%X|kX#9quL z#1qJuDsV!`PDH~$ zOWyCUuc*IlaL4etkqtr3So8qHfd!NCgU=4#F}-A#WbSQYWbwr^!)l+^d+W0{R<_)> zqjnYcK@Mt;;P~utfs>cBqVv*`yDrC%>Kxs4ZFf86ZbsxHj(8N2Ts?(7N62TrY`i(W zhkefZ9`(cfjr$h{I0mu>b_K-+s|EiEsXum%qDonz-VO~3lL~tko)n=JF&23`N;&F7 zbnbCN3^t}e_F|mh39WeM_>q(430{ffiLd@jIpvh3n9Q0ybGk3(MrvMKWV&O9N+w(8 z>#U4ysqA|>uDSHNjc5Gwr1IY9Upnh{PVC&|`PvKA0=0tGi_L`*MJh#$#f>FlmozSI zmOd&=E4QxTtr)E=z3h8MwF+DHrn;df^QvF1(KXR)%h#XR)z@d=h-~n@>DXv_%jC9h z)80GyW~S!(yTdKFTMO^S-}ioC)2839-l67{b+7?nH~~3O4vo+W&*1~i!zRLl2qE%_He!LeBcaG?q!?*L zo*`2x6iN)Gi*iFHpsG;)s1>vX+7cauzKR~juwwKv;h0*?B%LUo6I~wNAeIelj!nb% z&@<8>qEDwEWDsB=G1M_^GFmd0Gp;gOGhJn-VG7rFI^tsz@sbu&>N^B?EJ{C?$=~TJD=j-G*R<=DyuE^y!qV>EJ=Kb7dqb5x z_Sq^Mt7xh2R+CZ}(%{z=)RNFv(9zO8sAsp|Ro~kn$S~F@pK!zYr3oC|dx&g$-mJ%b z%~IBiY+YvycFy*#j?#y-oz{-HyL7qoxVgGl5@$%Np2_5KZ&ROcKNtVuK>47!;DKYp zlptzb7;kuJ#LFl`^k__TTv|NgA(~2o=X?E$KG840R<&2*x$qzWEdx585 zzOcWz{Zdm|OU1*>{Z&IX&ue?Gch(O#%--T_GHecSxpc4n!ASc`r%abmck5Hh-t_*p zA-CbXFPBE#-fE7weY`#8H*NLRWA@44l*L=iF00gE9P7jlg^iWXR2r6+NTUIO1Q@_k z2!{+PhbHKOF_=RD;Xou1WrTn@BECorl7&$BLs036s>J=J`R!94z zi_t?EdW<$E6jO`&w0-kKSaz%>HXA!c&r9z@UrzsxL6sqqVUSUrF^sW?NrEYkX_Q%; zxtImT;>R+~x}UWHCy6Uy<6_HZXJ^mj5acN1+|Aj-WzIFh9ma#<$>tT~y}?J|8|4oZ z;1XyMv=>|xDiuB`{8OYv)Le8;>-|AL{h#zR~-(f5QN0C}%_< z_!ws&Xf^qMh}~4j%-j6DMXwc;^+B5)+cA4Zha|^ICllxEE`mohTsPc(iML2}o_6Fb zUh6&%zK#Ao0TF>?!A2oBC}Px{Fm!ly#C(*0^g>KloYINDlL3j0f0ZX`BtJ`WO`T1T z&ZN(}kRzVkm}ijRch2X6NWoZPNwM`My3(d{ze>T&y;Vsygj)7%lXV?88g5>@^`NP% znbIQNI(omTjntvmDe!3X@#2%!XUu(40~SNahwER?j>wN{juFPg-#1LsO}R`z`X)6S z_kHLGW|41+X?bupcx`;0cLTdIwpm28pwR$80|Frr8esswB7pEB@`xegfMl z@)ISDGDcBR<*3(acJv{13c44=gK@=FV}8;Z(w(9Egw@36VrS`1>FXH;7?K%Q8G{+W zFp-%)FuOC4v$(T-W{t$*aJ6jKY#Z#?IEb7)oIPAo+|t~GJaN1VydU_A`N;wrg2IBI zg{}yDi^zyfi(VBA#%qYvi@%k)Bl&p;kF>Ik$xdt8!*Z^>{Ny7Q5_ae8xuV#$cXl7Y zGC_r^TCCQq0h+p6iQ0X-B6>di8}!!=VZXq?(6<==Q|xEmlEzZ!^8V$z z6|t4fm4($qt7WUJYgTI&Ym>jEevyAw{+e2sUiVthS--bFy}`Aix8b*uztO%iv&p|{ zvUy_j_T~&tlIBdyp>@+}09*YM001x<1_uOF@MeZa|D(D4|5{)`*zX*i0KgLDVPUzA zr$a)m>;Qll0M@ACwgdoR2SC`pe2ouoW4tHP$P@sC0T9J#A43ZOU;#kPyeP&tTl*k~ z{fLLG000Mo_>qI`ZMXTzkN_*QZJZhsYG4BZ`~XNHnM(NW(-`ez=eQl)MG3R92LLGm zWY|C0)Mh)^iYM7^Ib5RA4*JP-j7 zh7b%CAOjVQVG=0d{m07zCV>LJ{}!bHUO)s2gaa9<;15$kfj~Fk1Pb7{J)QswC_sdu z-;Cl_#V?BMf7T5l7y=*|{)sUC59YVRe|&wx6N3Nnq~E-oZvrK-!7KcHaFnKl4@MfJ zgi*m5V019L7!AN;Q#60015AwE2I3XQAZCPyhhK;E*VaueVPqUOyxxfGm$c7)07F zk5^JuREK{7Eu(XH1v_`6000OsNkl$#<3~3WJ#zv!3%e6M0f}&VkZ^%SG$cgX zI=iy$%0H|=a{`!%c5r&ZU+5rQ43%{ia{-T~f}|873GRpYAHR#mL} z$Gzh3g5GfR=+GdhC61LQny7M&tCz1>0yv6FUB~AZ6ygBX!5Be@?Hg60z^@ku0;{7= z-(3*#O_=-DogKk&NC2t;eSW_DcxA9`*+Oe>5d23^4;J1Sl!F zb+90m*S+S6-&_n}6GHD+mN;Kry6lGQC(;)I^_oXp9^A0;v0o)+6D4VoiK}ErOww_k zOhQLVYsmfGJoLb=x374n`&8v+(cY$D5IaCFM^aJrBPUO-?vqD&?}bD;w*4_f4!FX(Ecv`;BdL!~=SD?eeSAJqgmW1UcD6%bMuAg(_!= z&YtbO_VUo&=Qh0l%8Hx6b#bgCF7?{b_s8NtPr=jG@T^}me%uRT8J4`U^&My7tkU}K zp24cOUVY`SfB_imp4^a_c<+}00Pi2A#h?7#sHnQ~s>G2Y;wQS1;|lVqLN23`8Xl&$ zuD;M$;&tvh_|8^eV8Of%2ai8wz`6^a;(EVf-Tevh%?0yV|Ng%4;Z(`j_8!Sr4|W}M z?tE|4%fRT=g(^)Q*wM!}p{?DlZn|tq`ec@oqa(;CRWgYTiZV<(p5)TyS4W3)qH)Kc zciaK|tJ%Xdc~0-$dvhw61CQ?M;LZIqoxMXKh@@g&Qb^jop`(MrsqZ`eAOc>HdF1Wg zEo=N=U4*l2J_;_9JutWrFr$2Spi6uDmn|ECt-y`Vb&V@+qiIy&zE?J~`=m)vERNG* zN6%%+GkT3JTgEQ5a{>T(t|#%xzxVEZ*xy)#tGomaCwa9^9M5^RMfd&v{`cXD6B}>( z)4`X%yJqvpt-3#xtYa6u@vtUsM;0s7Ag0AT;9ye+87mZPw+Y1Lc*c?fvw!TQqm7tN`? zr)%#{Qn46yH4CGipMG+1(+CIDEn604`#&Ys_5r)suj6oUj0j+0B&50x5hd&%-Af4_#IyiJoO4oOsHGxSQ1S+cC-nzr5PPW7(hNIC7DhED1BJ{Qo z;3_R~96ix_EDqbtOJ*+@lPR1CNDdc*DB%j{BRlM;hBKL$(9Jf>W;2ty-2QV-U^@9; z{Pwkn(tFxUMu!tvm8I;+jP82-Xy-jk=9Yexjg9zC!vF*Xz*!XHi`ei_2eR3chJ}JjQqCd> zO z3=iC|f0{C=>V7l4J`blTAzHY~e@B?>UsC}Bxr(&lua zD;lC*M3O{ANjk0I^8ru}b@hIjR}@}h+9k=Acpct6o(=xawJ$T{@x-A#o*M7LE_el4 zxZnT(@yA9$%&>h)(tRc?3qMb@{;!a<=LHez{tBP3JT9gsgL?Kx5aIv_2h~A=$oUn4 z(D?#a9Qf+OBdZT*rY_Sp>8s9Y}Eki0EkG^W|5h5*eSv zBBogjJA3DMg=82w2!g>u*j2b|J9{^vN%8pq73Q9D6poQCdjD3iw;2Yu{f9vReu>bp zZzST61))g$kwh$U=GxN~OA9HVuWjuD!NS}N`u6e0+8bkNDc13^R$1R>%21uK@I*5UC;LJ}k(1E7C@zrPv& z*ha6wMIf*rhtE&}b~ZW2IV7TR4NxB6ApSrOzeps_6p%q+)z23!aqj$W#$OZH=pDQi zgbw5J8LGhEwh*kXQV{zfAZV|zyYUN!@^L~c1lNX-5&Pc-zZ&uTge(V3w--ZTmw14NHTz+JU+$o z*KzYlAkv+85QN~|_xgK{KjQZbTLT2Y1w?`doI@i~4;2Ed$VL2aof1|eM%`&6AiO^3 zbm!j>|G-B7@by3nrUM~p0wJGU$7LW_7XeA>E=wg+3qr((3aLP_vhw@e%^xRSd}Bbw zW*~*FK#8W|9GVCs;V5v_l>ter4;tr=P&u93XDuy$Ub|12|8uyu|La4oiJce}y$!L? z2b!3T?_df@B$L3|y&Nb~I$O+$O$3Dd8xF#-$9kW`-i_bdn%jSk``7%x8avupW~`Wb zj-kot7>YC_g!cA2&-`OSQM-T^wgE#dK>V}7SvMQ)p`pM~ObSV`9t0EmleINB_q%ZS z#BXKg3qmRg1OmxGB0c0N-ya2MK5^-jD9aV zc{@03)&fH=0Wq}%Xi*#Zhpz>NWBdnVnjnRQb`{CQ;&|h}^=RCC*4#5W2!cRJ`+-2D z=|r39Wd{fCh5lBOOEe|U{4>zwnB$npdNGvPTFfTzjY~faF)2sECwMog-8KQE-3YAD z4xoK^f-Zb7gePtUhSs!`1no{sW4^cNN8s51a_;wh2JYO(Ho$0?l58Z{Id}u#LA(KF zI{dfzX_A$Rh=@}ZDQn>RqA9{m<(#}FI^_Upytf1Cu?^U`!ywK$3F5++AgcKta1F1! zPYUe)D8^^6Se^=&eFJ{8c7K82Xp4G*JwF)uA}6X4Bw(<EquQ$KOF10(`>z&%;j8TAj!6&ai>p{9Xd#19yX~SIrNkh_3G*7e(Z-B{6f} z1zq!Xa9?-_qBeX2;_xAx2nRCXQCIjK$)q55|3}!LjGx2K7loh>LhJ(Z@;_(XNfn1Z zJYg5er0qaB%mcY}F~lSv1~>nP9|%%h^Y|I^rmT$J@ioNk{syGk4aY?eM5(jeu+QY` zp@=&O%hSic8#^~=1bljxOse?r=L|tgHSVQvMkMY6xvT>y;Q|no?Wi9Yfy!|W#1@?c zUCONQNQtsqN)RTDSZXF6wPw4dyzM+e$ZA~l)t@@Mk3t(d0N8ACOpsOi&kj;VNOb!0Yf(uD5IbTOws+x1z0R^LfYqUn4BZQK@(ZA*hJLg-c)LWIhPsfmGr2MU?YMKkZ$E2auo3$H5Ee7w^oF&wHlNx!>D$3c z;WrTZfAkJePb*w}s;|WiJ_btFYUBczc43yU2G)7KQMa=(2SJ>93h1(n;5p|#h}!W5 zND8MN7q~<;(PF1kw9`z)G-JjWs5Xn3Q1r{u&qe>dTTfb;dqOW0cd!wJLU_y&m_Bj% zgI&wU|8vol3OKU74$_muFCu=EKRWuPSfO=|oAO>v$vIH@;~wO)30RMBeoI&@wMJV4}Y&z7aB-{s^erMAJof%FIy_4f{oA6 zr?)4V@Z7<&e-OmP=EA&bb+BboEzGLTg`$)|7#ya9ogH;hUYPhU-u{Q|1yZWkx(``& zJGSNuC=-qV6LG+}7qJOPfgOAT*xa+g4!Z#C=+}Xsa2;IR??CjP`yd%Sa;L~Ax;U`$ zm9GM$*Mp;`9QS^YA1H#ULmgrqLn}*Bhn20D!Cn{+17q``ZALw8Y99wJBQqdB-Urfx z93ea088X5&uyNiPm|BzjEw8sZGLFx7kW!W0wP?ky*rvBZk%jXj74hQyU~>@j(3gO% zcoo<&uLC>bZD6Lp2Z{wBK-|H9f^vA<6`@ia<1_o!Pr@^gfC{xCR51Y*Ar%T%)~as@XQg#Ke`fp1^&R8?SzDR_ z*)e*^2z7#rtVqaB3W9a>Y79GDOAQ$*QIPB}hk4D_hVvVnAw1mgEaLC=@1B8HXr#KT z4c7)P`T(3qT?VT7We}HN1h(puah|Ys*MOaT9oT8_0XydoF!S$%Zsn&Cx%W$8f>YWk zeQ;3N%r_tE+*W{{RSD|KNW|}pYt_rh?O3BNIdS}WXsAku^iU0Ch2b0tQNxrmd4^+4 z$3jW|AUM8z1)S}ugzU5^804>nNn;8P@10r;&6SB@BO=nDhM!iuD~OWyH^Xoq$F#o- z5esfYQ0onl;ye-8Tm`lP$Gq{KF6^`42X^6I5Vw5<;d{OV-Q*qrARX*g>bx1tA_`7} z$gU1~hE(K30->L!FYZObARq(5+TI(62*G zP)f0X^U+&D3viCqzX{^Wh<)l!U}xP1cHRfTEcg(dmVE*dJMRG#l)QvuSR22R?H{>& zu0Wo!7{{Ko>-~(_g@pUJgh(v+T$Bu;B~voObL1O))`?v!8!x`c9CD8^OjZ1IIlS#~=L;G8jZ6*>Orp z826vEci%3rDolX15a&nuRWPnR1KvE?1`Ars;KH$W@W$#Y*xr-@Ej9TtBrV2p=HNQR zJKHD1#>NZ?jtt)XB&<*RO&XGx7PI;;D7m%Te4`8h%-hCwE5-K^z5X*$q}IG97T7%} zca9wu9J>(&4pnHga6EGm7GK041Oy@9g}TSM|G(Dx+S1DGXh_5PpB|mS~jyiPn z(kXCg(P(fB^lu%#a+Os-8=Gg^i=|s~cTU7&f6@y{^k~u%! zeQlrN^;K1{bbLDMpJ*5ouED)b1&e2oH@v-X4y5H{gWaEopDk|9Y?t{<(w_@a}p(WvvSEW>f__P|fb_h5|0IDf8u zcW>n6(bI%m!GSR8PX$MaDBKK_0zQR<%W+h z?ts&CvY|LD`YWECe)5A`w7pL@4jqfKD?!LMgjK^0~^oJxYYld7-Z;gBLEtz5@;!i;Q$R+;@KTXg^{BM2w_OjM7 z>9B5H`%mwmSO(js<-y6L>)_z*Vi<{fC}*ILfm_#{Zsp1lw1bA{#=)%%yJ6R~T=4Yu zSjCOQvmDZp^vsxL?}KvO8zAOv9^??1_@m$)v>Sq>mm!BHU&8gn&}?6=zVwb*Qi<5( zAL0Lb{*a_%kH|r2#Q%ojhomJ<#gLUA2Unk8YgpM_07v(&g!N515Fg`jwA;F|b9Huv z7K)OC;OfauuxfG+kWxw6vtZ}iNQ2WdV;0{A<;Y7${)0_@9vJT(;Oemjd_!j-e(?mZ z9gcq=M_u+!iL37$7=r^ou`@C)tuM*Nom1RGaQxtAR44;S_=HY^yQ1N(QafL$|( zLejus$V8hn2W`3TXUK_ift)xmIJdhU=F|>>zBabOoXlrAq(Or-W7=+lviubgb9R2j zexT8B(R*$HkAQjuP12=YKMXB4bJS+OC6Sju#^3L;{tQO07RTRSV8qY0-*bHP+3bWs zxUjb!W{=H)9b1>du?6Lvp8|!kZjg^98*y`mTfrjEHE*LKaL2WcUv>MGfZqVQ9ugsY$&gk(Ys0-*{ZNoc;;K zchI%}BL2MuU-TINk=M^HACVaf`<6C9+oU2`KDQo@E~|qvC4*pMUNB6`4}q~6{!kk4 zVSI-4P-ob@unwje{eV7-XTi?3VSNW>#>{#bT=LI?B6J_>oAn@v>Iq;lHQw4Um$>>|fpuNfYiO5}IT#FFNc2Pp|vHyDf#^ve5v%+Cwb^x>#M?xdw zomvqOQ}Dd0AQYMl!=R-o+*sFS1{l}ylBvUC{^XJ$d-bt!?Uwm_HLY}!=<=7}h!}Aa z!ZUY6*uV`C9JdnZ&NQGX{dSE1*mn0p5Sl8vI>#VWX96RO$2dC|r$!FMLH8BLSaQ1C zarLTjZAqa{9hBrAVSEc``-G zl1M_Dgu3ZB<8|)4`w?fZp6WPucITnP`fZ1s<@K68iJI(3)pG2R&d%g73+b>N& zaem*HV@FoLcxY?u?Y%1+zB##OJRF%*2+*_Pd`ddfA^)| z*-tyOmz_M8Yhh*e3;bX_csziIrj3)0wFz&s$#J~KI(JQ1t-9Y;+i>mh_wIUnLDzM6 zd*l7QNB3jRGsFLTJ!;QgORl<-YFbz1J^G%><06B5-v|mf+ZBoUzMlDh`mar$^i#yHfW!a&>tWP`EV3xJTQNt zh%a*4FA%xx z(o7)G9_I`FFvjEakT1lTn@D{M?d50olYS4EToo$UdDOly`ON$D@5y&`3YWTg)ZP+c4uoz0EU>kSNBKTq zqHa}^Nc1@f{QhO+S&%pH-`}6ht+|Wx9e=OiXZ>I06d`r>ns7@b7?1uT<{{Z8A*UAb z4NbFZ5g`w==LZ-pkyGZl@4rI6qjRLx-DmPGQrL)jL}g%ap8+Hjc%P;uGJMyC_6}X~ z*S}1@2Ic!U-{NBMe7kDQeai=09P|${!|+9frj?5?a=8zGm3*}}PU_(|^%hOG8gr2d zq71~+;OkU{hKOVTSIL(vy;)EHnKv1#4RdjtftA*PMDBkGc^g;0$o~`0 zPUN`w9`f(d6s>&L`SqlB5>{b>>#INaA1jdu`-XIU&Jv4(afBiRT(+=eB#W=4u#@em8^FMNsBAL5VUHqC)NYk6XA#*=<@z{jAXC=s# z>%h~0#*eu6Qc2qP`TqTXo3~?UAAlG+r{8 z-yMtL=75b&Ao7m-uTVdv{5yF)|09F~ftz>bX}$Lbkg`i5Abbm`H3Rn(Bx|7>P#F`n ztn<6THJ#tH-Z68O%A^YdQIau7vs-TW`{!~XZR~VkGL*#V+w_$2v~!eg@8(^0)YET^ zF-L-OSO^NmdI-vY0hE!2`)LQe=cIA@gT&FL8=cfS*BG_{W&bi~f_v_MnE%($#^pnu zcTn2XWc~VgLf~NM?p<-j%Wo?%=$nZ#mdeHSX!k83&pHdCOFsp9+Su*feGKUwCRZ2y z%gPGpJQu6$%f-OFz^s=Z%%1Z?zai+G5hIFrE@^8xLNZj~uVwxEnxI&LK4i=>AB+t# zlGPwqtOnK+CsG58z?^ ziejQ5dq@$?o?QN*qow5Gsu>j+FSm>QwY;9!ghWAj4xV}1Cm%VDkbNMI#Q4;pW5&1? zgB+83_FV{AcI|UO{u7BL7IHH=xx=ApL^2c( z^nk1g9juyJ1&#{wFMOt+vPG=IUZ1z%gwNQwfX+A#;+!+a7#@o;JTY=zlDQv1;MRK} zLD~9>RR`RHH-fdr0JJYqw+Iz`GYYfmy2iRKb5ZVu@&w3@bBFT5fiSu--jEjV3Y+JT zfyB7Lbvc|7#{A%J*`zMegN~_~?o)r@9lzQ=SG}iPdnlf4Q)*;2+#Gr})ZXMood?@N$&Bqy=`*8xC>7 zF0f$ANO)t{ObGMSZS9uL)r{89l6DWbV_X8QXnlyS*Qk{{TrmznHzKc#d^(Ne?z-+9 zaI0>Ak&!ujddIH)nr~Bzds`63?*j1sOP44Vj;0IOv@OoTI9*zx0_sYV;OOcm*uS9_ z&MzGSb;A;2VqJyd*5UckSds9dTJO%8$4@+5^IN8Rw0#0}6LR}AZUHm16a1Hc2y*|p z2Bpk9-qmX%@_{M1#{5yfKgNWFWP9M{pdDydaJ(eV_-10!yvbbP5v99erUF~AX~kIFDColy;YcPug7J}?6o)}%smxEqwGhr)?gVG;#CRlw+^%-ao8UEQ#y=#CBVXrHGD9L8iSjXq*T>KJ{I*sC zV~)^?t7li2q=myvYa0wl79lS(W2|Ar!g|B7jBq30oQE;sO9z+0zScst)A_kim0yVR z=iCK`i$RxM0JqF@;G!?!V)Z;nP2wVktwj0AMPZH+H~#%C`9}FKzIJ%m!l|R+($*F@ zv3VM7m|hBb8PNugFUvu`l8fb@*|7kYPRhHFu?GE9<>yar)lcs<%4Xt^gGb0tP^%+% za_v>lgO($`hwq+~&pkiJL;Bb@aQ|KT^v1-RVX3fxMHB2nUaocA5J(J0&NosAxriYh zImo>$nxS@B(iKYwz8!bZlMc7s=2rcr8^-vy*mWCtc&!A5%CCcKt5nCfQ*=?6eB`3- z`GIKr5)-)pUi$P-*o4t}hOG-~V9WBUu&lWR(xUW`k6d9ka)pC2rn(w+U{O}gF|Pg7 z4)^Sqxt=v|10Apzb6{43hffDc3M;ktb8-{6sSG1_2Rj5Qx0K6A37zJ#q5p>#AjoCf_x3b;#Fo{9RLS z5KPVugr@u;7@O(|tz)w=PMiE9&)lNV)8tb!wHd4OaFr>&LZvddP==OkSh3iIYfp%r zO=wD~XBbr|@>y8Aa<#etE`y=p*&7$LkDl2%_2AjPYxnG5abnxX*{`o(JmJF?v#S2t zUY7}*rx(NY5y|(=%*}tpe{p^MnaA0me(ZJb%Mb0(zH{F3#M$kBCqH<5@Qb%Ek3I0> zfi`DP4`YrwCl6s;!$aP`FL!ws(8)7Fj>H5vcXQ-MtdZAXADHm0kq@!%#BvHtqmkL* zSO}i!eO{i3WLywE~gbHi6Kojb)_I6z}5h{q^Cf$=SoO72v?fZ$1dGJ1oQWB}nyHTe|?@ z@{=9--WbPV<9>bLvtDc6Dv$B)t1)Joj6OpE`t%GTB6V{)KgQxYAM|xG|C#*dw_?xQ zH`h+*UVVfRj>qo~vhbUU;QJKCDsWsj_Os-^wZZ-!xc=G1KhkHo;0$cH%b1)k@-_=x|$e^<}i!!$Vy{oZP{QKN34Pw0-A?{W6_WBdf0&=b=iId}IO zcSuYx!gn|s_uc5DBqhu5{*H?4-wwwIbDjUt_=ss4C09gLJ85IadHBrxm=P`oN5@5& z{}wPyM7XT)XZf?gZe#0*-y&%CVm^O0NyvUzj#%szOcAuJ#wDjM7;}py-n)T~KMCHc z>+i9Qb3I=e@oiUr-=q9E&L6Yma)dL9Uen}O z{|@NeKLmA5;Tnbxzi8F(XMeZL=fO1~aDSf_OP{*WauRW0N%Zh*|Bj`WfzD+MxP^_o zOvxODE|m+WYs=d%$Q6TLvUi9#et*~P%UK|HYX`Ttq%x(;(^2zyGO1gTj!|4ODo|4g$1*bs~x=e-2(b8r8o9kc43+&g!Nm>}i&4N`L-a}Ufz^u)a07|74g z#~g^v2Ww`QgP(_b^ppCRdBrCC414}NS;`p@r@jDe@p)isuYv1|d!VVFyUQ(W(J7@< z1M*Fp*Bzalh@hzCj-lB@VSHr}Yprs!W2x38Bk-_D?Fcz2u>p*?}!rAIK`+ zz`V*=x?=a3|DjxQAM{iAU2;kuJ6jvL8Z1nM!5n?au&C6|dDDi!J3P%F#udcCyh)`n zr8*796vo4%=88{x_3ka{mO;6Nxv4UmIy@^@>~)`f@jlDtHgLYnf{#GI{vIe}vufO9 z+g^0AE;fEwW7S`CEifeh@G$gM1_!EOU2sjU+oD#*WXa^? z(B}$9KGPFaE`vwRQej7&WG3iKU&xZ)^>H z^~z1EY3SqBTm!d?S3s?c8l%=E6;Sjr?4SGQ(F>H$?x}~by&OEHz68#$s)a3WV-4-o zstrSv{2&!`DG#q|hPK*_&yd~!4X3RizG9PV5{`ZDd2o-~1X8ggPN51Z6A_%x*ww$2 zvwPab53YMIo>OahetiqAW z07-FyZBOjKXz?ap-W5=3FfTwq`Ckkrlt|^i1tc>R`4sMZ4a}qJV`KE?*Kb;QW?D%M z%&*CWiTPnLKFc454RD2);VCd6+JE+w`j>gd$GAn*Zg$m|ZIdfQjrKE1Y9+{@uE1|F zwg`nzi%AhfbN3#<^!%LVb4S0ocxu@fZ4>ey&K;Wu(}%^tqOqxvJ1BVHlluQ7zn%a1 zT@#EC_Bjg=sWYG4^g40jK+2J`yQ@2Pwg;P;_1&w2CO3pmx;J1TL^@j@eTsF^)w~_w|fkVG#+YgUX#F~0(D#P zXACXzv$6HL$L)(E!qMn|bhmq2t#K`C6!L38AdLK!q7=#2{e3V7F;|c3iJK4! zO`SDES1FLcW)+*jwP43b(%`6InQqWBoBmN?W}=7Qy}iNPC;lHHq5c`axR;gm)`r!t zQw8n^CU6f(%P)iL)HAo$;iFDc0&xm{qj6BFh=q*ABxuI{A}=L$L-+l(+Ref(Z^2H- z;co(i`AgzyouFNK>5fb0tUUx(fwrf|o#LXbJ+&nXFnx47jLDCM3BxmPnf2~%$MwPJ zBURe0rpeldmp)>qVII-SFF{+_v`nd~f6LN55acSqb)|WUH!D;9;rNE>FuyJbHq0FZ zk)fU?-59vqwRZ1G@vM(Q({ky1jpyLe6jAce-lkd@TvQx2eM;5EN#)6Kaqm2sIU)fT zPArDJ!n7(|ih9hMyVUGG&DM8th^TGbJI)fL7~$rr&{TX|9<)uq_mr6G6f3Io!z{WtGkwPt&J9n=gR z04V_q+|N@WG|01}`(Cbg$!y!J4crDQRq_h7|2#!x%0p`#uLn+AclD~ROB?2nN`7^|o*8*0du3ae+njnp)XEfcd(adyU=O)?R3?KmlB#2n)Rn<%P-u>fM0R@okCfSta zb5_4|9#D9wdhg!%``%x^zk)xY|6h~%{T{#{&<7)9p7B7GK#YNyQCbu(UtWFXGoSvO ztMl_$_4|F^{=FFFAJCnXG0*0J3WzCSB1z?eZY*ZD+H$7e5mR4(U5vDQYGi%0JG!#6 zS*EsA-3q)YxcYgEj=4ZPu19^W=dRZ@uwu9$34Td;b9deh{n$?%#C)96E4F2oT~u%+3D4gfWKs#Rc_2?sxRy zAptkJ+wfUqgfYg1jHl zqSMPj@Rz3cE7vYraTsxM#NyJ$wTr-oTb<8c2f&d-2Sy7{_(vR*K9(f-QQES?7?Gx6 zZt#8sC@HsG-#_iy_B%%o9J~fJ=NA{^Te;t%0|&isxBCMoO&=AIYd4$r;JP;=g8?C1 zelB;)bKKKM4jkMBw&oWX!uPwEG0%v4Q2Ae9yx{%dx#jlA##T4C*;E9fA?PVEy1#lA z<($E{6~{&lu1s+(g-RllM3E*5sWM13+JK4_-F8Yn(R6z$VPXg)O_V~C8k7cYGV~5s zlh%UqVubJIh0z9`#*7wHetU7{Eue8L&kpV~03N+><`a*NZT`%CrSknVpobG*kR;n`gSz=?bJ-Sfwf z^nT&9haWsps*IDSk~Gw$u^|Z|jRYz+D5Fu@kZMh83<|h!|0GXb+w#8f?!_ORx1A{O zy1#0SX#-m~yWfGi{nc<|^FM#2TKw3t*-2syajJ-wB2}7Xa1X7wL8pyD1EVFFjAHNN zM*VXQWA;_6)&C7+!uJE65T90y8?S7|agUGfyU4kB=1IMAmRA++ZbFzqC)6YW>x{%e5%0hY{;+o5drD}t?6s)wdYa7EDRQE(0I##Rz^qRY2)z1zI~&FRV6)<6Hu zeTNTJMn*`bfxL_HB*v8(S71DWvL#wdv?bAYc8HuyY@k>x^SOHu3#--ser3$Zf!QMm z4&D@?-5~hU$IJPTJ$QJQNNcoXql7>KO6(kZaE|P?krHhQgl%zjaumPQIT1$D_X{EJ z0SfOo&!DHYN_w0gNfo%!wKC@-tLb;O& zoDdR~CbZU;hwT` zc*f=U|Aq60a73> zkhZ`8mI2!VqcZ^LbmtI4ATf|Aq!8Ga#Ih|gkxRDvZ#cgFbNhu1I~6HJW>cua+Gf`Q zH-5~<41Ul@!L7~Hm}l&l&WoBv5yvUga!8fp#PLau8z@t<}1)$fzc+@pGso^mSdB% zElARw$s~YtquXg??3(9RZ6Qddz^LnEIqwYyvs+-?GRe`UP3xdkJ;EqPS~iXapZL&2 zG@S!{>(p5`mg;JrL!#vRv3#f(_VAP9eEhE+V%*H)}%>H z+-(tc1GI`U#vp{;mIm9SH->!9F^*RhVVq*LLZH#c=-ZvqT~Yy=&XdAqVqJo?GUgix ztbqzh!S*r$q%o*iATXdb!Wb;qX59C&!870su#X%#DCQRzc1_9{Y-wALFd(GBXuWIJ zX6WbcbJxGV&3RU@Zc-7tOBBXP%fWMP%B2FmSYk;?F-g=V3A?0mgwZN<8T-eLNE<^T=LyU9NzxwLXp{!al9JnWFzylnOkV@xD%>?2pzq&G1ASE> ztsQ6Am@JZ|vs_08fV6+8z6e(e%dx5CJaWePsWAl$JZ``%W3-FGm4j&6o_7qZcw=sE z5188&1z{rY+ib+vjOfzs#UxRHjxB6U8l{ae#vrA@b0m%p2uYl3qBun*F`h_?!Vr}} z90s6w7=Jrt43vvGgK!WAVSp%+l*{>nCv(e&;9Uy@$w9PB3{*h>u)*pdwhNY zC5jb7TI6#cp68y2^I~A?-I(qG5VH$w-pATO2ht8rFn?_)moEjek#&73Zchv}F z5a#Sl0kc;$o6IdVCX)nC3Mv&O5$GsE0>lEk*S4cJ6 z4*$-ZuBQ(q2CRqAt=I=t+{L6JX`0CkyoSk#9L3e!&;o7!svXX^K`UzMTzK z%Ih(Gt29tEVaHJefK1Q0MWO`gf!VPqz0tDBLN2D z-^mT6fPo~=3_&Rv&v~FtkxZPq*FObh-gPpYQwWSw^fl1XfCg{98{+?)B1&`Le{n@y zLUl=_9*N3sQYv`Zwj%@vAuXh35owoP-a~0fQ;kvztrR&Ak`ye@L!v-f*6mxMgP9mZ&UdI*YbI4-TN1}{QCLJK&-7I7=9vQ=n6njK@1 z9XCt}&_i|trXS`rnL8+w1l!f59m8n3fTMM#pP(sR7Yq_245GMaOX(+o?b_Q$pcvQ! zyG(*?On3Oq(c1s$3>{Maswj@F(<%kN!z}tl+Y|5!3F)fW-4W>b;nJX@tm5 zkRpel%j4#<(0wKfIsSZi#j$CX7^HI^4!sjA5i&Fu8o$tksnOupF0MzQ|gm$H@4^ zJ-|WW?5$kyF4+LX32w2&=nqq6tJ8fm(a2mmZPwJL9LJ@FDNQwDr6~Ix#xDN zNwtzPW49&ckQZRK)xj`*5CPEYMK8n>l2Wlip*qH!SIXS~=o5VUlMm=mf9zOdl9tI5L$^W3P%Y=NZBHb9*VD2c zTt7#^zHK{oILG9BHI2@DQ@>iukmescrr8BZtDS z#>RC7X_9hft;dVczbHoXJrkwiS`yE(u^b=I_gLJpnXINHsz>#Y#_=cIa3cU*7a|^31wKgA!^sIO&+x!xj=W69wPBn+wR%zz zH=?*Bguue!I5w6LnZVhW0(X}zY1TZ~!gHY4%K)&|?D6Zr`E4=cG;nN(l5gXCF1DA$ zbtM1kw^|&kZZT$GW2&qOKbd<|M_odIrTBQ6~M`Jnz=)VDO+`vdT&d`81Oxd zk&zm4YDA(msnQ?>mhIwrKJ~U?YIC!m%yhdiNUjm2;+6a&T-(&a<-^J({a4oBZO_%lwHCJ7&tdXimOz zmOuYvKcQvy5vi2K_g%EKSnJjJ_~R2q!B+NwZaNl2&UY!7OT?)mRaxmfWYI<2v1xY| z#Zsvt1k<-Nzq=O+WXA~HK6eu-AcliOgSdbvuq=g0yEtW+iLnC5YqA7t3aGIsAS4Wd zO_nIgB!I*c?2-b5acz&~&SrMc>7}`!{`~XqzHYmYNxFowLRvOLK%5$0eCahYov#}y zGh0=ueql$j$-+{LZ$AC3c=OZ(KlYdZnyJh@Av>W)>lDjzC{?N~H(c!MVIDo|5cPUD zRR!7@DkYy>zD%5Gx{)I2B_we~nuHjo=!Gd>F7J7+J#{OyyK7#+UfT7*_W)rR2nN9+ z7i?E!;udb%qdZ>Zfoae2>~xl2v)Y4!LAZVY53CD}-RvpV z3_7T=jY)e%ab|e~xQ@lkZ(U*O>`BHef>tLc3K}#wH`qUBalCw)Q)iaVU;giZFx`Le zrwC){#WAg3ij)?9u}ZNz#>*F7PMo-hu|khDy|Jv)+EAb|90q|LvIlIF6_Tc%EMHJ`IW|*fG#5{5!x;U5 z(BRUkkXuG;Y$0$Q8{4vwi2kvo4n8;ul_;%~Bmo^GNl8)=QufuIKx@O-pLAJO%#)@Gcj)~(F$?%4qJBY}Gd z&5lj5tqKMpu(E_vRxcp1U68tmFcHE|IW#-QJ!8ofnEk*AL#g4e)~!|;)x+1g_^` z^&%``$O@A(Corog%e-te6eYfmxiuVHwT))Nwk(uNNEQ7;%^ko?A^6VA7x>{HIuh3k z%hoHepR<1Sulz03@$M7#g*wZtO@8aOn1_!_7S>0|xfZ@-ktl<4Jp5vnQmxCGs}Uc2 z$Z-K)G8)SC5<2wt;QCICNx`%ug(9$FFgHMfs6m{XMX**W_Ncb!0@SI zeK4e#cY6k6mwRa00?X16x3PU2+mqbCzv`5{bWR&F&9FOYTk|XJj`18D&v(h?d>kRL z`@tb8b|$xXbhqaXm}a%0HWA=B7Aj59$gKWxh%8Ie3}U|gm1pczZ(k9A?9csG)Ab(^ z-CjbXQl#$j+PQ%7e1q{~ge@)do`qv&$t=e&P^^xjtP=08*ZAaE1)1-;&W1oRFbIch2Z&oZc@IR&@q-f_n}`pk znge9s006%4T-b=an^FjT&mmvR;Ru1l00jMXF}njqw@X5NIvy)V3yy8!I5trjVQwNg zr4THwv}C1PN%zmqBsOi@VS>^IOG3RBaq03F_l$31ghk1>@oXy#m_oUoPNSnH`Y1hnZI(Z^-%E zgF>~CTrBtk%d!Z=7$dBk7IONY!%J^ma{tf&>*t;2Q(xuY@ixVrk7c_Ut$Al5BCoHI zcfogTJV$187Xr(1@rxBIBcq&O^?2mO0el)Lo$Y9H9Sh&j3u7SfITWikHhX!3+=B=I z_TT-{|Kr==`d7D{0~pXCb?ibON5Ydw{L;Q! zbYH5(0s3m-(!ydKx%%ZGZDGZVVy#5Zb8rwts(@|JU{{z}u+6}1-`;j4{)SvFc9ezf9a(*2iW_c)o`~v$U>Rzq&}(@8G%)dC$V?mx5;? z@W>U)q$0;+Yn)F%HBH*7V~n7Xb8)-^N@?=GO`%+4bY_nEdX1M?4}br!zo?)4-+$&O z|I49+6A#=9-{39*pp;X^qA$xx2N0Mv-Bp%-BZ;uRFo!s^?HWojy*ifXBBbH))CiC4 zj}E7b{XlIH6^y0*QcE|GVMH$Pkt_I_TQ~p!VwW1Y5r6wzX)NAjD;ns_BSjL+vIwID z@jf+uLI@nk!nQ3e+Y{A-i|b^UZg*2wSL=9c1JAMX9E)swqCuw#Y1|`t+DAA28`ZPQ%=z-%r0 z_;_`sB&3be8XZT#ZaeUX6*)t}nq4Vm3rM<{q=U>CsEkbUiQ{IfQc(9QEoSHkg?h?; zsUh2e?PUR>(nuM%4}cvI48t|qA1%D z^nRw~pmeKTDjHi#vKnShnrgNhUBcFy@%x^DB_&2F;$Da5`U+mM&WDb{3?8i?_Cv2mopH+rny={zZ=ydRj}3xaPw^I~h? zgx|I;i!e(2W!>*h#z5Y8$ma@~4hPWLuHt4Zq*-6bcO;%Ikpfhj&aZiDr+ z-=Sy)JaX7EwpU6l6qTZl<9fu2B2`&gnN}%rFCb_&IJVDXVgBMt;H6vT->#V)J9tnE zVZ2o1>!C^(juMD+jkL8z+^B=q9>_tQxT%mELAVQGpEFdI}$Qnl^DRC>nDHpJ#gyXfc)lSoo*W1k#Mu>)l z1R+c=NzLbCQ ze%5^ecveCu=wQ2fT-PR5DRDO|8=D*-HQLxBQ6XWhiKCESdyB@#D&KR|tCF

S?NdOF#W&#!88Fo(qRCG{zY`Kg3W#dW7c624R%o2}vvrHd!BnffNuTV^GqKFbLdo9^#a$ zaHzUYg{%q*45VqwT)BWT4wh7;sX|)szp=vrrGk%?u1L1`2BcVaO1INR77}5}tb888 zC`FPa6m3Jd+e2g>Hnw{xNLw;CQX$cTI8}6`l&IULy|qrsji|XzzWtq3zXF`Q9ROTs z1AOg;m9PHn^Myb2QZs0@(iBk|r?s*|r_m;s9+8%WsU$HL%Jnd=M|Wv~?&3w#UYLPG z?;L~8pjr=Y4(-2Zc(?GtE*mQ31wh9gbkqjd#}Yo@_lP@v@4o2CC=o}G95`6HaOrY% zv6*})W{vWM%jkhBzVJq(hj1Zza8e^5C8zry>j)d zKlgI($4|DJjn>vRY`=iam#}h0qOC2$wQF=&RtPT6(|YGLy_Kt^-4LCc-HaYIgHSu? zwt+EtpWY1y^bin?0F{!~uOi$WMtU6He@Oh<54WGRE%UfB?4R3rNW8X``wy$_g*aE% zl*SAb_v|C*IOK%DmjX}93;^3M;ZEIx(T21e(c5SdZnlVnE=eaO?Pi{gT=w>7OG$^fwK zN*f9}mrA7~da-6RhzZ&)TI*LS+95T2Gy3lHZ~q(M;#~m%!0)`e^5tKCqx_d%m_J`v zMnRmAcDqO~N8D%IJmsY)3&Rh$>VOYbV&G_*VDzy@xkoZF4iOegw&LbFRnl`9J zk#r-{UX(G>wuoCD#^?6YGkGi>(hJk=(Cp1<;Mzt}!qQg6g_SmOFCa}Kd`Hu2h1iaZ z6oVC>HLx@)#di&zZi?;fxSHA+N=2Vsu4sZVWuuj?{?^tyj~sD$f z0QjBPR=)HLFZ+Mt8?U|840-{k8)6m86b{`3P7Y%PMhUbM*R6Inbm(-aAAsKTTMa%l z{P}@YFxitdDka&v0>+?{keS&B_~8#HPk4_0gfZNgq(c72o5g>z*1gg#jM?Oi0W*)x zQE(jcLXejdUuHnC`-!5PRSQTdaY{MdN*>jVupLQpYK~6gGBq{I6OSFnwX<#Bdwcv0 zlO-kJe(9`iES+S3DKI(;Fxp_7F6;Fke!gH5Wrz|@oT{uPj8>R5q7y1?+u4bqX~Sr( zXgsedRvRIkn|11|%N&{zM4RV3zw_{;Lnps*6F0^IsyvOCw&5ccXR${=* zVR<={c7RURw*KDb_wV`J46o|p$l&nnp@p?8XMh2fAl)1?R|W;s)!4tXu~j_1eFF`dhB4ds$fA&LF^^X+prQmz zN-nQ&^6>Y5I+2r)TCbfCIq|?D9>2GYMa=qERvsqUG3Ev&rJ&IYnV(+}(}h4hbhyZP z%{9yCPm6aK+C1^0$0<%87fTyGR=3&&^)-6UHAcNA7v@)SN)y}R>QreSxqp^NAN>#~ zFSNzt!UFBJ1)e;b@Wo&K>en_lgP#VH+hcu3x3o|B^0nZdmKZzbnQMP&|LADRP7Sh9 z!pap$f&d+-yA3%rWZ3E5=k@|*7Z~=E82}+`zd{;gRVL9Y#&hz_l~>2kT!q!9y;v#N zRyI50!V~+~|KjNQoTF2PT^J#_+9Hl)QZnWz{dTbmgBgkp2q|##K5j9GjuV72T;6Q5 zX}e37iT>;NJym}D-eZ&YTTAx%mF^fv4)5chgJq^i96FsItsued0fLl*PB-D*`86&t ztr2zG;=)n~<5ZZQtWcRb!kY_$2%1~;nrlQsowD5G(sCWYJhA;yw9$WnW)~# zB*CyDHw=Iq{k<0)Lten}cDcS5$X@sLh+Uh;Xv0{wDy9pI`@i+Bzo`_Pn}PFYx!4}P zZz}w-=Z<3eE{HtsD|M98D6(ZT=u$e61W18X^zkZ1R2XAh7R#GW>Yg`0Ix_ZaOPAMv z_2hS7{<$Cc!{fj5Km5==8zU8a_SCXBw$L79a(0#vpBQJl3hkgrGf4WKUk2S$vYkso zH)eG+Kx1LqHe2f;cz0ys2E|p2-`sjm&vlQ*XkSPHx~U>+hSv3-F@z!>9y~j z8>>#Jp2n-y=&ncf+Fg`3Bqr-PHE1J?5ZJ|BCicS^OG=hE8f;qjO072X1&n#twyif; zS6j`W`43-i|FeJkbKm*tpZx5X9zB?BhTZN*ms>^qLcKzDY>E#(G{I;=6ZS%aZoegb zKc*q2$o4Sy^Ab_4ICFMcwl=PcBa?7+wqT~JnDb}Pi&k31^Yhz$wJpi_e(Eu{%!GL3 z^f{bpm6`l%^!2A-_-ESO+9uGu0sw&J)!@t)W6!6(%iq7hRx5bIM&t_Er7CfwjSAD< zZl2m>+zl-LAy42&5+VbL*sU%Al*U9Itnvg#Yh16wJrm7|dZ#_{?iFt7 zVC%~2)$LXGFaPxy+F$$1cRsUm^0z;ANG{6e-^{2Q=E8SbAJL*-kw%NYib*h)6&gJjhU#YonY7qG%R<1y}*+3`yI`?mz z$$R4gs?T_J6A%nX4hOH50+ry5>_hbeO64(*)~-%p+|d5z4QF|??YtVQsPovq&Ciu; z`$>BVUTu=@Mw_tP%i7dSfmJEs)QTj%7|W6@t=DOK{zA1{{W9=dz^g!G@3)E+^M#+X zTb+McP*TpnJ%p0> zJMwP3z4Fx2iHV96YlNS}$`^^79nvVhUIOfu1nH187>XJWK(PxT!ybh(m?U$XtjYu? zO{i5TI9Oeodvg(853JU;hWlN|ZB-ACbRY5E8Y)zHl`%T?tXHo+R>rOsiGvVr442mH zY}(G1YPI@#GUmTd_Dv$&%s+DQpe2<0mt&H@cWAVNuq002V`FKPQ_r7f@#G8a_trQv zYeg$KzH7)2w2?+-f7t*uQfL>{prIaW1eR$WU+`{D$`qQq9S#j zkFQd8oipi9tY8;lhJi5@7s#YU)*VzwT|i)!YM3ZyW@_3xT)lMvsVla9t?n$JTK4}- z-rtzHcSbyv%Z(7$Lkj!$VOR24r98n_3(Ijhb7hS+$Gudm)&46(Vt?<-H%kBWNlW~_ z{k1aM7^Gtp*4yYf>)_pPg}i<83=1!Qn{xXsPd!}2AD!m(T7k7*nd#YCo_c7M>6)Mw z^k@cgmJr*c9)>ARHAm(~IsVWS;^f=c#JID{weu&}PM==!O51QNdy6Zy$TShb++~KpS+_#mbkExgt7> znHjHI2dY<&om#fy&8D+>VcmOrrn>UcnVOjP+%ekAU4-k&080jSrXP_wO0EQ z#ym^L{Q5BS4<9%rjn@C9qSJqLpjJj{jpaFn^)@O>wwWlU#Byw+NV9P63Kw2F$!L6u zCmyVk8=dCTrp=|T0^?KDJo(TBwY(yVVuH?YgK?PBRB_+YX{L`p!W-{g6|?zu-g)(f z`O8ZiKg%5ke}?G;0{~!sD>&8h$DUuhcI8v|jE+=&V<8Gf+)|m|WZ`-10P zJ2YA794@-!q&-Q}>GAb57kI~r#Zs;M%hK?h+-&8i>Ca4B;(ys+D}xZ(%5MbQ%->5w zW?R;~-GuX}E;4`eB?`TH9@>{McW8zS^&)52e2Udc9=vakhmRI9X-K^p5vRL>pp@ps z{RhZR+$+vrSYocQ%1ht+ofkK^I={g6Ep%_2?g{_^>W%hdoU6?*E-(J6LC5r(e;Z?I?O15`@oe>`dNxA#}e zXl;;=MOY6|QT)D*r?M}Iu;_JSmM$!_@Xot<^>aLaSTc2BhRdxY7uFqOtHz@b%<%Bh z5-JJVYGnC-fxsBeV<(OZT|UI6OG}K~S9$q6FTT(YqA%VJzTGtd05qDxxu`gD;oRbt zr|zE^FME*&KZl*q)7$Kl_R^hIzc(+C4A}{T*XfW-7#NIw39u{J)b|WjuY*-6BfK0a z!M^dDeb4yHee>%$3v2GB%XRk~h0)GEJ<-|!8(Y?!cA@-h67wwZ8rO;agC1J_^r$dD zxxZRMleM#tZjt%JG}x`2#wjanP2PFwRou;Yxp$vnY|KZwBfPd~(G@iwI6h?#&z8hw z&8FE5==2hvc>I3Tv8Kezl_f^RGA}=W@|(Rdz2z^5d@s7I003yVf-`YxPd8A)JXu;gn zq;-7s(uw7UsGYmwom*(QUs$zSs7Hk_NO%P}4Q%dhNsre5bjA{Y|KLa!BS6?TVLc!X zvqs$Sx3l1e|5rNeZ0>tpo?l`9X zC`y0(HgA1H`dtA4K&R7rQ;&|GeCyJcPdzd>KIDERoJ6{2QE+-_N4T7y}OqT-QdiBlcdtvhI``q@H=yweO0G%MXC~CEL z-k!hwLk~}l+j%5*wSZSC(%Wohxr2TlcsuE1hIlG6@vxmwFx-#{Vq2IPq-*1iPT-A> zqdP79T#l91clh~}V)^qgl)vIw<|QFm2EuxC_cyFsQv8Dt<=nsX#F06oFb2<|yWS+} zMY|-{jTpXH8oZx$FiK-mg;Vswz(_HViIUPwrx!+Ed*$pi;pXBm{`jAmef!CW$0ye| z!pZqdYqXnn4v#0Ce*MkYf==}Gdq4eKqu*5k00coWuWKWhPG4I7$nlX%v7im2n8Pjl zM9ly_=xja6`1L=x-D!M#yBXI>jNNa^0yFuU8FDlG(7g_}W3jY!nxA_i_`gbb?cQWc1`ai&wt)(dn_0uMJMMh*vEU)!W(EsO_rExPjP2?5F_4 zp2Uhl+BW&=NlFI}5N~bb*cNB!U*kW%(p~!6Ta{mQEc3J!yv7au{Lxy~F52Nw98awW z?%g+uaV)STaW5k2MmuTa?E!LA5ZsncV8EmbohqDS9<4PM-$y2KwI1~z4r9@L=9_OV ze(Sl{zV^|_YQOkD{rD5F)JoQ=FMjd2zgw>d7q|;?fZMfUJ`hb#PW{kqr}GPc?W2$S zHP6KwDG}8JHcnkcg)sxp3o|U%cYX^YvgQxgPP1v-Gx$ZD@}UW8_spS#I;C2Hvx{%> zpU>2nUpQa;vL(%z`zwEI`0MG99z19rn1~;rU-7O=%SfWuzxcz_|B+8U@c>u18nhgn zs|zb^Y&7VliiANL8BAY!-N+q$uVr~7$sh#SzKd5Yq2dHvN>&;zu4!?xIx_PAq>1`Q z94BvG+qibi-vxB5^m`%!2Gr>UZ<~?P*Iu2!{HgoLYUPpuJO_WIMAYb@2b)R-+_ZD1 z5Zgs30MfQ8ADN|mXqI@ZiR0M3c;HdFY9AbLJ-E_Q)0bAgYi`c_{Dq(sMUCc1zURsMbdTnW#dXiMo25dj*=)D&sQ&Lp z^m`5ffKJd}(ADbYcP=e|;&81}C>XFydAw?Ya4X2RP;E;Bu3u)xV0kv>LsL|a&JovJ zxQ@+pr{CdMmpb#;I+ZV5f~SSJKEik8;6eAt{t*A@$38jwlOyBz3-UE;l~In4Y#pd~ z!|8=J?@BJ`|N25VY^Kfj$Bs{piX=*~Dg~5MBw?I^02m(B&9Zs#MK}70Q5v13*f|fa zH6`Ciq-k*@=pJ!=KXPnqqgJW5N2=9iuNQMC^!ow;fKI1#+Ki06`o_hj&ptLYUh-3o z%=`GYQWhsjvoxR>G6u3HR0d&NRF2Fra_<4US2uAji~suK>wICgeK|1IU$O;H3wUjQ zaba_ppTGa0^{1cM{Mor%=( z(5W{YA6Omyz5xKB69kvs(XrF>7nlF!1N+8Z-w5nl5vPzNtYI-l2rHn#pZC6UzS*|I zdh5f-rbdaP1gBag>V~9AdP|P@9oqzb!_dyTXk#dQ9wv@Q*4x2@uJ0$7lEEJ}^P;_#EAuLj+fe7XF23%&u{uHG)bJbp<3+aG>t@~@vbcrOZrT`19AnkNk!7_HE82O%U@ zWgHX7Opi{WJ4>^#Ua>n`%XQcHe(h4LJ(G{(ho&nHoAPXB?wUPPrmzKW&|7-8ce&f2% z`|q4{_gOd`idrmNq$4}Bo7gG3AOQ*#36MGgf+BX{qE3sTe?a@v1wnwc4bZ0oeMo`6 z6zF47v#`C0x+pNK>hEV+kl~YYjojry4dK;A}z<1yHIlj~BUa2-3|IUHu z0lbMZS8sl9o}D?y{^YrjocQV|&OXMJmT;;SbT2JHby_yO$ZTbiPWoWn1@j{?VmMxl z#OlV%gNs{1TQIih1i`nLwp&lm*D7ay#-X^KGazGWGm*$#*O88^ISB~ zJp{i|L9*3F;Jf(dD{tcYZvSGn(fAh*yZ|fqH_OC-cy8YHqB--Aiwne}e>Nk5DRnL6m*syeu*7ytlz-R}F& z%(1tZmR3LivD3%BPzZF_+tB?4QQ+g7ufB=z^#|80Q;ly4;6(s$VS?C?9DMoNx!JFL z?um~;f#5c3XkS=By4?nu$66gPpjn;4&e}@j?JW$9ajv?8zqV*l zGvBPUIF%5U07*B7$#(|>`;r2;s}4x(EKXnzLz5(xt^Qy>tW?$*V;umUTag6!90LGA zuiL$-8`B@Wb7}cgryVzHh5?vT`0mf%$BTpHVs)zVEdlmj!tU7q{M@`Fi2mxChfe(Y z)4%j#A&J0!7hCTxAl_(Y^N!d^U4gAAVlW2KiL@O;)h1Lg#i?o&ufM-KyDt50!La3f z{`c3Clz5VS@ z-#EcN$p8S*?RDQV)6KWfU%B?#xvg&W^5s>0U+K%$M&lctC~w->mDu6N{OvOj9Q(6p zpExI^CV0&nHr~8wiEj*w=u%GjQqzj9JR+pM7|e~Jd4LEWuC8<{uV3%^zVEMq=&~CG z|GCy`e|)Z9ISXX~-+@e|?T-ZRdWMB`opkv=QA=>X0ie4d!|JU$6h0Ao?#%F!%uAc(eJ(Pd{+%FQ5J8$DoXX+o)pWjRg$W zx3eb8QOXoeDCLrL#h?=d*^}^(oknk?hbZ;&ql;IklOR|Zj9t34w76WGZeHtcw}1b^ zW}PcTfa^dFlHo|WJ7j`=)e~rw#R&*gQ3y)XN-Itt^24yj8Qa;9VR%n7008WCch39A zPrT#=;foIN1_AG3jQ9`F%?nQE>%V=X`L*A9;v5pGAgUqOUb}$aHQTC2#mQ%*gjd57 zbS(ju0xAKLj)F`%cy$(`_Mox&8f!q%PUvjOdRh_r1h`VeIM{$D5anh z`+MJRqP}eX-w=5rY%1Y~0gN_iR6C!GXMZ|I-RRCjm8Fm zO#r>|lP?(l`l%@R)8{_^Bzo;08jqjB&eB>o3oropTw6z=$o3LYUQV9%8I+4sQ$QvR zX`e9ImT+n_=yfEfDgj=h`Ofaq?&wMQfa~P24~o+!qW00Nd@B z%C2$^ANL&bwa;k!%I80Jj(f9H*je3xVMI8#UU^fA=uPQr;oq&tc2R+5uX zzKnr%03=TyhY4J`f}^+7cQ3?g&vo5p5M5hZzP#*JYdT=&Gj-Pm2>~sY&1hop zilFlCeo)jxxB`By0)Hxk6L{dh3*kH9j)2hyYLGzq9%@2>cKX$AEzf#>kZ{hs(^HN1 zBuQ}JFu*t-o}2duo%Yv1W$4SF4yzoDp?b23pc24zV{l_23?PWWQUw*K2xc1yXQz?0 z21t7nI;G;&>x==?0U!sN6i5kast}!-MYq#K5PJCW>bfU_;4%X)Z?{|5XJ?PU-EDWC zJ~dUDm8k+;0Xaymq?z!&Z>N%&F+lh(g6S%R=h#_{1aP8k0xJQ^zzsZb!I5_Rs2B#5 zrqf$#`cXd&T8y!40Ct*lLkpS-H-f%@%^`8o0yl zk75XkA=a#im2m4F0e`9jFb0!I zNDy}71c@f;FebCw6av{9$fUHL<;z{TQdZmT&QsIV)31Q&D(pOg;v?=`28hDwKX#(Z zk6t7?MRt=22N7tinx)vV000V)Nkl3eel;l>pC!@hpBT9 zBL4At7^$;Vvwcay07Otk0doqB^-$8FwYq^|?gVtFhq=)AUXSC){7Pkxh-v^5zY_h^ zrPlTzPFKPyo{gK4NeV`jrNAWYSwzYJw;I}KNR7=wj4Okr?^SC*@hkp+Gq7cVdX%y9 zcynpCV@GDhmlzX3_bmf(&es5J`HU^1JnnA77+2hF0%{Eso_WdP?FmKN`*9^me*kH1IZy3YC4LGo6t?QBEtdzRotwk3`1iPzZx zDGfVKOiG3Rb{9qn_@RdxN4Qd|2f^4ZvgH*)5WL(r^r{5e;?B_DwONezxZMj3fN%vE zx8r2HQjz02%S3bm!1A3EA2?(TaQX5QamKfbG;8V96U#0t>_@xCB^xLe@ARRxh9<&n z6hJ4*NdU(HL;!GUY0>Z?{6;sMF~~Rvcka;1VgNV?1E3ATFfIk>Eh1`T|B~;9I8+P( z01jis!R)D526=_Kl>+v}8)P;>rUEn(;$Dn&kRVMprmG>CR80a-0;uOZjKV1T_C`Nm zXPm)kZRZr;DToGEb*!E29zPBv4qLy_(eka#+Yfw*c1Rae{U}cUfAr%#2Cw% zz@7LV_M+?f|I5|JcO%PuC>bCpemUxA@}55$+5q`w)c`@J62uua5qyVpt@RXuD(ucd z0D$lNKiyIKl9U=S1jl!8NV%J4{*ENT!D5`~x!-TEOy}pPKX(N)RUlJo8NwKNjsR(0 z2T%v#=kI^{%JQZX&i{4?F-U0$-v#4)|ID`%8onF6aOfBypTJEe(LZDejGp|GJE%cW zN?Yk*2wX52ts_`Ba74}v&-+1FN;?yXTUBr?Q>EA*e;lcR1A;yE1Y(vMia-!Df@elx z*$XhRGy9COuA{>^$TC0$B?$lkp6|cf)ux;07`S(90oQR>xtkSchn4|~fDceo14EUd zsT3Q?7(^Qbtt~?^#y~_KfDk1c002DKy_f(C1^`Uv4&K7|8+k4rsepqAjr!HY3}D#+ z9G?7wL5yAgSXky-`(#7GULFhrfW_s@F>&#hCR@A5T`{qJcrWYzF57)0IFt;K2ZF|J zfT3g2>0e~{m zON2-1qe9BOBnrQH**iYyc_( zL4|<`?2-b8tOrm?4PyjZ4`4R~2(dPhst*Q`A)6Q~J>zC+VBEsvcY;I50Qt$!iI0*o zSmXncX97oK1bGCpXaXbRAet-zIB)fp?%JnX2Dq)0e53;I4$69eC2X6=9x z`YbJ99vH?iO0B^-19N%tj7cJWKVtsK4Lm^Xvi>nl4CTx)^b&@NBdv8oNQZN)2KM+J z4QGpq(so=t=esI|fV(c?Z;C_70F;M=VYi3o;ofmCK$lfQ_IPeHl;#%iNki_s7hD>ilzJIil8GEa6l-kfoZHH43iT>G=62w1I7p}>mQlG0G#u-0lG%phuIzsa6`iG z5j;9596AOlhk!={Ka^zv0gTrLj1oV49|9O-hJi^SD<&QT=R8)xiZli=G-3mkiN0UL z9!Y=$LgDIB+0dh+55SbJ7|T!pMEWR0+iAAi%noz7`S%-tqoZWd0U~$z zij;35_H%=;YzLGALYeGm%$E72L{NZYwoyZ2FIUiYmsBQl@@XEx=t(cXaQsbLlamVz0wQ)W4r0Ac{?uEmoX2-r?crUDqtJORod8-MN?Z19M3 zQFRw&!J%V-j+DKvojw5A#Z|{@V4edw0_1Q&aKPdAzQZlS1P5~joNPiT7?4T{FdhTw z!S?4*gyXs^sn*1}U3u?DflUo_w&%q;0~ZWVS2Q0 zc{@0C5+JHn{_*AZ&g<`fu!6vGpav<7*0$28)H6^h>MUGnSJi8~(Q9oBTb#E7-+L|V z)$i=}dCMzT)}$Z&!^-t-n?1~90>F(gB0y<4f}`Jyu_Bd;>h;$c@GdOV-SNse;69!b zcDrbI+JpLu<3Cu~-2RPtvvm?mL283Im58OXx2ZxZ6%whCN{v)0q^U&Qi*aSMjTJ&a zYEHlC3g-o6!>Y%(laz7%`0@AF`-5NYb-E8b8ABue?w(yLaqI^o8~UY~h&lke<5Nrb@tF;} v!RL*E7-JGtd|rQHX>tFPr;c!ho8kWef|%xJne5ER00000NkvXXu0mjf;tgTa diff --git a/pe/qt/base/images/dgpe_logo_32.png b/pe/qt/base/images/dgpe_logo_32.png deleted file mode 100755 index 5384a81c130023c54c59af9e3a10893998f0ad57..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5818 zcmV;r7DefaP)4Tx0C=3OmuWQBZ`a57@64uij$=+_$P_Y#Cd!bR5E-MxK}UvzV+fHFQYtEu zIa5MX%8(%nDN`AeC{m#$BBVI?i|+cbXFcm)&wAFn_p8sf*WUZO_VsWbMrIEvv1fdCjF0SSbt?m-M4OG`8OkG3=j2mnBzB9+1LnXH%KZ{E55 zygWUQbRl!B=0E2F001%2-oX(7Q2+p5uiv~E0C?Sgvkd_7hB6sU0OSY&@VXzN(g2W1 z08j|DvDO1XZU6w$>o?Z`0MYF?KLG$@h`SdP02uagGtGq;K2_ zJX?4>_=5zcg}Q|UL@1&~Vrt^)lE~(xQiC$uvV~jN<#iNfwr*7tQ?66hQOi~*Xe4Of z-rl(5?JmCEhFWIYL~YhybzNhdAx@x2MR&YVjOJsWl| zE-Cr^l?&+?(~?tC;x8S)?0>~Ib^q19X{zZ`*Mu{8Gs#)xY@Qt9T#3BR*M;*5`CkkA z3ag89ixY1Im$==uEY&VkEEl=OaqCCL$J;L|8}1a{JyYd#&-}h(HAnTQnkTjSbtfJ; z*Kcp&X_#qjZOVEW(5&CW-!l2=-s9+2qc-lgSMAwPs2$=RBTo~bX+E3iOzG0=TJA3E z@#q!n9etkl!lh5V??eC10sleu!Cx<%hhkqD4s#6myiOjm85J6R`=;RS(J{5LU*qlX zk|&(rOTVB0Q1>x*(tL`4>dkb)C;I2DpBH8tzg(E5ecd%jnVX)k`R9g7;{5W^7gyvkA64FMkGmC(<`Li-4kH6S_gd{7#* zB)B@{QD|q_i(~!aeG%P}t;cJlN~6*j2LS=7v&2=~CH5dC#r7iu~JgmGnC%cehj#swVHX-p{Q*UgJ=!UPrE* ze$Y~Xt--HRr-`TO{lh!WkuCck5g!dc&S-UQlWd!8zxl+sL#1QsY5lX3dr?7CY|zPIUr5 zG5o&dL+D5SN%G|IRNl1LC&f>TpW9}Nza-B_fAyVnnAiCxy?|esSZw%yd5ONPwL)GQ z|8Zya^iPLhifgE~f%UZC`lB$YWegUJ#j)eD1QI)egUHFo#X;mI5xKc3WF87Hg`ZD= zPe@>cu&{{82GLEM#KlD=BqcXXNlQz~$jZuY*&>fskl(shNl95r#YR;{O>NsYbqx&- z&F$MYckbRtQOD>gML|?mj`IdC)vP zJwJMR9ih|d-rnAyjvn>#@%8cZ^PBbeXD|W+0_Fn)gMye$=3;PgNJwaC=yF)tv18%k z5fKqTBO{L=KfV?f6&-!zL`=*{u$d;IR_UyTHNlE9q z&Y!<<;o`;Q`jGBPqVv$C>9va@q?a&zh*C@d^0DwZxTzHy_Z)N9E3)yLYRq?%lh8zq-1n zrna`O?!kll`i6$a#-^r+51X4?S{^-m{J6EXt*yQN$&-$br%#_f>+J06>hA8b=;`V0 zeg6E#i@v`8{sEhTfx*F-FNcO+y&4{V{d#0%boA}pv9a-Y6BF;>fBZN(IX(U9^UTcb zm#<&v=NA^fe_vi+UH$cIeVxSu_&xgo0B{-rIgJ4JYyjpp09Sngq}Kp=^#Cln0Bm*u z$|f98`$PfBuK6=R00nHo3lgvm48a)!AOUiq8hT+GK_eo_cEko@AW29m@)Vgyv7zKp zrYJvD3aSS628}~2qn*&_(2eMMj1X~|i^In5=>6~*GBTlW^Te+l}8vE{m#*zSr!uViM6F@l zJ@p!mCe5eYhjvWwT;DCAC85o?2lmYEozxlA9n+iC|89UW6f#mXHaDT0#+l`r*IV@O z|75vtO*|lKqiSn)FvhOY0qtn&l;qswLUFY@e3kmzeG|>gqtcUgWIw&Yd(DUH+u^Uy zC<+t~N)9H4oD1VTb}?c@WO3Av=>C|XQ-ZN=anT7{XIN*SCFNd-O-{XZ_sWy2gXv=# zZ?lGTdh(j{D+}|BFO{4wjVzC-I9?fjH{ssJ>MONb5AqvIno651AJw%!YJbu3{@H3b zPp{?+m;N(@g+sT8t46BdG>r|uTl^q2sW%bO3UikH9Hmy08oGf z5^MlD*a_x9g%C)B0;q=pm_-PPB(eu_M#7Oaq#AjNte}KZ+9-F_8Psjm5E_Nv`akqf z#&lp&SS@Tgwh1SSv%_7*jo=0GcK8hZ6hWB~M5tm@W6Nd-b`SP0jvXAgIo@%Wb53z> z=DJ1XCfXA-NGhajBo^6?{E*Dz*5=NqkSQ^g1s-pn54=>~aXxpxDSoB^B5+AiMzB%H zPH28ZlCZq+vyDeZh$2;@PMh$X?uxmK^NV*$#7pXL#%ykvIxp=gBO|jU+a{Ov|JHs= zXIyte?~DGr0k5Hok&SVt%KZlz~NLpem5ETbc zGa+9N&T4$+|GGS1`|a97M!({&XV3*h&@1ONb7 zx;fPy{>$d;6a1$p2>>|hOk*1WAOOH)&p-p~KkVmbvHuTy1noQUH->It{D&D#%fGqd zM{Ml=us7Aj@^2j<3$s5=^U&Y-hmZQ3{=Lh?&-QQqppXN9V`x-;(|_vgS^Qz3gE>&a z{r3(D0DvO{WZDA&G<!^cBR&)=63%=8FU5Ht33 z->M*{s;sOI0Pru|AUX|q`+6b(00@#vL_t(og}s(rY#dh^$Ny)}Ty}P6@9T|kYdgNi zbsegt2q~onL7^%^RY9~5eSr#rP`LomPz2(&1q4EZszOvvL9$SBt27C%lae$^ zn^Z{~C$Zz$>)o}zyWZWIo%=b5hnUo@lQgsy|Cbr*(>dSo`=8qf{2wF^eOj)G3yFW6 zJ=^!>-=5j^$RiIJ|BcBv7$Q4+AI}AY&Y6?F zvSxIY7c2JMu_GS`U|kd7(fe}oo!R~;yrw?UY;ay`wf+6etB;>9mtUOdh;@!8a=-P2 zC|G_V%php32K-0M_4?7KXl9;#YX5qcBQs&5|lDL>OzInQg7k6a0-H{j4mzG!Spo}4Ai7&wmuQz!t;pQ|SW&y9|c-wGW~R2m_Qm(GI|6`|Wk;Jb)K4b&Tf zf33BAhe!sL2!W9yNcRyKI!9VnvjC_}Wu1aRKtK>cTp1z&pi-{g?pQ5dRVZXtht+Dr zZq?x1CWPRiR6;tHf*@M=4FLc^Rsk&zBs&RG`2g97oCA=KP%0Ej0>RbfSAQxs=Zk&) z1w>O_pwR^Mcml~p6herN!YFdN971XgPjukwDcI@&0Eq%>1c;>xQe6SMw@EofbA37` zt%yuP3E2WIF=xF}uE6IKET$tGA-HYVPGQ7kFvg)rR78ygh?*SOwqxh$4ILm7KovlX z0(v^c_%K!XjvJGHKpIh1hC&F)M)d+*2=RNx^T{o<=P#i$cM7Y;1&noRV%x-ClJ71+ zLI}f%!L)ra-*Pk~mAWo~1c?Ixp#W6@&EV+k4sqvX{H}9rt%jjT5S8c^fGb9beRzpC ztu};^aPsJ3bSIc_q9Y`tFeC@}Ad`tB8Z}T|E5Wc&eQkRBm4nv>@F_`B0N{Wm0kTG* zX*PC^>YsV5UGlP7ZMh?&ZMZ!e(FFBrgD09z_zV3&qp0bm=^vSSU2MYm-1C$VifrW{_2R%A3b0*~W&{R27_YSnD;wYGe7r8T5bZv%+;<;;t_( z&Q@W*%Yip8*I$LCYCAH~hn4cmNNc4D-y!gAAaH@uXE0#nD|@=Wb&;(cx@A`uow5o9 z43g<2Q?l}q7Ku(Kl1T{8VcH(5l|^`7h`_aAS}s&QNs6B$f@SmP3 zKeuNzHJphUllGd0z!M040v-ZF5HR0Gp*Pe0)@pmcw@a@ZmOe6HZi2;B)CJ~xZr}Zl zvGK7~DxDr)sjg%3;w(%%0CQWY*XkJBF@ehb8&6M9P5)sFz=p)L$I4Tm80i|$Y3a#k z(S*|iJPQaMAPfWu^Dwx5+pd@AO8*?{&gQ6ZWY1X6!h$c|KePXZ1^2|^@5-yi+`wQz z^21}O)*Dz}ECP~(oj40Om^xLO8t?2J&e7ClZO(*M6R;Zuu0`Or zL&PYfgM;~fr|5W)oHdT^VxsN9vi9`OQ@VW0o0Z-Fgl3t(eW@APe_WU zv5UuE_+_cIwss=`01*2xl%|IAox|Pnn>`I8v-k zjdXPk4;J#1$J_ShU%!6gzI*#J_T2-8gUNQYps_Z#Z_kfC_xjrJ5~_Y{;Q6tTZEBPV z0rNw|WSWksK={GeDf^ zT04lzN0-*0%0{)%IkpRdO%K)vgrO>_2qk-|7J1cloZ`&_002AJXdbW}$GJO``Ey4y z-eD0rppK%~7F?{(yI`u(KT~V|DK1OhBxHp!xp&CKAjWzqupv>-3ziz;Td}U zazMTegs;o89Fb+&Di({*jk@A3Itc_Y%^Colxm0}H@9(J*2{{KGRGO>GkZ z0TK_rEd_rV0B|ng3jtRM*das6SOS`=2!SJ#EaBY**Vcyu2!%i(1Uw<&G6v3>5~t8z z!DoeZb5nnCfZi(rp5Sl=hc5&e7gCfm)ercBkOBH;+XUVc& zZfA@z%)}i(776SiJfUV{E5%l;?uKjzMkJIHQ72H9zzNt1voGCx?7nHEW4Ycz61qj6vx>_XEY z3%!3ME{-*o31Z4@L3A$U1;+x8vn^y8(`;E(N{qzZswpO(le6*JOqMk-#Ik@EhB3~S z1k%Z%z^Y$v6YdEC%#)92F5RCTjAe$?I6^lTtwSB3`#_UEM0x#PWw5i`R zLM|{u&beaXb09v!@q;WWJsRJ=UbWd;w8Wpg7&vwW=(K$yF`*;G#g+iecK`zy07)db zG-5I6$q0%&E-nbs?J^;zeWqG#^ z#70*k{&K{h1Grxoox^W%JgjGPfztdu{e8&$Hx7T2yK+%h-dZr|?!-Ba0&T(oh>z(E zI{i%`kOIU{Ry5)_ug|xyXXPLKLg7|$K zq5qBJ|K!w;i?WOV0-RwMmMO?XV^Ee328r|o5&2&dVwiV;=bH|||ptMc;( z^1l=Dhf6jAZGZWWf0_YM(fYCA(Bqi@hS(Lk|_!-xW1}^VswEy40-+57P=~l$P z2n2Q>aLhE6Lt{{vxDD}V|BJ}7M-*f&!v{_#YJa|Wo54Sq&h>g;$p7>6%6EXsE(4KU zg!7mUJUa>8-U%Rx8HbF7#Imf?g_H6iHYV6S{m-Aj^RFj%j?pHhcxzsWba)g*W+h16 zauAq>U^dT#w7^8vi7cBb9=*Z&42Sw$Q@x>a4^aM(jmbh?BMgjzJZB_|+;k48leSWb zxnLS0Oed8QxP4x-`=fiWuzMthBI}D6tM?$>iNtO|3;vAx{1oMS$ z7`?S3Id$fl%0ACQY3HXPzx+w$lT9Gm9|V%N8D!5EP*OHRVfB+>wO4*Di>%0U?)xND z6-2iWHcS7;IZ%C6XUG8q9|4}^1JyHPV$5HoPTc|N-{L|nm;0Mz&1I~?wnQNcN81Rz zsVFOd;i>%coj|PXK|tK3;0YjAyFl#vJorbw3AsHt<6f{#$FnyNZGk!iTZ*&~ToON> zN82dWg>WxWIjhxKKgTuCyIh>jy~nYnG}Slhlqa|l^#I#;g~;k0c+ON5EMIZ5pyp*z zIy?Qw_W&m<2k^JxoG!T zK1J;L&0)ISdmO9Wlo6bKGE%k`sC*1W7ULWok0^4(9293kaLDFw3TONs$SrS!GW9(u z+3+=FOnm%%P9{A>F>}2=1@-B~LTKM6>yotp8Frlo`Hn;`7iV+taSSQ(gr=O#E8h-^ zyaHrl1@ii%5Gq(n=dfLoM6}A0m)zM!S#w`GQFQ;8P_+3Q$XfL-aBlxZk#jF{dPbv6 zsY1S?e1CD?Igg$44ANb^TAopgJwb|n2PkAE%ERR-+gE}jt%kzNoe=7HKk&B9t%T?6 z_d`}$QRcdjzQqDuX5l=EO<9zlH4$V}AH*GQg8jVB)yO;Nu_MputXI!7oKoTmPCZpn z@hBL{-H4mg{%%z+O8gp7Tz`SW;rk$X+cUs>O12Y@D_a80Ur zL&4mm5M25$hME*SVp*wJ$KV`LAE5FkK0erRuBtNf z>dvO*Kyl_RkDe@^b{NdP_JV}xKI!!WkU#xRr8T+v4IsB2077xk`~bxH#~|?FSCGHs zE8w#$m-4xt2IMdQur4#Q0TQAIs{54{P-kJhz%cR-T!(sIVx=M-lm0a)=HvXXk&JYQ zihbjsK2<#T127GI1;lMbC*Bkl=@fJ{FMWZowrE&dFW*8CGX?EDUl zHFMtJMA_+`_tJ+2{r3WsFb?8ks*t`Rb)QmN>lh!fjLJ*uI&iF%pOBdHpG2+o_|@tJ zLRxN-Z|ZZW%2$30_FLWpVeo4pQ(BLH8*v{5awp=R@}5d_vfvnyWuJq*;vbOr@G0=D zeC;&POXZ}~t#_2pdJnATrRZnq4>8dNh#h4O>RvI?0pyYVhB=U}jl9G$Vi5f&=smyk zi}jXND&zN_DMebAt^OEF?mhzKVZ@#TEc)D3J3S@^^m_4eoP)ZjuN--OOTk-Q zAceXQF=ltHF@>u78H@W(2+x1Z390w8c=WaU*-o<-Q zqb*V-rA0CeMm!G=JEi>qE~csc^ry&oCIUH@%v<5{2$DrK9h^OJ&88% zo#fA)t!=6gwla4^~vQNNTH1r_hIjtxd`9NgOf2@vLh>Pyl zAa^j1^1pz8)=`kA;Qpn180Dul?p;V4djN_iqI^vXt3D7(vZVy_=AI$S7^HnijQyZ) zfO|I@^)wdGrW;0o%KtOo2Z^4w6HyM`fqL<_WYjOoa6F4q2a<}Pf=H*`;7YAIAqjlAp8hG7 zX_=G3VCaYV%lx&V+-V!Jq9*M^~bU#I_mMeD^q;q z@l2(%rvvhT1N~L`d1LrT4pwz3y6XdQ+_oR2KcODi=~<9N4};OX4*d;V!EDKUlSq>4 zYY;_yvCDHC>d1({J;hJ?zh3^_u>G4=(WPkF``{S7*T?2kRe{abC4cyeVc;R8NA>Rl^Pl1%W34QqYfZ4bT!X;}! zl2XyRXFDm1cto^$)!8N ziT+oIeI0nS)`KsH`rE9l6k=3;r-Go^s6WkQ9SS;KM@ZC`qD_^DzK@7{{xSw-59*qj zWpe#4$!LqWR?m1YZ{#y)i~4Sdg39|LuY3&z3zmVbbof9PrE|F#$VaVN&hwxpxz8Ds zqci4cbinvxKGHsnKEk9gP`~ef{g9s@Ns^Q((d5pQmE?R`vZ+NB%y}5MlFq+X{}+)+ zOMy4~MA4Lw`h;3AXCV)L+~y14z1F`0&bHW_tI+_|wWvK`N9uonw5+}+4Rvk9d-b&@ zcjbEeT$8I&-^W^g-sE>{e6G>ts_$b>pPz225oxW?q#B@>{-J}qh(^;&b0?fuG@7WE z8aROoO>x-nSR0Lo;QMTX*6vJ)_XOY*11^MYBVjUK^k_ZY0-dz)-AAIF8zI4Q;W5qJ^#DmKF(5o z{@ZH9=?mX)rI|+isf(%wtXX;pDOe&{evdpYjx)Csq>KS3Tj3Vjn6-@;Gj{N-Wu}qT z+XNgdI~X=;Ei1TpVA;+v_6KC?d~OrMx~hv9{faAn^cP0USYWsw=nJ_C7&eRMAC=OM zF)p>3<}Sp=XG0w30B8+ijL%s&UDcpX8rz*69I3vE|IjDiqUz^yF&$L9NEAtLb$r&f zB`z*ojnTA?@}vCA2Fz94)})K8`qS<#wWViH{+u<8$NYgmL3~^#jvt}rF;SR`*rn*KS*Oi|RG`bAg+Ic|;>I52q*aaxv=bq%W>Ptey#cLv8K$ztjAaaC~2AE{;DJNb&?Q*>3tjS>z>7NPi?Yj*fpmXHK2J=-AMN zg?{s&9OH^IDiy6Qw)~bvvF3&f)|{Ym175cPY)Ny$Y|DFA;DuC%4}BS}#$wKopO8rT zKk09H_UZHndn<$9f_<6)@LND|Oj39Beed^$#_SE|EjXS=W6{#RAWMG+Z)g=5jqY){ z$M4h`3eL^Fp8h_x0kSa0jJg}2_Ktx~eWbSd^z;zUrqGPeKKhHT+<7ad&(Dv3b`CwpuKX9DVpU5UW z^e%m|EODd<#vSnnr~f30rhfG8WKZ9_gZ*Cwx%c0|IPMVSuJ{OyX_2|IoVA7JF$Nj+ z^B7Dc57JyLJ(K=#k+09Ox%9|1t{~Yx!8;D6`yPUH??%YXnf0B-DSc@>ch{-+gzx!3 z$l3HQIQmR@OqRW?jMlz&(dgr8+;{4{8~8sVk>;FcoJ9H9`Pas!&++`Sd4l5(dIB_8 z2=hGjE5RFl3<8~3eX=$F5tOH0thGji%i7cIWV)Vq z17f^K#<+uV{9(v>@LymXJo8ao)s(Hyz__nOPF8u+z}na7@Qknb&7Y$=t~cr0IJZmH zT$11B@4x1tf7sg~l|2J8#!X32jL{6<2QvCuWb`j)Z~hjNhcDP^?sVIdP@i3OiZUE| z%KIJCwgH1y3FOY{rSp<#Wy*w0nm?bjW+hVG#Q?#DvhVqxyGh-v<$Wz*h3&vpA3yJOv1Pb#?a&?pMiPJS2*`C&PthuBhqVDK9m;T1TlZaeU7{qgL2Kl z7pSo)jK_`=I4K%S%B5)C^ckPiYhZHkd23R8Pg@$Cw)05p9Q2LSv1g)hcMz zXt`!N^u~go5gh*vnT|f`8Am~!`aY!1J_hOCrXLV=aZ#zhVUH1IJdRJ}FoDML&*f`h z?mRE{o;v>Wzn>vfFusERaOt*pAaKi@;7G6D$QzheDc)f(@aT_i-!31|*WYRUCGVXf zC}(8M5h8tFg;1x5!K9?uq8w?RoH}qH!_?sTG%pm-(t7$|yZPsJj|3MUK0`)hj1PT8 z#{8!tkc)W{#L-U@Ev+5y{;yMiVsty|i@WyYn<8B!q1gw|keYqylidlHIem65E-5_^vmEyR5a|69c!FEOpS=N+-2Fb01VN_#8!g4#6w6?g zw-w0y1vozaUc@-|T8vHmkTCDlm|9rj3iO!bO0AgUNG_Rz>z?1(Kk8p5qIky1O6pWW za!tj!=P=Y~(V=nW7u2TGLGR9#C=9Qq&#`a^P$X(H41 zqU)qqlYkxw^pQX&h?NY z850Cfj$v5m0}Pk`yn#*IBk<;%ame3%S?u{Ooaw_~=K0&upIMFPd@k|;$--~U0ts4- zFQQzgwh!i^zVw?rZ)lL>xpfa?7=wO=&e(q*wJCG;jIAs#9=`>kc?W1$bAo&KmG+Rr$AzCU-X?6QT!3KTWVtQJFRQce_H0aJpwli{Q^VKCs0Ox5@D8i%~$;a*8qP; zVA38@ScrBv=Dd^Y9|S=P>6wh@sm(|I9S!u^b*1#l2#nuN#Je#seLC8-T|SjWDXh~6 z|4sADs7_7$=LOeq>R;d-HmkkYP-jZT2yfQXFD2$4%qdv~_T=(yJZ~KrpFnf<>%UV` zW4xT_OI(vtTCHPOD&LwL!(<5$^@o@JXtfvI#q0Ij^pbgdl83(oDT}^8;X4l5`iPNn!t?zA>+zQTkz=zUCD*4_d|; zzWJ{I2fh{ij$1p8nB%G1@!$A%Rgj3uP~o8OY(JCxFI#+3B=ADoq@qmGRFxq0n3;GOf#F;hx*bz087-$zGPAb$k#Tau&= zxUWJiW6CA6@v{9q#)9rn&%z@=NwYo%3GFWTs;?lqCtioM>q>?hfc*!ruK&VbUeC;fKS{&i0&~UlkP)c;O5_FJ>gas0 zPS;!APgjHAoZr*unfPWMk2;{)wH4AbM!hHS+#fCWinTgz59FP5^cnSQv}o?R67HB0 z82bj;BhP@_wHcCA`o2NmGuevo;)MSAZBBvetCb|n@aEe8+C9O1vC;MZe>#4Ad;sZb9o@7o&kAi_DZ?MJ$oGLIX*0MxE__)j>6~K? z58R@iCW)L z`aJ=Tdj{JMYuc9g6lmSO?t0qR?+uXpy@GcB``})2_?wiPC66dccjEsPaJ|O!#!foD z=UtR4zu4o}s@uoCVF-p?}JD{|Rv4JpX~Dz^ud3Z3@vQ_5G9O?DH`W((D;4 z`pf=cE$-`b%{}s?xDfMY7acg2&`G%T(0I=A8A;X}R_@ zaQD3HHLE#ixSHn^<40TFqQ)OD+87^Tdxvw%+oz3v-vnp+z>P*@_OQgn&bZE*Kx9kV zrP|VGrovvy?vby9+4~gcUsNtpjDecOgs#{=NXM`Iwf2(JpWrU7-Rw>u_pH&FRzhTl zTaXKPF>Lx%7!$Bus{Pp(UEes1JNayj{*Ru;9eEmGK7mr>7~b29ziXrLMx3Idv2_}k NuboDV(okz!|9`pBT*UwY diff --git a/pe/qt/base/images/dgse_logo_128.png b/pe/qt/base/images/dgse_logo_128.png deleted file mode 100644 index f50221b216d9dcae2626bb246dab97898b7b4e75..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14474 zcmV;5ICaN~P)}~vR`4+yP0WbhG0D6xd^F1wh69D$*^Rpi> zL479?O(z+%lLRcu0KNa^B?Ax%xo*4m(-!a=C3551=Cv&V>dhW>et!PjRhs^gFf>IB z765*$_Zt9=^UadyeUp>*n&N7ch<2BjmZG=vWB>mmcLe|^Po8XkWTEpr4^-ukpJ~i7 zTb7V0fg}Rr7*Gl@M9_xvJrT4vh{G7Ku5RI5LD&u})juw|_8)4-p1gkj`X+7<0?}$T z{`qvA{G;>DTE%iLC;*xW${1*qAIA{E5Q@El(S&XkV>?w{UaLMh-Gt zlDly|-b->f0AT<0_f`Mk$ImVN=(!V@kwywxtf4a?ivg7~s07f)0MFvU}GziQR zxPEm7|NhnM*lN^&uk6^rn;P@pB>#W|LX@YpD8mbks1TCEs)?aoI`U4;ta$&j9>ugAdzq297<_OTcT!3Y<7Al zgy;1fAzrgxcRL7z?6BA5y(IFmUvRFK{pfus&%+1?ceaJ@)td;{+sHzLG$N=NNMj(4 z0Hq680D=Y}n-$b9o#D&AY-ygJnVDJI+}zw(8EFvzt&g-CGmdQ| zR0=xD&|BU@rZqA{`RGUCY6vjM-2efA;IL{XXhU#32Q#+Cwxjq#S*@L}AbE8i2o8Hq z-b-?50JxHu+_DE*a*_4|#Otfjkp^W9lmSK|-zR`d`T)pXLK-G;ryH; zr(lV?<2d#~#}v$*nyZ(gv<7!9gg4faMah5zXb==JXfiP91Au`5m@ES^z-d*XlMEHt z#i$CqV!g0!(Yp z7)ghS4?c86#HjM>U@4&)~$%bG%nT<1jjHX^xhY4CXXT=BU7faT{50MPVy=0xsUIlKixei2#D zuOK9H>Q;<=^z~x^odH?_S;oK^z=?oNK?n}c7^EdYiaa=QrBW6S7>#8nW-C&H7y}ss znW3`B6n)?s)9|$;GuHQ)LjynvfH49qMiUnV61mbv0RUqF(tzdWuE6LV92^X!Gz911 z45&Ib7?DeWmkDto0EFO{;2fF(IlJ4dMH)fDo{>NLzQbOC_qZGy006)^8?yo=kzaV3 zD_WXjuL>)W0VZZJARyD=g^PA<3!E7TfGhL>0F%NwXBJrQPR1g|32oBoNCyEd2LZzo zbM&{6!&?BB^DmZ%+hBs_rX7Ij56;30NIQ=T`c}Xs0F(v;1LqvJ5P;Gi0GDy*Ag2m? z@{k~7_m!9r|JF|7NXy|#AOlcD0bl?tRL1wAZ!{7fk_J=?=*+;x8LXBI z0l;A-$(RZ;R`NV=Pcq1LU29hIio+R<;4qmUCVmdoDI0^s`!bZnlR#du@&4%Nx!E^t z+&c6L8ZeC2wE~PLP#VLso3Oz!=Q!f3R&^%rD(8IP)U0I*5?~OV1KOZC{4J(7$Cb|x zczhJ)?j(XhV{QP28z2mU01P97e)7)B7<5KpCV`X^LNH7@7IZhN!Ddy)n7i*W4S{n8 z27n8`k3tWqK$+Md{LZ5zcP9_fH~*ul)#A$X{zI0Bh5f_~8Nj%J&In)$ghh~+g}IUk z)(NVNu__2ICX72{j5K8821N9UCFnSy8?j0M9hcD|F)%erRK`RuAWhSxV|3d9bIcH=B?spJAamZIGI(G1 z%3TQqG3f&On&?l`6=3K;KgtP611eMCEP>N>FyE}A#!v|iS{7q9{@*Zf-0R*xK4knB*Y?z&Xpef@g5X_EqlW;D6s34CU|u=*t8N93+PN zKqwMKTn?v-g#}25!LkWVM-VolT(fYz<_e=!6O!#$7@|Nd6^wxwDdaxj`XvCjjtKUg z_K}vm0{}+D`io-6`H&R$!^1IaFj6ldZ35FVgwU`&;Nol zI7Bh>^KG084)*U4`+emuSpYU1d3+=i7@a2&;}(DyZa@}pfKdb?G?=Av->C*#mYOme zvqGsVCI)Jtbtl#uk|7TtIfp=D5ioQ?lc%`P!y}K2LjxfHg1whMnK+}Q{qCR@A%o5k)B!ev_1Osf_f&dC0FboIz$SEB(S9H|f!Jz?QfTAE{ zC~VXM6#FrEAOOZGCx#<=0cJu-8(INfzS>PPUoZwi1jludhlMCCL16{nSNd(`E|I`}DE7paADb{5jSNP^ zLt#Kk7gpH?w}6Yso4jhNd1J(Uxm>RFzmIt zSa5_sXBb;(wOZcbupYj(6ZcSZ9F!^^T*rpR0Eq$wBN0LW;Cpj~@5|vyAaNSdzyb)j zoq8Ry1{ebY1A_u3ZHSTw&J3=caNW6jdPZxp;CWtca9k;#?wKwuDd4ycys`@m3>IUM zV1)(9vx8$?fKfpR^JnOY&l4XS0JV~?67o7QqxIODywTM;qcZA~_f!;f1B%)Zjtww_ z(=&BEa3VgP8Jx6WO`~uFMC|z;)9o^oLV}^(sKDYpw*rL)$jRWrW1nHrB#1LcQd`!c z`z5^B^Au60=Qd`3NpG(&Bs3D=Uj72vrg7=QqMK2V^os7)gPQV5VzRRV8u z&1)D5H;;lG3ILxtpZvmw#S?RsDUczkpqE$tVp#XL@ABLU8Mp%mFu?$mLHnEFt_NAJ z;h}R=_J>Y~4@AaXsM)nv@v&q(#g`JV2g_urG%F~TUD%AlW`!&0bAjR*{n1Z`V3Yw9 z44x(78Qn;-?25G6h2zJM&*E-XO1%eU@`rNZv<;|Rup~*xk7wnNU+Vp%|LjA}|F%_{ z$$R}M1rta4+Pl%Hu&?+&`MX64_h1$g1d2O=PGEF`sp&<0;=aw(UwwsLfWbU~6#(g) zFTNaD{{J{to8tgSwN*#26TsF4s{lb-fIt;Yg|Xc4t0;k8FGB?}PBg3h#oe8cn?|GU z*>-kraq&3{?9H^CrnkF*Ns@@7lSzj6V1FSA0N?x2vHLE}rT=-5NXzJ`L?CI5f-%VL zav?+@$D-=xIsK9EyK>^gr)N(?Co$~ld4$(rhRGBdV|o2QN3lEz`Lew{Wmu5K{4pB= z#?Uzzt2eV4l=J{$!E$T(_H*U(rN!uh=iVrtJbwK6tINyF+pAsujomc3{(*UUCfiKV zI5vfyr42;JNAc965YN;y9TRsbM{bZv0ghLdDCUn@73c6WbfSICb@%YK?zw8=tB z`=?|uzSe;QEF(%Qp7*)=`T1AZ*4B3MUIjpq>h#jF?1w)xt-rg85)1*NIt{Ba4WkrD z=e-n(08Ivtbm5hoFxo)WT8LI|AnWXoDSWUMmxM+D!A8o%_ktn!)I=IO*oHlK9y;t{ zVc`sZ8+1Q>z=VZ#`(TJ13= z#1kC23E&o1^WGH!0IM6p?ze7MKUuEh(y@}duW8lMxcp%t%z!9`TWP^|Yq0GyT(<`4 zR>5o+Mk~b2*O6>*f|MS!>})?1zt7{b3}Nv?KaET}gQ!da04%$LS#QVv+UqJ_+jVYC zm+Xyp5CoN4O@GI^>}ML)85l6=RKj1|EqVn38iZ`nENTRs1duV%X$HnPglB^Z4o3=j zmIc?g;Mfv2;}A-tQZK^>15$-`GBC}?(~WB7{{hhpG7MLCoG8|IniUlP8ZIuc{z{tTeIv#wM~%K{G&6 z-Ze}iZ%~y7C?jWL2s%?RX$CULT^RB}h6@3!T0#~kV2mS)6I_pD=tlj8YPtHEOwpI& z^OxUful4r+^7n>}#l4+QCtmhv9<5aU?^v*+)2;clNV**`X(3#?foOFJ>DD@u-5sdV z2W7fw&r&2+`d`#9zdmRVmW^A0{eY2wlmMe3%1tl;LVB3;Hmi@nrozphwcKjeH(z^g zb@kF|``ncU>m#;R2Qh(gD?`){idJzzRscXF?-FfH{xW?2^tJZ2F82C@KQCm$t&cx3@ABaJN%7OAH^HmHGmVnc!Ku8HvxrKXWa|cPa_Ee)<`Tb1MWAOQY%15hK z`)9J-`8QD(|DbeTVM`g5tJO}o+f8tfT7v^qpSwX2tVJ`ADz3h)XNaI1eAs`2Cr`J zBCA$kY*cHXRw{dp1>&Xs%zmrU__7N%k@HAiuH1}>ffs%xMl9U z*Xuzqv3sWm^>w)rtqHl)V{-XPr`I z$KtG0t=D^_E8ktY?c_(V*Nb*B^SDy0-#u-aW3AdWvM_`_Jp&!5&`~_F1byY2 z5tl%tz%aw-#&QJ{7NUQN#;z0rlqHb$S&-Hcb`2-%k=yn=^>1FcuTNF#og3HFubx~8 zAG&mU=DdjsR;>aZOZb~zD5IeX!sHj{BPVi`mKwzQ;uK^S{sld)G8iWrpjAA|GeduA_3}_t5v_(>)lhF;7!eb z?e2DWwtCG+w$%2en(19SHno5(O5hz^gibPKehibDy!A`J2hfPJ`yD~{{7*&O7#jW$ z+6xjYCU8hAkV+t1i!f0S<@y57G`D7-U&`E-9k~<2TDiI@zVh&irT^mCv6Eir2PnCv#H+G<^wQp9dm4B~5UqF{XwY0Ugr+s~`T>aNA zt$yXgY)g_h;I;)2K{z(FHmKSbq}H|dcKboARAv^pI+a>w_g-=bZ+iN(9R$H{HvMQ% zb?&RE?xo{X$B=Y-@D`3k$0;&Df>8!!*dAr)y@6=>8jS$t01`Rc0hUJMHqsdYf^5t~ z1syb+Ggv5JJ^ADfI|(DJy}E-J+;U2nr*}SHpE`}C-GNn|L9i7djgvuMAOiqPz;0H+ zZ3)UWj3K{mU~mP3TNPmR+R15NR)j&J?V& z3u6dX*MT(J+30jHTO}uPgjly^r9jbV@R zu=DJGK7Xpw_%EF}{_P91Q*x;M2+EIO41u%;6(G==Lbc>V8f|a%f^$x}ylr#duGH)P zy}$|HasfoI*9+6E@rCVp>$|3eIWgCmLfQ-AOiv?=5@bOFqiEO-Oy}l(&>V84P24Cz z`t4-Rhzl^27GmHcG)x>qlpA2ug0QMsY#O)Wte<)2rW^W^y#B_D{ExF!TOT^v;wQN& zfwX{KZz1+0XaLT_6kuCOgBXSZFK+B0sZ^e?RjQ8=nJ=bBJ$++ybC20?)muOBB;jvg zm}$Bog7h4uew4dGGkhy)1JVXo)yqNPxe!Ksn|^r4a!Zi_^$Je#u3CaSs(@(+K@i#X zKfSuM{Q)QLT{tthfHX)^T3kdHWk|abR7$y$RnZJ;kzA9u^M<)tf6Ty;C`gkO8=n*GUUb>;qZ$GkHZuOit^P(8H>ccy_P zPLRbZl+pOs(gu=R{j1eV^^+RrQ3E`^w6ruy_;|fs{k5ja{^f<)DG4HQ$41%>kOlFe zVfCnx(x6PkDtj=-pkiB)O4Zlfod>N_87`MSyX1C!-+$NU5$?DIs@)F#n71BX4ZByY zpnKu;^em#bkMhamP(cm?ozZaQtNeZe{l`X6Y-(r;2Fg|kxCqKQ1(brx5?IrVFmV9s zHgI;of6jXB#p~xuc=%}+FKQ7%{K0kYBE>fp04yoQyM_j0pVeT0mDRCDx|Kc>=Vv-#6r8vo|SnU>Y} z`I#R>1-DjyUr1|^(vYQs6O>&@t(}cd_mWdCx4Gb3wOZ|6;RJ^+g7keqGZDOIb%J3V)BSE^uxAP7*Ie)L-_8y}!fc>eOSImCW~%BclpeulUmLnSo0VMM=W z{NVpdo{$ayr%~52N+QQB1i{1+Oyt9=&%me*UTGRv7Olpq=E?`3UvsiomaWHM-LgMh zp6y+}p~J;5`{h5aluMr>t-q*RSEU#o3n40SL!N+6|HU(Y`+a zb`>F{G>kH^%kF>^2%~Fjo!$eM=R$Jcn{wUWZV-fbLoaVe?phgiuh;8Z)yAJayV-t# zcKq{~=VuYMLsZWlM;4?=dJ(juappg15eg;izRB+^e3TRxIfOyJpg2Am0%ZwI8o{19 z4i)=w%QHCLB)3sozx3>qovpX67neHrV{Iv3>V@{_In1+~v6pV%yxGBo>222QztAG} zKVO`i;zk(=*D8E|GN$}nDSf}!NEsNVhj{|WhSa*W(eB=7dF4#l)@GeGciVCM9dQSD zwGKWAf}m7yJofB%`+TbcR&f}E5RXQ>6f8e!ssaD(U`OS|0f+4dMXW8oN>S}2>K)xhj4KYr87zt!ucbpZkoT5Hri7h=E(N`SDxzEnoO3u+voJ{r zZad)qE=peDzP!oO8|(ISB~mwTuCH%~VYuf4H=E6$ZY1%)eRy%+GfF{}J*0k~;WL`{ zRqCA-1Cll%rC?QD=rrd9Qfp_+4^F#fuWd2beJ7GdZ_7%UAP9o`{M@6D-(3GT9)#!a zTbx7G@lic950ffnVUpJq^c6mG1&8xN#eM!pfG{X9?f)4?0TCEu5MBx8)8}DSnzxWk zyLkHM>*iPg-_q9~f7$(0!SGBkOI8Zyk4dZ1_@ygE|LYH2Ip;6}nCl?k=_2WdAiW=G zhtgl#F!|hm?;HHqB08`tMV`R1V1ue_?d}7X=aOWsSGSu%J8Xw|n?UfkErsfKyFIT_ zd-U1u_P6n_fA0R%$C3CUs%ICVqBKvQ^lvNPU#wGHox$T`X!o;&7|}sLGn`Ku6F6fK zu8Z=?(-4jgN@56R@bcAf;n%<9J^77SN}qOZ^A!d2(%Rad8b0e-v-vN+J=4GVgO^To z;X2TqA@2By+kSCh!W@)fIbh$+sDCr>$t^)YSwtvX7Npjt^=|jFRVt;z#%`rr-F>^F zgST}pTo{IarBQ$ErFQp0-u2Jjw=j#S3};A!1W6bVZaPiH!5Iz)!5z5+V~V+h+!9nh2SRCgv+G~7OI~aXu~uiz-rGS8 z-}c3jVHoiX&I zjwgecL5) z+lCGT5NUk&o8O?{f8PGaOUva?Nv+5?$y?^>stzZ6o z-<$l#y6;Ti-1c5gEcwST1if0k)xL7&_$<;Ofj8HH)&^;mz>wJ&1aH;%A0++gz%DpJ z)v-|$%vh$er_oyt459J9~8at?3GgsNWuiQ z({p(#c90A?0Wc1P3762%0?Lw$)(6f&dNy<~1n0n?KKV`j;T!hXS3B+_j-)SZ#-7_( z`9E3z7eDn8^NXiT<>unF{~b>)NxtjLH=K(1=@)jpUMWr=y8q-n{M{a$**a9Jkc3Ij zN7&FE(B%2;bINZ;2E?%J!DxafCAe0l&5nPcT`DKslAThy-0AgtcVz@*=n5Le|~Wm{*DjXtRjgLs7wuRML+n`TL%bx&;NkOgRkpzg2EEiTnAib z?nc+YE%C-;Lwju3)~Eyel0sy4`NCK0WpM=hwF% zaFXo&+1VD7UWn4d)F4kl{dO~>7nU*DwGw7NcpkFsLU#g$afaXiizn%EEuYpX|B>YM zsOj>jZfxwG*!epjum9tp`=0V|+;{qcG7N#)E|~3t$iTMBI5l0jX6>cRuWyS=r)Ry! zT^B+Dt-9Q?ruo@MpB!fz`L3D7d4kULhG@dc+a{`CK-Rk-mq*v0i$(pSkbk9Fksy>e+edC`A^Ird@NwCrb`yK6nno zv7tHvIA{3Xzj_K!aO>-yQ~orE`GNwTxxRjVPbJUa{_ff@{nW$tUwQESLzYn*qS`>V zy^5^84q2*!IEO79oSJRQDz0C8X3fr$*nTbJ`ctoW!-`DO51*TDAq-;Jtt!$WMwY4m z+2K~|J`4~>If2#&725{WS#`bByJC5z)Rv;_dfpuumAtDxal$b4r{?D#dv0s{A=!;i zUtXNcIl-BE=rBPR#egw|&v($ee6kq%K7N2*GqhBclr49tIoC~?JW}c z^itQquxQE4t!f!jl0sBm#Jvc5WMS;+ZE+5gJIfOkl|@w#TIH62YvpeA{ELoPj%{IY zwghY6i92}Ln<90)-Ck>M?y+Y#wjc76^z@~L8HC-S06~h>k03o8t;;9j%{F1WAvovw z-LE`}uK{mVtJOayqYw1?m(Nc9*pFST{N^X_e@JFw0DEROU*Nj20@4Z~$}eyn!fG9Z zP9nHo9SspxUfXQXto7Utm)XnktiO1DW&MZG)~hu~2xI^>1F|U1=QaTBStN@?x&aK= z#f%a|tu^YN3$8M6qvv0AN=}D6&U#ab;H`q-T?YV=gP=Cm`ur>H?xL5b51gH8A?`&e zAD@C#E2DOL9>TR?dLcS~gn#?Br}31OPgN?lKQfB`j0e4^u5WDYspL6-qVeDV@afWT z{?G?LPy!LGdL7Zq3Zj)2kjnCc60oAiFolUjSj~B;Bu1lDM^$alzPg>(JF(TVNIe%j z&X=yQZv4QhMzv-!4)Sa$rJ&MmIEnQZfBWq?!${66=LDICRdNPX$u1*rs~239UMYp` z`EKd0ae{jS06;#0)SR39;y0H!KI%mA>HAL3Bi!l2X;z@36qG110KfCqr|5ah`9{4` z{$tYW3x?yFYwPQKeE!08?Vtaxh3fzEcR%v54P#)p8t7fUfpB#VMk(}Hh7K!@2|9^k zk`z{J0VY*gs83^Oq{$3Ut+c9Yg-#zfBV98QzmH!S@n>H35?Ri+yL7* zdVR~X-&Yu&7fj@qAm;?qnDSbud&McasUu~#;W+-AO%~lV008o2QEP7Y3(sxse%R{x zr#^6Y5ow&lwk=Sa;s5^Yr|_(0JzcMq|JW$?mDHFQ3bQ{^z*+kze{-hxOF#3S45H+Ye)9`_x&_MQ?2f|KaJE@pZwkRVuYVB(1)X8uP;P^4=xd z^?K{a|3}Aq!*J%{e4~j7Ft} z_R^;J)ONzTv|n>PZxf7tc_)ZJcD7kPiA;fc4s@oVl59LfSjb^AH3zwffjg*rFeWc5 z;aQ&(cpDsq}@b3=iyh?q7Ib z=@A(9IDG%vtGl~Dn#1D)5`a^(+-4MwVr^sqLu zU!SNIpc6vUOTe5elyP8X7J}Wx`NmF&wrlmUXJ6^}~&l2UX-MvM9MD zGI*!buQnd#1TKTS+Ub74DY>yN#4S0&JqrK;h@vQIPEGw87vf1t=6VKPFWCNs@*Dr; zd#m0r|NZa%Fp^FHcfN_87p}vCcX@}oxo zZ$JNuk0RXaz?*M@afU32VAsp=rm7IGTNIn->-7xfKp2?)zVg$2Qwc*F5u#lM5@i_6 zg$z^N+}v@NjEP*wzFw|W)>cDiEu1ttVI7qBZHl3s*HtqkJ_rL8!+=y>H&NU_d2?@n@pdmaFV^s4~Iu4!wg^|KEo z*>C>D{G2=Wz*+bk?Yz5}C1I6a*tIh3S{ZJ$ipt^)tV#)4m_fxFrdYP76<{(zCxA+T zEC$jDNCJkaONavnZ*dxFrr?0^&GoI4EP2a<=;m&>yEZ*D{rq;j_k*(~XI@GHI5JNT zX>*_jxRsy3!7U413V<=_G|S0h*@cs7urMj@rpYDS@xrp} z?0V&LG|CCyD*%`jKHF;jv+v0Cum4!3=5n1c$!?sOf#e)4Rp8oy7cnz825At(ovx#D zVis{bLeh=%5uX7n&9^Kb*%BaA25FLk*F0#?h9?DHU)m^dOVM?>b)8%C`r6uB$F0}4 zlAiw))2_=%2pAAloZa%i2?u434*~#VfRF-Cy#iUXVN!*`?Qr_bs11P;V7WGQlpynC zR2YY=l(&_}-zMEs&lY^ATCaCIolXM0N4sLrwpu@G#qrMsj=S>7B({w)g1+@4i*MM{aRGCejR>qKOX>=cM>rS&`P1Rx&vo% z8p;sPHLG~N8=j@AJmEM_9YC*CDt&HaD|+IjQXjQIuu3kHZUk-AfxqD(Z1gMMCSPZq z!K!$Wo&_DI;DTez53!r1Ah1Aip7lRQL`8ewd@7Yo0VpMU-S+o18;!ptA`d_pfPPN` zz}1y6W_9Opyu$ec5ufSbU|OU2J1iL{M$&$EW$Pnfy0-FD|KPhHoIcAql;9|zm_hrM zr946ysXppg`xq4MWXK@c?SaiT5G5H-Ox0m;?jF;X>Is{%7J#*-rKPAFf&G*^`F#KJ9_1rnmS&Y*s33u6z}59_M3%i&s@A_Ir1d07FB9>UF_zCs zP1yE&{oI1)MXuQ?>k_$j0F6ZkGbbd!bRS{fQy{6-D$CfL-7BHRt@6 zhp}y2udJ+O$Xg}Rd(r|FqE=Q`4z{s-WFJArJKlsiE9XJ+5)dTgyVJ{-& zATU7DQ!w9BWe~P~u$ekcn&Eh*#8%_@v{$JeE2CW5#HPR0UD}zd)c&j!Cl|_6Lb!I` zbu3CSI?no|d~y#QU?m7(Qh-|$CP|SQgP6;$dbReKM(MAl&|h0xTH1N*moJyQblbT1 zhA^g|*o?EQah!q-LD*JN-$w(JZ-7A)Nkc`4Q6faWFmEcMG^Q#ZkfzgMnB#U4A&}A# zwl#j2iGAeuZh?xMcI1_(nISOFBLE$p2_JpxbBA)I004k2aALf_?obaZeDg#@VFe0H zpbMaA5VC$*IA^e=KvQyuG|dvwnx3Av0bqG~c{>x$*+^aHc>! z$wa1b=hOng(F4F>O&JmLuAa!ly?zr(!-|nV!T~e^N^3;D2qZWR5o(TY8LeiR5L2VK zx3|jPf7tXx%{b5RsN&k=hrY#Wy``VQ3^BIfZ8_eW96bQ=A=w-33wO}>`;#1?PYTm0 zhT#la6Uw%P(YgtsUgF#vIl@z2ZC=i_&ci`lj@%4uuZ)x>yeo3_0DyiS0LGL)hyZBB z9UyNp-?s>9oEG7sfo(|`rE1KeMu3OnT5fJ``r5L-(n}I>!NDyF#&b8YUyT25DZVd9 z4giDZll_rD00I@04Wpx*{P-8=mxd#s1})|LH<#$&cuE6MnTAR=3kf@Y|WKJE8y>DGI@Gnz0XpF)lzO;4lVRmcf7(pEouc zT_(Vb<9Gmojg5_U#rZRFra&t1BF^?(_54=O_t3w3e>s`}=(~Uc6&!H^qaeU|c&H6Q zD-GiL?PzUdYy&ER$g8NzSiDDEel6B{^J6YJnBaFjlHbI7lRK8934nc)KYdUPKu{!t z#=t-Xm8oLN4Un91GVlbGPFhOG((1M?p9r)z{R(7$7xDpbha5Qo49q_UQ|QBLlYYpD zg8h$>KvVp$GYw)4Chrj|4CxX9o}Jla0AQB&t(a&P3_y`~-cR1`*|;MI0AMusr$L9Z z;pm^p3ye4lLjA=R%hwW}FKJ_(=d*h6_K=^DBMAW0CxOHUN*x{L z!%Sd*MsTQn({~X?t+_VZV$4`t>pd*bFj|RHH9!TE{adT=l{dX0orB#G{s#mm=tf?v`;2~-cJPG zMd=52!}x#1i24L@Oi%50yQ3I;Lls}j1s}ZQc0T{@ zO#hC`kpsYBw2%1mand(Z7g!{M`UxT$`8*AYz%37_f#p;ewX2L3*hgxF|s#xll8bH(?)e;8+CE$w$D$)9*r=lOmV@OBBA zK|F70eB}3Ye1j?A5ugk<&V~Ro1aM}EnBrXRTb;`t>uO>Q`uDV9;@-OC_tx3GLo%-P z{Zqc9iJ;N>fMMJKY|rXt05HaEC0R}a`W6KH)N;OClR`%h0E1*te+qac z{X023IB*61+yMrWW3FiiaLzR|&<9oMT9&n)0y}-TF*+acZX|vt<;Vd5z|hQ(hJs@* zU_$95Ti#u+crh2v7-^AZ`?>%tw7MXKC;-RVU{UvhN@v(7rhJ>^XaazS*}g&SkI~qF z#Qaap7WUJ+`4<5UTpPx8a)1lC(YX<3M(^r8VeqC%;MnorW;t>I7)$72v<7gr{%AhLagcjO@=ui8`^KSQuzH;@ZAzqD4)!rFTkF-e~+o`z_! z4q$AuXh0V8&od$Nj-a_l4C5jNr!}*XBt{-Xl-Enn=yFDLKjVy@3r2u6L zYux~LsqzXl>~(0pv9hvqpfaQ@|Dp|ib+_ZgvL$qs=6#C?{-)pMYiNaF@}7n|Nx=YE zTwrs%i^#FNwr#&)lv-ho9r`x&_hzyB+bCfeuAy3uR+iU)&{GS{9-n@BpPuJA9D(`k#`nx68{QUfnh$Q)h zr}PIMDVl~zYV`Fm!v04i1VfZ@whhO-;koYDImcfIN%HF2+S(4@M5m60g@uQ*F#I{8 z^uq#F2V;`4+mr|qL2E16Fnu}F04PKtZ#nm-+#%i_@% diff --git a/pe/qt/base/images/dgse_logo_32.png b/pe/qt/base/images/dgse_logo_32.png deleted file mode 100755 index 29d53d2d12fdd9932032c2e60195edd76d7b5f63..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1828 zcmV+<2iy3GP)c$e2xSyP&*6Mde7Eb{o!Olk-rYNg1oB}g zMT|VR>zUo1XP$ZHeV-x582B;^kA9?(Ffy_qX2FCSKxqwxfEBe+LIR7DxKCk&0mJdX z=+7`h!l-3FMJWZvVlOQ2UyNVCL606e3b);I3kWGe1BI)4$j#roy8G$IB+E8^1#eGH zzqj8D?nNN=0@06jbNc;;2&fl^;WXth7|VVR=E0=nIIwZkCV1r0EiDG3!Z-(xv-WX5 zYgTlw|7O2+!=cI))Tb@T6R7}s=hyZy4Ak_)b)o-9T50K;5mC=kX zMm0TS2A5mT2_VupLvHLeKu6v{X-NnJ-S$_d(!}P>OECHc1Bg1XhuZ+j4gw4EKCCJy zR)$JvDPvO|mId_x4FK6iQK6JobTvW5LJT5FvHGy)8hdGHnp_<)dZ;IrJS`r44c{U` zi#c*>0HRpXmDWqT(q^F8MV77TO|6!G5UY#e6NUz`yrjwMa%%k)5&vA8?|=-Dwu*JX z(duhP<24MIoln>o1Q6gHmiL6WZQIa0vg59S{Wo;Tb@kc|gcA380Ie7!8}O60U3VTA z?vcgitY5NHK#;Cv(=U}me@~C}J~Bc)tT8g0`2vW-?B7T2S6=+v$zR!lvb{c2pDLAKGSaT)oyXK0-x`Vv=+Sjk`g)D|1jZ9IcS)K7WOeQnPeeYo={NI|GyEy`g zmM)3Jy|K@GYRj(bkj(ahtlMyETw{f}HmU^b$0uR)w+nZ_Q=J~otxS)u9wR2^}Al>%$@y4DvaE_?%$U7~lV0 z@rPZf3U~JveG{d0!*Bjo)m)ka7GMoAD&(oaTZDZ!di3plZIF>dKu=Tl~}X6oW8r z5b~T%Tr_AzAVxhoyrVK{b0F96^W>d37jm0_ykOyzN8cKQS|NbG<;Cu`%errU{oTpo z6}ik7A9d2^;q-Da$Adx#3{GO2!M=^yEHBEyY{66vqf$L_K!iV7qSZa?dO9DU6hu3s z3&!5#@b>E`VYz4Dq&XX#qI9oqTX3dBiakKm#)wKG7Y6rguesF_!eKsPljBDi!ge^{ z{rpE0*Ge$<50@6VmBZBPYSlrFH=vlZ*G|kdU+7QpHE1H*$-axb}dO9nvNX*>cTFv$6_N!5{?%Z7yh*HC(Sj%Y3jA`>Gc zE8k`z@{HJ=z?6I>f!OW7ED$Rutv?^A3`FID>1hT0^0c2NwYZ4BHs?#XG_n`@GpO0^qtXNGVUBSS~tytnbV?zi+1PRSq7W zTs*AQ9QR#ehljCCj08QrYa-F$Gcz-=`^{arhU%#l17_%h2U0t^5%t1P6U Sm55~k00003552EP)Nkl$NWTs#~O zyRBC1^QTXrerjc9LY3#Egd{-o18{fp75D@qhd0AAIwuef-={ z#HmxKKJ>uTPd`0;{`u$U0rEU2%QBKAA&MfBB%vq@lv22^%fi9}^?DsCB~=BZ(Jn=j zaQoIx3Z1jj@ALMRo8;aCgHegyUF78HhiT3AhVA9nexPMF;Yq#$H8$Ndc7WHSrR7+MNu*s^l==Irsr_|%DcSzhqu|wQWDX?FI#kvKFq>r zze=;-;d2KSRRQrdCN9eO+NB_9>iqPO#^#+Z`0A5SJ}Jwxq$moG9zBY(ZL%yQO%ir@ zc5v%1MHn#jr%0{1^2c|%J{UoxiGA!*&OdRUGKTIPq&}oEL{Y-_#+We7$deq)s|o4O zDIulhr(*OWz_ZUj`{UE6PXn;9u)yx_9-=aMp3COO21%T7@zvLn-3467L3<58^W|@F z=HLP+T*!cpA@~AzZX`^@lr$|V3XLrV^@dB!b4a8rrCF^2JYOt)R< z#TS3ar8nQDb8L}AM~?GP-}*N9%{OShpBn=c;My2QdxkhG$ch4E1g@iyQjisjD9lKc z9H|S0Q>Su0rz}lA4(08A07Ah0T#Ijg`-hx=>MQ)q_kPGhPksal>{j3#ND?S=&>Fmk zg{5i`Rro>a2!A@B1>pvDq9;%cRexQ2au*2zxwrW`1IM&@Xe>c0>Gr2 zmAHBzz!Km|kPV1{$rPg45XU*C0k`g8DM^xPvLq*n3$!*!W1!KfEQuFaa?;O4xDTKx z3Wht|te$_81Q=8>2h;%#6au0G;u5kPjE0&Ema?!cmuZkQo+hMefzcJxk+fPKWoanV zl%y2eDAg#9-a4$v_E#WD>G!wz(y5st=Ya$m0~X*^P_x0YA@d*z;r332pA_iKfCa6# zN4@Eg#+u|@#NdxoOr@zwfpo2E6brB0 z6~Uhch_i(5p*gg)7?)6#Fz?PhP#P$JX#!~pMLP3^>wyp!p>&x{Q~V$!Nj1W%upLFG zCMk1>qY#}{RjsC++qo6(1IWTD{&0vRB{iT_pe*n+(AF%+7BfMqA(1c*U^;;$F42`i z*cOeJhjKJwkTMQ(ilRUuY1%fGqby+=If~@|3Mf|*=Njo9ATcnULahdKb@*Tsqy>zo zke47N*fuoVAZ*2STrwKRq?yK;3gsx8Eel;Z#HnW24^YYzp>4C+CcS(&fWJLrI2vJ` zlBR&P36+68A1r(3hnfTJ4ip;3Qy7jQ3@frs0~PIV1FtkpCK;n~L{^yDFsdo$yG;^D zRY_WjgNH@7uLAAf0did;9mObzTif8);PCxW15yKi3PAu#2)cqs6TF7N4;ACFVQ(*D zGEERh(CT>flq541evq)S?NhtYR+b~HBqiMkuzjnKAH-M-he5*J99R}?51}YPD)3zB zv}eO73}J5sexS*6gCzy6wujfK5e7MvX-1GlAS!IB=(d|^&$ePCs!KOW_5pO-Z43&h zv%q!);V#r0&}@R^0Ro~J{9VXD0BS-dC01-P8m3Hwj4an+32aBwR4#c@;rj_eS>o2~ zIzh^MUF@&G*7ks`ENC=6lmf2-MFqD6NY7;>pt(25Mk~h%uH6k_${ei?8Fe-XP-W0+L%lV7C4&f}1j=l7 z|0_V*5@A~elZ-IRC<={Rw>fmAgV(TeYBTdnQ;|+`8mgO*Uz8-`9RzaF!d); z8JcYuuUQdm*qaEd5eNmX#;o_HshEs3eqhjAV@X4&Q{&)a7t5MG&qWDVp$XOlhHGyp z{VNyWyLI)oi<>vzczJvC+W&Mr?V{aoe;mMZ6lGx;3|%H+MWZI@bik=Wn#1})GZ`9M za~2B=k|W0zWeHIZQ7{wLVM(|(VRYkSdi9O}{r$E7fAPO}Ze9Id6ihGodc7$?x6}S; z_S^{&_&!#p>CAh0mLLu_H`Wc76119%`2!XYJRngu2;$idD07HzrvzIAwys|6Z(e!z z(%{-_uMBR!^Rhn~zSZmXrmmxKdp-VKeB1+~qReyFHlU$$=G%gkpRnn61qkpX*xCbs zM-$u{v%7Y2vU&N!9|kvGzc5&T`{l{*=9{H9o4sDIvL*EHA$o6sQec^a2Ol)tf1pN^ zLL9)_RTyu^?5(|(_1}BxovrH^FAUb+`u%X{=IeQuuJ?Mq44~7U`-_760M^#lJm6~| zQE1}Ow$G{%HPF7yJ87hZ{)7>pXC5LASD4sfvvM3zlp2gVMJjdtC-t%tRxhrjJ# zpZD+jua!B+H1UQvdGdY#_kZ8_KJW4Wf$Q==d%!S-etqrK9=MeZs*?&#XM1sO=O-BB z{w$H3;@qs3%8E}leT*+`OCPH-s^hDz&bo>+^E{M0Z-vRi@$lnkfWTAS?xu0TH?MO#Z58&)6^l&fxhSu!gT>~8xuPO}&LHGRpd3BBhzr|^BZP;T z(i0j1TEEP7%ky`eEMoOvlygNBQv;T5c-Bm;f`O@^DT+^cN}^IKS!GlfY6xrTo6;wZ z02A$Y+k*A9zQ6Qs@sokd${LGTW-^!iO|nspl!W$-!28@g=;KnF$Y})PNkoy$Fseo_ zNs`Y_vPuH+pchIkh*UIyZ{Kx1y1aJNHmB?6YDs9@!h4ZDu!YHtXNb#0TqBTGVSuUt z3UkuH#G9eSLp}_({R->+jY!4Pu-M(KntPZJ^HBHn=cof*`Or)FbI2-TbbCB#u-|l%6nkQ(*)Dv9dO?zZj^9|A{m9a7|~8lC#4=Z ze{yf~yWeo0Cj{(n`MyvfphBewlt}`a@)dB^y~ursh)J*TVTQu{COi-wYQf$Eof0d_ zeDQ+rwgLe}1Ffj4T1_4)BoaYboLjk1I<9FhoM2me77bS+e=mgSPOJ^>p zZb+vSNEs_&wpH@{u)gt{C#rZL4qwY>IMj$PK6Da3LbslC=md<0oSLKc$z%}auKQUL z43{ExJxienvdS3h>zjNmJ&EN7Lb6y~ql-=;6(54CrVxwyQMH~Qu@p&XO33PmW;7vz zk+yo&H^ln5UlLGCFJEFs7w7~MSNxz8kTKa{b*yBn94Xp%?Ng;Ji-w<$hK~QCxn8Uf z5hd2kt&;>&=r9_Ln{K%2KJui;uF?8zhF`ScWnuuazyL1v49C7cro~>$PP{PXUzARO z)WYGhHrSWHz&vgH$M`GB2)iT|hCIY&w04b$+Am?OxeJ5GTC`3j+3qEp2>ChCdOyb! z(Fqt0{0t>RSmI{+_3Yxer8NHWE2D^oy;^Jb`@NZti^*VXA4Z!x;cdErLEai8q{b!F zJm7B?K^cHG2n3HAU+pKExtaI6erEZ#3C@F%>x13;(GOczV3rf1X?} zbt(#n2h z6p3}BSe~WT6%p<64ovfi^MvC%tfKNSrke_6`I71 zmGR6~!e1a=oTne{se5q VY%2RVTYUfk002ovPDHLkV1oUj+W`Oo diff --git a/pe/qt/base/images/gear.png b/pe/qt/base/images/gear.png deleted file mode 100755 index 41ff2dd6d78b9827ab401185d625f35184687699..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 394 zcmeAS@N?(olHy`uVBq!ia0vp^JRr=$1|-8uW1a&kwj^(N7l!{JxM1({$v_d#0*}aI z1_o|n5N2eUHAey{$X?><>&kwWhn-hLtURzvhk=1n+|$J|MB{w!#e-f~3I*6doPYCA zR5WthRol+0h;3G_;z!LtNbZuns_b!IL1bS;(uI}T0&liP-(8ZtU_&XVbw`}jtz0XM zq!#AGH9u?X57(scyW_h1l~P=1MeL7wztvg%{u?`4bxy}BwrGp#p)m@*T*{pZ$N2B8h+e&7oveb317H3V|%s+_Ln#?%p zU9;-$wvF+^YCmo!?As)JX4;fI(~`WMlkDyvOYc$2;i%;)=nLJWP$hV7LpwjO-bahe z28VRy9xl+cQ2d`#)xlnIf0cGlqGfRNdew!Y70YaH>?{2+eRlQvhZ65UJUPa8zOYGK m=FuVBjZOViwf|49XOukW9JuC4h#N4}7(8A5T-G@yGywqT51()V diff --git a/pe/qt/base/images/power_marker32.png b/pe/qt/base/images/power_marker32.png deleted file mode 100755 index 48129026bc3e9d827169bb5e8d4c7160690c5444..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2132 zcmV-a2&?yrP)Oc*tiKUB_?Sbf!w6DHHj1|DpjEhRip|PB~n$T zl~P4XTA`*&(@;u7fCi_$48)jJEx0(?u`%`rWBghcjJ#}q)?ecHg|Xa z%zSs|+;hKkzVA|oVc@Hb-riocw6uWddDPU@U}R(jBg3OuS-B!>-D3?xR%Vv^|K_s` z-86Iqkw5@KAV6DPj)xeA;b0=AZa<%o(}pX3GPitrFp)^?TD*9170a^VI1ZjXFG7(Z zP7LhIn+{Dqwe2S__`Wm%O_tF6?rvP#yJyK0+kf1xXtYg^MhuFgAYSw1`oCJ}(Z`>D zX6K&w4_G;C&F0OU>uyaXe3Qtk-Y?XA=h@i37+eqlMSccFBh#B(Js+935A_d2 z4u>gCmN85KTnLIcx7Dxz$Rwn1piAU48DQQC)gs`0_-&kg?*fXnwL_aXH~CWnkffPh z?kxA$LxFKj`+U&iafCDt-SIGVuXo?7va&fd?nbO1ANX3ZzIZ$O!Uxg%eh)URcoCPG zep*)Kd*%>*x6jioO{HaK)Wn213`K!K)0i`K%S@aGT5PuRSU3WMtXw8HU95o&8}|=yBy9AZ`)8#YXScfS zD=eGPdq~E*2ezW9qyVva)O-zpKL8fq0-^#V5v$O3q>0g3QUDCuaF3NWa$W9HZ+x_; zYxwH(j}a9Pja)hV$-WDMu3C-WL=R54oq`|;lshebfVcChGr-7+I$0(wk46a|11wR1 zg|btONYyom40&nonvcxb^78Um`h0y&IN_T0H2re)jyJUtt*Q^Fi<3{Ci?F)J##Na&q*XCGZwTr8NeRn^r8JI6*=eyP zw?>!~-1Teg-I64kCIHbgi3@5w&L0~Zf8wWQH&S9i=ekh(J_;u@cVKe#;msTps&WWisM$>t#sT185z46Wxcyc(`EW#FHDoJ(W@pTUXi*eXQ$g+4=bO2NacFeUUt!Nouh$q0W}*><4GsB><&cABNTM3aWX-25FslnV zG4?8MwB}&Tw_buPlgIs-&|Mkucsxc%niC??zbh-fIx-hUM4Si1pudo_b*j*LD!BoG zD2vWQX&wAWa!~)!_n{ghg25mZNlv3u^#M}_8u>VJL&o7ws|fgKkj`kxrY$G4t;(wd zig=&__|%M@3{L#vjW_m}Ivp;JPX}86OS<#oq755xT>nEj@H&zpPlE8ij^xdPhtlyUq#j;OVQBipu4i7N=OX= zaYKj8Q4T_aP8gwKvCJkCOS>X?y|I`R_1kgFmoMK#jOP)R)C+}NKYDH}24~J;SMx7$ z`0(Lc0-!5}Vnp27^x+IwRU$Hvc_D%u2^}4A8Fq?0=AfxtNk~`o6!$a%p*AJw;1!l(Cp;m`3_aTxH5Yjb7b$z&;<-3eq3QVTq&}6qEUl>QJ zwH{$-A3p4Q9W(9uSX8GK8AyJF?Ra61J8pk@{##~1Mwluy7E6)-oX;jMP*qj+r^~r?DZ*0iRpsY@=C_Xti z;br-bI~q4qAt$GA+8j~|rrQ@|P1R~#8S*88MI%u=P1C6XFbU4h$eLX3bP*9ykI1UT zhz3sQvs~2lh%XLBm0#HL^Iv7=Wak>0ya(MqJ@EDSQyVul3TCUGx(^T)kVXtH+VBWs z92~_Ragi0txbqxQQ(Nn4YHISBrTEb`Ymk|l3Et{J_7Zrzx~x)>XPE(U+(?e-iOUQs@W4TEc)k8;HB5Lq3!g?$G;)~3LNAICnWuB zKEs8y1!AsTxe_ZbF2&5u3@C%agtZ-iIr19(Z8qO;b~jy0-3OA5DFs1Oy zk`Pq%hfcz0*^NVO?;v+-J$}%*1v;%!GO^|q88DKqxUNW9gOW73L}y3UWcnG<$?3?D zm1M0jbD%KoYjh6F=7`_#FA>55Z?oD+yqsjkWXGB26v?T1^j-k|E5HDx{l9{o3w;Ct0000< KMNUMnLSTa2=m_or diff --git a/pe/qt/base/images/preferences32.png b/pe/qt/base/images/preferences32.png deleted file mode 100755 index bdbc4051bed43e22ea6057ac4844866396219458..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2899 zcmWlbdpy(oAIHDD4_~Yl8$hqQgSWp0I3ayzVZ>Qtvr z*5#Db#Wtf0GC!#lHJeLH<<4Z=_qYD|eBO`GAD_?X@p^xrulMJj9~u&1Y-ni+0Dy5& zpnsS?Qhp9RRzDxj%9_##bdqndF96g%r(+5}>(`|8jp6A#QWMjU#~eujeB)DN6DUC` zF-Zww2{G}RhhHRk1AyURkiYNF-`>Y+V3;9eA|W6a9UDg(La25yHNS5jI)b?ENH7A% z4^Vsr!T(dra!CLZ@1I1Yxju=HP4q#=Z^NkUC~Hx%F*N5yj4GDmgKnyW)J6mbN_C9+ zHUCoig$oD@#Iz;6Y?rz!+NJr^&*TcQZm;+Jr0zq{Y^@Lbw~78|#Rr&kTSBv~^J-D! zF66Qs$niy#44Ex#mS=%Fyow-(43KDo5kah-omCt^dLY0&nKaF)i(apt|Ne62_hK8x z^#;yt#Nc3I(q4Iu&kx;n>%~0^G3UgXw&y`4tkXUMyN;vsM)^~y+YW?OMC_V!sh^)8 z$UFwx_+&!{w^4@;2}gm$s|kF2y}kj6uHU0w>gd>-l-%E>nUzOBrKj%Ic^%XAiQTT6 zxZ?2zce=gisMVgCfuH{;^V&})R~Mb$+Qwi!J~8?EThH56b~enOme-0cK@gUfSrLg; zJ3BjSUST2MdaO^15`2Dj)y+F|0zFi+kK$v{GzqD#m;jeNW_fLFq)x#ho!VGmQAGA= z?CROBh1*`pB*SxZ;gthZjODJy0W(NV-BYtW>s(;+~lZ7-EhaTQ|i$Q&ok#eAad;tuzMy_~hn^ zj6(w}Z|#E-_b$#U#NSwTS1w9wg!AI%=Hl8mAuU5SfF1)AwejZg z;w2vwyKf(}mdR@w#I*;zn?%P(%tfg0y$$Wulvtxz{Qm=GEk77Hwo60}});k$AVIvKa<00jDogh60Ws|;k%W>ZP!L=vfit!?QW z9Bdv_kH-nB=&7{qA5;0#r!Cde&dyTb9a{=xbecKVj=fK^P1>YeS@(rP3TnlQ6&}MJ zSZC}QPn2?>LRUod#=)l#{yZKCoMCw3y!cgn0GTTD6{*p01L?ijds0M1BtL^>KC+&L*pQY+ zr5;}6HZ?QDGv2-XHPZ5C>zL+UoZ!Hoc5XBCT}pGoVopbMI6v?+7>7uyA)y?l=g~OiU0B#1@WHmxc#nVC zw2rW~c?ryC-0VkxEQexJj@rq3t{x}qbh_rTY3<4ZXZ7MrBMDYt8QTzIFXwhGTef3Q&U#?0t{GzCJO6DEW;mCiM%9U390wTlWI zFj(z2fr8P1n(XHYTx=-YCE!g9Xeb}Jk&Z1#>8th-v`y%d`^$uKy&alqKo|f@Mq~a% z#^W6p916dir_?=(6sm~MCQc;o1#?+Nu$#mn^j(*A6j_7H`a0qjh=_Ns z-s3g`1g6M+%piQLPNUNr)#Ec~zprsOPUD%GKR+V$zDL4rPWuW<)}myZUrvI1Txv0e z9C!v3RRl@qzD-JPfxlO<5id2m&(>WCa^Dd_Ma3^9b6Y(CMb*5 z*pZvj%9384*;NGf*!%7Q6EMcK}43*}Jd!4t5a_nawfNOuKCo#iELk%%&eUyU|g%~FK zuz#R~V5WkvN3c zAvP-60BN*5bd1;S%`D9EZ<4;7Qq0OhPD2BZ{z9$BK%Ptimw(QxyWKr8bZ6LYjJ=V* zLGp8|oGR1+XGDru=L5?tHXpxUBRnZclnPQUg8|BnrE3APG$8z~IZp3dpf6 zCuc!-=h+1H)cCRv+sEK2gB_!u8pL$~p4iY$B^P$Y6VU5URpE9n0ebRC8=19tktd%U zZH1!1%})CzODoR!ZuMJqO>jn8d=HJXxv>D{MRmWhb0^8)e~wrW1+_MI7Q}rk$jJZ@ zO}z8ij}0g0EyGtnLH%GFhR=vmPg^;HEf-yokv?+npcwG*_hDzt(!Mn96?i>Zjoi>j z_ZTrSFrY 0: - msg = "Are you sure you want to start a new duplicate scan?" - if not self._confirm(title, msg): - return - try: - self.app.start_scanning() - except NoScannableFileError: - msg = "The selected directories contain no scannable file." - QMessageBox.warning(self, title, msg) - self.app.show_directories() - except AllFilesAreRefError: - msg = "You cannot make a duplicate scan with only reference directories." - QMessageBox.warning(self, title, msg) - - def showHelpTriggered(self): - self.app.show_help() - - #--- Events - def application_will_terminate(self): - self._save_columns() - - def columnToggled(self, action): - colid = action.column_index - if colid == -1: - self.app.prefs.reset_columns() - self._load_columns() - else: - h = self.resultsView.header() - h.setSectionHidden(colid, not h.isSectionHidden(colid)) - self._update_column_actions_status() - - def dupeMarkingChanged(self): - self._redraw_results() - self._update_status_line() - - def resultsChanged(self): - self.resultsView.model().reset() - - def resultsReset(self): - self.resultsView.expandAll() - self._update_status_line() - - def selectionChanged(self, selected, deselected): - index = self.resultsView.selectionModel().currentIndex() - dupe = index.internalPointer().dupe if index.isValid() else None - self.app.select_duplicate(dupe) - diff --git a/pe/qt/base/main_window.ui b/pe/qt/base/main_window.ui deleted file mode 100644 index 754f265c..00000000 --- a/pe/qt/base/main_window.ui +++ /dev/null @@ -1,911 +0,0 @@ - - - MainWindow - - - - 0 - 0 - 630 - 514 - - - - dupeGuru - - - - - 0 - - - - - QAbstractItemView::ExtendedSelection - - - QAbstractItemView::SelectRows - - - false - - - true - - - false - - - true - - - false - - - false - - - - - - - - - 0 - 0 - 630 - 22 - - - - - Columns - - - - - Actions - - - - - - - - - - - - - - - - - - - - Mark - - - - - - - - - Modes - - - - - - - Windows - - - - - - - - Help - - - - - - - - - File - - - - - - - - - - - - - - - - - - toolBar - - - false - - - Qt::ToolButtonTextUnderIcon - - - false - - - TopToolBarArea - - - false - - - - - - - - - - - - true - - - - - - :/logo_pe:/logo_pe - - - Start Scan - - - Start scanning for duplicates - - - Ctrl+S - - - - - - :/folder:/folder - - - Directories - - - Ctrl+4 - - - - - - :/details:/details - - - Details - - - Ctrl+3 - - - - - - :/actions:/actions - - - Actions - - - - - - :/preferences:/preferences - - - Preferences - - - Ctrl+5 - - - - - true - - - - :/delta:/delta - - - Delta Values - - - Ctrl+2 - - - - - true - - - - :/power_marker:/power_marker - - - Power Marker - - - Ctrl+1 - - - - - Send Marked to Recycle Bin - - - Ctrl+D - - - - - Move Marked to... - - - Ctrl+M - - - - - Copy Marked to... - - - Ctrl+Shift+M - - - - - Remove Marked from Results - - - Ctrl+R - - - - - Remove Selected from Results - - - Ctrl+Del - - - - - Add Selected to Ignore List - - - Ctrl+Shift+Del - - - - - Make Selected Reference - - - Ctrl+Space - - - - - Open Selected with Default Application - - - Ctrl+O - - - - - Open Containing Folder of Selected - - - Ctrl+Shift+O - - - - - Rename Selected - - - F2 - - - - - Mark All - - - Ctrl+A - - - - - Mark None - - - Ctrl+Shift+A - - - - - Invert Marking - - - Ctrl+Alt+A - - - - - Mark Selected - - - - - Clear Ignore List - - - - - Quit - - - Ctrl+Q - - - - - Apply Filter - - - Ctrl+F - - - - - Cancel Filter - - - Ctrl+Shift+F - - - - - dupeGuru Help - - - F1 - - - - - About dupeGuru - - - - - Register dupeGuru - - - - - Check for Update - - - - - - ResultsView - QTreeView -

results_model
- - - - - - - - actionDirectories - triggered() - MainWindow - directoriesTriggered() - - - -1 - -1 - - - 314 - 256 - - - - - actionActions - triggered() - MainWindow - actionsTriggered() - - - -1 - -1 - - - 314 - 256 - - - - - actionCopyMarked - triggered() - MainWindow - copyTriggered() - - - -1 - -1 - - - 314 - 256 - - - - - actionDeleteMarked - triggered() - MainWindow - deleteTriggered() - - - -1 - -1 - - - 314 - 256 - - - - - actionDelta - triggered() - MainWindow - deltaTriggered() - - - -1 - -1 - - - 314 - 256 - - - - - actionDetails - triggered() - MainWindow - detailsTriggered() - - - -1 - -1 - - - 314 - 256 - - - - - actionIgnoreSelected - triggered() - MainWindow - addToIgnoreListTriggered() - - - -1 - -1 - - - 314 - 256 - - - - - actionMakeSelectedReference - triggered() - MainWindow - makeReferenceTriggered() - - - -1 - -1 - - - 314 - 256 - - - - - actionMoveMarked - triggered() - MainWindow - moveTriggered() - - - -1 - -1 - - - 314 - 256 - - - - - actionOpenSelected - triggered() - MainWindow - openTriggered() - - - -1 - -1 - - - 314 - 256 - - - - - actionPowerMarker - triggered() - MainWindow - powerMarkerTriggered() - - - -1 - -1 - - - 314 - 256 - - - - - actionPreferences - triggered() - MainWindow - preferencesTriggered() - - - -1 - -1 - - - 314 - 256 - - - - - actionRemoveMarked - triggered() - MainWindow - removeMarkedTriggered() - - - -1 - -1 - - - 314 - 256 - - - - - actionRemoveSelected - triggered() - MainWindow - removeSelectedTriggered() - - - -1 - -1 - - - 314 - 256 - - - - - actionRevealSelected - triggered() - MainWindow - revealTriggered() - - - -1 - -1 - - - 314 - 256 - - - - - actionRenameSelected - triggered() - MainWindow - renameTriggered() - - - -1 - -1 - - - 314 - 256 - - - - - actionScan - triggered() - MainWindow - scanTriggered() - - - -1 - -1 - - - 314 - 256 - - - - - actionClearIgnoreList - triggered() - MainWindow - clearIgnoreListTriggered() - - - -1 - -1 - - - 314 - 256 - - - - - actionMarkAll - triggered() - MainWindow - markAllTriggered() - - - -1 - -1 - - - 314 - 256 - - - - - actionMarkNone - triggered() - MainWindow - markNoneTriggered() - - - -1 - -1 - - - 314 - 256 - - - - - actionMarkSelected - triggered() - MainWindow - markSelectedTriggered() - - - -1 - -1 - - - 314 - 256 - - - - - actionInvertMarking - triggered() - MainWindow - markInvertTriggered() - - - -1 - -1 - - - 314 - 256 - - - - - actionApplyFilter - triggered() - MainWindow - applyFilterTriggered() - - - -1 - -1 - - - 314 - 256 - - - - - actionCancelFilter - triggered() - MainWindow - cancelFilterTriggered() - - - -1 - -1 - - - 314 - 256 - - - - - actionShowHelp - triggered() - MainWindow - showHelpTriggered() - - - -1 - -1 - - - 314 - 256 - - - - - actionAbout - triggered() - MainWindow - aboutTriggered() - - - -1 - -1 - - - 314 - 256 - - - - - actionRegister - triggered() - MainWindow - registerTrigerred() - - - -1 - -1 - - - 314 - 256 - - - - - actionCheckForUpdate - triggered() - MainWindow - checkForUpdateTriggered() - - - -1 - -1 - - - 314 - 256 - - - - - - directoriesTriggered() - scanTriggered() - actionsTriggered() - detailsTriggered() - preferencesTriggered() - deltaTriggered() - powerMarkerTriggered() - deleteTriggered() - moveTriggered() - copyTriggered() - removeMarkedTriggered() - removeSelectedTriggered() - addToIgnoreListTriggered() - makeReferenceTriggered() - openTriggered() - revealTriggered() - renameTriggered() - clearIgnoreListTriggered() - clearPictureCacheTriggered() - markAllTriggered() - markNoneTriggered() - markInvertTriggered() - markSelectedTriggered() - applyFilterTriggered() - cancelFilterTriggered() - showHelpTriggered() - aboutTriggered() - registerTrigerred() - checkForUpdateTriggered() - - diff --git a/pe/qt/base/preferences.py b/pe/qt/base/preferences.py deleted file mode 100644 index 64ad64d5..00000000 --- a/pe/qt/base/preferences.py +++ /dev/null @@ -1,109 +0,0 @@ -#!/usr/bin/env python -# Unit Name: preferences -# Created By: Virgil Dupras -# Created On: 2009-05-03 -# $Id$ -# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) - -from PyQt4.QtCore import QSettings, QVariant - -from hsutil.misc import tryint - -def variant_to_py(v): - value = None - ok = False - t = v.type() - if t == QVariant.String: - value = unicode(v.toString()) - ok = True # anyway - # might be bool or int, try them - if v == 'true': - value = True - elif value == 'false': - value = False - else: - value = tryint(value, value) - elif t == QVariant.Int: - value, ok = v.toInt() - elif t == QVariant.Bool: - value, ok = v.toBool(), True - elif t in (QVariant.List, QVariant.StringList): - value, ok = map(variant_to_py, v.toList()), True - if not ok: - raise TypeError() - return value - -class Preferences(object): - # (width, is_visible) - COLUMNS_DEFAULT_ATTRS = [] - - def __init__(self): - self.reset() - self.reset_columns() - - def _load_specific(self, settings, get): - # load prefs specific to the dg edition - pass - - def load(self): - self.reset() - settings = QSettings() - def get(name, default): - if settings.contains(name): - return variant_to_py(settings.value(name)) - else: - return default - - self.filter_hardness = get('FilterHardness', self.filter_hardness) - self.mix_file_kind = get('MixFileKind', self.mix_file_kind) - self.use_regexp = get('UseRegexp', self.use_regexp) - self.remove_empty_folders = get('RemoveEmptyFolders', self.remove_empty_folders) - self.destination_type = get('DestinationType', self.destination_type) - widths = get('ColumnsWidth', self.columns_width) - # only set nonzero values - for index, width in enumerate(widths[:len(self.columns_width)]): - if width > 0: - self.columns_width[index] = width - self.columns_visible = get('ColumnsVisible', self.columns_visible) - self.registration_code = get('RegistrationCode', self.registration_code) - self.registration_email = get('RegistrationEmail', self.registration_email) - self._load_specific(settings, get) - - def _reset_specific(self): - # reset prefs specific to the dg edition - pass - - def reset(self): - self.filter_hardness = 95 - self.mix_file_kind = True - self.use_regexp = False - self.remove_empty_folders = False - self.destination_type = 1 - self.registration_code = '' - self.registration_email = '' - self._reset_specific() - - def reset_columns(self): - self.columns_width = [width for width, _ in self.COLUMNS_DEFAULT_ATTRS] - self.columns_visible = [visible for _, visible in self.COLUMNS_DEFAULT_ATTRS] - - def _save_specific(self, settings, set_): - # save prefs specific to the dg edition - pass - - def save(self): - settings = QSettings() - def set_(name, value): - settings.setValue(name, QVariant(value)) - - set_('FilterHardness', self.filter_hardness) - set_('MixFileKind', self.mix_file_kind) - set_('UseRegexp', self.use_regexp) - set_('RemoveEmptyFolders', self.remove_empty_folders) - set_('DestinationType', self.destination_type) - set_('ColumnsWidth', self.columns_width) - set_('ColumnsVisible', self.columns_visible) - set_('RegistrationCode', self.registration_code) - set_('RegistrationEmail', self.registration_email) - self._save_specific(settings, set_) - diff --git a/pe/qt/base/reg.py b/pe/qt/base/reg.py deleted file mode 100644 index 59fd0bc3..00000000 --- a/pe/qt/base/reg.py +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env python -# Unit Name: reg -# Created By: Virgil Dupras -# Created On: 2009-05-09 -# $Id$ -# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) - -from hashlib import md5 - -from PyQt4.QtGui import QDialog - -from reg_submit_dialog import RegSubmitDialog -from reg_demo_dialog import RegDemoDialog - -class Registration(object): - def __init__(self, app): - self.app = app - - def ask_for_code(self): - dialog = RegSubmitDialog(self.app.main_window, self.app.is_code_valid) - result = dialog.exec_() - code = unicode(dialog.codeEdit.text()) - email = unicode(dialog.emailEdit.text()) - dialog.setParent(None) # free it - if result == QDialog.Accepted and self.app.is_code_valid(code, email): - self.app.set_registration(code, email) - return True - return False - - def show_nag(self): - dialog = RegDemoDialog(self.app.main_window, self) - dialog.exec_() - dialog.setParent(None) # free it - diff --git a/pe/qt/base/reg_demo_dialog.py b/pe/qt/base/reg_demo_dialog.py deleted file mode 100644 index 95280314..00000000 --- a/pe/qt/base/reg_demo_dialog.py +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env python -# Unit Name: reg_demo_dialog -# Created By: Virgil Dupras -# Created On: 2009-05-10 -# $Id$ -# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) - -from PyQt4.QtCore import SIGNAL, Qt, QUrl, QCoreApplication -from PyQt4.QtGui import QDialog, QMessageBox, QDesktopServices - -from reg_demo_dialog_ui import Ui_RegDemoDialog - -class RegDemoDialog(QDialog, Ui_RegDemoDialog): - def __init__(self, parent, reg): - flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint - QDialog.__init__(self, parent, flags) - self.reg = reg - self._setupUi() - - self.connect(self.enterCodeButton, SIGNAL('clicked()'), self.enterCodeClicked) - self.connect(self.purchaseButton, SIGNAL('clicked()'), self.purchaseClicked) - - def _setupUi(self): - self.setupUi(self) - # Stuff that can't be setup in the Designer - appname = QCoreApplication.instance().applicationName() - title = self.windowTitle() - title = title.replace('$appname', appname) - self.setWindowTitle(title) - title = self.titleLabel.text() - title = title.replace('$appname', appname) - self.titleLabel.setText(title) - desc = self.demoDescLabel.text() - desc = desc.replace('$appname', appname) - self.demoDescLabel.setText(desc) - - #--- Events - def enterCodeClicked(self): - if self.reg.ask_for_code(): - self.accept() - - def purchaseClicked(self): - url = QUrl('http://www.hardcoded.net/purchase.htm') - QDesktopServices.openUrl(url) - diff --git a/pe/qt/base/reg_demo_dialog.ui b/pe/qt/base/reg_demo_dialog.ui deleted file mode 100644 index ef918225..00000000 --- a/pe/qt/base/reg_demo_dialog.ui +++ /dev/null @@ -1,140 +0,0 @@ - - - RegDemoDialog - - - - 0 - 0 - 387 - 161 - - - - $appname Demo Version - - - - - - - 75 - true - - - - $appname Demo Version - - - - - - - You are currently running a demo version of $appname. This version has limited functionalities, and you need to buy it to have access to these functionalities. - - - true - - - - - - - In the demo version, only 10 duplicates per session can be sent to the recycle bin, moved or copied. - - - true - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - 110 - 0 - - - - Try Demo - - - - - - - - 110 - 0 - - - - Enter Code - - - - - - - - 110 - 0 - - - - Purchase - - - - - - - - - - - tryButton - clicked() - RegDemoDialog - accept() - - - 112 - 161 - - - 201 - 94 - - - - - diff --git a/pe/qt/base/reg_submit_dialog.py b/pe/qt/base/reg_submit_dialog.py deleted file mode 100644 index 4ba680b6..00000000 --- a/pe/qt/base/reg_submit_dialog.py +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env python -# Unit Name: reg_submit_dialog -# Created By: Virgil Dupras -# Created On: 2009-05-09 -# $Id$ -# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) - -from PyQt4.QtCore import SIGNAL, Qt, QUrl, QCoreApplication -from PyQt4.QtGui import QDialog, QMessageBox, QDesktopServices - -from reg_submit_dialog_ui import Ui_RegSubmitDialog - -class RegSubmitDialog(QDialog, Ui_RegSubmitDialog): - def __init__(self, parent, is_valid_func): - flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint - QDialog.__init__(self, parent, flags) - self._setupUi() - self.is_valid_func = is_valid_func - - self.connect(self.submitButton, SIGNAL('clicked()'), self.submitClicked) - self.connect(self.purchaseButton, SIGNAL('clicked()'), self.purchaseClicked) - - def _setupUi(self): - self.setupUi(self) - # Stuff that can't be setup in the Designer - appname = QCoreApplication.instance().applicationName() - prompt = self.promptLabel.text() - prompt = prompt.replace('$appname', appname) - self.promptLabel.setText(prompt) - - #--- Events - def purchaseClicked(self): - url = QUrl('http://www.hardcoded.net/purchase.htm') - QDesktopServices.openUrl(url) - - def submitClicked(self): - code = unicode(self.codeEdit.text()) - email = unicode(self.emailEdit.text()) - title = "Registration" - if self.is_valid_func(code, email): - msg = "This code is valid. Thanks!" - QMessageBox.information(self, title, msg) - self.accept() - else: - msg = "This code is invalid" - QMessageBox.warning(self, title, msg) - diff --git a/pe/qt/base/reg_submit_dialog.ui b/pe/qt/base/reg_submit_dialog.ui deleted file mode 100644 index 06de4191..00000000 --- a/pe/qt/base/reg_submit_dialog.ui +++ /dev/null @@ -1,149 +0,0 @@ - - - RegSubmitDialog - - - - 0 - 0 - 365 - 134 - - - - Enter your registration code - - - - - - Please enter your $appname registration code and registered e-mail (the e-mail you used for the purchase), then press "Submit". - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop - - - true - - - - - - - QLayout::SetNoConstraint - - - QFormLayout::ExpandingFieldsGrow - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop - - - - - Registration code: - - - - - - - Registered e-mail: - - - - - - - - - - - - - - - - - Purchase - - - false - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - 0 - 0 - - - - Cancel - - - false - - - - - - - - 0 - 0 - - - - Submit - - - false - - - true - - - - - - - - - - - cancelButton - clicked() - RegSubmitDialog - reject() - - - 260 - 159 - - - 198 - 97 - - - - - diff --git a/pe/qt/base/results_model.py b/pe/qt/base/results_model.py deleted file mode 100644 index d28d6da3..00000000 --- a/pe/qt/base/results_model.py +++ /dev/null @@ -1,175 +0,0 @@ -#!/usr/bin/env python -# Unit Name: -# Created By: Virgil Dupras -# Created On: 2009-04-23 -# $Id$ -# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) - -from PyQt4.QtCore import SIGNAL, Qt, QAbstractItemModel, QVariant, QModelIndex, QRect -from PyQt4.QtGui import QBrush, QStyledItemDelegate, QFont, QTreeView, QColor - -from tree_model import TreeNode, TreeModel - -class ResultNode(TreeNode): - def __init__(self, model, parent, row, dupe, group): - TreeNode.__init__(self, parent, row) - self.model = model - self.dupe = dupe - self.group = group - self._normalData = None - self._deltaData = None - - def _get_children(self): - children = [] - if self.dupe is self.group.ref: - for index, dupe in enumerate(self.group.dupes): - children.append(ResultNode(self.model, self, index, dupe, self.group)) - return children - - def reset(self): - self._normalData = None - self._deltaData = None - - @property - def normalData(self): - if self._normalData is None: - self._normalData = self.model._data.GetDisplayInfo(self.dupe, self.group, delta=False) - return self._normalData - - @property - def deltaData(self): - if self._deltaData is None: - self._deltaData = self.model._data.GetDisplayInfo(self.dupe, self.group, delta=True) - return self._deltaData - - -class ResultsDelegate(QStyledItemDelegate): - def initStyleOption(self, option, index): - QStyledItemDelegate.initStyleOption(self, option, index) - node = index.internalPointer() - if node.group.ref is node.dupe: - newfont = QFont(option.font) - newfont.setBold(True) - option.font = newfont - - -class ResultsModel(TreeModel): - def __init__(self, app): - self._app = app - self._results = app.results - self._data = app.data - self._delta_columns = app.DELTA_COLUMNS - self.delta = False - self._power_marker = False - TreeModel.__init__(self) - - def _root_nodes(self): - nodes = [] - if self.power_marker: - for index, dupe in enumerate(self._results.dupes): - group = self._results.get_group_of_duplicate(dupe) - nodes.append(ResultNode(self, None, index, dupe, group)) - else: - for index, group in enumerate(self._results.groups): - nodes.append(ResultNode(self, None, index, group.ref, group)) - return nodes - - def columnCount(self, parent): - return len(self._data.COLUMNS) - - def data(self, index, role): - if not index.isValid(): - return QVariant() - node = index.internalPointer() - if role == Qt.DisplayRole: - data = node.deltaData if self.delta else node.normalData - return QVariant(data[index.column()]) - elif role == Qt.CheckStateRole: - if index.column() == 0 and node.dupe is not node.group.ref: - state = Qt.Checked if self._results.is_marked(node.dupe) else Qt.Unchecked - return QVariant(state) - elif role == Qt.ForegroundRole: - if node.dupe is node.group.ref or node.dupe.is_ref: - return QVariant(QBrush(Qt.blue)) - elif self.delta and index.column() in self._delta_columns: - return QVariant(QBrush(QColor(255, 142, 40))) # orange - elif role == Qt.EditRole: - if index.column() == 0: - return QVariant(node.normalData[index.column()]) - return QVariant() - - def dupesForIndexes(self, indexes): - nodes = [index.internalPointer() for index in indexes] - return [node.dupe for node in nodes] - - def flags(self, index): - if not index.isValid(): - return Qt.ItemIsEnabled - flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable - if index.column() == 0: - flags |= Qt.ItemIsUserCheckable | Qt.ItemIsEditable - return flags - - def headerData(self, section, orientation, role): - if orientation == Qt.Horizontal and role == Qt.DisplayRole and section < len(self._data.COLUMNS): - return QVariant(self._data.COLUMNS[section]['display']) - - return QVariant() - - def setData(self, index, value, role): - if not index.isValid(): - return False - node = index.internalPointer() - if role == Qt.CheckStateRole: - if index.column() == 0: - self._app.toggle_marking_for_dupes([node.dupe]) - return True - if role == Qt.EditRole: - if index.column() == 0: - value = unicode(value.toString()) - if self._app.rename_dupe(node.dupe, value): - node.reset() - return True - return False - - def sort(self, column, order): - if self.power_marker: - self._results.sort_dupes(column, order == Qt.AscendingOrder, self.delta) - else: - self._results.sort_groups(column, order == Qt.AscendingOrder) - self.reset() - - def toggleMarked(self, indexes): - assert indexes - dupes = self.dupesForIndexes(indexes) - self._app.toggle_marking_for_dupes(dupes) - - #--- Properties - @property - def power_marker(self): - return self._power_marker - - @power_marker.setter - def power_marker(self, value): - if value == self._power_marker: - return - self._power_marker = value - self.reset() - - -class ResultsView(QTreeView): - #--- Override - def keyPressEvent(self, event): - if event.text() == ' ': - self.model().toggleMarked(self.selectionModel().selectedRows()) - return - QTreeView.keyPressEvent(self, event) - - def setModel(self, model): - assert isinstance(model, ResultsModel) - QTreeView.setModel(self, model) - - #--- Public - def selectedDupes(self): - return self.model().dupesForIndexes(self.selectionModel().selectedRows()) - diff --git a/pe/qt/base/tree_model.py b/pe/qt/base/tree_model.py deleted file mode 100644 index b3a994b3..00000000 --- a/pe/qt/base/tree_model.py +++ /dev/null @@ -1,66 +0,0 @@ -#!/usr/bin/env python -# Unit Name: tree_model -# Created By: Virgil Dupras -# Created On: 2009-05-04 -# $Id$ -# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) - -from PyQt4.QtCore import Qt, QAbstractItemModel, QVariant, QModelIndex - -class TreeNode(object): - def __init__(self, parent, row): - self.parent = parent - self.row = row - self._children = None - - def _get_children(self): - raise NotImplementedError() - - @property - def children(self): - if self._children is None: - self._children = self._get_children() - return self._children - - -class TreeModel(QAbstractItemModel): - def __init__(self): - QAbstractItemModel.__init__(self) - self._nodes = None - - def _root_nodes(self): - raise NotImplementedError() - - def index(self, row, column, parent): - if not self.nodes: - return QModelIndex() - if not parent.isValid(): - return self.createIndex(row, column, self.nodes[row]) - node = parent.internalPointer() - return self.createIndex(row, column, node.children[row]) - - def parent(self, index): - if not index.isValid(): - return QModelIndex() - node = index.internalPointer() - if node.parent is None: - return QModelIndex() - else: - return self.createIndex(node.parent.row, 0, node.parent) - - def reset(self): - self._nodes = None - QAbstractItemModel.reset(self) - - def rowCount(self, parent): - if not parent.isValid(): - return len(self.nodes) - node = parent.internalPointer() - return len(node.children) - - @property - def nodes(self): - if self._nodes is None: - self._nodes = self._root_nodes() - return self._nodes - diff --git a/pe/qt/dupeguru/__init__.py b/pe/qt/dupeguru/__init__.py deleted file mode 100644 index 8b137891..00000000 --- a/pe/qt/dupeguru/__init__.py +++ /dev/null @@ -1 +0,0 @@ - diff --git a/pe/qt/dupeguru/app.py b/pe/qt/dupeguru/app.py deleted file mode 100644 index 0e03603d..00000000 --- a/pe/qt/dupeguru/app.py +++ /dev/null @@ -1,229 +0,0 @@ -#!/usr/bin/env python -""" -Unit Name: dupeguru.app -Created By: Virgil Dupras -Created On: 2006/11/11 -Last modified by:$Author: virgil $ -Last modified on:$Date: 2009-05-28 16:02:48 +0200 (Thu, 28 May 2009) $ - $Revision: 4388 $ -Copyright 2006 Hardcoded Software (http://www.hardcoded.net) -""" -import os -import os.path as op -import logging - -from hsfs import IT_ATTRS, IT_EXTRA -from hsutil import job, io, files -from hsutil.path import Path -from hsutil.reg import RegistrableApplication, RegistrationRequired -from hsutil.misc import flatten, first -from hsutil.str import escape - -import directories -import results -import scanner - -JOB_SCAN = 'job_scan' -JOB_LOAD = 'job_load' -JOB_MOVE = 'job_move' -JOB_COPY = 'job_copy' -JOB_DELETE = 'job_delete' - -class NoScannableFileError(Exception): - pass - -class AllFilesAreRefError(Exception): - pass - -class DupeGuru(RegistrableApplication): - def __init__(self, data_module, appdata, appid): - RegistrableApplication.__init__(self, appid) - self.appdata = appdata - if not op.exists(self.appdata): - os.makedirs(self.appdata) - self.data = data_module - self.directories = directories.Directories() - self.results = results.Results(data_module) - self.scanner = scanner.Scanner() - self.action_count = 0 - self.last_op_error_count = 0 - self.options = { - 'escape_filter_regexp': True, - 'clean_empty_dirs': False, - } - - def _demo_check(self): - if self.registered: - return - count = self.results.mark_count - if count + self.action_count > 10: - raise RegistrationRequired() - else: - self.action_count += count - - def _do_delete(self, j): - def op(dupe): - j.add_progress() - return self._do_delete_dupe(dupe) - - j.start_job(self.results.mark_count) - self.last_op_error_count = self.results.perform_on_marked(op, True) - - def _do_delete_dupe(self, dupe): - if not io.exists(dupe.path): - dupe.parent = None - return True - self._recycle_dupe(dupe) - self.clean_empty_dirs(dupe.path[:-1]) - if not io.exists(dupe.path): - dupe.parent = None - return True - logging.warning(u"Could not send {0} to trash.".format(unicode(dupe.path))) - return False - - def _do_load(self, j): - self.directories.LoadFromFile(op.join(self.appdata, 'last_directories.xml')) - j = j.start_subjob([1, 9]) - self.results.load_from_xml(op.join(self.appdata, 'last_results.xml'), self._get_file, j) - files = flatten(g[:] for g in self.results.groups) - for file in j.iter_with_progress(files, 'Reading metadata %d/%d'): - file._read_all_info(sections=[IT_ATTRS, IT_EXTRA]) - - def _get_file(self, str_path): - p = Path(str_path) - for d in self.directories: - if p not in d.path: - continue - result = d.find_path(p[d.path:]) - if result is not None: - return result - - @staticmethod - def _recycle_dupe(dupe): - raise NotImplementedError() - - def _start_job(self, jobid, func): - # func(j) - raise NotImplementedError() - - def AddDirectory(self, d): - try: - self.directories.add_path(Path(d)) - return 0 - except directories.AlreadyThereError: - return 1 - except directories.InvalidPathError: - return 2 - - def AddToIgnoreList(self, dupe): - g = self.results.get_group_of_duplicate(dupe) - for other in g: - if other is not dupe: - self.scanner.ignore_list.Ignore(unicode(other.path), unicode(dupe.path)) - - def ApplyFilter(self, filter): - self.results.apply_filter(None) - if self.options['escape_filter_regexp']: - filter = escape(filter, '()[]\\.|+?^') - filter = escape(filter, '*', '.') - self.results.apply_filter(filter) - - def clean_empty_dirs(self, path): - if self.options['clean_empty_dirs']: - while files.delete_if_empty(path, ['.DS_Store']): - path = path[:-1] - - def CopyOrMove(self, dupe, copy, destination, dest_type): - """ - copy: True = Copy False = Move - destination: string. - dest_type: 0 = right in destination. - 1 = relative re-creation. - 2 = absolute re-creation. - """ - source_path = dupe.path - location_path = dupe.root.path - dest_path = Path(destination) - if dest_type == 2: - dest_path = dest_path + source_path[1:-1] #Remove drive letter and filename - elif dest_type == 1: - dest_path = dest_path + source_path[location_path:-1] - if not io.exists(dest_path): - io.makedirs(dest_path) - try: - if copy: - files.copy(source_path, dest_path) - else: - files.move(source_path, dest_path) - self.clean_empty_dirs(source_path[:-1]) - except (IOError, OSError) as e: - operation = 'Copy' if copy else 'Move' - logging.warning('%s operation failed on %s. Error: %s' % (operation, unicode(dupe.path), unicode(e))) - return False - return True - - def copy_or_move_marked(self, copy, destination, recreate_path): - def do(j): - def op(dupe): - j.add_progress() - return self.CopyOrMove(dupe, copy, destination, recreate_path) - - j.start_job(self.results.mark_count) - self.last_op_error_count = self.results.perform_on_marked(op, not copy) - - self._demo_check() - jobid = JOB_COPY if copy else JOB_MOVE - self._start_job(jobid, do) - - def delete_marked(self): - self._demo_check() - self._start_job(JOB_DELETE, self._do_delete) - - def load(self): - self._start_job(JOB_LOAD, self._do_load) - self.LoadIgnoreList() - - def LoadIgnoreList(self): - p = op.join(self.appdata, 'ignore_list.xml') - self.scanner.ignore_list.load_from_xml(p) - - def make_reference(self, duplicates): - changed_groups = set() - for dupe in duplicates: - g = self.results.get_group_of_duplicate(dupe) - if g not in changed_groups: - self.results.make_ref(dupe) - changed_groups.add(g) - - def Save(self): - self.directories.SaveToFile(op.join(self.appdata, 'last_directories.xml')) - self.results.save_to_xml(op.join(self.appdata, 'last_results.xml')) - - def SaveIgnoreList(self): - p = op.join(self.appdata, 'ignore_list.xml') - self.scanner.ignore_list.save_to_xml(p) - - def start_scanning(self): - def do(j): - j.set_progress(0, 'Collecting files to scan') - files = list(self.directories.get_files()) - logging.info('Scanning %d files' % len(files)) - self.results.groups = self.scanner.GetDupeGroups(files, j) - - files = self.directories.get_files() - first_file = first(files) - if first_file is None: - raise NoScannableFileError() - if first_file.is_ref and all(f.is_ref for f in files): - raise AllFilesAreRefError() - self.results.groups = [] - self._start_job(JOB_SCAN, do) - - #--- Properties - @property - def stat_line(self): - result = self.results.stat_line - if self.scanner.discarded_file_count: - result = '%s (%d discarded)' % (result, self.scanner.discarded_file_count) - return result - diff --git a/pe/qt/dupeguru/app_cocoa.py b/pe/qt/dupeguru/app_cocoa.py deleted file mode 100644 index 4974d700..00000000 --- a/pe/qt/dupeguru/app_cocoa.py +++ /dev/null @@ -1,304 +0,0 @@ -#!/usr/bin/env python -""" -Unit Name: dupeguru.app_cocoa -Created By: Virgil Dupras -Created On: 2006/11/11 -Last modified by:$Author: virgil $ -Last modified on:$Date: 2009-05-28 16:33:32 +0200 (Thu, 28 May 2009) $ - $Revision: 4392 $ -Copyright 2006 Hardcoded Software (http://www.hardcoded.net) -""" -from AppKit import * -import logging -import os.path as op - -import hsfs as fs -from hsfs.phys.bundle import Bundle -from hsutil.cocoa import install_exception_hook -from hsutil.str import get_file_ext -from hsutil import io, cocoa, job -from hsutil.reg import RegistrationRequired - -import export, app, data - -JOBID2TITLE = { - app.JOB_SCAN: "Scanning for duplicates", - app.JOB_LOAD: "Loading", - app.JOB_MOVE: "Moving", - app.JOB_COPY: "Copying", - app.JOB_DELETE: "Sending to Trash", -} - -class DGDirectory(fs.phys.Directory): - def _create_sub_dir(self,name,with_parent = True): - ext = get_file_ext(name) - if ext == 'app': - if with_parent: - parent = self - else: - parent = None - return Bundle(parent,name) - else: - return super(DGDirectory,self)._create_sub_dir(name,with_parent) - - -def demo_method(method): - def wrapper(self, *args, **kwargs): - try: - return method(self, *args, **kwargs) - except RegistrationRequired: - NSNotificationCenter.defaultCenter().postNotificationName_object_('RegistrationRequired', self) - - return wrapper - -class DupeGuru(app.DupeGuru): - def __init__(self, data_module, appdata_subdir, appid): - LOGGING_LEVEL = logging.DEBUG if NSUserDefaults.standardUserDefaults().boolForKey_('debug') else logging.WARNING - logging.basicConfig(level=LOGGING_LEVEL, format='%(levelname)s %(message)s') - logging.debug('started in debug mode') - install_exception_hook() - if data_module is None: - data_module = data - appdata = op.expanduser(op.join('~', '.hsoftdata', appdata_subdir)) - app.DupeGuru.__init__(self, data_module, appdata, appid) - self.progress = cocoa.ThreadedJobPerformer() - self.directories.dirclass = DGDirectory - self.display_delta_values = False - self.selected_dupes = [] - self.RefreshDetailsTable(None,None) - - #--- Override - @staticmethod - def _recycle_dupe(dupe): - if not io.exists(dupe.path): - dupe.parent = None - return True - directory = unicode(dupe.parent.path) - filename = dupe.name - result, tag = NSWorkspace.sharedWorkspace().performFileOperation_source_destination_files_tag_( - NSWorkspaceRecycleOperation, directory, '', [filename]) - if not io.exists(dupe.path): - dupe.parent = None - return True - logging.warning('Could not send %s to trash. tag: %d' % (unicode(dupe.path), tag)) - return False - - def _start_job(self, jobid, func): - try: - j = self.progress.create_job() - self.progress.run_threaded(func, args=(j, )) - except job.JobInProgressError: - NSNotificationCenter.defaultCenter().postNotificationName_object_('JobInProgress', self) - else: - ud = {'desc': JOBID2TITLE[jobid], 'jobid':jobid} - NSNotificationCenter.defaultCenter().postNotificationName_object_userInfo_('JobStarted', self, ud) - - #---Helpers - def GetObjects(self,node_path): - #returns a tuple g,d - try: - g = self.results.groups[node_path[0]] - if len(node_path) == 2: - return (g,self.results.groups[node_path[0]].dupes[node_path[1]]) - else: - return (g,None) - except IndexError: - return (None,None) - - def GetDirectory(self,node_path,curr_dir=None): - if not node_path: - return curr_dir - if curr_dir is not None: - l = curr_dir.dirs - else: - l = self.directories - d = l[node_path[0]] - return self.GetDirectory(node_path[1:],d) - - def RefreshDetailsTable(self,dupe,group): - l1 = self.data.GetDisplayInfo(dupe,group,False) - if group is not None: - l2 = self.data.GetDisplayInfo(group.ref,group,False) - else: - l2 = l1 #To have a list of empty '---' values - names = [c['display'] for c in self.data.COLUMNS] - self.details_table = zip(names,l1,l2) - - #---Public - def AddSelectedToIgnoreList(self): - for dupe in self.selected_dupes: - self.AddToIgnoreList(dupe) - - copy_or_move_marked = demo_method(app.DupeGuru.copy_or_move_marked) - delete_marked = demo_method(app.DupeGuru.delete_marked) - - def ExportToXHTML(self,column_ids,xslt_path,css_path): - columns = [] - for index,column in enumerate(self.data.COLUMNS): - display = column['display'] - enabled = str(index) in column_ids - columns.append((display,enabled)) - xml_path = op.join(self.appdata,'results_export.xml') - self.results.save_to_xml(xml_path,self.data.GetDisplayInfo) - return export.export_to_xhtml(xml_path,xslt_path,css_path,columns) - - def MakeSelectedReference(self): - self.make_reference(self.selected_dupes) - - def OpenSelected(self): - if self.selected_dupes: - path = unicode(self.selected_dupes[0].path) - NSWorkspace.sharedWorkspace().openFile_(path) - - def PurgeIgnoreList(self): - self.scanner.ignore_list.Filter(lambda f,s:op.exists(f) and op.exists(s)) - - def RefreshDetailsWithSelected(self): - if self.selected_dupes: - self.RefreshDetailsTable( - self.selected_dupes[0], - self.results.get_group_of_duplicate(self.selected_dupes[0]) - ) - else: - self.RefreshDetailsTable(None,None) - - def RemoveDirectory(self,index): - try: - del self.directories[index] - except IndexError: - pass - - def RemoveSelected(self): - self.results.remove_duplicates(self.selected_dupes) - - def RenameSelected(self,newname): - try: - d = self.selected_dupes[0] - d = d.move(d.parent,newname) - return True - except (IndexError,fs.FSError),e: - logging.warning("dupeGuru Warning: %s" % str(e)) - return False - - def RevealSelected(self): - if self.selected_dupes: - path = unicode(self.selected_dupes[0].path) - NSWorkspace.sharedWorkspace().selectFile_inFileViewerRootedAtPath_(path,'') - - def start_scanning(self): - self.RefreshDetailsTable(None, None) - try: - app.DupeGuru.start_scanning(self) - return 0 - except app.NoScannableFileError: - return 3 - except app.AllFilesAreRefError: - return 1 - - def SelectResultNodePaths(self,node_paths): - def extract_dupe(t): - g,d = t - if d is not None: - return d - else: - if g is not None: - return g.ref - - selected = [extract_dupe(self.GetObjects(p)) for p in node_paths] - self.selected_dupes = [dupe for dupe in selected if dupe is not None] - - def SelectPowerMarkerNodePaths(self,node_paths): - rows = [p[0] for p in node_paths] - self.selected_dupes = [ - self.results.dupes[row] for row in rows if row in xrange(len(self.results.dupes)) - ] - - def SetDirectoryState(self,node_path,state): - d = self.GetDirectory(node_path) - self.directories.SetState(d.path,state) - - def sort_dupes(self,key,asc): - self.results.sort_dupes(key,asc,self.display_delta_values) - - def sort_groups(self,key,asc): - self.results.sort_groups(key,asc) - - def ToggleSelectedMarkState(self): - for dupe in self.selected_dupes: - self.results.mark_toggle(dupe) - - #---Data - def GetOutlineViewMaxLevel(self, tag): - if tag == 0: - return 2 - elif tag == 1: - return 0 - elif tag == 2: - return 1 - - def GetOutlineViewChildCounts(self, tag, node_path): - if self.progress._job_running: - return [] - if tag == 0: #Normal results - assert not node_path # no other value is possible - return [len(g.dupes) for g in self.results.groups] - elif tag == 1: #Directories - dirs = self.GetDirectory(node_path).dirs if node_path else self.directories - return [d.dircount for d in dirs] - else: #Power Marker - assert not node_path # no other value is possible - return [0 for d in self.results.dupes] - - def GetOutlineViewValues(self, tag, node_path): - if self.progress._job_running: - return - if not node_path: - return - if tag in (0,2): #Normal results / Power Marker - if tag == 0: - g, d = self.GetObjects(node_path) - if d is None: - d = g.ref - else: - d = self.results.dupes[node_path[0]] - g = self.results.get_group_of_duplicate(d) - result = self.data.GetDisplayInfo(d, g, self.display_delta_values) - return result - elif tag == 1: #Directories - d = self.GetDirectory(node_path) - return [ - d.name, - self.directories.GetState(d.path) - ] - - def GetOutlineViewMarked(self, tag, node_path): - # 0=unmarked 1=marked 2=unmarkable - if self.progress._job_running: - return - if not node_path: - return 2 - if tag == 1: #Directories - return 2 - if tag == 0: #Normal results - g, d = self.GetObjects(node_path) - else: #Power Marker - d = self.results.dupes[node_path[0]] - if (d is None) or (not self.results.is_markable(d)): - return 2 - elif self.results.is_marked(d): - return 1 - else: - return 0 - - def GetTableViewCount(self, tag): - if self.progress._job_running: - return 0 - return len(self.details_table) - - def GetTableViewMarkedIndexes(self,tag): - return [] - - def GetTableViewValues(self,tag,row): - return self.details_table[row] - - diff --git a/pe/qt/dupeguru/app_cocoa_test.py b/pe/qt/dupeguru/app_cocoa_test.py deleted file mode 100644 index ad8b937a..00000000 --- a/pe/qt/dupeguru/app_cocoa_test.py +++ /dev/null @@ -1,320 +0,0 @@ -#!/usr/bin/env python -""" -Unit Name: dupeguru.tests.app_cocoa -Created By: Virgil Dupras -Created On: 2006/11/11 -Last modified by:$Author: virgil $ -Last modified on:$Date: 2009-05-29 17:51:41 +0200 (Fri, 29 May 2009) $ - $Revision: 4409 $ -Copyright 2006 Hardcoded Software (http://www.hardcoded.net) -""" -import tempfile -import shutil -import logging - -from hsutil.path import Path -from hsutil.testcase import TestCase -from hsutil.decorators import log_calls -import hsfs.phys -import os.path as op - -from . import engine, data -try: - from .app_cocoa import DupeGuru as DupeGuruBase, DGDirectory -except ImportError: - from nose.plugins.skip import SkipTest - raise SkipTest("These tests can only be run on OS X") -from .results_test import GetTestGroups - -class DupeGuru(DupeGuruBase): - def __init__(self): - DupeGuruBase.__init__(self, data, '/tmp', appid=4) - - def _start_job(self, jobid, func): - func(nulljob) - - -def r2np(rows): - #Transforms a list of rows [1,2,3] into a list of node paths [[1],[2],[3]] - return [[i] for i in rows] - -class TCDupeGuru(TestCase): - def setUp(self): - self.app = DupeGuru() - self.objects,self.matches,self.groups = GetTestGroups() - self.app.results.groups = self.groups - - def test_GetObjects(self): - app = self.app - objects = self.objects - groups = self.groups - g,d = app.GetObjects([0]) - self.assert_(g is groups[0]) - self.assert_(d is None) - g,d = app.GetObjects([0,0]) - self.assert_(g is groups[0]) - self.assert_(d is objects[1]) - g,d = app.GetObjects([1,0]) - self.assert_(g is groups[1]) - self.assert_(d is objects[4]) - - def test_GetObjects_after_sort(self): - app = self.app - objects = self.objects - groups = self.groups[:] #To keep the old order in memory - app.sort_groups(0,False) #0 = Filename - #Now, the group order is supposed to be reversed - g,d = app.GetObjects([0,0]) - self.assert_(g is groups[1]) - self.assert_(d is objects[4]) - - def test_GetObjects_out_of_range(self): - app = self.app - self.assertEqual((None,None),app.GetObjects([2])) - self.assertEqual((None,None),app.GetObjects([])) - self.assertEqual((None,None),app.GetObjects([1,2])) - - def test_selectResultNodePaths(self): - app = self.app - objects = self.objects - app.SelectResultNodePaths([[0,0],[0,1]]) - self.assertEqual(2,len(app.selected_dupes)) - self.assert_(app.selected_dupes[0] is objects[1]) - self.assert_(app.selected_dupes[1] is objects[2]) - - def test_selectResultNodePaths_with_ref(self): - app = self.app - objects = self.objects - app.SelectResultNodePaths([[0,0],[0,1],[1]]) - self.assertEqual(3,len(app.selected_dupes)) - self.assert_(app.selected_dupes[0] is objects[1]) - self.assert_(app.selected_dupes[1] is objects[2]) - self.assert_(app.selected_dupes[2] is self.groups[1].ref) - - def test_selectResultNodePaths_empty(self): - self.app.SelectResultNodePaths([]) - self.assertEqual(0,len(self.app.selected_dupes)) - - def test_selectResultNodePaths_after_sort(self): - app = self.app - objects = self.objects - groups = self.groups[:] #To keep the old order in memory - app.sort_groups(0,False) #0 = Filename - #Now, the group order is supposed to be reversed - app.SelectResultNodePaths([[0,0],[1],[1,0]]) - self.assertEqual(3,len(app.selected_dupes)) - self.assert_(app.selected_dupes[0] is objects[4]) - self.assert_(app.selected_dupes[1] is groups[0].ref) - self.assert_(app.selected_dupes[2] is objects[1]) - - def test_selectResultNodePaths_out_of_range(self): - app = self.app - app.SelectResultNodePaths([[0,0],[0,1],[1],[1,1],[2]]) - self.assertEqual(3,len(app.selected_dupes)) - - def test_selectPowerMarkerRows(self): - app = self.app - objects = self.objects - app.SelectPowerMarkerNodePaths(r2np([0,1,2])) - self.assertEqual(3,len(app.selected_dupes)) - self.assert_(app.selected_dupes[0] is objects[1]) - self.assert_(app.selected_dupes[1] is objects[2]) - self.assert_(app.selected_dupes[2] is objects[4]) - - def test_selectPowerMarkerRows_empty(self): - self.app.SelectPowerMarkerNodePaths([]) - self.assertEqual(0,len(self.app.selected_dupes)) - - def test_selectPowerMarkerRows_after_sort(self): - app = self.app - objects = self.objects - app.sort_dupes(0,False) #0 = Filename - app.SelectPowerMarkerNodePaths(r2np([0,1,2])) - self.assertEqual(3,len(app.selected_dupes)) - self.assert_(app.selected_dupes[0] is objects[4]) - self.assert_(app.selected_dupes[1] is objects[2]) - self.assert_(app.selected_dupes[2] is objects[1]) - - def test_selectPowerMarkerRows_out_of_range(self): - app = self.app - app.SelectPowerMarkerNodePaths(r2np([0,1,2,3])) - self.assertEqual(3,len(app.selected_dupes)) - - def test_toggleSelectedMark(self): - app = self.app - objects = self.objects - app.ToggleSelectedMarkState() - self.assertEqual(0,app.results.mark_count) - app.SelectPowerMarkerNodePaths(r2np([0,2])) - app.ToggleSelectedMarkState() - self.assertEqual(2,app.results.mark_count) - self.assert_(not app.results.is_marked(objects[0])) - self.assert_(app.results.is_marked(objects[1])) - self.assert_(not app.results.is_marked(objects[2])) - self.assert_(not app.results.is_marked(objects[3])) - self.assert_(app.results.is_marked(objects[4])) - - def test_refreshDetailsWithSelected(self): - def mock_refresh(dupe,group): - self.called = True - if self.app.selected_dupes: - self.assert_(dupe is self.app.selected_dupes[0]) - self.assert_(group is self.app.results.get_group_of_duplicate(dupe)) - else: - self.assert_(dupe is None) - self.assert_(group is None) - - self.app.RefreshDetailsTable = mock_refresh - self.called = False - self.app.SelectPowerMarkerNodePaths(r2np([0,2])) - self.app.RefreshDetailsWithSelected() - self.assert_(self.called) - self.called = False - self.app.SelectPowerMarkerNodePaths([]) - self.app.RefreshDetailsWithSelected() - self.assert_(self.called) - - def test_makeSelectedReference(self): - app = self.app - objects = self.objects - groups = self.groups - app.SelectPowerMarkerNodePaths(r2np([0,2])) - app.MakeSelectedReference() - self.assert_(groups[0].ref is objects[1]) - self.assert_(groups[1].ref is objects[4]) - - def test_makeSelectedReference_by_selecting_two_dupes_in_the_same_group(self): - app = self.app - objects = self.objects - groups = self.groups - app.SelectPowerMarkerNodePaths(r2np([0,1,2])) - #Only 0 and 2 must go ref, not 1 because it is a part of the same group - app.MakeSelectedReference() - self.assert_(groups[0].ref is objects[1]) - self.assert_(groups[1].ref is objects[4]) - - def test_removeSelected(self): - app = self.app - app.SelectPowerMarkerNodePaths(r2np([0,2])) - app.RemoveSelected() - self.assertEqual(1,len(app.results.dupes)) - app.RemoveSelected() - self.assertEqual(1,len(app.results.dupes)) - app.SelectPowerMarkerNodePaths(r2np([0,2])) - app.RemoveSelected() - self.assertEqual(0,len(app.results.dupes)) - - def test_addDirectory_simple(self): - app = self.app - self.assertEqual(0,app.AddDirectory(self.datadirpath())) - self.assertEqual(1,len(app.directories)) - - def test_addDirectory_already_there(self): - app = self.app - self.assertEqual(0,app.AddDirectory(self.datadirpath())) - self.assertEqual(1,app.AddDirectory(self.datadirpath())) - - def test_addDirectory_does_not_exist(self): - app = self.app - self.assertEqual(2,app.AddDirectory('/does_not_exist')) - - def test_ignore(self): - app = self.app - app.SelectPowerMarkerNodePaths(r2np([2])) #The dupe of the second, 2 sized group - app.AddSelectedToIgnoreList() - self.assertEqual(1,len(app.scanner.ignore_list)) - app.SelectPowerMarkerNodePaths(r2np([0])) #first dupe of the 3 dupes group - app.AddSelectedToIgnoreList() - #BOTH the ref and the other dupe should have been added - self.assertEqual(3,len(app.scanner.ignore_list)) - - def test_purgeIgnoreList(self): - app = self.app - p1 = self.filepath('zerofile') - p2 = self.filepath('zerofill') - dne = '/does_not_exist' - app.scanner.ignore_list.Ignore(dne,p1) - app.scanner.ignore_list.Ignore(p2,dne) - app.scanner.ignore_list.Ignore(p1,p2) - app.PurgeIgnoreList() - self.assertEqual(1,len(app.scanner.ignore_list)) - self.assert_(app.scanner.ignore_list.AreIgnored(p1,p2)) - self.assert_(not app.scanner.ignore_list.AreIgnored(dne,p1)) - - def test_only_unicode_is_added_to_ignore_list(self): - def FakeIgnore(first,second): - if not isinstance(first,unicode): - self.fail() - if not isinstance(second,unicode): - self.fail() - - app = self.app - app.scanner.ignore_list.Ignore = FakeIgnore - app.SelectPowerMarkerNodePaths(r2np([2])) #The dupe of the second, 2 sized group - app.AddSelectedToIgnoreList() - - def test_dirclass(self): - self.assert_(self.app.directories.dirclass is DGDirectory) - - -class TCDupeGuru_renameSelected(TestCase): - def setUp(self): - p = Path(tempfile.mkdtemp()) - fp = open(str(p + 'foo bar 1'),mode='w') - fp.close() - fp = open(str(p + 'foo bar 2'),mode='w') - fp.close() - fp = open(str(p + 'foo bar 3'),mode='w') - fp.close() - refdir = hsfs.phys.Directory(None,str(p)) - matches = engine.MatchFactory().getmatches(refdir.files) - groups = engine.get_groups(matches) - g = groups[0] - g.prioritize(lambda x:x.name) - app = DupeGuru() - app.results.groups = groups - self.app = app - self.groups = groups - self.p = p - self.refdir = refdir - - def tearDown(self): - shutil.rmtree(str(self.p)) - - def test_simple(self): - app = self.app - refdir = self.refdir - g = self.groups[0] - app.SelectPowerMarkerNodePaths(r2np([0])) - self.assert_(app.RenameSelected('renamed')) - self.assert_('renamed' in refdir) - self.assert_('foo bar 2' not in refdir) - self.assert_(g.dupes[0] is refdir['renamed']) - self.assert_(g.dupes[0] in refdir) - - def test_none_selected(self): - app = self.app - refdir = self.refdir - g = self.groups[0] - app.SelectPowerMarkerNodePaths([]) - self.mock(logging, 'warning', log_calls(lambda msg: None)) - self.assert_(not app.RenameSelected('renamed')) - msg = logging.warning.calls[0]['msg'] - self.assertEqual('dupeGuru Warning: list index out of range', msg) - self.assert_('renamed' not in refdir) - self.assert_('foo bar 2' in refdir) - self.assert_(g.dupes[0] is refdir['foo bar 2']) - - def test_name_already_exists(self): - app = self.app - refdir = self.refdir - g = self.groups[0] - app.SelectPowerMarkerNodePaths(r2np([0])) - self.mock(logging, 'warning', log_calls(lambda msg: None)) - self.assert_(not app.RenameSelected('foo bar 1')) - msg = logging.warning.calls[0]['msg'] - self.assert_(msg.startswith('dupeGuru Warning: \'foo bar 2\' already exists in')) - self.assert_('foo bar 1' in refdir) - self.assert_('foo bar 2' in refdir) - self.assert_(g.dupes[0] is refdir['foo bar 2']) - diff --git a/pe/qt/dupeguru/app_me_cocoa.py b/pe/qt/dupeguru/app_me_cocoa.py deleted file mode 100644 index 51a61767..00000000 --- a/pe/qt/dupeguru/app_me_cocoa.py +++ /dev/null @@ -1,68 +0,0 @@ -#!/usr/bin/env python -""" -Unit Name: dupeguru.app_me_cocoa -Created By: Virgil Dupras -Created On: 2006/11/16 -Last modified by:$Author: virgil $ -Last modified on:$Date: 2009-05-28 16:33:32 +0200 (Thu, 28 May 2009) $ - $Revision: 4392 $ -Copyright 2006 Hardcoded Software (http://www.hardcoded.net) -""" -import os.path as op -import logging -from appscript import app, k, CommandError -import time - -from hsutil.cocoa import as_fetch -import hsfs.phys.music - -import app_cocoa, data_me, scanner - -JOB_REMOVE_DEAD_TRACKS = 'jobRemoveDeadTracks' -JOB_SCAN_DEAD_TRACKS = 'jobScanDeadTracks' - -app_cocoa.JOBID2TITLE.update({ - JOB_REMOVE_DEAD_TRACKS: "Removing dead tracks from your iTunes Library", - JOB_SCAN_DEAD_TRACKS: "Scanning the iTunes Library", -}) - -class DupeGuruME(app_cocoa.DupeGuru): - def __init__(self): - app_cocoa.DupeGuru.__init__(self, data_me, 'dupeguru_me', appid=1) - self.scanner = scanner.ScannerME() - self.directories.dirclass = hsfs.phys.music.Directory - self.dead_tracks = [] - - def remove_dead_tracks(self): - def do(j): - a = app('iTunes') - for index, track in enumerate(j.iter_with_progress(self.dead_tracks)): - if index % 100 == 0: - time.sleep(.1) - try: - track.delete() - except CommandError as e: - logging.warning('Error while trying to remove a track from iTunes: %s' % unicode(e)) - - self._start_job(JOB_REMOVE_DEAD_TRACKS, do) - - def scan_dead_tracks(self): - def do(j): - a = app('iTunes') - try: - [source] = [s for s in a.sources() if s.kind() == k.library] - [library] = source.library_playlists() - except ValueError: - logging.warning('Some unexpected iTunes configuration encountered') - 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() == k.missing_value: - self.dead_tracks.append(track) - logging.info('Found %d dead tracks' % len(self.dead_tracks)) - - self._start_job(JOB_SCAN_DEAD_TRACKS, do) - diff --git a/pe/qt/dupeguru/app_pe_cocoa.py b/pe/qt/dupeguru/app_pe_cocoa.py deleted file mode 100644 index 5969d1c3..00000000 --- a/pe/qt/dupeguru/app_pe_cocoa.py +++ /dev/null @@ -1,212 +0,0 @@ -#!/usr/bin/env python -""" -Unit Name: dupeguru.app_pe_cocoa -Created By: Virgil Dupras -Created On: 2006/11/13 -Last modified by:$Author: virgil $ -Last modified on:$Date: 2009-05-28 16:33:32 +0200 (Thu, 28 May 2009) $ - $Revision: 4392 $ -Copyright 2006 Hardcoded Software (http://www.hardcoded.net) -""" -import os -import os.path as op -import logging -import plistlib - -import objc -from Foundation import * -from AppKit import * -from appscript import app, k - -from hsutil import job, io -import hsfs as fs -from hsfs import phys -from hsutil import files -from hsutil.str import get_file_ext -from hsutil.path import Path -from hsutil.cocoa import as_fetch - -import app_cocoa, data_pe, directories, picture.matchbase -from picture.cache import string_to_colors, Cache - -mainBundle = NSBundle.mainBundle() -PictureBlocks = mainBundle.classNamed_('PictureBlocks') -assert PictureBlocks is not None - -class Photo(phys.File): - cls_info_map = { - 'size': fs.IT_ATTRS, - 'ctime': fs.IT_ATTRS, - 'mtime': fs.IT_ATTRS, - 'md5': fs.IT_MD5, - 'md5partial': fs.IT_MD5, - 'dimensions': fs.IT_EXTRA, - } - - def _initialize_info(self,section): - super(Photo, self)._initialize_info(section) - if section == fs.IT_EXTRA: - self._info.update({ - 'dimensions': (0,0), - }) - - def _read_info(self,section): - super(Photo, self)._read_info(section) - if section == fs.IT_EXTRA: - size = PictureBlocks.getImageSize_(unicode(self.path)) - self._info['dimensions'] = (size.width, size.height) - - def get_blocks(self, block_count_per_side): - try: - blocks = PictureBlocks.getBlocksFromImagePath_blockCount_scanArea_(unicode(self.path), block_count_per_side, 0) - except Exception, e: - raise IOError('The reading of "%s" failed with "%s"' % (unicode(self.path), unicode(e))) - if not blocks: - raise IOError('The picture %s could not be read' % unicode(self.path)) - return string_to_colors(blocks) - - -class IPhoto(Photo): - def __init__(self, parent, whole_path): - super(IPhoto, self).__init__(parent, whole_path[-1]) - self.whole_path = whole_path - - def _build_path(self): - return self.whole_path - - @property - def display_path(self): - return super(IPhoto, self)._build_path() - - -class Directory(phys.Directory): - cls_file_class = Photo - cls_supported_exts = ('png', 'jpg', 'jpeg', 'gif', 'psd', 'bmp', 'tiff', 'nef', 'cr2') - - def _fetch_subitems(self): - subdirs, subfiles = super(Directory,self)._fetch_subitems() - return subdirs, [name for name in subfiles if get_file_ext(name) in self.cls_supported_exts] - - -class IPhotoLibrary(fs.Directory): - def __init__(self, plistpath): - self.plistpath = plistpath - self.refpath = plistpath[:-1] - # the AlbumData.xml file lives right in the library path - super(IPhotoLibrary, self).__init__(None, 'iPhoto Library') - - def _update_photo(self, photo_data): - if photo_data['MediaType'] != 'Image': - return - photo_path = Path(photo_data['ImagePath']) - subpath = photo_path[len(self.refpath):-1] - subdir = self - for element in subpath: - try: - subdir = subdir[element] - except KeyError: - subdir = fs.Directory(subdir, element) - IPhoto(subdir, photo_path) - - def update(self): - self.clear() - s = open(unicode(self.plistpath)).read() - # There was a case where a guy had 0x10 chars in his plist, causing expat errors on loading - s = s.replace('\x10', '') - plist = plistlib.readPlistFromString(s) - for photo_data in plist['Master Image List'].values(): - self._update_photo(photo_data) - - def force_update(self): # Don't update - pass - - -class DupeGuruPE(app_cocoa.DupeGuru): - def __init__(self): - app_cocoa.DupeGuru.__init__(self, data_pe, 'dupeguru_pe', appid=5) - self.scanner.match_factory = picture.matchbase.AsyncMatchFactory() - self.directories.dirclass = Directory - self.directories.special_dirclasses[Path('iPhoto Library')] = lambda _, __: self._create_iphoto_library() - p = op.join(self.appdata, 'cached_pictures.db') - self.scanner.match_factory.cached_blocks = Cache(p) - - def _create_iphoto_library(self): - ud = NSUserDefaults.standardUserDefaults() - prefs = ud.persistentDomainForName_('com.apple.iApps') - plisturl = NSURL.URLWithString_(prefs['iPhotoRecentDatabases'][0]) - plistpath = Path(plisturl.path()) - return IPhotoLibrary(plistpath) - - def _do_delete(self, j): - def op(dupe): - j.add_progress() - return self._do_delete_dupe(dupe) - - marked = [dupe for dupe in self.results.dupes if self.results.is_marked(dupe)] - self.path2iphoto = {} - if any(isinstance(dupe, IPhoto) for dupe in marked): - a = app('iPhoto') - a.select(a.photo_library_album()) - photos = as_fetch(a.photo_library_album().photos, k.item) - for photo in photos: - self.path2iphoto[photo.image_path()] = photo - self.last_op_error_count = self.results.perform_on_marked(op, True) - del self.path2iphoto - - def _do_delete_dupe(self, dupe): - if isinstance(dupe, IPhoto): - photo = self.path2iphoto[unicode(dupe.path)] - app('iPhoto').remove(photo) - return True - else: - return app_cocoa.DupeGuru._do_delete_dupe(self, dupe) - - def _do_load(self, j): - self.directories.LoadFromFile(op.join(self.appdata, 'last_directories.xml')) - for d in self.directories: - if isinstance(d, IPhotoLibrary): - d.update() - self.results.load_from_xml(op.join(self.appdata, 'last_results.xml'), self._get_file, j) - - def _get_file(self, str_path): - p = Path(str_path) - for d in self.directories: - result = None - if p in d.path: - result = d.find_path(p[d.path:]) - if isinstance(d, IPhotoLibrary) and p in d.refpath: - result = d.find_path(p[d.refpath:]) - if result is not None: - return result - - def AddDirectory(self, d): - try: - added = self.directories.add_path(Path(d)) - if d == 'iPhoto Library': - added.update() - return 0 - except directories.AlreadyThereError: - return 1 - - def CopyOrMove(self, dupe, copy, destination, dest_type): - if isinstance(dupe, IPhoto): - copy = True - return app_cocoa.DupeGuru.CopyOrMove(self, dupe, copy, destination, dest_type) - - def start_scanning(self): - for directory in self.directories: - if isinstance(directory, IPhotoLibrary): - self.directories.SetState(directory.refpath, directories.STATE_EXCLUDED) - return app_cocoa.DupeGuru.start_scanning(self) - - 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 - return ref.path - diff --git a/pe/qt/dupeguru/app_se_cocoa.py b/pe/qt/dupeguru/app_se_cocoa.py deleted file mode 100644 index 3d8c62b2..00000000 --- a/pe/qt/dupeguru/app_se_cocoa.py +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env python -# Unit Name: app_se_cocoa -# Created By: Virgil Dupras -# Created On: 2009-05-24 -# $Id$ -# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) - -import app_cocoa, data - -class DupeGuru(app_cocoa.DupeGuru): - def __init__(self): - app_cocoa.DupeGuru.__init__(self, data, 'dupeguru', appid=4) - diff --git a/pe/qt/dupeguru/app_test.py b/pe/qt/dupeguru/app_test.py deleted file mode 100644 index af47067f..00000000 --- a/pe/qt/dupeguru/app_test.py +++ /dev/null @@ -1,137 +0,0 @@ -#!/usr/bin/env python -""" -Unit Name: dupeguru.tests.app -Created By: Virgil Dupras -Created On: 2007-06-23 -Last modified by:$Author: virgil $ -Last modified on:$Date: 2009-05-28 16:02:48 +0200 (Thu, 28 May 2009) $ - $Revision: 4388 $ -Copyright 2007 Hardcoded Software (http://www.hardcoded.net) -""" -import unittest -import os - -from hsutil.testcase import TestCase -from hsutil import io -from hsutil.path import Path -from hsutil.decorators import log_calls -import hsfs as fs -import hsfs.phys -import hsutil.files -from hsutil.job import nulljob - -from . import data, app -from .app import DupeGuru as DupeGuruBase - -class DupeGuru(DupeGuruBase): - def __init__(self): - DupeGuruBase.__init__(self, data, '/tmp', appid=4) - - def _start_job(self, jobid, func): - func(nulljob) - - -class TCDupeGuru(TestCase): - cls_tested_module = app - def test_ApplyFilter_calls_results_apply_filter(self): - app = DupeGuru() - self.mock(app.results, 'apply_filter', log_calls(app.results.apply_filter)) - app.ApplyFilter('foo') - self.assertEqual(2, len(app.results.apply_filter.calls)) - call = app.results.apply_filter.calls[0] - self.assert_(call['filter_str'] is None) - call = app.results.apply_filter.calls[1] - self.assertEqual('foo', call['filter_str']) - - def test_ApplyFilter_escapes_regexp(self): - app = DupeGuru() - self.mock(app.results, 'apply_filter', log_calls(app.results.apply_filter)) - app.ApplyFilter('()[]\\.|+?^abc') - call = app.results.apply_filter.calls[1] - self.assertEqual('\\(\\)\\[\\]\\\\\\.\\|\\+\\?\\^abc', call['filter_str']) - app.ApplyFilter('(*)') # In "simple mode", we want the * to behave as a wilcard - call = app.results.apply_filter.calls[3] - self.assertEqual('\(.*\)', call['filter_str']) - app.options['escape_filter_regexp'] = False - app.ApplyFilter('(abc)') - call = app.results.apply_filter.calls[5] - self.assertEqual('(abc)', call['filter_str']) - - def test_CopyOrMove(self): - # The goal here is just to have a test for a previous blowup I had. I know my test coverage - # for this unit is pathetic. What's done is done. My approach now is to add tests for - # every change I want to make. The blowup was caused by a missing import. - dupe_parent = fs.Directory(None, 'foo') - dupe = fs.File(dupe_parent, 'bar') - dupe.copy = log_calls(lambda dest, newname: None) - self.mock(hsutil.files, 'copy', log_calls(lambda source_path, dest_path: None)) - self.mock(os, 'makedirs', lambda path: None) # We don't want the test to create that fake directory - self.mock(fs.phys, 'Directory', fs.Directory) # We don't want an error because makedirs didn't work - app = DupeGuru() - app.CopyOrMove(dupe, True, 'some_destination', 0) - self.assertEqual(1, len(hsutil.files.copy.calls)) - call = hsutil.files.copy.calls[0] - self.assertEqual('some_destination', call['dest_path']) - self.assertEqual(dupe.path, call['source_path']) - - def test_CopyOrMove_clean_empty_dirs(self): - tmppath = Path(self.tmpdir()) - sourcepath = tmppath + 'source' - io.mkdir(sourcepath) - io.open(sourcepath + 'myfile', 'w') - tmpdir = hsfs.phys.Directory(None, unicode(tmppath)) - myfile = tmpdir['source']['myfile'] - app = DupeGuru() - self.mock(app, 'clean_empty_dirs', log_calls(lambda path: None)) - app.CopyOrMove(myfile, False, tmppath + 'dest', 0) - calls = app.clean_empty_dirs.calls - self.assertEqual(1, len(calls)) - self.assertEqual(sourcepath, calls[0]['path']) - - def test_Scan_with_objects_evaluating_to_false(self): - # At some point, any() was used in a wrong way that made Scan() wrongly return 1 - app = DupeGuru() - f1, f2 = [fs.File(None, 'foo') for i in range(2)] - f1.is_ref, f2.is_ref = (False, False) - assert not (bool(f1) and bool(f2)) - app.directories.get_files = lambda: [f1, f2] - app.directories._dirs.append('this is just so Scan() doesnt return 3') - app.start_scanning() # no exception - - -class TCDupeGuru_clean_empty_dirs(TestCase): - cls_tested_module = app - def setUp(self): - self.mock(hsutil.files, 'delete_if_empty', log_calls(lambda path, files_to_delete=[]: None)) - self.app = DupeGuru() - - def test_option_off(self): - self.app.clean_empty_dirs(Path('/foo/bar')) - self.assertEqual(0, len(hsutil.files.delete_if_empty.calls)) - - def test_option_on(self): - self.app.options['clean_empty_dirs'] = True - self.app.clean_empty_dirs(Path('/foo/bar')) - calls = hsutil.files.delete_if_empty.calls - self.assertEqual(1, len(calls)) - self.assertEqual(Path('/foo/bar'), calls[0]['path']) - self.assertEqual(['.DS_Store'], calls[0]['files_to_delete']) - - def test_recurse_up(self): - # delete_if_empty must be recursively called up in the path until it returns False - @log_calls - def mock_delete_if_empty(path, files_to_delete=[]): - return len(path) > 1 - - self.mock(hsutil.files, 'delete_if_empty', mock_delete_if_empty) - self.app.options['clean_empty_dirs'] = True - self.app.clean_empty_dirs(Path('not-empty/empty/empty')) - calls = hsutil.files.delete_if_empty.calls - self.assertEqual(3, len(calls)) - self.assertEqual(Path('not-empty/empty/empty'), calls[0]['path']) - self.assertEqual(Path('not-empty/empty'), calls[1]['path']) - self.assertEqual(Path('not-empty'), calls[2]['path']) - - -if __name__ == '__main__': - unittest.main() \ No newline at end of file diff --git a/pe/qt/dupeguru/data.py b/pe/qt/dupeguru/data.py deleted file mode 100644 index 568a3400..00000000 --- a/pe/qt/dupeguru/data.py +++ /dev/null @@ -1,105 +0,0 @@ -#!/usr/bin/env python -""" -Unit Name: dupeguru.data -Created By: Virgil Dupras -Created On: 2006/03/15 -Last modified by:$Author: virgil $ -Last modified on:$Date: 2009-05-28 15:22:39 +0200 (Thu, 28 May 2009) $ - $Revision: 4385 $ -Copyright 2004-2006 Hardcoded Software (http://www.hardcoded.net) -""" - -from hsutil.str import format_time, FT_DECIMAL, format_size - -import time - -def format_path(p): - return unicode(p[:-1]) - -def format_timestamp(t, delta): - if delta: - return format_time(t, FT_DECIMAL) - else: - if t > 0: - return time.strftime('%Y/%m/%d %H:%M:%S', time.localtime(t)) - else: - return '---' - -def format_words(w): - def do_format(w): - if isinstance(w, list): - return '(%s)' % ', '.join(do_format(item) for item in w) - else: - return w.replace('\n', ' ') - - return ', '.join(do_format(item) for item in w) - -def format_perc(p): - return "%0.0f" % p - -def format_dupe_count(c): - return str(c) if c else '---' - -def cmp_value(value): - return value.lower() if isinstance(value, basestring) else value - -COLUMNS = [ - {'attr':'name','display':'Filename'}, - {'attr':'path','display':'Directory'}, - {'attr':'size','display':'Size (KB)'}, - {'attr':'extension','display':'Kind'}, - {'attr':'ctime','display':'Creation'}, - {'attr':'mtime','display':'Modification'}, - {'attr':'percentage','display':'Match %'}, - {'attr':'words','display':'Words Used'}, - {'attr':'dupe_count','display':'Dupe Count'}, -] - -def GetDisplayInfo(dupe, group, delta=False): - if (dupe is None) or (group is None): - return ['---'] * len(COLUMNS) - size = dupe.size - ctime = dupe.ctime - mtime = dupe.mtime - m = group.get_match_of(dupe) - if m: - percentage = m.percentage - dupe_count = 0 - if delta: - r = group.ref - size -= r.size - ctime -= r.ctime - mtime -= r.mtime - else: - percentage = group.percentage - dupe_count = len(group.dupes) - return [ - dupe.name, - format_path(dupe.path), - format_size(size, 0, 1, False), - dupe.extension, - format_timestamp(ctime, delta and m), - format_timestamp(mtime, delta and m), - format_perc(percentage), - format_words(dupe.words), - format_dupe_count(dupe_count) - ] - -def GetDupeSortKey(dupe, get_group, key, delta): - if key == 6: - m = get_group().get_match_of(dupe) - return m.percentage - if key == 8: - return 0 - r = cmp_value(getattr(dupe, COLUMNS[key]['attr'])) - if delta and (key in (2, 4, 5)): - r -= cmp_value(getattr(get_group().ref, COLUMNS[key]['attr'])) - return r - -def GetGroupSortKey(group, key): - if key == 6: - return group.percentage - if key == 8: - return len(group) - return cmp_value(getattr(group.ref, COLUMNS[key]['attr'])) - diff --git a/pe/qt/dupeguru/data_me.py b/pe/qt/dupeguru/data_me.py deleted file mode 100644 index 70d3ae66..00000000 --- a/pe/qt/dupeguru/data_me.py +++ /dev/null @@ -1,100 +0,0 @@ -#!/usr/bin/env python -""" -Unit Name: dupeguru.data -Created By: Virgil Dupras -Created On: 2006/03/15 -Last modified by:$Author: virgil $ -Last modified on:$Date: 2009-05-28 15:22:39 +0200 (Thu, 28 May 2009) $ - $Revision: 4385 $ -Copyright 2004-2006 Hardcoded Software (http://www.hardcoded.net) -""" - -from hsutil.str import format_time, FT_MINUTES, format_size -from .data import (format_path, format_timestamp, format_words, format_perc, - format_dupe_count, cmp_value) - -COLUMNS = [ - {'attr':'name','display':'Filename'}, - {'attr':'path','display':'Directory'}, - {'attr':'size','display':'Size (MB)'}, - {'attr':'duration','display':'Time'}, - {'attr':'bitrate','display':'Bitrate'}, - {'attr':'samplerate','display':'Sample Rate'}, - {'attr':'extension','display':'Kind'}, - {'attr':'ctime','display':'Creation'}, - {'attr':'mtime','display':'Modification'}, - {'attr':'title','display':'Title'}, - {'attr':'artist','display':'Artist'}, - {'attr':'album','display':'Album'}, - {'attr':'genre','display':'Genre'}, - {'attr':'year','display':'Year'}, - {'attr':'track','display':'Track Number'}, - {'attr':'comment','display':'Comment'}, - {'attr':'percentage','display':'Match %'}, - {'attr':'words','display':'Words Used'}, - {'attr':'dupe_count','display':'Dupe Count'}, -] - -def GetDisplayInfo(dupe, group, delta=False): - if (dupe is None) or (group is None): - return ['---'] * len(COLUMNS) - size = dupe.size - duration = dupe.duration - bitrate = dupe.bitrate - samplerate = dupe.samplerate - ctime = dupe.ctime - mtime = dupe.mtime - m = group.get_match_of(dupe) - if m: - percentage = m.percentage - dupe_count = 0 - if delta: - r = group.ref - size -= r.size - duration -= r.duration - bitrate -= r.bitrate - samplerate -= r.samplerate - ctime -= r.ctime - mtime -= r.mtime - else: - percentage = group.percentage - dupe_count = len(group.dupes) - return [ - dupe.name, - format_path(dupe.path), - format_size(size, 2, 2, False), - format_time(duration, FT_MINUTES), - str(bitrate), - str(samplerate), - dupe.extension, - format_timestamp(ctime,delta and m), - format_timestamp(mtime,delta and m), - dupe.title, - dupe.artist, - dupe.album, - dupe.genre, - dupe.year, - str(dupe.track), - dupe.comment, - format_perc(percentage), - format_words(dupe.words), - format_dupe_count(dupe_count) - ] - -def GetDupeSortKey(dupe, get_group, key, delta): - if key == 16: - m = get_group().get_match_of(dupe) - return m.percentage - if key == 18: - return 0 - r = cmp_value(getattr(dupe, COLUMNS[key]['attr'])) - if delta and (key in (2, 3, 4, 7, 8)): - r -= cmp_value(getattr(get_group().ref, COLUMNS[key]['attr'])) - return r - -def GetGroupSortKey(group, key): - if key == 16: - return group.percentage - if key == 18: - return len(group) - return cmp_value(getattr(group.ref, COLUMNS[key]['attr'])) diff --git a/pe/qt/dupeguru/data_pe.py b/pe/qt/dupeguru/data_pe.py deleted file mode 100644 index 94bdd99d..00000000 --- a/pe/qt/dupeguru/data_pe.py +++ /dev/null @@ -1,77 +0,0 @@ -#!/usr/bin/env python -""" -Unit Name: dupeguru.data -Created By: Virgil Dupras -Created On: 2006/03/15 -Last modified by:$Author: virgil $ -Last modified on:$Date: 2009-05-28 15:22:39 +0200 (Thu, 28 May 2009) $ - $Revision: 4385 $ -Copyright 2004-2006 Hardcoded Software (http://www.hardcoded.net) -""" -from hsutil.str import format_size -from .data import format_path, format_timestamp, format_perc, format_dupe_count, cmp_value - -def format_dimensions(dimensions): - return '%d x %d' % (dimensions[0], dimensions[1]) - -COLUMNS = [ - {'attr':'name','display':'Filename'}, - {'attr':'path','display':'Directory'}, - {'attr':'size','display':'Size (KB)'}, - {'attr':'extension','display':'Kind'}, - {'attr':'dimensions','display':'Dimensions'}, - {'attr':'ctime','display':'Creation'}, - {'attr':'mtime','display':'Modification'}, - {'attr':'percentage','display':'Match %'}, - {'attr':'dupe_count','display':'Dupe Count'}, -] - -def GetDisplayInfo(dupe,group,delta=False): - if (dupe is None) or (group is None): - return ['---'] * len(COLUMNS) - size = dupe.size - ctime = dupe.ctime - mtime = dupe.mtime - m = group.get_match_of(dupe) - if m: - percentage = m.percentage - dupe_count = 0 - if delta: - r = group.ref - size -= r.size - ctime -= r.ctime - mtime -= r.mtime - else: - percentage = group.percentage - dupe_count = len(group.dupes) - dupe_path = getattr(dupe, 'display_path', dupe.path) - return [ - dupe.name, - format_path(dupe_path), - format_size(size, 0, 1, False), - dupe.extension, - format_dimensions(dupe.dimensions), - format_timestamp(ctime, delta and m), - format_timestamp(mtime, delta and m), - format_perc(percentage), - format_dupe_count(dupe_count) - ] - -def GetDupeSortKey(dupe, get_group, key, delta): - if key == 7: - m = get_group().get_match_of(dupe) - return m.percentage - if key == 8: - return 0 - r = cmp_value(getattr(dupe, COLUMNS[key]['attr'])) - if delta and (key in (2, 5, 6)): - r -= cmp_value(getattr(get_group().ref, COLUMNS[key]['attr'])) - return r - -def GetGroupSortKey(group, key): - if key == 7: - return group.percentage - if key == 8: - return len(group) - return cmp_value(getattr(group.ref, COLUMNS[key]['attr'])) - diff --git a/pe/qt/dupeguru/directories.py b/pe/qt/dupeguru/directories.py deleted file mode 100644 index 3d73b5c5..00000000 --- a/pe/qt/dupeguru/directories.py +++ /dev/null @@ -1,161 +0,0 @@ -#!/usr/bin/env python -""" -Unit Name: dupeguru.directories -Created By: Virgil Dupras -Created On: 2006/02/27 -Last modified by:$Author: virgil $ -Last modified on:$Date: 2009-05-28 16:02:48 +0200 (Thu, 28 May 2009) $ - $Revision: 4388 $ -Copyright 2004-2006 Hardcoded Software (http://www.hardcoded.net) -""" -import xml.dom.minidom - -from hsfs import phys -import hsfs as fs -from hsutil.files import FileOrPath -from hsutil.path import Path - -(STATE_NORMAL, -STATE_REFERENCE, -STATE_EXCLUDED) = range(3) - -class AlreadyThereError(Exception): - """The path being added is already in the directory list""" - -class InvalidPathError(Exception): - """The path being added is invalid""" - -class Directories(object): - #---Override - def __init__(self): - self._dirs = [] - self.states = {} - self.dirclass = phys.Directory - self.special_dirclasses = {} - - def __contains__(self,path): - for d in self._dirs: - if path in d.path: - return True - return False - - def __delitem__(self,key): - self._dirs.__delitem__(key) - - def __getitem__(self,key): - return self._dirs.__getitem__(key) - - def __len__(self): - return len(self._dirs) - - #---Private - def _get_files(self, from_dir, state=STATE_NORMAL): - state = self.states.get(from_dir.path, state) - result = [] - for subdir in from_dir.dirs: - for file in self._get_files(subdir, state): - yield file - if state != STATE_EXCLUDED: - for file in from_dir.files: - file.is_ref = state == STATE_REFERENCE - yield file - - #---Public - def add_path(self, path): - """Adds 'path' to self, if not already there. - - Raises AlreadyThereError if 'path' is already in self. If path is a directory containing - some of the directories already present in self, 'path' will be added, but all directories - under it will be removed. Can also raise InvalidPathError if 'path' does not exist. - """ - if path in self: - raise AlreadyThereError - self._dirs = [d for d in self._dirs if d.path not in path] - try: - dirclass = self.special_dirclasses.get(path, self.dirclass) - d = dirclass(None, unicode(path)) - d[:] #If an InvalidPath exception has to be raised, it will be raised here - self._dirs.append(d) - return d - except fs.InvalidPath: - raise InvalidPathError - - def get_files(self): - """Returns a list of all files that are not excluded. - - Returned files also have their 'is_ref' attr set. - """ - for d in self._dirs: - d.force_update() - try: - for file in self._get_files(d): - yield file - except fs.InvalidPath: - pass - - def GetState(self, path): - """Returns the state of 'path' (One of the STATE_* const.) - - Raises LookupError if 'path' is not in self. - """ - if path not in self: - raise LookupError("The path '%s' is not in the directory list." % str(path)) - try: - return self.states[path] - except KeyError: - if path[-1].startswith('.'): # hidden - return STATE_EXCLUDED - parent = path[:-1] - if parent in self: - return self.GetState(parent) - else: - return STATE_NORMAL - - def LoadFromFile(self,infile): - try: - doc = xml.dom.minidom.parse(infile) - except: - return - root_dir_nodes = doc.getElementsByTagName('root_directory') - for rdn in root_dir_nodes: - if not rdn.getAttributeNode('path'): - continue - path = rdn.getAttributeNode('path').nodeValue - try: - self.add_path(Path(path)) - except (AlreadyThereError,InvalidPathError): - pass - state_nodes = doc.getElementsByTagName('state') - for sn in state_nodes: - if not (sn.getAttributeNode('path') and sn.getAttributeNode('value')): - continue - path = sn.getAttributeNode('path').nodeValue - state = sn.getAttributeNode('value').nodeValue - self.SetState(Path(path), int(state)) - - def Remove(self,directory): - self._dirs.remove(directory) - - def SaveToFile(self,outfile): - with FileOrPath(outfile, 'wb') as fp: - doc = xml.dom.minidom.Document() - root = doc.appendChild(doc.createElement('directories')) - for root_dir in self: - root_dir_node = root.appendChild(doc.createElement('root_directory')) - root_dir_node.setAttribute('path', unicode(root_dir.path).encode('utf-8')) - for path,state in self.states.iteritems(): - state_node = root.appendChild(doc.createElement('state')) - state_node.setAttribute('path', unicode(path).encode('utf-8')) - state_node.setAttribute('value', str(state)) - doc.writexml(fp,'\t','\t','\n',encoding='utf-8') - - def SetState(self,path,state): - try: - if self.GetState(path) == state: - return - self.states[path] = state - if (self.GetState(path[:-1]) == state) and (not path[-1].startswith('.')): - del self.states[path] - except LookupError: - pass - diff --git a/pe/qt/dupeguru/directories_test.py b/pe/qt/dupeguru/directories_test.py deleted file mode 100644 index 7d34c343..00000000 --- a/pe/qt/dupeguru/directories_test.py +++ /dev/null @@ -1,280 +0,0 @@ -#!/usr/bin/env python -""" -Unit Name: dupeguru.tests.directories -Created By: Virgil Dupras -Created On: 2006/02/27 -Last modified by:$Author: virgil $ -Last modified on:$Date: 2009-05-29 08:51:14 +0200 (Fri, 29 May 2009) $ - $Revision: 4398 $ -Copyright 2004-2006 Hardcoded Software (http://www.hardcoded.net) -""" -import unittest -import os.path as op -import os -import time -import shutil - -from hsutil import job, io -from hsutil.path import Path -from hsutil.testcase import TestCase -import hsfs.phys -from hsfs.phys import phys_test - -from directories import * - -testpath = Path(TestCase.datadirpath()) - -class TCDirectories(TestCase): - def test_empty(self): - d = Directories() - self.assertEqual(0,len(d)) - self.assert_('foobar' not in d) - - def test_add_path(self): - d = Directories() - p = testpath + 'utils' - added = d.add_path(p) - self.assertEqual(1,len(d)) - self.assert_(p in d) - self.assert_((p + 'foobar') in d) - self.assert_(p[:-1] not in d) - self.assertEqual(p,added.path) - self.assert_(d[0] is added) - p = self.tmppath() - d.add_path(p) - self.assertEqual(2,len(d)) - self.assert_(p in d) - - def test_AddPath_when_path_is_already_there(self): - d = Directories() - p = testpath + 'utils' - d.add_path(p) - self.assertRaises(AlreadyThereError, d.add_path, p) - self.assertRaises(AlreadyThereError, d.add_path, p + 'foobar') - self.assertEqual(1, len(d)) - - def test_AddPath_containing_paths_already_there(self): - d = Directories() - d.add_path(testpath + 'utils') - self.assertEqual(1, len(d)) - added = d.add_path(testpath) - self.assertEqual(1, len(d)) - self.assert_(added is d[0]) - - def test_AddPath_non_latin(self): - p = Path(self.tmpdir()) - to_add = p + u'unicode\u201a' - os.mkdir(unicode(to_add)) - d = Directories() - try: - d.add_path(to_add) - except UnicodeDecodeError: - self.fail() - - def test_del(self): - d = Directories() - d.add_path(testpath + 'utils') - try: - del d[1] - self.fail() - except IndexError: - pass - d.add_path(self.tmppath()) - del d[1] - self.assertEqual(1, len(d)) - - def test_states(self): - d = Directories() - p = testpath + 'utils' - d.add_path(p) - self.assertEqual(STATE_NORMAL,d.GetState(p)) - d.SetState(p,STATE_REFERENCE) - self.assertEqual(STATE_REFERENCE,d.GetState(p)) - self.assertEqual(STATE_REFERENCE,d.GetState(p + 'dir1')) - self.assertEqual(1,len(d.states)) - self.assertEqual(p,d.states.keys()[0]) - self.assertEqual(STATE_REFERENCE,d.states[p]) - - def test_GetState_with_path_not_there(self): - d = Directories() - d.add_path(testpath + 'utils') - self.assertRaises(LookupError,d.GetState,testpath) - - def test_states_remain_when_larger_directory_eat_smaller_ones(self): - d = Directories() - p = testpath + 'utils' - d.add_path(p) - d.SetState(p,STATE_EXCLUDED) - d.add_path(testpath) - d.SetState(testpath,STATE_REFERENCE) - self.assertEqual(STATE_EXCLUDED,d.GetState(p)) - self.assertEqual(STATE_EXCLUDED,d.GetState(p + 'dir1')) - self.assertEqual(STATE_REFERENCE,d.GetState(testpath)) - - def test_SetState_keep_state_dict_size_to_minimum(self): - d = Directories() - p = Path(phys_test.create_fake_fs(self.tmpdir())) - d.add_path(p) - d.SetState(p,STATE_REFERENCE) - d.SetState(p + 'dir1',STATE_REFERENCE) - self.assertEqual(1,len(d.states)) - self.assertEqual(STATE_REFERENCE,d.GetState(p + 'dir1')) - d.SetState(p + 'dir1',STATE_NORMAL) - self.assertEqual(2,len(d.states)) - self.assertEqual(STATE_NORMAL,d.GetState(p + 'dir1')) - d.SetState(p + 'dir1',STATE_REFERENCE) - self.assertEqual(1,len(d.states)) - self.assertEqual(STATE_REFERENCE,d.GetState(p + 'dir1')) - - def test_get_files(self): - d = Directories() - p = Path(phys_test.create_fake_fs(self.tmpdir())) - d.add_path(p) - d.SetState(p + 'dir1',STATE_REFERENCE) - d.SetState(p + 'dir2',STATE_EXCLUDED) - files = d.get_files() - self.assertEqual(5, len(list(files))) - for f in files: - if f.parent.path == p + 'dir1': - self.assert_(f.is_ref) - else: - self.assert_(not f.is_ref) - - def test_get_files_with_inherited_exclusion(self): - d = Directories() - p = testpath + 'utils' - d.add_path(p) - d.SetState(p,STATE_EXCLUDED) - self.assertEqual([], list(d.get_files())) - - def test_save_and_load(self): - d1 = Directories() - d2 = Directories() - p1 = self.tmppath() - p2 = self.tmppath() - d1.add_path(p1) - d1.add_path(p2) - d1.SetState(p1, STATE_REFERENCE) - d1.SetState(p1 + 'dir1',STATE_EXCLUDED) - tmpxml = op.join(self.tmpdir(), 'directories_testunit.xml') - d1.SaveToFile(tmpxml) - d2.LoadFromFile(tmpxml) - self.assertEqual(2, len(d2)) - self.assertEqual(STATE_REFERENCE,d2.GetState(p1)) - self.assertEqual(STATE_EXCLUDED,d2.GetState(p1 + 'dir1')) - - def test_invalid_path(self): - d = Directories() - p = Path('does_not_exist') - self.assertRaises(InvalidPathError, d.add_path, p) - self.assertEqual(0, len(d)) - - def test_SetState_on_invalid_path(self): - d = Directories() - try: - d.SetState(Path('foobar',),STATE_NORMAL) - except LookupError: - self.fail() - - def test_default_dirclass(self): - self.assert_(Directories().dirclass is hsfs.phys.Directory) - - def test_dirclass(self): - class MySpecialDirclass(hsfs.phys.Directory): pass - d = Directories() - d.dirclass = MySpecialDirclass - d.add_path(testpath) - self.assert_(isinstance(d[0], MySpecialDirclass)) - - def test_LoadFromFile_with_invalid_path(self): - #This test simulates a load from file resulting in a - #InvalidPath raise. Other directories must be loaded. - d1 = Directories() - d1.add_path(testpath + 'utils') - #Will raise InvalidPath upon loading - d1.add_path(self.tmppath()).name = 'does_not_exist' - tmpxml = op.join(self.tmpdir(), 'directories_testunit.xml') - d1.SaveToFile(tmpxml) - d2 = Directories() - d2.LoadFromFile(tmpxml) - self.assertEqual(1, len(d2)) - - def test_LoadFromFile_with_same_paths(self): - #This test simulates a load from file resulting in a - #AlreadyExists raise. Other directories must be loaded. - d1 = Directories() - p1 = self.tmppath() - p2 = self.tmppath() - d1.add_path(p1) - d1.add_path(p2) - #Will raise AlreadyExists upon loading - d1.add_path(self.tmppath()).name = unicode(p1) - tmpxml = op.join(self.tmpdir(), 'directories_testunit.xml') - d1.SaveToFile(tmpxml) - d2 = Directories() - d2.LoadFromFile(tmpxml) - self.assertEqual(2, len(d2)) - - def test_Remove(self): - d = Directories() - d1 = d.add_path(self.tmppath()) - d2 = d.add_path(self.tmppath()) - d.Remove(d1) - self.assertEqual(1, len(d)) - self.assert_(d[0] is d2) - - def test_unicode_save(self): - d = Directories() - p1 = self.tmppath() + u'hello\xe9' - io.mkdir(p1) - io.mkdir(p1 + u'foo\xe9') - d.add_path(p1) - d.SetState(d[0][0].path, STATE_EXCLUDED) - tmpxml = op.join(self.tmpdir(), 'directories_testunit.xml') - try: - d.SaveToFile(tmpxml) - except UnicodeDecodeError: - self.fail() - - def test_get_files_refreshes_its_directories(self): - d = Directories() - p = Path(phys_test.create_fake_fs(self.tmpdir())) - d.add_path(p) - files = d.get_files() - self.assertEqual(6, len(list(files))) - time.sleep(1) - os.remove(str(p + ('dir1','file1.test'))) - files = d.get_files() - self.assertEqual(5, len(list(files))) - - def test_get_files_does_not_choke_on_non_existing_directories(self): - d = Directories() - p = Path(self.tmpdir()) - d.add_path(p) - io.rmtree(p) - self.assertEqual([], list(d.get_files())) - - def test_GetState_returns_excluded_by_default_for_hidden_directories(self): - d = Directories() - p = Path(self.tmpdir()) - hidden_dir_path = p + '.foo' - io.mkdir(p + '.foo') - d.add_path(p) - self.assertEqual(d.GetState(hidden_dir_path), STATE_EXCLUDED) - # But it can be overriden - d.SetState(hidden_dir_path, STATE_NORMAL) - self.assertEqual(d.GetState(hidden_dir_path), STATE_NORMAL) - - def test_special_dirclasses(self): - # if a path is in special_dirclasses, use this class instead - class MySpecialDirclass(hsfs.phys.Directory): pass - d = Directories() - p1 = self.tmppath() - p2 = self.tmppath() - d.special_dirclasses[p1] = MySpecialDirclass - self.assert_(isinstance(d.add_path(p2), hsfs.phys.Directory)) - self.assert_(isinstance(d.add_path(p1), MySpecialDirclass)) - - -if __name__ == "__main__": - unittest.main() \ No newline at end of file diff --git a/pe/qt/dupeguru/engine.py b/pe/qt/dupeguru/engine.py deleted file mode 100644 index a826902d..00000000 --- a/pe/qt/dupeguru/engine.py +++ /dev/null @@ -1,360 +0,0 @@ -#!/usr/bin/env python -""" -Unit Name: dupeguru.engine -Created By: Virgil Dupras -Created On: 2006/01/29 -Last modified by:$Author: virgil $ -Last modified on:$Date: $ - $Revision: $ -Copyright 2007 Hardcoded Software (http://www.hardcoded.net) -""" -from __future__ import division -import difflib -import logging -import string -from collections import defaultdict, namedtuple -from unicodedata import normalize - -from hsutil.str import multi_replace -from hsutil import job - -(WEIGHT_WORDS, -MATCH_SIMILAR_WORDS, -NO_FIELD_ORDER) = range(3) - -JOB_REFRESH_RATE = 100 - -def getwords(s): - if isinstance(s, unicode): - s = normalize('NFD', s) - s = multi_replace(s, "-_&+():;\\[]{}.,<>/?~!@#$*", ' ').lower() - s = ''.join(c for c in s if c in string.ascii_letters + string.digits + string.whitespace) - return filter(None, s.split(' ')) # filter() is to remove empty elements - -def getfields(s): - fields = [getwords(field) for field in s.split(' - ')] - return filter(None, fields) - -def unpack_fields(fields): - result = [] - for field in fields: - if isinstance(field, list): - result += field - else: - result.append(field) - return result - -def compare(first, second, flags=()): - """Returns the % of words that match between first and second - - The result is a int in the range 0..100. - First and second can be either a string or a list. - """ - if not (first and second): - return 0 - if any(isinstance(element, list) for element in first): - return compare_fields(first, second, flags) - second = second[:] #We must use a copy of second because we remove items from it - match_similar = MATCH_SIMILAR_WORDS in flags - weight_words = WEIGHT_WORDS in flags - joined = first + second - total_count = (sum(len(word) for word in joined) if weight_words else len(joined)) - match_count = 0 - in_order = True - for word in first: - if match_similar and (word not in second): - similar = difflib.get_close_matches(word, second, 1, 0.8) - if similar: - word = similar[0] - if word in second: - if second[0] != word: - in_order = False - second.remove(word) - match_count += (len(word) if weight_words else 1) - result = round(((match_count * 2) / total_count) * 100) - if (result == 100) and (not in_order): - result = 99 # We cannot consider a match exact unless the ordering is the same - return result - -def compare_fields(first, second, flags=()): - """Returns the score for the lowest matching fields. - - first and second must be lists of lists of string. - """ - if len(first) != len(second): - return 0 - if NO_FIELD_ORDER in flags: - results = [] - #We don't want to remove field directly in the list. We must work on a copy. - second = second[:] - for field1 in first: - max = 0 - matched_field = None - for field2 in second: - r = compare(field1, field2, flags) - if r > max: - max = r - matched_field = field2 - results.append(max) - if matched_field: - second.remove(matched_field) - else: - results = [compare(word1, word2, flags) for word1, word2 in zip(first, second)] - return min(results) if results else 0 - -def build_word_dict(objects, j=job.nulljob): - """Returns a dict of objects mapped by their words. - - objects must have a 'words' attribute being a list of strings or a list of lists of strings. - - The result will be a dict with words as keys, lists of objects as values. - """ - result = defaultdict(set) - for object in j.iter_with_progress(objects, 'Prepared %d/%d files', JOB_REFRESH_RATE): - for word in unpack_fields(object.words): - result[word].add(object) - return result - -def merge_similar_words(word_dict): - """Take all keys in word_dict that are similar, and merge them together. - """ - keys = word_dict.keys() - keys.sort(key=len)# we want the shortest word to stay - while keys: - key = keys.pop(0) - similars = difflib.get_close_matches(key, keys, 100, 0.8) - if not similars: - continue - objects = word_dict[key] - for similar in similars: - objects |= word_dict[similar] - del word_dict[similar] - keys.remove(similar) - -def reduce_common_words(word_dict, threshold): - """Remove all objects from word_dict values where the object count >= threshold - - The exception to this removal are the objects where all the words of the object are common. - Because if we remove them, we will miss some duplicates! - """ - uncommon_words = set(word for word, objects in word_dict.items() if len(objects) < threshold) - for word, objects in word_dict.items(): - if len(objects) < threshold: - continue - reduced = set() - for o in objects: - if not any(w in uncommon_words for w in unpack_fields(o.words)): - reduced.add(o) - if reduced: - word_dict[word] = reduced - else: - del word_dict[word] - -Match = namedtuple('Match', 'first second percentage') -def get_match(first, second, flags=()): - #it is assumed here that first and second both have a "words" attribute - percentage = compare(first.words, second.words, flags) - return Match(first, second, percentage) - -class MatchFactory(object): - common_word_threshold = 50 - match_similar_words = False - min_match_percentage = 0 - weight_words = False - no_field_order = False - limit = 5000000 - - def getmatches(self, objects, j=job.nulljob): - j = j.start_subjob(2) - sj = j.start_subjob(2) - for o in objects: - if not hasattr(o, 'words'): - o.words = getwords(o.name) - word_dict = build_word_dict(objects, sj) - reduce_common_words(word_dict, self.common_word_threshold) - if self.match_similar_words: - merge_similar_words(word_dict) - match_flags = [] - if self.weight_words: - match_flags.append(WEIGHT_WORDS) - if self.match_similar_words: - match_flags.append(MATCH_SIMILAR_WORDS) - if self.no_field_order: - match_flags.append(NO_FIELD_ORDER) - j.start_job(len(word_dict), '0 matches found') - compared = defaultdict(set) - result = [] - try: - # This whole 'popping' thing is there to avoid taking too much memory at the same time. - while word_dict: - items = word_dict.popitem()[1] - while items: - ref = items.pop() - compared_already = compared[ref] - to_compare = items - compared_already - compared_already |= to_compare - for other in to_compare: - m = get_match(ref, other, match_flags) - if m.percentage >= self.min_match_percentage: - result.append(m) - if len(result) >= self.limit: - return result - j.add_progress(desc='%d matches found' % len(result)) - except MemoryError: - # This is the place where the memory usage is at its peak during the scan. - # Just continue the process with an incomplete list of matches. - del compared # This should give us enough room to call logging. - logging.warning('Memory Overflow. Matches: %d. Word dict: %d' % (len(result), len(word_dict))) - return result - return result - - -class Group(object): - #---Override - def __init__(self): - self._clear() - - def __contains__(self, item): - return item in self.unordered - - def __getitem__(self, key): - return self.ordered.__getitem__(key) - - def __iter__(self): - return iter(self.ordered) - - def __len__(self): - return len(self.ordered) - - #---Private - def _clear(self): - self._percentage = None - self._matches_for_ref = None - self.matches = set() - self.candidates = defaultdict(set) - self.ordered = [] - self.unordered = set() - - def _get_matches_for_ref(self): - if self._matches_for_ref is None: - ref = self.ref - self._matches_for_ref = [match for match in self.matches if ref in match] - return self._matches_for_ref - - #---Public - def add_match(self, match): - def add_candidate(item, match): - matches = self.candidates[item] - matches.add(match) - if self.unordered <= matches: - self.ordered.append(item) - self.unordered.add(item) - - if match in self.matches: - return - self.matches.add(match) - first, second, _ = match - if first not in self.unordered: - add_candidate(first, second) - if second not in self.unordered: - add_candidate(second, first) - self._percentage = None - self._matches_for_ref = None - - def clean_matches(self): - self.matches = set(m for m in self.matches if (m.first in self.unordered) and (m.second in self.unordered)) - self.candidates = defaultdict(set) - - def get_match_of(self, item): - if item is self.ref: - return - for m in self._get_matches_for_ref(): - if item in m: - return m - - def prioritize(self, key_func, tie_breaker=None): - # tie_breaker(ref, dupe) --> True if dupe should be ref - self.ordered.sort(key=key_func) - if tie_breaker is None: - return - ref = self.ref - key_value = key_func(ref) - for dupe in self.dupes: - if key_func(dupe) != key_value: - break - if tie_breaker(ref, dupe): - ref = dupe - if ref is not self.ref: - self.switch_ref(ref) - - def remove_dupe(self, item, clean_matches=True): - try: - self.ordered.remove(item) - self.unordered.remove(item) - self._percentage = None - self._matches_for_ref = None - if (len(self) > 1) and any(not getattr(item, 'is_ref', False) for item in self): - if clean_matches: - self.matches = set(m for m in self.matches if item not in m) - else: - self._clear() - except ValueError: - pass - - def switch_ref(self, with_dupe): - try: - self.ordered.remove(with_dupe) - self.ordered.insert(0, with_dupe) - self._percentage = None - self._matches_for_ref = None - except ValueError: - pass - - dupes = property(lambda self: self[1:]) - - @property - def percentage(self): - if self._percentage is None: - if self.dupes: - matches = self._get_matches_for_ref() - self._percentage = sum(match.percentage for match in matches) // len(matches) - else: - self._percentage = 0 - return self._percentage - - @property - def ref(self): - if self: - return self[0] - - -def get_groups(matches, j=job.nulljob): - matches.sort(key=lambda match: -match.percentage) - dupe2group = {} - groups = [] - for match in j.iter_with_progress(matches, 'Grouped %d/%d matches', JOB_REFRESH_RATE): - first, second, _ = match - first_group = dupe2group.get(first) - second_group = dupe2group.get(second) - if first_group: - if second_group: - if first_group is second_group: - target_group = first_group - else: - continue - else: - target_group = first_group - dupe2group[second] = target_group - else: - if second_group: - target_group = second_group - dupe2group[first] = target_group - else: - target_group = Group() - groups.append(target_group) - dupe2group[first] = target_group - dupe2group[second] = target_group - target_group.add_match(match) - for group in groups: - group.clean_matches() - return groups diff --git a/pe/qt/dupeguru/engine_test.py b/pe/qt/dupeguru/engine_test.py deleted file mode 100644 index 8e9706d9..00000000 --- a/pe/qt/dupeguru/engine_test.py +++ /dev/null @@ -1,822 +0,0 @@ -#!/usr/bin/env python -""" -Unit Name: dupeguru.engine_test -Created By: Virgil Dupras -Created On: 2006/01/29 -Last modified by:$Author: virgil $ -Last modified on:$Date: $ - $Revision: $ -Copyright 2004-2008 Hardcoded Software (http://www.hardcoded.net) -""" -import unittest -import sys - -from hsutil import job -from hsutil.decorators import log_calls -from hsutil.testcase import TestCase - -from . import engine -from .engine import * - -class NamedObject(object): - def __init__(self, name="foobar", with_words=False): - self.name = name - if with_words: - self.words = getwords(name) - - -def get_match_triangle(): - o1 = NamedObject(with_words=True) - o2 = NamedObject(with_words=True) - o3 = NamedObject(with_words=True) - m1 = get_match(o1,o2) - m2 = get_match(o1,o3) - m3 = get_match(o2,o3) - return [m1, m2, m3] - -def get_test_group(): - m1, m2, m3 = get_match_triangle() - result = Group() - result.add_match(m1) - result.add_match(m2) - result.add_match(m3) - return result - -class TCgetwords(TestCase): - def test_spaces(self): - self.assertEqual(['a', 'b', 'c', 'd'], getwords("a b c d")) - self.assertEqual(['a', 'b', 'c', 'd'], getwords(" a b c d ")) - - def test_splitter_chars(self): - self.assertEqual( - [chr(i) for i in xrange(ord('a'),ord('z')+1)], - getwords("a-b_c&d+e(f)g;h\\i[j]k{l}m:n.o,pr/s?t~u!v@w#x$y*z") - ) - - def test_joiner_chars(self): - self.assertEqual(["aec"], getwords(u"a'e\u0301c")) - - def test_empty(self): - self.assertEqual([], getwords('')) - - def test_returns_lowercase(self): - self.assertEqual(['foo', 'bar'], getwords('FOO BAR')) - - def test_decompose_unicode(self): - self.assertEqual(getwords(u'foo\xe9bar'), ['fooebar']) - - -class TCgetfields(TestCase): - def test_simple(self): - self.assertEqual([['a', 'b'], ['c', 'd', 'e']], getfields('a b - c d e')) - - def test_empty(self): - self.assertEqual([], getfields('')) - - def test_cleans_empty_fields(self): - expected = [['a', 'bc', 'def']] - actual = getfields(' - a bc def') - self.assertEqual(expected, actual) - expected = [['bc', 'def']] - - -class TCunpack_fields(TestCase): - def test_with_fields(self): - expected = ['a', 'b', 'c', 'd', 'e', 'f'] - actual = unpack_fields([['a'], ['b', 'c'], ['d', 'e', 'f']]) - self.assertEqual(expected, actual) - - def test_without_fields(self): - expected = ['a', 'b', 'c', 'd', 'e', 'f'] - actual = unpack_fields(['a', 'b', 'c', 'd', 'e', 'f']) - self.assertEqual(expected, actual) - - def test_empty(self): - self.assertEqual([], unpack_fields([])) - - -class TCWordCompare(TestCase): - def test_list(self): - self.assertEqual(100, compare(['a', 'b', 'c', 'd'],['a', 'b', 'c', 'd'])) - self.assertEqual(86, compare(['a', 'b', 'c', 'd'],['a', 'b', 'c'])) - - def test_unordered(self): - #Sometimes, users don't want fuzzy matching too much When they set the slider - #to 100, they don't expect a filename with the same words, but not the same order, to match. - #Thus, we want to return 99 in that case. - self.assertEqual(99, compare(['a', 'b', 'c', 'd'], ['d', 'b', 'c', 'a'])) - - def test_word_occurs_twice(self): - #if a word occurs twice in first, but once in second, we want the word to be only counted once - self.assertEqual(89, compare(['a', 'b', 'c', 'd', 'a'], ['d', 'b', 'c', 'a'])) - - def test_uses_copy_of_lists(self): - first = ['foo', 'bar'] - second = ['bar', 'bleh'] - compare(first, second) - self.assertEqual(['foo', 'bar'], first) - self.assertEqual(['bar', 'bleh'], second) - - def test_word_weight(self): - self.assertEqual(int((6.0 / 13.0) * 100), compare(['foo', 'bar'], ['bar', 'bleh'], (WEIGHT_WORDS, ))) - - def test_similar_words(self): - self.assertEqual(100, compare(['the', 'white', 'stripes'],['the', 'whites', 'stripe'], (MATCH_SIMILAR_WORDS, ))) - - def test_empty(self): - self.assertEqual(0, compare([], [])) - - def test_with_fields(self): - self.assertEqual(67, compare([['a', 'b'], ['c', 'd', 'e']], [['a', 'b'], ['c', 'd', 'f']])) - - def test_propagate_flags_with_fields(self): - def mock_compare(first, second, flags): - self.assertEqual((0, 1, 2, 3, 5), flags) - - self.mock(engine, 'compare_fields', mock_compare) - compare([['a']], [['a']], (0, 1, 2, 3, 5)) - - -class TCWordCompareWithFields(TestCase): - def test_simple(self): - self.assertEqual(67, compare_fields([['a', 'b'], ['c', 'd', 'e']], [['a', 'b'], ['c', 'd', 'f']])) - - def test_empty(self): - self.assertEqual(0, compare_fields([], [])) - - def test_different_length(self): - self.assertEqual(0, compare_fields([['a'], ['b']], [['a'], ['b'], ['c']])) - - def test_propagates_flags(self): - def mock_compare(first, second, flags): - self.assertEqual((0, 1, 2, 3, 5), flags) - - self.mock(engine, 'compare_fields', mock_compare) - compare_fields([['a']], [['a']],(0, 1, 2, 3, 5)) - - def test_order(self): - first = [['a', 'b'], ['c', 'd', 'e']] - second = [['c', 'd', 'f'], ['a', 'b']] - self.assertEqual(0, compare_fields(first, second)) - - def test_no_order(self): - first = [['a','b'],['c','d','e']] - second = [['c','d','f'],['a','b']] - self.assertEqual(67, compare_fields(first, second, (NO_FIELD_ORDER, ))) - first = [['a','b'],['a','b']] #a field can only be matched once. - second = [['c','d','f'],['a','b']] - self.assertEqual(0, compare_fields(first, second, (NO_FIELD_ORDER, ))) - first = [['a','b'],['a','b','c']] - second = [['c','d','f'],['a','b']] - self.assertEqual(33, compare_fields(first, second, (NO_FIELD_ORDER, ))) - - def test_compare_fields_without_order_doesnt_alter_fields(self): - #The NO_ORDER comp type altered the fields! - first = [['a','b'],['c','d','e']] - second = [['c','d','f'],['a','b']] - self.assertEqual(67, compare_fields(first, second, (NO_FIELD_ORDER, ))) - self.assertEqual([['a','b'],['c','d','e']],first) - self.assertEqual([['c','d','f'],['a','b']],second) - - -class TCbuild_word_dict(TestCase): - def test_with_standard_words(self): - l = [NamedObject('foo bar',True)] - l.append(NamedObject('bar baz',True)) - l.append(NamedObject('baz bleh foo',True)) - d = build_word_dict(l) - self.assertEqual(4,len(d)) - self.assertEqual(2,len(d['foo'])) - self.assert_(l[0] in d['foo']) - self.assert_(l[2] in d['foo']) - self.assertEqual(2,len(d['bar'])) - self.assert_(l[0] in d['bar']) - self.assert_(l[1] in d['bar']) - self.assertEqual(2,len(d['baz'])) - self.assert_(l[1] in d['baz']) - self.assert_(l[2] in d['baz']) - self.assertEqual(1,len(d['bleh'])) - self.assert_(l[2] in d['bleh']) - - def test_unpack_fields(self): - o = NamedObject('') - o.words = [['foo','bar'],['baz']] - d = build_word_dict([o]) - self.assertEqual(3,len(d)) - self.assertEqual(1,len(d['foo'])) - - def test_words_are_unaltered(self): - o = NamedObject('') - o.words = [['foo','bar'],['baz']] - d = build_word_dict([o]) - self.assertEqual([['foo','bar'],['baz']],o.words) - - def test_object_instances_can_only_be_once_in_words_object_list(self): - o = NamedObject('foo foo',True) - d = build_word_dict([o]) - self.assertEqual(1,len(d['foo'])) - - def test_job(self): - def do_progress(p,d=''): - self.log.append(p) - return True - - j = job.Job(1,do_progress) - self.log = [] - s = "foo bar" - build_word_dict([NamedObject(s, True), NamedObject(s, True), NamedObject(s, True)], j) - self.assertEqual(0,self.log[0]) - self.assertEqual(33,self.log[1]) - self.assertEqual(66,self.log[2]) - self.assertEqual(100,self.log[3]) - - -class TCmerge_similar_words(TestCase): - def test_some_similar_words(self): - d = { - 'foobar':set([1]), - 'foobar1':set([2]), - 'foobar2':set([3]), - } - merge_similar_words(d) - self.assertEqual(1,len(d)) - self.assertEqual(3,len(d['foobar'])) - - - -class TCreduce_common_words(TestCase): - def test_typical(self): - d = { - 'foo': set([NamedObject('foo bar',True) for i in range(50)]), - 'bar': set([NamedObject('foo bar',True) for i in range(49)]) - } - reduce_common_words(d, 50) - self.assert_('foo' not in d) - self.assertEqual(49,len(d['bar'])) - - def test_dont_remove_objects_with_only_common_words(self): - d = { - 'common': set([NamedObject("common uncommon",True) for i in range(50)] + [NamedObject("common",True)]), - 'uncommon': set([NamedObject("common uncommon",True)]) - } - reduce_common_words(d, 50) - self.assertEqual(1,len(d['common'])) - self.assertEqual(1,len(d['uncommon'])) - - def test_values_still_are_set_instances(self): - d = { - 'common': set([NamedObject("common uncommon",True) for i in range(50)] + [NamedObject("common",True)]), - 'uncommon': set([NamedObject("common uncommon",True)]) - } - reduce_common_words(d, 50) - self.assert_(isinstance(d['common'],set)) - self.assert_(isinstance(d['uncommon'],set)) - - def test_dont_raise_KeyError_when_a_word_has_been_removed(self): - #If a word has been removed by the reduce, an object in a subsequent common word that - #contains the word that has been removed would cause a KeyError. - d = { - 'foo': set([NamedObject('foo bar baz',True) for i in range(50)]), - 'bar': set([NamedObject('foo bar baz',True) for i in range(50)]), - 'baz': set([NamedObject('foo bar baz',True) for i in range(49)]) - } - try: - reduce_common_words(d, 50) - except KeyError: - self.fail() - - def test_unpack_fields(self): - #object.words may be fields. - def create_it(): - o = NamedObject('') - o.words = [['foo','bar'],['baz']] - return o - - d = { - 'foo': set([create_it() for i in range(50)]) - } - try: - reduce_common_words(d, 50) - except TypeError: - self.fail("must support fields.") - - def test_consider_a_reduced_common_word_common_even_after_reduction(self): - #There was a bug in the code that causeda word that has already been reduced not to - #be counted as a common word for subsequent words. For example, if 'foo' is processed - #as a common word, keeping a "foo bar" file in it, and the 'bar' is processed, "foo bar" - #would not stay in 'bar' because 'foo' is not a common word anymore. - only_common = NamedObject('foo bar',True) - d = { - 'foo': set([NamedObject('foo bar baz',True) for i in range(49)] + [only_common]), - 'bar': set([NamedObject('foo bar baz',True) for i in range(49)] + [only_common]), - 'baz': set([NamedObject('foo bar baz',True) for i in range(49)]) - } - reduce_common_words(d, 50) - self.assertEqual(1,len(d['foo'])) - self.assertEqual(1,len(d['bar'])) - self.assertEqual(49,len(d['baz'])) - - -class TCget_match(TestCase): - def test_simple(self): - o1 = NamedObject("foo bar",True) - o2 = NamedObject("bar bleh",True) - m = get_match(o1,o2) - self.assertEqual(50,m.percentage) - self.assertEqual(['foo','bar'],m.first.words) - self.assertEqual(['bar','bleh'],m.second.words) - self.assert_(m.first is o1) - self.assert_(m.second is o2) - - def test_in(self): - o1 = NamedObject("foo",True) - o2 = NamedObject("bar",True) - m = get_match(o1,o2) - self.assert_(o1 in m) - self.assert_(o2 in m) - self.assert_(object() not in m) - - def test_word_weight(self): - self.assertEqual(int((6.0 / 13.0) * 100),get_match(NamedObject("foo bar",True),NamedObject("bar bleh",True),(WEIGHT_WORDS,)).percentage) - - -class TCMatchFactory(TestCase): - def test_empty(self): - self.assertEqual([],MatchFactory().getmatches([])) - - def test_defaults(self): - mf = MatchFactory() - self.assertEqual(50,mf.common_word_threshold) - self.assertEqual(False,mf.weight_words) - self.assertEqual(False,mf.match_similar_words) - self.assertEqual(False,mf.no_field_order) - self.assertEqual(0,mf.min_match_percentage) - - def test_simple(self): - l = [NamedObject("foo bar"),NamedObject("bar bleh"),NamedObject("a b c foo")] - r = MatchFactory().getmatches(l) - self.assertEqual(2,len(r)) - seek = [m for m in r if m.percentage == 50] #"foo bar" and "bar bleh" - m = seek[0] - self.assertEqual(['foo','bar'],m.first.words) - self.assertEqual(['bar','bleh'],m.second.words) - seek = [m for m in r if m.percentage == 33] #"foo bar" and "a b c foo" - m = seek[0] - self.assertEqual(['foo','bar'],m.first.words) - self.assertEqual(['a','b','c','foo'],m.second.words) - - def test_null_and_unrelated_objects(self): - l = [NamedObject("foo bar"),NamedObject("bar bleh"),NamedObject(""),NamedObject("unrelated object")] - r = MatchFactory().getmatches(l) - self.assertEqual(1,len(r)) - m = r[0] - self.assertEqual(50,m.percentage) - self.assertEqual(['foo','bar'],m.first.words) - self.assertEqual(['bar','bleh'],m.second.words) - - def test_twice_the_same_word(self): - l = [NamedObject("foo foo bar"),NamedObject("bar bleh")] - r = MatchFactory().getmatches(l) - self.assertEqual(1,len(r)) - - def test_twice_the_same_word_when_preworded(self): - l = [NamedObject("foo foo bar",True),NamedObject("bar bleh",True)] - r = MatchFactory().getmatches(l) - self.assertEqual(1,len(r)) - - def test_two_words_match(self): - l = [NamedObject("foo bar"),NamedObject("foo bar bleh")] - r = MatchFactory().getmatches(l) - self.assertEqual(1,len(r)) - - def test_match_files_with_only_common_words(self): - #If a word occurs more than 50 times, it is excluded from the matching process - #The problem with the common_word_threshold is that the files containing only common - #words will never be matched together. We *should* match them. - mf = MatchFactory() - mf.common_word_threshold = 50 - l = [NamedObject("foo") for i in range(50)] - r = mf.getmatches(l) - self.assertEqual(1225,len(r)) - - def test_use_words_already_there_if_there(self): - o1 = NamedObject('foo') - o2 = NamedObject('bar') - o2.words = ['foo'] - self.assertEqual(1,len(MatchFactory().getmatches([o1,o2]))) - - def test_job(self): - def do_progress(p,d=''): - self.log.append(p) - return True - - j = job.Job(1,do_progress) - self.log = [] - s = "foo bar" - MatchFactory().getmatches([NamedObject(s),NamedObject(s),NamedObject(s)],j) - self.assert_(len(self.log) > 2) - self.assertEqual(0,self.log[0]) - self.assertEqual(100,self.log[-1]) - - def test_weight_words(self): - mf = MatchFactory() - mf.weight_words = True - l = [NamedObject("foo bar"),NamedObject("bar bleh")] - m = mf.getmatches(l)[0] - self.assertEqual(int((6.0 / 13.0) * 100),m.percentage) - - def test_similar_word(self): - mf = MatchFactory() - mf.match_similar_words = True - l = [NamedObject("foobar"),NamedObject("foobars")] - self.assertEqual(1,len(mf.getmatches(l))) - self.assertEqual(100,mf.getmatches(l)[0].percentage) - l = [NamedObject("foobar"),NamedObject("foo")] - self.assertEqual(0,len(mf.getmatches(l))) #too far - l = [NamedObject("bizkit"),NamedObject("bizket")] - self.assertEqual(1,len(mf.getmatches(l))) - l = [NamedObject("foobar"),NamedObject("foosbar")] - self.assertEqual(1,len(mf.getmatches(l))) - - def test_single_object_with_similar_words(self): - mf = MatchFactory() - mf.match_similar_words = True - l = [NamedObject("foo foos")] - self.assertEqual(0,len(mf.getmatches(l))) - - def test_double_words_get_counted_only_once(self): - mf = MatchFactory() - l = [NamedObject("foo bar foo bleh"),NamedObject("foo bar bleh bar")] - m = mf.getmatches(l)[0] - self.assertEqual(75,m.percentage) - - def test_with_fields(self): - mf = MatchFactory() - o1 = NamedObject("foo bar - foo bleh") - o2 = NamedObject("foo bar - bleh bar") - o1.words = getfields(o1.name) - o2.words = getfields(o2.name) - m = mf.getmatches([o1, o2])[0] - self.assertEqual(50, m.percentage) - - def test_with_fields_no_order(self): - mf = MatchFactory() - mf.no_field_order = True - o1 = NamedObject("foo bar - foo bleh") - o2 = NamedObject("bleh bang - foo bar") - o1.words = getfields(o1.name) - o2.words = getfields(o2.name) - m = mf.getmatches([o1, o2])[0] - self.assertEqual(50 ,m.percentage) - - def test_only_match_similar_when_the_option_is_set(self): - mf = MatchFactory() - mf.match_similar_words = False - l = [NamedObject("foobar"),NamedObject("foobars")] - self.assertEqual(0,len(mf.getmatches(l))) - - def test_dont_recurse_do_match(self): - # with nosetests, the stack is increased. The number has to be high enough not to be failing falsely - sys.setrecursionlimit(100) - mf = MatchFactory() - files = [NamedObject('foo bar') for i in range(101)] - try: - mf.getmatches(files) - except RuntimeError: - self.fail() - finally: - sys.setrecursionlimit(1000) - - def test_min_match_percentage(self): - l = [NamedObject("foo bar"),NamedObject("bar bleh"),NamedObject("a b c foo")] - mf = MatchFactory() - mf.min_match_percentage = 50 - r = mf.getmatches(l) - self.assertEqual(1,len(r)) #Only "foo bar" / "bar bleh" should match - - def test_limit(self): - l = [NamedObject(),NamedObject(),NamedObject()] - mf = MatchFactory() - mf.limit = 2 - r = mf.getmatches(l) - self.assertEqual(2,len(r)) - - def test_MemoryError(self): - @log_calls - def mocked_match(first, second, flags): - if len(mocked_match.calls) > 42: - raise MemoryError() - return Match(first, second, 0) - - objects = [NamedObject() for i in range(10)] # results in 45 matches - self.mock(engine, 'get_match', mocked_match) - mf = MatchFactory() - try: - r = mf.getmatches(objects) - except MemoryError: - self.fail('MemorryError must be handled') - self.assertEqual(42, len(r)) - - -class TCGroup(TestCase): - def test_empy(self): - g = Group() - self.assertEqual(None,g.ref) - self.assertEqual([],g.dupes) - self.assertEqual(0,len(g.matches)) - - def test_add_match(self): - g = Group() - m = get_match(NamedObject("foo",True),NamedObject("bar",True)) - g.add_match(m) - self.assert_(g.ref is m.first) - self.assertEqual([m.second],g.dupes) - self.assertEqual(1,len(g.matches)) - self.assert_(m in g.matches) - - def test_multiple_add_match(self): - g = Group() - o1 = NamedObject("a",True) - o2 = NamedObject("b",True) - o3 = NamedObject("c",True) - o4 = NamedObject("d",True) - g.add_match(get_match(o1,o2)) - self.assert_(g.ref is o1) - self.assertEqual([o2],g.dupes) - self.assertEqual(1,len(g.matches)) - g.add_match(get_match(o1,o3)) - self.assertEqual([o2],g.dupes) - self.assertEqual(2,len(g.matches)) - g.add_match(get_match(o2,o3)) - self.assertEqual([o2,o3],g.dupes) - self.assertEqual(3,len(g.matches)) - g.add_match(get_match(o1,o4)) - self.assertEqual([o2,o3],g.dupes) - self.assertEqual(4,len(g.matches)) - g.add_match(get_match(o2,o4)) - self.assertEqual([o2,o3],g.dupes) - self.assertEqual(5,len(g.matches)) - g.add_match(get_match(o3,o4)) - self.assertEqual([o2,o3,o4],g.dupes) - self.assertEqual(6,len(g.matches)) - - def test_len(self): - g = Group() - self.assertEqual(0,len(g)) - g.add_match(get_match(NamedObject("foo",True),NamedObject("bar",True))) - self.assertEqual(2,len(g)) - - def test_add_same_match_twice(self): - g = Group() - m = get_match(NamedObject("foo",True),NamedObject("foo",True)) - g.add_match(m) - self.assertEqual(2,len(g)) - self.assertEqual(1,len(g.matches)) - g.add_match(m) - self.assertEqual(2,len(g)) - self.assertEqual(1,len(g.matches)) - - def test_in(self): - g = Group() - o1 = NamedObject("foo",True) - o2 = NamedObject("bar",True) - self.assert_(o1 not in g) - g.add_match(get_match(o1,o2)) - self.assert_(o1 in g) - self.assert_(o2 in g) - - def test_remove(self): - g = Group() - o1 = NamedObject("foo",True) - o2 = NamedObject("bar",True) - o3 = NamedObject("bleh",True) - g.add_match(get_match(o1,o2)) - g.add_match(get_match(o1,o3)) - g.add_match(get_match(o2,o3)) - self.assertEqual(3,len(g.matches)) - self.assertEqual(3,len(g)) - g.remove_dupe(o3) - self.assertEqual(1,len(g.matches)) - self.assertEqual(2,len(g)) - g.remove_dupe(o1) - self.assertEqual(0,len(g.matches)) - self.assertEqual(0,len(g)) - - def test_remove_with_ref_dupes(self): - g = Group() - o1 = NamedObject("foo",True) - o2 = NamedObject("bar",True) - o3 = NamedObject("bleh",True) - g.add_match(get_match(o1,o2)) - g.add_match(get_match(o1,o3)) - g.add_match(get_match(o2,o3)) - o1.is_ref = True - o2.is_ref = True - g.remove_dupe(o3) - self.assertEqual(0,len(g)) - - def test_switch_ref(self): - o1 = NamedObject(with_words=True) - o2 = NamedObject(with_words=True) - g = Group() - g.add_match(get_match(o1,o2)) - self.assert_(o1 is g.ref) - g.switch_ref(o2) - self.assert_(o2 is g.ref) - self.assertEqual([o1],g.dupes) - g.switch_ref(o2) - self.assert_(o2 is g.ref) - g.switch_ref(NamedObject('',True)) - self.assert_(o2 is g.ref) - - def test_get_match_of(self): - g = Group() - for m in get_match_triangle(): - g.add_match(m) - o = g.dupes[0] - m = g.get_match_of(o) - self.assert_(g.ref in m) - self.assert_(o in m) - self.assert_(g.get_match_of(NamedObject('',True)) is None) - self.assert_(g.get_match_of(g.ref) is None) - - def test_percentage(self): - #percentage should return the avg percentage in relation to the ref - m1,m2,m3 = get_match_triangle() - m1 = Match(m1[0], m1[1], 100) - m2 = Match(m2[0], m2[1], 50) - m3 = Match(m3[0], m3[1], 33) - g = Group() - g.add_match(m1) - g.add_match(m2) - g.add_match(m3) - self.assertEqual(75,g.percentage) - g.switch_ref(g.dupes[0]) - self.assertEqual(66,g.percentage) - g.remove_dupe(g.dupes[0]) - self.assertEqual(33,g.percentage) - g.add_match(m1) - g.add_match(m2) - self.assertEqual(66,g.percentage) - - def test_percentage_on_empty_group(self): - g = Group() - self.assertEqual(0,g.percentage) - - def test_prioritize(self): - m1,m2,m3 = get_match_triangle() - o1 = m1.first - o2 = m1.second - o3 = m2.second - o1.name = 'c' - o2.name = 'b' - o3.name = 'a' - g = Group() - g.add_match(m1) - g.add_match(m2) - g.add_match(m3) - self.assert_(o1 is g.ref) - g.prioritize(lambda x:x.name) - self.assert_(o3 is g.ref) - - def test_prioritize_with_tie_breaker(self): - # if the ref has the same key as one or more of the dupe, run the tie_breaker func among them - g = get_test_group() - o1, o2, o3 = g.ordered - tie_breaker = lambda ref, dupe: dupe is o3 - g.prioritize(lambda x:0, tie_breaker) - self.assertTrue(g.ref is o3) - - def test_prioritize_with_tie_breaker_runs_on_all_dupes(self): - # Even if a dupe is chosen to switch with ref with a tie breaker, we still run the tie breaker - # with other dupes and the newly chosen ref - g = get_test_group() - o1, o2, o3 = g.ordered - o1.foo = 1 - o2.foo = 2 - o3.foo = 3 - tie_breaker = lambda ref, dupe: dupe.foo > ref.foo - g.prioritize(lambda x:0, tie_breaker) - self.assertTrue(g.ref is o3) - - def test_prioritize_with_tie_breaker_runs_only_on_tie_dupes(self): - # The tie breaker only runs on dupes that had the same value for the key_func - g = get_test_group() - o1, o2, o3 = g.ordered - o1.foo = 2 - o2.foo = 2 - o3.foo = 1 - o1.bar = 1 - o2.bar = 2 - o3.bar = 3 - key_func = lambda x: -x.foo - tie_breaker = lambda ref, dupe: dupe.bar > ref.bar - g.prioritize(key_func, tie_breaker) - self.assertTrue(g.ref is o2) - - def test_list_like(self): - g = Group() - o1,o2 = (NamedObject("foo",True),NamedObject("bar",True)) - g.add_match(get_match(o1,o2)) - self.assert_(g[0] is o1) - self.assert_(g[1] is o2) - - def test_clean_matches(self): - g = Group() - o1,o2,o3 = (NamedObject("foo",True),NamedObject("bar",True),NamedObject("baz",True)) - g.add_match(get_match(o1,o2)) - g.add_match(get_match(o1,o3)) - g.clean_matches() - self.assertEqual(1,len(g.matches)) - self.assertEqual(0,len(g.candidates)) - - -class TCget_groups(TestCase): - def test_empty(self): - r = get_groups([]) - self.assertEqual([],r) - - def test_simple(self): - l = [NamedObject("foo bar"),NamedObject("bar bleh")] - matches = MatchFactory().getmatches(l) - m = matches[0] - r = get_groups(matches) - self.assertEqual(1,len(r)) - g = r[0] - self.assert_(g.ref is m.first) - self.assertEqual([m.second],g.dupes) - - def test_group_with_multiple_matches(self): - #This results in 3 matches - l = [NamedObject("foo"),NamedObject("foo"),NamedObject("foo")] - matches = MatchFactory().getmatches(l) - r = get_groups(matches) - self.assertEqual(1,len(r)) - g = r[0] - self.assertEqual(3,len(g)) - - def test_must_choose_a_group(self): - l = [NamedObject("a b"),NamedObject("a b"),NamedObject("b c"),NamedObject("c d"),NamedObject("c d")] - #There will be 2 groups here: group "a b" and group "c d" - #"b c" can go either of them, but not both. - matches = MatchFactory().getmatches(l) - r = get_groups(matches) - self.assertEqual(2,len(r)) - self.assertEqual(5,len(r[0])+len(r[1])) - - def test_should_all_go_in_the_same_group(self): - l = [NamedObject("a b"),NamedObject("a b"),NamedObject("a b"),NamedObject("a b")] - #There will be 2 groups here: group "a b" and group "c d" - #"b c" can fit in both, but it must be in only one of them - matches = MatchFactory().getmatches(l) - r = get_groups(matches) - self.assertEqual(1,len(r)) - - def test_give_priority_to_matches_with_higher_percentage(self): - o1 = NamedObject(with_words=True) - o2 = NamedObject(with_words=True) - o3 = NamedObject(with_words=True) - m1 = Match(o1, o2, 1) - m2 = Match(o2, o3, 2) - r = get_groups([m1,m2]) - self.assertEqual(1,len(r)) - g = r[0] - self.assertEqual(2,len(g)) - self.assert_(o1 not in g) - self.assert_(o2 in g) - self.assert_(o3 in g) - - def test_four_sized_group(self): - l = [NamedObject("foobar") for i in xrange(4)] - m = MatchFactory().getmatches(l) - r = get_groups(m) - self.assertEqual(1,len(r)) - self.assertEqual(4,len(r[0])) - - def test_referenced_by_ref2(self): - o1 = NamedObject(with_words=True) - o2 = NamedObject(with_words=True) - o3 = NamedObject(with_words=True) - m1 = get_match(o1,o2) - m2 = get_match(o3,o1) - m3 = get_match(o3,o2) - r = get_groups([m1,m2,m3]) - self.assertEqual(3,len(r[0])) - - def test_job(self): - def do_progress(p,d=''): - self.log.append(p) - return True - - self.log = [] - j = job.Job(1,do_progress) - m1,m2,m3 = get_match_triangle() - #101%: To make sure it is processed first so the job test works correctly - m4 = Match(NamedObject('a',True), NamedObject('a',True), 101) - get_groups([m1,m2,m3,m4],j) - self.assertEqual(0,self.log[0]) - self.assertEqual(100,self.log[-1]) - - -if __name__ == "__main__": - unittest.main() diff --git a/pe/qt/dupeguru/export.py b/pe/qt/dupeguru/export.py deleted file mode 100644 index c6293a5d..00000000 --- a/pe/qt/dupeguru/export.py +++ /dev/null @@ -1,67 +0,0 @@ -#!/usr/bin/env python -""" -Unit Name: dupeguru.export -Created By: Virgil Dupras -Created On: 2006/09/16 -Last modified by:$Author: virgil $ -Last modified on:$Date: 2009-05-28 15:22:39 +0200 (Thu, 28 May 2009) $ - $Revision: 4385 $ -Copyright 2004-2006 Hardcoded Software (http://www.hardcoded.net) -""" -from xml.dom import minidom -import tempfile -import os.path as op -import os -from StringIO import StringIO - -from hsutil.files import FileOrPath - -def output_column_xml(outfile, columns): - """Creates a xml file outfile with the supplied columns. - - outfile can be a filename or a file object. - columns is a list of 2 sized tuples (display,enabled) - """ - doc = minidom.Document() - root = doc.appendChild(doc.createElement('columns')) - for display,enabled in columns: - col_node = root.appendChild(doc.createElement('column')) - col_node.setAttribute('display', display) - col_node.setAttribute('enabled', {True:'y',False:'n'}[enabled]) - with FileOrPath(outfile, 'wb') as fp: - doc.writexml(fp, '\t','\t','\n', encoding='utf-8') - -def merge_css_into_xhtml(xhtml, css): - with FileOrPath(xhtml, 'r+') as xhtml: - with FileOrPath(css) as css: - try: - doc = minidom.parse(xhtml) - except Exception: - return False - head = doc.getElementsByTagName('head')[0] - links = head.getElementsByTagName('link') - for link in links: - if link.getAttribute('rel') == 'stylesheet': - head.removeChild(link) - style = head.appendChild(doc.createElement('style')) - style.setAttribute('type','text/css') - style.appendChild(doc.createTextNode(css.read())) - xhtml.truncate(0) - doc.writexml(xhtml, '\t','\t','\n', encoding='utf-8') - xhtml.seek(0) - return True - -def export_to_xhtml(xml, xslt, css, columns, cmd='xsltproc --path "%(folder)s" "%(xslt)s" "%(xml)s"'): - folder = op.split(xml)[0] - output_column_xml(op.join(folder,'columns.xml'),columns) - html = StringIO() - cmd = cmd % {'folder': folder, 'xslt': xslt, 'xml': xml} - html.write(os.popen(cmd).read()) - html.seek(0) - merge_css_into_xhtml(html,css) - html.seek(0) - html_path = op.join(folder,'export.htm') - html_file = open(html_path,'w') - html_file.write(html.read().encode('utf-8')) - html_file.close() - return html_path diff --git a/pe/qt/dupeguru/export_test.py b/pe/qt/dupeguru/export_test.py deleted file mode 100644 index 5c4a6d87..00000000 --- a/pe/qt/dupeguru/export_test.py +++ /dev/null @@ -1,91 +0,0 @@ -#!/usr/bin/env python -""" -Unit Name: dupeguru.tests.export -Created By: Virgil Dupras -Created On: 2006/09/16 -Last modified by:$Author: virgil $ -Last modified on:$Date: 2009-05-28 15:22:39 +0200 (Thu, 28 May 2009) $ - $Revision: 4385 $ -Copyright 2004-2006 Hardcoded Software (http://www.hardcoded.net) -""" -import unittest -from xml.dom import minidom -from StringIO import StringIO - -from hsutil.testcase import TestCase - -from .export import * -from . import export - -class TCoutput_columns_xml(TestCase): - def test_empty_columns(self): - f = StringIO() - output_column_xml(f,[]) - f.seek(0) - doc = minidom.parse(f) - root = doc.documentElement - self.assertEqual('columns',root.nodeName) - self.assertEqual(0,len(root.childNodes)) - - def test_some_columns(self): - f = StringIO() - output_column_xml(f,[('foo',True),('bar',False),('baz',True)]) - f.seek(0) - doc = minidom.parse(f) - columns = doc.getElementsByTagName('column') - self.assertEqual(3,len(columns)) - c1,c2,c3 = columns - self.assertEqual('foo',c1.getAttribute('display')) - self.assertEqual('bar',c2.getAttribute('display')) - self.assertEqual('baz',c3.getAttribute('display')) - self.assertEqual('y',c1.getAttribute('enabled')) - self.assertEqual('n',c2.getAttribute('enabled')) - self.assertEqual('y',c3.getAttribute('enabled')) - - -class TCmerge_css_into_xhtml(TestCase): - def test_main(self): - css = StringIO() - css.write('foobar') - css.seek(0) - xhtml = StringIO() - xhtml.write(""" - - - - - dupeGuru - Duplicate file scanner - - - - - - """) - xhtml.seek(0) - self.assert_(merge_css_into_xhtml(xhtml,css)) - xhtml.seek(0) - doc = minidom.parse(xhtml) - head = doc.getElementsByTagName('head')[0] - #A style node should have been added in head. - styles = head.getElementsByTagName('style') - self.assertEqual(1,len(styles)) - style = styles[0] - self.assertEqual('text/css',style.getAttribute('type')) - self.assertEqual('foobar',style.firstChild.nodeValue.strip()) - #all should be removed - self.assertEqual(1,len(head.getElementsByTagName('link'))) - - def test_empty(self): - self.assert_(not merge_css_into_xhtml(StringIO(),StringIO())) - - def test_malformed(self): - xhtml = StringIO() - xhtml.write(""" - - """) - xhtml.seek(0) - self.assert_(not merge_css_into_xhtml(xhtml,StringIO())) - - -if __name__ == "__main__": - unittest.main() \ No newline at end of file diff --git a/pe/qt/dupeguru/gen.py b/pe/qt/dupeguru/gen.py deleted file mode 100644 index 0a842372..00000000 --- a/pe/qt/dupeguru/gen.py +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env python -# Unit Name: gen -# Created By: Virgil Dupras -# Created On: 2009-05-26 -# $Id$ -# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) - -import os -import os.path as op - -def move(src, dst): - if not op.exists(src): - return - if op.exists(dst): - os.remove(dst) - print 'Moving %s --> %s' % (src, dst) - os.rename(src, dst) - - -os.chdir(op.join('modules', 'block')) -os.system('python setup.py build_ext --inplace') -os.chdir(op.join('..', 'cache')) -os.system('python setup.py build_ext --inplace') -os.chdir(op.join('..', '..')) -move(op.join('modules', 'block', '_block.so'), op.join('picture', '_block.so')) -move(op.join('modules', 'block', '_block.pyd'), op.join('picture', '_block.pyd')) -move(op.join('modules', 'cache', '_cache.so'), op.join('picture', '_cache.so')) -move(op.join('modules', 'cache', '_cache.pyd'), op.join('picture', '_cache.pyd')) diff --git a/pe/qt/dupeguru/ignore.py b/pe/qt/dupeguru/ignore.py deleted file mode 100644 index 97060786..00000000 --- a/pe/qt/dupeguru/ignore.py +++ /dev/null @@ -1,117 +0,0 @@ -#!/usr/bin/env python -""" -Unit Name: ignore -Created By: Virgil Dupras -Created On: 2006/05/02 -Last modified by:$Author: virgil $ -Last modified on:$Date: 2009-05-28 15:22:39 +0200 (Thu, 28 May 2009) $ - $Revision: 4385 $ -Copyright 2004-2006 Hardcoded Software (http://www.hardcoded.net) -""" -from hsutil.files import FileOrPath - -import xml.dom.minidom - -class IgnoreList(object): - """An ignore list implementation that is iterable, filterable and exportable to XML. - - Call Ignore to add an ignore list entry, and AreIgnore to check if 2 items are in the list. - When iterated, 2 sized tuples will be returned, the tuples containing 2 items ignored together. - """ - #---Override - def __init__(self): - self._ignored = {} - self._count = 0 - - def __iter__(self): - for first,seconds in self._ignored.iteritems(): - for second in seconds: - yield (first,second) - - def __len__(self): - return self._count - - #---Public - def AreIgnored(self,first,second): - def do_check(first,second): - try: - matches = self._ignored[first] - return second in matches - except KeyError: - return False - - return do_check(first,second) or do_check(second,first) - - def Clear(self): - self._ignored = {} - self._count = 0 - - def Filter(self,func): - """Applies a filter on all ignored items, and remove all matches where func(first,second) - doesn't return True. - """ - filtered = IgnoreList() - for first,second in self: - if func(first,second): - filtered.Ignore(first,second) - self._ignored = filtered._ignored - self._count = filtered._count - - def Ignore(self,first,second): - if self.AreIgnored(first,second): - return - try: - matches = self._ignored[first] - matches.add(second) - except KeyError: - try: - matches = self._ignored[second] - matches.add(first) - except KeyError: - matches = set() - matches.add(second) - self._ignored[first] = matches - self._count += 1 - - def load_from_xml(self,infile): - """Loads the ignore list from a XML created with save_to_xml. - - infile can be a file object or a filename. - """ - try: - doc = xml.dom.minidom.parse(infile) - except Exception: - return - file_nodes = doc.getElementsByTagName('file') - for fn in file_nodes: - if not fn.getAttributeNode('path'): - continue - file_path = fn.getAttributeNode('path').nodeValue - subfile_nodes = fn.getElementsByTagName('file') - for sfn in subfile_nodes: - if not sfn.getAttributeNode('path'): - continue - subfile_path = sfn.getAttributeNode('path').nodeValue - self.Ignore(file_path,subfile_path) - - def save_to_xml(self,outfile): - """Create a XML file that can be used by load_from_xml. - - outfile can be a file object or a filename. - """ - doc = xml.dom.minidom.Document() - root = doc.appendChild(doc.createElement('ignore_list')) - for file,subfiles in self._ignored.items(): - file_node = root.appendChild(doc.createElement('file')) - if isinstance(file,unicode): - file = file.encode('utf-8') - file_node.setAttribute('path',file) - for subfile in subfiles: - subfile_node = file_node.appendChild(doc.createElement('file')) - if isinstance(subfile,unicode): - subfile = subfile.encode('utf-8') - subfile_node.setAttribute('path',subfile) - with FileOrPath(outfile, 'wb') as fp: - doc.writexml(fp,'\t','\t','\n',encoding='utf-8') - - diff --git a/pe/qt/dupeguru/ignore_test.py b/pe/qt/dupeguru/ignore_test.py deleted file mode 100644 index 8ff91f52..00000000 --- a/pe/qt/dupeguru/ignore_test.py +++ /dev/null @@ -1,158 +0,0 @@ -#!/usr/bin/env python -""" -Unit Name: ignore -Created By: Virgil Dupras -Created On: 2006/05/02 -Last modified by:$Author: virgil $ -Last modified on:$Date: 2009-05-28 15:22:39 +0200 (Thu, 28 May 2009) $ - $Revision: 4385 $ -Copyright 2004-2006 Hardcoded Software (http://www.hardcoded.net) -""" -import unittest -import cStringIO -import xml.dom.minidom - -from .ignore import * - -class TCIgnoreList(unittest.TestCase): - def test_empty(self): - il = IgnoreList() - self.assertEqual(0,len(il)) - self.assert_(not il.AreIgnored('foo','bar')) - - def test_simple(self): - il = IgnoreList() - il.Ignore('foo','bar') - self.assert_(il.AreIgnored('foo','bar')) - self.assert_(il.AreIgnored('bar','foo')) - self.assert_(not il.AreIgnored('foo','bleh')) - self.assert_(not il.AreIgnored('bleh','bar')) - self.assertEqual(1,len(il)) - - def test_multiple(self): - il = IgnoreList() - il.Ignore('foo','bar') - il.Ignore('foo','bleh') - il.Ignore('bleh','bar') - il.Ignore('aybabtu','bleh') - self.assert_(il.AreIgnored('foo','bar')) - self.assert_(il.AreIgnored('bar','foo')) - self.assert_(il.AreIgnored('foo','bleh')) - self.assert_(il.AreIgnored('bleh','bar')) - self.assert_(not il.AreIgnored('aybabtu','bar')) - self.assertEqual(4,len(il)) - - def test_clear(self): - il = IgnoreList() - il.Ignore('foo','bar') - il.Clear() - self.assert_(not il.AreIgnored('foo','bar')) - self.assert_(not il.AreIgnored('bar','foo')) - self.assertEqual(0,len(il)) - - def test_add_same_twice(self): - il = IgnoreList() - il.Ignore('foo','bar') - il.Ignore('bar','foo') - self.assertEqual(1,len(il)) - - def test_save_to_xml(self): - il = IgnoreList() - il.Ignore('foo','bar') - il.Ignore('foo','bleh') - il.Ignore('bleh','bar') - f = cStringIO.StringIO() - il.save_to_xml(f) - f.seek(0) - doc = xml.dom.minidom.parse(f) - root = doc.documentElement - self.assertEqual('ignore_list',root.nodeName) - children = [c for c in root.childNodes if c.localName] - self.assertEqual(2,len(children)) - self.assertEqual(2,len([c for c in children if c.nodeName == 'file'])) - f1,f2 = children - subchildren = [c for c in f1.childNodes if c.localName == 'file'] +\ - [c for c in f2.childNodes if c.localName == 'file'] - self.assertEqual(3,len(subchildren)) - - def test_SaveThenLoad(self): - il = IgnoreList() - il.Ignore('foo','bar') - il.Ignore('foo','bleh') - il.Ignore('bleh','bar') - il.Ignore(u'\u00e9','bar') - f = cStringIO.StringIO() - il.save_to_xml(f) - f.seek(0) - il = IgnoreList() - il.load_from_xml(f) - self.assertEqual(4,len(il)) - self.assert_(il.AreIgnored(u'\u00e9','bar')) - - def test_LoadXML_with_empty_file_tags(self): - f = cStringIO.StringIO() - f.write('') - f.seek(0) - il = IgnoreList() - il.load_from_xml(f) - self.assertEqual(0,len(il)) - - def test_AreIgnore_works_when_a_child_is_a_key_somewhere_else(self): - il = IgnoreList() - il.Ignore('foo','bar') - il.Ignore('bar','baz') - self.assert_(il.AreIgnored('bar','foo')) - - - def test_no_dupes_when_a_child_is_a_key_somewhere_else(self): - il = IgnoreList() - il.Ignore('foo','bar') - il.Ignore('bar','baz') - il.Ignore('bar','foo') - self.assertEqual(2,len(il)) - - def test_iterate(self): - #It must be possible to iterate through ignore list - il = IgnoreList() - expected = [('foo','bar'),('bar','baz'),('foo','baz')] - for i in expected: - il.Ignore(i[0],i[1]) - for i in il: - expected.remove(i) #No exception should be raised - self.assert_(not expected) #expected should be empty - - def test_filter(self): - il = IgnoreList() - il.Ignore('foo','bar') - il.Ignore('bar','baz') - il.Ignore('foo','baz') - il.Filter(lambda f,s: f == 'bar') - self.assertEqual(1,len(il)) - self.assert_(not il.AreIgnored('foo','bar')) - self.assert_(il.AreIgnored('bar','baz')) - - def test_save_with_non_ascii_non_unicode_items(self): - il = IgnoreList() - il.Ignore('\xac','\xbf') - f = cStringIO.StringIO() - try: - il.save_to_xml(f) - except Exception,e: - self.fail(str(e)) - - def test_len(self): - il = IgnoreList() - self.assertEqual(0,len(il)) - il.Ignore('foo','bar') - self.assertEqual(1,len(il)) - - def test_nonzero(self): - il = IgnoreList() - self.assert_(not il) - il.Ignore('foo','bar') - self.assert_(il) - - -if __name__ == "__main__": - unittest.main() - diff --git a/pe/qt/dupeguru/modules/block/block.pyx b/pe/qt/dupeguru/modules/block/block.pyx deleted file mode 100644 index db4c7500..00000000 --- a/pe/qt/dupeguru/modules/block/block.pyx +++ /dev/null @@ -1,93 +0,0 @@ -# Created By: Virgil Dupras -# Created On: 2009-04-23 -# $Id$ -# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) - -cdef extern from "stdlib.h": - int abs(int n) # required so that abs() is applied on ints, not python objects - -class NoBlocksError(Exception): - """avgdiff/maxdiff has been called with empty lists""" - -class DifferentBlockCountError(Exception): - """avgdiff/maxdiff has been called with 2 block lists of different size.""" - - -cdef object getblock(object image): - """Returns a 3 sized tuple containing the mean color of 'image'. - - image: a PIL image or crop. - """ - cdef int pixel_count, red, green, blue, r, g, b - if image.size[0]: - pixel_count = image.size[0] * image.size[1] - red = green = blue = 0 - for r, g, b in image.getdata(): - red += r - green += g - blue += b - return (red // pixel_count, green // pixel_count, blue // pixel_count) - else: - return (0, 0, 0) - -def getblocks2(image, int block_count_per_side): - """Returns a list of blocks (3 sized tuples). - - image: A PIL image to base the blocks on. - block_count_per_side: This integer determine the number of blocks the function will return. - If it is 10, for example, 100 blocks will be returns (10 width, 10 height). The blocks will not - necessarely cover square areas. The area covered by each block will be proportional to the image - itself. - """ - if not image.size[0]: - return [] - cdef int width, height, block_width, block_height, ih, iw, top, bottom, left, right - width, height = image.size - block_width = max(width // block_count_per_side, 1) - block_height = max(height // block_count_per_side, 1) - result = [] - for ih in range(block_count_per_side): - top = min(ih * block_height, height - block_height) - bottom = top + block_height - for iw in range(block_count_per_side): - left = min(iw * block_width, width - block_width) - right = left + block_width - box = (left, top, right, bottom) - crop = image.crop(box) - result.append(getblock(crop)) - return result - -cdef int diff(first, second): - """Returns the difference between the first block and the second. - - It returns an absolute sum of the 3 differences (RGB). - """ - cdef int r1, g1, b1, r2, g2, b2 - r1, g1, b1 = first - r2, g2, b2 = second - return abs(r1 - r2) + abs(g1 - g2) + abs(b1 - b2) - -def avgdiff(first, second, int limit, int min_iterations): - """Returns the average diff between first blocks and seconds. - - If the result surpasses limit, limit + 1 is returned, except if less than min_iterations - iterations have been made in the blocks. - """ - cdef int count, sum, i, iteration_count - count = len(first) - if count != len(second): - raise DifferentBlockCountError() - if not count: - raise NoBlocksError() - sum = 0 - for i in range(count): - iteration_count = i + 1 - item1 = first[i] - item2 = second[i] - sum += diff(item1, item2) - if sum > limit * iteration_count and iteration_count >= min_iterations: - return limit + 1 - result = sum // count - if (not result) and sum: - result = 1 - return result \ No newline at end of file diff --git a/pe/qt/dupeguru/modules/block/setup.py b/pe/qt/dupeguru/modules/block/setup.py deleted file mode 100644 index 9d8f4cb5..00000000 --- a/pe/qt/dupeguru/modules/block/setup.py +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env python -# Created By: Virgil Dupras -# Created On: 2009-04-23 -# $Id$ -# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) - -from distutils.core import setup -from distutils.extension import Extension -from Cython.Distutils import build_ext - -setup( - cmdclass = {'build_ext': build_ext}, - ext_modules = [Extension("_block", ["block.pyx"])] -) \ No newline at end of file diff --git a/pe/qt/dupeguru/modules/cache/cache.pyx b/pe/qt/dupeguru/modules/cache/cache.pyx deleted file mode 100644 index 7bd2407d..00000000 --- a/pe/qt/dupeguru/modules/cache/cache.pyx +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env python -# Created By: Virgil Dupras -# Created On: 2009-04-23 -# $Id$ -# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) - -# ok, this is hacky and stuff, but I don't know C well enough to play with char buffers, copy -# them around and stuff -cdef int xchar_to_int(char c): - if 48 <= c <= 57: # 0-9 - return c - 48 - elif 65 <= c <= 70: # A-F - return c - 55 - elif 97 <= c <= 102: # a-f - return c - 87 - -def string_to_colors(s): - """Transform the string 's' in a list of 3 sized tuples. - """ - result = [] - cdef int i, char_count, r, g, b - cdef char* cs - char_count = len(s) - char_count = (char_count // 6) * 6 - cs = s - for i in range(0, char_count, 6): - r = xchar_to_int(cs[i]) << 4 - r += xchar_to_int(cs[i+1]) - g = xchar_to_int(cs[i+2]) << 4 - g += xchar_to_int(cs[i+3]) - b = xchar_to_int(cs[i+4]) << 4 - b += xchar_to_int(cs[i+5]) - result.append((r, g, b)) - return result diff --git a/pe/qt/dupeguru/modules/cache/setup.py b/pe/qt/dupeguru/modules/cache/setup.py deleted file mode 100644 index 2b6cd31b..00000000 --- a/pe/qt/dupeguru/modules/cache/setup.py +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env python -# Created By: Virgil Dupras -# Created On: 2009-04-23 -# $Id$ -# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) - -from distutils.core import setup -from distutils.extension import Extension -from Cython.Distutils import build_ext - -setup( - cmdclass = {'build_ext': build_ext}, - ext_modules = [Extension("_cache", ["cache.pyx"])] -) \ No newline at end of file diff --git a/pe/qt/dupeguru/picture/__init__.py b/pe/qt/dupeguru/picture/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/pe/qt/dupeguru/picture/block.py b/pe/qt/dupeguru/picture/block.py deleted file mode 100644 index 70015a50..00000000 --- a/pe/qt/dupeguru/picture/block.py +++ /dev/null @@ -1,124 +0,0 @@ -#!/usr/bin/env python -""" -Unit Name: hs.picture.block -Created By: Virgil Dupras -Created On: 2006/09/01 -Last modified by:$Author: virgil $ -Last modified on:$Date: 2009-05-26 18:12:39 +0200 (Tue, 26 May 2009) $ - $Revision: 4365 $ -Copyright 2004-2006 Hardcoded Software (http://www.hardcoded.net) -""" -from _block import NoBlocksError, DifferentBlockCountError, avgdiff, getblocks2 - -# Converted to Cython -# def getblock(image): -# """Returns a 3 sized tuple containing the mean color of 'image'. -# -# image: a PIL image or crop. -# """ -# if image.size[0]: -# pixel_count = image.size[0] * image.size[1] -# red = green = blue = 0 -# for r,g,b in image.getdata(): -# red += r -# green += g -# blue += b -# return (red // pixel_count, green // pixel_count, blue // pixel_count) -# else: -# return (0,0,0) - -# This is not used anymore -# def getblocks(image,blocksize): -# """Returns a list of blocks (3 sized tuples). -# -# image: A PIL image to base the blocks on. -# blocksize: The size of the blocks to be create. This is a single integer, defining -# both width and height (blocks are square). -# """ -# if min(image.size) < blocksize: -# return () -# result = [] -# for i in xrange(image.size[1] // blocksize): -# for j in xrange(image.size[0] // blocksize): -# box = (blocksize * j, blocksize * i, blocksize * (j + 1), blocksize * (i + 1)) -# crop = image.crop(box) -# result.append(getblock(crop)) -# return result - -# Converted to Cython -# def getblocks2(image,block_count_per_side): -# """Returns a list of blocks (3 sized tuples). -# -# image: A PIL image to base the blocks on. -# block_count_per_side: This integer determine the number of blocks the function will return. -# If it is 10, for example, 100 blocks will be returns (10 width, 10 height). The blocks will not -# necessarely cover square areas. The area covered by each block will be proportional to the image -# itself. -# """ -# if not image.size[0]: -# return [] -# width,height = image.size -# block_width = max(width // block_count_per_side,1) -# block_height = max(height // block_count_per_side,1) -# result = [] -# for ih in range(block_count_per_side): -# top = min(ih * block_height, height - block_height) -# bottom = top + block_height -# for iw in range(block_count_per_side): -# left = min(iw * block_width, width - block_width) -# right = left + block_width -# box = (left,top,right,bottom) -# crop = image.crop(box) -# result.append(getblock(crop)) -# return result - -# Converted to Cython -# def diff(first, second): -# """Returns the difference between the first block and the second. -# -# It returns an absolute sum of the 3 differences (RGB). -# """ -# r1, g1, b1 = first -# r2, g2, b2 = second -# return abs(r1 - r2) + abs(g1 - g2) + abs(b1 - b2) - -# Converted to Cython -# def avgdiff(first, second, limit=768, min_iterations=1): -# """Returns the average diff between first blocks and seconds. -# -# If the result surpasses limit, limit + 1 is returned, except if less than min_iterations -# iterations have been made in the blocks. -# """ -# if len(first) != len(second): -# raise DifferentBlockCountError -# if not first: -# raise NoBlocksError -# count = len(first) -# sum = 0 -# zipped = izip(xrange(1, count + 1), first, second) -# for i, first, second in zipped: -# sum += diff(first, second) -# if sum > limit * i and i >= min_iterations: -# return limit + 1 -# result = sum // count -# if (not result) and sum: -# result = 1 -# return result - -# This is not used anymore -# def maxdiff(first,second,limit=768): -# """Returns the max diff between first blocks and seconds. -# -# If the result surpasses limit, the first max being over limit is returned. -# """ -# if len(first) != len(second): -# raise DifferentBlockCountError -# if not first: -# raise NoBlocksError -# result = 0 -# zipped = zip(first,second) -# for first,second in zipped: -# result = max(result,diff(first,second)) -# if result > limit: -# return result -# return result diff --git a/pe/qt/dupeguru/picture/block_test.py b/pe/qt/dupeguru/picture/block_test.py deleted file mode 100644 index a06cf617..00000000 --- a/pe/qt/dupeguru/picture/block_test.py +++ /dev/null @@ -1,313 +0,0 @@ -#!/usr/bin/env python -""" -Unit Name: tests.picture.block -Created By: Virgil Dupras -Created On: 2006/09/01 -Last modified by:$Author: virgil $ -Last modified on:$Date: 2009-05-28 15:22:39 +0200 (Thu, 28 May 2009) $ - $Revision: 4385 $ -Copyright 2004-2006 Hardcoded Software (http://www.hardcoded.net) -""" -# The commented out tests are tests for function that have been converted to pure C for speed -import unittest - -from .block import * - -def my_avgdiff(first, second, limit=768, min_iter=3): # this is so I don't have to re-write every call - return avgdiff(first, second, limit, min_iter) - -BLACK = (0,0,0) -RED = (0xff,0,0) -GREEN = (0,0xff,0) -BLUE = (0,0,0xff) - -class FakeImage(object): - def __init__(self, size, data): - self.size = size - self.data = data - - def getdata(self): - return self.data - - def crop(self, box): - pixels = [] - for i in range(box[1], box[3]): - for j in range(box[0], box[2]): - pixel = self.data[i * self.size[0] + j] - pixels.append(pixel) - return FakeImage((box[2] - box[0], box[3] - box[1]), pixels) - -def empty(): - return FakeImage((0,0), []) - -def single_pixel(): #one red pixel - return FakeImage((1, 1), [(0xff,0,0)]) - -def four_pixels(): - pixels = [RED,(0,0x80,0xff),(0x80,0,0),(0,0x40,0x80)] - return FakeImage((2, 2), pixels) - -class TCgetblock(unittest.TestCase): - def test_single_pixel(self): - im = single_pixel() - [b] = getblocks2(im, 1) - self.assertEqual(RED,b) - - def test_no_pixel(self): - im = empty() - self.assertEqual([], getblocks2(im, 1)) - - def test_four_pixels(self): - im = four_pixels() - [b] = getblocks2(im, 1) - meanred = (0xff + 0x80) // 4 - meangreen = (0x80 + 0x40) // 4 - meanblue = (0xff + 0x80) // 4 - self.assertEqual((meanred,meangreen,meanblue),b) - - -# class TCdiff(unittest.TestCase): -# def test_diff(self): -# b1 = (10, 20, 30) -# b2 = (1, 2, 3) -# self.assertEqual(9 + 18 + 27,diff(b1,b2)) -# -# def test_diff_negative(self): -# b1 = (10, 20, 30) -# b2 = (1, 2, 3) -# self.assertEqual(9 + 18 + 27,diff(b2,b1)) -# -# def test_diff_mixed_positive_and_negative(self): -# b1 = (1, 5, 10) -# b2 = (10, 1, 15) -# self.assertEqual(9 + 4 + 5,diff(b1,b2)) -# - -# class TCgetblocks(unittest.TestCase): -# def test_empty_image(self): -# im = empty() -# blocks = getblocks(im,1) -# self.assertEqual(0,len(blocks)) -# -# def test_one_block_image(self): -# im = four_pixels() -# blocks = getblocks2(im, 1) -# self.assertEqual(1,len(blocks)) -# block = blocks[0] -# meanred = (0xff + 0x80) // 4 -# meangreen = (0x80 + 0x40) // 4 -# meanblue = (0xff + 0x80) // 4 -# self.assertEqual((meanred,meangreen,meanblue),block) -# -# def test_not_enough_height_to_fit_a_block(self): -# im = FakeImage((2,1), [BLACK, BLACK]) -# blocks = getblocks(im,2) -# self.assertEqual(0,len(blocks)) -# -# def xtest_dont_include_leftovers(self): -# # this test is disabled because getblocks is not used and getblock in cdeffed -# pixels = [ -# RED,(0,0x80,0xff),BLACK, -# (0x80,0,0),(0,0x40,0x80),BLACK, -# BLACK,BLACK,BLACK -# ] -# im = FakeImage((3,3), pixels) -# blocks = getblocks(im,2) -# block = blocks[0] -# #Because the block is smaller than the image, only blocksize must be considered. -# meanred = (0xff + 0x80) // 4 -# meangreen = (0x80 + 0x40) // 4 -# meanblue = (0xff + 0x80) // 4 -# self.assertEqual((meanred,meangreen,meanblue),block) -# -# def xtest_two_blocks(self): -# # this test is disabled because getblocks is not used and getblock in cdeffed -# pixels = [BLACK for i in xrange(4 * 2)] -# pixels[0] = RED -# pixels[1] = (0,0x80,0xff) -# pixels[4] = (0x80,0,0) -# pixels[5] = (0,0x40,0x80) -# im = FakeImage((4, 2), pixels) -# blocks = getblocks(im,2) -# self.assertEqual(2,len(blocks)) -# block = blocks[0] -# #Because the block is smaller than the image, only blocksize must be considered. -# meanred = (0xff + 0x80) // 4 -# meangreen = (0x80 + 0x40) // 4 -# meanblue = (0xff + 0x80) // 4 -# self.assertEqual((meanred,meangreen,meanblue),block) -# self.assertEqual(BLACK,blocks[1]) -# -# def test_four_blocks(self): -# pixels = [BLACK for i in xrange(4 * 4)] -# pixels[0] = RED -# pixels[1] = (0,0x80,0xff) -# pixels[4] = (0x80,0,0) -# pixels[5] = (0,0x40,0x80) -# im = FakeImage((4, 4), pixels) -# blocks = getblocks2(im, 2) -# self.assertEqual(4,len(blocks)) -# block = blocks[0] -# #Because the block is smaller than the image, only blocksize must be considered. -# meanred = (0xff + 0x80) // 4 -# meangreen = (0x80 + 0x40) // 4 -# meanblue = (0xff + 0x80) // 4 -# self.assertEqual((meanred,meangreen,meanblue),block) -# self.assertEqual(BLACK,blocks[1]) -# self.assertEqual(BLACK,blocks[2]) -# self.assertEqual(BLACK,blocks[3]) -# - -class TCgetblocks2(unittest.TestCase): - def test_empty_image(self): - im = empty() - blocks = getblocks2(im,1) - self.assertEqual(0,len(blocks)) - - def test_one_block_image(self): - im = four_pixels() - blocks = getblocks2(im,1) - self.assertEqual(1,len(blocks)) - block = blocks[0] - meanred = (0xff + 0x80) // 4 - meangreen = (0x80 + 0x40) // 4 - meanblue = (0xff + 0x80) // 4 - self.assertEqual((meanred,meangreen,meanblue),block) - - def test_four_blocks_all_black(self): - im = FakeImage((2, 2), [BLACK, BLACK, BLACK, BLACK]) - blocks = getblocks2(im,2) - self.assertEqual(4,len(blocks)) - for block in blocks: - self.assertEqual(BLACK,block) - - def test_two_pixels_image_horizontal(self): - pixels = [RED,BLUE] - im = FakeImage((2, 1), pixels) - blocks = getblocks2(im,2) - self.assertEqual(4,len(blocks)) - self.assertEqual(RED,blocks[0]) - self.assertEqual(BLUE,blocks[1]) - self.assertEqual(RED,blocks[2]) - self.assertEqual(BLUE,blocks[3]) - - def test_two_pixels_image_vertical(self): - pixels = [RED,BLUE] - im = FakeImage((1, 2), pixels) - blocks = getblocks2(im,2) - self.assertEqual(4,len(blocks)) - self.assertEqual(RED,blocks[0]) - self.assertEqual(RED,blocks[1]) - self.assertEqual(BLUE,blocks[2]) - self.assertEqual(BLUE,blocks[3]) - - -class TCavgdiff(unittest.TestCase): - def test_empty(self): - self.assertRaises(NoBlocksError, my_avgdiff, [], []) - - def test_two_blocks(self): - im = empty() - b1 = (5,10,15) - b2 = (255,250,245) - b3 = (0,0,0) - b4 = (255,0,255) - blocks1 = [b1,b2] - blocks2 = [b3,b4] - expected1 = 5 + 10 + 15 - expected2 = 0 + 250 + 10 - expected = (expected1 + expected2) // 2 - self.assertEqual(expected, my_avgdiff(blocks1, blocks2)) - - def test_blocks_not_the_same_size(self): - b = (0,0,0) - self.assertRaises(DifferentBlockCountError,my_avgdiff,[b,b],[b]) - - def test_first_arg_is_empty_but_not_second(self): - #Don't return 0 (as when the 2 lists are empty), raise! - b = (0,0,0) - self.assertRaises(DifferentBlockCountError,my_avgdiff,[],[b]) - - def test_limit(self): - ref = (0,0,0) - b1 = (10,10,10) #avg 30 - b2 = (20,20,20) #avg 45 - b3 = (30,30,30) #avg 60 - blocks1 = [ref,ref,ref] - blocks2 = [b1,b2,b3] - self.assertEqual(45,my_avgdiff(blocks1,blocks2,44)) - - def test_min_iterations(self): - ref = (0,0,0) - b1 = (10,10,10) #avg 30 - b2 = (20,20,20) #avg 45 - b3 = (10,10,10) #avg 40 - blocks1 = [ref,ref,ref] - blocks2 = [b1,b2,b3] - self.assertEqual(40,my_avgdiff(blocks1,blocks2,45 - 1,3)) - - # Bah, I don't know why this test fails, but I don't think it matters very much - # def test_just_over_the_limit(self): - # #A score just over the limit might return exactly the limit due to truncating. We should - # #ceil() the result in this case. - # ref = (0,0,0) - # b1 = (10,0,0) - # b2 = (11,0,0) - # blocks1 = [ref,ref] - # blocks2 = [b1,b2] - # self.assertEqual(11,my_avgdiff(blocks1,blocks2,10)) - # - def test_return_at_least_1_at_the_slightest_difference(self): - ref = (0,0,0) - b1 = (1,0,0) - blocks1 = [ref for i in xrange(250)] - blocks2 = [ref for i in xrange(250)] - blocks2[0] = b1 - self.assertEqual(1,my_avgdiff(blocks1,blocks2)) - - def test_return_0_if_there_is_no_difference(self): - ref = (0,0,0) - blocks1 = [ref,ref] - blocks2 = [ref,ref] - self.assertEqual(0,my_avgdiff(blocks1,blocks2)) - - -# class TCmaxdiff(unittest.TestCase): -# def test_empty(self): -# self.assertRaises(NoBlocksError,maxdiff,[],[]) -# -# def test_two_blocks(self): -# b1 = (5,10,15) -# b2 = (255,250,245) -# b3 = (0,0,0) -# b4 = (255,0,255) -# blocks1 = [b1,b2] -# blocks2 = [b3,b4] -# expected1 = 5 + 10 + 15 -# expected2 = 0 + 250 + 10 -# expected = max(expected1,expected2) -# self.assertEqual(expected,maxdiff(blocks1,blocks2)) -# -# def test_blocks_not_the_same_size(self): -# b = (0,0,0) -# self.assertRaises(DifferentBlockCountError,maxdiff,[b,b],[b]) -# -# def test_first_arg_is_empty_but_not_second(self): -# #Don't return 0 (as when the 2 lists are empty), raise! -# b = (0,0,0) -# self.assertRaises(DifferentBlockCountError,maxdiff,[],[b]) -# -# def test_limit(self): -# b1 = (5,10,15) -# b2 = (255,250,245) -# b3 = (0,0,0) -# b4 = (255,0,255) -# blocks1 = [b1,b2] -# blocks2 = [b3,b4] -# expected1 = 5 + 10 + 15 -# expected2 = 0 + 250 + 10 -# self.assertEqual(expected1,maxdiff(blocks1,blocks2,expected1 - 1)) -# - -if __name__ == "__main__": - unittest.main() \ No newline at end of file diff --git a/pe/qt/dupeguru/picture/cache.py b/pe/qt/dupeguru/picture/cache.py deleted file mode 100644 index 6ff0d2d1..00000000 --- a/pe/qt/dupeguru/picture/cache.py +++ /dev/null @@ -1,134 +0,0 @@ -#!/usr/bin/env python -""" -Unit Name: hs.picture.cache -Created By: Virgil Dupras -Created On: 2006/09/14 -Last modified by:$Author: virgil $ -Last modified on:$Date: 2009-05-28 16:33:32 +0200 (Thu, 28 May 2009) $ - $Revision: 4392 $ -Copyright 2004-2006 Hardcoded Software (http://www.hardcoded.net) -""" -import os -import logging -import sqlite3 as sqlite - -import hsutil.sqlite - -from _cache import string_to_colors - -def colors_to_string(colors): - """Transform the 3 sized tuples 'colors' into a hex string. - - [(0,100,255)] --> 0064ff - [(1,2,3),(4,5,6)] --> 010203040506 - """ - return ''.join(['%02x%02x%02x' % (r,g,b) for r,g,b in colors]) - -# This function is an important bottleneck of dupeGuru PE. It has been converted to Cython. -# def string_to_colors(s): -# """Transform the string 's' in a list of 3 sized tuples. -# """ -# result = [] -# for i in xrange(0, len(s), 6): -# number = int(s[i:i+6], 16) -# result.append((number >> 16, (number >> 8) & 0xff, number & 0xff)) -# return result - -class Cache(object): - """A class to cache picture blocks. - """ - def __init__(self, db=':memory:', threaded=True): - def create_tables(): - sql = "create table pictures(path TEXT, blocks TEXT)" - self.con.execute(sql); - sql = "create index idx_path on pictures (path)" - self.con.execute(sql) - - self.dbname = db - if threaded: - self.con = hsutil.sqlite.ThreadedConn(db, True) - else: - self.con = sqlite.connect(db, isolation_level=None) - try: - self.con.execute("select * from pictures where 1=2") - except sqlite.OperationalError: # new db - create_tables() - except sqlite.DatabaseError, e: # corrupted db - logging.warning('Could not create picture cache because of an error: %s', str(e)) - self.con.close() - os.remove(db) - if threaded: - self.con = hsutil.sqlite.ThreadedConn(db, True) - else: - self.con = sqlite.connect(db, isolation_level=None) - create_tables() - - def __contains__(self, key): - sql = "select count(*) from pictures where path = ?" - result = self.con.execute(sql, [key]).fetchall() - return result[0][0] > 0 - - def __delitem__(self, key): - if key not in self: - raise KeyError(key) - sql = "delete from pictures where path = ?" - self.con.execute(sql, [key]) - - # Optimized - def __getitem__(self, key): - if isinstance(key, int): - sql = "select blocks from pictures where rowid = ?" - else: - sql = "select blocks from pictures where path = ?" - result = self.con.execute(sql, [key]).fetchone() - if result: - result = string_to_colors(result[0]) - return result - else: - raise KeyError(key) - - def __iter__(self): - sql = "select path from pictures" - result = self.con.execute(sql) - return (row[0] for row in result) - - def __len__(self): - sql = "select count(*) from pictures" - result = self.con.execute(sql).fetchall() - return result[0][0] - - def __setitem__(self, key, value): - value = colors_to_string(value) - if key in self: - sql = "update pictures set blocks = ? where path = ?" - else: - sql = "insert into pictures(blocks,path) values(?,?)" - try: - self.con.execute(sql, [value, key]) - except sqlite.OperationalError: - logging.warning('Picture cache could not set %r for key %r', value, key) - except sqlite.DatabaseError, e: - logging.warning('DatabaseError while setting %r for key %r: %s', value, key, str(e)) - - def clear(self): - sql = "delete from pictures" - self.con.execute(sql) - - def filter(self, func): - to_delete = [key for key in self if not func(key)] - for key in to_delete: - del self[key] - - def get_id(self, path): - sql = "select rowid from pictures where path = ?" - result = self.con.execute(sql, [path]).fetchone() - if result: - return result[0] - else: - raise ValueError(path) - - def get_multiple(self, rowids): - sql = "select rowid, blocks from pictures where rowid in (%s)" % ','.join(map(str, rowids)) - cur = self.con.execute(sql) - return ((rowid, string_to_colors(blocks)) for rowid, blocks in cur) - diff --git a/pe/qt/dupeguru/picture/cache_test.py b/pe/qt/dupeguru/picture/cache_test.py deleted file mode 100644 index f453112f..00000000 --- a/pe/qt/dupeguru/picture/cache_test.py +++ /dev/null @@ -1,159 +0,0 @@ -#!/usr/bin/env python -""" -Unit Name: tests.picture.cache -Created By: Virgil Dupras -Created On: 2006/09/14 -Last modified by:$Author: virgil $ -Last modified on:$Date: 2009-05-28 15:22:39 +0200 (Thu, 28 May 2009) $ - $Revision: 4385 $ -Copyright 2004-2006 Hardcoded Software (http://www.hardcoded.net) -""" -import unittest -from StringIO import StringIO -import os.path as op -import os -import threading - -from hsutil.testcase import TestCase -from .cache import * - -class TCcolors_to_string(unittest.TestCase): - def test_no_color(self): - self.assertEqual('',colors_to_string([])) - - def test_single_color(self): - self.assertEqual('000000',colors_to_string([(0,0,0)])) - self.assertEqual('010101',colors_to_string([(1,1,1)])) - self.assertEqual('0a141e',colors_to_string([(10,20,30)])) - - def test_two_colors(self): - self.assertEqual('000102030405',colors_to_string([(0,1,2),(3,4,5)])) - - -class TCstring_to_colors(unittest.TestCase): - def test_empty(self): - self.assertEqual([],string_to_colors('')) - - def test_single_color(self): - self.assertEqual([(0,0,0)],string_to_colors('000000')) - self.assertEqual([(2,3,4)],string_to_colors('020304')) - self.assertEqual([(10,20,30)],string_to_colors('0a141e')) - - def test_two_colors(self): - self.assertEqual([(10,20,30),(40,50,60)],string_to_colors('0a141e28323c')) - - def test_incomplete_color(self): - # don't return anything if it's not a complete color - self.assertEqual([],string_to_colors('102')) - - -class TCCache(TestCase): - def test_empty(self): - c = Cache() - self.assertEqual(0,len(c)) - self.assertRaises(KeyError,c.__getitem__,'foo') - - def test_set_then_retrieve_blocks(self): - c = Cache() - b = [(0,0,0),(1,2,3)] - c['foo'] = b - self.assertEqual(b,c['foo']) - - def test_delitem(self): - c = Cache() - c['foo'] = '' - del c['foo'] - self.assert_('foo' not in c) - self.assertRaises(KeyError,c.__delitem__,'foo') - - def test_persistance(self): - DBNAME = op.join(self.tmpdir(), 'hstest.db') - c = Cache(DBNAME) - c['foo'] = [(1,2,3)] - del c - c = Cache(DBNAME) - self.assertEqual([(1,2,3)],c['foo']) - del c - os.remove(DBNAME) - - def test_filter(self): - c = Cache() - c['foo'] = '' - c['bar'] = '' - c['baz'] = '' - c.filter(lambda p:p != 'bar') #only 'bar' is removed - self.assertEqual(2,len(c)) - self.assert_('foo' in c) - self.assert_('baz' in c) - self.assert_('bar' not in c) - - def test_clear(self): - c = Cache() - c['foo'] = '' - c['bar'] = '' - c['baz'] = '' - c.clear() - self.assertEqual(0,len(c)) - self.assert_('foo' not in c) - self.assert_('baz' not in c) - self.assert_('bar' not in c) - - def test_corrupted_db(self): - dbname = op.join(self.tmpdir(), 'foo.db') - fp = open(dbname, 'w') - fp.write('invalid sqlite content') - fp.close() - c = Cache(dbname) # should not raise a DatabaseError - c['foo'] = [(1, 2, 3)] - del c - c = Cache(dbname) - self.assertEqual(c['foo'], [(1, 2, 3)]) - - def test_by_id(self): - # it's possible to use the cache by referring to the files by their row_id - c = Cache() - b = [(0,0,0),(1,2,3)] - c['foo'] = b - foo_id = c.get_id('foo') - self.assertEqual(c[foo_id], b) - - -class TCCacheSQLEscape(unittest.TestCase): - def test_contains(self): - c = Cache() - self.assert_("foo'bar" not in c) - - def test_getitem(self): - c = Cache() - self.assertRaises(KeyError, c.__getitem__, "foo'bar") - - def test_setitem(self): - c = Cache() - c["foo'bar"] = [] - - def test_delitem(self): - c = Cache() - c["foo'bar"] = [] - try: - del c["foo'bar"] - except KeyError: - self.fail() - - -class TCCacheThreaded(unittest.TestCase): - def test_access_cache(self): - def thread_run(): - try: - c['foo'] = [(1,2,3)] - except sqlite.ProgrammingError: - self.fail() - - c = Cache() - t = threading.Thread(target=thread_run) - t.start() - t.join() - self.assertEqual([(1,2,3)], c['foo']) - - -if __name__ == "__main__": - unittest.main() \ No newline at end of file diff --git a/pe/qt/dupeguru/picture/matchbase.py b/pe/qt/dupeguru/picture/matchbase.py deleted file mode 100644 index cf0d1e89..00000000 --- a/pe/qt/dupeguru/picture/matchbase.py +++ /dev/null @@ -1,136 +0,0 @@ -#!/usr/bin/env python -""" -Unit Name: hs.picture._match -Created By: Virgil Dupras -Created On: 2007/02/25 -Last modified by:$Author: virgil $ -Last modified on:$Date: 2009-05-28 16:02:48 +0200 (Thu, 28 May 2009) $ - $Revision: 4388 $ -Copyright 2007 Hardcoded Software (http://www.hardcoded.net) -""" -import logging -import multiprocessing -from Queue import Empty -from collections import defaultdict - -from hsutil import job -from hs.utils.misc import dedupe - -from dupeguru.engine import Match -from block import avgdiff, DifferentBlockCountError, NoBlocksError -from cache import Cache - -MIN_ITERATIONS = 3 - -def get_match(first,second,percentage): - if percentage < 0: - percentage = 0 - return Match(first,second,percentage) - -class MatchFactory(object): - cached_blocks = None - block_count_per_side = 15 - threshold = 75 - match_scaled = False - - def _do_getmatches(self, files, j): - raise NotImplementedError() - - def getmatches(self, files, j=job.nulljob): - # The MemoryError handlers in there use logging without first caring about whether or not - # there is enough memory left to carry on the operation because it is assumed that the - # MemoryError happens when trying to read an image file, which is freed from memory by the - # time that MemoryError is raised. - j = j.start_subjob([2, 8]) - logging.info('Preparing %d files' % len(files)) - prepared = self.prepare_files(files, j) - logging.info('Finished preparing %d files' % len(prepared)) - return self._do_getmatches(prepared, j) - - def prepare_files(self, files, j=job.nulljob): - prepared = [] # only files for which there was no error getting blocks - try: - for picture in j.iter_with_progress(files, 'Analyzed %d/%d pictures'): - picture.dimensions - picture.unicode_path = unicode(picture.path) - try: - if picture.unicode_path not in self.cached_blocks: - blocks = picture.get_blocks(self.block_count_per_side) - self.cached_blocks[picture.unicode_path] = blocks - prepared.append(picture) - except IOError as e: - logging.warning(unicode(e)) - except MemoryError: - logging.warning(u'Ran out of memory while reading %s of size %d' % (picture.unicode_path, picture.size)) - if picture.size < 10 * 1024 * 1024: # We're really running out of memory - raise - except MemoryError: - logging.warning('Ran out of memory while preparing files') - return prepared - - -def async_compare(ref_id, other_ids, dbname, threshold): - cache = Cache(dbname, threaded=False) - limit = 100 - threshold - ref_blocks = cache[ref_id] - pairs = cache.get_multiple(other_ids) - results = [] - for other_id, other_blocks in pairs: - try: - diff = avgdiff(ref_blocks, other_blocks, limit, MIN_ITERATIONS) - percentage = 100 - diff - except (DifferentBlockCountError, NoBlocksError): - percentage = 0 - if percentage >= threshold: - results.append((ref_id, other_id, percentage)) - cache.con.close() - return results - -class AsyncMatchFactory(MatchFactory): - def _do_getmatches(self, pictures, j): - def empty_out_queue(queue, into): - try: - while True: - into.append(queue.get(block=False)) - except Empty: - pass - - j = j.start_subjob([1, 8, 1], 'Preparing for matching') - cache = self.cached_blocks - id2picture = {} - dimensions2pictures = defaultdict(set) - for picture in pictures[:]: - try: - picture.cache_id = cache.get_id(picture.unicode_path) - id2picture[picture.cache_id] = picture - except ValueError: - pictures.remove(picture) - if not self.match_scaled: - dimensions2pictures[picture.dimensions].add(picture) - pool = multiprocessing.Pool() - async_results = [] - pictures_copy = set(pictures) - for ref in j.iter_with_progress(pictures): - others = pictures_copy if self.match_scaled else dimensions2pictures[ref.dimensions] - others.remove(ref) - if others: - cache_ids = [f.cache_id for f in others] - args = (ref.cache_id, cache_ids, self.cached_blocks.dbname, self.threshold) - async_results.append(pool.apply_async(async_compare, args)) - - matches = [] - for result in j.iter_with_progress(async_results, 'Matched %d/%d pictures'): - matches.extend(result.get()) - - result = [] - for ref_id, other_id, percentage in j.iter_with_progress(matches, 'Verified %d/%d matches', every=10): - ref = id2picture[ref_id] - other = id2picture[other_id] - if percentage == 100 and ref.md5 != other.md5: - percentage = 99 - if percentage >= self.threshold: - result.append(get_match(ref, other, percentage)) - return result - - -multiprocessing.freeze_support() \ No newline at end of file diff --git a/pe/qt/dupeguru/results.py b/pe/qt/dupeguru/results.py deleted file mode 100644 index a7ded5c0..00000000 --- a/pe/qt/dupeguru/results.py +++ /dev/null @@ -1,359 +0,0 @@ -#!/usr/bin/env python -""" -Unit Name: dupeguru.results -Created By: Virgil Dupras -Created On: 2006/02/23 -Last modified by:$Author: virgil $ -Last modified on:$Date: 2009-05-28 16:33:32 +0200 (Thu, 28 May 2009) $ - $Revision: 4392 $ -Copyright 2004-2006 Hardcoded Software (http://www.hardcoded.net) -""" -import re -from xml.sax import handler, make_parser, SAXException -from xml.sax.saxutils import XMLGenerator -from xml.sax.xmlreader import AttributesImpl - -from . import engine -from hsutil.job import nulljob -from hsutil.markable import Markable -from hsutil.misc import flatten, cond, nonone -from hsutil.str import format_size -from hsutil.files import open_if_filename - -class Results(Markable): - #---Override - def __init__(self, data_module): - super(Results, self).__init__() - self.__groups = [] - self.__group_of_duplicate = {} - self.__groups_sort_descriptor = None # This is a tuple (key, asc) - self.__dupes = None - self.__dupes_sort_descriptor = None # This is a tuple (key, asc, delta) - self.__filters = None - self.__filtered_dupes = None - self.__filtered_groups = None - self.__recalculate_stats() - self.__marked_size = 0 - self.data = data_module - - def _did_mark(self, dupe): - self.__marked_size += dupe.size - - def _did_unmark(self, dupe): - self.__marked_size -= dupe.size - - def _get_markable_count(self): - return self.__total_count - - def _is_markable(self, dupe): - if dupe.is_ref: - return False - g = self.get_group_of_duplicate(dupe) - if not g: - return False - if dupe is g.ref: - return False - if self.__filtered_dupes and dupe not in self.__filtered_dupes: - return False - return True - - #---Private - def __get_dupe_list(self): - if self.__dupes is None: - self.__dupes = flatten(group.dupes for group in self.groups) - if self.__filtered_dupes: - self.__dupes = [dupe for dupe in self.__dupes if dupe in self.__filtered_dupes] - sd = self.__dupes_sort_descriptor - if sd: - self.sort_dupes(sd[0], sd[1], sd[2]) - return self.__dupes - - def __get_groups(self): - if self.__filtered_groups is None: - return self.__groups - else: - return self.__filtered_groups - - def __get_stat_line(self): - if self.__filtered_dupes is None: - mark_count = self.mark_count - marked_size = self.__marked_size - total_count = self.__total_count - total_size = self.__total_size - else: - mark_count = len([dupe for dupe in self.__filtered_dupes if self.is_marked(dupe)]) - marked_size = sum(dupe.size for dupe in self.__filtered_dupes if self.is_marked(dupe)) - total_count = len([dupe for dupe in self.__filtered_dupes if self.is_markable(dupe)]) - total_size = sum(dupe.size for dupe in self.__filtered_dupes if self.is_markable(dupe)) - if self.mark_inverted: - marked_size = self.__total_size - marked_size - result = '%d / %d (%s / %s) duplicates marked.' % ( - mark_count, - total_count, - format_size(marked_size, 2), - format_size(total_size, 2), - ) - if self.__filters: - result += ' filter: %s' % ' --> '.join(self.__filters) - return result - - def __recalculate_stats(self): - self.__total_size = 0 - self.__total_count = 0 - for group in self.groups: - markable = [dupe for dupe in group.dupes if self._is_markable(dupe)] - self.__total_count += len(markable) - self.__total_size += sum(dupe.size for dupe in markable) - - def __set_groups(self, new_groups): - self.mark_none() - self.__groups = new_groups - self.__group_of_duplicate = {} - for g in self.__groups: - for dupe in g: - self.__group_of_duplicate[dupe] = g - if not hasattr(dupe, 'is_ref'): - dupe.is_ref = False - old_filters = nonone(self.__filters, []) - self.apply_filter(None) - for filter_str in old_filters: - self.apply_filter(filter_str) - - #---Public - def apply_filter(self, filter_str): - ''' Applies a filter 'filter_str' to self.groups - - When you apply the filter, only dupes with the filename matching 'filter_str' will be in - in the results. To cancel the filter, just call apply_filter with 'filter_str' to None, - and the results will go back to normal. - - If call apply_filter on a filtered results, the filter will be applied - *on the filtered results*. - - 'filter_str' is a string containing a regexp to filter dupes with. - ''' - if not filter_str: - self.__filtered_dupes = None - self.__filtered_groups = None - self.__filters = None - else: - if not self.__filters: - self.__filters = [] - self.__filters.append(filter_str) - filter_re = re.compile(filter_str, re.IGNORECASE) - if self.__filtered_dupes is None: - self.__filtered_dupes = flatten(g[:] for g in self.groups) - self.__filtered_dupes = set(dupe for dupe in self.__filtered_dupes if filter_re.search(dupe.name)) - filtered_groups = set() - for dupe in self.__filtered_dupes: - filtered_groups.add(self.get_group_of_duplicate(dupe)) - self.__filtered_groups = list(filtered_groups) - self.__recalculate_stats() - sd = self.__groups_sort_descriptor - if sd: - self.sort_groups(sd[0], sd[1]) - self.__dupes = None - - def get_group_of_duplicate(self, dupe): - try: - return self.__group_of_duplicate[dupe] - except (TypeError, KeyError): - return None - - is_markable = _is_markable - - def load_from_xml(self, infile, get_file, j=nulljob): - self.apply_filter(None) - handler = _ResultsHandler(get_file) - parser = make_parser() - parser.setContentHandler(handler) - try: - infile, must_close = open_if_filename(infile) - except IOError: - return - BUFSIZE = 1024 * 1024 # 1mb buffer - infile.seek(0, 2) - j.start_job(infile.tell() // BUFSIZE) - infile.seek(0, 0) - try: - while True: - data = infile.read(BUFSIZE) - if not data: - break - parser.feed(data) - j.add_progress() - except SAXException: - return - self.groups = handler.groups - for dupe_file in handler.marked: - self.mark(dupe_file) - - def make_ref(self, dupe): - g = self.get_group_of_duplicate(dupe) - r = g.ref - self._remove_mark_flag(dupe) - g.switch_ref(dupe); - if not r.is_ref: - self.__total_count += 1 - self.__total_size += r.size - if not dupe.is_ref: - self.__total_count -= 1 - self.__total_size -= dupe.size - self.__dupes = None - - def perform_on_marked(self, func, remove_from_results): - problems = [] - for d in self.dupes: - if self.is_marked(d) and (not func(d)): - problems.append(d) - if remove_from_results: - to_remove = [d for d in self.dupes if self.is_marked(d) and (d not in problems)] - self.remove_duplicates(to_remove) - self.mark_none() - for d in problems: - self.mark(d) - return len(problems) - - def remove_duplicates(self, dupes): - '''Remove 'dupes' from their respective group, and remove the group is it ends up empty. - ''' - affected_groups = set() - for dupe in dupes: - group = self.get_group_of_duplicate(dupe) - if dupe not in group.dupes: - return - group.remove_dupe(dupe, False) - self._remove_mark_flag(dupe) - self.__total_count -= 1 - self.__total_size -= dupe.size - if not group: - self.__groups.remove(group) - if self.__filtered_groups: - self.__filtered_groups.remove(group) - else: - affected_groups.add(group) - for group in affected_groups: - group.clean_matches() - self.__dupes = None - - def save_to_xml(self, outfile, with_data=False): - self.apply_filter(None) - outfile, must_close = open_if_filename(outfile, 'wb') - writer = XMLGenerator(outfile, 'utf-8') - writer.startDocument() - empty_attrs = AttributesImpl({}) - writer.startElement('results', empty_attrs) - for g in self.groups: - writer.startElement('group', empty_attrs) - dupe2index = {} - for index, d in enumerate(g): - dupe2index[d] = index - try: - words = engine.unpack_fields(d.words) - except AttributeError: - words = () - attrs = AttributesImpl({ - 'path': unicode(d.path), - 'is_ref': cond(d.is_ref, 'y', 'n'), - 'words': ','.join(words), - 'marked': cond(self.is_marked(d), 'y', 'n') - }) - writer.startElement('file', attrs) - if with_data: - data_list = self.data.GetDisplayInfo(d, g) - for data in data_list: - attrs = AttributesImpl({ - 'value': data, - }) - writer.startElement('data', attrs) - writer.endElement('data') - writer.endElement('file') - for match in g.matches: - attrs = AttributesImpl({ - 'first': str(dupe2index[match.first]), - 'second': str(dupe2index[match.second]), - 'percentage': str(int(match.percentage)), - }) - writer.startElement('match', attrs) - writer.endElement('match') - writer.endElement('group') - writer.endElement('results') - writer.endDocument() - if must_close: - outfile.close() - - def sort_dupes(self, key, asc=True, delta=False): - if not self.__dupes: - self.__get_dupe_list() - self.__dupes.sort(key=lambda d: self.data.GetDupeSortKey(d, lambda: self.get_group_of_duplicate(d), key, delta)) - if not asc: - self.__dupes.reverse() - self.__dupes_sort_descriptor = (key,asc,delta) - - def sort_groups(self,key,asc=True): - self.groups.sort(key=lambda g: self.data.GetGroupSortKey(g, key)) - if not asc: - self.groups.reverse() - self.__groups_sort_descriptor = (key,asc) - - #---Properties - dupes = property(__get_dupe_list) - groups = property(__get_groups, __set_groups) - stat_line = property(__get_stat_line) - -class _ResultsHandler(handler.ContentHandler): - def __init__(self, get_file): - self.group = None - self.dupes = None - self.marked = set() - self.groups = [] - self.get_file = get_file - - def startElement(self, name, attrs): - if name == 'group': - self.group = engine.Group() - self.dupes = [] - return - if (name == 'file') and (self.group is not None): - if not (('path' in attrs) and ('words' in attrs)): - return - path = attrs['path'] - file = self.get_file(path) - if file is None: - return - file.words = attrs['words'].split(',') - file.is_ref = attrs.get('is_ref') == 'y' - self.dupes.append(file) - if attrs.get('marked') == 'y': - self.marked.add(file) - if (name == 'match') and (self.group is not None): - try: - first_file = self.dupes[int(attrs['first'])] - second_file = self.dupes[int(attrs['second'])] - percentage = int(attrs['percentage']) - self.group.add_match(engine.Match(first_file, second_file, percentage)) - except (IndexError, KeyError, ValueError): # Covers missing attr, non-int values and indexes out of bounds - pass - - def endElement(self, name): - def do_match(ref_file, other_files, group): - if not other_files: - return - for other_file in other_files: - group.add_match(engine.get_match(ref_file, other_file)) - do_match(other_files[0], other_files[1:], group) - - if name == 'group': - group = self.group - self.group = None - dupes = self.dupes - self.dupes = [] - if group is None: - return - if len(dupes) < 2: - return - if not group.matches: # elements not present, do it manually, without % - do_match(dupes[0], dupes[1:], group) - group.prioritize(lambda x: dupes.index(x)) - self.groups.append(group) - diff --git a/pe/qt/dupeguru/results_test.py b/pe/qt/dupeguru/results_test.py deleted file mode 100644 index 1e74efc6..00000000 --- a/pe/qt/dupeguru/results_test.py +++ /dev/null @@ -1,742 +0,0 @@ -#!/usr/bin/env python -""" -Unit Name: dupeguru.tests.results -Created By: Virgil Dupras -Created On: 2006/02/23 -Last modified by:$Author: virgil $ -Last modified on:$Date: 2009-05-28 15:22:39 +0200 (Thu, 28 May 2009) $ - $Revision: 4385 $ -Copyright 2004-2006 Hardcoded Software (http://www.hardcoded.net) -""" -import unittest -import StringIO -import xml.dom.minidom -import os.path as op - -from hsutil.path import Path -from hsutil.testcase import TestCase -from hsutil.misc import first - -from . import engine_test -from . import data -from . import engine -from .results import * - -class NamedObject(engine_test.NamedObject): - size = 1 - path = property(lambda x:Path('basepath') + x.name) - is_ref = False - - def __nonzero__(self): - return False #Make sure that operations are made correctly when the bool value of files is false. - -# Returns a group set that looks like that: -# "foo bar" (1) -# "bar bleh" (1024) -# "foo bleh" (1) -# "ibabtu" (1) -# "ibabtu" (1) -def GetTestGroups(): - objects = [NamedObject("foo bar"),NamedObject("bar bleh"),NamedObject("foo bleh"),NamedObject("ibabtu"),NamedObject("ibabtu")] - objects[1].size = 1024 - matches = engine.MatchFactory().getmatches(objects) #we should have 5 matches - groups = engine.get_groups(matches) #We should have 2 groups - for g in groups: - g.prioritize(lambda x:objects.index(x)) #We want the dupes to be in the same order as the list is - groups.sort(key=len, reverse=True) # We want the group with 3 members to be first. - return (objects,matches,groups) - -class TCResultsEmpty(TestCase): - def setUp(self): - self.results = Results(data) - - def test_stat_line(self): - self.assertEqual("0 / 0 (0.00 B / 0.00 B) duplicates marked.",self.results.stat_line) - - def test_groups(self): - self.assertEqual(0,len(self.results.groups)) - - def test_get_group_of_duplicate(self): - self.assert_(self.results.get_group_of_duplicate('foo') is None) - - def test_save_to_xml(self): - f = StringIO.StringIO() - self.results.save_to_xml(f) - f.seek(0) - doc = xml.dom.minidom.parse(f) - root = doc.documentElement - self.assertEqual('results',root.nodeName) - - -class TCResultsWithSomeGroups(TestCase): - def setUp(self): - self.results = Results(data) - self.objects,self.matches,self.groups = GetTestGroups() - self.results.groups = self.groups - - def test_stat_line(self): - self.assertEqual("0 / 3 (0.00 B / 1.01 KB) duplicates marked.",self.results.stat_line) - - def test_groups(self): - self.assertEqual(2,len(self.results.groups)) - - def test_get_group_of_duplicate(self): - for o in self.objects: - g = self.results.get_group_of_duplicate(o) - self.assert_(isinstance(g, engine.Group)) - self.assert_(o in g) - self.assert_(self.results.get_group_of_duplicate(self.groups[0]) is None) - - def test_remove_duplicates(self): - g1,g2 = self.results.groups - self.results.remove_duplicates([g1.dupes[0]]) - self.assertEqual(2,len(g1)) - self.assert_(g1 in self.results.groups) - self.results.remove_duplicates([g1.ref]) - self.assertEqual(2,len(g1)) - self.assert_(g1 in self.results.groups) - self.results.remove_duplicates([g1.dupes[0]]) - self.assertEqual(0,len(g1)) - self.assert_(g1 not in self.results.groups) - self.results.remove_duplicates([g2.dupes[0]]) - self.assertEqual(0,len(g2)) - self.assert_(g2 not in self.results.groups) - self.assertEqual(0,len(self.results.groups)) - - def test_remove_duplicates_with_ref_files(self): - g1,g2 = self.results.groups - self.objects[0].is_ref = True - self.objects[1].is_ref = True - self.results.remove_duplicates([self.objects[2]]) - self.assertEqual(0,len(g1)) - self.assert_(g1 not in self.results.groups) - - def test_make_ref(self): - g = self.results.groups[0] - d = g.dupes[0] - self.results.make_ref(d) - self.assert_(d is g.ref) - - def test_sort_groups(self): - self.results.make_ref(self.objects[1]) #We want to make the 1024 sized object to go ref. - g1,g2 = self.groups - self.results.sort_groups(2) #2 is the key for size - self.assert_(self.results.groups[0] is g2) - self.assert_(self.results.groups[1] is g1) - self.results.sort_groups(2,False) - self.assert_(self.results.groups[0] is g1) - self.assert_(self.results.groups[1] is g2) - - def test_set_groups_when_sorted(self): - self.results.make_ref(self.objects[1]) #We want to make the 1024 sized object to go ref. - self.results.sort_groups(2) - objects,matches,groups = GetTestGroups() - g1,g2 = groups - g1.switch_ref(objects[1]) - self.results.groups = groups - self.assert_(self.results.groups[0] is g2) - self.assert_(self.results.groups[1] is g1) - - def test_get_dupe_list(self): - self.assertEqual([self.objects[1],self.objects[2],self.objects[4]],self.results.dupes) - - def test_dupe_list_is_cached(self): - self.assert_(self.results.dupes is self.results.dupes) - - def test_dupe_list_cache_is_invalidated_when_needed(self): - o1,o2,o3,o4,o5 = self.objects - self.assertEqual([o2,o3,o5],self.results.dupes) - self.results.make_ref(o2) - self.assertEqual([o1,o3,o5],self.results.dupes) - objects,matches,groups = GetTestGroups() - o1,o2,o3,o4,o5 = objects - self.results.groups = groups - self.assertEqual([o2,o3,o5],self.results.dupes) - - def test_dupe_list_sort(self): - o1,o2,o3,o4,o5 = self.objects - o1.size = 5 - o2.size = 4 - o3.size = 3 - o4.size = 2 - o5.size = 1 - self.results.sort_dupes(2) - self.assertEqual([o5,o3,o2],self.results.dupes) - self.results.sort_dupes(2,False) - self.assertEqual([o2,o3,o5],self.results.dupes) - - def test_dupe_list_remember_sort(self): - o1,o2,o3,o4,o5 = self.objects - o1.size = 5 - o2.size = 4 - o3.size = 3 - o4.size = 2 - o5.size = 1 - self.results.sort_dupes(2) - self.results.make_ref(o2) - self.assertEqual([o5,o3,o1],self.results.dupes) - - def test_dupe_list_sort_delta_values(self): - o1,o2,o3,o4,o5 = self.objects - o1.size = 10 - o2.size = 2 #-8 - o3.size = 3 #-7 - o4.size = 20 - o5.size = 1 #-19 - self.results.sort_dupes(2,delta=True) - self.assertEqual([o5,o2,o3],self.results.dupes) - - def test_sort_empty_list(self): - #There was an infinite loop when sorting an empty list. - r = Results(data) - r.sort_dupes(0) - self.assertEqual([],r.dupes) - - def test_dupe_list_update_on_remove_duplicates(self): - o1,o2,o3,o4,o5 = self.objects - self.assertEqual(3,len(self.results.dupes)) - self.results.remove_duplicates([o2]) - self.assertEqual(2,len(self.results.dupes)) - - -class TCResultsMarkings(TestCase): - def setUp(self): - self.results = Results(data) - self.objects,self.matches,self.groups = GetTestGroups() - self.results.groups = self.groups - - def test_stat_line(self): - self.assertEqual("0 / 3 (0.00 B / 1.01 KB) duplicates marked.",self.results.stat_line) - self.results.mark(self.objects[1]) - self.assertEqual("1 / 3 (1.00 KB / 1.01 KB) duplicates marked.",self.results.stat_line) - self.results.mark_invert() - self.assertEqual("2 / 3 (2.00 B / 1.01 KB) duplicates marked.",self.results.stat_line) - self.results.mark_invert() - self.results.unmark(self.objects[1]) - self.results.mark(self.objects[2]) - self.results.mark(self.objects[4]) - self.assertEqual("2 / 3 (2.00 B / 1.01 KB) duplicates marked.",self.results.stat_line) - self.results.mark(self.objects[0]) #this is a ref, it can't be counted - self.assertEqual("2 / 3 (2.00 B / 1.01 KB) duplicates marked.",self.results.stat_line) - self.results.groups = self.groups - self.assertEqual("0 / 3 (0.00 B / 1.01 KB) duplicates marked.",self.results.stat_line) - - def test_with_ref_duplicate(self): - self.objects[1].is_ref = True - self.results.groups = self.groups - self.assert_(not self.results.mark(self.objects[1])) - self.results.mark(self.objects[2]) - self.assertEqual("1 / 2 (1.00 B / 2.00 B) duplicates marked.",self.results.stat_line) - - def test_perform_on_marked(self): - def log_object(o): - log.append(o) - return True - - log = [] - self.results.mark_all() - self.results.perform_on_marked(log_object,False) - self.assert_(self.objects[1] in log) - self.assert_(self.objects[2] in log) - self.assert_(self.objects[4] in log) - self.assertEqual(3,len(log)) - log = [] - self.results.mark_none() - self.results.mark(self.objects[4]) - self.results.perform_on_marked(log_object,True) - self.assertEqual(1,len(log)) - self.assert_(self.objects[4] in log) - self.assertEqual(1,len(self.results.groups)) - - def test_perform_on_marked_with_problems(self): - def log_object(o): - log.append(o) - return o is not self.objects[1] - - log = [] - self.results.mark_all() - self.assert_(self.results.is_marked(self.objects[1])) - self.assertEqual(1,self.results.perform_on_marked(log_object, True)) - self.assertEqual(3,len(log)) - self.assertEqual(1,len(self.results.groups)) - self.assertEqual(2,len(self.results.groups[0])) - self.assert_(self.objects[1] in self.results.groups[0]) - self.assert_(not self.results.is_marked(self.objects[2])) - self.assert_(self.results.is_marked(self.objects[1])) - - def test_perform_on_marked_with_ref(self): - def log_object(o): - log.append(o) - return True - - log = [] - self.objects[0].is_ref = True - self.objects[1].is_ref = True - self.results.mark_all() - self.results.perform_on_marked(log_object,True) - self.assert_(self.objects[1] not in log) - self.assert_(self.objects[2] in log) - self.assert_(self.objects[4] in log) - self.assertEqual(2,len(log)) - self.assertEqual(0,len(self.results.groups)) - - def test_perform_on_marked_remove_objects_only_at_the_end(self): - def check_groups(o): - self.assertEqual(3,len(g1)) - self.assertEqual(2,len(g2)) - return True - - g1,g2 = self.results.groups - self.results.mark_all() - self.results.perform_on_marked(check_groups,True) - self.assertEqual(0,len(g1)) - self.assertEqual(0,len(g2)) - self.assertEqual(0,len(self.results.groups)) - - def test_remove_duplicates(self): - g1 = self.results.groups[0] - g2 = self.results.groups[1] - self.results.mark(g1.dupes[0]) - self.assertEqual("1 / 3 (1.00 KB / 1.01 KB) duplicates marked.",self.results.stat_line) - self.results.remove_duplicates([g1.dupes[1]]) - self.assertEqual("1 / 2 (1.00 KB / 1.01 KB) duplicates marked.",self.results.stat_line) - self.results.remove_duplicates([g1.dupes[0]]) - self.assertEqual("0 / 1 (0.00 B / 1.00 B) duplicates marked.",self.results.stat_line) - - def test_make_ref(self): - g = self.results.groups[0] - d = g.dupes[0] - self.results.mark(d) - self.assertEqual("1 / 3 (1.00 KB / 1.01 KB) duplicates marked.",self.results.stat_line) - self.results.make_ref(d) - self.assertEqual("0 / 3 (0.00 B / 3.00 B) duplicates marked.",self.results.stat_line) - self.results.make_ref(d) - self.assertEqual("0 / 3 (0.00 B / 3.00 B) duplicates marked.",self.results.stat_line) - - def test_SaveXML(self): - self.results.mark(self.objects[1]) - self.results.mark_invert() - f = StringIO.StringIO() - self.results.save_to_xml(f) - f.seek(0) - doc = xml.dom.minidom.parse(f) - root = doc.documentElement - g1,g2 = root.getElementsByTagName('group') - d1,d2,d3 = g1.getElementsByTagName('file') - self.assertEqual('n',d1.getAttributeNode('marked').nodeValue) - self.assertEqual('n',d2.getAttributeNode('marked').nodeValue) - self.assertEqual('y',d3.getAttributeNode('marked').nodeValue) - d1,d2 = g2.getElementsByTagName('file') - self.assertEqual('n',d1.getAttributeNode('marked').nodeValue) - self.assertEqual('y',d2.getAttributeNode('marked').nodeValue) - - def test_LoadXML(self): - def get_file(path): - return [f for f in self.objects if str(f.path) == path][0] - - self.objects[4].name = 'ibabtu 2' #we can't have 2 files with the same path - self.results.mark(self.objects[1]) - self.results.mark_invert() - f = StringIO.StringIO() - self.results.save_to_xml(f) - f.seek(0) - r = Results(data) - r.load_from_xml(f,get_file) - self.assert_(not r.is_marked(self.objects[0])) - self.assert_(not r.is_marked(self.objects[1])) - self.assert_(r.is_marked(self.objects[2])) - self.assert_(not r.is_marked(self.objects[3])) - self.assert_(r.is_marked(self.objects[4])) - - -class TCResultsXML(TestCase): - def setUp(self): - self.results = Results(data) - self.objects, self.matches, self.groups = GetTestGroups() - self.results.groups = self.groups - - def get_file(self, path): # use this as a callback for load_from_xml - return [o for o in self.objects if o.path == path][0] - - def test_save_to_xml(self): - self.objects[0].is_ref = True - self.objects[0].words = [['foo','bar']] - f = StringIO.StringIO() - self.results.save_to_xml(f) - f.seek(0) - doc = xml.dom.minidom.parse(f) - root = doc.documentElement - self.assertEqual('results',root.nodeName) - children = [c for c in root.childNodes if c.localName] - self.assertEqual(2,len(children)) - self.assertEqual(2,len([c for c in children if c.nodeName == 'group'])) - g1,g2 = children - children = [c for c in g1.childNodes if c.localName] - self.assertEqual(6,len(children)) - self.assertEqual(3,len([c for c in children if c.nodeName == 'file'])) - self.assertEqual(3,len([c for c in children if c.nodeName == 'match'])) - d1,d2,d3 = [c for c in children if c.nodeName == 'file'] - self.assertEqual(op.join('basepath','foo bar'),d1.getAttributeNode('path').nodeValue) - self.assertEqual(op.join('basepath','bar bleh'),d2.getAttributeNode('path').nodeValue) - self.assertEqual(op.join('basepath','foo bleh'),d3.getAttributeNode('path').nodeValue) - self.assertEqual('y',d1.getAttributeNode('is_ref').nodeValue) - self.assertEqual('n',d2.getAttributeNode('is_ref').nodeValue) - self.assertEqual('n',d3.getAttributeNode('is_ref').nodeValue) - self.assertEqual('foo,bar',d1.getAttributeNode('words').nodeValue) - self.assertEqual('bar,bleh',d2.getAttributeNode('words').nodeValue) - self.assertEqual('foo,bleh',d3.getAttributeNode('words').nodeValue) - children = [c for c in g2.childNodes if c.localName] - self.assertEqual(3,len(children)) - self.assertEqual(2,len([c for c in children if c.nodeName == 'file'])) - self.assertEqual(1,len([c for c in children if c.nodeName == 'match'])) - d1,d2 = [c for c in children if c.nodeName == 'file'] - self.assertEqual(op.join('basepath','ibabtu'),d1.getAttributeNode('path').nodeValue) - self.assertEqual(op.join('basepath','ibabtu'),d2.getAttributeNode('path').nodeValue) - self.assertEqual('n',d1.getAttributeNode('is_ref').nodeValue) - self.assertEqual('n',d2.getAttributeNode('is_ref').nodeValue) - self.assertEqual('ibabtu',d1.getAttributeNode('words').nodeValue) - self.assertEqual('ibabtu',d2.getAttributeNode('words').nodeValue) - - def test_save_to_xml_with_columns(self): - class FakeDataModule: - def GetDisplayInfo(self,dupe,group): - return [str(dupe.size),dupe.foo.upper()] - - for i,object in enumerate(self.objects): - object.size = i - object.foo = u'bar\u00e9' - f = StringIO.StringIO() - self.results.data = FakeDataModule() - self.results.save_to_xml(f,True) - f.seek(0) - doc = xml.dom.minidom.parse(f) - root = doc.documentElement - g1,g2 = root.getElementsByTagName('group') - d1,d2,d3 = g1.getElementsByTagName('file') - d4,d5 = g2.getElementsByTagName('file') - self.assertEqual('0',d1.getElementsByTagName('data')[0].getAttribute('value')) - self.assertEqual(u'BAR\u00c9',d1.getElementsByTagName('data')[1].getAttribute('value')) #\u00c9 is upper of \u00e9 - self.assertEqual('1',d2.getElementsByTagName('data')[0].getAttribute('value')) - self.assertEqual('2',d3.getElementsByTagName('data')[0].getAttribute('value')) - self.assertEqual('3',d4.getElementsByTagName('data')[0].getAttribute('value')) - self.assertEqual('4',d5.getElementsByTagName('data')[0].getAttribute('value')) - - def test_LoadXML(self): - def get_file(path): - return [f for f in self.objects if str(f.path) == path][0] - - self.objects[0].is_ref = True - self.objects[4].name = 'ibabtu 2' #we can't have 2 files with the same path - f = StringIO.StringIO() - self.results.save_to_xml(f) - f.seek(0) - r = Results(data) - r.load_from_xml(f,get_file) - self.assertEqual(2,len(r.groups)) - g1,g2 = r.groups - self.assertEqual(3,len(g1)) - self.assert_(g1[0].is_ref) - self.assert_(not g1[1].is_ref) - self.assert_(not g1[2].is_ref) - self.assert_(g1[0] is self.objects[0]) - self.assert_(g1[1] is self.objects[1]) - self.assert_(g1[2] is self.objects[2]) - self.assertEqual(['foo','bar'],g1[0].words) - self.assertEqual(['bar','bleh'],g1[1].words) - self.assertEqual(['foo','bleh'],g1[2].words) - self.assertEqual(2,len(g2)) - self.assert_(not g2[0].is_ref) - self.assert_(not g2[1].is_ref) - self.assert_(g2[0] is self.objects[3]) - self.assert_(g2[1] is self.objects[4]) - self.assertEqual(['ibabtu'],g2[0].words) - self.assertEqual(['ibabtu'],g2[1].words) - - def test_LoadXML_with_filename(self): - def get_file(path): - return [f for f in self.objects if str(f.path) == path][0] - - filename = op.join(self.tmpdir(), 'dupeguru_results.xml') - self.objects[4].name = 'ibabtu 2' #we can't have 2 files with the same path - self.results.save_to_xml(filename) - r = Results(data) - r.load_from_xml(filename,get_file) - self.assertEqual(2,len(r.groups)) - - def test_LoadXML_with_some_files_that_dont_exist_anymore(self): - def get_file(path): - if path.endswith('ibabtu 2'): - return None - return [f for f in self.objects if str(f.path) == path][0] - - self.objects[4].name = 'ibabtu 2' #we can't have 2 files with the same path - f = StringIO.StringIO() - self.results.save_to_xml(f) - f.seek(0) - r = Results(data) - r.load_from_xml(f,get_file) - self.assertEqual(1,len(r.groups)) - self.assertEqual(3,len(r.groups[0])) - - def test_LoadXML_missing_attributes_and_bogus_elements(self): - def get_file(path): - return [f for f in self.objects if str(f.path) == path][0] - - doc = xml.dom.minidom.Document() - root = doc.appendChild(doc.createElement('foobar')) #The root element shouldn't matter, really. - group_node = root.appendChild(doc.createElement('group')) - dupe_node = group_node.appendChild(doc.createElement('file')) #Perfectly correct file - dupe_node.setAttribute('path',op.join('basepath','foo bar')) - dupe_node.setAttribute('is_ref','y') - dupe_node.setAttribute('words','foo,bar') - dupe_node = group_node.appendChild(doc.createElement('file')) #is_ref missing, default to 'n' - dupe_node.setAttribute('path',op.join('basepath','foo bleh')) - dupe_node.setAttribute('words','foo,bleh') - dupe_node = group_node.appendChild(doc.createElement('file')) #words are missing, invalid. - dupe_node.setAttribute('path',op.join('basepath','bar bleh')) - dupe_node = group_node.appendChild(doc.createElement('file')) #path is missing, invalid. - dupe_node.setAttribute('words','foo,bleh') - dupe_node = group_node.appendChild(doc.createElement('foobar')) #Invalid element name - dupe_node.setAttribute('path',op.join('basepath','bar bleh')) - dupe_node.setAttribute('is_ref','y') - dupe_node.setAttribute('words','bar,bleh') - match_node = group_node.appendChild(doc.createElement('match')) # match pointing to a bad index - match_node.setAttribute('first', '42') - match_node.setAttribute('second', '45') - match_node = group_node.appendChild(doc.createElement('match')) # match with missing attrs - match_node = group_node.appendChild(doc.createElement('match')) # match with non-int values - match_node.setAttribute('first', 'foo') - match_node.setAttribute('second', 'bar') - match_node.setAttribute('percentage', 'baz') - group_node = root.appendChild(doc.createElement('foobar')) #invalid group - group_node = root.appendChild(doc.createElement('group')) #empty group - f = StringIO.StringIO() - doc.writexml(f,'\t','\t','\n',encoding='utf-8') - f.seek(0) - r = Results(data) - r.load_from_xml(f,get_file) - self.assertEqual(1,len(r.groups)) - self.assertEqual(2,len(r.groups[0])) - - def test_xml_non_ascii(self): - def get_file(path): - if path == op.join('basepath',u'\xe9foo bar'): - return objects[0] - if path == op.join('basepath',u'bar bleh'): - return objects[1] - - objects = [NamedObject(u"\xe9foo bar",True),NamedObject("bar bleh",True)] - matches = engine.MatchFactory().getmatches(objects) #we should have 5 matches - groups = engine.get_groups(matches) #We should have 2 groups - for g in groups: - g.prioritize(lambda x:objects.index(x)) #We want the dupes to be in the same order as the list is - results = Results(data) - results.groups = groups - f = StringIO.StringIO() - results.save_to_xml(f) - f.seek(0) - r = Results(data) - r.load_from_xml(f,get_file) - g = r.groups[0] - self.assertEqual(u"\xe9foo bar",g[0].name) - self.assertEqual(['efoo','bar'],g[0].words) - - def test_load_invalid_xml(self): - f = StringIO.StringIO() - f.write(' len(ref.path) - - def GetDupeGroups(self, files, j=job.nulljob): - j = j.start_subjob([8, 2]) - for f in [f for f in files if not hasattr(f, 'is_ref')]: - f.is_ref = False - if self.size_threshold: - files = [f for f in files if f.size >= self.size_threshold] - logging.info('Getting matches') - if self.match_factory is None: - matches = self._getmatches(files, j) - else: - matches = self.match_factory.getmatches(files, j) - logging.info('Found %d matches' % len(matches)) - if not self.mix_file_kind: - j.set_progress(100, 'Removing false matches') - matches = [m for m in matches if get_file_ext(m.first.name) == get_file_ext(m.second.name)] - if self.ignore_list: - j = j.start_subjob(2) - iter_matches = j.iter_with_progress(matches, 'Processed %d/%d matches against the ignore list') - matches = [m for m in iter_matches - if not self.ignore_list.AreIgnored(unicode(m.first.path), unicode(m.second.path))] - matched_files = dedupe([m.first for m in matches] + [m.second for m in matches]) - if self.scan_type in (SCAN_TYPE_CONTENT, SCAN_TYPE_CONTENT_AUDIO): - md5attrname = 'md5partial' if self.scan_type == SCAN_TYPE_CONTENT_AUDIO else 'md5' - md5 = lambda f: getattr(f, md5attrname) - j = j.start_subjob(2) - for matched_file in j.iter_with_progress(matched_files, 'Analyzed %d/%d matching files'): - md5(matched_file) - j.set_progress(100, 'Removing false matches') - matches = [m for m in matches if md5(m.first) == md5(m.second)] - words_for_content = ['--'] # We compared md5. No words were involved. - for m in matches: - m.first.words = words_for_content - m.second.words = words_for_content - logging.info('Grouping matches') - groups = engine.get_groups(matches, j) - groups = [g for g in groups if any(not f.is_ref for f in g)] - logging.info('Created %d groups' % len(groups)) - j.set_progress(100, 'Doing group prioritization') - for g in groups: - g.prioritize(self._key_func, self._tie_breaker) - matched_files = dedupe([m.first for m in matches] + [m.second for m in matches]) - self.discarded_file_count = len(matched_files) - sum(len(g) for g in groups) - return groups - - match_factory = None - match_similar_words = False - min_match_percentage = 80 - mix_file_kind = True - scan_type = SCAN_TYPE_FILENAME - scanned_tags = set(['artist', 'title']) - size_threshold = 0 - word_weighting = False - -class ScannerME(Scanner): # Scanner for Music Edition - @staticmethod - def _key_func(dupe): - return (not dupe.is_ref, -dupe.bitrate, -dupe.size) - diff --git a/pe/qt/dupeguru/scanner_test.py b/pe/qt/dupeguru/scanner_test.py deleted file mode 100644 index 89ad1417..00000000 --- a/pe/qt/dupeguru/scanner_test.py +++ /dev/null @@ -1,468 +0,0 @@ -#!/usr/bin/env python -""" -Unit Name: dupeguru.tests.scanner -Created By: Virgil Dupras -Created On: 2006/03/03 -Last modified by:$Author: virgil $ -Last modified on:$Date: 2009-05-28 15:22:39 +0200 (Thu, 28 May 2009) $ - $Revision: 4385 $ -Copyright 2004-2006 Hardcoded Software (http://www.hardcoded.net) -""" -import unittest - -from hsutil import job -from hsutil.path import Path -from hsutil.testcase import TestCase - -from .engine import getwords, Match -from .ignore import IgnoreList -from .scanner import * - -class NamedObject(object): - def __init__(self, name="foobar", size=1): - self.name = name - self.size = size - self.path = Path('') - self.words = getwords(name) - - -no = NamedObject - -class TCScanner(TestCase): - def test_empty(self): - s = Scanner() - r = s.GetDupeGroups([]) - self.assertEqual([],r) - - def test_default_settings(self): - s = Scanner() - self.assertEqual(80,s.min_match_percentage) - self.assertEqual(SCAN_TYPE_FILENAME,s.scan_type) - self.assertEqual(True,s.mix_file_kind) - self.assertEqual(False,s.word_weighting) - self.assertEqual(False,s.match_similar_words) - self.assert_(isinstance(s.ignore_list,IgnoreList)) - - def test_simple_with_default_settings(self): - s = Scanner() - f = [no('foo bar'),no('foo bar'),no('foo bleh')] - r = s.GetDupeGroups(f) - self.assertEqual(1,len(r)) - g = r[0] - #'foo bleh' cannot be in the group because the default min match % is 80 - self.assertEqual(2,len(g)) - self.assert_(g.ref in f[:2]) - self.assert_(g.dupes[0] in f[:2]) - - def test_simple_with_lower_min_match(self): - s = Scanner() - s.min_match_percentage = 50 - f = [no('foo bar'),no('foo bar'),no('foo bleh')] - r = s.GetDupeGroups(f) - self.assertEqual(1,len(r)) - g = r[0] - self.assertEqual(3,len(g)) - - def test_trim_all_ref_groups(self): - s = Scanner() - f = [no('foo'),no('foo'),no('bar'),no('bar')] - f[2].is_ref = True - f[3].is_ref = True - r = s.GetDupeGroups(f) - self.assertEqual(1,len(r)) - - def test_priorize(self): - s = Scanner() - f = [no('foo'),no('foo'),no('bar'),no('bar')] - f[1].size = 2 - f[2].size = 3 - f[3].is_ref = True - r = s.GetDupeGroups(f) - g1,g2 = r - self.assert_(f[1] in (g1.ref,g2.ref)) - self.assert_(f[0] in (g1.dupes[0],g2.dupes[0])) - self.assert_(f[3] in (g1.ref,g2.ref)) - self.assert_(f[2] in (g1.dupes[0],g2.dupes[0])) - - def test_content_scan(self): - s = Scanner() - s.scan_type = SCAN_TYPE_CONTENT - f = [no('foo'), no('bar'), no('bleh')] - f[0].md5 = 'foobar' - f[1].md5 = 'foobar' - f[2].md5 = 'bleh' - r = s.GetDupeGroups(f) - self.assertEqual(len(r), 1) - self.assertEqual(len(r[0]), 2) - self.assertEqual(s.discarded_file_count, 0) # don't count the different md5 as discarded! - - def test_content_scan_compare_sizes_first(self): - class MyFile(no): - def get_md5(file): - self.fail() - md5 = property(get_md5) - - s = Scanner() - s.scan_type = SCAN_TYPE_CONTENT - f = [MyFile('foo',1),MyFile('bar',2)] - self.assertEqual(0,len(s.GetDupeGroups(f))) - - def test_min_match_perc_doesnt_matter_for_content_scan(self): - s = Scanner() - s.scan_type = SCAN_TYPE_CONTENT - f = [no('foo'),no('bar'),no('bleh')] - f[0].md5 = 'foobar' - f[1].md5 = 'foobar' - f[2].md5 = 'bleh' - s.min_match_percentage = 101 - r = s.GetDupeGroups(f) - self.assertEqual(1,len(r)) - self.assertEqual(2,len(r[0])) - s.min_match_percentage = 0 - r = s.GetDupeGroups(f) - self.assertEqual(1,len(r)) - self.assertEqual(2,len(r[0])) - - def test_content_scan_puts_md5_in_words_at_the_end(self): - s = Scanner() - s.scan_type = SCAN_TYPE_CONTENT - f = [no('foo'),no('bar')] - f[0].md5 = '\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f' - f[1].md5 = '\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f' - r = s.GetDupeGroups(f) - g = r[0] - self.assertEqual(['--'],g.ref.words) - self.assertEqual(['--'],g.dupes[0].words) - - def test_extension_is_not_counted_in_filename_scan(self): - s = Scanner() - s.min_match_percentage = 100 - f = [no('foo.bar'),no('foo.bleh')] - r = s.GetDupeGroups(f) - self.assertEqual(1,len(r)) - self.assertEqual(2,len(r[0])) - - def test_job(self): - def do_progress(progress,desc=''): - log.append(progress) - return True - s = Scanner() - log = [] - f = [no('foo bar'),no('foo bar'),no('foo bleh')] - r = s.GetDupeGroups(f, job.Job(1,do_progress)) - self.assertEqual(0,log[0]) - self.assertEqual(100,log[-1]) - - def test_mix_file_kind(self): - s = Scanner() - s.mix_file_kind = False - f = [no('foo.1'),no('foo.2')] - r = s.GetDupeGroups(f) - self.assertEqual(0,len(r)) - - def test_word_weighting(self): - s = Scanner() - s.min_match_percentage = 75 - s.word_weighting = True - f = [no('foo bar'),no('foo bar bleh')] - r = s.GetDupeGroups(f) - self.assertEqual(1,len(r)) - g = r[0] - m = g.get_match_of(g.dupes[0]) - self.assertEqual(75,m.percentage) # 16 letters, 12 matching - - def test_similar_words(self): - s = Scanner() - s.match_similar_words = True - f = [no('The White Stripes'),no('The Whites Stripe'),no('Limp Bizkit'),no('Limp Bizkitt')] - r = s.GetDupeGroups(f) - self.assertEqual(2,len(r)) - - def test_fields(self): - s = Scanner() - s.scan_type = SCAN_TYPE_FIELDS - f = [no('The White Stripes - Little Ghost'),no('The White Stripes - Little Acorn')] - r = s.GetDupeGroups(f) - self.assertEqual(0,len(r)) - - def test_fields_no_order(self): - s = Scanner() - s.scan_type = SCAN_TYPE_FIELDS_NO_ORDER - f = [no('The White Stripes - Little Ghost'),no('Little Ghost - The White Stripes')] - r = s.GetDupeGroups(f) - self.assertEqual(1,len(r)) - - def test_tag_scan(self): - s = Scanner() - s.scan_type = SCAN_TYPE_TAG - o1 = no('foo') - o2 = no('bar') - o1.artist = 'The White Stripes' - o1.title = 'The Air Near My Fingers' - o2.artist = 'The White Stripes' - o2.title = 'The Air Near My Fingers' - r = s.GetDupeGroups([o1,o2]) - self.assertEqual(1,len(r)) - - def test_tag_with_album_scan(self): - s = Scanner() - s.scan_type = SCAN_TYPE_TAG_WITH_ALBUM - o1 = no('foo') - o2 = no('bar') - o3 = no('bleh') - o1.artist = 'The White Stripes' - o1.title = 'The Air Near My Fingers' - o1.album = 'Elephant' - o2.artist = 'The White Stripes' - o2.title = 'The Air Near My Fingers' - o2.album = 'Elephant' - o3.artist = 'The White Stripes' - o3.title = 'The Air Near My Fingers' - o3.album = 'foobar' - r = s.GetDupeGroups([o1,o2,o3]) - self.assertEqual(1,len(r)) - - def test_that_dash_in_tags_dont_create_new_fields(self): - s = Scanner() - s.scan_type = SCAN_TYPE_TAG_WITH_ALBUM - s.min_match_percentage = 50 - o1 = no('foo') - o2 = no('bar') - o1.artist = 'The White Stripes - a' - o1.title = 'The Air Near My Fingers - a' - o1.album = 'Elephant - a' - o2.artist = 'The White Stripes - b' - o2.title = 'The Air Near My Fingers - b' - o2.album = 'Elephant - b' - r = s.GetDupeGroups([o1,o2]) - self.assertEqual(1,len(r)) - - def test_tag_scan_with_different_scanned(self): - s = Scanner() - s.scan_type = SCAN_TYPE_TAG - s.scanned_tags = set(['track', 'year']) - o1 = no('foo') - o2 = no('bar') - o1.artist = 'The White Stripes' - o1.title = 'some title' - o1.track = 'foo' - o1.year = 'bar' - o2.artist = 'The White Stripes' - o2.title = 'another title' - o2.track = 'foo' - o2.year = 'bar' - r = s.GetDupeGroups([o1, o2]) - self.assertEqual(1, len(r)) - - def test_tag_scan_only_scans_existing_tags(self): - s = Scanner() - s.scan_type = SCAN_TYPE_TAG - s.scanned_tags = set(['artist', 'foo']) - o1 = no('foo') - o2 = no('bar') - o1.artist = 'The White Stripes' - o1.foo = 'foo' - o2.artist = 'The White Stripes' - o2.foo = 'bar' - r = s.GetDupeGroups([o1, o2]) - self.assertEqual(1, len(r)) # Because 'foo' is not scanned, they match - - def test_tag_scan_converts_to_str(self): - s = Scanner() - s.scan_type = SCAN_TYPE_TAG - s.scanned_tags = set(['track']) - o1 = no('foo') - o2 = no('bar') - o1.track = 42 - o2.track = 42 - try: - r = s.GetDupeGroups([o1, o2]) - except TypeError: - self.fail() - self.assertEqual(1, len(r)) - - def test_tag_scan_non_ascii(self): - s = Scanner() - s.scan_type = SCAN_TYPE_TAG - s.scanned_tags = set(['title']) - o1 = no('foo') - o2 = no('bar') - o1.title = u'foobar\u00e9' - o2.title = u'foobar\u00e9' - try: - r = s.GetDupeGroups([o1, o2]) - except UnicodeEncodeError: - self.fail() - self.assertEqual(1, len(r)) - - def test_audio_content_scan(self): - s = Scanner() - s.scan_type = SCAN_TYPE_CONTENT_AUDIO - f = [no('foo'),no('bar'),no('bleh')] - f[0].md5 = 'foo' - f[1].md5 = 'bar' - f[2].md5 = 'bleh' - f[0].md5partial = 'foo' - f[1].md5partial = 'foo' - f[2].md5partial = 'bleh' - f[0].audiosize = 1 - f[1].audiosize = 1 - f[2].audiosize = 1 - r = s.GetDupeGroups(f) - self.assertEqual(1,len(r)) - self.assertEqual(2,len(r[0])) - - def test_audio_content_scan_compare_sizes_first(self): - class MyFile(no): - def get_md5(file): - self.fail() - md5partial = property(get_md5) - - s = Scanner() - s.scan_type = SCAN_TYPE_CONTENT_AUDIO - f = [MyFile('foo'),MyFile('bar')] - f[0].audiosize = 1 - f[1].audiosize = 2 - self.assertEqual(0,len(s.GetDupeGroups(f))) - - def test_ignore_list(self): - s = Scanner() - f1 = no('foobar') - f2 = no('foobar') - f3 = no('foobar') - f1.path = Path('dir1/foobar') - f2.path = Path('dir2/foobar') - f3.path = Path('dir3/foobar') - s.ignore_list.Ignore(str(f1.path),str(f2.path)) - s.ignore_list.Ignore(str(f1.path),str(f3.path)) - r = s.GetDupeGroups([f1,f2,f3]) - self.assertEqual(1,len(r)) - g = r[0] - self.assertEqual(1,len(g.dupes)) - self.assert_(f1 not in g) - self.assert_(f2 in g) - self.assert_(f3 in g) - # Ignored matches are not counted as discarded - self.assertEqual(s.discarded_file_count, 0) - - def test_ignore_list_checks_for_unicode(self): - #scanner was calling path_str for ignore list checks. Since the Path changes, it must - #be unicode(path) - s = Scanner() - f1 = no('foobar') - f2 = no('foobar') - f3 = no('foobar') - f1.path = Path(u'foo1\u00e9') - f2.path = Path(u'foo2\u00e9') - f3.path = Path(u'foo3\u00e9') - s.ignore_list.Ignore(unicode(f1.path),unicode(f2.path)) - s.ignore_list.Ignore(unicode(f1.path),unicode(f3.path)) - r = s.GetDupeGroups([f1,f2,f3]) - self.assertEqual(1,len(r)) - g = r[0] - self.assertEqual(1,len(g.dupes)) - self.assert_(f1 not in g) - self.assert_(f2 in g) - self.assert_(f3 in g) - - def test_custom_match_factory(self): - class MatchFactory(object): - def getmatches(self,objects,j=None): - return [Match(objects[0], objects[1], 420)] - - - s = Scanner() - s.match_factory = MatchFactory() - o1,o2 = no('foo'),no('bar') - groups = s.GetDupeGroups([o1,o2]) - self.assertEqual(1,len(groups)) - g = groups[0] - self.assertEqual(2,len(g)) - g.switch_ref(o1) - m = g.get_match_of(o2) - self.assertEqual((o1,o2,420),m) - - def test_file_evaluates_to_false(self): - # A very wrong way to use any() was added at some point, causing resulting group list - # to be empty. - class FalseNamedObject(NamedObject): - def __nonzero__(self): - return False - - - s = Scanner() - f1 = FalseNamedObject('foobar') - f2 = FalseNamedObject('foobar') - r = s.GetDupeGroups([f1,f2]) - self.assertEqual(1,len(r)) - - def test_size_threshold(self): - # Only file equal or higher than the size_threshold in size are scanned - s = Scanner() - f1 = no('foo', 1) - f2 = no('foo', 2) - f3 = no('foo', 3) - s.size_threshold = 2 - groups = s.GetDupeGroups([f1,f2,f3]) - self.assertEqual(len(groups), 1) - [group] = groups - self.assertEqual(len(group), 2) - self.assertTrue(f1 not in group) - self.assertTrue(f2 in group) - self.assertTrue(f3 in group) - - def test_tie_breaker_path_deepness(self): - # If there is a tie in prioritization, path deepness is used as a tie breaker - s = Scanner() - o1, o2 = no('foo'), no('foo') - o1.path = Path('foo') - o2.path = Path('foo/bar') - [group] = s.GetDupeGroups([o1, o2]) - self.assertTrue(group.ref is o2) - - def test_tie_breaker_copy(self): - # if copy is in the words used (even if it has a deeper path), it becomes a dupe - s = Scanner() - o1, o2 = no('foo bar Copy'), no('foo bar') - o1.path = Path('deeper/path') - o2.path = Path('foo') - [group] = s.GetDupeGroups([o1, o2]) - self.assertTrue(group.ref is o2) - - def test_tie_breaker_same_name_plus_digit(self): - # if ref has the same words as dupe, but has some just one extra word which is a digit, it - # becomes a dupe - s = Scanner() - o1, o2 = no('foo bar 42'), no('foo bar') - o1.path = Path('deeper/path') - o2.path = Path('foo') - [group] = s.GetDupeGroups([o1, o2]) - self.assertTrue(group.ref is o2) - - def test_partial_group_match(self): - # Count the number od discarded matches (when a file doesn't match all other dupes of the - # group) in Scanner.discarded_file_count - s = Scanner() - o1, o2, o3 = no('a b'), no('a'), no('b') - s.min_match_percentage = 50 - [group] = s.GetDupeGroups([o1, o2, o3]) - self.assertEqual(len(group), 2) - self.assertTrue(o1 in group) - self.assertTrue(o2 in group) - self.assertTrue(o3 not in group) - self.assertEqual(s.discarded_file_count, 1) - - -class TCScannerME(TestCase): - def test_priorize(self): - # in ScannerME, bitrate goes first (right after is_ref) in priorization - s = ScannerME() - o1, o2 = no('foo'), no('foo') - o1.bitrate = 1 - o2.bitrate = 2 - [group] = s.GetDupeGroups([o1, o2]) - self.assertTrue(group.ref is o2) - - -if __name__ == "__main__": - unittest.main() \ No newline at end of file diff --git a/pe/qt/help/changelog.yaml b/pe/qt/help/changelog.yaml deleted file mode 100644 index f387c334..00000000 --- a/pe/qt/help/changelog.yaml +++ /dev/null @@ -1,174 +0,0 @@ -- date: 2009-05-27 - version: 1.7.2 - description: | - * Fixed a bug causing '.jpeg' files not to be scanned. - * Fixed a bug causing a GUI freeze at the beginning of a scan with a lot of files. - * Fixed a bug that sometimes caused a crash when an action was cancelled, and then started again. - * Improved scanning speed. -- date: 2009-05-26 - version: 1.7.1 - description: | - * Fixed a bug causing the "Match Scaled" preference to be inverted. -- date: 2009-05-20 - version: 1.7.0 - description: | - * Fixed the bug from 1.6.0 preventing PowerPC macs from running the application. - * Converted the Windows GUI to Qt, thus enabling multiprocessing and making the scanning process - faster. -- date: 2009-03-24 - description: "* **Improved** scanning speed, mainly on OS X where all cores of the\ - \ CPU are now used.\r\n* **Fixed** an occasional crash caused by permission issues.\r\ - \n* **Fixed** a bug where the \"X discarded\" notice would show a too large number\ - \ of discarded duplicates." - version: 1.6.0 -- date: 2008-09-10 - description: "
    \n\t\t\t\t\t\t
  • Added a notice in the status bar when\ - \ matches were discarded during the scan.
  • \n\t\t\t\t\t\t
  • Improved\ - \ duplicate prioritization (smartly chooses which file you will keep).
  • \n\t\ - \t\t\t\t\t
  • Improved scan progress feedback.
  • \n\t\t\t\t\t\t
  • Improved\ - \ responsiveness of the user interface for certain actions.
  • \n\t\t \ - \
" - version: 1.5.0 -- date: 2008-07-28 - description: "
    \n\t\t\t\t\t\t
  • Improved iPhoto compatibility on Mac\ - \ OS X.
  • \n\t\t\t\t\t\t
  • Improved the speed of results loading and\ - \ saving.
  • \n\t\t\t\t\t\t
  • Fixed a crash sometimes occurring during\ - \ duplicate deletion.
  • \n\t\t
" - version: 1.4.2 -- date: 2008-04-12 - description: "
    \n\t\t\t\t\t\t
  • Improved iPhoto Library loading feedback\ - \ on Mac OS X.
  • \n\t\t\t\t\t\t
  • Fixed the directory selection dialog.\ - \ Bundles can be selected again on Mac OS X.
  • \n\t\t\t\t\t\t
  • Fixed\ - \ \"Clear Ignore List\" crash in Windows.
  • \n\t\t
" - version: 1.4.1 -- date: 2008-02-20 - description: "
    \n\t\t\t\t\t\t
  • Added iPhoto Library support on Mac OS\ - \ X.
  • \n\t\t\t\t\t\t
  • Fixed occasional crashes when scanning corrupted\ - \ pictures.
  • \n\t\t
" - version: 1.4.0 -- date: 2008-02-20 - description: "
    \n\t\t\t\t\t\t
  • Added iPhoto Library support on Mac OS\ - \ X.
  • \n\t\t\t\t\t\t
  • Fixed occasional crashes when scanning corrupted\ - \ pictures.
  • \n\t\t
" - version: 1.4.0 -- date: 2008-01-12 - description: "
    \n\t\t\t\t\t\t
  • Improved scan, delete and move speed\ - \ in situations where there were a lot of duplicates.
  • \n\t\t\t\t\t\t
  • Fixed\ - \ occasional crashes when moving a lot of files at once.
  • \n\t\t\t\t\t\t
  • Fixed\ - \ an issue sometimes preventing the application from starting at all.
  • \n\t\ - \t
" - version: 1.3.4 -- date: 2007-12-03 - description: "
    \n\t\t\t\t\t\t
  • Improved the handling of low memory situations.
  • \n\ - \t\t\t\t\t\t
  • Improved the directory panel. The \"Remove\" button changes\ - \ to \"Put Back\" when an excluded directory is selected.
  • \n\t\t\t\t\t\t
  • Fixed\ - \ the directory selection dialog. iPhoto '08 library files can now be selected.
  • \n\ - \t\t
" - version: 1.3.3 -- date: 2007-11-24 - description: "
    \n\t\t\t\t\t\t
  • Added the \"Remove empty folders\" option.
  • \n\ - \t\t\t\t\t\t
  • Fixed results load/save issues.
  • \n\t\t\t\t\t\t
  • Fixed\ - \ occasional status bar inaccuracies when the results are filtered.
  • \n\t\t\ - \
" - version: 1.3.2 -- date: 2007-10-21 - description: "
    \n\t\t\t\t\t\t
  • Improved results loading speed.
  • \n\ - \t\t\t\t\t\t
  • Improved details panel's picture loading (made it asynchronous).
  • \n\ - \t\t\t\t\t\t
  • Fixed a bug where the stats line at the bottom would sometimes\ - \ go confused while having a filter active.
  • \n\t\t\t\t\t\t
  • Fixed\ - \ a bug under Windows where some duplicate markings would be lost.
  • \n\t\t\ - \
" - version: 1.3.1 -- date: 2007-09-22 - description: "
    \n\t\t\t\t\t\t
  • Added post scan filtering.
  • \n\t\t\ - \t\t\t\t
  • Fixed issues with the rename feature under Windows
  • \n\t\ - \t\t\t\t\t
  • Fixed some user interface annoyances under Windows
  • \n\ - \t\t
" - version: 1.3.0 -- date: 2007-05-19 - description: "
    \n\t\t\t\t\t\t
  • Improved UI responsiveness (using threads)\ - \ under Mac OS X.
  • \n\t\t\t\t\t\t
  • Improved result load/save speed\ - \ and memory usage.
  • \n\t\t
" - version: 1.2.1 -- date: 2007-03-17 - description: "
    \n\t\t\t\t\t\t
  • Changed the picture decoding libraries\ - \ for both Mac OS X and Windows. The Mac OS X version uses the Core Graphics library\ - \ and the Windows version uses the .NET framework imaging capabilities. This results\ - \ in much faster scans. As a bonus, the Mac OS X version of dupeGuru PE now supports\ - \ RAW images.
  • \n\t\t
" - version: 1.2.0 -- date: 2007-02-11 - description: "
    \n\t\t\t\t\t\t
  • Added Re-orderable columns. In fact,\ - \ I re-added the feature which was lost in the C# conversion in 2.4.0 (Windows).
  • \n\ - \t\t\t\t\t\t
  • Fixed a bug with all the Delete/Move/Copy actions with\ - \ certain kinds of files.
  • \n\t\t
" - version: 1.1.6 -- date: 2007-01-11 - description: "
    \n\t\t\t\t\t\t
  • Fixed a bug with the Move action.
  • \n\ - \t\t
" - version: 1.1.5 -- date: 2007-01-09 - description: "
    \n\t\t\t\t\t\t
  • Fixed a \"ghosting\" bug. Dupes deleted\ - \ by dupeGuru would sometimes come back in subsequent scans (Windows).
  • \n\t\ - \t\t\t\t\t
  • Fixed bugs sometimes making dupeGuru crash when marking a\ - \ dupe (Windows).
  • \n\t\t\t\t\t\t
  • Fixed some minor visual glitches\ - \ (Windows).
  • \n\t\t
" - version: 1.1.4 -- date: 2006-12-23 - description: "
    \n\t\t\t\t\t\t
  • Improved the caching system. This makes\ - \ duplicate scans significantly faster.
  • \n\t\t\t\t\t\t
  • Improved\ - \ the rename file dialog to exclude the extension from the original selection\ - \ (so when you start typing your new filename, it doesn't overwrite it) (Windows).
  • \n\ - \t\t\t\t\t\t
  • Changed some menu key shortcuts that created conflicts\ - \ (Windows).
  • \n\t\t\t\t\t\t
  • Fixed a bug preventing files from \"\ - reference\" directories to be displayed in blue in the results (Windows).
  • \n\ - \t\t\t\t\t\t
  • Fixed a bug preventing some files to be sent to the recycle\ - \ bin (Windows).
  • \n\t\t\t\t\t\t
  • Fixed a bug with the \"Remove\"\ - \ button of the directories panel (Windows).
  • \n\t\t\t\t\t\t
  • Fixed\ - \ a bug in the packaging preventing certain Windows configurations to start dupeGuru\ - \ at all.
  • \n\t\t
" - version: 1.1.3 -- date: 2006-11-18 - description: "
    \n\t\t\t\t\t\t
  • Fixed a bug with directory states.
  • \n\ - \t\t
" - version: 1.1.2 -- date: 2006-11-17 - description: "
    \n\t\t\t\t\t\t
  • Fixed a bug causing the ignore list not\ - \ to be saved.
  • \n\t\t\t\t\t\t
  • Fixed a bug with selection under Power\ - \ Marker mode.
  • \n\t\t
" - version: 1.1.1 -- date: 2006-11-15 - description: "
    \n\t\t\t\t\t\t
  • Changed the Windows interface. It is\ - \ now .NET based.
  • \n\t\t\t\t\t\t
  • Added an auto-update feature to\ - \ the windows version.
  • \n\t\t\t\t\t\t
  • Changed the way power marking\ - \ works. It is now a mode instead of a separate window.
  • \n\t\t\t\t\t\t
  • Changed\ - \ the \"Size (MB)\" column for a \"Size (KB)\" column. The values are now \"ceiled\"\ - \ instead of rounded. Therefore, a size \"0\" is now really 0 bytes, not just\ - \ a value too small to be rounded up. It is also the case for delta values.
  • \n\ - \t\t\t\t\t\t
  • Fixed a bug sometimes making delete and move operations\ - \ stall.
  • \n\t\t
" - version: 1.1.0 -- date: 2006-10-12 - description: "
    \n\t\t\t\t\t\t
  • Added an auto-update feature in the Mac\ - \ OS X version (with Sparkle).
  • \n\t\t \t
  • Fixed a bug\ - \ sometimes causing inaccuracies of the Match %.
  • \n\t\t
" - version: 1.0.5 -- date: 2006-09-21 - description: "
    \n\t\t \t
  • Fixed a bug with the cache system.
  • \n\ - \t\t
" - version: 1.0.4 -- date: 2006-09-15 - description: "
    \n\t\t\t\t\t\t
  • Added the ability to search for scaled\ - \ duplicates.
  • \n\t\t\t\t\t\t
  • Added a cache system for faster scans.
  • \n\ - \t\t \t
  • Improved speed of the scanning engine.
  • \n\t\t\ - \
" - version: 1.0.3 -- date: 2006-09-11 - description: "
    \n\t\t \t
  • Improved speed of the scanning\ - \ engine.
  • \n\t\t\t\t\t\t
  • Improved the display of pictures in the\ - \ details panel (Windows).
  • \n\t\t
" - version: 1.0.2 -- date: 2006-09-08 - description: "
    \n\t\t \t
  • Initial release.
  • \n\t\t \ - \
" - version: 1.0.0 diff --git a/pe/qt/help/gen.py b/pe/qt/help/gen.py deleted file mode 100644 index 8ed33e4e..00000000 --- a/pe/qt/help/gen.py +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env python -# Unit Name: -# Created By: Virgil Dupras -# Created On: 2009-05-24 -# $Id$ -# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) - -import os - -from web import generate_help - -generate_help.main('.', 'dupeguru_pe_help', force_render=True) diff --git a/pe/qt/help/skeleton/hardcoded.css b/pe/qt/help/skeleton/hardcoded.css deleted file mode 100644 index a3b17c5b..00000000 --- a/pe/qt/help/skeleton/hardcoded.css +++ /dev/null @@ -1,409 +0,0 @@ -/***************************************************** - General settings -*****************************************************/ - -BODY -{ - background-color:white; -} - -BODY,A,P,UL,TABLE,TR,TD -{ - font-family:Tahoma,Arial,sans-serif; - font-size:10pt; - color: #4477AA;/*darker than 5588bb for the sake of the eyes*/ -} - -/***************************************************** - "A" settings -*****************************************************/ - -A -{ - color: #ae322b; - text-decoration:underline; - font-weight:bold; -} - -A.glossaryword {color:#A0A0A0;} - -A.noline -{ - text-decoration: none; -} - - -/***************************************************** - Menu and mainframe settings -*****************************************************/ - -.maincontainer -{ - display:block; - margin-left:7%; - margin-right:7%; - padding-left:5px; - padding-right:0px; - border-color:#CCCCCC; - border-style:solid; - border-width:2px; - border-right-width:0px; - border-bottom-width:0px; - border-top-color:#ae322b; - vertical-align:top; -} - -TD.menuframe -{ - width:30%; -} - -.menu -{ - margin:4px 4px 4px 4px; - margin-top: 16pt; - border-color:gray; - border-width:1px; - border-style:dotted; - padding-top:10pt; - padding-bottom:10pt; - padding-right:6pt; -} - -.submenu -{ - list-style-type: none; - margin-left:26pt; - margin-top:0pt; - margin-bottom:0pt; - padding-left:0pt; -} - -A.menuitem,A.menuitem_selected -{ - font-size:14pt; - font-family:Tahoma,Arial,sans-serif; - font-weight:normal; - padding-left:10pt; - color:#5588bb; - margin-right:2pt; - margin-left:4pt; - text-decoration:none; -} - -A.menuitem_selected -{ - font-weight:bold; -} - -A.submenuitem -{ - font-family:Tahoma,Arial,sans-serif; - font-weight:normal; - color:#5588bb; - text-decoration:none; -} - -.titleline -{ - border-width:3px; - border-style:solid; - border-left-width:0px; - border-right-width:0px; - border-top-width:0px; - border-color:#CCCCCC; - margin-left:28pt; - margin-right:2pt; - line-height:1px; - padding-top:0px; - margin-top:0px; - display:block; -} - -.titledescrip -{ - text-align:left; - display:block; - margin-left:26pt; - color:#ae322b; -} - -.mainlogo -{ - display:block; - margin-left:8%; - margin-top:4pt; - margin-bottom:4pt; -} - -/***************************************************** - IMG settings -*****************************************************/ - -IMG -{ - border-style:none; -} - -IMG.smallbutton -{ - margin-right: 20px; - float:none; -} - -IMG.floating -{ - float:left; - margin-right: 4pt; - margin-bottom: 4pt; -} - -IMG.lefticon -{ - vertical-align: middle; - padding-right: 2pt; -} - -IMG.righticon -{ - vertical-align: middle; - padding-left: 2pt; -} - -/***************************************************** - TABLE settings -*****************************************************/ - -TABLE -{ - border-style:none; -} - -TABLE.box -{ - width: 90%; - margin-left:5%; -} - -TABLE.centered -{ - margin-left: auto; - margin-right: auto; -} - -TABLE.hardcoded -{ - background-color: #225588; - margin-left: auto; - margin-right: auto; - width: 90%; -} - -TR { background-color: transparent; } - -TABLE.hardcoded TR { background-color: white } - -TABLE.hardcoded TR.header -{ - font-weight: bold; - color: black; - background-color: #C8D6E5; -} - -TABLE.hardcoded TR.header TD {color:black;} - -TABLE.hardcoded TD { padding-left: 2pt; } - -TD.minimelem { - padding-right:0px; - padding-left:0px; - text-align:center; -} - -TD.rightelem -{ - text-align:right; - /*padding-left:0pt;*/ - padding-right: 2pt; - width: 17%; -} - -/***************************************************** - P settings -*****************************************************/ - -p,.sub{text-align:justify;} -.centered{text-align:center;} -.sub -{ - padding-left: 16pt; - padding-right:16pt; -} - -.Note, .ContactInfo -{ - border-color: #ae322b; - border-width: 1pt; - border-style: dashed; - text-align:justify; - padding: 2pt 2pt 2pt 2pt; - margin-bottom:4pt; - margin-top:8pt; - list-style-position:inside; -} - -.ContactInfo -{ - width:60%; - margin-left:5%; -} - -.NewsItem -{ - border-color:#ae322b; - border-style: solid; - border-right:none; - border-top:none; - border-left:none; - border-bottom-width:1px; - text-align:justify; - padding-left:4pt; - padding-right:4pt; - padding-bottom:8pt; -} - -/***************************************************** - Lists settings -*****************************************************/ -UL.plain -{ - list-style-type: none; - padding-left:0px; - margin-left:0px; -} - -LI.plain -{ - list-style-type: none; -} - -LI.section -{ - padding-top: 6pt; -} - -UL.longtext LI -{ - border-color: #ae322b; - border-width:0px; - border-top-width:1px; - border-style:solid; - margin-top:12px; -} - -/* - with UL.longtext LI, there can be anything between - the UL and the LI, and it will still make the - lontext thing, I must break it with this hack -*/ -UL.longtext UL LI -{ - border-style:none; - margin-top:2px; -} - - -/***************************************************** - Titles settings -*****************************************************/ - -H1,H2,H3 -{ - font-family:"Courier New",monospace; - color:#5588bb; -} - -H1 -{ - font-size:18pt; - color: #ae322b; - border-color: #70A0CF; - border-width: 1pt; - border-style: solid; - margin-top: 16pt; - margin-left: 5%; - margin-right: 5%; - padding-top: 2pt; - padding-bottom:2pt; - text-align: center; -} - -H2 -{ - border-color: #ae322b; - border-bottom-width: 2px; - border-top-width: 0pt; - border-left-width: 2px; - border-right-width: 0pt; - border-bottom-color: #cccccc; - border-style: solid; - margin-top: 16pt; - margin-left: 0pt; - margin-right: 0pt; - padding-bottom:3pt; - padding-left:5pt; - text-align: left; - font-size:16pt; -} - -H3 -{ - display:block; - color:#ae322b; - border-color: #70A0CF; - border-bottom-width: 2px; - border-top-width: 0pt; - border-left-width: 0pt; - border-right-width: 0pt; - border-style: dashed; - margin-top: 12pt; - margin-left: 0pt; - margin-bottom: 4pt; - width:auto; - padding-bottom:3pt; - padding-right:2pt; - padding-left:2pt; - text-align: left; - font-weight:bold; -} - - -/***************************************************** - Misc. classes -*****************************************************/ -.longtext:first-letter {font-size: 150%} - -.price, .loweredprice, .specialprice {font-weight:bold;} - -.loweredprice {text-decoration:line-through} - -.specialprice {color:red} - -form -{ - margin:0px; -} - -.program_summary -{ - float:right; - margin: 32pt; - margin-top:0pt; - margin-bottom:0pt; -} - -.screenshot -{ - float:left; - margin: 8pt; -} \ No newline at end of file diff --git a/pe/qt/help/skeleton/images/hs_title.png b/pe/qt/help/skeleton/images/hs_title.png deleted file mode 100644 index 07bd89c69dd50a3967b46a441741f274b8854de8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1817 zcmWkt2~Y|q^t%aD27PTfWQbSo7y0Xrdp{OF7oMwX4{+byFr3&y7B zlLpel1d+*^<>$!E@c2A9sOuxRq}jS+G}rcy>y2h&rngp=tT$NjxX)#-@ zR;$fwvyo;C3xs)cj4wl35`-y%c{0>!qGG~a8Nvb)0K&L3RHVdNQtA}p%Q4Dz$v^^f z%s`q=W-AZEN|l62NeGp=(QGM^zcEYpnJq53Zg_psX$pDf}jksh9!YZ6*y0d6t`*06htZIC4pHW%9Epf zh*AWzsT7ofh)_XrrKsL$;mT2tj52IVPjY1#SB?oFlp&##5=^Q9qlF{I7FJ9G2rF*a z2s?-pFR&PpFjtBRA?yV{E9}sUAgt)Ih9}1>x(SvT zsqP+UieWa0F{DUM&ub2ZbHs4dvq=VsEE&xV>e|H!OM+4TQTc&M3CEU=q(F(^fAYG# zOS_;aD|@ud29hH~gpd|cpbwr+aO8+kiBoq^eznVg<_$ zP1%d1$UV?R~TV5+i6w>mO|X i&$iNccmR(ItgDH2&`ti^@h8+@ro}}kMz!wGy!C(b`7gZy diff --git a/pe/qt/help/templates/base_dg.mako b/pe/qt/help/templates/base_dg.mako deleted file mode 100644 index 7767c49f..00000000 --- a/pe/qt/help/templates/base_dg.mako +++ /dev/null @@ -1,14 +0,0 @@ -<%inherit file="/base_help.mako"/> -${next.body()} - -<%def name="menu()"><% -self.menuitem('intro.htm', 'Introduction', 'Introduction to dupeGuru') -self.menuitem('quick_start.htm', 'Quick Start', 'Quickly get into the action') -self.menuitem('directories.htm', 'Directories', 'Managing dupeGuru directories') -self.menuitem('preferences.htm', 'Preferences', 'Setting dupeGuru preferences') -self.menuitem('results.htm', 'Results', 'Time to delete these duplicates!') -self.menuitem('power_marker.htm', 'Power Marker', 'Take control of your duplicates') -self.menuitem('faq.htm', 'F.A.Q.', 'Frequently Asked Questions') -self.menuitem('versions.htm', 'Version History', 'Changes dupeGuru went through') -self.menuitem('credits.htm', 'Credits', 'People who contributed to dupeGuru') -%> \ No newline at end of file diff --git a/pe/qt/help/templates/credits.mako b/pe/qt/help/templates/credits.mako deleted file mode 100644 index 9de91bd2..00000000 --- a/pe/qt/help/templates/credits.mako +++ /dev/null @@ -1,25 +0,0 @@ -## -*- coding: utf-8 -*- -<%! - title = 'Credits' - selected_menu_item = 'Credits' -%> -<%inherit file="/base_dg.mako"/> -Below is the list of people who contributed, directly or indirectly to dupeGuru. - -${self.credit('Virgil Dupras', 'Developer', "That's me, Hardcoded Software founder", 'www.hardcoded.net', 'hsoft@hardcoded.net')} - -${self.credit(u'Jérôme Cantin', u'Icon designer', u"Icons in dupeGuru are from him")} - -${self.credit('Python', 'Programming language', "The bestest of the bests", 'www.python.org')} - -${self.credit('PyObjC', 'Python-to-Cocoa bridge', "Used for the Mac OS X version", 'pyobjc.sourceforge.net')} - -${self.credit('PyQt', 'Python-to-Qt bridge', "Used for the Windows version", 'www.riverbankcomputing.co.uk')} - -${self.credit('Qt', 'GUI Toolkit', "Used for the Windows version", 'www.qtsoftware.com')} - -${self.credit('Sparkle', 'Auto-update library', "Used for the Mac OS X version", 'andymatuschak.org/pages/sparkle')} - -${self.credit('Python Imaging Library', 'Picture analyzer', "Used for the Windows version", 'www.pythonware.com/products/pil/')} - -${self.credit('You', 'dupeGuru user', "What would I do without you?")} diff --git a/pe/qt/help/templates/directories.mako b/pe/qt/help/templates/directories.mako deleted file mode 100644 index e75b47bd..00000000 --- a/pe/qt/help/templates/directories.mako +++ /dev/null @@ -1,24 +0,0 @@ -<%! - title = 'Directories' - selected_menu_item = 'Directories' -%> -<%inherit file="/base_dg.mako"/> - -There is a panel in dupeGuru called **Directories**. You can open it by clicking on the **Directories** button. This directory contains the list of the directories that will be scanned when you click on **Start Scanning**. - -This panel is quite straightforward to use. If you want to add a directory, click on **Add**. If you added directories before, a popup menu with a list of recent directories you added will pop. You can click on one of them to add it directly to your list. If you click on the first item of the popup menu, **Add New Directory...**, you will be prompted for a directory to add. If you never added a directory, no menu will pop and you will directly be prompted for a new directory to add. - -To remove a directory, select the directory to remove and click on **Remove**. If a subdirectory is selected when you click remove, the selected directory will be set to **excluded** state (see below) instead of being removed. - -Directory states ------ - -Every directory can be in one of these 3 states: - -* **Normal:** Duplicates found in these directories can be deleted. -* **Reference:** Duplicates found in this directory **cannot** be deleted. Files in reference directories will be in a blue color in the results. -* **Excluded:** Files in this directory will not be included in the scan. - -The default state of a directory is, of course, **Normal**. You can use **Reference** state for a directory if you want to be sure that you won't delete any file from it. - -When you set the state of a directory, all subdirectories of this directory automatically inherit this state unless you explicitly set a subdirectory's state. diff --git a/pe/qt/help/templates/faq.mako b/pe/qt/help/templates/faq.mako deleted file mode 100644 index 1c4e998f..00000000 --- a/pe/qt/help/templates/faq.mako +++ /dev/null @@ -1,64 +0,0 @@ -<%! - title = 'dupeGuru F.A.Q.' - selected_menu_item = 'F.A.Q.' -%> -<%inherit file="/base_dg.mako"/> - -<%text filter="md"> -### What is dupeGuru PE? - -dupeGuru Picture Edition (PE for short) is a tool to find duplicate pictures on your computer. Not only can it find exact matches, but it can also find duplicates among pictures of different kind (PNG, JPG, GIF etc..) and quality. - -### What makes it better than other duplicate scanners? - -The scanning engine is extremely flexible. You can tweak it to really get the kind of results you want. You can read more about dupeGuru tweaking option at the [Preferences page](preferences.htm). - -### How safe is it to use dupeGuru PE? - -Very safe. dupeGuru has been designed to make sure you don't delete files you didn't mean to delete. First, there is the reference directory system that lets you define directories where you absolutely **don't** want dupeGuru to let you delete files there, and then there is the group reference system that makes sure that you will **always** keep at least one member of the duplicate group. - -### What are the demo limitations of dupeGuru PE? - -In demo mode, you can only perform actions (delete/copy/move) on 10 duplicates per session. - -### The mark box of a file I want to delete is disabled. What must I do? - -You cannot mark the reference (The first file) of a duplicate group. However, what you can do is to promote a duplicate file to reference. Thus, if a file you want to mark is reference, select a duplicate file in the group that you want to promote to reference, and click on **Actions-->Make Selected Reference**. If the reference file is from a reference directory (filename written in blue letters), you cannot remove it from the reference position. - -### I have a directory from which I really don't want to delete files. - -If you want to be sure that dupeGuru will never delete file from a particular directory, just open the **Directories panel**, select that directory, and set its state to **Reference**. - -### What is this '(X discarded)' notice in the status bar? - -In some cases, some matches are not included in the final results for security reasons. Let me use an example. We have 3 file: A, B and C. We scan them using a low filter hardness. The scanner determines that A matches with B, A matches with C, but B does **not** match with C. Here, dupeGuru has kind of a problem. It cannot create a duplicate group with A, B and C in it because not all files in the group would match together. It could create 2 groups: one A-B group and then one A-C group, but it will not, for security reasons. Lets think about it: If B doesn't match with C, it probably means that either B, C or both are not actually duplicates. If there would be 2 groups (A-B and A-C), you would end up delete both B and C. And if one of them is not a duplicate, that is really not what you want to do, right? So what dupeGuru does in a case like this is to discard the A-C match (and adds a notice in the status bar). Thus, if you delete B and re-run a scan, you will have a A-C match in your next results. - -### I want to mark all files from a specific directory. What can I do? - -Enable the [Power Marker](power_marker.htm) mode and click on the Directory column to sort your duplicates by Directory. It will then be easy for you to select all duplicates from the same directory, and then press Space to mark all selected duplicates. - -### I want to remove all files that are more than 300 KB away from their reference file. What can I do? - -* Enable the [Power Marker](power_marker.htm) mode. -* Enable the **Delta Values** mode. -* Click on the "Size" column to sort the results by size. -* Select all duplicates below -300. -* Click on **Remove Selected from Results**. -* Select all duplicates over 300. -* Click on **Remove Selected from Results**. - -### I want to make my latest modified files reference files. What can I do? - -* Enable the [Power Marker](power_marker.htm) mode. -* Enable the **Delta Values** mode. -* Click on the "Modification" column to sort the results by modification date. -* Click on the "Modification" column again to reverse the sort order (see Power Marker page to know why). -* Select all duplicates over 0. -* Click on **Make Selected Reference**. - -### I want to mark all duplicates containing the word "copy". How do I do that? - -* **Windows**: Click on **Actions --> Apply Filter**, then type "copy", then click OK. -* **Mac OS X**: Type "copy" in the "Filter" field in the toolbar. -* Click on **Mark --> Mark All**. - \ No newline at end of file diff --git a/pe/qt/help/templates/intro.mako b/pe/qt/help/templates/intro.mako deleted file mode 100644 index 51d058c9..00000000 --- a/pe/qt/help/templates/intro.mako +++ /dev/null @@ -1,13 +0,0 @@ -<%! - title = 'Introduction to dupeGuru PE' - selected_menu_item = 'introduction' -%> -<%inherit file="/base_dg.mako"/> - -dupeGuru Picture Edition (PE for short) is a tool to find duplicate pictures on your computer. Not only can it find exact matches, but it can also find duplicates among pictures of different kind (PNG, JPG, GIF etc..) and quality. - -Although dupeGuru can easily be used without documentation, reading this file will help you to master it. If you are looking for guidance for your first duplicate scan, you can take a look at the [Quick Start](quick_start.htm) section. - -It is a good idea to keep dupeGuru PE updated. You can download the latest version on the [dupeGuru PE homepage](http://www.hardcoded.net/dupeguru_pe/). - -<%def name="meta()"> diff --git a/pe/qt/help/templates/power_marker.mako b/pe/qt/help/templates/power_marker.mako deleted file mode 100644 index 26078f3d..00000000 --- a/pe/qt/help/templates/power_marker.mako +++ /dev/null @@ -1,33 +0,0 @@ -<%! - title = 'Power Marker' - selected_menu_item = 'Power Marker' -%> -<%inherit file="/base_dg.mako"/> - -You will probably not use the Power Marker feature very often, but if you get into a situation where you need it, you will be pretty happy that this feature exists. - -What is it? ------ - -When the Power Marker mode is enabled, the duplicates are shown without their respective reference file. You can select, mark and sort this list, just like in normal mode. - -So, what is it for? ------ - -The dupeGuru results, when in normal mode, are sorted according to duplicate groups' **reference file**. This means that if you want, for example, to mark all duplicates with the "exe" extension, you cannot just sort the results by "Kind" to have all exe duplicates together because a group can be composed of more than one kind of files. That is where Power Marker comes into play. To mark all your "exe" duplicates, you just have to: - -* Enable the Power marker mode. -* Add the "Kind" column with the "Columns" menu. -* Click on that "Kind" column to sort the list by kind. -* Locate the first duplicate with a "exe" kind. -* Select it. -* Scroll down the list to locate the last duplicate with a "exe" kind. -* Hold Shift and click on it. -* Press Space to mark all selected duplicates. - -Power Marker and delta values ------ - -The Power Marker unveil its true power when you use it with the **Delta Values** switch turned on. When you turn it on, relative values will be displayed instead of absolute ones. So if, for example, you want to remove from your results all duplicates that are more than 300 KB away from their reference, you could sort the Power Marker by Size, select all duplicates under -300 in the Size column, delete them, and then do the same for duplicates over 300 at the bottom of the list. - -You could also use it to change the reference priority of your duplicate list. When you make a fresh scan, if there are no reference directories, the reference file of every group is the biggest file. If you want to change that, for example, to the latest modification time, you can sort the Power Marker by modification time in **descending** order, select all duplicates with a modification time delta value higher than 0 and click on **Make Selected Reference**. The reason why you must make the sort order descending is because if 2 files among the same duplicate group are selected when you click on **Make Selected Reference**, only the first of the list will be made reference, the other will be ignored. And since you want the last modified file to be reference, having the sort order descending assures you that the first item of the list will be the last modified. diff --git a/pe/qt/help/templates/preferences.mako b/pe/qt/help/templates/preferences.mako deleted file mode 100644 index 0ef6a2ba..00000000 --- a/pe/qt/help/templates/preferences.mako +++ /dev/null @@ -1,23 +0,0 @@ -<%! - title = 'Preferences' - selected_menu_item = 'Preferences' -%> -<%inherit file="/base_dg.mako"/> - -**Filter Hardness:** The higher is this setting, the "harder" is the filter (In other words, the less results you get). Most pictures of the same quality match at 100% even if the format is different (PNG and JPG for example.). However, if you want to make a PNG match with a lower quality JPG, you will have to set the filer hardness to lower than 100. The default, 95, is a sweet spot. - -**Match scaled pictures together:** If you check this box, pictures of different dimensions will be allowed in the same duplicate group. - -**Can mix file kind:** If you check this box, duplicate groups are allowed to have files with different extensions. If you don't check it, well, they aren't! - -**Use regular expressions when filtering:** If you check this box, the filtering feature will treat your filter query as a **regular expression**. Explaining them is beyond the scope of this document. A good place to start learning it is . - -**Remove empty folders after delete or move:** When this option is enabled, folders are deleted after a file is deleted or moved and the folder is empty. - -**Copy and Move:** Determines how the Copy and Move operations (in the Action menu) will behave. - -* **Right in destination:** All files will be sent directly in the selected destination, without trying to recreate the source path at all. -* **Recreate relative path:** The source file's path will be re-created in the destination directory up to the root selection in the Directories panel. For example, if you added "/Users/foobar/Picture" to your Directories panel and you move "/Users/foobar/Picture/2006/06/photo.jpg" to the destination "/Users/foobar/MyDestination", the final destination for the file will be "/Users/foobar/MyDestination/2006/06" ("/Users/foobar/Picture" has been trimmed from source's path in the final destination.). -* **Recreate absolute path:** The source file's path will be re-created in the destination directory in it's entirety. For example, if you move "/Users/foobar/Picture/2006/06/photo.jpg" to the destination "/Users/foobar/MyDestination", the final destination for the file will be "/Users/foobar/MyDestination/Users/foobar/Picture/2006/06". - -In all cases, dupeGuru PE nicely handles naming conflicts by prepending a number to the destination filename if the filename already exists in the destination. diff --git a/pe/qt/help/templates/quick_start.mako b/pe/qt/help/templates/quick_start.mako deleted file mode 100644 index dde33c65..00000000 --- a/pe/qt/help/templates/quick_start.mako +++ /dev/null @@ -1,18 +0,0 @@ -<%! - title = 'Quick Start' - selected_menu_item = 'Quick Start' -%> -<%inherit file="/base_dg.mako"/> - -To get you quickly started with dupeGuru, let's just make a standard scan using default preferences. - -* Click on **Directories**. -* Click on **Add**. -* Choose a directory you want to scan for duplicates. -* Click on **Start Scanning**. -* Wait until the scan process is over. -* Look at every duplicate (The files that are indented) and verify that it is indeed a duplicate to the group's reference (The file above the duplicate that is not indented and have a disabled mark box). -* If a file is a false duplicate, select it and click on **Actions-->Remove Selected from Results**. -* Once you are sure that there is no false duplicate in your results, click on **Edit-->Mark All**, and then **Actions-->Send Marked to Recycle bin**. - -That is only a basic scan. There are a lot of tweaking you can do to get different results and several methods of examining and modifying your results. To know about them, just read the rest of this help file. diff --git a/pe/qt/help/templates/results.mako b/pe/qt/help/templates/results.mako deleted file mode 100644 index 53aa176f..00000000 --- a/pe/qt/help/templates/results.mako +++ /dev/null @@ -1,73 +0,0 @@ -<%! - title = 'Results' - selected_menu_item = 'Results' -%> -<%inherit file="/base_dg.mako"/> - -When dupeGuru is finished scanning for duplicates, it will show its results in the form of duplicate group list. - -About duplicate groups ------ - -A duplicate group is a group of files that all match together. Every group has a **reference file** and one or more **duplicate files**. The reference file is the first file of the group. Its mark box is disabled. Below it, and indented, are the duplicate files. - -You can mark duplicate files, but you can never mark the reference file of a group. This is a security measure to prevent dupeGuru from deleting not only duplicate files, but their reference. You sure don't want that, do you? - -What determines which files are reference and which files are duplicates is first their directory state. A files from a reference directory will always be reference in a duplicate group. If all files are from a normal directory, the size determine which file will be the reference of a duplicate group. dupeGuru assumes that you always want to keep the biggest file, so the biggest files will take the reference position. - -You can change the reference file of a group manually. To do so, select the duplicate file you want to promote to reference, and click on **Actions-->Make Selected Reference**. - -Reviewing results ------ - -Although you can just click on **Edit-->Mark All** and then **Actions-->Send Marked to Recycle bin** to quickly delete all duplicate files in your results, it is always recommended to review all duplicates before deleting them. - -To help you reviewing the results, you can bring up the **Details panel**. This panel shows all the details of the currently selected file as well as its reference's details. This is very handy to quickly determine if a duplicate really is a duplicate. You can also double-click on a file to open it with its associated application. - -If you have more false duplicates than true duplicates (If your filter hardness is very low), the best way to proceed would be to review duplicates, mark true duplicates and then click on **Actions-->Send Marked to Recycle bin**. If you have more true duplicates than false duplicates, you can instead mark all files that are false duplicates, and use **Actions-->Remove Marked from Results**. - -Marking and Selecting ------ - -A **marked** duplicate is a duplicate with the little box next to it having a check-mark. A **selected** duplicate is a duplicate being highlighted. The multiple selection actions can be performed in dupeGuru in the standard way (Shift/Command/Control click). You can toggle all selected duplicates' mark state by pressing **space**. - -Delta Values ------ - -If you turn this switch on, some columns will display the value relative to the duplicate's reference instead of the absolute values. These delta values will also be displayed in a different color so you can spot them easily. For example, if a duplicate is 1.2 MB and its reference is 1.4 MB, the Size column will display -0.2 MB. This option is a killer feature when combined with the [Power Marker](power_marker.htm). - -Filtering ------ - -dupeGuru supports post-scan filtering. With it, you can narrow down your results so you can perform actions on a subset of it. For example, you could easily mark all duplicates with their filename containing "copy" from your results using the filter. - -**Windows:** To use the filtering feature, click on Actions --> Apply Filter, write down the filter you want to apply and click OK. To go back to unfiltered results, click on Actions --> Cancel Filter. - -**Mac OS X:** To use the filtering feature, type your filter in the "Filter" search field in the toolbar. To go back to unfiltered result, blank out the field, or click on the "X". - -In simple mode (the default mode), whatever you type as the filter is the string used to perform the actual filtering, with the exception of one wildcard: **\***. Thus, if you type "[*]" as your filter, it will match anything with [] brackets in it, whatever is in between those brackets. - -For more advanced filtering, you can turn "Use regular expressions when filtering" on. The filtering feature will then use **regular expressions**. A regular expression is a language for matching text. Explaining them is beyond the scope of this document. A good place to start learning it is . - -Matches are case insensitive in both simple and regexp mode. - -For the filter to match, your regular expression don't have to match the whole filename, it just have to contain a string matching the expression. - -You might notice that not all duplicates in the filtered results will match your filter. That is because as soon as one single duplicate in a group matches the filter, the whole group stays in the results so you can have a better view of the duplicate's context. However, non-matching duplicates are in "reference mode". Therefore, you can perform actions like Mark All and be sure to only mark filtered duplicates. - -Action Menu ------ - -* **Start Duplicate Scan:** Starts a new duplicate scan. -* **Clear Ignore List:** Remove all ignored matches you added. You have to start a new scan for the newly cleared ignore list to be effective. -* **Export Results to XHTML:** Take the current results, and create an XHTML file out of it. The columns that are visible when you click on this button will be the columns present in the XHTML file. The file will automatically be opened in your default browser. -* **Send Marked to Trash:** Send all marked duplicates to trash, obviously. -* **Move Marked to...:** Prompt you for a destination, and then move all marked files to that destination. Source file's path might be re-created in destination, depending on the "Copy and Move" preference. -* **Copy Marked to...:** Prompt you for a destination, and then copy all marked files to that destination. Source file's path might be re-created in destination, depending on the "Copy and Move" preference. -* **Remove Marked from Results:** Remove all marked duplicates from results. The actual files will not be touched and will stay where they are. -* **Remove Selected from Results:** Remove all selected duplicates from results. Note that all selected reference files will be ignored, only duplicates can be removed with this action. -* **Make Selected Reference:** Promote all selected duplicates to reference. If a duplicate is a part of a group having a reference file coming from a reference directory (in blue color), no action will be taken for this duplicate. If more than one duplicate among the same group are selected, only the first of each group will be promoted. -* **Add Selected to Ignore List:** This first removes all selected duplicates from results, and then add the match of that duplicate and the current reference in the ignore list. This match will not come up again in further scan. The duplicate itself might come back, but it will be matched with another reference file. You can clear the ignore list with the Clear Ignore List command. -* **Open Selected with Default Application:** Open the file with the application associated with selected file's type. -* **Reveal Selected in Finder:** Open the folder containing selected file. -* **Rename Selected:** Prompts you for a new name, and then rename the selected file. diff --git a/pe/qt/help/templates/versions.mako b/pe/qt/help/templates/versions.mako deleted file mode 100644 index 157c26ba..00000000 --- a/pe/qt/help/templates/versions.mako +++ /dev/null @@ -1,6 +0,0 @@ -<%! - title = 'dupeGuru PE version history' - selected_menu_item = 'Version History' -%> -<%inherit file="/base_dg.mako"/> -${self.output_changelogs(changelog)} \ No newline at end of file From a600ce15d375103686d22be6c65b46c43e149406 Mon Sep 17 00:00:00 2001 From: hsoft Date: Sun, 7 Jun 2009 07:11:52 +0000 Subject: [PATCH 014/275] Mass Rename: AddDirectory() --> add_directory() --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4014 --- base/qt/directories_dialog.py | 2 +- me/cocoa/py/dg_cocoa.py | 2 +- pe/cocoa/py/dg_cocoa.py | 2 +- py/app.py | 2 +- py/app_cocoa_test.py | 8 ++++---- py/app_pe_cocoa.py | 4 ++-- se/cocoa/py/dg_cocoa.py | 2 +- 7 files changed, 11 insertions(+), 11 deletions(-) diff --git a/base/qt/directories_dialog.py b/base/qt/directories_dialog.py index e2f3ddb3..9a559d45 100644 --- a/base/qt/directories_dialog.py +++ b/base/qt/directories_dialog.py @@ -55,7 +55,7 @@ class DirectoriesDialog(QDialog, Ui_DirectoriesDialog): dirpath = unicode(QFileDialog.getExistingDirectory(self, title, '', flags)) if not dirpath: return - self.app.AddDirectory(dirpath) + self.app.add_directory(dirpath) self.directoriesModel.reset() def directoriesChanged(self): diff --git a/me/cocoa/py/dg_cocoa.py b/me/cocoa/py/dg_cocoa.py index 53413c71..313b196b 100644 --- a/me/cocoa/py/dg_cocoa.py +++ b/me/cocoa/py/dg_cocoa.py @@ -21,7 +21,7 @@ class PyDupeGuru(PyApp): #---Directories def addDirectory_(self,directory): - return self.app.AddDirectory(directory) + return self.app.add_directory(directory) def removeDirectory_(self,index): self.app.RemoveDirectory(index) diff --git a/pe/cocoa/py/dg_cocoa.py b/pe/cocoa/py/dg_cocoa.py index b3aadcb6..45a09cf3 100644 --- a/pe/cocoa/py/dg_cocoa.py +++ b/pe/cocoa/py/dg_cocoa.py @@ -20,7 +20,7 @@ class PyDupeGuru(PyApp): #---Directories def addDirectory_(self,directory): - return self.app.AddDirectory(directory) + return self.app.add_directory(directory) def removeDirectory_(self,index): self.app.RemoveDirectory(index) diff --git a/py/app.py b/py/app.py index 99041c7a..f5d2d003 100644 --- a/py/app.py +++ b/py/app.py @@ -104,7 +104,7 @@ class DupeGuru(RegistrableApplication): # func(j) raise NotImplementedError() - def AddDirectory(self, d): + def add_directory(self, d): try: self.directories.add_path(Path(d)) return 0 diff --git a/py/app_cocoa_test.py b/py/app_cocoa_test.py index ad8b937a..54d901b5 100644 --- a/py/app_cocoa_test.py +++ b/py/app_cocoa_test.py @@ -206,17 +206,17 @@ class TCDupeGuru(TestCase): def test_addDirectory_simple(self): app = self.app - self.assertEqual(0,app.AddDirectory(self.datadirpath())) + self.assertEqual(0,app.add_directory(self.datadirpath())) self.assertEqual(1,len(app.directories)) def test_addDirectory_already_there(self): app = self.app - self.assertEqual(0,app.AddDirectory(self.datadirpath())) - self.assertEqual(1,app.AddDirectory(self.datadirpath())) + self.assertEqual(0,app.add_directory(self.datadirpath())) + self.assertEqual(1,app.add_directory(self.datadirpath())) def test_addDirectory_does_not_exist(self): app = self.app - self.assertEqual(2,app.AddDirectory('/does_not_exist')) + self.assertEqual(2,app.add_directory('/does_not_exist')) def test_ignore(self): app = self.app diff --git a/py/app_pe_cocoa.py b/py/app_pe_cocoa.py index 30745606..978b9d43 100644 --- a/py/app_pe_cocoa.py +++ b/py/app_pe_cocoa.py @@ -186,8 +186,8 @@ class DupeGuruPE(app_cocoa.DupeGuru): if result is not None: return result - def AddDirectory(self, d): - result = app_cocoa.DupeGuru.AddDirectory(self, d) + def add_directory(self, d): + result = app_cocoa.DupeGuru.add_directory(self, d) if (result == 0) and (d == 'iPhoto Library'): [iphotolib] = [dir for dir in self.directories if dir.path == d] iphotolib.update() diff --git a/se/cocoa/py/dg_cocoa.py b/se/cocoa/py/dg_cocoa.py index 63a4df2c..2d465c94 100644 --- a/se/cocoa/py/dg_cocoa.py +++ b/se/cocoa/py/dg_cocoa.py @@ -20,7 +20,7 @@ class PyDupeGuru(PyApp): #---Directories def addDirectory_(self,directory): - return self.app.AddDirectory(directory) + return self.app.add_directory(directory) def removeDirectory_(self,index): self.app.RemoveDirectory(index) From b21257d87529ff8ec9a91fcbfa7cfc69a43a0608 Mon Sep 17 00:00:00 2001 From: hsoft Date: Sun, 7 Jun 2009 07:13:07 +0000 Subject: [PATCH 015/275] Mass Rename: AddToIgnoreList() --> add_to_ignore_list() --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4015 --- base/qt/app.py | 2 +- py/app.py | 2 +- py/app_cocoa.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/base/qt/app.py b/base/qt/app.py index 3fa340bf..83fbd849 100644 --- a/base/qt/app.py +++ b/base/qt/app.py @@ -156,7 +156,7 @@ class DupeGuru(DupeGuruBase, QObject): #--- Public def add_dupes_to_ignore_list(self, duplicates): for dupe in duplicates: - self.AddToIgnoreList(dupe) + self.add_to_ignore_list(dupe) self.remove_duplicates(duplicates) def ApplyFilter(self, filter): diff --git a/py/app.py b/py/app.py index f5d2d003..1c3e24a4 100644 --- a/py/app.py +++ b/py/app.py @@ -113,7 +113,7 @@ class DupeGuru(RegistrableApplication): except directories.InvalidPathError: return 2 - def AddToIgnoreList(self, dupe): + def add_to_ignore_list(self, dupe): g = self.results.get_group_of_duplicate(dupe) for other in g: if other is not dupe: diff --git a/py/app_cocoa.py b/py/app_cocoa.py index 4974d700..0f5e5d28 100644 --- a/py/app_cocoa.py +++ b/py/app_cocoa.py @@ -127,7 +127,7 @@ class DupeGuru(app.DupeGuru): #---Public def AddSelectedToIgnoreList(self): for dupe in self.selected_dupes: - self.AddToIgnoreList(dupe) + self.add_to_ignore_list(dupe) copy_or_move_marked = demo_method(app.DupeGuru.copy_or_move_marked) delete_marked = demo_method(app.DupeGuru.delete_marked) From 750225d27b6d598f9bf54dc405dd2a041d9e9a6c Mon Sep 17 00:00:00 2001 From: hsoft Date: Sun, 7 Jun 2009 07:14:47 +0000 Subject: [PATCH 016/275] Mass Rename: ApplyFilter() --> apply_filter() --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4016 --- base/qt/app.py | 4 ++-- base/qt/main_window.py | 4 ++-- me/cocoa/py/dg_cocoa.py | 2 +- pe/cocoa/py/dg_cocoa.py | 2 +- py/app.py | 2 +- py/app_test.py | 12 ++++++------ se/cocoa/py/dg_cocoa.py | 2 +- 7 files changed, 14 insertions(+), 14 deletions(-) diff --git a/base/qt/app.py b/base/qt/app.py index 83fbd849..b710c9c5 100644 --- a/base/qt/app.py +++ b/base/qt/app.py @@ -159,8 +159,8 @@ class DupeGuru(DupeGuruBase, QObject): self.add_to_ignore_list(dupe) self.remove_duplicates(duplicates) - def ApplyFilter(self, filter): - DupeGuruBase.ApplyFilter(self, filter) + def apply_filter(self, filter): + DupeGuruBase.apply_filter(self, filter) self.emit(SIGNAL('resultsChanged()')) def ask_for_reg_code(self): diff --git a/base/qt/main_window.py b/base/qt/main_window.py index 3ca20de3..1f93d0f2 100644 --- a/base/qt/main_window.py +++ b/base/qt/main_window.py @@ -156,11 +156,11 @@ class MainWindow(QMainWindow, Ui_MainWindow): if not ok: return answer = unicode(answer) - self.app.ApplyFilter(answer) + self.app.apply_filter(answer) self._last_filter = answer def cancelFilterTriggered(self): - self.app.ApplyFilter('') + self.app.apply_filter('') def checkForUpdateTriggered(self): QProcess.execute('updater.exe', ['/checknow']) diff --git a/me/cocoa/py/dg_cocoa.py b/me/cocoa/py/dg_cocoa.py index 313b196b..b10b725a 100644 --- a/me/cocoa/py/dg_cocoa.py +++ b/me/cocoa/py/dg_cocoa.py @@ -80,7 +80,7 @@ class PyDupeGuru(PyApp): self.app.AddSelectedToIgnoreList() def applyFilter_(self, filter): - self.app.ApplyFilter(filter) + self.app.apply_filter(filter) def deleteMarked(self): self.app.delete_marked() diff --git a/pe/cocoa/py/dg_cocoa.py b/pe/cocoa/py/dg_cocoa.py index 45a09cf3..f74a4f7a 100644 --- a/pe/cocoa/py/dg_cocoa.py +++ b/pe/cocoa/py/dg_cocoa.py @@ -85,7 +85,7 @@ class PyDupeGuru(PyApp): self.app.delete_marked() def applyFilter_(self, filter): - self.app.ApplyFilter(filter) + self.app.apply_filter(filter) def makeSelectedReference(self): self.app.MakeSelectedReference() diff --git a/py/app.py b/py/app.py index 1c3e24a4..276a879c 100644 --- a/py/app.py +++ b/py/app.py @@ -119,7 +119,7 @@ class DupeGuru(RegistrableApplication): if other is not dupe: self.scanner.ignore_list.Ignore(unicode(other.path), unicode(dupe.path)) - def ApplyFilter(self, filter): + def apply_filter(self, filter): self.results.apply_filter(None) if self.options['escape_filter_regexp']: filter = escape(filter, '()[]\\.|+?^') diff --git a/py/app_test.py b/py/app_test.py index af47067f..9cd732d3 100644 --- a/py/app_test.py +++ b/py/app_test.py @@ -33,27 +33,27 @@ class DupeGuru(DupeGuruBase): class TCDupeGuru(TestCase): cls_tested_module = app - def test_ApplyFilter_calls_results_apply_filter(self): + def test_apply_filter_calls_results_apply_filter(self): app = DupeGuru() self.mock(app.results, 'apply_filter', log_calls(app.results.apply_filter)) - app.ApplyFilter('foo') + app.apply_filter('foo') self.assertEqual(2, len(app.results.apply_filter.calls)) call = app.results.apply_filter.calls[0] self.assert_(call['filter_str'] is None) call = app.results.apply_filter.calls[1] self.assertEqual('foo', call['filter_str']) - def test_ApplyFilter_escapes_regexp(self): + def test_apply_filter_escapes_regexp(self): app = DupeGuru() self.mock(app.results, 'apply_filter', log_calls(app.results.apply_filter)) - app.ApplyFilter('()[]\\.|+?^abc') + app.apply_filter('()[]\\.|+?^abc') call = app.results.apply_filter.calls[1] self.assertEqual('\\(\\)\\[\\]\\\\\\.\\|\\+\\?\\^abc', call['filter_str']) - app.ApplyFilter('(*)') # In "simple mode", we want the * to behave as a wilcard + app.apply_filter('(*)') # In "simple mode", we want the * to behave as a wilcard call = app.results.apply_filter.calls[3] self.assertEqual('\(.*\)', call['filter_str']) app.options['escape_filter_regexp'] = False - app.ApplyFilter('(abc)') + app.apply_filter('(abc)') call = app.results.apply_filter.calls[5] self.assertEqual('(abc)', call['filter_str']) diff --git a/se/cocoa/py/dg_cocoa.py b/se/cocoa/py/dg_cocoa.py index 2d465c94..f9d73d27 100644 --- a/se/cocoa/py/dg_cocoa.py +++ b/se/cocoa/py/dg_cocoa.py @@ -82,7 +82,7 @@ class PyDupeGuru(PyApp): self.app.delete_marked() def applyFilter_(self, filter): - self.app.ApplyFilter(filter) + self.app.apply_filter(filter) def makeSelectedReference(self): self.app.MakeSelectedReference() From 57f89183bb38f9922adcc3e71fbd114742fc2418 Mon Sep 17 00:00:00 2001 From: hsoft Date: Sun, 7 Jun 2009 07:15:56 +0000 Subject: [PATCH 017/275] Mass Rename: CopyOrMove() --> copy_or_move() --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4017 --- py/app.py | 4 ++-- py/app_pe_cocoa.py | 4 ++-- py/app_test.py | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/py/app.py b/py/app.py index 276a879c..387df4e1 100644 --- a/py/app.py +++ b/py/app.py @@ -131,7 +131,7 @@ class DupeGuru(RegistrableApplication): while files.delete_if_empty(path, ['.DS_Store']): path = path[:-1] - def CopyOrMove(self, dupe, copy, destination, dest_type): + def copy_or_move(self, dupe, copy, destination, dest_type): """ copy: True = Copy False = Move destination: string. @@ -164,7 +164,7 @@ class DupeGuru(RegistrableApplication): def do(j): def op(dupe): j.add_progress() - return self.CopyOrMove(dupe, copy, destination, recreate_path) + return self.copy_or_move(dupe, copy, destination, recreate_path) j.start_job(self.results.mark_count) self.last_op_error_count = self.results.perform_on_marked(op, not copy) diff --git a/py/app_pe_cocoa.py b/py/app_pe_cocoa.py index 978b9d43..6e27bb0a 100644 --- a/py/app_pe_cocoa.py +++ b/py/app_pe_cocoa.py @@ -193,10 +193,10 @@ class DupeGuruPE(app_cocoa.DupeGuru): iphotolib.update() return result - def CopyOrMove(self, dupe, copy, destination, dest_type): + def copy_or_move(self, dupe, copy, destination, dest_type): if isinstance(dupe, IPhoto): copy = True - return app_cocoa.DupeGuru.CopyOrMove(self, dupe, copy, destination, dest_type) + return app_cocoa.DupeGuru.copy_or_move(self, dupe, copy, destination, dest_type) def start_scanning(self): for directory in self.directories: diff --git a/py/app_test.py b/py/app_test.py index 9cd732d3..812a0553 100644 --- a/py/app_test.py +++ b/py/app_test.py @@ -57,7 +57,7 @@ class TCDupeGuru(TestCase): call = app.results.apply_filter.calls[5] self.assertEqual('(abc)', call['filter_str']) - def test_CopyOrMove(self): + def test_copy_or_move(self): # The goal here is just to have a test for a previous blowup I had. I know my test coverage # for this unit is pathetic. What's done is done. My approach now is to add tests for # every change I want to make. The blowup was caused by a missing import. @@ -68,13 +68,13 @@ class TCDupeGuru(TestCase): self.mock(os, 'makedirs', lambda path: None) # We don't want the test to create that fake directory self.mock(fs.phys, 'Directory', fs.Directory) # We don't want an error because makedirs didn't work app = DupeGuru() - app.CopyOrMove(dupe, True, 'some_destination', 0) + app.copy_or_move(dupe, True, 'some_destination', 0) self.assertEqual(1, len(hsutil.files.copy.calls)) call = hsutil.files.copy.calls[0] self.assertEqual('some_destination', call['dest_path']) self.assertEqual(dupe.path, call['source_path']) - def test_CopyOrMove_clean_empty_dirs(self): + def test_copy_or_move_clean_empty_dirs(self): tmppath = Path(self.tmpdir()) sourcepath = tmppath + 'source' io.mkdir(sourcepath) @@ -83,7 +83,7 @@ class TCDupeGuru(TestCase): myfile = tmpdir['source']['myfile'] app = DupeGuru() self.mock(app, 'clean_empty_dirs', log_calls(lambda path: None)) - app.CopyOrMove(myfile, False, tmppath + 'dest', 0) + app.copy_or_move(myfile, False, tmppath + 'dest', 0) calls = app.clean_empty_dirs.calls self.assertEqual(1, len(calls)) self.assertEqual(sourcepath, calls[0]['path']) From 7760ef1b215bfad09c636b4a4a71881639e2f18f Mon Sep 17 00:00:00 2001 From: hsoft Date: Sun, 7 Jun 2009 07:16:58 +0000 Subject: [PATCH 018/275] Mass Rename: LoadIgnoreList() --> load_ignore_list() --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4018 --- me/cocoa/py/dg_cocoa.py | 2 +- pe/cocoa/py/dg_cocoa.py | 2 +- py/app.py | 4 ++-- se/cocoa/py/dg_cocoa.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/me/cocoa/py/dg_cocoa.py b/me/cocoa/py/dg_cocoa.py index b10b725a..85064535 100644 --- a/me/cocoa/py/dg_cocoa.py +++ b/me/cocoa/py/dg_cocoa.py @@ -40,7 +40,7 @@ class PyDupeGuru(PyApp): return self.app.ExportToXHTML(column_ids,xslt_path,css_path) def loadIgnoreList(self): - self.app.LoadIgnoreList() + self.app.load_ignore_list() def loadResults(self): self.app.load() diff --git a/pe/cocoa/py/dg_cocoa.py b/pe/cocoa/py/dg_cocoa.py index f74a4f7a..dcbe2d89 100644 --- a/pe/cocoa/py/dg_cocoa.py +++ b/pe/cocoa/py/dg_cocoa.py @@ -42,7 +42,7 @@ class PyDupeGuru(PyApp): return self.app.ExportToXHTML(column_ids,xslt_path,css_path) def loadIgnoreList(self): - self.app.LoadIgnoreList() + self.app.load_ignore_list() def loadResults(self): self.app.load() diff --git a/py/app.py b/py/app.py index 387df4e1..0b1708c0 100644 --- a/py/app.py +++ b/py/app.py @@ -179,9 +179,9 @@ class DupeGuru(RegistrableApplication): def load(self): self._start_job(JOB_LOAD, self._do_load) - self.LoadIgnoreList() + self.load_ignore_list() - def LoadIgnoreList(self): + def load_ignore_list(self): p = op.join(self.appdata, 'ignore_list.xml') self.scanner.ignore_list.load_from_xml(p) diff --git a/se/cocoa/py/dg_cocoa.py b/se/cocoa/py/dg_cocoa.py index f9d73d27..07dc92a6 100644 --- a/se/cocoa/py/dg_cocoa.py +++ b/se/cocoa/py/dg_cocoa.py @@ -39,7 +39,7 @@ class PyDupeGuru(PyApp): return self.app.ExportToXHTML(column_ids,xslt_path,css_path) def loadIgnoreList(self): - self.app.LoadIgnoreList() + self.app.load_ignore_list() def loadResults(self): self.app.load() From 1da443000cf7dd92be420575348543e5eba1f824 Mon Sep 17 00:00:00 2001 From: hsoft Date: Sun, 7 Jun 2009 07:17:56 +0000 Subject: [PATCH 019/275] Mass Rename: SaveIgnoreList() --> save_ignore_list() --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4019 --- base/qt/app.py | 2 +- me/cocoa/py/dg_cocoa.py | 2 +- pe/cocoa/py/dg_cocoa.py | 2 +- py/app.py | 2 +- se/cocoa/py/dg_cocoa.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/base/qt/app.py b/base/qt/app.py index b710c9c5..1a3326a2 100644 --- a/base/qt/app.py +++ b/base/qt/app.py @@ -257,7 +257,7 @@ class DupeGuru(DupeGuruBase, QObject): #--- Events def application_will_terminate(self): self.Save() - self.SaveIgnoreList() + self.save_ignore_list() def job_finished(self, jobid): self.emit(SIGNAL('resultsChanged()')) diff --git a/me/cocoa/py/dg_cocoa.py b/me/cocoa/py/dg_cocoa.py index 85064535..97c1c175 100644 --- a/me/cocoa/py/dg_cocoa.py +++ b/me/cocoa/py/dg_cocoa.py @@ -61,7 +61,7 @@ class PyDupeGuru(PyApp): self.app.ToggleSelectedMarkState() def saveIgnoreList(self): - self.app.SaveIgnoreList() + self.app.save_ignore_list() def saveResults(self): self.app.Save() diff --git a/pe/cocoa/py/dg_cocoa.py b/pe/cocoa/py/dg_cocoa.py index dcbe2d89..bdbdf7e7 100644 --- a/pe/cocoa/py/dg_cocoa.py +++ b/pe/cocoa/py/dg_cocoa.py @@ -63,7 +63,7 @@ class PyDupeGuru(PyApp): self.app.ToggleSelectedMarkState() def saveIgnoreList(self): - self.app.SaveIgnoreList() + self.app.save_ignore_list() def saveResults(self): self.app.Save() diff --git a/py/app.py b/py/app.py index 0b1708c0..d4bf6cbd 100644 --- a/py/app.py +++ b/py/app.py @@ -197,7 +197,7 @@ class DupeGuru(RegistrableApplication): self.directories.SaveToFile(op.join(self.appdata, 'last_directories.xml')) self.results.save_to_xml(op.join(self.appdata, 'last_results.xml')) - def SaveIgnoreList(self): + def save_ignore_list(self): p = op.join(self.appdata, 'ignore_list.xml') self.scanner.ignore_list.save_to_xml(p) diff --git a/se/cocoa/py/dg_cocoa.py b/se/cocoa/py/dg_cocoa.py index 07dc92a6..631e8b00 100644 --- a/se/cocoa/py/dg_cocoa.py +++ b/se/cocoa/py/dg_cocoa.py @@ -60,7 +60,7 @@ class PyDupeGuru(PyApp): self.app.ToggleSelectedMarkState() def saveIgnoreList(self): - self.app.SaveIgnoreList() + self.app.save_ignore_list() def saveResults(self): self.app.Save() From ea024d456b171250dc1e02f6f41d31688076e674 Mon Sep 17 00:00:00 2001 From: hsoft Date: Sun, 7 Jun 2009 07:18:59 +0000 Subject: [PATCH 020/275] Mass Rename: Save() --> save() --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4020 --- base/qt/app.py | 2 +- me/cocoa/py/dg_cocoa.py | 2 +- pe/cocoa/py/dg_cocoa.py | 2 +- py/app.py | 2 +- se/cocoa/py/dg_cocoa.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/base/qt/app.py b/base/qt/app.py index 1a3326a2..60a01389 100644 --- a/base/qt/app.py +++ b/base/qt/app.py @@ -256,7 +256,7 @@ class DupeGuru(DupeGuruBase, QObject): #--- Events def application_will_terminate(self): - self.Save() + self.save() self.save_ignore_list() def job_finished(self, jobid): diff --git a/me/cocoa/py/dg_cocoa.py b/me/cocoa/py/dg_cocoa.py index 97c1c175..e3b86b41 100644 --- a/me/cocoa/py/dg_cocoa.py +++ b/me/cocoa/py/dg_cocoa.py @@ -64,7 +64,7 @@ class PyDupeGuru(PyApp): self.app.save_ignore_list() def saveResults(self): - self.app.Save() + self.app.save() def refreshDetailsWithSelected(self): self.app.RefreshDetailsWithSelected() diff --git a/pe/cocoa/py/dg_cocoa.py b/pe/cocoa/py/dg_cocoa.py index bdbdf7e7..57b60f59 100644 --- a/pe/cocoa/py/dg_cocoa.py +++ b/pe/cocoa/py/dg_cocoa.py @@ -66,7 +66,7 @@ class PyDupeGuru(PyApp): self.app.save_ignore_list() def saveResults(self): - self.app.Save() + self.app.save() def refreshDetailsWithSelected(self): self.app.RefreshDetailsWithSelected() diff --git a/py/app.py b/py/app.py index d4bf6cbd..48dc0412 100644 --- a/py/app.py +++ b/py/app.py @@ -193,7 +193,7 @@ class DupeGuru(RegistrableApplication): self.results.make_ref(dupe) changed_groups.add(g) - def Save(self): + def save(self): self.directories.SaveToFile(op.join(self.appdata, 'last_directories.xml')) self.results.save_to_xml(op.join(self.appdata, 'last_results.xml')) diff --git a/se/cocoa/py/dg_cocoa.py b/se/cocoa/py/dg_cocoa.py index 631e8b00..af32e561 100644 --- a/se/cocoa/py/dg_cocoa.py +++ b/se/cocoa/py/dg_cocoa.py @@ -63,7 +63,7 @@ class PyDupeGuru(PyApp): self.app.save_ignore_list() def saveResults(self): - self.app.Save() + self.app.save() def refreshDetailsWithSelected(self): self.app.RefreshDetailsWithSelected() From f90a5551543e93f1e26a0cf049fafef831030286 Mon Sep 17 00:00:00 2001 From: hsoft Date: Sun, 7 Jun 2009 07:49:19 +0000 Subject: [PATCH 021/275] pe help: v1.7.3 --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4021 --- pe/help/changelog.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pe/help/changelog.yaml b/pe/help/changelog.yaml index f387c334..e324a783 100644 --- a/pe/help/changelog.yaml +++ b/pe/help/changelog.yaml @@ -1,3 +1,8 @@ +- date: 2009-06-07 + version: 1.7.3 + description: | + * Fixed an occasional crash on Copy/Move operations. + * Fixed bugs with iPhoto integration. - date: 2009-05-27 version: 1.7.2 description: | From 0f7c8d1e6cbf1a7fd59ea3e20bcdc52d3bd38d3f Mon Sep 17 00:00:00 2001 From: hsoft Date: Sun, 7 Jun 2009 07:49:37 +0000 Subject: [PATCH 022/275] pe cocoa: v1.7.3 --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4022 --- pe/cocoa/Info.plist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pe/cocoa/Info.plist b/pe/cocoa/Info.plist index 6be0548d..3e7b42f7 100644 --- a/pe/cocoa/Info.plist +++ b/pe/cocoa/Info.plist @@ -23,7 +23,7 @@ CFBundleSignature hsft CFBundleVersion - 1.7.2 + 1.7.3 NSMainNibFile MainMenu NSPrincipalClass From 2498517e0e4e606c9fbefc9a47d6931c111dbdfd Mon Sep 17 00:00:00 2001 From: hsoft Date: Sun, 7 Jun 2009 08:17:48 +0000 Subject: [PATCH 023/275] qt base: set externals and ignores --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4023 From 9c1473b8773a5c9bc0b0a22d4bb9cf52595997ea Mon Sep 17 00:00:00 2001 From: hsoft Date: Sun, 7 Jun 2009 08:18:42 +0000 Subject: [PATCH 024/275] pe qt: adjusted to the new dg structure, and set v1.7.3 --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4024 --- pe/qt/app.py | 23 +++++++++++------------ pe/qt/build.py | 7 +++++-- pe/qt/gen.py | 4 ++-- 3 files changed, 18 insertions(+), 16 deletions(-) diff --git a/pe/qt/app.py b/pe/qt/app.py index f40580ad..a3b693e2 100644 --- a/pe/qt/app.py +++ b/pe/qt/app.py @@ -10,9 +10,8 @@ import os.path as op from PyQt4.QtGui import QImage import PIL.Image -from hs import fs -from hs.fs import phys -from hs.utils.str import get_file_ext +from hsfs import phys, IT_ATTRS, IT_MD5, IT_EXTRA +from hsutil.str import get_file_ext from dupeguru import data_pe from dupeguru.picture.cache import Cache @@ -27,24 +26,24 @@ from preferences_dialog import PreferencesDialog class File(phys.File): cls_info_map = { - 'size': fs.IT_ATTRS, - 'ctime': fs.IT_ATTRS, - 'mtime': fs.IT_ATTRS, - 'md5': fs.IT_MD5, - 'md5partial': fs.IT_MD5, - 'dimensions': fs.IT_EXTRA, + 'size': IT_ATTRS, + 'ctime': IT_ATTRS, + 'mtime': IT_ATTRS, + 'md5': IT_MD5, + 'md5partial': IT_MD5, + 'dimensions': IT_EXTRA, } def _initialize_info(self, section): super(File, self)._initialize_info(section) - if section == fs.IT_EXTRA: + if section == IT_EXTRA: self._info.update({ 'dimensions': (0,0), }) def _read_info(self, section): super(File, self)._read_info(section) - if section == fs.IT_EXTRA: + if section == IT_EXTRA: im = PIL.Image.open(unicode(self.path)) self._info['dimensions'] = im.size @@ -66,7 +65,7 @@ class Directory(phys.Directory): class DupeGuru(DupeGuruBase): LOGO_NAME = 'logo_pe' NAME = 'dupeGuru Picture Edition' - VERSION = '1.7.2' + VERSION = '1.7.3' DELTA_COLUMNS = frozenset([2, 5, 6]) def __init__(self): diff --git a/pe/qt/build.py b/pe/qt/build.py index 6e454952..9af18e34 100644 --- a/pe/qt/build.py +++ b/pe/qt/build.py @@ -9,6 +9,7 @@ # The release version is outdated. Use at least r672 on http://svn.pyinstaller.org/trunk import os +import os.path as op import shutil from app import DupeGuru @@ -17,8 +18,10 @@ def print_and_do(cmd): os.system(cmd) # Removing build and dist -shutil.rmtree('build') -shutil.rmtree('dist') +if op.exists('build'): + shutil.rmtree('build') +if op.exists('dist'): + shutil.rmtree('dist') version = DupeGuru.VERSION versioncomma = version.replace('.', ', ') + ', 0' diff --git a/pe/qt/gen.py b/pe/qt/gen.py index 8bec8d5b..7da1519c 100644 --- a/pe/qt/gen.py +++ b/pe/qt/gen.py @@ -30,9 +30,9 @@ os.chdir('..') os.chdir(op.join('modules', 'block')) os.system('python setup.py build_ext --inplace') -move(op.join('modules', 'block', '_block.so'), op.join('picture', '_block.so')) -move(op.join('modules', 'block', '_block.pyd'), op.join('picture', '_block.pyd')) os.chdir(op.join('..', '..')) +move(op.join('modules', 'block', '_block.so'), op.join('.', '_block.so')) +move(op.join('modules', 'block', '_block.pyd'), op.join('.', '_block.pyd')) print_and_do("pyuic4 details_dialog.ui > details_dialog_ui.py") print_and_do("pyuic4 preferences_dialog.ui > preferences_dialog_ui.py") From 1f72e2a6be80c445246c3d152ef473f88583d3a1 Mon Sep 17 00:00:00 2001 From: hsoft Date: Sun, 7 Jun 2009 14:26:46 +0000 Subject: [PATCH 025/275] Moved the tests to a "tests" subfolder, and "un-unittest"'ed some of them. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4025 --- py/ignore_test.py | 158 ---------------------------- py/tests/__init__.py | 0 py/{ => tests}/app_cocoa_test.py | 22 ++-- py/{ => tests}/app_test.py | 24 ++--- py/{picture => tests}/block_test.py | 23 ++-- py/{picture => tests}/cache_test.py | 31 +++--- py/{ => tests}/directories_test.py | 22 ++-- py/{ => tests}/engine_test.py | 24 ++--- py/{ => tests}/export_test.py | 24 ++--- py/tests/ignore_test.py | 149 ++++++++++++++++++++++++++ py/{ => tests}/results_test.py | 26 ++--- py/{ => tests}/scanner_test.py | 25 ++--- 12 files changed, 226 insertions(+), 302 deletions(-) delete mode 100644 py/ignore_test.py create mode 100644 py/tests/__init__.py rename py/{ => tests}/app_cocoa_test.py (96%) rename py/{ => tests}/app_test.py (92%) rename py/{picture => tests}/block_test.py (96%) rename py/{picture => tests}/cache_test.py (87%) rename py/{ => tests}/directories_test.py (95%) rename py/{ => tests}/engine_test.py (98%) rename py/{ => tests}/export_test.py (87%) create mode 100644 py/tests/ignore_test.py rename py/{ => tests}/results_test.py (98%) rename py/{ => tests}/scanner_test.py (96%) diff --git a/py/ignore_test.py b/py/ignore_test.py deleted file mode 100644 index 8ff91f52..00000000 --- a/py/ignore_test.py +++ /dev/null @@ -1,158 +0,0 @@ -#!/usr/bin/env python -""" -Unit Name: ignore -Created By: Virgil Dupras -Created On: 2006/05/02 -Last modified by:$Author: virgil $ -Last modified on:$Date: 2009-05-28 15:22:39 +0200 (Thu, 28 May 2009) $ - $Revision: 4385 $ -Copyright 2004-2006 Hardcoded Software (http://www.hardcoded.net) -""" -import unittest -import cStringIO -import xml.dom.minidom - -from .ignore import * - -class TCIgnoreList(unittest.TestCase): - def test_empty(self): - il = IgnoreList() - self.assertEqual(0,len(il)) - self.assert_(not il.AreIgnored('foo','bar')) - - def test_simple(self): - il = IgnoreList() - il.Ignore('foo','bar') - self.assert_(il.AreIgnored('foo','bar')) - self.assert_(il.AreIgnored('bar','foo')) - self.assert_(not il.AreIgnored('foo','bleh')) - self.assert_(not il.AreIgnored('bleh','bar')) - self.assertEqual(1,len(il)) - - def test_multiple(self): - il = IgnoreList() - il.Ignore('foo','bar') - il.Ignore('foo','bleh') - il.Ignore('bleh','bar') - il.Ignore('aybabtu','bleh') - self.assert_(il.AreIgnored('foo','bar')) - self.assert_(il.AreIgnored('bar','foo')) - self.assert_(il.AreIgnored('foo','bleh')) - self.assert_(il.AreIgnored('bleh','bar')) - self.assert_(not il.AreIgnored('aybabtu','bar')) - self.assertEqual(4,len(il)) - - def test_clear(self): - il = IgnoreList() - il.Ignore('foo','bar') - il.Clear() - self.assert_(not il.AreIgnored('foo','bar')) - self.assert_(not il.AreIgnored('bar','foo')) - self.assertEqual(0,len(il)) - - def test_add_same_twice(self): - il = IgnoreList() - il.Ignore('foo','bar') - il.Ignore('bar','foo') - self.assertEqual(1,len(il)) - - def test_save_to_xml(self): - il = IgnoreList() - il.Ignore('foo','bar') - il.Ignore('foo','bleh') - il.Ignore('bleh','bar') - f = cStringIO.StringIO() - il.save_to_xml(f) - f.seek(0) - doc = xml.dom.minidom.parse(f) - root = doc.documentElement - self.assertEqual('ignore_list',root.nodeName) - children = [c for c in root.childNodes if c.localName] - self.assertEqual(2,len(children)) - self.assertEqual(2,len([c for c in children if c.nodeName == 'file'])) - f1,f2 = children - subchildren = [c for c in f1.childNodes if c.localName == 'file'] +\ - [c for c in f2.childNodes if c.localName == 'file'] - self.assertEqual(3,len(subchildren)) - - def test_SaveThenLoad(self): - il = IgnoreList() - il.Ignore('foo','bar') - il.Ignore('foo','bleh') - il.Ignore('bleh','bar') - il.Ignore(u'\u00e9','bar') - f = cStringIO.StringIO() - il.save_to_xml(f) - f.seek(0) - il = IgnoreList() - il.load_from_xml(f) - self.assertEqual(4,len(il)) - self.assert_(il.AreIgnored(u'\u00e9','bar')) - - def test_LoadXML_with_empty_file_tags(self): - f = cStringIO.StringIO() - f.write('') - f.seek(0) - il = IgnoreList() - il.load_from_xml(f) - self.assertEqual(0,len(il)) - - def test_AreIgnore_works_when_a_child_is_a_key_somewhere_else(self): - il = IgnoreList() - il.Ignore('foo','bar') - il.Ignore('bar','baz') - self.assert_(il.AreIgnored('bar','foo')) - - - def test_no_dupes_when_a_child_is_a_key_somewhere_else(self): - il = IgnoreList() - il.Ignore('foo','bar') - il.Ignore('bar','baz') - il.Ignore('bar','foo') - self.assertEqual(2,len(il)) - - def test_iterate(self): - #It must be possible to iterate through ignore list - il = IgnoreList() - expected = [('foo','bar'),('bar','baz'),('foo','baz')] - for i in expected: - il.Ignore(i[0],i[1]) - for i in il: - expected.remove(i) #No exception should be raised - self.assert_(not expected) #expected should be empty - - def test_filter(self): - il = IgnoreList() - il.Ignore('foo','bar') - il.Ignore('bar','baz') - il.Ignore('foo','baz') - il.Filter(lambda f,s: f == 'bar') - self.assertEqual(1,len(il)) - self.assert_(not il.AreIgnored('foo','bar')) - self.assert_(il.AreIgnored('bar','baz')) - - def test_save_with_non_ascii_non_unicode_items(self): - il = IgnoreList() - il.Ignore('\xac','\xbf') - f = cStringIO.StringIO() - try: - il.save_to_xml(f) - except Exception,e: - self.fail(str(e)) - - def test_len(self): - il = IgnoreList() - self.assertEqual(0,len(il)) - il.Ignore('foo','bar') - self.assertEqual(1,len(il)) - - def test_nonzero(self): - il = IgnoreList() - self.assert_(not il) - il.Ignore('foo','bar') - self.assert_(il) - - -if __name__ == "__main__": - unittest.main() - diff --git a/py/tests/__init__.py b/py/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/py/app_cocoa_test.py b/py/tests/app_cocoa_test.py similarity index 96% rename from py/app_cocoa_test.py rename to py/tests/app_cocoa_test.py index 54d901b5..60d2e83d 100644 --- a/py/app_cocoa_test.py +++ b/py/tests/app_cocoa_test.py @@ -1,13 +1,9 @@ -#!/usr/bin/env python -""" -Unit Name: dupeguru.tests.app_cocoa -Created By: Virgil Dupras -Created On: 2006/11/11 -Last modified by:$Author: virgil $ -Last modified on:$Date: 2009-05-29 17:51:41 +0200 (Fri, 29 May 2009) $ - $Revision: 4409 $ -Copyright 2006 Hardcoded Software (http://www.hardcoded.net) -""" +# Unit Name: dupeguru.tests.app_cocoa_test +# Created By: Virgil Dupras +# Created On: 2006/11/11 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + import tempfile import shutil import logging @@ -18,13 +14,13 @@ from hsutil.decorators import log_calls import hsfs.phys import os.path as op -from . import engine, data +from .results_test import GetTestGroups +from .. import engine, data try: - from .app_cocoa import DupeGuru as DupeGuruBase, DGDirectory + from ..app_cocoa import DupeGuru as DupeGuruBase, DGDirectory except ImportError: from nose.plugins.skip import SkipTest raise SkipTest("These tests can only be run on OS X") -from .results_test import GetTestGroups class DupeGuru(DupeGuruBase): def __init__(self): diff --git a/py/app_test.py b/py/tests/app_test.py similarity index 92% rename from py/app_test.py rename to py/tests/app_test.py index 812a0553..fbb6afc3 100644 --- a/py/app_test.py +++ b/py/tests/app_test.py @@ -1,14 +1,9 @@ -#!/usr/bin/env python -""" -Unit Name: dupeguru.tests.app -Created By: Virgil Dupras -Created On: 2007-06-23 -Last modified by:$Author: virgil $ -Last modified on:$Date: 2009-05-28 16:02:48 +0200 (Thu, 28 May 2009) $ - $Revision: 4388 $ -Copyright 2007 Hardcoded Software (http://www.hardcoded.net) -""" -import unittest +# Unit Name: dupeguru.tests.app_test +# Created By: Virgil Dupras +# Created On: 2007-06-23 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + import os from hsutil.testcase import TestCase @@ -20,8 +15,8 @@ import hsfs.phys import hsutil.files from hsutil.job import nulljob -from . import data, app -from .app import DupeGuru as DupeGuruBase +from .. import data, app +from ..app import DupeGuru as DupeGuruBase class DupeGuru(DupeGuruBase): def __init__(self): @@ -132,6 +127,3 @@ class TCDupeGuru_clean_empty_dirs(TestCase): self.assertEqual(Path('not-empty/empty'), calls[1]['path']) self.assertEqual(Path('not-empty'), calls[2]['path']) - -if __name__ == '__main__': - unittest.main() \ No newline at end of file diff --git a/py/picture/block_test.py b/py/tests/block_test.py similarity index 96% rename from py/picture/block_test.py rename to py/tests/block_test.py index a06cf617..25d97783 100644 --- a/py/picture/block_test.py +++ b/py/tests/block_test.py @@ -1,17 +1,13 @@ -#!/usr/bin/env python -""" -Unit Name: tests.picture.block -Created By: Virgil Dupras -Created On: 2006/09/01 -Last modified by:$Author: virgil $ -Last modified on:$Date: 2009-05-28 15:22:39 +0200 (Thu, 28 May 2009) $ - $Revision: 4385 $ -Copyright 2004-2006 Hardcoded Software (http://www.hardcoded.net) -""" +# Unit Name: tests.picture.block +# Created By: Virgil Dupras +# Created On: 2006/09/01 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) # The commented out tests are tests for function that have been converted to pure C for speed + import unittest -from .block import * +from ..picture.block import * def my_avgdiff(first, second, limit=768, min_iter=3): # this is so I don't have to re-write every call return avgdiff(first, second, limit, min_iter) @@ -307,7 +303,4 @@ class TCavgdiff(unittest.TestCase): # expected1 = 5 + 10 + 15 # expected2 = 0 + 250 + 10 # self.assertEqual(expected1,maxdiff(blocks1,blocks2,expected1 - 1)) -# - -if __name__ == "__main__": - unittest.main() \ No newline at end of file +# \ No newline at end of file diff --git a/py/picture/cache_test.py b/py/tests/cache_test.py similarity index 87% rename from py/picture/cache_test.py rename to py/tests/cache_test.py index f453112f..c16f330e 100644 --- a/py/picture/cache_test.py +++ b/py/tests/cache_test.py @@ -1,23 +1,19 @@ -#!/usr/bin/env python -""" -Unit Name: tests.picture.cache -Created By: Virgil Dupras -Created On: 2006/09/14 -Last modified by:$Author: virgil $ -Last modified on:$Date: 2009-05-28 15:22:39 +0200 (Thu, 28 May 2009) $ - $Revision: 4385 $ -Copyright 2004-2006 Hardcoded Software (http://www.hardcoded.net) -""" -import unittest +# Unit Name: dupeguru.tests.cache_test +# Created By: Virgil Dupras +# Created On: 2006/09/14 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + from StringIO import StringIO import os.path as op import os import threading from hsutil.testcase import TestCase -from .cache import * -class TCcolors_to_string(unittest.TestCase): +from ..picture.cache import * + +class TCcolors_to_string(TestCase): def test_no_color(self): self.assertEqual('',colors_to_string([])) @@ -30,7 +26,7 @@ class TCcolors_to_string(unittest.TestCase): self.assertEqual('000102030405',colors_to_string([(0,1,2),(3,4,5)])) -class TCstring_to_colors(unittest.TestCase): +class TCstring_to_colors(TestCase): def test_empty(self): self.assertEqual([],string_to_colors('')) @@ -118,7 +114,7 @@ class TCCache(TestCase): self.assertEqual(c[foo_id], b) -class TCCacheSQLEscape(unittest.TestCase): +class TCCacheSQLEscape(TestCase): def test_contains(self): c = Cache() self.assert_("foo'bar" not in c) @@ -140,7 +136,7 @@ class TCCacheSQLEscape(unittest.TestCase): self.fail() -class TCCacheThreaded(unittest.TestCase): +class TCCacheThreaded(TestCase): def test_access_cache(self): def thread_run(): try: @@ -154,6 +150,3 @@ class TCCacheThreaded(unittest.TestCase): t.join() self.assertEqual([(1,2,3)], c['foo']) - -if __name__ == "__main__": - unittest.main() \ No newline at end of file diff --git a/py/directories_test.py b/py/tests/directories_test.py similarity index 95% rename from py/directories_test.py rename to py/tests/directories_test.py index 7d34c343..5b22f542 100644 --- a/py/directories_test.py +++ b/py/tests/directories_test.py @@ -1,14 +1,9 @@ -#!/usr/bin/env python -""" -Unit Name: dupeguru.tests.directories -Created By: Virgil Dupras -Created On: 2006/02/27 -Last modified by:$Author: virgil $ -Last modified on:$Date: 2009-05-29 08:51:14 +0200 (Fri, 29 May 2009) $ - $Revision: 4398 $ -Copyright 2004-2006 Hardcoded Software (http://www.hardcoded.net) -""" -import unittest +# Unit Name: dupeguru.tests.directories_test +# Created By: Virgil Dupras +# Created On: 2006/02/27 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + import os.path as op import os import time @@ -20,7 +15,7 @@ from hsutil.testcase import TestCase import hsfs.phys from hsfs.phys import phys_test -from directories import * +from ..directories import * testpath = Path(TestCase.datadirpath()) @@ -275,6 +270,3 @@ class TCDirectories(TestCase): self.assert_(isinstance(d.add_path(p2), hsfs.phys.Directory)) self.assert_(isinstance(d.add_path(p1), MySpecialDirclass)) - -if __name__ == "__main__": - unittest.main() \ No newline at end of file diff --git a/py/engine_test.py b/py/tests/engine_test.py similarity index 98% rename from py/engine_test.py rename to py/tests/engine_test.py index 8e9706d9..bd0851ec 100644 --- a/py/engine_test.py +++ b/py/tests/engine_test.py @@ -1,22 +1,17 @@ -#!/usr/bin/env python -""" -Unit Name: dupeguru.engine_test -Created By: Virgil Dupras -Created On: 2006/01/29 -Last modified by:$Author: virgil $ -Last modified on:$Date: $ - $Revision: $ -Copyright 2004-2008 Hardcoded Software (http://www.hardcoded.net) -""" -import unittest +# Unit Name: dupeguru.tests.engine_test +# Created By: Virgil Dupras +# Created On: 2006/01/29 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + import sys from hsutil import job from hsutil.decorators import log_calls from hsutil.testcase import TestCase -from . import engine -from .engine import * +from .. import engine +from ..engine import * class NamedObject(object): def __init__(self, name="foobar", with_words=False): @@ -817,6 +812,3 @@ class TCget_groups(TestCase): self.assertEqual(0,self.log[0]) self.assertEqual(100,self.log[-1]) - -if __name__ == "__main__": - unittest.main() diff --git a/py/export_test.py b/py/tests/export_test.py similarity index 87% rename from py/export_test.py rename to py/tests/export_test.py index 5c4a6d87..9469936b 100644 --- a/py/export_test.py +++ b/py/tests/export_test.py @@ -1,21 +1,16 @@ -#!/usr/bin/env python -""" -Unit Name: dupeguru.tests.export -Created By: Virgil Dupras -Created On: 2006/09/16 -Last modified by:$Author: virgil $ -Last modified on:$Date: 2009-05-28 15:22:39 +0200 (Thu, 28 May 2009) $ - $Revision: 4385 $ -Copyright 2004-2006 Hardcoded Software (http://www.hardcoded.net) -""" -import unittest +# Unit Name: dupeguru.tests.export_test +# Created By: Virgil Dupras +# Created On: 2006/09/16 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + from xml.dom import minidom from StringIO import StringIO from hsutil.testcase import TestCase -from .export import * -from . import export +from .. import export +from ..export import * class TCoutput_columns_xml(TestCase): def test_empty_columns(self): @@ -86,6 +81,3 @@ class TCmerge_css_into_xhtml(TestCase): xhtml.seek(0) self.assert_(not merge_css_into_xhtml(xhtml,StringIO())) - -if __name__ == "__main__": - unittest.main() \ No newline at end of file diff --git a/py/tests/ignore_test.py b/py/tests/ignore_test.py new file mode 100644 index 00000000..b19283d4 --- /dev/null +++ b/py/tests/ignore_test.py @@ -0,0 +1,149 @@ +# Unit Name: dupeguru.tests.ignore_test +# Created By: Virgil Dupras +# Created On: 2006/05/02 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +import cStringIO +import xml.dom.minidom + +from nose.tools import eq_ + +from ..ignore import * + +def test_empty(): + il = IgnoreList() + eq_(0,len(il)) + assert not il.AreIgnored('foo','bar') + +def test_simple(): + il = IgnoreList() + il.Ignore('foo','bar') + assert il.AreIgnored('foo','bar') + assert il.AreIgnored('bar','foo') + assert not il.AreIgnored('foo','bleh') + assert not il.AreIgnored('bleh','bar') + eq_(1,len(il)) + +def test_multiple(): + il = IgnoreList() + il.Ignore('foo','bar') + il.Ignore('foo','bleh') + il.Ignore('bleh','bar') + il.Ignore('aybabtu','bleh') + assert il.AreIgnored('foo','bar') + assert il.AreIgnored('bar','foo') + assert il.AreIgnored('foo','bleh') + assert il.AreIgnored('bleh','bar') + assert not il.AreIgnored('aybabtu','bar') + eq_(4,len(il)) + +def test_clear(): + il = IgnoreList() + il.Ignore('foo','bar') + il.Clear() + assert not il.AreIgnored('foo','bar') + assert not il.AreIgnored('bar','foo') + eq_(0,len(il)) + +def test_add_same_twice(): + il = IgnoreList() + il.Ignore('foo','bar') + il.Ignore('bar','foo') + eq_(1,len(il)) + +def test_save_to_xml(): + il = IgnoreList() + il.Ignore('foo','bar') + il.Ignore('foo','bleh') + il.Ignore('bleh','bar') + f = cStringIO.StringIO() + il.save_to_xml(f) + f.seek(0) + doc = xml.dom.minidom.parse(f) + root = doc.documentElement + eq_('ignore_list',root.nodeName) + children = [c for c in root.childNodes if c.localName] + eq_(2,len(children)) + eq_(2,len([c for c in children if c.nodeName == 'file'])) + f1,f2 = children + subchildren = [c for c in f1.childNodes if c.localName == 'file'] +\ + [c for c in f2.childNodes if c.localName == 'file'] + eq_(3,len(subchildren)) + +def test_SaveThenLoad(): + il = IgnoreList() + il.Ignore('foo','bar') + il.Ignore('foo','bleh') + il.Ignore('bleh','bar') + il.Ignore(u'\u00e9','bar') + f = cStringIO.StringIO() + il.save_to_xml(f) + f.seek(0) + il = IgnoreList() + il.load_from_xml(f) + eq_(4,len(il)) + assert il.AreIgnored(u'\u00e9','bar') + +def test_LoadXML_with_empty_file_tags(): + f = cStringIO.StringIO() + f.write('') + f.seek(0) + il = IgnoreList() + il.load_from_xml(f) + eq_(0,len(il)) + +def test_AreIgnore_works_when_a_child_is_a_key_somewhere_else(): + il = IgnoreList() + il.Ignore('foo','bar') + il.Ignore('bar','baz') + assert il.AreIgnored('bar','foo') + + +def test_no_dupes_when_a_child_is_a_key_somewhere_else(): + il = IgnoreList() + il.Ignore('foo','bar') + il.Ignore('bar','baz') + il.Ignore('bar','foo') + eq_(2,len(il)) + +def test_iterate(): + #It must be possible to iterate through ignore list + il = IgnoreList() + expected = [('foo','bar'),('bar','baz'),('foo','baz')] + for i in expected: + il.Ignore(i[0],i[1]) + for i in il: + expected.remove(i) #No exception should be raised + assert not expected #expected should be empty + +def test_filter(): + il = IgnoreList() + il.Ignore('foo','bar') + il.Ignore('bar','baz') + il.Ignore('foo','baz') + il.Filter(lambda f,s: f == 'bar') + eq_(1,len(il)) + assert not il.AreIgnored('foo','bar') + assert il.AreIgnored('bar','baz') + +def test_save_with_non_ascii_non_unicode_items(): + il = IgnoreList() + il.Ignore('\xac','\xbf') + f = cStringIO.StringIO() + try: + il.save_to_xml(f) + except Exception as e: + raise AssertionError(unicode(e)) + +def test_len(): + il = IgnoreList() + eq_(0,len(il)) + il.Ignore('foo','bar') + eq_(1,len(il)) + +def test_nonzero(): + il = IgnoreList() + assert not il + il.Ignore('foo','bar') + assert il diff --git a/py/results_test.py b/py/tests/results_test.py similarity index 98% rename from py/results_test.py rename to py/tests/results_test.py index 1e74efc6..096e9d4e 100644 --- a/py/results_test.py +++ b/py/tests/results_test.py @@ -1,13 +1,9 @@ -#!/usr/bin/env python -""" -Unit Name: dupeguru.tests.results -Created By: Virgil Dupras -Created On: 2006/02/23 -Last modified by:$Author: virgil $ -Last modified on:$Date: 2009-05-28 15:22:39 +0200 (Thu, 28 May 2009) $ - $Revision: 4385 $ -Copyright 2004-2006 Hardcoded Software (http://www.hardcoded.net) -""" +# Unit Name: dupeguru.tests.results_test +# Created By: Virgil Dupras +# Created On: 2006/02/23 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + import unittest import StringIO import xml.dom.minidom @@ -18,9 +14,8 @@ from hsutil.testcase import TestCase from hsutil.misc import first from . import engine_test -from . import data -from . import engine -from .results import * +from .. import data, engine +from ..results import * class NamedObject(engine_test.NamedObject): size = 1 @@ -715,7 +710,7 @@ class TCResultsFilter(TestCase): self.assertEqual(expected, self.results.stat_line) -class TCResultsRefFile(unittest.TestCase): +class TCResultsRefFile(TestCase): def setUp(self): self.results = Results(data) self.objects, self.matches, self.groups = GetTestGroups() @@ -737,6 +732,3 @@ class TCResultsRefFile(unittest.TestCase): expected = '0 / 2 (0.00 B / 2.00 B) duplicates marked.' self.assertEqual(expected, self.results.stat_line) - -if __name__ == "__main__": - unittest.main() diff --git a/py/scanner_test.py b/py/tests/scanner_test.py similarity index 96% rename from py/scanner_test.py rename to py/tests/scanner_test.py index 89ad1417..8a4510a1 100644 --- a/py/scanner_test.py +++ b/py/tests/scanner_test.py @@ -1,22 +1,16 @@ -#!/usr/bin/env python -""" -Unit Name: dupeguru.tests.scanner -Created By: Virgil Dupras -Created On: 2006/03/03 -Last modified by:$Author: virgil $ -Last modified on:$Date: 2009-05-28 15:22:39 +0200 (Thu, 28 May 2009) $ - $Revision: 4385 $ -Copyright 2004-2006 Hardcoded Software (http://www.hardcoded.net) -""" -import unittest +# Unit Name: dupeguru.tests.scanner_test +# Created By: Virgil Dupras +# Created On: 2006/03/03 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) from hsutil import job from hsutil.path import Path from hsutil.testcase import TestCase -from .engine import getwords, Match -from .ignore import IgnoreList -from .scanner import * +from ..engine import getwords, Match +from ..ignore import IgnoreList +from ..scanner import * class NamedObject(object): def __init__(self, name="foobar", size=1): @@ -463,6 +457,3 @@ class TCScannerME(TestCase): [group] = s.GetDupeGroups([o1, o2]) self.assertTrue(group.ref is o2) - -if __name__ == "__main__": - unittest.main() \ No newline at end of file From 6b36c7641fd668e26630fc4061ba9ba886768d1b Mon Sep 17 00:00:00 2001 From: hsoft Date: Mon, 8 Jun 2009 08:25:18 +0000 Subject: [PATCH 026/275] se help: set ignores. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4026 From b7e391cafb2b9caefe87d6eae3ad0214ba985ecb Mon Sep 17 00:00:00 2001 From: hsoft Date: Mon, 8 Jun 2009 08:25:37 +0000 Subject: [PATCH 027/275] se cocoa: Set externals and ignores, and adjusted the code to cocoalib changes made for [#18] --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4027 --- se/cocoa/ResultWindow.m | 6 + se/cocoa/dupeguru.xcodeproj/hsoft.mode1 | 1369 ------------------- se/cocoa/dupeguru.xcodeproj/project.pbxproj | 29 +- 3 files changed, 31 insertions(+), 1373 deletions(-) delete mode 100644 se/cocoa/dupeguru.xcodeproj/hsoft.mode1 diff --git a/se/cocoa/ResultWindow.m b/se/cocoa/ResultWindow.m index 49b80b56..f3910ca5 100644 --- a/se/cocoa/ResultWindow.m +++ b/se/cocoa/ResultWindow.m @@ -36,6 +36,12 @@ [[self window] setToolbar:t]; } +/* Override */ +- (NSString *)logoImageName +{ + return @"dgse_logo_32"; +} + /* Actions */ - (IBAction)changePowerMarker:(id)sender diff --git a/se/cocoa/dupeguru.xcodeproj/hsoft.mode1 b/se/cocoa/dupeguru.xcodeproj/hsoft.mode1 deleted file mode 100644 index 6ffe0908..00000000 --- a/se/cocoa/dupeguru.xcodeproj/hsoft.mode1 +++ /dev/null @@ -1,1369 +0,0 @@ - - - - - ActivePerspectiveName - Project - AllowedModules - - - BundleLoadPath - - MaxInstances - n - Module - PBXSmartGroupTreeModule - Name - Groups and Files Outline View - - - BundleLoadPath - - MaxInstances - n - Module - PBXNavigatorGroup - Name - Editor - - - BundleLoadPath - - MaxInstances - n - Module - XCTaskListModule - Name - Task List - - - BundleLoadPath - - MaxInstances - n - Module - XCDetailModule - Name - File and Smart Group Detail Viewer - - - BundleLoadPath - - MaxInstances - 1 - Module - PBXBuildResultsModule - Name - Detailed Build Results Viewer - - - BundleLoadPath - - MaxInstances - 1 - Module - PBXProjectFindModule - Name - Project Batch Find Tool - - - BundleLoadPath - - MaxInstances - n - Module - PBXRunSessionModule - Name - Run Log - - - BundleLoadPath - - MaxInstances - n - Module - PBXBookmarksModule - Name - Bookmarks Tool - - - BundleLoadPath - - MaxInstances - n - Module - PBXClassBrowserModule - Name - Class Browser - - - BundleLoadPath - - MaxInstances - n - Module - PBXCVSModule - Name - Source Code Control Tool - - - BundleLoadPath - - MaxInstances - n - Module - PBXDebugBreakpointsModule - Name - Debug Breakpoints Tool - - - BundleLoadPath - - MaxInstances - n - Module - XCDockableInspector - Name - Inspector - - - BundleLoadPath - - MaxInstances - n - Module - PBXOpenQuicklyModule - Name - Open Quickly Tool - - - BundleLoadPath - - MaxInstances - 1 - Module - PBXDebugSessionModule - Name - Debugger - - - BundleLoadPath - - MaxInstances - 1 - Module - PBXDebugCLIModule - Name - Debug Console - - - Description - DefaultDescriptionKey - DockingSystemVisible - - Extension - mode1 - FavBarConfig - - PBXProjectModuleGUID - CE381CB409914B41003581CE - XCBarModuleItemNames - - XCBarModuleItems - - - FirstTimeWindowDisplayed - - Identifier - com.apple.perspectives.project.mode1 - MajorVersion - 31 - MinorVersion - 1 - Name - Default - Notifications - - OpenEditors - - PerspectiveWidths - - -1 - -1 - - Perspectives - - - ChosenToolbarItems - - active-executable-popup - action - active-buildstyle-popup - active-target-popup - buildOrClean - build-and-runOrDebug - com.apple.ide.PBXToolbarStopButton - get-info - toggle-editor - - ControllerClassBaseName - - IconName - WindowOfProjectWithEditor - Identifier - perspective.project - IsVertical - - Layout - - - BecomeActive - - ContentConfiguration - - PBXBottomSmartGroupGIDs - - 1C37FBAC04509CD000000102 - 1C37FAAC04509CD000000102 - 1C08E77C0454961000C914BD - 1C37FABC05509CD000000102 - 1C37FABC05539CD112110102 - E2644B35053B69B200211256 - 1C37FABC04509CD000100104 - - PBXProjectModuleGUID - 1CE0B1FE06471DED0097A5F4 - PBXProjectModuleLabel - Files - PBXProjectStructureProvided - yes - PBXSmartGroupTreeModuleColumnData - - PBXSmartGroupTreeModuleColumnWidthsKey - - 194 - - PBXSmartGroupTreeModuleColumnsKey_v4 - - MainColumn - - - PBXSmartGroupTreeModuleOutlineStateKey_v7 - - PBXSmartGroupTreeModuleOutlineStateExpansionKey - - 29B97314FDCFA39411CA2CEA - 080E96DDFE201D6D7F000001 - 29B97317FDCFA39411CA2CEA - 29B97323FDCFA39411CA2CEA - 1058C7A0FEA54F0111CA2CBB - 1058C7A2FEA54F0111CA2CBB - 19C28FACFE9D520D11CA2CBB - 1C37FBAC04509CD000000102 - 1C37FAAC04509CD000000102 - 1C37FABC05509CD000000102 - - PBXSmartGroupTreeModuleOutlineStateSelectionKey - - - 37 - - - PBXSmartGroupTreeModuleOutlineStateVisibleRectKey - {{0, 0}, {194, 764}} - - PBXTopSmartGroupGIDs - - XCIncludePerspectivesSwitch - - XCSharingToken - com.apple.Xcode.GFSharingToken - - GeometryConfiguration - - Frame - {{0, 0}, {211, 782}} - GroupTreeTableConfiguration - - MainColumn - 194 - - RubberWindowFrame - 1 55 1366 823 0 0 1440 878 - - Module - PBXSmartGroupTreeModule - Proportion - 211pt - - - Dock - - - ContentConfiguration - - PBXProjectModuleGUID - 1CE0B20306471E060097A5F4 - PBXProjectModuleLabel - Info.plist - PBXSplitModuleInNavigatorKey - - Split0 - - PBXProjectModuleGUID - 1CE0B20406471E060097A5F4 - PBXProjectModuleLabel - Info.plist - _historyCapacity - 10 - bookmark - CE6D39D50C9B15B600C7FE6C - history - - CEDA43440B07D11900B3091A - CECEEB580B0CAE8E00E6972C - CECEEBE40B0CB68A00E6972C - CECEEBE60B0CB68A00E6972C - CECEEBE70B0CB68A00E6972C - CED9635E0B0F954E00DDBB8C - CE24ED9F0B552A5D00DDF502 - CE6D394A0C9B111900C7FE6C - - prevStack - - CEF411790A110C7F00E7F110 - CE6E6AE60AA528B2002F29BE - CE6E6AE70AA528B2002F29BE - CE17C9AC0B04D16D0023E222 - CE17C9AD0B04D16D0023E222 - CE17CA230B04E15F0023E222 - CED963600B0F954E00DDBB8C - CE24EDA00B552A5D00DDF502 - - - SplitCount - 1 - - StatusBarVisibility - - - GeometryConfiguration - - Frame - {{0, 0}, {1150, 544}} - RubberWindowFrame - 1 55 1366 823 0 0 1440 878 - - Module - PBXNavigatorGroup - Proportion - 544pt - - - ContentConfiguration - - PBXProjectModuleGUID - 1CE0B20506471E060097A5F4 - PBXProjectModuleLabel - Detail - - GeometryConfiguration - - Frame - {{0, 549}, {1150, 233}} - RubberWindowFrame - 1 55 1366 823 0 0 1440 878 - - Module - XCDetailModule - Proportion - 233pt - - - Proportion - 1150pt - - - Name - Project - ServiceClasses - - XCModuleDock - PBXSmartGroupTreeModule - XCModuleDock - PBXNavigatorGroup - XCDetailModule - - TableOfContents - - CE6D39CF0C9B114700C7FE6C - 1CE0B1FE06471DED0097A5F4 - CE6D39D00C9B114700C7FE6C - 1CE0B20306471E060097A5F4 - 1CE0B20506471E060097A5F4 - - ToolbarConfiguration - xcode.toolbar.config.default - - - ControllerClassBaseName - - IconName - WindowOfProject - Identifier - perspective.morph - IsVertical - 0 - Layout - - - BecomeActive - 1 - ContentConfiguration - - PBXBottomSmartGroupGIDs - - 1C37FBAC04509CD000000102 - 1C37FAAC04509CD000000102 - 1C08E77C0454961000C914BD - 1C37FABC05509CD000000102 - 1C37FABC05539CD112110102 - E2644B35053B69B200211256 - 1C37FABC04509CD000100104 - 1CC0EA4004350EF90044410B - 1CC0EA4004350EF90041110B - - PBXProjectModuleGUID - 11E0B1FE06471DED0097A5F4 - PBXProjectModuleLabel - Files - PBXProjectStructureProvided - yes - PBXSmartGroupTreeModuleColumnData - - PBXSmartGroupTreeModuleColumnWidthsKey - - 186 - - PBXSmartGroupTreeModuleColumnsKey_v4 - - MainColumn - - - PBXSmartGroupTreeModuleOutlineStateKey_v7 - - PBXSmartGroupTreeModuleOutlineStateExpansionKey - - 29B97314FDCFA39411CA2CEA - 1C37FABC05509CD000000102 - - PBXSmartGroupTreeModuleOutlineStateSelectionKey - - - 0 - - - PBXSmartGroupTreeModuleOutlineStateVisibleRectKey - {{0, 0}, {186, 337}} - - PBXTopSmartGroupGIDs - - XCIncludePerspectivesSwitch - 1 - XCSharingToken - com.apple.Xcode.GFSharingToken - - GeometryConfiguration - - Frame - {{0, 0}, {203, 355}} - GroupTreeTableConfiguration - - MainColumn - 186 - - RubberWindowFrame - 373 269 690 397 0 0 1440 878 - - Module - PBXSmartGroupTreeModule - Proportion - 100% - - - Name - Morph - PreferredWidth - 300 - ServiceClasses - - XCModuleDock - PBXSmartGroupTreeModule - - TableOfContents - - 11E0B1FE06471DED0097A5F4 - - ToolbarConfiguration - xcode.toolbar.config.default.short - - - PerspectivesBarVisible - - ShelfIsVisible - - SourceDescription - file at '/System/Library/PrivateFrameworks/DevToolsInterface.framework/Versions/A/Resources/XCPerspectivesSpecificationMode1.xcperspec' - StatusbarIsVisible - - TimeStamp - 0.0 - ToolbarDisplayMode - 1 - ToolbarIsVisible - - ToolbarSizeMode - 1 - Type - Perspectives - UpdateMessage - The Default Workspace in this version of Xcode now includes support to hide and show the detail view (what has been referred to as the "Metro-Morph" feature). You must discard your current Default Workspace settings and update to the latest Default Workspace in order to gain this feature. Do you wish to update to the latest Workspace defaults for project '%@'? - WindowJustification - 5 - WindowOrderList - - 1C0AD2B3069F1EA900FABCE6 - CE381CCE09914BC8003581CE - /Users/hsoft/src/dupeguru_cocoa/dupeguru.xcodeproj - - WindowString - 1 55 1366 823 0 0 1440 878 - WindowTools - - - FirstTimeWindowDisplayed - - Identifier - windowTool.build - IsVertical - - Layout - - - Dock - - - ContentConfiguration - - PBXProjectModuleGUID - 1CD0528F0623707200166675 - PBXProjectModuleLabel - - StatusBarVisibility - - - GeometryConfiguration - - Frame - {{0, 0}, {1366, 540}} - RubberWindowFrame - 0 56 1366 822 0 0 1440 878 - - Module - PBXNavigatorGroup - Proportion - 540pt - - - ContentConfiguration - - PBXProjectModuleGUID - XCMainBuildResultsModuleGUID - PBXProjectModuleLabel - Build - XCBuildResultsTrigger_Collapse - 1021 - XCBuildResultsTrigger_Open - 1011 - - GeometryConfiguration - - Frame - {{0, 545}, {1366, 236}} - RubberWindowFrame - 0 56 1366 822 0 0 1440 878 - - Module - PBXBuildResultsModule - Proportion - 236pt - - - Proportion - 781pt - - - Name - Build Results - ServiceClasses - - PBXBuildResultsModule - - StatusbarIsVisible - - TableOfContents - - CE381CCE09914BC8003581CE - CE6D39D10C9B114700C7FE6C - 1CD0528F0623707200166675 - XCMainBuildResultsModuleGUID - - ToolbarConfiguration - xcode.toolbar.config.build - WindowString - 0 56 1366 822 0 0 1440 878 - WindowToolGUID - CE381CCE09914BC8003581CE - WindowToolIsVisible - - - - FirstTimeWindowDisplayed - - Identifier - windowTool.debugger - IsVertical - - Layout - - - Dock - - - ContentConfiguration - - Debugger - - HorizontalSplitView - - _collapsingFrameDimension - 0.0 - _indexOfCollapsedView - 0 - _percentageOfCollapsedView - 0.0 - isCollapsed - yes - sizes - - {{0, 0}, {0, 258}} - {{0, 0}, {1024, 258}} - - - VerticalSplitView - - _collapsingFrameDimension - 0.0 - _indexOfCollapsedView - 0 - _percentageOfCollapsedView - 0.0 - isCollapsed - yes - sizes - - {{0, 0}, {1024, 258}} - {{0, 258}, {1024, 387}} - - - - LauncherConfigVersion - 8 - PBXProjectModuleGUID - 1C162984064C10D400B95A72 - PBXProjectModuleLabel - Debug - GLUTExamples (Underwater) - - GeometryConfiguration - - DebugConsoleDrawerSize - {100, 120} - DebugConsoleVisible - None - DebugConsoleWindowFrame - {{200, 200}, {500, 300}} - DebugSTDIOWindowFrame - {{200, 200}, {500, 300}} - Frame - {{0, 0}, {1024, 645}} - RubberWindowFrame - 342 192 1024 686 0 0 1440 878 - - Module - PBXDebugSessionModule - Proportion - 645pt - - - Proportion - 645pt - - - Name - Debugger - ServiceClasses - - PBXDebugSessionModule - - StatusbarIsVisible - - TableOfContents - - 1CD10A99069EF8BA00B06720 - CE24EDA90B552A6300DDF502 - 1C162984064C10D400B95A72 - CE24EDAA0B552A6300DDF502 - CE24EDAB0B552A6300DDF502 - CE24EDAC0B552A6300DDF502 - CE24EDAD0B552A6300DDF502 - CE24EDAE0B552A6300DDF502 - CE24EDAF0B552A6300DDF502 - - ToolbarConfiguration - xcode.toolbar.config.debug - WindowString - 342 192 1024 686 0 0 1440 878 - WindowToolGUID - 1CD10A99069EF8BA00B06720 - WindowToolIsVisible - - - - FirstTimeWindowDisplayed - - Identifier - windowTool.find - IsVertical - - Layout - - - Dock - - - Dock - - - ContentConfiguration - - PBXProjectModuleGUID - 1CDD528C0622207200134675 - PBXProjectModuleLabel - AppDelegate.m - StatusBarVisibility - - - GeometryConfiguration - - Frame - {{0, 0}, {781, 212}} - RubberWindowFrame - 84 374 781 470 0 0 1440 878 - - Module - PBXNavigatorGroup - Proportion - 781pt - - - Proportion - 212pt - - - BecomeActive - - ContentConfiguration - - PBXProjectModuleGUID - 1CD0528E0623707200166675 - PBXProjectModuleLabel - Project Find - - GeometryConfiguration - - Frame - {{0, 217}, {781, 212}} - RubberWindowFrame - 84 374 781 470 0 0 1440 878 - - Module - PBXProjectFindModule - Proportion - 212pt - - - Proportion - 429pt - - - Name - Project Find - ServiceClasses - - PBXProjectFindModule - - StatusbarIsVisible - - TableOfContents - - 1C530D57069F1CE1000CFCEE - CECEEB510B0CAE8800E6972C - CECEEB520B0CAE8800E6972C - 1CDD528C0622207200134675 - 1CD0528E0623707200166675 - - WindowString - 84 374 781 470 0 0 1440 878 - WindowToolGUID - 1C530D57069F1CE1000CFCEE - WindowToolIsVisible - - - - Identifier - MENUSEPARATOR - - - FirstTimeWindowDisplayed - - Identifier - windowTool.debuggerConsole - IsVertical - - Layout - - - Dock - - - ContentConfiguration - - PBXProjectModuleGUID - 1C78EAAC065D492600B07095 - PBXProjectModuleLabel - Debugger Console - - GeometryConfiguration - - Frame - {{0, 0}, {440, 358}} - RubberWindowFrame - 72 414 440 400 0 0 1440 878 - - Module - PBXDebugCLIModule - Proportion - 358pt - - - Proportion - 359pt - - - Name - Debugger Console - ServiceClasses - - PBXDebugCLIModule - - StatusbarIsVisible - - TableOfContents - - CECD0ADE099294C1003DC359 - CE24EDB00B552A6300DDF502 - 1C78EAAC065D492600B07095 - - WindowString - 72 414 440 400 0 0 1440 878 - WindowToolGUID - CECD0ADE099294C1003DC359 - WindowToolIsVisible - - - - FirstTimeWindowDisplayed - - Identifier - windowTool.run - IsVertical - - Layout - - - Dock - - - ContentConfiguration - - LauncherConfigVersion - 3 - PBXProjectModuleGUID - 1CD0528B0623707200166675 - PBXProjectModuleLabel - Run - Runner - - HorizontalSplitView - - _collapsingFrameDimension - 0.0 - _indexOfCollapsedView - 0 - _percentageOfCollapsedView - 0.0 - isCollapsed - yes - sizes - - {{0, 0}, {367, 168}} - {{0, 173}, {367, 270}} - - - VerticalSplitView - - _collapsingFrameDimension - 0.0 - _indexOfCollapsedView - 0 - _percentageOfCollapsedView - 0.0 - isCollapsed - yes - sizes - - {{0, 0}, {406, 443}} - {{411, 0}, {517, 443}} - - - - - GeometryConfiguration - - Frame - {{0, 0}, {1024, 645}} - RubberWindowFrame - 343 192 1024 686 0 0 1440 878 - - Module - PBXRunSessionModule - Proportion - 645pt - - - Proportion - 645pt - - - Name - Run Log - ServiceClasses - - PBXRunSessionModule - - StatusbarIsVisible - - TableOfContents - - 1C0AD2B3069F1EA900FABCE6 - CE6D39D20C9B114700C7FE6C - 1CD0528B0623707200166675 - CE6D39D30C9B114700C7FE6C - - ToolbarConfiguration - xcode.toolbar.config.run - WindowString - 343 192 1024 686 0 0 1440 878 - WindowToolGUID - 1C0AD2B3069F1EA900FABCE6 - WindowToolIsVisible - - - - Identifier - windowTool.scm - Layout - - - Dock - - - ContentConfiguration - - PBXProjectModuleGUID - 1C78EAB2065D492600B07095 - PBXProjectModuleLabel - <No Editor> - PBXSplitModuleInNavigatorKey - - Split0 - - PBXProjectModuleGUID - 1C78EAB3065D492600B07095 - - SplitCount - 1 - - StatusBarVisibility - 1 - - GeometryConfiguration - - Frame - {{0, 0}, {452, 0}} - RubberWindowFrame - 743 379 452 308 0 0 1280 1002 - - Module - PBXNavigatorGroup - Proportion - 0pt - - - BecomeActive - 1 - ContentConfiguration - - PBXProjectModuleGUID - 1CD052920623707200166675 - PBXProjectModuleLabel - SCM - - GeometryConfiguration - - ConsoleFrame - {{0, 259}, {452, 0}} - Frame - {{0, 7}, {452, 259}} - RubberWindowFrame - 743 379 452 308 0 0 1280 1002 - TableConfiguration - - Status - 30 - FileName - 199 - Path - 197.09500122070312 - - TableFrame - {{0, 0}, {452, 250}} - - Module - PBXCVSModule - Proportion - 262pt - - - Proportion - 266pt - - - Name - SCM - ServiceClasses - - PBXCVSModule - - StatusbarIsVisible - 1 - TableOfContents - - 1C78EAB4065D492600B07095 - 1C78EAB5065D492600B07095 - 1C78EAB2065D492600B07095 - 1CD052920623707200166675 - - ToolbarConfiguration - xcode.toolbar.config.scm - WindowString - 743 379 452 308 0 0 1280 1002 - - - FirstTimeWindowDisplayed - - Identifier - windowTool.breakpoints - IsVertical - - Layout - - - Dock - - - ContentConfiguration - - PBXBottomSmartGroupGIDs - - 1C77FABC04509CD000000102 - - PBXProjectModuleGUID - 1CE0B1FE06471DED0097A5F4 - PBXProjectModuleLabel - Files - PBXProjectStructureProvided - no - PBXSmartGroupTreeModuleColumnData - - PBXSmartGroupTreeModuleColumnWidthsKey - - 168 - - PBXSmartGroupTreeModuleColumnsKey_v4 - - MainColumn - - - PBXSmartGroupTreeModuleOutlineStateKey_v7 - - PBXSmartGroupTreeModuleOutlineStateExpansionKey - - 1C77FABC04509CD000000102 - - PBXSmartGroupTreeModuleOutlineStateSelectionKey - - - 0 - - - PBXSmartGroupTreeModuleOutlineStateVisibleRectKey - {{0, 0}, {168, 350}} - - PBXTopSmartGroupGIDs - - XCIncludePerspectivesSwitch - - - GeometryConfiguration - - Frame - {{0, 0}, {185, 368}} - GroupTreeTableConfiguration - - MainColumn - 168 - - RubberWindowFrame - 21 314 744 409 0 0 1024 746 - - Module - PBXSmartGroupTreeModule - Proportion - 185pt - - - BecomeActive - - ContentConfiguration - - PBXProjectModuleGUID - 1CA1AED706398EBD00589147 - PBXProjectModuleLabel - Detail - - GeometryConfiguration - - Frame - {{190, 0}, {554, 368}} - RubberWindowFrame - 21 314 744 409 0 0 1024 746 - - Module - XCDetailModule - Proportion - 554pt - - - Proportion - 368pt - - - MajorVersion - 2 - MinorVersion - 0 - Name - Breakpoints - ServiceClasses - - PBXSmartGroupTreeModule - XCDetailModule - - StatusbarIsVisible - - TableOfContents - - CEDA9EAC09D2BBCE00741F3F - CEDA9EAD09D2BBCE00741F3F - 1CE0B1FE06471DED0097A5F4 - 1CA1AED706398EBD00589147 - - ToolbarConfiguration - xcode.toolbar.config.breakpoints - WindowString - 21 314 744 409 0 0 1024 746 - WindowToolGUID - CEDA9EAC09D2BBCE00741F3F - WindowToolIsVisible - - - - Identifier - windowTool.debugAnimator - Layout - - - Dock - - - Module - PBXNavigatorGroup - Proportion - 100% - - - Proportion - 100% - - - Name - Debug Visualizer - ServiceClasses - - PBXNavigatorGroup - - StatusbarIsVisible - 1 - ToolbarConfiguration - xcode.toolbar.config.debugAnimator - WindowString - 100 100 700 500 0 0 1280 1002 - - - Identifier - windowTool.bookmarks - Layout - - - Dock - - - Module - PBXBookmarksModule - Proportion - 100% - - - Proportion - 100% - - - Name - Bookmarks - ServiceClasses - - PBXBookmarksModule - - StatusbarIsVisible - 0 - WindowString - 538 42 401 187 0 0 1280 1002 - - - Identifier - windowTool.classBrowser - Layout - - - Dock - - - BecomeActive - 1 - ContentConfiguration - - OptionsSetName - Hierarchy, all classes - PBXProjectModuleGUID - 1CA6456E063B45B4001379D8 - PBXProjectModuleLabel - Class Browser - NSObject - - GeometryConfiguration - - ClassesFrame - {{0, 0}, {374, 96}} - ClassesTreeTableConfiguration - - PBXClassNameColumnIdentifier - 208 - PBXClassBookColumnIdentifier - 22 - - Frame - {{0, 0}, {630, 331}} - MembersFrame - {{0, 105}, {374, 395}} - MembersTreeTableConfiguration - - PBXMemberTypeIconColumnIdentifier - 22 - PBXMemberNameColumnIdentifier - 216 - PBXMemberTypeColumnIdentifier - 97 - PBXMemberBookColumnIdentifier - 22 - - PBXModuleWindowStatusBarHidden2 - 1 - RubberWindowFrame - 385 179 630 352 0 0 1440 878 - - Module - PBXClassBrowserModule - Proportion - 332pt - - - Proportion - 332pt - - - Name - Class Browser - ServiceClasses - - PBXClassBrowserModule - - StatusbarIsVisible - 0 - TableOfContents - - 1C0AD2AF069F1E9B00FABCE6 - 1C0AD2B0069F1E9B00FABCE6 - 1CA6456E063B45B4001379D8 - - ToolbarConfiguration - xcode.toolbar.config.classbrowser - WindowString - 385 179 630 352 0 0 1440 878 - WindowToolGUID - 1C0AD2AF069F1E9B00FABCE6 - WindowToolIsVisible - 0 - - - - diff --git a/se/cocoa/dupeguru.xcodeproj/project.pbxproj b/se/cocoa/dupeguru.xcodeproj/project.pbxproj index 3755a27e..317a5089 100644 --- a/se/cocoa/dupeguru.xcodeproj/project.pbxproj +++ b/se/cocoa/dupeguru.xcodeproj/project.pbxproj @@ -25,12 +25,14 @@ CECA899909DB12CA00A3D774 /* Details.nib in Resources */ = {isa = PBXBuildFile; fileRef = CECA899709DB12CA00A3D774 /* Details.nib */; }; CECA899C09DB132E00A3D774 /* DetailsPanel.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = CECA899A09DB132E00A3D774 /* DetailsPanel.h */; }; CECA899D09DB132E00A3D774 /* DetailsPanel.m in Sources */ = {isa = PBXBuildFile; fileRef = CECA899B09DB132E00A3D774 /* DetailsPanel.m */; }; + CEDD92DA0FDD01640031C7B7 /* BRSingleLineFormatter.m in Sources */ = {isa = PBXBuildFile; fileRef = CEDD92D70FDD01640031C7B7 /* BRSingleLineFormatter.m */; }; + CEDD92DB0FDD01640031C7B7 /* NSCharacterSet_Extensions.m in Sources */ = {isa = PBXBuildFile; fileRef = CEDD92D90FDD01640031C7B7 /* NSCharacterSet_Extensions.m */; }; CEEB135209C837A2004D2330 /* dupeguru.icns in Resources */ = {isa = PBXBuildFile; fileRef = CEEB135109C837A2004D2330 /* dupeguru.icns */; }; CEF7823809C8AA0200EF38FF /* gear.png in Resources */ = {isa = PBXBuildFile; fileRef = CEF7823709C8AA0200EF38FF /* gear.png */; }; CEFC294609C89E3D00D9F998 /* folder32.png in Resources */ = {isa = PBXBuildFile; fileRef = CEFC294509C89E3D00D9F998 /* folder32.png */; }; CEFC295509C89FF200D9F998 /* details32.png in Resources */ = {isa = PBXBuildFile; fileRef = CEFC295309C89FF200D9F998 /* details32.png */; }; CEFC295609C89FF200D9F998 /* preferences32.png in Resources */ = {isa = PBXBuildFile; fileRef = CEFC295409C89FF200D9F998 /* preferences32.png */; }; - CEFC295E09C8A0B000D9F998 /* dg_logo32.png in Resources */ = {isa = PBXBuildFile; fileRef = CEFC295D09C8A0B000D9F998 /* dg_logo32.png */; }; + CEFC295E09C8A0B000D9F998 /* dgse_logo_32.png in Resources */ = {isa = PBXBuildFile; fileRef = CEFC295D09C8A0B000D9F998 /* dgse_logo_32.png */; }; CEFC7F9E0FC9517500CD5728 /* Dialogs.m in Sources */ = {isa = PBXBuildFile; fileRef = CEFC7F8B0FC9517500CD5728 /* Dialogs.m */; }; CEFC7F9F0FC9517500CD5728 /* HSErrorReportWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = CEFC7F8D0FC9517500CD5728 /* HSErrorReportWindow.m */; }; CEFC7FA00FC9517500CD5728 /* Outline.m in Sources */ = {isa = PBXBuildFile; fileRef = CEFC7F8F0FC9517500CD5728 /* Outline.m */; }; @@ -89,12 +91,16 @@ CECA899809DB12CA00A3D774 /* English */ = {isa = PBXFileReference; lastKnownFileType = wrapper.nib; name = English; path = English.lproj/Details.nib; sourceTree = ""; }; CECA899A09DB132E00A3D774 /* DetailsPanel.h */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.h; path = DetailsPanel.h; sourceTree = ""; }; CECA899B09DB132E00A3D774 /* DetailsPanel.m */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.objc; path = DetailsPanel.m; sourceTree = ""; }; + CEDD92D60FDD01640031C7B7 /* BRSingleLineFormatter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BRSingleLineFormatter.h; path = cocoalib/brsinglelineformatter/BRSingleLineFormatter.h; sourceTree = SOURCE_ROOT; }; + CEDD92D70FDD01640031C7B7 /* BRSingleLineFormatter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BRSingleLineFormatter.m; path = cocoalib/brsinglelineformatter/BRSingleLineFormatter.m; sourceTree = SOURCE_ROOT; }; + CEDD92D80FDD01640031C7B7 /* NSCharacterSet_Extensions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = NSCharacterSet_Extensions.h; path = cocoalib/brsinglelineformatter/NSCharacterSet_Extensions.h; sourceTree = SOURCE_ROOT; }; + CEDD92D90FDD01640031C7B7 /* NSCharacterSet_Extensions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = NSCharacterSet_Extensions.m; path = cocoalib/brsinglelineformatter/NSCharacterSet_Extensions.m; sourceTree = SOURCE_ROOT; }; CEEB135109C837A2004D2330 /* dupeguru.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; path = dupeguru.icns; sourceTree = ""; }; CEF7823709C8AA0200EF38FF /* gear.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = gear.png; path = images/gear.png; sourceTree = ""; }; CEFC294509C89E3D00D9F998 /* folder32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = folder32.png; path = images/folder32.png; sourceTree = SOURCE_ROOT; }; CEFC295309C89FF200D9F998 /* details32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = details32.png; path = images/details32.png; sourceTree = SOURCE_ROOT; }; CEFC295409C89FF200D9F998 /* preferences32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = preferences32.png; path = images/preferences32.png; sourceTree = SOURCE_ROOT; }; - CEFC295D09C8A0B000D9F998 /* dg_logo32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = dg_logo32.png; path = images/dg_logo32.png; sourceTree = SOURCE_ROOT; }; + CEFC295D09C8A0B000D9F998 /* dgse_logo_32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = dgse_logo_32.png; path = images/dgse_logo_32.png; sourceTree = ""; }; CEFC7F8A0FC9517500CD5728 /* Dialogs.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Dialogs.h; path = cocoalib/Dialogs.h; sourceTree = SOURCE_ROOT; }; CEFC7F8B0FC9517500CD5728 /* Dialogs.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Dialogs.m; path = cocoalib/Dialogs.m; sourceTree = SOURCE_ROOT; }; CEFC7F8C0FC9517500CD5728 /* HSErrorReportWindow.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = HSErrorReportWindow.h; path = cocoalib/HSErrorReportWindow.h; sourceTree = SOURCE_ROOT; }; @@ -235,11 +241,23 @@ path = w3; sourceTree = ""; }; + CEDD92D50FDD01640031C7B7 /* brsinglelineformatter */ = { + isa = PBXGroup; + children = ( + CEDD92D60FDD01640031C7B7 /* BRSingleLineFormatter.h */, + CEDD92D70FDD01640031C7B7 /* BRSingleLineFormatter.m */, + CEDD92D80FDD01640031C7B7 /* NSCharacterSet_Extensions.h */, + CEDD92D90FDD01640031C7B7 /* NSCharacterSet_Extensions.m */, + ); + name = brsinglelineformatter; + path = cocoalib/brsinglelineformatter; + sourceTree = SOURCE_ROOT; + }; CEFC294309C89E0000D9F998 /* images */ = { isa = PBXGroup; children = ( CEF7823709C8AA0200EF38FF /* gear.png */, - CEFC295D09C8A0B000D9F998 /* dg_logo32.png */, + CEFC295D09C8A0B000D9F998 /* dgse_logo_32.png */, CEFC295309C89FF200D9F998 /* details32.png */, CEFC295409C89FF200D9F998 /* preferences32.png */, CEFC294509C89E3D00D9F998 /* folder32.png */, @@ -250,6 +268,7 @@ CEFC7F890FC9513600CD5728 /* cocoalib */ = { isa = PBXGroup; children = ( + CEDD92D50FDD01640031C7B7 /* brsinglelineformatter */, CEFC7FA70FC9518A00CD5728 /* ErrorReportWindow.xib */, CEFC7FA90FC9518A00CD5728 /* progress.nib */, CEFC7FAB0FC9518A00CD5728 /* registration.nib */, @@ -347,7 +366,7 @@ CEFC294609C89E3D00D9F998 /* folder32.png in Resources */, CEFC295509C89FF200D9F998 /* details32.png in Resources */, CEFC295609C89FF200D9F998 /* preferences32.png in Resources */, - CEFC295E09C8A0B000D9F998 /* dg_logo32.png in Resources */, + CEFC295E09C8A0B000D9F998 /* dgse_logo_32.png in Resources */, CEF7823809C8AA0200EF38FF /* gear.png in Resources */, CECA899909DB12CA00A3D774 /* Details.nib in Resources */, CE3AA46709DB207900DB3A21 /* Directories.nib in Resources */, @@ -383,6 +402,8 @@ CEFC7FB90FC951A700CD5728 /* AppDelegate.m in Sources */, CEFC7FBA0FC951A700CD5728 /* DirectoryPanel.m in Sources */, CEFC7FBB0FC951A700CD5728 /* ResultWindow.m in Sources */, + CEDD92DA0FDD01640031C7B7 /* BRSingleLineFormatter.m in Sources */, + CEDD92DB0FDD01640031C7B7 /* NSCharacterSet_Extensions.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; From 6a4a93f7677c1197fddd5b6fe73c0c7006ce09d2 Mon Sep 17 00:00:00 2001 From: hsoft Date: Tue, 9 Jun 2009 15:35:17 +0000 Subject: [PATCH 028/275] [#20 state:port] Added default excludes to Directories. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4028 --- py/app_cocoa.py | 16 ---------------- py/app_se_cocoa.py | 37 +++++++++++++++++++++++++++++++++++- py/directories.py | 31 +++++++++++++++++++++++------- py/tests/directories_test.py | 26 ++++++++++++++++++++++++- 4 files changed, 85 insertions(+), 25 deletions(-) diff --git a/py/app_cocoa.py b/py/app_cocoa.py index 0f5e5d28..81589f2c 100644 --- a/py/app_cocoa.py +++ b/py/app_cocoa.py @@ -13,9 +13,7 @@ import logging import os.path as op import hsfs as fs -from hsfs.phys.bundle import Bundle from hsutil.cocoa import install_exception_hook -from hsutil.str import get_file_ext from hsutil import io, cocoa, job from hsutil.reg import RegistrationRequired @@ -29,19 +27,6 @@ JOBID2TITLE = { app.JOB_DELETE: "Sending to Trash", } -class DGDirectory(fs.phys.Directory): - def _create_sub_dir(self,name,with_parent = True): - ext = get_file_ext(name) - if ext == 'app': - if with_parent: - parent = self - else: - parent = None - return Bundle(parent,name) - else: - return super(DGDirectory,self)._create_sub_dir(name,with_parent) - - def demo_method(method): def wrapper(self, *args, **kwargs): try: @@ -62,7 +47,6 @@ class DupeGuru(app.DupeGuru): appdata = op.expanduser(op.join('~', '.hsoftdata', appdata_subdir)) app.DupeGuru.__init__(self, data_module, appdata, appid) self.progress = cocoa.ThreadedJobPerformer() - self.directories.dirclass = DGDirectory self.display_delta_values = False self.selected_dupes = [] self.RefreshDetailsTable(None,None) diff --git a/py/app_se_cocoa.py b/py/app_se_cocoa.py index 3d8c62b2..618ed8b9 100644 --- a/py/app_se_cocoa.py +++ b/py/app_se_cocoa.py @@ -5,9 +5,44 @@ # $Id$ # Copyright 2009 Hardcoded Software (http://www.hardcoded.net) -import app_cocoa, data +from hsfs.phys import Directory as DirectoryBase +from hsfs.phys.bundle import Bundle +from hsutil.path import Path +from hsutil.str import get_file_ext + + +from . import app_cocoa, data +from .directories import Directories as DirectoriesBase, STATE_EXCLUDED + +class DGDirectory(DirectoryBase): + def _create_sub_dir(self, name, with_parent = True): + ext = get_file_ext(name) + if ext == 'app': + parent = self if with_parent else None + return Bundle(parent, name) + else: + return super(DGDirectory, self)._create_sub_dir(name, with_parent) + + +class Directories(DirectoriesBase): + ROOT_PATH_TO_EXCLUDE = map(Path, ['/Library', '/Volumes', '/System', '/bin', '/sbin', '/opt', '/private']) + HOME_PATH_TO_EXCLUDE = [Path('Library')] + def __init__(self): + DirectoriesBase.__init__(self) + self.dirclass = DGDirectory + + def _default_state_for_path(self, path): + result = DirectoriesBase._default_state_for_path(self, path) + if result is not None: + return result + if path in self.ROOT_PATH_TO_EXCLUDE: + return STATE_EXCLUDED + if path[:2] == Path('/Users') and path[3:] in self.HOME_PATH_TO_EXCLUDE: + return STATE_EXCLUDED + class DupeGuru(app_cocoa.DupeGuru): def __init__(self): app_cocoa.DupeGuru.__init__(self, data, 'dupeguru', appid=4) + self.directories = Directories() diff --git a/py/directories.py b/py/directories.py index 3d73b5c5..17342c02 100644 --- a/py/directories.py +++ b/py/directories.py @@ -49,11 +49,23 @@ class Directories(object): return len(self._dirs) #---Private - def _get_files(self, from_dir, state=STATE_NORMAL): - state = self.states.get(from_dir.path, state) + def _default_state_for_path(self, path): + # Override this in subclasses to specify the state of some special folders. + if path[-1].startswith('.'): # hidden + return STATE_EXCLUDED + + def _get_files(self, from_dir): + from_path = from_dir.path + state = self.GetState(from_path) + if state == STATE_EXCLUDED: + # Recursively get files from folders with lots of subfolder is expensive. However, there + # might be a subfolder in this path that is not excluded. What we want to do is to skim + # through self.states and see if we must continue, or we can stop right here to save time + if not any(p[:len(from_path)] == from_path for p in self.states): + return result = [] for subdir in from_dir.dirs: - for file in self._get_files(subdir, state): + for file in self._get_files(subdir): yield file if state != STATE_EXCLUDED: for file in from_dir.files: @@ -103,8 +115,9 @@ class Directories(object): try: return self.states[path] except KeyError: - if path[-1].startswith('.'): # hidden - return STATE_EXCLUDED + default_state = self._default_state_for_path(path) + if default_state is not None: + return default_state parent = path[:-1] if parent in self: return self.GetState(parent) @@ -153,9 +166,13 @@ class Directories(object): try: if self.GetState(path) == state: return - self.states[path] = state - if (self.GetState(path[:-1]) == state) and (not path[-1].startswith('.')): + # we don't want to needlessly fill self.states. if GetState returns the same thing + # without an explicit entry, remove that entry + if path in self.states: del self.states[path] + if self.GetState(path) == state: # no need for an entry + return + self.states[path] = state except LookupError: pass diff --git a/py/tests/directories_test.py b/py/tests/directories_test.py index 5b22f542..40247218 100644 --- a/py/tests/directories_test.py +++ b/py/tests/directories_test.py @@ -9,11 +9,13 @@ import os import time import shutil +from nose.tools import eq_ + from hsutil import job, io from hsutil.path import Path from hsutil.testcase import TestCase import hsfs.phys -from hsfs.phys import phys_test +from hsfs.tests import phys_test from ..directories import * @@ -270,3 +272,25 @@ class TCDirectories(TestCase): self.assert_(isinstance(d.add_path(p2), hsfs.phys.Directory)) self.assert_(isinstance(d.add_path(p1), MySpecialDirclass)) + def test_default_path_state_override(self): + # It's possible for a subclass to override the default state of a path + class MyDirectories(Directories): + def _default_state_for_path(self, path): + if 'foobar' in path: + return STATE_EXCLUDED + + d = MyDirectories() + p1 = self.tmppath() + io.mkdir(p1 + 'foobar') + io.open(p1 + 'foobar/somefile', 'w').close() + io.mkdir(p1 + 'foobaz') + io.open(p1 + 'foobaz/somefile', 'w').close() + d.add_path(p1) + eq_(d.GetState(p1 + 'foobaz'), STATE_NORMAL) + eq_(d.GetState(p1 + 'foobar'), STATE_EXCLUDED) + eq_(len(list(d.get_files())), 1) # only the 'foobaz' file is there + # However, the default state can be changed + d.SetState(p1 + 'foobar', STATE_NORMAL) + eq_(d.GetState(p1 + 'foobar'), STATE_NORMAL) + eq_(len(list(d.get_files())), 2) + From ba75f3243d2880e48c34d140139215573e2ca2cc Mon Sep 17 00:00:00 2001 From: hsoft Date: Tue, 9 Jun 2009 17:39:42 +0000 Subject: [PATCH 029/275] [#25 state:fixed] Adjusted to the change in hsfs:[4] (Bundle change). It allows to leverage the File caching system, thus fixing #25. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4029 --- py/app_se_cocoa.py | 11 +++++++++-- py/scanner.py | 6 ++++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/py/app_se_cocoa.py b/py/app_se_cocoa.py index 618ed8b9..a07c7613 100644 --- a/py/app_se_cocoa.py +++ b/py/app_se_cocoa.py @@ -8,6 +8,7 @@ from hsfs.phys import Directory as DirectoryBase from hsfs.phys.bundle import Bundle from hsutil.path import Path +from hsutil.misc import extract from hsutil.str import get_file_ext @@ -15,13 +16,19 @@ from . import app_cocoa, data from .directories import Directories as DirectoriesBase, STATE_EXCLUDED class DGDirectory(DirectoryBase): - def _create_sub_dir(self, name, with_parent = True): + def _create_sub_file(self, name, with_parent=True): ext = get_file_ext(name) if ext == 'app': parent = self if with_parent else None return Bundle(parent, name) else: - return super(DGDirectory, self)._create_sub_dir(name, with_parent) + return super(DGDirectory, self)._create_sub_file(name, with_parent) + + def _fetch_subitems(self): + subdirs, subfiles = super(DGDirectory, self)._fetch_subitems() + apps, normal_dirs = extract(lambda name: get_file_ext(name) == 'app', subdirs) + subfiles += apps + return normal_dirs, subfiles class Directories(DirectoriesBase): diff --git a/py/scanner.py b/py/scanner.py index 9a7521c7..69a03b38 100644 --- a/py/scanner.py +++ b/py/scanner.py @@ -54,7 +54,11 @@ class Scanner(object): SCAN_TYPE_CONTENT_AUDIO: lambda f: [str(f.audiosize)] }[self.scan_type] for f in j.iter_with_progress(files, 'Read metadata of %d/%d files'): + if self.size_threshold: + f.size # pre-read, makes a smoother progress if read here (especially for bundles) f.words = func(f) + if self.size_threshold: + files = [f for f in files if f.size >= self.size_threshold] return mf.getmatches(files, j) @staticmethod @@ -75,8 +79,6 @@ class Scanner(object): j = j.start_subjob([8, 2]) for f in [f for f in files if not hasattr(f, 'is_ref')]: f.is_ref = False - if self.size_threshold: - files = [f for f in files if f.size >= self.size_threshold] logging.info('Getting matches') if self.match_factory is None: matches = self._getmatches(files, j) From 47117da6da9143b2b0c8632b775caa67745fa21f Mon Sep 17 00:00:00 2001 From: hsoft Date: Wed, 10 Jun 2009 09:00:31 +0000 Subject: [PATCH 030/275] se qt: set ignores and externals. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4030 --- se/qt/dgse.spec | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/se/qt/dgse.spec b/se/qt/dgse.spec index e2229597..124cf273 100644 --- a/se/qt/dgse.spec +++ b/se/qt/dgse.spec @@ -1,6 +1,6 @@ # -*- mode: python -*- a = Analysis([os.path.join(HOMEPATH,'support\\_mountzlib.py'), os.path.join(HOMEPATH,'support\\useUnicode.py'), 'start.py'], - pathex=['C:\\src\\dupeguru\\se\\qt']) + pathex=['C:\\src\\dupeguru\\se']) pyz = PYZ(a.pure) exe = EXE(pyz, a.scripts, @@ -9,7 +9,9 @@ exe = EXE(pyz, debug=False, strip=False, upx=True, - console=False , icon='base\\images\\dgse_logo.ico', version='verinfo_tmp') + console=False, + icon='base\\images\\dgse_logo.ico', + version='verinfo_tmp') coll = COLLECT( exe, a.binaries, a.zipfiles, From 126de7096720497a973d321487bef5cd88eddea7 Mon Sep 17 00:00:00 2001 From: hsoft Date: Wed, 10 Jun 2009 09:46:24 +0000 Subject: [PATCH 031/275] [#20 state:fixed] Added default excludes to the windows version. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4031 --- se/qt/app.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/se/qt/app.py b/se/qt/app.py index 3859a5f8..77a78f3e 100644 --- a/se/qt/app.py +++ b/se/qt/app.py @@ -6,12 +6,22 @@ # Copyright 2009 Hardcoded Software (http://www.hardcoded.net) from dupeguru import data +from dupeguru.directories import Directories as DirectoriesBase, STATE_EXCLUDED from base.app import DupeGuru as DupeGuruBase from details_dialog import DetailsDialog from preferences import Preferences from preferences_dialog import PreferencesDialog +class Directories(DirectoriesBase): + ROOT_PATH_TO_EXCLUDE = frozenset(['windows', 'program files']) + def _default_state_for_path(self, path): + result = DirectoriesBase._default_state_for_path(self, path) + if result is not None: + return result + if len(path) == 2 and path[1].lower() in self.ROOT_PATH_TO_EXCLUDE: + return STATE_EXCLUDED + class DupeGuru(DupeGuruBase): LOGO_NAME = 'logo_se' NAME = 'dupeGuru' @@ -21,6 +31,10 @@ class DupeGuru(DupeGuruBase): def __init__(self): DupeGuruBase.__init__(self, data, appid=4) + def _setup(self): + self.directories = Directories() + DupeGuruBase._setup(self) + def _update_options(self): DupeGuruBase._update_options(self) self.scanner.min_match_percentage = self.prefs.filter_hardness From 904b2928772d8d6c796a274fa0505e015766d129 Mon Sep 17 00:00:00 2001 From: hsoft Date: Wed, 10 Jun 2009 09:52:10 +0000 Subject: [PATCH 032/275] se help: v2.7.2 --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4032 --- se/help/changelog.yaml | 6 ++++++ se/help/skeleton/hardcoded.css | 6 ++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/se/help/changelog.yaml b/se/help/changelog.yaml index 8bb0a392..c7a17838 100644 --- a/se/help/changelog.yaml +++ b/se/help/changelog.yaml @@ -1,3 +1,9 @@ +- date: 2009-06-10 + version: 2.7.2 + description: | + * Fixed an occasional crash on Copy/Move operations. + * Added automatic exclusion for sensible folders (like system folders). + * Fixed an occasional crash when application files were part of the results (Mac OS X). - date: 2009-05-29 version: 2.7.1 description: | diff --git a/se/help/skeleton/hardcoded.css b/se/help/skeleton/hardcoded.css index a3b17c5b..2c0fa5fc 100644 --- a/se/help/skeleton/hardcoded.css +++ b/se/help/skeleton/hardcoded.css @@ -9,9 +9,9 @@ BODY BODY,A,P,UL,TABLE,TR,TD { - font-family:Tahoma,Arial,sans-serif; + font-family: "Helvetica Neue", "Lucida Grande", Arial, sans-serif; font-size:10pt; - color: #4477AA;/*darker than 5588bb for the sake of the eyes*/ + color: #336699;/*darker than 5588bb for the sake of the eyes*/ } /***************************************************** @@ -82,7 +82,6 @@ TD.menuframe A.menuitem,A.menuitem_selected { font-size:14pt; - font-family:Tahoma,Arial,sans-serif; font-weight:normal; padding-left:10pt; color:#5588bb; @@ -98,7 +97,6 @@ A.menuitem_selected A.submenuitem { - font-family:Tahoma,Arial,sans-serif; font-weight:normal; color:#5588bb; text-decoration:none; From 73ac65fa4eaaee4b5125eb6865930bcf8f8d45e7 Mon Sep 17 00:00:00 2001 From: hsoft Date: Wed, 10 Jun 2009 11:17:43 +0000 Subject: [PATCH 033/275] se cocoa: v2.7.2 --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4033 --- se/cocoa/Info.plist | 2 +- se/cocoa/py/gen.py | 11 +++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/se/cocoa/Info.plist b/se/cocoa/Info.plist index d24efa47..37ce5973 100644 --- a/se/cocoa/Info.plist +++ b/se/cocoa/Info.plist @@ -23,7 +23,7 @@ CFBundleSignature hsft CFBundleVersion - 2.7.1 + 2.7.2 NSMainNibFile MainMenu NSPrincipalClass diff --git a/se/cocoa/py/gen.py b/se/cocoa/py/gen.py index 2279eb36..6195927d 100644 --- a/se/cocoa/py/gen.py +++ b/se/cocoa/py/gen.py @@ -4,12 +4,15 @@ import os import os.path as op import shutil -print "Cleaning build and dist" +from hsutil.build import print_and_do + +os.chdir('dupeguru') +print_and_do('python gen.py') +os.chdir('..') + if op.exists('build'): shutil.rmtree('build') if op.exists('dist'): shutil.rmtree('dist') -print "Buiding the py2app plugin" - -os.system('python -u setup.py py2app') \ No newline at end of file +print_and_do('python -u setup.py py2app') \ No newline at end of file From 3db6e92d274231e6aaedf0e694cfc2d8d043079f Mon Sep 17 00:00:00 2001 From: hsoft Date: Wed, 10 Jun 2009 13:22:50 +0000 Subject: [PATCH 034/275] me cocoa: set ignores and externals --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4034 --- me/cocoa/dupeguru.xcodeproj/hsoft.mode1 | 1376 ----------------------- 1 file changed, 1376 deletions(-) delete mode 100644 me/cocoa/dupeguru.xcodeproj/hsoft.mode1 diff --git a/me/cocoa/dupeguru.xcodeproj/hsoft.mode1 b/me/cocoa/dupeguru.xcodeproj/hsoft.mode1 deleted file mode 100644 index fd8387ad..00000000 --- a/me/cocoa/dupeguru.xcodeproj/hsoft.mode1 +++ /dev/null @@ -1,1376 +0,0 @@ - - - - - ActivePerspectiveName - Project - AllowedModules - - - BundleLoadPath - - MaxInstances - n - Module - PBXSmartGroupTreeModule - Name - Groups and Files Outline View - - - BundleLoadPath - - MaxInstances - n - Module - PBXNavigatorGroup - Name - Editor - - - BundleLoadPath - - MaxInstances - n - Module - XCTaskListModule - Name - Task List - - - BundleLoadPath - - MaxInstances - n - Module - XCDetailModule - Name - File and Smart Group Detail Viewer - - - BundleLoadPath - - MaxInstances - 1 - Module - PBXBuildResultsModule - Name - Detailed Build Results Viewer - - - BundleLoadPath - - MaxInstances - 1 - Module - PBXProjectFindModule - Name - Project Batch Find Tool - - - BundleLoadPath - - MaxInstances - n - Module - PBXRunSessionModule - Name - Run Log - - - BundleLoadPath - - MaxInstances - n - Module - PBXBookmarksModule - Name - Bookmarks Tool - - - BundleLoadPath - - MaxInstances - n - Module - PBXClassBrowserModule - Name - Class Browser - - - BundleLoadPath - - MaxInstances - n - Module - PBXCVSModule - Name - Source Code Control Tool - - - BundleLoadPath - - MaxInstances - n - Module - PBXDebugBreakpointsModule - Name - Debug Breakpoints Tool - - - BundleLoadPath - - MaxInstances - n - Module - XCDockableInspector - Name - Inspector - - - BundleLoadPath - - MaxInstances - n - Module - PBXOpenQuicklyModule - Name - Open Quickly Tool - - - BundleLoadPath - - MaxInstances - 1 - Module - PBXDebugSessionModule - Name - Debugger - - - BundleLoadPath - - MaxInstances - 1 - Module - PBXDebugCLIModule - Name - Debug Console - - - Description - DefaultDescriptionKey - DockingSystemVisible - - Extension - mode1 - FavBarConfig - - PBXProjectModuleGUID - CE381CB409914B41003581CE - XCBarModuleItemNames - - XCBarModuleItems - - - FirstTimeWindowDisplayed - - Identifier - com.apple.perspectives.project.mode1 - MajorVersion - 31 - MinorVersion - 1 - Name - Default - Notifications - - OpenEditors - - PerspectiveWidths - - -1 - -1 - - Perspectives - - - ChosenToolbarItems - - active-executable-popup - action - active-buildstyle-popup - active-target-popup - buildOrClean - build-and-runOrDebug - com.apple.ide.PBXToolbarStopButton - get-info - toggle-editor - - ControllerClassBaseName - - IconName - WindowOfProjectWithEditor - Identifier - perspective.project - IsVertical - - Layout - - - BecomeActive - - ContentConfiguration - - PBXBottomSmartGroupGIDs - - 1C37FBAC04509CD000000102 - 1C37FAAC04509CD000000102 - 1C08E77C0454961000C914BD - 1C37FABC05509CD000000102 - 1C37FABC05539CD112110102 - E2644B35053B69B200211256 - 1C37FABC04509CD000100104 - 1CC0EA4004350EF90044410B - 1CC0EA4004350EF90041110B - - PBXProjectModuleGUID - 1CE0B1FE06471DED0097A5F4 - PBXProjectModuleLabel - Files - PBXProjectStructureProvided - yes - PBXSmartGroupTreeModuleColumnData - - PBXSmartGroupTreeModuleColumnWidthsKey - - 194 - - PBXSmartGroupTreeModuleColumnsKey_v4 - - MainColumn - - - PBXSmartGroupTreeModuleOutlineStateKey_v7 - - PBXSmartGroupTreeModuleOutlineStateExpansionKey - - 29B97314FDCFA39411CA2CEA - 080E96DDFE201D6D7F000001 - 29B97315FDCFA39411CA2CEA - 29B97317FDCFA39411CA2CEA - 29B97323FDCFA39411CA2CEA - 1058C7A0FEA54F0111CA2CBB - CE1425880AFB718500BD5167 - 19C28FACFE9D520D11CA2CBB - 1C37FBAC04509CD000000102 - 1C37FABC05509CD000000102 - - PBXSmartGroupTreeModuleOutlineStateSelectionKey - - - 37 - - - PBXSmartGroupTreeModuleOutlineStateVisibleRectKey - {{0, 0}, {194, 764}} - - PBXTopSmartGroupGIDs - - XCIncludePerspectivesSwitch - - XCSharingToken - com.apple.Xcode.GFSharingToken - - GeometryConfiguration - - Frame - {{0, 0}, {211, 782}} - GroupTreeTableConfiguration - - MainColumn - 194 - - RubberWindowFrame - 0 55 1372 823 0 0 1440 878 - - Module - PBXSmartGroupTreeModule - Proportion - 211pt - - - Dock - - - ContentConfiguration - - PBXProjectModuleGUID - 1CE0B20306471E060097A5F4 - PBXProjectModuleLabel - Info.plist - PBXSplitModuleInNavigatorKey - - Split0 - - PBXProjectModuleGUID - 1CE0B20406471E060097A5F4 - PBXProjectModuleLabel - Info.plist - _historyCapacity - 10 - bookmark - CE6D39470C9B111800C7FE6C - history - - CEEEF20D0BAD825F00F7AD7F - CEEEF3350BAD8AC700F7AD7F - CE7E210C0BB5B7DD00C69A50 - CE7E21360BB5BD8200C69A50 - CEE301FD0BF73B1900D6840C - CEE301FE0BF73B1900D6840C - CEE301FF0BF73B1900D6840C - CEE302000BF73B1900D6840C - CE6D38650C9B0E1A00C7FE6C - CE6D38680C9B0E1A00C7FE6C - - prevStack - - CE2CB4DA09AE70AA0015538F - CE9DA31409E03DC700B0AAC8 - CE962FD809E1A2310049C9D7 - CECD332A09FEDD9D00964507 - CEF3113D0A06AA42002EC022 - CE2AFF3E0A07838000443588 - CEEEF2130BAD825F00F7AD7F - CE6A176E0BB5A8310090A314 - CE6A176F0BB5A8310090A314 - - - SplitCount - 1 - - StatusBarVisibility - - - GeometryConfiguration - - Frame - {{0, 0}, {1156, 544}} - RubberWindowFrame - 0 55 1372 823 0 0 1440 878 - - Module - PBXNavigatorGroup - Proportion - 544pt - - - ContentConfiguration - - PBXProjectModuleGUID - 1CE0B20506471E060097A5F4 - PBXProjectModuleLabel - Detail - - GeometryConfiguration - - Frame - {{0, 549}, {1156, 233}} - RubberWindowFrame - 0 55 1372 823 0 0 1440 878 - - Module - XCDetailModule - Proportion - 233pt - - - Proportion - 1156pt - - - Name - Project - ServiceClasses - - XCModuleDock - PBXSmartGroupTreeModule - XCModuleDock - PBXNavigatorGroup - XCDetailModule - - TableOfContents - - CE6D39480C9B111800C7FE6C - 1CE0B1FE06471DED0097A5F4 - CE6D39490C9B111800C7FE6C - 1CE0B20306471E060097A5F4 - 1CE0B20506471E060097A5F4 - - ToolbarConfiguration - xcode.toolbar.config.default - - - ControllerClassBaseName - - IconName - WindowOfProject - Identifier - perspective.morph - IsVertical - 0 - Layout - - - BecomeActive - 1 - ContentConfiguration - - PBXBottomSmartGroupGIDs - - 1C37FBAC04509CD000000102 - 1C37FAAC04509CD000000102 - 1C08E77C0454961000C914BD - 1C37FABC05509CD000000102 - 1C37FABC05539CD112110102 - E2644B35053B69B200211256 - 1C37FABC04509CD000100104 - 1CC0EA4004350EF90044410B - 1CC0EA4004350EF90041110B - - PBXProjectModuleGUID - 11E0B1FE06471DED0097A5F4 - PBXProjectModuleLabel - Files - PBXProjectStructureProvided - yes - PBXSmartGroupTreeModuleColumnData - - PBXSmartGroupTreeModuleColumnWidthsKey - - 186 - - PBXSmartGroupTreeModuleColumnsKey_v4 - - MainColumn - - - PBXSmartGroupTreeModuleOutlineStateKey_v7 - - PBXSmartGroupTreeModuleOutlineStateExpansionKey - - 29B97314FDCFA39411CA2CEA - 1C37FABC05509CD000000102 - - PBXSmartGroupTreeModuleOutlineStateSelectionKey - - - 0 - - - PBXSmartGroupTreeModuleOutlineStateVisibleRectKey - {{0, 0}, {186, 337}} - - PBXTopSmartGroupGIDs - - XCIncludePerspectivesSwitch - 1 - XCSharingToken - com.apple.Xcode.GFSharingToken - - GeometryConfiguration - - Frame - {{0, 0}, {203, 355}} - GroupTreeTableConfiguration - - MainColumn - 186 - - RubberWindowFrame - 373 269 690 397 0 0 1440 878 - - Module - PBXSmartGroupTreeModule - Proportion - 100% - - - Name - Morph - PreferredWidth - 300 - ServiceClasses - - XCModuleDock - PBXSmartGroupTreeModule - - TableOfContents - - 11E0B1FE06471DED0097A5F4 - - ToolbarConfiguration - xcode.toolbar.config.default.short - - - PerspectivesBarVisible - - ShelfIsVisible - - SourceDescription - file at '/System/Library/PrivateFrameworks/DevToolsInterface.framework/Versions/A/Resources/XCPerspectivesSpecificationMode1.xcperspec' - StatusbarIsVisible - - TimeStamp - 0.0 - ToolbarDisplayMode - 1 - ToolbarIsVisible - - ToolbarSizeMode - 1 - Type - Perspectives - UpdateMessage - The Default Workspace in this version of Xcode now includes support to hide and show the detail view (what has been referred to as the "Metro-Morph" feature). You must discard your current Default Workspace settings and update to the latest Default Workspace in order to gain this feature. Do you wish to update to the latest Workspace defaults for project '%@'? - WindowJustification - 5 - WindowOrderList - - /Users/hsoft/src/dupeguru_me_cocoa/dupeguru.xcodeproj - - WindowString - 0 55 1372 823 0 0 1440 878 - WindowTools - - - FirstTimeWindowDisplayed - - Identifier - windowTool.build - IsVertical - - Layout - - - Dock - - - ContentConfiguration - - PBXProjectModuleGUID - 1CD0528F0623707200166675 - PBXProjectModuleLabel - - StatusBarVisibility - - - GeometryConfiguration - - Frame - {{0, 0}, {1024, 404}} - RubberWindowFrame - 289 192 1024 686 0 0 1440 878 - - Module - PBXNavigatorGroup - Proportion - 404pt - - - BecomeActive - - ContentConfiguration - - PBXProjectModuleGUID - XCMainBuildResultsModuleGUID - PBXProjectModuleLabel - Build - XCBuildResultsTrigger_Collapse - 1021 - XCBuildResultsTrigger_Open - 1011 - - GeometryConfiguration - - Frame - {{0, 409}, {1024, 236}} - RubberWindowFrame - 289 192 1024 686 0 0 1440 878 - - Module - PBXBuildResultsModule - Proportion - 236pt - - - Proportion - 645pt - - - Name - Build Results - ServiceClasses - - PBXBuildResultsModule - - StatusbarIsVisible - - TableOfContents - - CE381CCE09914BC8003581CE - CE6D38450C9B0D2500C7FE6C - 1CD0528F0623707200166675 - XCMainBuildResultsModuleGUID - - ToolbarConfiguration - xcode.toolbar.config.build - WindowString - 289 192 1024 686 0 0 1440 878 - WindowToolGUID - CE381CCE09914BC8003581CE - WindowToolIsVisible - - - - FirstTimeWindowDisplayed - - Identifier - windowTool.debugger - IsVertical - - Layout - - - Dock - - - ContentConfiguration - - Debugger - - HorizontalSplitView - - _collapsingFrameDimension - 0.0 - _indexOfCollapsedView - 0 - _percentageOfCollapsedView - 0.0 - isCollapsed - yes - sizes - - {{0, 0}, {150, 322}} - {{150, 0}, {874, 322}} - - - VerticalSplitView - - _collapsingFrameDimension - 0.0 - _indexOfCollapsedView - 0 - _percentageOfCollapsedView - 0.0 - isCollapsed - yes - sizes - - {{0, 0}, {1024, 322}} - {{0, 322}, {1024, 323}} - - - - LauncherConfigVersion - 8 - PBXProjectModuleGUID - 1C162984064C10D400B95A72 - PBXProjectModuleLabel - Debug - GLUTExamples (Underwater) - - GeometryConfiguration - - DebugConsoleDrawerSize - {100, 120} - DebugConsoleVisible - None - DebugConsoleWindowFrame - {{200, 200}, {500, 300}} - DebugSTDIOWindowFrame - {{200, 200}, {500, 300}} - Frame - {{0, 0}, {1024, 645}} - RubberWindowFrame - 348 192 1024 686 0 0 1440 878 - - Module - PBXDebugSessionModule - Proportion - 645pt - - - Proportion - 645pt - - - Name - Debugger - ServiceClasses - - PBXDebugSessionModule - - StatusbarIsVisible - - TableOfContents - - 1CD10A99069EF8BA00B06720 - CE7E21210BB5BCA400C69A50 - 1C162984064C10D400B95A72 - CE7E21220BB5BCA400C69A50 - CE7E21230BB5BCA400C69A50 - CE7E21240BB5BCA400C69A50 - CE7E21250BB5BCA400C69A50 - CE7E21260BB5BCA400C69A50 - CE7E21270BB5BCA400C69A50 - - ToolbarConfiguration - xcode.toolbar.config.debug - WindowString - 348 192 1024 686 0 0 1440 878 - WindowToolGUID - 1CD10A99069EF8BA00B06720 - WindowToolIsVisible - - - - FirstTimeWindowDisplayed - - Identifier - windowTool.find - IsVertical - - Layout - - - Dock - - - Dock - - - ContentConfiguration - - PBXProjectModuleGUID - 1CDD528C0622207200134675 - PBXProjectModuleLabel - ResultWindow.m - StatusBarVisibility - - - GeometryConfiguration - - Frame - {{0, 0}, {1377, 620}} - RubberWindowFrame - 0 0 1377 878 0 0 1440 878 - - Module - PBXNavigatorGroup - Proportion - 1377pt - - - Proportion - 620pt - - - BecomeActive - - ContentConfiguration - - PBXProjectModuleGUID - 1CD0528E0623707200166675 - PBXProjectModuleLabel - Project Find - - GeometryConfiguration - - Frame - {{0, 625}, {1377, 212}} - RubberWindowFrame - 0 0 1377 878 0 0 1440 878 - - Module - PBXProjectFindModule - Proportion - 212pt - - - Proportion - 837pt - - - Name - Project Find - ServiceClasses - - PBXProjectFindModule - - StatusbarIsVisible - - TableOfContents - - 1C530D57069F1CE1000CFCEE - CE6A17790BB5A8310090A314 - CE6A177A0BB5A8310090A314 - 1CDD528C0622207200134675 - 1CD0528E0623707200166675 - - WindowString - 0 0 1377 878 0 0 1440 878 - WindowToolGUID - 1C530D57069F1CE1000CFCEE - WindowToolIsVisible - - - - Identifier - MENUSEPARATOR - - - FirstTimeWindowDisplayed - - Identifier - windowTool.debuggerConsole - IsVertical - - Layout - - - Dock - - - ContentConfiguration - - PBXProjectModuleGUID - 1C78EAAC065D492600B07095 - PBXProjectModuleLabel - Debugger Console - - GeometryConfiguration - - Frame - {{0, 0}, {440, 358}} - RubberWindowFrame - 72 414 440 400 0 0 1440 878 - - Module - PBXDebugCLIModule - Proportion - 358pt - - - Proportion - 359pt - - - Name - Debugger Console - ServiceClasses - - PBXDebugCLIModule - - StatusbarIsVisible - - TableOfContents - - CECD0ADE099294C1003DC359 - CE7E21280BB5BCA400C69A50 - 1C78EAAC065D492600B07095 - - WindowString - 72 414 440 400 0 0 1440 878 - WindowToolGUID - CECD0ADE099294C1003DC359 - WindowToolIsVisible - - - - FirstTimeWindowDisplayed - - Identifier - windowTool.run - IsVertical - - Layout - - - Dock - - - ContentConfiguration - - LauncherConfigVersion - 3 - PBXProjectModuleGUID - 1CD0528B0623707200166675 - PBXProjectModuleLabel - Run - Runner - - HorizontalSplitView - - _collapsingFrameDimension - 0.0 - _indexOfCollapsedView - 0 - _percentageOfCollapsedView - 0.0 - isCollapsed - yes - sizes - - {{0, 0}, {367, 168}} - {{0, 173}, {367, 270}} - - - VerticalSplitView - - _collapsingFrameDimension - 0.0 - _indexOfCollapsedView - 0 - _percentageOfCollapsedView - 0.0 - isCollapsed - yes - sizes - - {{0, 0}, {406, 443}} - {{411, 0}, {517, 443}} - - - - - GeometryConfiguration - - Frame - {{0, 0}, {1024, 645}} - RubberWindowFrame - 262 127 1024 686 0 0 1440 878 - - Module - PBXRunSessionModule - Proportion - 645pt - - - Proportion - 645pt - - - Name - Run Log - ServiceClasses - - PBXRunSessionModule - - StatusbarIsVisible - - TableOfContents - - 1C0AD2B3069F1EA900FABCE6 - CECCEDD50BB6B39F00873A67 - 1CD0528B0623707200166675 - CECCEDD60BB6B39F00873A67 - - ToolbarConfiguration - xcode.toolbar.config.run - WindowString - 262 127 1024 686 0 0 1440 878 - WindowToolGUID - 1C0AD2B3069F1EA900FABCE6 - WindowToolIsVisible - - - - Identifier - windowTool.scm - Layout - - - Dock - - - ContentConfiguration - - PBXProjectModuleGUID - 1C78EAB2065D492600B07095 - PBXProjectModuleLabel - <No Editor> - PBXSplitModuleInNavigatorKey - - Split0 - - PBXProjectModuleGUID - 1C78EAB3065D492600B07095 - - SplitCount - 1 - - StatusBarVisibility - 1 - - GeometryConfiguration - - Frame - {{0, 0}, {452, 0}} - RubberWindowFrame - 743 379 452 308 0 0 1280 1002 - - Module - PBXNavigatorGroup - Proportion - 0pt - - - BecomeActive - 1 - ContentConfiguration - - PBXProjectModuleGUID - 1CD052920623707200166675 - PBXProjectModuleLabel - SCM - - GeometryConfiguration - - ConsoleFrame - {{0, 259}, {452, 0}} - Frame - {{0, 7}, {452, 259}} - RubberWindowFrame - 743 379 452 308 0 0 1280 1002 - TableConfiguration - - Status - 30 - FileName - 199 - Path - 197.09500122070312 - - TableFrame - {{0, 0}, {452, 250}} - - Module - PBXCVSModule - Proportion - 262pt - - - Proportion - 266pt - - - Name - SCM - ServiceClasses - - PBXCVSModule - - StatusbarIsVisible - 1 - TableOfContents - - 1C78EAB4065D492600B07095 - 1C78EAB5065D492600B07095 - 1C78EAB2065D492600B07095 - 1CD052920623707200166675 - - ToolbarConfiguration - xcode.toolbar.config.scm - WindowString - 743 379 452 308 0 0 1280 1002 - - - FirstTimeWindowDisplayed - - Identifier - windowTool.breakpoints - IsVertical - - Layout - - - Dock - - - BecomeActive - - ContentConfiguration - - PBXBottomSmartGroupGIDs - - 1C77FABC04509CD000000102 - - PBXProjectModuleGUID - 1CE0B1FE06471DED0097A5F4 - PBXProjectModuleLabel - Files - PBXProjectStructureProvided - no - PBXSmartGroupTreeModuleColumnData - - PBXSmartGroupTreeModuleColumnWidthsKey - - 168 - - PBXSmartGroupTreeModuleColumnsKey_v4 - - MainColumn - - - PBXSmartGroupTreeModuleOutlineStateKey_v7 - - PBXSmartGroupTreeModuleOutlineStateExpansionKey - - 1C77FABC04509CD000000102 - 1C3E0DCA080725EA00A55177 - - PBXSmartGroupTreeModuleOutlineStateSelectionKey - - - 2 - 0 - - - PBXSmartGroupTreeModuleOutlineStateVisibleRectKey - {{0, 0}, {168, 350}} - - PBXTopSmartGroupGIDs - - XCIncludePerspectivesSwitch - - - GeometryConfiguration - - Frame - {{0, 0}, {185, 368}} - GroupTreeTableConfiguration - - MainColumn - 168 - - RubberWindowFrame - 52 435 744 409 0 0 1440 878 - - Module - PBXSmartGroupTreeModule - Proportion - 185pt - - - ContentConfiguration - - PBXProjectModuleGUID - 1CA1AED706398EBD00589147 - PBXProjectModuleLabel - Detail - - GeometryConfiguration - - Frame - {{190, 0}, {554, 368}} - RubberWindowFrame - 52 435 744 409 0 0 1440 878 - - Module - XCDetailModule - Proportion - 554pt - - - Proportion - 368pt - - - MajorVersion - 2 - MinorVersion - 0 - Name - Breakpoints - ServiceClasses - - PBXSmartGroupTreeModule - XCDetailModule - - StatusbarIsVisible - - TableOfContents - - CE6B28F20AFB890700508D93 - CE6B28F30AFB890700508D93 - 1CE0B1FE06471DED0097A5F4 - 1CA1AED706398EBD00589147 - - ToolbarConfiguration - xcode.toolbar.config.breakpoints - WindowString - 52 435 744 409 0 0 1440 878 - WindowToolGUID - CE6B28F20AFB890700508D93 - WindowToolIsVisible - - - - Identifier - windowTool.debugAnimator - Layout - - - Dock - - - Module - PBXNavigatorGroup - Proportion - 100% - - - Proportion - 100% - - - Name - Debug Visualizer - ServiceClasses - - PBXNavigatorGroup - - StatusbarIsVisible - 1 - ToolbarConfiguration - xcode.toolbar.config.debugAnimator - WindowString - 100 100 700 500 0 0 1280 1002 - - - Identifier - windowTool.bookmarks - Layout - - - Dock - - - Module - PBXBookmarksModule - Proportion - 100% - - - Proportion - 100% - - - Name - Bookmarks - ServiceClasses - - PBXBookmarksModule - - StatusbarIsVisible - 0 - WindowString - 538 42 401 187 0 0 1280 1002 - - - Identifier - windowTool.classBrowser - Layout - - - Dock - - - BecomeActive - 1 - ContentConfiguration - - OptionsSetName - Hierarchy, all classes - PBXProjectModuleGUID - 1CA6456E063B45B4001379D8 - PBXProjectModuleLabel - Class Browser - NSObject - - GeometryConfiguration - - ClassesFrame - {{0, 0}, {374, 96}} - ClassesTreeTableConfiguration - - PBXClassNameColumnIdentifier - 208 - PBXClassBookColumnIdentifier - 22 - - Frame - {{0, 0}, {630, 331}} - MembersFrame - {{0, 105}, {374, 395}} - MembersTreeTableConfiguration - - PBXMemberTypeIconColumnIdentifier - 22 - PBXMemberNameColumnIdentifier - 216 - PBXMemberTypeColumnIdentifier - 97 - PBXMemberBookColumnIdentifier - 22 - - PBXModuleWindowStatusBarHidden2 - 1 - RubberWindowFrame - 385 179 630 352 0 0 1440 878 - - Module - PBXClassBrowserModule - Proportion - 332pt - - - Proportion - 332pt - - - Name - Class Browser - ServiceClasses - - PBXClassBrowserModule - - StatusbarIsVisible - 0 - TableOfContents - - 1C0AD2AF069F1E9B00FABCE6 - 1C0AD2B0069F1E9B00FABCE6 - 1CA6456E063B45B4001379D8 - - ToolbarConfiguration - xcode.toolbar.config.classbrowser - WindowString - 385 179 630 352 0 0 1440 878 - WindowToolGUID - 1C0AD2AF069F1E9B00FABCE6 - WindowToolIsVisible - 0 - - - - From 8f24c2baf319f24cee89f560f1cb8af60a9239dc Mon Sep 17 00:00:00 2001 From: hsoft Date: Wed, 10 Jun 2009 13:33:30 +0000 Subject: [PATCH 035/275] [#18 state:fixed] Adapted to copy/paste fix in reg interface and fixed a couple of things here and there. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4035 --- me/cocoa/ResultWindow.m | 2 +- me/cocoa/dupeguru.xcodeproj/project.pbxproj | 29 ++++++++++++++++++--- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/me/cocoa/ResultWindow.m b/me/cocoa/ResultWindow.m index 540216e3..eb4530c2 100644 --- a/me/cocoa/ResultWindow.m +++ b/me/cocoa/ResultWindow.m @@ -40,7 +40,7 @@ /* Overrides */ - (NSString *)logoImageName { - return @"dgme_logo32"; + return @"dgme_logo_32"; } /* Actions */ diff --git a/me/cocoa/dupeguru.xcodeproj/project.pbxproj b/me/cocoa/dupeguru.xcodeproj/project.pbxproj index da9f71bb..c44bd77a 100644 --- a/me/cocoa/dupeguru.xcodeproj/project.pbxproj +++ b/me/cocoa/dupeguru.xcodeproj/project.pbxproj @@ -32,6 +32,8 @@ CE381C9C09914ADF003581CE /* ResultWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = CE381C9A09914ADF003581CE /* ResultWindow.m */; }; CE381D0509915304003581CE /* dg_cocoa.plugin in Resources */ = {isa = PBXBuildFile; fileRef = CE381CF509915304003581CE /* dg_cocoa.plugin */; }; CE3AA46709DB207900DB3A21 /* Directories.nib in Resources */ = {isa = PBXBuildFile; fileRef = CE3AA46509DB207900DB3A21 /* Directories.nib */; }; + CE49DEF60FDFEB810098617B /* BRSingleLineFormatter.m in Sources */ = {isa = PBXBuildFile; fileRef = CE49DEF30FDFEB810098617B /* BRSingleLineFormatter.m */; }; + CE49DEF70FDFEB810098617B /* NSCharacterSet_Extensions.m in Sources */ = {isa = PBXBuildFile; fileRef = CE49DEF50FDFEB810098617B /* NSCharacterSet_Extensions.m */; }; CE515DF30FC6C12E00EC695D /* Dialogs.m in Sources */ = {isa = PBXBuildFile; fileRef = CE515DE10FC6C12E00EC695D /* Dialogs.m */; }; CE515DF40FC6C12E00EC695D /* HSErrorReportWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = CE515DE30FC6C12E00EC695D /* HSErrorReportWindow.m */; }; CE515DF50FC6C12E00EC695D /* Outline.m in Sources */ = {isa = PBXBuildFile; fileRef = CE515DE50FC6C12E00EC695D /* Outline.m */; }; @@ -49,11 +51,11 @@ CE515E1F0FC6C19300EC695D /* ResultWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = CE515E1C0FC6C19300EC695D /* ResultWindow.m */; }; CE68EE6809ABC48000971085 /* DirectoryPanel.m in Sources */ = {isa = PBXBuildFile; fileRef = CE68EE6609ABC48000971085 /* DirectoryPanel.m */; }; CE848A1909DD85810004CB44 /* Consts.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = CE848A1809DD85810004CB44 /* Consts.h */; }; + CEA7D2C50FDFED340037CD8C /* dgme_logo_32.png in Resources */ = {isa = PBXBuildFile; fileRef = CEA7D2C40FDFED340037CD8C /* dgme_logo_32.png */; }; CECA899909DB12CA00A3D774 /* Details.nib in Resources */ = {isa = PBXBuildFile; fileRef = CECA899709DB12CA00A3D774 /* Details.nib */; }; CECA899C09DB132E00A3D774 /* DetailsPanel.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = CECA899A09DB132E00A3D774 /* DetailsPanel.h */; }; CECA899D09DB132E00A3D774 /* DetailsPanel.m in Sources */ = {isa = PBXBuildFile; fileRef = CECA899B09DB132E00A3D774 /* DetailsPanel.m */; }; CED2A6880A05102700AC4C3F /* power_marker32.png in Resources */ = {isa = PBXBuildFile; fileRef = CED2A6870A05102600AC4C3F /* power_marker32.png */; }; - CED2A6970A05128900AC4C3F /* dgme_logo32.png in Resources */ = {isa = PBXBuildFile; fileRef = CED2A6960A05128900AC4C3F /* dgme_logo32.png */; }; CEEB135209C837A2004D2330 /* dupeguru.icns in Resources */ = {isa = PBXBuildFile; fileRef = CEEB135109C837A2004D2330 /* dupeguru.icns */; }; CEF7823809C8AA0200EF38FF /* gear.png in Resources */ = {isa = PBXBuildFile; fileRef = CEF7823709C8AA0200EF38FF /* gear.png */; }; CEFC294609C89E3D00D9F998 /* folder32.png in Resources */ = {isa = PBXBuildFile; fileRef = CEFC294509C89E3D00D9F998 /* folder32.png */; }; @@ -96,6 +98,10 @@ CE381C9B09914ADF003581CE /* ResultWindow.h */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.h; path = ResultWindow.h; sourceTree = SOURCE_ROOT; }; CE381CF509915304003581CE /* dg_cocoa.plugin */ = {isa = PBXFileReference; lastKnownFileType = folder; name = dg_cocoa.plugin; path = py/dist/dg_cocoa.plugin; sourceTree = SOURCE_ROOT; }; CE3AA46609DB207900DB3A21 /* English */ = {isa = PBXFileReference; lastKnownFileType = wrapper.nib; name = English; path = English.lproj/Directories.nib; sourceTree = ""; }; + CE49DEF20FDFEB810098617B /* BRSingleLineFormatter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BRSingleLineFormatter.h; path = cocoalib/brsinglelineformatter/BRSingleLineFormatter.h; sourceTree = SOURCE_ROOT; }; + CE49DEF30FDFEB810098617B /* BRSingleLineFormatter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BRSingleLineFormatter.m; path = cocoalib/brsinglelineformatter/BRSingleLineFormatter.m; sourceTree = SOURCE_ROOT; }; + CE49DEF40FDFEB810098617B /* NSCharacterSet_Extensions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = NSCharacterSet_Extensions.h; path = cocoalib/brsinglelineformatter/NSCharacterSet_Extensions.h; sourceTree = SOURCE_ROOT; }; + CE49DEF50FDFEB810098617B /* NSCharacterSet_Extensions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = NSCharacterSet_Extensions.m; path = cocoalib/brsinglelineformatter/NSCharacterSet_Extensions.m; sourceTree = SOURCE_ROOT; }; CE515DE00FC6C12E00EC695D /* Dialogs.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Dialogs.h; path = cocoalib/Dialogs.h; sourceTree = SOURCE_ROOT; }; CE515DE10FC6C12E00EC695D /* Dialogs.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Dialogs.m; path = cocoalib/Dialogs.m; sourceTree = SOURCE_ROOT; }; CE515DE20FC6C12E00EC695D /* HSErrorReportWindow.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = HSErrorReportWindow.h; path = cocoalib/HSErrorReportWindow.h; sourceTree = SOURCE_ROOT; }; @@ -129,11 +135,11 @@ CE68EE6509ABC48000971085 /* DirectoryPanel.h */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.h; path = DirectoryPanel.h; sourceTree = SOURCE_ROOT; }; CE68EE6609ABC48000971085 /* DirectoryPanel.m */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.objc; path = DirectoryPanel.m; sourceTree = SOURCE_ROOT; }; CE848A1809DD85810004CB44 /* Consts.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Consts.h; sourceTree = ""; }; + CEA7D2C40FDFED340037CD8C /* dgme_logo_32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = dgme_logo_32.png; path = images/dgme_logo_32.png; sourceTree = SOURCE_ROOT; }; CECA899809DB12CA00A3D774 /* English */ = {isa = PBXFileReference; lastKnownFileType = wrapper.nib; name = English; path = English.lproj/Details.nib; sourceTree = ""; }; CECA899A09DB132E00A3D774 /* DetailsPanel.h */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.h; path = DetailsPanel.h; sourceTree = ""; }; CECA899B09DB132E00A3D774 /* DetailsPanel.m */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.objc; path = DetailsPanel.m; sourceTree = ""; }; CED2A6870A05102600AC4C3F /* power_marker32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = power_marker32.png; path = images/power_marker32.png; sourceTree = SOURCE_ROOT; }; - CED2A6960A05128900AC4C3F /* dgme_logo32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = dgme_logo32.png; path = images/dgme_logo32.png; sourceTree = SOURCE_ROOT; }; CEEB135109C837A2004D2330 /* dupeguru.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; path = dupeguru.icns; sourceTree = ""; }; CEF7823709C8AA0200EF38FF /* gear.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = gear.png; path = images/gear.png; sourceTree = ""; }; CEFC294509C89E3D00D9F998 /* folder32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = folder32.png; path = images/folder32.png; sourceTree = SOURCE_ROOT; }; @@ -256,9 +262,22 @@ path = w3; sourceTree = SOURCE_ROOT; }; + CE49DEF10FDFEB810098617B /* brsinglelineformatter */ = { + isa = PBXGroup; + children = ( + CE49DEF20FDFEB810098617B /* BRSingleLineFormatter.h */, + CE49DEF30FDFEB810098617B /* BRSingleLineFormatter.m */, + CE49DEF40FDFEB810098617B /* NSCharacterSet_Extensions.h */, + CE49DEF50FDFEB810098617B /* NSCharacterSet_Extensions.m */, + ); + name = brsinglelineformatter; + path = cocoalib/brsinglelineformatter; + sourceTree = SOURCE_ROOT; + }; CE515DDD0FC6C09400EC695D /* cocoalib */ = { isa = PBXGroup; children = ( + CE49DEF10FDFEB810098617B /* brsinglelineformatter */, CE515DFC0FC6C13E00EC695D /* ErrorReportWindow.xib */, CE515DFE0FC6C13E00EC695D /* progress.nib */, CE515E000FC6C13E00EC695D /* registration.nib */, @@ -303,7 +322,7 @@ CEFC294309C89E0000D9F998 /* images */ = { isa = PBXGroup; children = ( - CED2A6960A05128900AC4C3F /* dgme_logo32.png */, + CEA7D2C40FDFED340037CD8C /* dgme_logo_32.png */, CED2A6870A05102600AC4C3F /* power_marker32.png */, CEF7823709C8AA0200EF38FF /* gear.png */, CEFC295309C89FF200D9F998 /* details32.png */, @@ -370,12 +389,12 @@ CECA899909DB12CA00A3D774 /* Details.nib in Resources */, CE3AA46709DB207900DB3A21 /* Directories.nib in Resources */, CED2A6880A05102700AC4C3F /* power_marker32.png in Resources */, - CED2A6970A05128900AC4C3F /* dgme_logo32.png in Resources */, CE12149E0AC86DB900E93983 /* dg.xsl in Resources */, CE12149F0AC86DB900E93983 /* hardcoded.css in Resources */, CE515E020FC6C13E00EC695D /* ErrorReportWindow.xib in Resources */, CE515E030FC6C13E00EC695D /* progress.nib in Resources */, CE515E040FC6C13E00EC695D /* registration.nib in Resources */, + CEA7D2C50FDFED340037CD8C /* dgme_logo_32.png in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -403,6 +422,8 @@ CE515E1D0FC6C19300EC695D /* AppDelegate.m in Sources */, CE515E1E0FC6C19300EC695D /* DirectoryPanel.m in Sources */, CE515E1F0FC6C19300EC695D /* ResultWindow.m in Sources */, + CE49DEF60FDFEB810098617B /* BRSingleLineFormatter.m in Sources */, + CE49DEF70FDFEB810098617B /* NSCharacterSet_Extensions.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; From becc4a87a7f74a17df354ec765c0f802bb91e21c Mon Sep 17 00:00:00 2001 From: hsoft Date: Wed, 10 Jun 2009 13:54:24 +0000 Subject: [PATCH 036/275] [#26 state:fixed] Attempt to fix those iTunes connection errors. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4036 --- py/app_me_cocoa.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/py/app_me_cocoa.py b/py/app_me_cocoa.py index 51a61767..f3f7096f 100644 --- a/py/app_me_cocoa.py +++ b/py/app_me_cocoa.py @@ -36,11 +36,12 @@ class DupeGuruME(app_cocoa.DupeGuru): def remove_dead_tracks(self): def do(j): a = app('iTunes') + 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() + track.delete(timeout=0) except CommandError as e: logging.warning('Error while trying to remove a track from iTunes: %s' % unicode(e)) @@ -49,9 +50,10 @@ class DupeGuruME(app_cocoa.DupeGuru): def scan_dead_tracks(self): def do(j): a = app('iTunes') + a.activate(timeout=0) try: - [source] = [s for s in a.sources() if s.kind() == k.library] - [library] = source.library_playlists() + [source] = [s for s in a.sources(timeout=0) if s.kind(timeout=0) == k.library] + [library] = source.library_playlists(timeout=0) except ValueError: logging.warning('Some unexpected iTunes configuration encountered') return @@ -60,7 +62,7 @@ class DupeGuruME(app_cocoa.DupeGuru): for index, track in enumerate(j.iter_with_progress(tracks)): if index % 100 == 0: time.sleep(.1) - if track.location() == k.missing_value: + if track.location(timeout=0) == k.missing_value: self.dead_tracks.append(track) logging.info('Found %d dead tracks' % len(self.dead_tracks)) From 0c14b3be7072b0e1592008c07ba67257d05415f6 Mon Sep 17 00:00:00 2001 From: hsoft Date: Wed, 10 Jun 2009 13:55:36 +0000 Subject: [PATCH 037/275] me help: v5.6.2 --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4037 --- me/help/changelog.yaml | 5 +++++ me/help/skeleton/hardcoded.css | 6 ++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/me/help/changelog.yaml b/me/help/changelog.yaml index db4644f2..39e03ff0 100644 --- a/me/help/changelog.yaml +++ b/me/help/changelog.yaml @@ -1,3 +1,8 @@ +- date: 2009-06-10 + version: 5.6.2 + description: | + * Fixed an occasional crash on Copy/Move operations. + * Fixed bugs with iTunes integration. - date: 2009-05-30 version: 5.6.1 description: | diff --git a/me/help/skeleton/hardcoded.css b/me/help/skeleton/hardcoded.css index a3b17c5b..2c0fa5fc 100644 --- a/me/help/skeleton/hardcoded.css +++ b/me/help/skeleton/hardcoded.css @@ -9,9 +9,9 @@ BODY BODY,A,P,UL,TABLE,TR,TD { - font-family:Tahoma,Arial,sans-serif; + font-family: "Helvetica Neue", "Lucida Grande", Arial, sans-serif; font-size:10pt; - color: #4477AA;/*darker than 5588bb for the sake of the eyes*/ + color: #336699;/*darker than 5588bb for the sake of the eyes*/ } /***************************************************** @@ -82,7 +82,6 @@ TD.menuframe A.menuitem,A.menuitem_selected { font-size:14pt; - font-family:Tahoma,Arial,sans-serif; font-weight:normal; padding-left:10pt; color:#5588bb; @@ -98,7 +97,6 @@ A.menuitem_selected A.submenuitem { - font-family:Tahoma,Arial,sans-serif; font-weight:normal; color:#5588bb; text-decoration:none; From 3bd37bf0d228740d5bded51ece762fae7a7cead9 Mon Sep 17 00:00:00 2001 From: hsoft Date: Wed, 10 Jun 2009 14:01:29 +0000 Subject: [PATCH 038/275] me qt: set ignores and externals --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4038 From 89b2bd4325ea1ff6fcaefec1de2c8672bb831406 Mon Sep 17 00:00:00 2001 From: hsoft Date: Wed, 10 Jun 2009 14:03:55 +0000 Subject: [PATCH 039/275] se qt: v2.7.2 --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4039 --- se/qt/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/se/qt/app.py b/se/qt/app.py index 77a78f3e..daf00010 100644 --- a/se/qt/app.py +++ b/se/qt/app.py @@ -25,7 +25,7 @@ class Directories(DirectoriesBase): class DupeGuru(DupeGuruBase): LOGO_NAME = 'logo_se' NAME = 'dupeGuru' - VERSION = '2.7.1' + VERSION = '2.7.2' DELTA_COLUMNS = frozenset([2, 4, 5]) def __init__(self): From 44720c803efa00c57d0d0409859935905c1ae1c3 Mon Sep 17 00:00:00 2001 From: hsoft Date: Thu, 11 Jun 2009 14:57:24 +0000 Subject: [PATCH 040/275] me help: now uses hsdocgen rather than web --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4040 --- me/help/gen.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/me/help/gen.py b/me/help/gen.py index 7fa6be01..9b74a83a 100644 --- a/me/help/gen.py +++ b/me/help/gen.py @@ -2,6 +2,6 @@ import os -from web import generate_help +from hsdocgen import generate_help generate_help.main('.', 'dupeguru_me_help', force_render=True) \ No newline at end of file From f23d1385d131d68ac525e7e9a514c08199a9bbdb Mon Sep 17 00:00:00 2001 From: hsoft Date: Thu, 11 Jun 2009 14:59:26 +0000 Subject: [PATCH 041/275] pe help: now uses hsdocgen rather than web --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4041 --- pe/help/gen.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/pe/help/gen.py b/pe/help/gen.py index 8ed33e4e..620f02eb 100644 --- a/pe/help/gen.py +++ b/pe/help/gen.py @@ -1,12 +1,7 @@ #!/usr/bin/env python -# Unit Name: -# Created By: Virgil Dupras -# Created On: 2009-05-24 -# $Id$ -# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) import os -from web import generate_help +from hsdocgen import generate_help generate_help.main('.', 'dupeguru_pe_help', force_render=True) From a03e5e1d27722f9061eae59ac4a7608e3cf1c6ae Mon Sep 17 00:00:00 2001 From: hsoft Date: Thu, 11 Jun 2009 15:00:24 +0000 Subject: [PATCH 042/275] se help: now uses hsdocgen rather than web --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4042 --- se/help/gen.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/se/help/gen.py b/se/help/gen.py index 3f067c83..4187233c 100644 --- a/se/help/gen.py +++ b/se/help/gen.py @@ -1,12 +1,7 @@ #!/usr/bin/env python -# Unit Name: -# Created By: Virgil Dupras -# Created On: 2009-05-24 -# $Id$ -# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) import os -from web import generate_help +from hsdocgen import generate_help generate_help.main('.', 'dupeguru_help', force_render=True) From d3ce49db6b68ec923eccdea234d8530c1ba82fa2 Mon Sep 17 00:00:00 2001 From: hsoft Date: Sun, 14 Jun 2009 14:48:47 +0000 Subject: [PATCH 043/275] [#29] Moving reference switching cocoa code to dgbase before modifying it. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4043 --- base/cocoa/ResultWindow.h | 9 ++++++++ base/cocoa/ResultWindow.m | 45 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/base/cocoa/ResultWindow.h b/base/cocoa/ResultWindow.h index 1cdfb70f..d7582dad 100644 --- a/base/cocoa/ResultWindow.h +++ b/base/cocoa/ResultWindow.h @@ -22,13 +22,22 @@ BOOL _powerMode; BOOL _displayDelta; } +/* Override */ - (NSString *)logoImageName; + +/* Helpers */ +- (NSArray *)getSelected:(BOOL)aDupesOnly; +- (NSArray *)getSelectedPaths:(BOOL)aDupesOnly; +- (void)performPySelection:(NSArray *)aIndexPaths; + /* Actions */ - (IBAction)changeDelta:(id)sender; - (IBAction)copyMarked:(id)sender; - (IBAction)deleteMarked:(id)sender; - (IBAction)expandAll:(id)sender; - (IBAction)moveMarked:(id)sender; +- (IBAction)switchSelected:(id)sender; + /* Notifications */ - (void)jobCompleted:(NSNotification *)aNotification; @end diff --git a/base/cocoa/ResultWindow.m b/base/cocoa/ResultWindow.m index 1aa6cb8a..dd254303 100644 --- a/base/cocoa/ResultWindow.m +++ b/base/cocoa/ResultWindow.m @@ -63,6 +63,44 @@ return @"dg_logo32"; } +/* Helpers */ +- (NSArray *)getSelected:(BOOL)aDupesOnly +{ + if (_powerMode) + aDupesOnly = NO; + NSIndexSet *indexes = [matches selectedRowIndexes]; + NSMutableArray *nodeList = [NSMutableArray array]; + OVNode *node; + int i = [indexes firstIndex]; + while (i != NSNotFound) + { + node = [matches itemAtRow:i]; + if (!aDupesOnly || ([node level] > 1)) + [nodeList addObject:node]; + i = [indexes indexGreaterThanIndex:i]; + } + return nodeList; +} + +- (NSArray *)getSelectedPaths:(BOOL)aDupesOnly +{ + NSMutableArray *r = [NSMutableArray array]; + NSArray *selected = [self getSelected:aDupesOnly]; + NSEnumerator *e = [selected objectEnumerator]; + OVNode *node; + while (node = [e nextObject]) + [r addObject:p2a([node indexPath])]; + return r; +} + +- (void)performPySelection:(NSArray *)aIndexPaths +{ + if (_powerMode) + [py selectPowerMarkerNodePaths:aIndexPaths]; + else + [py selectResultNodePaths:aIndexPaths]; +} + /* Actions */ - (IBAction)changeDelta:(id)sender { @@ -129,6 +167,13 @@ } } +- (IBAction)switchSelected:(id)sender +{ + [self performPySelection:[self getSelectedPaths:YES]]; + [py makeSelectedReference]; + [[NSNotificationCenter defaultCenter] postNotificationName:ResultsChangedNotification object:self]; +} + /* Delegate */ - (void)outlineView:(NSOutlineView *)outlineView didClickTableColumn:(NSTableColumn *)tableColumn From ef9c7f1d254c4ccbaba9ef9191510cf7f20c6cb6 Mon Sep 17 00:00:00 2001 From: hsoft Date: Sun, 14 Jun 2009 14:50:05 +0000 Subject: [PATCH 044/275] [#29] se cocoa: the code handling ref switching is duplicated across editions. Moved it down to dgbase. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4044 --- se/cocoa/ResultWindow.h | 4 ---- se/cocoa/ResultWindow.m | 43 ----------------------------------------- 2 files changed, 47 deletions(-) diff --git a/se/cocoa/ResultWindow.h b/se/cocoa/ResultWindow.h index 443d5afe..abb28e4d 100644 --- a/se/cocoa/ResultWindow.h +++ b/se/cocoa/ResultWindow.h @@ -37,7 +37,6 @@ - (IBAction)revealSelected:(id)sender; - (IBAction)showPreferencesPanel:(id)sender; - (IBAction)startDuplicateScan:(id)sender; -- (IBAction)switchSelected:(id)sender; - (IBAction)toggleColumn:(id)sender; - (IBAction)toggleDelta:(id)sender; - (IBAction)toggleDetailsPanel:(id)sender; @@ -46,10 +45,7 @@ - (NSTableColumn *)getColumnForIdentifier:(int)aIdentifier title:(NSString *)aTitle width:(int)aWidth refCol:(NSTableColumn *)aColumn; - (NSArray *)getColumnsOrder; - (NSDictionary *)getColumnsWidth; -- (NSArray *)getSelected:(BOOL)aDupesOnly; -- (NSArray *)getSelectedPaths:(BOOL)aDupesOnly; - (void)initResultColumns; -- (void)performPySelection:(NSArray *)aIndexPaths; - (void)refreshStats; - (void)restoreColumnsPosition:(NSArray *)aColumnsOrder widths:(NSDictionary *)aColumnsWidth; @end diff --git a/se/cocoa/ResultWindow.m b/se/cocoa/ResultWindow.m index f3910ca5..d4a29763 100644 --- a/se/cocoa/ResultWindow.m +++ b/se/cocoa/ResultWindow.m @@ -223,13 +223,7 @@ [Dialogs showMessage:@"The selected directories contain no scannable file."]; [app toggleDirectories:nil]; } -} -- (IBAction)switchSelected:(id)sender -{ - [self performPySelection:[self getSelectedPaths:YES]]; - [py makeSelectedReference]; - [[NSNotificationCenter defaultCenter] postNotificationName:ResultsChangedNotification object:self]; } - (IBAction)toggleColumn:(id)sender @@ -325,43 +319,6 @@ return result; } -- (NSArray *)getSelected:(BOOL)aDupesOnly -{ - if (_powerMode) - aDupesOnly = NO; - NSIndexSet *indexes = [matches selectedRowIndexes]; - NSMutableArray *nodeList = [NSMutableArray array]; - OVNode *node; - int i = [indexes firstIndex]; - while (i != NSNotFound) - { - node = [matches itemAtRow:i]; - if (!aDupesOnly || ([node level] > 1)) - [nodeList addObject:node]; - i = [indexes indexGreaterThanIndex:i]; - } - return nodeList; -} - -- (NSArray *)getSelectedPaths:(BOOL)aDupesOnly -{ - NSMutableArray *r = [NSMutableArray array]; - NSArray *selected = [self getSelected:aDupesOnly]; - NSEnumerator *e = [selected objectEnumerator]; - OVNode *node; - while (node = [e nextObject]) - [r addObject:p2a([node indexPath])]; - return r; -} - -- (void)performPySelection:(NSArray *)aIndexPaths -{ - if (_powerMode) - [py selectPowerMarkerNodePaths:aIndexPaths]; - else - [py selectResultNodePaths:aIndexPaths]; -} - - (void)initResultColumns { NSTableColumn *refCol = [matches tableColumnWithIdentifier:@"0"]; From 0f4a9e8a1995ae7bac8ee7e6e92e2c3988e4eef3 Mon Sep 17 00:00:00 2001 From: hsoft Date: Sun, 14 Jun 2009 15:13:11 +0000 Subject: [PATCH 045/275] [#29] dgbase cocoa: Moved some few more duplicated code, and replaced the ResultsChangedNotification for a ResultsUpdatedNotification for ref switching. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4045 --- base/cocoa/Consts.h | 3 +++ base/cocoa/ResultWindow.h | 2 ++ base/cocoa/ResultWindow.m | 23 ++++++++++++++++++++++- 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/base/cocoa/Consts.h b/base/cocoa/Consts.h index c0c8a7ee..81e19592 100644 --- a/base/cocoa/Consts.h +++ b/base/cocoa/Consts.h @@ -1,7 +1,10 @@ #import #define DuplicateSelectionChangedNotification @"DuplicateSelectionChangedNotification" +/* ResultsChangedNotification happens on major changes, which requires a complete reload of the data*/ #define ResultsChangedNotification @"ResultsChangedNotification" +/* ResultsChangedNotification happens on minor changes, which requires buffer flush*/ +#define ResultsUpdatedNotification @"ResultsUpdatedNotification" #define ResultsMarkingChangedNotification @"ResultsMarkingChangedNotification" #define RegistrationRequired @"RegistrationRequired" #define JobStarted @"JobStarted" diff --git a/base/cocoa/ResultWindow.h b/base/cocoa/ResultWindow.h index d7582dad..cf5b09da 100644 --- a/base/cocoa/ResultWindow.h +++ b/base/cocoa/ResultWindow.h @@ -18,6 +18,7 @@ IBOutlet NSView *filterFieldView; IBOutlet MatchesView *matches; IBOutlet NSView *pmSwitchView; + IBOutlet NSTextField *stats; BOOL _powerMode; BOOL _displayDelta; @@ -29,6 +30,7 @@ - (NSArray *)getSelected:(BOOL)aDupesOnly; - (NSArray *)getSelectedPaths:(BOOL)aDupesOnly; - (void)performPySelection:(NSArray *)aIndexPaths; +- (void)refreshStats; /* Actions */ - (IBAction)changeDelta:(id)sender; diff --git a/base/cocoa/ResultWindow.m b/base/cocoa/ResultWindow.m index dd254303..13a6f1fc 100644 --- a/base/cocoa/ResultWindow.m +++ b/base/cocoa/ResultWindow.m @@ -53,8 +53,11 @@ { [self window]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(registrationRequired:) name:RegistrationRequired object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(jobCompleted:) name:JobCompletedNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(jobStarted:) name:JobStarted object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(jobInProgress:) name:JobInProgress object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(resultsChanged:) name:ResultsChangedNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(resultsUpdated:) name:ResultsUpdatedNotification object:nil]; } /* Virtual */ @@ -101,6 +104,11 @@ [py selectResultNodePaths:aIndexPaths]; } +- (void)refreshStats +{ + [stats setStringValue:[py getStatLine]]; +} + /* Actions */ - (IBAction)changeDelta:(id)sender { @@ -171,7 +179,7 @@ { [self performPySelection:[self getSelectedPaths:YES]]; [py makeSelectedReference]; - [[NSNotificationCenter defaultCenter] postNotificationName:ResultsChangedNotification object:self]; + [[NSNotificationCenter defaultCenter] postNotificationName:ResultsUpdatedNotification object:self]; } /* Delegate */ @@ -248,6 +256,19 @@ [Dialogs showMessage:msg]; } +- (void)resultsChanged:(NSNotification *)aNotification +{ + [matches reloadData]; + [self expandAll:nil]; + [self outlineViewSelectionDidChange:nil]; + [self refreshStats]; +} + +- (void)resultsUpdated:(NSNotification *)aNotification +{ + [matches invalidateBuffers]; +} + /* Toolbar */ - (NSToolbarItem *)toolbar:(NSToolbar *)toolbar itemForItemIdentifier:(NSString *)itemIdentifier willBeInsertedIntoToolbar:(BOOL)flag { From 5ae07499ab9bda4b0b2341776fdcb695eb64c798 Mon Sep 17 00:00:00 2001 From: hsoft Date: Sun, 14 Jun 2009 15:14:39 +0000 Subject: [PATCH 046/275] [#29 state:port] se cocoa: moved some more duplicated code down to dgbase. #29 now finished. Just need to port it to pyqt and other editions. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4046 --- se/cocoa/ResultWindow.h | 2 -- se/cocoa/ResultWindow.m | 15 --------------- 2 files changed, 17 deletions(-) diff --git a/se/cocoa/ResultWindow.h b/se/cocoa/ResultWindow.h index abb28e4d..c391156d 100644 --- a/se/cocoa/ResultWindow.h +++ b/se/cocoa/ResultWindow.h @@ -11,7 +11,6 @@ IBOutlet NSSearchField *filterField; IBOutlet NSSegmentedControl *pmSwitch; IBOutlet NSWindow *preferencesPanel; - IBOutlet NSTextField *stats; NSString *_lastAction; DetailsPanel *_detailsPanel; @@ -46,6 +45,5 @@ - (NSArray *)getColumnsOrder; - (NSDictionary *)getColumnsWidth; - (void)initResultColumns; -- (void)refreshStats; - (void)restoreColumnsPosition:(NSArray *)aColumnsOrder widths:(NSDictionary *)aColumnsWidth; @end diff --git a/se/cocoa/ResultWindow.m b/se/cocoa/ResultWindow.m index d4a29763..42cae80d 100644 --- a/se/cocoa/ResultWindow.m +++ b/se/cocoa/ResultWindow.m @@ -25,8 +25,6 @@ [self refreshStats]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(resultsMarkingChanged:) name:ResultsMarkingChangedNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(duplicateSelectionChanged:) name:DuplicateSelectionChangedNotification object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(resultsChanged:) name:ResultsChangedNotification object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(jobCompleted:) name:JobCompletedNotification object:nil]; NSToolbar *t = [[[NSToolbar alloc] initWithIdentifier:@"ResultWindowToolbar"] autorelease]; [t setAllowsUserCustomization:YES]; @@ -334,11 +332,6 @@ [_resultColumns addObject:[self getColumnForIdentifier:8 title:@"Dupe Count" width:80 refCol:refCol]]; } --(void)refreshStats -{ - [stats setStringValue:[py getStatLine]]; -} - - (void)restoreColumnsPosition:(NSArray *)aColumnsOrder widths:(NSDictionary *)aColumnsWidth { NSTableColumn *col; @@ -407,14 +400,6 @@ [[NSNotificationCenter defaultCenter] postNotificationName:DuplicateSelectionChangedNotification object:self]; } -- (void)resultsChanged:(NSNotification *)aNotification -{ - [matches reloadData]; - [self expandAll:nil]; - [self outlineViewSelectionDidChange:nil]; - [self refreshStats]; -} - - (void)resultsMarkingChanged:(NSNotification *)aNotification { [matches invalidateMarkings]; From 8ce12d785f427ec95bc24aa5e1c8119cbb20b23a Mon Sep 17 00:00:00 2001 From: hsoft Date: Sun, 14 Jun 2009 15:22:03 +0000 Subject: [PATCH 047/275] me qt: v5.6.2 (forgot to commit) --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4047 --- me/qt/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/me/qt/app.py b/me/qt/app.py index 65314e2b..c20259ab 100644 --- a/me/qt/app.py +++ b/me/qt/app.py @@ -17,7 +17,7 @@ from preferences_dialog import PreferencesDialog class DupeGuru(DupeGuruBase): LOGO_NAME = 'logo_me' NAME = 'dupeGuru Music Edition' - VERSION = '5.6.1' + VERSION = '5.6.2' DELTA_COLUMNS = frozenset([2, 3, 4, 5, 7, 8]) def __init__(self): From f23c6e37bcfa448077bacc72be956aca2be2f8d9 Mon Sep 17 00:00:00 2001 From: hsoft Date: Mon, 15 Jun 2009 07:52:14 +0000 Subject: [PATCH 048/275] [#29] qt base: Made the resultsChanged event remember the selected dupe and re-select it. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4048 --- base/qt/details_table.py | 2 ++ base/qt/main_window.py | 8 +++++++- base/qt/results_model.py | 24 ++++++++++++++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/base/qt/details_table.py b/base/qt/details_table.py index 1c45de1e..0a27c2ae 100644 --- a/base/qt/details_table.py +++ b/base/qt/details_table.py @@ -48,6 +48,8 @@ class DetailsModel(QAbstractTableModel): #--- Events def duplicateSelected(self): dupe = self._app.selected_dupe + if dupe is None: + return group = self._app.results.get_group_of_duplicate(dupe) ref = group.ref self._dupe_data = self._data.GetDisplayInfo(dupe, group) diff --git a/base/qt/main_window.py b/base/qt/main_window.py index 1f93d0f2..5b91a85e 100644 --- a/base/qt/main_window.py +++ b/base/qt/main_window.py @@ -7,7 +7,7 @@ from PyQt4.QtCore import Qt, QCoreApplication, QProcess, SIGNAL from PyQt4.QtGui import (QMainWindow, QMenu, QPixmap, QIcon, QToolButton, QLabel, QHeaderView, - QMessageBox, QInputDialog, QLineEdit) + QMessageBox, QInputDialog, QLineEdit, QItemSelectionModel) from hsutil.misc import nonone @@ -292,6 +292,12 @@ class MainWindow(QMainWindow, Ui_MainWindow): def resultsChanged(self): self.resultsView.model().reset() + dupe = self.app.selected_dupe + if dupe is not None: + [modelIndex] = self.resultsModel.indexesForDupes([dupe]) + if modelIndex.isValid(): + flags = QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Rows + self.resultsView.selectionModel().select(modelIndex, flags) def resultsReset(self): self.resultsView.expandAll() diff --git a/base/qt/results_model.py b/base/qt/results_model.py index d28d6da3..cbffdfb8 100644 --- a/base/qt/results_model.py +++ b/base/qt/results_model.py @@ -102,6 +102,30 @@ class ResultsModel(TreeModel): nodes = [index.internalPointer() for index in indexes] return [node.dupe for node in nodes] + def indexesForDupes(self, dupes): + def index(dupe): + try: + if self.power_marker: + row = self._results.dupes.index(dupe) + node = self.nodes[row] + assert node.dupe is dupe + return self.createIndex(row, 0, node) + else: + group = self._results.get_group_of_duplicate(dupe) + row = self._results.groups.index(group) + node = self.nodes[row] + if dupe is group.ref: + assert node.dupe is dupe + return self.createIndex(row, 0, node) + subrow = group.dupes.index(dupe) + subnode = node.children[subrow] + assert subnode.dupe is dupe + return self.createIndex(subrow, 0, subnode) + except ValueError: # the dupe is not there anymore + return QModelIndex() + + return map(index, dupes) + def flags(self, index): if not index.isValid(): return Qt.ItemIsEnabled From 7e162f59515ace98bbe23c059738cab3bb8c310e Mon Sep 17 00:00:00 2001 From: hsoft Date: Mon, 15 Jun 2009 08:06:11 +0000 Subject: [PATCH 049/275] [#29] me cocoa: removed code that has been pushed down to dgbase. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4049 --- me/cocoa/Info.plist | 2 +- me/cocoa/ResultWindow.h | 6 ----- me/cocoa/ResultWindow.m | 59 ----------------------------------------- 3 files changed, 1 insertion(+), 66 deletions(-) diff --git a/me/cocoa/Info.plist b/me/cocoa/Info.plist index 97d43767..92004dd8 100644 --- a/me/cocoa/Info.plist +++ b/me/cocoa/Info.plist @@ -23,7 +23,7 @@ CFBundleSignature hsft CFBundleVersion - 5.6.1 + 5.6.2 NSMainNibFile MainMenu NSPrincipalClass diff --git a/me/cocoa/ResultWindow.h b/me/cocoa/ResultWindow.h index e421b99d..430cfe36 100644 --- a/me/cocoa/ResultWindow.h +++ b/me/cocoa/ResultWindow.h @@ -11,7 +11,6 @@ IBOutlet NSSearchField *filterField; IBOutlet NSSegmentedControl *pmSwitch; IBOutlet NSWindow *preferencesPanel; - IBOutlet NSTextField *stats; NSString *_lastAction; DetailsPanel *_detailsPanel; @@ -38,7 +37,6 @@ - (IBAction)revealSelected:(id)sender; - (IBAction)showPreferencesPanel:(id)sender; - (IBAction)startDuplicateScan:(id)sender; -- (IBAction)switchSelected:(id)sender; - (IBAction)toggleColumn:(id)sender; - (IBAction)toggleDelta:(id)sender; - (IBAction)toggleDetailsPanel:(id)sender; @@ -47,10 +45,6 @@ - (NSTableColumn *)getColumnForIdentifier:(int)aIdentifier title:(NSString *)aTitle width:(int)aWidth refCol:(NSTableColumn *)aColumn; - (NSArray *)getColumnsOrder; - (NSDictionary *)getColumnsWidth; -- (NSArray *)getSelected:(BOOL)aDupesOnly; -- (NSArray *)getSelectedPaths:(BOOL)aDupesOnly; - (void)initResultColumns; -- (void)performPySelection:(NSArray *)aIndexPaths; -- (void)refreshStats; - (void)restoreColumnsPosition:(NSArray *)aColumnsOrder widths:(NSDictionary *)aColumnsWidth; @end diff --git a/me/cocoa/ResultWindow.m b/me/cocoa/ResultWindow.m index eb4530c2..d1861093 100644 --- a/me/cocoa/ResultWindow.m +++ b/me/cocoa/ResultWindow.m @@ -26,8 +26,6 @@ [self refreshStats]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(resultsMarkingChanged:) name:ResultsMarkingChangedNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(duplicateSelectionChanged:) name:DuplicateSelectionChangedNotification object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(resultsChanged:) name:ResultsChangedNotification object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(jobCompleted:) name:JobCompletedNotification object:nil]; NSToolbar *t = [[[NSToolbar alloc] initWithIdentifier:@"ResultWindowToolbar"] autorelease]; [t setAllowsUserCustomization:YES]; @@ -234,13 +232,6 @@ } } -- (IBAction)switchSelected:(id)sender -{ - [self performPySelection:[self getSelectedPaths:YES]]; - [py makeSelectedReference]; - [[NSNotificationCenter defaultCenter] postNotificationName:ResultsChangedNotification object:self]; -} - - (IBAction)toggleColumn:(id)sender { NSMenuItem *mi = sender; @@ -334,43 +325,6 @@ return result; } -- (NSArray *)getSelected:(BOOL)aDupesOnly -{ - if (_powerMode) - aDupesOnly = NO; - NSIndexSet *indexes = [matches selectedRowIndexes]; - NSMutableArray *nodeList = [NSMutableArray array]; - OVNode *node; - int i = [indexes firstIndex]; - while (i != NSNotFound) - { - node = [matches itemAtRow:i]; - if (!aDupesOnly || ([node level] > 1)) - [nodeList addObject:node]; - i = [indexes indexGreaterThanIndex:i]; - } - return nodeList; -} - -- (NSArray *)getSelectedPaths:(BOOL)aDupesOnly -{ - NSMutableArray *r = [NSMutableArray array]; - NSArray *selected = [self getSelected:aDupesOnly]; - NSEnumerator *e = [selected objectEnumerator]; - OVNode *node; - while (node = [e nextObject]) - [r addObject:p2a([node indexPath])]; - return r; -} - -- (void)performPySelection:(NSArray *)aIndexPaths -{ - if (_powerMode) - [py selectPowerMarkerNodePaths:aIndexPaths]; - else - [py selectResultNodePaths:aIndexPaths]; -} - - (void)initResultColumns { NSTableColumn *refCol = [matches tableColumnWithIdentifier:@"0"]; @@ -396,11 +350,6 @@ [_resultColumns addObject:[self getColumnForIdentifier:18 title:@"Dupe Count" width:80 refCol:refCol]]; } --(void)refreshStats -{ - [stats setStringValue:[py getStatLine]]; -} - - (void)restoreColumnsPosition:(NSArray *)aColumnsOrder widths:(NSDictionary *)aColumnsWidth { NSTableColumn *col; @@ -489,14 +438,6 @@ [[NSNotificationCenter defaultCenter] postNotificationName:DuplicateSelectionChangedNotification object:self]; } -- (void)resultsChanged:(NSNotification *)aNotification -{ - [matches reloadData]; - [self expandAll:nil]; - [self outlineViewSelectionDidChange:nil]; - [self refreshStats]; -} - - (void)resultsMarkingChanged:(NSNotification *)aNotification { [matches invalidateMarkings]; From e5643c7d1a92fe3091c16551f12e850db3fd0028 Mon Sep 17 00:00:00 2001 From: hsoft Date: Mon, 15 Jun 2009 08:17:02 +0000 Subject: [PATCH 050/275] pe help: css update --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4050 --- pe/help/skeleton/hardcoded.css | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pe/help/skeleton/hardcoded.css b/pe/help/skeleton/hardcoded.css index a3b17c5b..2c0fa5fc 100644 --- a/pe/help/skeleton/hardcoded.css +++ b/pe/help/skeleton/hardcoded.css @@ -9,9 +9,9 @@ BODY BODY,A,P,UL,TABLE,TR,TD { - font-family:Tahoma,Arial,sans-serif; + font-family: "Helvetica Neue", "Lucida Grande", Arial, sans-serif; font-size:10pt; - color: #4477AA;/*darker than 5588bb for the sake of the eyes*/ + color: #336699;/*darker than 5588bb for the sake of the eyes*/ } /***************************************************** @@ -82,7 +82,6 @@ TD.menuframe A.menuitem,A.menuitem_selected { font-size:14pt; - font-family:Tahoma,Arial,sans-serif; font-weight:normal; padding-left:10pt; color:#5588bb; @@ -98,7 +97,6 @@ A.menuitem_selected A.submenuitem { - font-family:Tahoma,Arial,sans-serif; font-weight:normal; color:#5588bb; text-decoration:none; From 7e90ec31b3a473a8c5932a7ad8721fc10b7526c1 Mon Sep 17 00:00:00 2001 From: hsoft Date: Mon, 15 Jun 2009 08:17:39 +0000 Subject: [PATCH 051/275] [#29 state:fixed] pe cocoa: removed code that has been pushed down to dgbase. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4051 --- pe/cocoa/ResultWindow.h | 6 ----- pe/cocoa/ResultWindow.m | 59 ----------------------------------------- 2 files changed, 65 deletions(-) diff --git a/pe/cocoa/ResultWindow.h b/pe/cocoa/ResultWindow.h index 5816d50a..8aa0f0bb 100644 --- a/pe/cocoa/ResultWindow.h +++ b/pe/cocoa/ResultWindow.h @@ -10,7 +10,6 @@ IBOutlet NSSearchField *filterField; IBOutlet NSSegmentedControl *pmSwitch; IBOutlet NSWindow *preferencesPanel; - IBOutlet NSTextField *stats; NSMutableArray *_resultColumns; NSMutableIndexSet *_deltaColumns; @@ -35,7 +34,6 @@ - (IBAction)revealSelected:(id)sender; - (IBAction)showPreferencesPanel:(id)sender; - (IBAction)startDuplicateScan:(id)sender; -- (IBAction)switchSelected:(id)sender; - (IBAction)toggleColumn:(id)sender; - (IBAction)toggleDelta:(id)sender; - (IBAction)toggleDetailsPanel:(id)sender; @@ -45,10 +43,6 @@ - (NSTableColumn *)getColumnForIdentifier:(int)aIdentifier title:(NSString *)aTitle width:(int)aWidth refCol:(NSTableColumn *)aColumn; - (NSArray *)getColumnsOrder; - (NSDictionary *)getColumnsWidth; -- (NSArray *)getSelected:(BOOL)aDupesOnly; -- (NSArray *)getSelectedPaths:(BOOL)aDupesOnly; -- (void)performPySelection:(NSArray *)aIndexPaths; - (void)initResultColumns; -- (void)refreshStats; - (void)restoreColumnsPosition:(NSArray *)aColumnsOrder widths:(NSDictionary *)aColumnsWidth; @end diff --git a/pe/cocoa/ResultWindow.m b/pe/cocoa/ResultWindow.m index d2e415bf..cfd93106 100644 --- a/pe/cocoa/ResultWindow.m +++ b/pe/cocoa/ResultWindow.m @@ -25,8 +25,6 @@ [self initResultColumns]; [self refreshStats]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(resultsMarkingChanged:) name:ResultsMarkingChangedNotification object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(resultsChanged:) name:ResultsChangedNotification object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(jobCompleted:) name:JobCompletedNotification object:nil]; NSToolbar *t = [[[NSToolbar alloc] initWithIdentifier:@"ResultWindowToolbar"] autorelease]; [t setAllowsUserCustomization:YES]; @@ -229,13 +227,6 @@ } } -- (IBAction)switchSelected:(id)sender -{ - [self performPySelection:[self getSelectedPaths:YES]]; - [py makeSelectedReference]; - [[NSNotificationCenter defaultCenter] postNotificationName:ResultsChangedNotification object:self]; -} - - (IBAction)toggleColumn:(id)sender { NSMenuItem *mi = sender; @@ -330,43 +321,6 @@ return result; } -- (NSArray *)getSelected:(BOOL)aDupesOnly -{ - if (_powerMode) - aDupesOnly = NO; - NSIndexSet *indexes = [matches selectedRowIndexes]; - NSMutableArray *nodeList = [NSMutableArray array]; - OVNode *node; - int i = [indexes firstIndex]; - while (i != NSNotFound) - { - node = [matches itemAtRow:i]; - if (!aDupesOnly || ([node level] > 1)) - [nodeList addObject:node]; - i = [indexes indexGreaterThanIndex:i]; - } - return nodeList; -} - -- (NSArray *)getSelectedPaths:(BOOL)aDupesOnly -{ - NSMutableArray *r = [NSMutableArray array]; - NSArray *selected = [self getSelected:aDupesOnly]; - NSEnumerator *e = [selected objectEnumerator]; - OVNode *node; - while (node = [e nextObject]) - [r addObject:p2a([node indexPath])]; - return r; -} - -- (void)performPySelection:(NSArray *)aIndexPaths -{ - if (_powerMode) - [py selectPowerMarkerNodePaths:aIndexPaths]; - else - [py selectResultNodePaths:aIndexPaths]; -} - - (void)initResultColumns { NSTableColumn *refCol = [matches tableColumnWithIdentifier:@"0"]; @@ -382,11 +336,6 @@ [_resultColumns addObject:[self getColumnForIdentifier:8 title:@"Dupe Count" width:80 refCol:refCol]]; } --(void)refreshStats -{ - [stats setStringValue:[py getStatLine]]; -} - - (void)restoreColumnsPosition:(NSArray *)aColumnsOrder widths:(NSDictionary *)aColumnsWidth { NSTableColumn *col; @@ -448,14 +397,6 @@ [[NSNotificationCenter defaultCenter] postNotificationName:DuplicateSelectionChangedNotification object:self]; } -- (void)resultsChanged:(NSNotification *)aNotification -{ - [matches reloadData]; - [self expandAll:nil]; - [self outlineViewSelectionDidChange:nil]; - [self refreshStats]; -} - - (void)resultsMarkingChanged:(NSNotification *)aNotification { [matches invalidateMarkings]; From 69bc962f75bc459aa366cd6d2730bd7066dc0759 Mon Sep 17 00:00:00 2001 From: hsoft Date: Mon, 15 Jun 2009 12:31:37 +0000 Subject: [PATCH 052/275] base cocoa: removed framework-related files (not needed anymore) --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4052 --- base/cocoa/English.lproj/InfoPlist.strings | Bin 204 -> 0 bytes base/cocoa/Info.plist | 26 -- base/cocoa/dgbase.xcodeproj/project.pbxproj | 408 -------------------- 3 files changed, 434 deletions(-) delete mode 100644 base/cocoa/English.lproj/InfoPlist.strings delete mode 100644 base/cocoa/Info.plist delete mode 100644 base/cocoa/dgbase.xcodeproj/project.pbxproj diff --git a/base/cocoa/English.lproj/InfoPlist.strings b/base/cocoa/English.lproj/InfoPlist.strings deleted file mode 100644 index 948e12b991d433e9dc3ebc46350cb859c5bc1e07..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 204 zcmW-a%?`m}5Jk`0Q#6*1sMuJDl?@3NJb)A}LVwaCsW - - - - CFBundleDevelopmentRegion - English - CFBundleExecutable - ${EXECUTABLE_NAME} - CFBundleName - ${PRODUCT_NAME} - CFBundleIconFile - - CFBundleIdentifier - com.yourcompany.yourcocoaframework - CFBundleInfoDictionaryVersion - 6.0 - CFBundlePackageType - FMWK - CFBundleSignature - ???? - CFBundleVersion - 1.0 - NSPrincipalClass - - - diff --git a/base/cocoa/dgbase.xcodeproj/project.pbxproj b/base/cocoa/dgbase.xcodeproj/project.pbxproj deleted file mode 100644 index 54889c77..00000000 --- a/base/cocoa/dgbase.xcodeproj/project.pbxproj +++ /dev/null @@ -1,408 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 44; - objects = { - -/* Begin PBXBuildFile section */ - 8DC2EF530486A6940098B216 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 089C1666FE841158C02AAC07 /* InfoPlist.strings */; }; - 8DC2EF570486A6940098B216 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1058C7B1FEA5585E11CA2CBB /* Cocoa.framework */; }; - CE62CA730CFAF80D0001B6E0 /* ResultWindow.h in Headers */ = {isa = PBXBuildFile; fileRef = CE62CA710CFAF80D0001B6E0 /* ResultWindow.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CE62CA740CFAF80D0001B6E0 /* ResultWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = CE62CA720CFAF80D0001B6E0 /* ResultWindow.m */; }; - CE70A0FC0CF8C2560048C314 /* PyDupeGuru.h in Headers */ = {isa = PBXBuildFile; fileRef = CE70A0FB0CF8C2560048C314 /* PyDupeGuru.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CE7D77050CF8987800BA287E /* Consts.h in Headers */ = {isa = PBXBuildFile; fileRef = CE7D77030CF8987800BA287E /* Consts.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CE80DA190FC191320086DCA6 /* RecentDirectories.h in Headers */ = {isa = PBXBuildFile; fileRef = CE80DA170FC191320086DCA6 /* RecentDirectories.h */; settings = {ATTRIBUTES = (Private, ); }; }; - CE80DA1A0FC191320086DCA6 /* RecentDirectories.m in Sources */ = {isa = PBXBuildFile; fileRef = CE80DA180FC191320086DCA6 /* RecentDirectories.m */; }; - CE80DA2E0FC191980086DCA6 /* Dialogs.h in Headers */ = {isa = PBXBuildFile; fileRef = CE80DA250FC191980086DCA6 /* Dialogs.h */; settings = {ATTRIBUTES = (Private, ); }; }; - CE80DA2F0FC191980086DCA6 /* Dialogs.m in Sources */ = {isa = PBXBuildFile; fileRef = CE80DA260FC191980086DCA6 /* Dialogs.m */; }; - CE80DA300FC191980086DCA6 /* ProgressController.h in Headers */ = {isa = PBXBuildFile; fileRef = CE80DA270FC191980086DCA6 /* ProgressController.h */; settings = {ATTRIBUTES = (Private, ); }; }; - CE80DA310FC191980086DCA6 /* ProgressController.m in Sources */ = {isa = PBXBuildFile; fileRef = CE80DA280FC191980086DCA6 /* ProgressController.m */; }; - CE80DA320FC191980086DCA6 /* PyApp.h in Headers */ = {isa = PBXBuildFile; fileRef = CE80DA290FC191980086DCA6 /* PyApp.h */; settings = {ATTRIBUTES = (Private, ); }; }; - CE80DA330FC191980086DCA6 /* RegistrationInterface.h in Headers */ = {isa = PBXBuildFile; fileRef = CE80DA2A0FC191980086DCA6 /* RegistrationInterface.h */; settings = {ATTRIBUTES = (Private, ); }; }; - CE80DA340FC191980086DCA6 /* RegistrationInterface.m in Sources */ = {isa = PBXBuildFile; fileRef = CE80DA2B0FC191980086DCA6 /* RegistrationInterface.m */; }; - CE80DA350FC191980086DCA6 /* Utils.h in Headers */ = {isa = PBXBuildFile; fileRef = CE80DA2C0FC191980086DCA6 /* Utils.h */; settings = {ATTRIBUTES = (Private, ); }; }; - CE80DA360FC191980086DCA6 /* Utils.m in Sources */ = {isa = PBXBuildFile; fileRef = CE80DA2D0FC191980086DCA6 /* Utils.m */; }; - CE80DA3B0FC191C40086DCA6 /* Outline.h in Headers */ = {isa = PBXBuildFile; fileRef = CE80DA370FC191C40086DCA6 /* Outline.h */; settings = {ATTRIBUTES = (Private, ); }; }; - CE80DA3C0FC191C40086DCA6 /* Outline.m in Sources */ = {isa = PBXBuildFile; fileRef = CE80DA380FC191C40086DCA6 /* Outline.m */; }; - CE80DA3D0FC191C40086DCA6 /* Table.h in Headers */ = {isa = PBXBuildFile; fileRef = CE80DA390FC191C40086DCA6 /* Table.h */; settings = {ATTRIBUTES = (Private, ); }; }; - CE80DA3E0FC191C40086DCA6 /* Table.m in Sources */ = {isa = PBXBuildFile; fileRef = CE80DA3A0FC191C40086DCA6 /* Table.m */; }; - CE80DB6D0FC194560086DCA6 /* progress.nib in Resources */ = {isa = PBXBuildFile; fileRef = CE80DB690FC194560086DCA6 /* progress.nib */; }; - CE80DB6E0FC194560086DCA6 /* registration.nib in Resources */ = {isa = PBXBuildFile; fileRef = CE80DB6B0FC194560086DCA6 /* registration.nib */; }; - CE895B5F0CFAE78300B5ADEA /* AppDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = CE895B5D0CFAE78300B5ADEA /* AppDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CE895B600CFAE78300B5ADEA /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = CE895B5E0CFAE78300B5ADEA /* AppDelegate.m */; }; - CE895C150CFAF0C900B5ADEA /* DirectoryPanel.h in Headers */ = {isa = PBXBuildFile; fileRef = CE895C130CFAF0C900B5ADEA /* DirectoryPanel.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CE895C160CFAF0C900B5ADEA /* DirectoryPanel.m in Sources */ = {isa = PBXBuildFile; fileRef = CE895C140CFAF0C900B5ADEA /* DirectoryPanel.m */; }; -/* End PBXBuildFile section */ - -/* Begin PBXFileReference section */ - 0867D69BFE84028FC02AAC07 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = /System/Library/Frameworks/Foundation.framework; sourceTree = ""; }; - 0867D6A5FE840307C02AAC07 /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = /System/Library/Frameworks/AppKit.framework; sourceTree = ""; }; - 089C1667FE841158C02AAC07 /* English */ = {isa = PBXFileReference; fileEncoding = 10; lastKnownFileType = text.plist.strings; name = English; path = English.lproj/InfoPlist.strings; sourceTree = ""; }; - 1058C7B1FEA5585E11CA2CBB /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = /System/Library/Frameworks/Cocoa.framework; sourceTree = ""; }; - 8DC2EF5A0486A6940098B216 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 8DC2EF5B0486A6940098B216 /* dgbase.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = dgbase.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - CE62CA710CFAF80D0001B6E0 /* ResultWindow.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ResultWindow.h; sourceTree = ""; }; - CE62CA720CFAF80D0001B6E0 /* ResultWindow.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ResultWindow.m; sourceTree = ""; }; - CE70A0FB0CF8C2560048C314 /* PyDupeGuru.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PyDupeGuru.h; sourceTree = ""; }; - CE7D77030CF8987800BA287E /* Consts.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Consts.h; sourceTree = ""; }; - CE80DA170FC191320086DCA6 /* RecentDirectories.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RecentDirectories.h; path = ../../../cocoalib/RecentDirectories.h; sourceTree = SOURCE_ROOT; }; - CE80DA180FC191320086DCA6 /* RecentDirectories.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RecentDirectories.m; path = ../../../cocoalib/RecentDirectories.m; sourceTree = SOURCE_ROOT; }; - CE80DA250FC191980086DCA6 /* Dialogs.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Dialogs.h; path = ../../../cocoalib/Dialogs.h; sourceTree = SOURCE_ROOT; }; - CE80DA260FC191980086DCA6 /* Dialogs.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Dialogs.m; path = ../../../cocoalib/Dialogs.m; sourceTree = SOURCE_ROOT; }; - CE80DA270FC191980086DCA6 /* ProgressController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ProgressController.h; path = ../../../cocoalib/ProgressController.h; sourceTree = SOURCE_ROOT; }; - CE80DA280FC191980086DCA6 /* ProgressController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ProgressController.m; path = ../../../cocoalib/ProgressController.m; sourceTree = SOURCE_ROOT; }; - CE80DA290FC191980086DCA6 /* PyApp.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PyApp.h; path = ../../../cocoalib/PyApp.h; sourceTree = SOURCE_ROOT; }; - CE80DA2A0FC191980086DCA6 /* RegistrationInterface.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RegistrationInterface.h; path = ../../../cocoalib/RegistrationInterface.h; sourceTree = SOURCE_ROOT; }; - CE80DA2B0FC191980086DCA6 /* RegistrationInterface.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RegistrationInterface.m; path = ../../../cocoalib/RegistrationInterface.m; sourceTree = SOURCE_ROOT; }; - CE80DA2C0FC191980086DCA6 /* Utils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Utils.h; path = ../../../cocoalib/Utils.h; sourceTree = SOURCE_ROOT; }; - CE80DA2D0FC191980086DCA6 /* Utils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Utils.m; path = ../../../cocoalib/Utils.m; sourceTree = SOURCE_ROOT; }; - CE80DA370FC191C40086DCA6 /* Outline.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Outline.h; path = ../../../cocoalib/Outline.h; sourceTree = SOURCE_ROOT; }; - CE80DA380FC191C40086DCA6 /* Outline.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Outline.m; path = ../../../cocoalib/Outline.m; sourceTree = SOURCE_ROOT; }; - CE80DA390FC191C40086DCA6 /* Table.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Table.h; path = ../../../cocoalib/Table.h; sourceTree = SOURCE_ROOT; }; - CE80DA3A0FC191C40086DCA6 /* Table.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Table.m; path = ../../../cocoalib/Table.m; sourceTree = SOURCE_ROOT; }; - CE80DB6A0FC194560086DCA6 /* English */ = {isa = PBXFileReference; lastKnownFileType = wrapper.nib; name = English; path = ../../../cocoalib/English.lproj/progress.nib; sourceTree = SOURCE_ROOT; }; - CE80DB6C0FC194560086DCA6 /* English */ = {isa = PBXFileReference; lastKnownFileType = wrapper.nib; name = English; path = ../../../cocoalib/English.lproj/registration.nib; sourceTree = SOURCE_ROOT; }; - CE895B5D0CFAE78300B5ADEA /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; - CE895B5E0CFAE78300B5ADEA /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; - CE895C130CFAF0C900B5ADEA /* DirectoryPanel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DirectoryPanel.h; sourceTree = ""; }; - CE895C140CFAF0C900B5ADEA /* DirectoryPanel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DirectoryPanel.m; sourceTree = ""; }; - D2F7E79907B2D74100F64583 /* CoreData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreData.framework; path = /System/Library/Frameworks/CoreData.framework; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 8DC2EF560486A6940098B216 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 8DC2EF570486A6940098B216 /* Cocoa.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 034768DFFF38A50411DB9C8B /* Products */ = { - isa = PBXGroup; - children = ( - 8DC2EF5B0486A6940098B216 /* dgbase.framework */, - ); - name = Products; - sourceTree = ""; - }; - 0867D691FE84028FC02AAC07 /* dgbase */ = { - isa = PBXGroup; - children = ( - 08FB77AEFE84172EC02AAC07 /* Classes */, - CE80DA160FC1910F0086DCA6 /* cocoalib */, - 32C88DFF0371C24200C91783 /* Other Sources */, - 089C1665FE841158C02AAC07 /* Resources */, - 0867D69AFE84028FC02AAC07 /* External Frameworks and Libraries */, - 034768DFFF38A50411DB9C8B /* Products */, - ); - name = dgbase; - sourceTree = ""; - }; - 0867D69AFE84028FC02AAC07 /* External Frameworks and Libraries */ = { - isa = PBXGroup; - children = ( - 1058C7B0FEA5585E11CA2CBB /* Linked Frameworks */, - 1058C7B2FEA5585E11CA2CBB /* Other Frameworks */, - ); - name = "External Frameworks and Libraries"; - sourceTree = ""; - }; - 089C1665FE841158C02AAC07 /* Resources */ = { - isa = PBXGroup; - children = ( - CE80DB690FC194560086DCA6 /* progress.nib */, - CE80DB6B0FC194560086DCA6 /* registration.nib */, - 8DC2EF5A0486A6940098B216 /* Info.plist */, - 089C1666FE841158C02AAC07 /* InfoPlist.strings */, - ); - name = Resources; - sourceTree = ""; - }; - 08FB77AEFE84172EC02AAC07 /* Classes */ = { - isa = PBXGroup; - children = ( - CE62CA710CFAF80D0001B6E0 /* ResultWindow.h */, - CE62CA720CFAF80D0001B6E0 /* ResultWindow.m */, - CE895C130CFAF0C900B5ADEA /* DirectoryPanel.h */, - CE895C140CFAF0C900B5ADEA /* DirectoryPanel.m */, - CE895B5D0CFAE78300B5ADEA /* AppDelegate.h */, - CE895B5E0CFAE78300B5ADEA /* AppDelegate.m */, - ); - name = Classes; - sourceTree = ""; - }; - 1058C7B0FEA5585E11CA2CBB /* Linked Frameworks */ = { - isa = PBXGroup; - children = ( - 1058C7B1FEA5585E11CA2CBB /* Cocoa.framework */, - ); - name = "Linked Frameworks"; - sourceTree = ""; - }; - 1058C7B2FEA5585E11CA2CBB /* Other Frameworks */ = { - isa = PBXGroup; - children = ( - 0867D6A5FE840307C02AAC07 /* AppKit.framework */, - D2F7E79907B2D74100F64583 /* CoreData.framework */, - 0867D69BFE84028FC02AAC07 /* Foundation.framework */, - ); - name = "Other Frameworks"; - sourceTree = ""; - }; - 32C88DFF0371C24200C91783 /* Other Sources */ = { - isa = PBXGroup; - children = ( - CE70A0FB0CF8C2560048C314 /* PyDupeGuru.h */, - CE7D77030CF8987800BA287E /* Consts.h */, - ); - name = "Other Sources"; - sourceTree = ""; - }; - CE80DA160FC1910F0086DCA6 /* cocoalib */ = { - isa = PBXGroup; - children = ( - CE80DA370FC191C40086DCA6 /* Outline.h */, - CE80DA380FC191C40086DCA6 /* Outline.m */, - CE80DA390FC191C40086DCA6 /* Table.h */, - CE80DA3A0FC191C40086DCA6 /* Table.m */, - CE80DA250FC191980086DCA6 /* Dialogs.h */, - CE80DA260FC191980086DCA6 /* Dialogs.m */, - CE80DA270FC191980086DCA6 /* ProgressController.h */, - CE80DA280FC191980086DCA6 /* ProgressController.m */, - CE80DA290FC191980086DCA6 /* PyApp.h */, - CE80DA2A0FC191980086DCA6 /* RegistrationInterface.h */, - CE80DA2B0FC191980086DCA6 /* RegistrationInterface.m */, - CE80DA2C0FC191980086DCA6 /* Utils.h */, - CE80DA2D0FC191980086DCA6 /* Utils.m */, - CE80DA170FC191320086DCA6 /* RecentDirectories.h */, - CE80DA180FC191320086DCA6 /* RecentDirectories.m */, - ); - name = cocoalib; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXHeadersBuildPhase section */ - 8DC2EF500486A6940098B216 /* Headers */ = { - isa = PBXHeadersBuildPhase; - buildActionMask = 2147483647; - files = ( - CE7D77050CF8987800BA287E /* Consts.h in Headers */, - CE70A0FC0CF8C2560048C314 /* PyDupeGuru.h in Headers */, - CE895B5F0CFAE78300B5ADEA /* AppDelegate.h in Headers */, - CE895C150CFAF0C900B5ADEA /* DirectoryPanel.h in Headers */, - CE62CA730CFAF80D0001B6E0 /* ResultWindow.h in Headers */, - CE80DA190FC191320086DCA6 /* RecentDirectories.h in Headers */, - CE80DA2E0FC191980086DCA6 /* Dialogs.h in Headers */, - CE80DA300FC191980086DCA6 /* ProgressController.h in Headers */, - CE80DA320FC191980086DCA6 /* PyApp.h in Headers */, - CE80DA330FC191980086DCA6 /* RegistrationInterface.h in Headers */, - CE80DA350FC191980086DCA6 /* Utils.h in Headers */, - CE80DA3B0FC191C40086DCA6 /* Outline.h in Headers */, - CE80DA3D0FC191C40086DCA6 /* Table.h in Headers */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXHeadersBuildPhase section */ - -/* Begin PBXNativeTarget section */ - 8DC2EF4F0486A6940098B216 /* dgbase */ = { - isa = PBXNativeTarget; - buildConfigurationList = 1DEB91AD08733DA50010E9CD /* Build configuration list for PBXNativeTarget "dgbase" */; - buildPhases = ( - 8DC2EF500486A6940098B216 /* Headers */, - 8DC2EF520486A6940098B216 /* Resources */, - 8DC2EF540486A6940098B216 /* Sources */, - 8DC2EF560486A6940098B216 /* Frameworks */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = dgbase; - productInstallPath = "$(HOME)/Library/Frameworks"; - productName = dgbase; - productReference = 8DC2EF5B0486A6940098B216 /* dgbase.framework */; - productType = "com.apple.product-type.framework"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 0867D690FE84028FC02AAC07 /* Project object */ = { - isa = PBXProject; - buildConfigurationList = 1DEB91B108733DA50010E9CD /* Build configuration list for PBXProject "dgbase" */; - compatibilityVersion = "Xcode 3.0"; - hasScannedForEncodings = 1; - mainGroup = 0867D691FE84028FC02AAC07 /* dgbase */; - productRefGroup = 034768DFFF38A50411DB9C8B /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 8DC2EF4F0486A6940098B216 /* dgbase */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 8DC2EF520486A6940098B216 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 8DC2EF530486A6940098B216 /* InfoPlist.strings in Resources */, - CE80DB6D0FC194560086DCA6 /* progress.nib in Resources */, - CE80DB6E0FC194560086DCA6 /* registration.nib in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 8DC2EF540486A6940098B216 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - CE895B600CFAE78300B5ADEA /* AppDelegate.m in Sources */, - CE895C160CFAF0C900B5ADEA /* DirectoryPanel.m in Sources */, - CE62CA740CFAF80D0001B6E0 /* ResultWindow.m in Sources */, - CE80DA1A0FC191320086DCA6 /* RecentDirectories.m in Sources */, - CE80DA2F0FC191980086DCA6 /* Dialogs.m in Sources */, - CE80DA310FC191980086DCA6 /* ProgressController.m in Sources */, - CE80DA340FC191980086DCA6 /* RegistrationInterface.m in Sources */, - CE80DA360FC191980086DCA6 /* Utils.m in Sources */, - CE80DA3C0FC191C40086DCA6 /* Outline.m in Sources */, - CE80DA3E0FC191C40086DCA6 /* Table.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXVariantGroup section */ - 089C1666FE841158C02AAC07 /* InfoPlist.strings */ = { - isa = PBXVariantGroup; - children = ( - 089C1667FE841158C02AAC07 /* English */, - ); - name = InfoPlist.strings; - sourceTree = ""; - }; - CE80DB690FC194560086DCA6 /* progress.nib */ = { - isa = PBXVariantGroup; - children = ( - CE80DB6A0FC194560086DCA6 /* English */, - ); - name = progress.nib; - sourceTree = SOURCE_ROOT; - }; - CE80DB6B0FC194560086DCA6 /* registration.nib */ = { - isa = PBXVariantGroup; - children = ( - CE80DB6C0FC194560086DCA6 /* English */, - ); - name = registration.nib; - sourceTree = SOURCE_ROOT; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 1DEB91AE08733DA50010E9CD /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - COPY_PHASE_STRIP = NO; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "\"$(SRCROOT)/../../../cocoalib/build/Release\"", - ); - FRAMEWORK_VERSION = A; - GCC_DYNAMIC_NO_PIC = NO; - GCC_ENABLE_FIX_AND_CONTINUE = YES; - GCC_MODEL_TUNING = G5; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PRECOMPILE_PREFIX_HEADER = YES; - INFOPLIST_FILE = Info.plist; - INSTALL_PATH = "$(HOME)/Library/Frameworks"; - PRODUCT_NAME = dgbase; - WRAPPER_EXTENSION = framework; - ZERO_LINK = YES; - }; - name = Debug; - }; - 1DEB91AF08733DA50010E9CD /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "\"$(SRCROOT)/../../../cocoalib/build/Release\"", - ); - FRAMEWORK_VERSION = A; - GCC_MODEL_TUNING = G5; - GCC_PRECOMPILE_PREFIX_HEADER = NO; - GCC_PREFIX_HEADER = ""; - INFOPLIST_FILE = Info.plist; - INSTALL_PATH = "@executable_path/../Frameworks"; - PRODUCT_NAME = dgbase; - WRAPPER_EXTENSION = framework; - }; - name = Release; - }; - 1DEB91B208733DA50010E9CD /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - GCC_C_LANGUAGE_STANDARD = c99; - GCC_WARN_ABOUT_RETURN_TYPE = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - PREBINDING = NO; - SDKROOT = "$(DEVELOPER_SDK_DIR)/MacOSX10.4u.sdk"; - }; - name = Debug; - }; - 1DEB91B308733DA50010E9CD /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ARCHS = ( - ppc, - i386, - ); - GCC_C_LANGUAGE_STANDARD = c99; - GCC_WARN_ABOUT_RETURN_TYPE = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - PREBINDING = NO; - SDKROOT = "$(DEVELOPER_SDK_DIR)/MacOSX10.4u.sdk"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 1DEB91AD08733DA50010E9CD /* Build configuration list for PBXNativeTarget "dgbase" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 1DEB91AE08733DA50010E9CD /* Debug */, - 1DEB91AF08733DA50010E9CD /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 1DEB91B108733DA50010E9CD /* Build configuration list for PBXProject "dgbase" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 1DEB91B208733DA50010E9CD /* Debug */, - 1DEB91B308733DA50010E9CD /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 0867D690FE84028FC02AAC07 /* Project object */; -} From 308ab0cd529e684449c0c602fae143c70c8788ae Mon Sep 17 00:00:00 2001 From: hsoft Date: Mon, 15 Jun 2009 12:33:58 +0000 Subject: [PATCH 053/275] base cocoa: pushed DetailsPanel common code down. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4053 --- base/cocoa/DetailsPanel.h | 13 +++++++++++++ base/cocoa/DetailsPanel.m | 16 ++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 base/cocoa/DetailsPanel.h create mode 100644 base/cocoa/DetailsPanel.m diff --git a/base/cocoa/DetailsPanel.h b/base/cocoa/DetailsPanel.h new file mode 100644 index 00000000..8bbd0f52 --- /dev/null +++ b/base/cocoa/DetailsPanel.h @@ -0,0 +1,13 @@ +#import +#import "PyApp.h" +#import "Table.h" + + +@interface DetailsPanelBase : NSWindowController +{ + IBOutlet TableView *detailsTable; +} +- (id)initWithPy:(PyApp *)aPy; + +- (void)refresh; +@end \ No newline at end of file diff --git a/base/cocoa/DetailsPanel.m b/base/cocoa/DetailsPanel.m new file mode 100644 index 00000000..7cca6ede --- /dev/null +++ b/base/cocoa/DetailsPanel.m @@ -0,0 +1,16 @@ +#import "DetailsPanel.h" + +@implementation DetailsPanelBase +- (id)initWithPy:(PyApp *)aPy +{ + self = [super initWithWindowNibName:@"Details"]; + [self window]; //So the detailsTable is initialized. + [detailsTable setPy:aPy]; + return self; +} + +- (void)refresh +{ + [detailsTable reloadData]; +} +@end From 1bffe0361621535c37c1f6fe24535947b664156d Mon Sep 17 00:00:00 2001 From: hsoft Date: Mon, 15 Jun 2009 12:34:32 +0000 Subject: [PATCH 054/275] se cocoa: pushed DetailsPanel common code down to dgbase. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4054 --- se/cocoa/DetailsPanel.h | 12 ++---------- se/cocoa/DetailsPanel.m | 12 ------------ se/cocoa/dupeguru.xcodeproj/project.pbxproj | 6 ++++++ 3 files changed, 8 insertions(+), 22 deletions(-) diff --git a/se/cocoa/DetailsPanel.h b/se/cocoa/DetailsPanel.h index 0d4c025d..c3191a00 100644 --- a/se/cocoa/DetailsPanel.h +++ b/se/cocoa/DetailsPanel.h @@ -1,13 +1,5 @@ #import -#import "cocoalib/PyApp.h" -#import "cocoalib/Table.h" +#import "dgbase/DetailsPanel.h" - -@interface DetailsPanel : NSWindowController -{ - IBOutlet TableView *detailsTable; -} -- (id)initWithPy:(PyApp *)aPy; - -- (void)refresh; +@interface DetailsPanel : DetailsPanelBase @end \ No newline at end of file diff --git a/se/cocoa/DetailsPanel.m b/se/cocoa/DetailsPanel.m index 1baac387..e676aef9 100644 --- a/se/cocoa/DetailsPanel.m +++ b/se/cocoa/DetailsPanel.m @@ -1,16 +1,4 @@ #import "DetailsPanel.h" @implementation DetailsPanel -- (id)initWithPy:(PyApp *)aPy -{ - self = [super initWithWindowNibName:@"Details"]; - [self window]; //So the detailsTable is initialized. - [detailsTable setPy:aPy]; - return self; -} - -- (void)refresh -{ - [detailsTable reloadData]; -} @end diff --git a/se/cocoa/dupeguru.xcodeproj/project.pbxproj b/se/cocoa/dupeguru.xcodeproj/project.pbxproj index 317a5089..334ad249 100644 --- a/se/cocoa/dupeguru.xcodeproj/project.pbxproj +++ b/se/cocoa/dupeguru.xcodeproj/project.pbxproj @@ -27,6 +27,7 @@ CECA899D09DB132E00A3D774 /* DetailsPanel.m in Sources */ = {isa = PBXBuildFile; fileRef = CECA899B09DB132E00A3D774 /* DetailsPanel.m */; }; CEDD92DA0FDD01640031C7B7 /* BRSingleLineFormatter.m in Sources */ = {isa = PBXBuildFile; fileRef = CEDD92D70FDD01640031C7B7 /* BRSingleLineFormatter.m */; }; CEDD92DB0FDD01640031C7B7 /* NSCharacterSet_Extensions.m in Sources */ = {isa = PBXBuildFile; fileRef = CEDD92D90FDD01640031C7B7 /* NSCharacterSet_Extensions.m */; }; + CEE7EA130FE675C80004E467 /* DetailsPanel.m in Sources */ = {isa = PBXBuildFile; fileRef = CEE7EA120FE675C80004E467 /* DetailsPanel.m */; }; CEEB135209C837A2004D2330 /* dupeguru.icns in Resources */ = {isa = PBXBuildFile; fileRef = CEEB135109C837A2004D2330 /* dupeguru.icns */; }; CEF7823809C8AA0200EF38FF /* gear.png in Resources */ = {isa = PBXBuildFile; fileRef = CEF7823709C8AA0200EF38FF /* gear.png */; }; CEFC294609C89E3D00D9F998 /* folder32.png in Resources */ = {isa = PBXBuildFile; fileRef = CEFC294509C89E3D00D9F998 /* folder32.png */; }; @@ -95,6 +96,8 @@ CEDD92D70FDD01640031C7B7 /* BRSingleLineFormatter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BRSingleLineFormatter.m; path = cocoalib/brsinglelineformatter/BRSingleLineFormatter.m; sourceTree = SOURCE_ROOT; }; CEDD92D80FDD01640031C7B7 /* NSCharacterSet_Extensions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = NSCharacterSet_Extensions.h; path = cocoalib/brsinglelineformatter/NSCharacterSet_Extensions.h; sourceTree = SOURCE_ROOT; }; CEDD92D90FDD01640031C7B7 /* NSCharacterSet_Extensions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = NSCharacterSet_Extensions.m; path = cocoalib/brsinglelineformatter/NSCharacterSet_Extensions.m; sourceTree = SOURCE_ROOT; }; + CEE7EA110FE675C80004E467 /* DetailsPanel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = DetailsPanel.h; path = dgbase/DetailsPanel.h; sourceTree = SOURCE_ROOT; }; + CEE7EA120FE675C80004E467 /* DetailsPanel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = DetailsPanel.m; path = dgbase/DetailsPanel.m; sourceTree = SOURCE_ROOT; }; CEEB135109C837A2004D2330 /* dupeguru.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; path = dupeguru.icns; sourceTree = ""; }; CEF7823709C8AA0200EF38FF /* gear.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = gear.png; path = images/gear.png; sourceTree = ""; }; CEFC294509C89E3D00D9F998 /* folder32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = folder32.png; path = images/folder32.png; sourceTree = SOURCE_ROOT; }; @@ -302,6 +305,8 @@ CEFC7FB10FC951A700CD5728 /* AppDelegate.h */, CEFC7FB20FC951A700CD5728 /* AppDelegate.m */, CEFC7FB30FC951A700CD5728 /* Consts.h */, + CEE7EA110FE675C80004E467 /* DetailsPanel.h */, + CEE7EA120FE675C80004E467 /* DetailsPanel.m */, CEFC7FB40FC951A700CD5728 /* DirectoryPanel.h */, CEFC7FB50FC951A700CD5728 /* DirectoryPanel.m */, CEFC7FB60FC951A700CD5728 /* PyDupeGuru.h */, @@ -404,6 +409,7 @@ CEFC7FBB0FC951A700CD5728 /* ResultWindow.m in Sources */, CEDD92DA0FDD01640031C7B7 /* BRSingleLineFormatter.m in Sources */, CEDD92DB0FDD01640031C7B7 /* NSCharacterSet_Extensions.m in Sources */, + CEE7EA130FE675C80004E467 /* DetailsPanel.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; From 1ab2c23893703f2fdc666edbe53a882b96a77b19 Mon Sep 17 00:00:00 2001 From: hsoft Date: Mon, 15 Jun 2009 12:40:15 +0000 Subject: [PATCH 055/275] me cocoa: pushed DetailsPanel common code down to dgbase. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4055 --- me/cocoa/DetailsPanel.h | 11 ++--------- me/cocoa/DetailsPanel.m | 12 ------------ me/cocoa/dupeguru.xcodeproj/project.pbxproj | 6 ++++++ 3 files changed, 8 insertions(+), 21 deletions(-) diff --git a/me/cocoa/DetailsPanel.h b/me/cocoa/DetailsPanel.h index 0d4c025d..07779bac 100644 --- a/me/cocoa/DetailsPanel.h +++ b/me/cocoa/DetailsPanel.h @@ -1,13 +1,6 @@ #import -#import "cocoalib/PyApp.h" -#import "cocoalib/Table.h" +#import "dgbase/DetailsPanel.h" -@interface DetailsPanel : NSWindowController -{ - IBOutlet TableView *detailsTable; -} -- (id)initWithPy:(PyApp *)aPy; - -- (void)refresh; +@interface DetailsPanel : DetailsPanelBase @end \ No newline at end of file diff --git a/me/cocoa/DetailsPanel.m b/me/cocoa/DetailsPanel.m index 1baac387..e676aef9 100644 --- a/me/cocoa/DetailsPanel.m +++ b/me/cocoa/DetailsPanel.m @@ -1,16 +1,4 @@ #import "DetailsPanel.h" @implementation DetailsPanel -- (id)initWithPy:(PyApp *)aPy -{ - self = [super initWithWindowNibName:@"Details"]; - [self window]; //So the detailsTable is initialized. - [detailsTable setPy:aPy]; - return self; -} - -- (void)refresh -{ - [detailsTable reloadData]; -} @end diff --git a/me/cocoa/dupeguru.xcodeproj/project.pbxproj b/me/cocoa/dupeguru.xcodeproj/project.pbxproj index c44bd77a..81740288 100644 --- a/me/cocoa/dupeguru.xcodeproj/project.pbxproj +++ b/me/cocoa/dupeguru.xcodeproj/project.pbxproj @@ -49,6 +49,7 @@ CE515E1D0FC6C19300EC695D /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = CE515E160FC6C19300EC695D /* AppDelegate.m */; }; CE515E1E0FC6C19300EC695D /* DirectoryPanel.m in Sources */ = {isa = PBXBuildFile; fileRef = CE515E190FC6C19300EC695D /* DirectoryPanel.m */; }; CE515E1F0FC6C19300EC695D /* ResultWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = CE515E1C0FC6C19300EC695D /* ResultWindow.m */; }; + CE6032C00FE6784C007E33FF /* DetailsPanel.m in Sources */ = {isa = PBXBuildFile; fileRef = CE6032BF0FE6784C007E33FF /* DetailsPanel.m */; }; CE68EE6809ABC48000971085 /* DirectoryPanel.m in Sources */ = {isa = PBXBuildFile; fileRef = CE68EE6609ABC48000971085 /* DirectoryPanel.m */; }; CE848A1909DD85810004CB44 /* Consts.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = CE848A1809DD85810004CB44 /* Consts.h */; }; CEA7D2C50FDFED340037CD8C /* dgme_logo_32.png in Resources */ = {isa = PBXBuildFile; fileRef = CEA7D2C40FDFED340037CD8C /* dgme_logo_32.png */; }; @@ -132,6 +133,8 @@ CE515E1A0FC6C19300EC695D /* PyDupeGuru.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PyDupeGuru.h; path = dgbase/PyDupeGuru.h; sourceTree = SOURCE_ROOT; }; CE515E1B0FC6C19300EC695D /* ResultWindow.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ResultWindow.h; path = dgbase/ResultWindow.h; sourceTree = SOURCE_ROOT; }; CE515E1C0FC6C19300EC695D /* ResultWindow.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ResultWindow.m; path = dgbase/ResultWindow.m; sourceTree = SOURCE_ROOT; }; + CE6032BE0FE6784C007E33FF /* DetailsPanel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = DetailsPanel.h; path = dgbase/DetailsPanel.h; sourceTree = SOURCE_ROOT; }; + CE6032BF0FE6784C007E33FF /* DetailsPanel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = DetailsPanel.m; path = dgbase/DetailsPanel.m; sourceTree = SOURCE_ROOT; }; CE68EE6509ABC48000971085 /* DirectoryPanel.h */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.h; path = DirectoryPanel.h; sourceTree = SOURCE_ROOT; }; CE68EE6609ABC48000971085 /* DirectoryPanel.m */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.objc; path = DirectoryPanel.m; sourceTree = SOURCE_ROOT; }; CE848A1809DD85810004CB44 /* Consts.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Consts.h; sourceTree = ""; }; @@ -310,6 +313,8 @@ CE515E150FC6C19300EC695D /* AppDelegate.h */, CE515E160FC6C19300EC695D /* AppDelegate.m */, CE515E170FC6C19300EC695D /* Consts.h */, + CE6032BE0FE6784C007E33FF /* DetailsPanel.h */, + CE6032BF0FE6784C007E33FF /* DetailsPanel.m */, CE515E180FC6C19300EC695D /* DirectoryPanel.h */, CE515E190FC6C19300EC695D /* DirectoryPanel.m */, CE515E1A0FC6C19300EC695D /* PyDupeGuru.h */, @@ -424,6 +429,7 @@ CE515E1F0FC6C19300EC695D /* ResultWindow.m in Sources */, CE49DEF60FDFEB810098617B /* BRSingleLineFormatter.m in Sources */, CE49DEF70FDFEB810098617B /* NSCharacterSet_Extensions.m in Sources */, + CE6032C00FE6784C007E33FF /* DetailsPanel.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; From f0c234637dc79c003dbaa30c2e326d5ae6a72380 Mon Sep 17 00:00:00 2001 From: hsoft Date: Mon, 15 Jun 2009 12:45:08 +0000 Subject: [PATCH 056/275] pe cocoa: pushed DetailsPanel common code down to dgbase. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4056 --- pe/cocoa/DetailsPanel.h | 9 ++------- pe/cocoa/DetailsPanel.m | 4 +--- pe/cocoa/dupeguru.xcodeproj/project.pbxproj | 6 ++++++ 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/pe/cocoa/DetailsPanel.h b/pe/cocoa/DetailsPanel.h index 119e6cf6..d7dbcaa8 100644 --- a/pe/cocoa/DetailsPanel.h +++ b/pe/cocoa/DetailsPanel.h @@ -1,11 +1,8 @@ #import -#import "PyApp.h" -#import "Table.h" +#import "dgbase/DetailsPanel.h" - -@interface DetailsPanel : NSWindowController +@interface DetailsPanel : DetailsPanelBase { - IBOutlet TableView *detailsTable; IBOutlet NSImageView *dupeImage; IBOutlet NSProgressIndicator *dupeProgressIndicator; IBOutlet NSImageView *refImage; @@ -16,6 +13,4 @@ NSString *_dupePath; NSString *_refPath; } -- (id)initWithPy:(PyApp *)aPy; -- (void)refresh; @end \ No newline at end of file diff --git a/pe/cocoa/DetailsPanel.m b/pe/cocoa/DetailsPanel.m index 00845b33..acfd608f 100644 --- a/pe/cocoa/DetailsPanel.m +++ b/pe/cocoa/DetailsPanel.m @@ -8,9 +8,7 @@ @implementation DetailsPanel - (id)initWithPy:(PyApp *)aPy { - self = [super initWithWindowNibName:@"Details"]; - [self window]; //So the detailsTable is initialized. - [detailsTable setPy:aPy]; + self = [super initWithPy:aPy]; py = aPy; _needsRefresh = YES; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(duplicateSelectionChanged:) name:DuplicateSelectionChangedNotification object:nil]; diff --git a/pe/cocoa/dupeguru.xcodeproj/project.pbxproj b/pe/cocoa/dupeguru.xcodeproj/project.pbxproj index d8ffd079..05f0139d 100644 --- a/pe/cocoa/dupeguru.xcodeproj/project.pbxproj +++ b/pe/cocoa/dupeguru.xcodeproj/project.pbxproj @@ -19,6 +19,7 @@ CE381C9C09914ADF003581CE /* ResultWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = CE381C9A09914ADF003581CE /* ResultWindow.m */; }; CE381D0509915304003581CE /* dg_cocoa.plugin in Resources */ = {isa = PBXBuildFile; fileRef = CE381CF509915304003581CE /* dg_cocoa.plugin */; }; CE3AA46709DB207900DB3A21 /* Directories.nib in Resources */ = {isa = PBXBuildFile; fileRef = CE3AA46509DB207900DB3A21 /* Directories.nib */; }; + CE6044EC0FE6796200B71262 /* DetailsPanel.m in Sources */ = {isa = PBXBuildFile; fileRef = CE6044EB0FE6796200B71262 /* DetailsPanel.m */; }; CE68EE6809ABC48000971085 /* DirectoryPanel.m in Sources */ = {isa = PBXBuildFile; fileRef = CE68EE6609ABC48000971085 /* DirectoryPanel.m */; }; CE80DB2E0FC192D60086DCA6 /* Dialogs.m in Sources */ = {isa = PBXBuildFile; fileRef = CE80DB1C0FC192D60086DCA6 /* Dialogs.m */; }; CE80DB2F0FC192D60086DCA6 /* HSErrorReportWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = CE80DB1E0FC192D60086DCA6 /* HSErrorReportWindow.m */; }; @@ -89,6 +90,8 @@ CE381C9B09914ADF003581CE /* ResultWindow.h */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.h; path = ResultWindow.h; sourceTree = SOURCE_ROOT; }; CE381CF509915304003581CE /* dg_cocoa.plugin */ = {isa = PBXFileReference; lastKnownFileType = folder; name = dg_cocoa.plugin; path = py/dist/dg_cocoa.plugin; sourceTree = SOURCE_ROOT; }; CE3AA46609DB207900DB3A21 /* English */ = {isa = PBXFileReference; lastKnownFileType = wrapper.nib; name = English; path = English.lproj/Directories.nib; sourceTree = ""; }; + CE6044EA0FE6796200B71262 /* DetailsPanel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = DetailsPanel.h; path = dgbase/DetailsPanel.h; sourceTree = SOURCE_ROOT; }; + CE6044EB0FE6796200B71262 /* DetailsPanel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = DetailsPanel.m; path = dgbase/DetailsPanel.m; sourceTree = SOURCE_ROOT; }; CE68EE6509ABC48000971085 /* DirectoryPanel.h */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.h; path = DirectoryPanel.h; sourceTree = SOURCE_ROOT; }; CE68EE6609ABC48000971085 /* DirectoryPanel.m */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.objc; path = DirectoryPanel.m; sourceTree = SOURCE_ROOT; }; CE80DB1B0FC192D60086DCA6 /* Dialogs.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Dialogs.h; path = cocoalib/Dialogs.h; sourceTree = SOURCE_ROOT; }; @@ -293,6 +296,8 @@ CE80DB820FC1951C0086DCA6 /* AppDelegate.h */, CE80DB830FC1951C0086DCA6 /* AppDelegate.m */, CE80DB840FC1951C0086DCA6 /* Consts.h */, + CE6044EA0FE6796200B71262 /* DetailsPanel.h */, + CE6044EB0FE6796200B71262 /* DetailsPanel.m */, CE80DB850FC1951C0086DCA6 /* DirectoryPanel.h */, CE80DB860FC1951C0086DCA6 /* DirectoryPanel.m */, CE80DB870FC1951C0086DCA6 /* PyDupeGuru.h */, @@ -429,6 +434,7 @@ CE80DB8C0FC1951C0086DCA6 /* ResultWindow.m in Sources */, CEBAE4270FDA97E000B7887D /* BRSingleLineFormatter.m in Sources */, CEBAE4280FDA97E000B7887D /* NSCharacterSet_Extensions.m in Sources */, + CE6044EC0FE6796200B71262 /* DetailsPanel.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; From e7be4177bcf124a3dca7c75050b148b6328b20d4 Mon Sep 17 00:00:00 2001 From: hsoft Date: Mon, 15 Jun 2009 12:53:47 +0000 Subject: [PATCH 057/275] pe cocoa: moved the details panel refreshing mechanism down to dgbase, and refactored the Consts unit. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4057 --- base/cocoa/DetailsPanel.h | 3 +++ base/cocoa/DetailsPanel.m | 9 +++++++++ pe/cocoa/Consts.h | 4 ++-- pe/cocoa/Consts.m | 5 ----- pe/cocoa/DetailsPanel.m | 4 +--- pe/cocoa/dupeguru.xcodeproj/project.pbxproj | 4 ---- 6 files changed, 15 insertions(+), 14 deletions(-) delete mode 100644 pe/cocoa/Consts.m diff --git a/base/cocoa/DetailsPanel.h b/base/cocoa/DetailsPanel.h index 8bbd0f52..ca66455a 100644 --- a/base/cocoa/DetailsPanel.h +++ b/base/cocoa/DetailsPanel.h @@ -10,4 +10,7 @@ - (id)initWithPy:(PyApp *)aPy; - (void)refresh; + +/* Notifications */ +- (void)duplicateSelectionChanged:(NSNotification *)aNotification; @end \ No newline at end of file diff --git a/base/cocoa/DetailsPanel.m b/base/cocoa/DetailsPanel.m index 7cca6ede..2c45ef45 100644 --- a/base/cocoa/DetailsPanel.m +++ b/base/cocoa/DetailsPanel.m @@ -1,4 +1,5 @@ #import "DetailsPanel.h" +#import "Consts.h" @implementation DetailsPanelBase - (id)initWithPy:(PyApp *)aPy @@ -6,6 +7,7 @@ self = [super initWithWindowNibName:@"Details"]; [self window]; //So the detailsTable is initialized. [detailsTable setPy:aPy]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(duplicateSelectionChanged:) name:DuplicateSelectionChangedNotification object:nil]; return self; } @@ -13,4 +15,11 @@ { [detailsTable reloadData]; } + +/* Notifications */ +- (void)duplicateSelectionChanged:(NSNotification *)aNotification +{ + if ([[self window] isVisible]) + [self refresh]; +} @end diff --git a/pe/cocoa/Consts.h b/pe/cocoa/Consts.h index c8aea9fa..f8f8bc8a 100644 --- a/pe/cocoa/Consts.h +++ b/pe/cocoa/Consts.h @@ -1,4 +1,4 @@ #import "dgbase/Consts.h" -extern NSString *ImageLoadedNotification; -extern NSString *APPNAME; \ No newline at end of file +#define APPNAME @"dupeGuru PE" +#define ImageLoadedNotification @"ImageLoadedNotification" diff --git a/pe/cocoa/Consts.m b/pe/cocoa/Consts.m deleted file mode 100644 index b7c57178..00000000 --- a/pe/cocoa/Consts.m +++ /dev/null @@ -1,5 +0,0 @@ -#import "Consts.h" - -NSString *ImageLoadedNotification = @"ImageLoadedNotification"; -NSString *APPNAME = @"dupeGuru PE"; - diff --git a/pe/cocoa/DetailsPanel.m b/pe/cocoa/DetailsPanel.m index acfd608f..809c565c 100644 --- a/pe/cocoa/DetailsPanel.m +++ b/pe/cocoa/DetailsPanel.m @@ -11,7 +11,6 @@ self = [super initWithPy:aPy]; py = aPy; _needsRefresh = YES; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(duplicateSelectionChanged:) name:DuplicateSelectionChangedNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(imageLoaded:) name:ImageLoadedNotification object:self]; return self; } @@ -55,8 +54,7 @@ - (void)duplicateSelectionChanged:(NSNotification *)aNotification { _needsRefresh = YES; - if ([[self window] isVisible]) - [self refresh]; + [super duplicateSelectionChanged:aNotification]; } - (void)imageLoaded:(NSNotification *)aNotification diff --git a/pe/cocoa/dupeguru.xcodeproj/project.pbxproj b/pe/cocoa/dupeguru.xcodeproj/project.pbxproj index 05f0139d..703c7c9d 100644 --- a/pe/cocoa/dupeguru.xcodeproj/project.pbxproj +++ b/pe/cocoa/dupeguru.xcodeproj/project.pbxproj @@ -47,7 +47,6 @@ CEDA432E0B07C6E600B3091A /* dg.xsl in Resources */ = {isa = PBXBuildFile; fileRef = CEDA432C0B07C6E600B3091A /* dg.xsl */; }; CEDA432F0B07C6E600B3091A /* hardcoded.css in Resources */ = {isa = PBXBuildFile; fileRef = CEDA432D0B07C6E600B3091A /* hardcoded.css */; }; CEEB135209C837A2004D2330 /* dupeguru.icns in Resources */ = {isa = PBXBuildFile; fileRef = CEEB135109C837A2004D2330 /* dupeguru.icns */; }; - CEF4112B0A11069600E7F110 /* Consts.m in Sources */ = {isa = PBXBuildFile; fileRef = CEF4112A0A11069600E7F110 /* Consts.m */; }; CEF7823809C8AA0200EF38FF /* gear.png in Resources */ = {isa = PBXBuildFile; fileRef = CEF7823709C8AA0200EF38FF /* gear.png */; }; CEFC294609C89E3D00D9F998 /* folder32.png in Resources */ = {isa = PBXBuildFile; fileRef = CEFC294509C89E3D00D9F998 /* folder32.png */; }; CEFC295509C89FF200D9F998 /* details32.png in Resources */ = {isa = PBXBuildFile; fileRef = CEFC295309C89FF200D9F998 /* details32.png */; }; @@ -139,7 +138,6 @@ CEDA432C0B07C6E600B3091A /* dg.xsl */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = text.xml; name = dg.xsl; path = w3/dg.xsl; sourceTree = SOURCE_ROOT; }; CEDA432D0B07C6E600B3091A /* hardcoded.css */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = text; name = hardcoded.css; path = w3/hardcoded.css; sourceTree = SOURCE_ROOT; }; CEEB135109C837A2004D2330 /* dupeguru.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; path = dupeguru.icns; sourceTree = ""; }; - CEF4112A0A11069600E7F110 /* Consts.m */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.objc; path = Consts.m; sourceTree = SOURCE_ROOT; }; CEF7823709C8AA0200EF38FF /* gear.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = gear.png; path = images/gear.png; sourceTree = ""; }; CEFC294509C89E3D00D9F998 /* folder32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = folder32.png; path = images/folder32.png; sourceTree = SOURCE_ROOT; }; CEFC295309C89FF200D9F998 /* details32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = details32.png; path = images/details32.png; sourceTree = SOURCE_ROOT; }; @@ -169,7 +167,6 @@ CE381C9509914ACE003581CE /* AppDelegate.h */, CE381C9409914ACE003581CE /* AppDelegate.m */, CE848A1809DD85810004CB44 /* Consts.h */, - CEF4112A0A11069600E7F110 /* Consts.m */, CECA899A09DB132E00A3D774 /* DetailsPanel.h */, CECA899B09DB132E00A3D774 /* DetailsPanel.m */, CE68EE6509ABC48000971085 /* DirectoryPanel.h */, @@ -416,7 +413,6 @@ CE381C9C09914ADF003581CE /* ResultWindow.m in Sources */, CE68EE6809ABC48000971085 /* DirectoryPanel.m in Sources */, CECA899D09DB132E00A3D774 /* DetailsPanel.m in Sources */, - CEF4112B0A11069600E7F110 /* Consts.m in Sources */, CE0C46AA0FA0647E000BE99B /* PictureBlocks.m in Sources */, CE80DB2E0FC192D60086DCA6 /* Dialogs.m in Sources */, CE80DB2F0FC192D60086DCA6 /* HSErrorReportWindow.m in Sources */, From f67d7ce1e5963f990199925e3384604122ad214d Mon Sep 17 00:00:00 2001 From: hsoft Date: Mon, 15 Jun 2009 12:55:59 +0000 Subject: [PATCH 058/275] se cocoa: Removed the details panel refresh mechanism because it is now handled in dgbase. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4058 --- se/cocoa/ResultWindow.m | 7 ------- 1 file changed, 7 deletions(-) diff --git a/se/cocoa/ResultWindow.m b/se/cocoa/ResultWindow.m index 42cae80d..b0905cbe 100644 --- a/se/cocoa/ResultWindow.m +++ b/se/cocoa/ResultWindow.m @@ -24,7 +24,6 @@ [self initResultColumns]; [self refreshStats]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(resultsMarkingChanged:) name:ResultsMarkingChangedNotification object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(duplicateSelectionChanged:) name:DuplicateSelectionChangedNotification object:nil]; NSToolbar *t = [[[NSToolbar alloc] initWithIdentifier:@"ResultWindowToolbar"] autorelease]; [t setAllowsUserCustomization:YES]; @@ -387,12 +386,6 @@ } /* Notifications */ -- (void)duplicateSelectionChanged:(NSNotification *)aNotification -{ - if (_detailsPanel) - [_detailsPanel refresh]; -} - - (void)outlineViewSelectionDidChange:(NSNotification *)notification { [self performPySelection:[self getSelectedPaths:NO]]; From a3a269ce99217e6ef13322fcf3a4356a3c063098 Mon Sep 17 00:00:00 2001 From: hsoft Date: Mon, 15 Jun 2009 12:57:34 +0000 Subject: [PATCH 059/275] me cocoa: Removed the details panel refresh mechanism because it is now handled in dgbase. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4059 --- me/cocoa/ResultWindow.m | 7 ------- 1 file changed, 7 deletions(-) diff --git a/me/cocoa/ResultWindow.m b/me/cocoa/ResultWindow.m index d1861093..6ba5b2d7 100644 --- a/me/cocoa/ResultWindow.m +++ b/me/cocoa/ResultWindow.m @@ -25,7 +25,6 @@ [self initResultColumns]; [self refreshStats]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(resultsMarkingChanged:) name:ResultsMarkingChangedNotification object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(duplicateSelectionChanged:) name:DuplicateSelectionChangedNotification object:nil]; NSToolbar *t = [[[NSToolbar alloc] initWithIdentifier:@"ResultWindowToolbar"] autorelease]; [t setAllowsUserCustomization:YES]; @@ -405,12 +404,6 @@ } /* Notifications */ -- (void)duplicateSelectionChanged:(NSNotification *)aNotification -{ - if (_detailsPanel) - [_detailsPanel refresh]; -} - - (void)jobCompleted:(NSNotification *)aNotification { [super jobCompleted:aNotification]; From 9493fab56bd187c8ae6df2792333480fc54e51ef Mon Sep 17 00:00:00 2001 From: hsoft Date: Tue, 16 Jun 2009 07:12:59 +0000 Subject: [PATCH 060/275] [#3] qt base: Moved the results' selection restoration code into the modelReset slot, so that everytime to model is reset, the selection is kept. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4060 --- base/qt/main_window.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/base/qt/main_window.py b/base/qt/main_window.py index 5b91a85e..f79b14e0 100644 --- a/base/qt/main_window.py +++ b/base/qt/main_window.py @@ -292,15 +292,15 @@ class MainWindow(QMainWindow, Ui_MainWindow): def resultsChanged(self): self.resultsView.model().reset() + + def resultsReset(self): + self.resultsView.expandAll() dupe = self.app.selected_dupe if dupe is not None: [modelIndex] = self.resultsModel.indexesForDupes([dupe]) if modelIndex.isValid(): flags = QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Rows - self.resultsView.selectionModel().select(modelIndex, flags) - - def resultsReset(self): - self.resultsView.expandAll() + self.resultsView.selectionModel().setCurrentIndex(modelIndex, flags) self._update_status_line() def selectionChanged(self, selected, deselected): From f765e90138b725348140182e4e6526ed78bd5521 Mon Sep 17 00:00:00 2001 From: hsoft Date: Tue, 16 Jun 2009 08:04:23 +0000 Subject: [PATCH 061/275] [#3] base cocoa: Pushed power marker related code down to base. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4061 --- base/cocoa/ResultWindow.h | 3 +++ base/cocoa/ResultWindow.m | 20 ++++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/base/cocoa/ResultWindow.h b/base/cocoa/ResultWindow.h index cf5b09da..e76886c0 100644 --- a/base/cocoa/ResultWindow.h +++ b/base/cocoa/ResultWindow.h @@ -17,6 +17,7 @@ IBOutlet NSView *deltaSwitchView; IBOutlet NSView *filterFieldView; IBOutlet MatchesView *matches; + IBOutlet NSSegmentedControl *pmSwitch; IBOutlet NSView *pmSwitchView; IBOutlet NSTextField *stats; @@ -34,11 +35,13 @@ /* Actions */ - (IBAction)changeDelta:(id)sender; +- (IBAction)changePowerMarker:(id)sender; - (IBAction)copyMarked:(id)sender; - (IBAction)deleteMarked:(id)sender; - (IBAction)expandAll:(id)sender; - (IBAction)moveMarked:(id)sender; - (IBAction)switchSelected:(id)sender; +- (IBAction)togglePowerMarker:(id)sender; /* Notifications */ - (void)jobCompleted:(NSNotification *)aNotification; diff --git a/base/cocoa/ResultWindow.m b/base/cocoa/ResultWindow.m index 13a6f1fc..e535aa84 100644 --- a/base/cocoa/ResultWindow.m +++ b/base/cocoa/ResultWindow.m @@ -118,6 +118,17 @@ [self expandAll:nil]; } +- (IBAction)changePowerMarker:(id)sender +{ + _powerMode = [pmSwitch selectedSegment] == 1; + if (_powerMode) + [matches setTag:2]; + else + [matches setTag:0]; + [self expandAll:nil]; + [self outlineView:matches didClickTableColumn:nil]; +} + - (IBAction)copyMarked:(id)sender { int mark_count = [[py getMarkCount] intValue]; @@ -182,6 +193,15 @@ [[NSNotificationCenter defaultCenter] postNotificationName:ResultsUpdatedNotification object:self]; } +- (IBAction)togglePowerMarker:(id)sender +{ + if ([pmSwitch selectedSegment] == 1) + [pmSwitch setSelectedSegment:0]; + else + [pmSwitch setSelectedSegment:1]; + [self changePowerMarker:sender]; +} + /* Delegate */ - (void)outlineView:(NSOutlineView *)outlineView didClickTableColumn:(NSTableColumn *)tableColumn From f183681ef6328307f548620c3bc6fc162f6ac5a2 Mon Sep 17 00:00:00 2001 From: hsoft Date: Tue, 16 Jun 2009 08:04:47 +0000 Subject: [PATCH 062/275] [#3] se cocoa: Pushed power marker related code down to base. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4062 --- se/cocoa/ResultWindow.h | 3 --- se/cocoa/ResultWindow.m | 21 --------------------- 2 files changed, 24 deletions(-) diff --git a/se/cocoa/ResultWindow.h b/se/cocoa/ResultWindow.h index c391156d..68e24301 100644 --- a/se/cocoa/ResultWindow.h +++ b/se/cocoa/ResultWindow.h @@ -9,7 +9,6 @@ IBOutlet NSPopUpButton *actionMenu; IBOutlet NSMenu *columnsMenu; IBOutlet NSSearchField *filterField; - IBOutlet NSSegmentedControl *pmSwitch; IBOutlet NSWindow *preferencesPanel; NSString *_lastAction; @@ -17,7 +16,6 @@ NSMutableArray *_resultColumns; NSMutableIndexSet *_deltaColumns; } -- (IBAction)changePowerMarker:(id)sender; - (IBAction)clearIgnoreList:(id)sender; - (IBAction)exportToXHTML:(id)sender; - (IBAction)filter:(id)sender; @@ -39,7 +37,6 @@ - (IBAction)toggleColumn:(id)sender; - (IBAction)toggleDelta:(id)sender; - (IBAction)toggleDetailsPanel:(id)sender; -- (IBAction)togglePowerMarker:(id)sender; - (NSTableColumn *)getColumnForIdentifier:(int)aIdentifier title:(NSString *)aTitle width:(int)aWidth refCol:(NSTableColumn *)aColumn; - (NSArray *)getColumnsOrder; diff --git a/se/cocoa/ResultWindow.m b/se/cocoa/ResultWindow.m index b0905cbe..6ec6e324 100644 --- a/se/cocoa/ResultWindow.m +++ b/se/cocoa/ResultWindow.m @@ -40,18 +40,6 @@ } /* Actions */ - -- (IBAction)changePowerMarker:(id)sender -{ - _powerMode = [pmSwitch selectedSegment] == 1; - if (_powerMode) - [matches setTag:2]; - else - [matches setTag:0]; - [self expandAll:nil]; - [self outlineView:matches didClickTableColumn:nil]; -} - - (IBAction)clearIgnoreList:(id)sender { int i = n2i([py getIgnoreListCount]); @@ -262,15 +250,6 @@ [[_detailsPanel window] orderFront:nil]; } -- (IBAction)togglePowerMarker:(id)sender -{ - if ([pmSwitch selectedSegment] == 1) - [pmSwitch setSelectedSegment:0]; - else - [pmSwitch setSelectedSegment:1]; - [self changePowerMarker:sender]; -} - /* Public */ - (NSTableColumn *)getColumnForIdentifier:(int)aIdentifier title:(NSString *)aTitle width:(int)aWidth refCol:(NSTableColumn *)aColumn { From 3deba9459805bb01dada4ec0e717bbd11ba5dbab Mon Sep 17 00:00:00 2001 From: hsoft Date: Tue, 16 Jun 2009 08:09:42 +0000 Subject: [PATCH 063/275] [#3] me cocoa: Pushed power marker related code down to base. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4063 --- me/cocoa/ResultWindow.h | 3 --- me/cocoa/ResultWindow.m | 21 --------------------- 2 files changed, 24 deletions(-) diff --git a/me/cocoa/ResultWindow.h b/me/cocoa/ResultWindow.h index 430cfe36..5b196907 100644 --- a/me/cocoa/ResultWindow.h +++ b/me/cocoa/ResultWindow.h @@ -9,7 +9,6 @@ IBOutlet NSPopUpButton *actionMenu; IBOutlet NSMenu *columnsMenu; IBOutlet NSSearchField *filterField; - IBOutlet NSSegmentedControl *pmSwitch; IBOutlet NSWindow *preferencesPanel; NSString *_lastAction; @@ -17,7 +16,6 @@ NSMutableArray *_resultColumns; NSMutableIndexSet *_deltaColumns; } -- (IBAction)changePowerMarker:(id)sender; - (IBAction)clearIgnoreList:(id)sender; - (IBAction)exportToXHTML:(id)sender; - (IBAction)filter:(id)sender; @@ -40,7 +38,6 @@ - (IBAction)toggleColumn:(id)sender; - (IBAction)toggleDelta:(id)sender; - (IBAction)toggleDetailsPanel:(id)sender; -- (IBAction)togglePowerMarker:(id)sender; - (NSTableColumn *)getColumnForIdentifier:(int)aIdentifier title:(NSString *)aTitle width:(int)aWidth refCol:(NSTableColumn *)aColumn; - (NSArray *)getColumnsOrder; diff --git a/me/cocoa/ResultWindow.m b/me/cocoa/ResultWindow.m index 6ba5b2d7..9887111d 100644 --- a/me/cocoa/ResultWindow.m +++ b/me/cocoa/ResultWindow.m @@ -41,18 +41,6 @@ } /* Actions */ - -- (IBAction)changePowerMarker:(id)sender -{ - _powerMode = [pmSwitch selectedSegment] == 1; - if (_powerMode) - [matches setTag:2]; - else - [matches setTag:0]; - [self expandAll:nil]; - [self outlineView:matches didClickTableColumn:nil]; -} - - (IBAction)clearIgnoreList:(id)sender { int i = n2i([py getIgnoreListCount]); @@ -270,15 +258,6 @@ [[_detailsPanel window] orderFront:nil]; } -- (IBAction)togglePowerMarker:(id)sender -{ - if ([pmSwitch selectedSegment] == 1) - [pmSwitch setSelectedSegment:0]; - else - [pmSwitch setSelectedSegment:1]; - [self changePowerMarker:sender]; -} - /* Public */ - (NSTableColumn *)getColumnForIdentifier:(int)aIdentifier title:(NSString *)aTitle width:(int)aWidth refCol:(NSTableColumn *)aColumn { From a3da162ea22bf9a88ae3784ecde5dc779f34a170 Mon Sep 17 00:00:00 2001 From: hsoft Date: Tue, 16 Jun 2009 08:12:42 +0000 Subject: [PATCH 064/275] [#3] pe cocoa: Pushed power marker related code down to base. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4064 --- pe/cocoa/ResultWindow.h | 3 --- pe/cocoa/ResultWindow.m | 21 --------------------- 2 files changed, 24 deletions(-) diff --git a/pe/cocoa/ResultWindow.h b/pe/cocoa/ResultWindow.h index 8aa0f0bb..941164f0 100644 --- a/pe/cocoa/ResultWindow.h +++ b/pe/cocoa/ResultWindow.h @@ -8,13 +8,11 @@ IBOutlet NSPopUpButton *actionMenu; IBOutlet NSMenu *columnsMenu; IBOutlet NSSearchField *filterField; - IBOutlet NSSegmentedControl *pmSwitch; IBOutlet NSWindow *preferencesPanel; NSMutableArray *_resultColumns; NSMutableIndexSet *_deltaColumns; } -- (IBAction)changePowerMarker:(id)sender; - (IBAction)clearIgnoreList:(id)sender; - (IBAction)clearPictureCache:(id)sender; - (IBAction)exportToXHTML:(id)sender; @@ -37,7 +35,6 @@ - (IBAction)toggleColumn:(id)sender; - (IBAction)toggleDelta:(id)sender; - (IBAction)toggleDetailsPanel:(id)sender; -- (IBAction)togglePowerMarker:(id)sender; - (IBAction)toggleDirectories:(id)sender; - (NSTableColumn *)getColumnForIdentifier:(int)aIdentifier title:(NSString *)aTitle width:(int)aWidth refCol:(NSTableColumn *)aColumn; diff --git a/pe/cocoa/ResultWindow.m b/pe/cocoa/ResultWindow.m index cfd93106..886d34e0 100644 --- a/pe/cocoa/ResultWindow.m +++ b/pe/cocoa/ResultWindow.m @@ -41,18 +41,6 @@ } /* Actions */ - -- (IBAction)changePowerMarker:(id)sender -{ - _powerMode = [pmSwitch selectedSegment] == 1; - if (_powerMode) - [matches setTag:2]; - else - [matches setTag:0]; - [self expandAll:nil]; - [self outlineView:matches didClickTableColumn:nil]; -} - - (IBAction)clearIgnoreList:(id)sender { int i = n2i([py getIgnoreListCount]); @@ -262,15 +250,6 @@ [(AppDelegate *)app toggleDetailsPanel:sender]; } -- (IBAction)togglePowerMarker:(id)sender -{ - if ([pmSwitch selectedSegment] == 1) - [pmSwitch setSelectedSegment:0]; - else - [pmSwitch setSelectedSegment:1]; - [self changePowerMarker:sender]; -} - - (IBAction)toggleDirectories:(id)sender { [(AppDelegate *)app toggleDirectories:sender]; From d30591fd39983091c823b0109191c80cc2fd1a77 Mon Sep 17 00:00:00 2001 From: hsoft Date: Tue, 16 Jun 2009 09:04:41 +0000 Subject: [PATCH 065/275] [#3] py: added Dupeguru.get_*_node_paths() --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4065 --- py/app_cocoa.py | 29 ++++++++++++++++++++++++- py/tests/app_cocoa_test.py | 44 +++++++++++++++++++++++++++++++++----- 2 files changed, 67 insertions(+), 6 deletions(-) diff --git a/py/app_cocoa.py b/py/app_cocoa.py index 81589f2c..808a5415 100644 --- a/py/app_cocoa.py +++ b/py/app_cocoa.py @@ -13,8 +13,9 @@ import logging import os.path as op import hsfs as fs -from hsutil.cocoa import install_exception_hook from hsutil import io, cocoa, job +from hsutil.cocoa import install_exception_hook +from hsutil.misc import stripnone from hsutil.reg import RegistrationRequired import export, app, data @@ -179,6 +180,32 @@ class DupeGuru(app.DupeGuru): except app.AllFilesAreRefError: return 1 + def selected_result_node_paths(self): + def get_path(dupe): + try: + group = self.results.get_group_of_duplicate(dupe) + groupindex = self.results.groups.index(group) + if dupe is group.ref: + return [groupindex] + dupeindex = group.dupes.index(dupe) + return [groupindex, dupeindex] + except ValueError: # dupe not in there + return None + + dupes = self.selected_dupes + return stripnone(get_path(dupe) for dupe in dupes) + + def selected_powermarker_node_paths(self): + def get_path(dupe): + try: + dupeindex = self.results.dupes.index(dupe) + return [dupeindex] + except ValueError: # dupe not in there + return None + + dupes = self.selected_dupes + return stripnone(get_path(dupe) for dupe in dupes) + def SelectResultNodePaths(self,node_paths): def extract_dupe(t): g,d = t diff --git a/py/tests/app_cocoa_test.py b/py/tests/app_cocoa_test.py index 60d2e83d..c1ded17a 100644 --- a/py/tests/app_cocoa_test.py +++ b/py/tests/app_cocoa_test.py @@ -7,17 +7,19 @@ import tempfile import shutil import logging +import os.path as op + +from nose.tools import eq_ from hsutil.path import Path from hsutil.testcase import TestCase from hsutil.decorators import log_calls import hsfs.phys -import os.path as op from .results_test import GetTestGroups from .. import engine, data try: - from ..app_cocoa import DupeGuru as DupeGuruBase, DGDirectory + from ..app_cocoa import DupeGuru as DupeGuruBase except ImportError: from nose.plugins.skip import SkipTest raise SkipTest("These tests can only be run on OS X") @@ -70,6 +72,24 @@ class TCDupeGuru(TestCase): self.assertEqual((None,None),app.GetObjects([])) self.assertEqual((None,None),app.GetObjects([1,2])) + def test_selected_result_node_paths(self): + # app.selected_dupes is correctly converted into node paths + app = self.app + objects = self.objects + paths = [[0, 0], [0, 1], [1]] + app.SelectResultNodePaths(paths) + eq_(app.selected_result_node_paths(), paths) + + def test_selected_result_node_paths_after_deletion(self): + # cases where the selected dupes aren't there are correctly handled + app = self.app + objects = self.objects + paths = [[0, 0], [0, 1], [1]] + app.SelectResultNodePaths(paths) + app.RemoveSelected() + # The first 2 dupes have been removed. The 3rd one is a ref. it stays there, in first pos. + eq_(app.selected_result_node_paths(), [[0]]) # no exception + def test_selectResultNodePaths(self): app = self.app objects = self.objects @@ -108,6 +128,23 @@ class TCDupeGuru(TestCase): app.SelectResultNodePaths([[0,0],[0,1],[1],[1,1],[2]]) self.assertEqual(3,len(app.selected_dupes)) + def test_selected_powermarker_node_paths(self): + # app.selected_dupes is correctly converted into paths + app = self.app + objects = self.objects + paths = r2np([0, 1, 2]) + app.SelectPowerMarkerNodePaths(paths) + eq_(app.selected_powermarker_node_paths(), paths) + + def test_selected_powermarker_node_paths_after_deletion(self): + # cases where the selected dupes aren't there are correctly handled + app = self.app + objects = self.objects + paths = r2np([0, 1, 2]) + app.SelectPowerMarkerNodePaths(paths) + app.RemoveSelected() + eq_(app.selected_powermarker_node_paths(), []) # no exception + def test_selectPowerMarkerRows(self): app = self.app objects = self.objects @@ -249,9 +286,6 @@ class TCDupeGuru(TestCase): app.SelectPowerMarkerNodePaths(r2np([2])) #The dupe of the second, 2 sized group app.AddSelectedToIgnoreList() - def test_dirclass(self): - self.assert_(self.app.directories.dirclass is DGDirectory) - class TCDupeGuru_renameSelected(TestCase): def setUp(self): From 12482fed1b7b3f5b07ceaeb78191535fc347764a Mon Sep 17 00:00:00 2001 From: hsoft Date: Tue, 16 Jun 2009 09:07:33 +0000 Subject: [PATCH 066/275] [#3] base cocoa: added selection synchronization with the py side. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4066 --- base/cocoa/PyDupeGuru.h | 2 ++ base/cocoa/ResultWindow.h | 1 + base/cocoa/ResultWindow.m | 11 +++++++++++ 3 files changed, 14 insertions(+) diff --git a/base/cocoa/PyDupeGuru.h b/base/cocoa/PyDupeGuru.h index 6a660b03..6d7ef768 100644 --- a/base/cocoa/PyDupeGuru.h +++ b/base/cocoa/PyDupeGuru.h @@ -16,7 +16,9 @@ - (NSNumber *)doScan; +- (NSArray *)selectedPowerMarkerNodePaths; - (void)selectPowerMarkerNodePaths:(NSArray *)aIndexPaths; +- (NSArray *)selectedResultNodePaths; - (void)selectResultNodePaths:(NSArray *)aIndexPaths; - (void)toggleSelectedMark; diff --git a/base/cocoa/ResultWindow.h b/base/cocoa/ResultWindow.h index e76886c0..dbbb1c61 100644 --- a/base/cocoa/ResultWindow.h +++ b/base/cocoa/ResultWindow.h @@ -30,6 +30,7 @@ /* Helpers */ - (NSArray *)getSelected:(BOOL)aDupesOnly; - (NSArray *)getSelectedPaths:(BOOL)aDupesOnly; +- (void)updatePySelection; - (void)performPySelection:(NSArray *)aIndexPaths; - (void)refreshStats; diff --git a/base/cocoa/ResultWindow.m b/base/cocoa/ResultWindow.m index e535aa84..aa4b7775 100644 --- a/base/cocoa/ResultWindow.m +++ b/base/cocoa/ResultWindow.m @@ -96,6 +96,16 @@ return r; } +- (void)updatePySelection +{ + NSArray *selection; + if (_powerMode) + selection = [py selectedPowerMarkerNodePaths]; + else + selection = [py selectedResultNodePaths]; + [matches selectNodePaths:selection]; +} + - (void)performPySelection:(NSArray *)aIndexPaths { if (_powerMode) @@ -127,6 +137,7 @@ [matches setTag:0]; [self expandAll:nil]; [self outlineView:matches didClickTableColumn:nil]; + [self updatePySelection]; } - (IBAction)copyMarked:(id)sender From 52205ab07b7f86779db0fccd6ae87c9d1e1d7396 Mon Sep 17 00:00:00 2001 From: hsoft Date: Tue, 16 Jun 2009 09:08:16 +0000 Subject: [PATCH 067/275] [#3] se cocoa: adapted to the recent API changes. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4067 --- se/cocoa/py/dg_cocoa.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/se/cocoa/py/dg_cocoa.py b/se/cocoa/py/dg_cocoa.py index af32e561..8bf26603 100644 --- a/se/cocoa/py/dg_cocoa.py +++ b/se/cocoa/py/dg_cocoa.py @@ -68,9 +68,15 @@ class PyDupeGuru(PyApp): def refreshDetailsWithSelected(self): self.app.RefreshDetailsWithSelected() + def selectedResultNodePaths(self): + return self.app.selected_result_node_paths() + def selectResultNodePaths_(self,node_paths): self.app.SelectResultNodePaths(node_paths) + def selectedPowerMarkerNodePaths(self): + return self.app.selected_powermarker_node_paths() + def selectPowerMarkerNodePaths_(self,node_paths): self.app.SelectPowerMarkerNodePaths(node_paths) From 26b2599bf4d5194639901864de89193ad862046b Mon Sep 17 00:00:00 2001 From: hsoft Date: Tue, 16 Jun 2009 09:09:19 +0000 Subject: [PATCH 068/275] [#3] me cocoa: adapted to the recent API changes. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4068 --- me/cocoa/py/dg_cocoa.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/me/cocoa/py/dg_cocoa.py b/me/cocoa/py/dg_cocoa.py index e3b86b41..db6af459 100644 --- a/me/cocoa/py/dg_cocoa.py +++ b/me/cocoa/py/dg_cocoa.py @@ -69,9 +69,15 @@ class PyDupeGuru(PyApp): def refreshDetailsWithSelected(self): self.app.RefreshDetailsWithSelected() + def selectedResultNodePaths(self): + return self.app.selected_result_node_paths() + def selectResultNodePaths_(self,node_paths): self.app.SelectResultNodePaths(node_paths) + def selectedPowerMarkerNodePaths(self): + return self.app.selected_powermarker_node_paths() + def selectPowerMarkerNodePaths_(self,node_paths): self.app.SelectPowerMarkerNodePaths(node_paths) From cda0cdf5427ec416d84bbeab4d8b7200e7df6c43 Mon Sep 17 00:00:00 2001 From: hsoft Date: Tue, 16 Jun 2009 09:09:51 +0000 Subject: [PATCH 069/275] [#3] pe cocoa: adapted to the recent API changes. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4069 --- pe/cocoa/py/dg_cocoa.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pe/cocoa/py/dg_cocoa.py b/pe/cocoa/py/dg_cocoa.py index 57b60f59..995d12a9 100644 --- a/pe/cocoa/py/dg_cocoa.py +++ b/pe/cocoa/py/dg_cocoa.py @@ -71,9 +71,15 @@ class PyDupeGuru(PyApp): def refreshDetailsWithSelected(self): self.app.RefreshDetailsWithSelected() + def selectedResultNodePaths(self): + return self.app.selected_result_node_paths() + def selectResultNodePaths_(self,node_paths): self.app.SelectResultNodePaths(node_paths) + def selectedPowerMarkerNodePaths(self): + return self.app.selected_powermarker_node_paths() + def selectPowerMarkerNodePaths_(self,node_paths): self.app.SelectPowerMarkerNodePaths(node_paths) From ede178b3ff4bfee1e7ded9066f9734ea306ff4f2 Mon Sep 17 00:00:00 2001 From: hsoft Date: Wed, 17 Jun 2009 12:12:02 +0000 Subject: [PATCH 070/275] base qt: Fixed an incompatibility with PyQt 4.5.1 (lists of non Q values couldn't be sent directly to QVariant anymore). --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4070 --- base/qt/preferences.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/base/qt/preferences.py b/base/qt/preferences.py index 64ad64d5..ab7653b9 100644 --- a/base/qt/preferences.py +++ b/base/qt/preferences.py @@ -30,9 +30,14 @@ def variant_to_py(v): elif t in (QVariant.List, QVariant.StringList): value, ok = map(variant_to_py, v.toList()), True if not ok: - raise TypeError() + raise TypeError(u"Can't convert {0} of type {1}".format(repr(v), v.type())) return value +def py_to_variant(v): + if isinstance(v, (list, tuple)): + return QVariant(map(py_to_variant, v)) + return QVariant(v) + class Preferences(object): # (width, is_visible) COLUMNS_DEFAULT_ATTRS = [] @@ -94,7 +99,7 @@ class Preferences(object): def save(self): settings = QSettings() def set_(name, value): - settings.setValue(name, QVariant(value)) + settings.setValue(name, py_to_variant(value)) set_('FilterHardness', self.filter_hardness) set_('MixFileKind', self.mix_file_kind) From 3ada0ff89c00054a7966d071de235bae6defe460 Mon Sep 17 00:00:00 2001 From: hsoft Date: Thu, 18 Jun 2009 18:13:45 +0000 Subject: [PATCH 071/275] [#38 state:fixed] Removed those LookupErrors. They were useless anyway. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4071 --- py/directories.py | 23 ++++++++--------------- py/tests/directories_test.py | 3 ++- 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/py/directories.py b/py/directories.py index 17342c02..7aa04c42 100644 --- a/py/directories.py +++ b/py/directories.py @@ -107,11 +107,7 @@ class Directories(object): def GetState(self, path): """Returns the state of 'path' (One of the STATE_* const.) - - Raises LookupError if 'path' is not in self. """ - if path not in self: - raise LookupError("The path '%s' is not in the directory list." % str(path)) try: return self.states[path] except KeyError: @@ -163,16 +159,13 @@ class Directories(object): doc.writexml(fp,'\t','\t','\n',encoding='utf-8') def SetState(self,path,state): - try: - if self.GetState(path) == state: + if self.GetState(path) == state: + return + # we don't want to needlessly fill self.states. if GetState returns the same thing + # without an explicit entry, remove that entry + if path in self.states: + del self.states[path] + if self.GetState(path) == state: # no need for an entry return - # we don't want to needlessly fill self.states. if GetState returns the same thing - # without an explicit entry, remove that entry - if path in self.states: - del self.states[path] - if self.GetState(path) == state: # no need for an entry - return - self.states[path] = state - except LookupError: - pass + self.states[path] = state diff --git a/py/tests/directories_test.py b/py/tests/directories_test.py index 40247218..80120295 100644 --- a/py/tests/directories_test.py +++ b/py/tests/directories_test.py @@ -93,9 +93,10 @@ class TCDirectories(TestCase): self.assertEqual(STATE_REFERENCE,d.states[p]) def test_GetState_with_path_not_there(self): + # When the path's not there, just return STATE_NORMAL d = Directories() d.add_path(testpath + 'utils') - self.assertRaises(LookupError,d.GetState,testpath) + eq_(d.GetState(testpath), STATE_NORMAL) def test_states_remain_when_larger_directory_eat_smaller_ones(self): d = Directories() From a7a76f2b40878822027ade2c10ca21b9e96ee4e3 Mon Sep 17 00:00:00 2001 From: hsoft Date: Thu, 18 Jun 2009 18:37:53 +0000 Subject: [PATCH 072/275] base py: pep8ified dupeguru.directories (about time!). --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4072 --- py/app.py | 4 +- py/app_cocoa.py | 4 +- py/app_pe_cocoa.py | 4 +- py/directories.py | 66 ++++++++++------------- py/tests/directories_test.py | 100 ++++++++++++++++------------------- 5 files changed, 81 insertions(+), 97 deletions(-) diff --git a/py/app.py b/py/app.py index 48dc0412..7dccb474 100644 --- a/py/app.py +++ b/py/app.py @@ -80,7 +80,7 @@ class DupeGuru(RegistrableApplication): return False def _do_load(self, j): - self.directories.LoadFromFile(op.join(self.appdata, 'last_directories.xml')) + self.directories.load_from_file(op.join(self.appdata, 'last_directories.xml')) j = j.start_subjob([1, 9]) self.results.load_from_xml(op.join(self.appdata, 'last_results.xml'), self._get_file, j) files = flatten(g[:] for g in self.results.groups) @@ -194,7 +194,7 @@ class DupeGuru(RegistrableApplication): changed_groups.add(g) def save(self): - self.directories.SaveToFile(op.join(self.appdata, 'last_directories.xml')) + self.directories.save_to_file(op.join(self.appdata, 'last_directories.xml')) self.results.save_to_xml(op.join(self.appdata, 'last_results.xml')) def save_ignore_list(self): diff --git a/py/app_cocoa.py b/py/app_cocoa.py index 808a5415..814dbe2d 100644 --- a/py/app_cocoa.py +++ b/py/app_cocoa.py @@ -226,7 +226,7 @@ class DupeGuru(app.DupeGuru): def SetDirectoryState(self,node_path,state): d = self.GetDirectory(node_path) - self.directories.SetState(d.path,state) + self.directories.set_state(d.path,state) def sort_dupes(self,key,asc): self.results.sort_dupes(key,asc,self.display_delta_values) @@ -279,7 +279,7 @@ class DupeGuru(app.DupeGuru): d = self.GetDirectory(node_path) return [ d.name, - self.directories.GetState(d.path) + self.directories.get_state(d.path) ] def GetOutlineViewMarked(self, tag, node_path): diff --git a/py/app_pe_cocoa.py b/py/app_pe_cocoa.py index 6e27bb0a..91c51dc1 100644 --- a/py/app_pe_cocoa.py +++ b/py/app_pe_cocoa.py @@ -169,7 +169,7 @@ class DupeGuruPE(app_cocoa.DupeGuru): return app_cocoa.DupeGuru._do_delete_dupe(self, dupe) def _do_load(self, j): - self.directories.LoadFromFile(op.join(self.appdata, 'last_directories.xml')) + self.directories.load_from_file(op.join(self.appdata, 'last_directories.xml')) for d in self.directories: if isinstance(d, IPhotoLibrary): d.update() @@ -201,7 +201,7 @@ class DupeGuruPE(app_cocoa.DupeGuru): def start_scanning(self): for directory in self.directories: if isinstance(directory, IPhotoLibrary): - self.directories.SetState(directory.refpath, directories.STATE_EXCLUDED) + self.directories.set_state(directory.refpath, directories.STATE_EXCLUDED) return app_cocoa.DupeGuru.start_scanning(self) def selected_dupe_path(self): diff --git a/py/directories.py b/py/directories.py index 7aa04c42..f61e5d70 100644 --- a/py/directories.py +++ b/py/directories.py @@ -1,13 +1,9 @@ -#!/usr/bin/env python -""" -Unit Name: dupeguru.directories -Created By: Virgil Dupras -Created On: 2006/02/27 -Last modified by:$Author: virgil $ -Last modified on:$Date: 2009-05-28 16:02:48 +0200 (Thu, 28 May 2009) $ - $Revision: 4388 $ -Copyright 2004-2006 Hardcoded Software (http://www.hardcoded.net) -""" +# Unit Name: dupeguru.directories +# Created By: Virgil Dupras +# Created On: 2006/02/27 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + import xml.dom.minidom from hsfs import phys @@ -56,7 +52,7 @@ class Directories(object): def _get_files(self, from_dir): from_path = from_dir.path - state = self.GetState(from_path) + state = self.get_state(from_path) if state == STATE_EXCLUDED: # Recursively get files from folders with lots of subfolder is expensive. However, there # might be a subfolder in this path that is not excluded. What we want to do is to skim @@ -90,7 +86,7 @@ class Directories(object): self._dirs.append(d) return d except fs.InvalidPath: - raise InvalidPathError + raise InvalidPathError() def get_files(self): """Returns a list of all files that are not excluded. @@ -105,22 +101,21 @@ class Directories(object): except fs.InvalidPath: pass - def GetState(self, path): + def get_state(self, path): """Returns the state of 'path' (One of the STATE_* const.) """ - try: + if path in self.states: return self.states[path] - except KeyError: - default_state = self._default_state_for_path(path) - if default_state is not None: - return default_state - parent = path[:-1] - if parent in self: - return self.GetState(parent) - else: - return STATE_NORMAL + default_state = self._default_state_for_path(path) + if default_state is not None: + return default_state + parent = path[:-1] + if parent in self: + return self.get_state(parent) + else: + return STATE_NORMAL - def LoadFromFile(self,infile): + def load_from_file(self, infile): try: doc = xml.dom.minidom.parse(infile) except: @@ -128,11 +123,11 @@ class Directories(object): root_dir_nodes = doc.getElementsByTagName('root_directory') for rdn in root_dir_nodes: if not rdn.getAttributeNode('path'): - continue + continue path = rdn.getAttributeNode('path').nodeValue try: self.add_path(Path(path)) - except (AlreadyThereError,InvalidPathError): + except (AlreadyThereError, InvalidPathError): pass state_nodes = doc.getElementsByTagName('state') for sn in state_nodes: @@ -140,32 +135,29 @@ class Directories(object): continue path = sn.getAttributeNode('path').nodeValue state = sn.getAttributeNode('value').nodeValue - self.SetState(Path(path), int(state)) + self.set_state(Path(path), int(state)) - def Remove(self,directory): - self._dirs.remove(directory) - - def SaveToFile(self,outfile): + def save_to_file(self,outfile): with FileOrPath(outfile, 'wb') as fp: doc = xml.dom.minidom.Document() root = doc.appendChild(doc.createElement('directories')) for root_dir in self: root_dir_node = root.appendChild(doc.createElement('root_directory')) root_dir_node.setAttribute('path', unicode(root_dir.path).encode('utf-8')) - for path,state in self.states.iteritems(): + for path, state in self.states.iteritems(): state_node = root.appendChild(doc.createElement('state')) state_node.setAttribute('path', unicode(path).encode('utf-8')) state_node.setAttribute('value', str(state)) - doc.writexml(fp,'\t','\t','\n',encoding='utf-8') + doc.writexml(fp, '\t', '\t', '\n', encoding='utf-8') - def SetState(self,path,state): - if self.GetState(path) == state: + def set_state(self, path, state): + if self.get_state(path) == state: return - # we don't want to needlessly fill self.states. if GetState returns the same thing + # we don't want to needlessly fill self.states. if get_state returns the same thing # without an explicit entry, remove that entry if path in self.states: del self.states[path] - if self.GetState(path) == state: # no need for an entry + if self.get_state(path) == state: # no need for an entry return self.states[path] = state diff --git a/py/tests/directories_test.py b/py/tests/directories_test.py index 80120295..846d2b6a 100644 --- a/py/tests/directories_test.py +++ b/py/tests/directories_test.py @@ -84,52 +84,52 @@ class TCDirectories(TestCase): d = Directories() p = testpath + 'utils' d.add_path(p) - self.assertEqual(STATE_NORMAL,d.GetState(p)) - d.SetState(p,STATE_REFERENCE) - self.assertEqual(STATE_REFERENCE,d.GetState(p)) - self.assertEqual(STATE_REFERENCE,d.GetState(p + 'dir1')) + self.assertEqual(STATE_NORMAL,d.get_state(p)) + d.set_state(p,STATE_REFERENCE) + self.assertEqual(STATE_REFERENCE,d.get_state(p)) + self.assertEqual(STATE_REFERENCE,d.get_state(p + 'dir1')) self.assertEqual(1,len(d.states)) self.assertEqual(p,d.states.keys()[0]) self.assertEqual(STATE_REFERENCE,d.states[p]) - def test_GetState_with_path_not_there(self): + def test_get_state_with_path_not_there(self): # When the path's not there, just return STATE_NORMAL d = Directories() d.add_path(testpath + 'utils') - eq_(d.GetState(testpath), STATE_NORMAL) + eq_(d.get_state(testpath), STATE_NORMAL) def test_states_remain_when_larger_directory_eat_smaller_ones(self): d = Directories() p = testpath + 'utils' d.add_path(p) - d.SetState(p,STATE_EXCLUDED) + d.set_state(p,STATE_EXCLUDED) d.add_path(testpath) - d.SetState(testpath,STATE_REFERENCE) - self.assertEqual(STATE_EXCLUDED,d.GetState(p)) - self.assertEqual(STATE_EXCLUDED,d.GetState(p + 'dir1')) - self.assertEqual(STATE_REFERENCE,d.GetState(testpath)) + d.set_state(testpath,STATE_REFERENCE) + self.assertEqual(STATE_EXCLUDED,d.get_state(p)) + self.assertEqual(STATE_EXCLUDED,d.get_state(p + 'dir1')) + self.assertEqual(STATE_REFERENCE,d.get_state(testpath)) - def test_SetState_keep_state_dict_size_to_minimum(self): + def test_set_state_keep_state_dict_size_to_minimum(self): d = Directories() p = Path(phys_test.create_fake_fs(self.tmpdir())) d.add_path(p) - d.SetState(p,STATE_REFERENCE) - d.SetState(p + 'dir1',STATE_REFERENCE) + d.set_state(p,STATE_REFERENCE) + d.set_state(p + 'dir1',STATE_REFERENCE) self.assertEqual(1,len(d.states)) - self.assertEqual(STATE_REFERENCE,d.GetState(p + 'dir1')) - d.SetState(p + 'dir1',STATE_NORMAL) + self.assertEqual(STATE_REFERENCE,d.get_state(p + 'dir1')) + d.set_state(p + 'dir1',STATE_NORMAL) self.assertEqual(2,len(d.states)) - self.assertEqual(STATE_NORMAL,d.GetState(p + 'dir1')) - d.SetState(p + 'dir1',STATE_REFERENCE) + self.assertEqual(STATE_NORMAL,d.get_state(p + 'dir1')) + d.set_state(p + 'dir1',STATE_REFERENCE) self.assertEqual(1,len(d.states)) - self.assertEqual(STATE_REFERENCE,d.GetState(p + 'dir1')) + self.assertEqual(STATE_REFERENCE,d.get_state(p + 'dir1')) def test_get_files(self): d = Directories() p = Path(phys_test.create_fake_fs(self.tmpdir())) d.add_path(p) - d.SetState(p + 'dir1',STATE_REFERENCE) - d.SetState(p + 'dir2',STATE_EXCLUDED) + d.set_state(p + 'dir1',STATE_REFERENCE) + d.set_state(p + 'dir2',STATE_EXCLUDED) files = d.get_files() self.assertEqual(5, len(list(files))) for f in files: @@ -142,7 +142,7 @@ class TCDirectories(TestCase): d = Directories() p = testpath + 'utils' d.add_path(p) - d.SetState(p,STATE_EXCLUDED) + d.set_state(p,STATE_EXCLUDED) self.assertEqual([], list(d.get_files())) def test_save_and_load(self): @@ -152,14 +152,14 @@ class TCDirectories(TestCase): p2 = self.tmppath() d1.add_path(p1) d1.add_path(p2) - d1.SetState(p1, STATE_REFERENCE) - d1.SetState(p1 + 'dir1',STATE_EXCLUDED) + d1.set_state(p1, STATE_REFERENCE) + d1.set_state(p1 + 'dir1',STATE_EXCLUDED) tmpxml = op.join(self.tmpdir(), 'directories_testunit.xml') - d1.SaveToFile(tmpxml) - d2.LoadFromFile(tmpxml) + d1.save_to_file(tmpxml) + d2.load_from_file(tmpxml) self.assertEqual(2, len(d2)) - self.assertEqual(STATE_REFERENCE,d2.GetState(p1)) - self.assertEqual(STATE_EXCLUDED,d2.GetState(p1 + 'dir1')) + self.assertEqual(STATE_REFERENCE,d2.get_state(p1)) + self.assertEqual(STATE_EXCLUDED,d2.get_state(p1 + 'dir1')) def test_invalid_path(self): d = Directories() @@ -167,10 +167,10 @@ class TCDirectories(TestCase): self.assertRaises(InvalidPathError, d.add_path, p) self.assertEqual(0, len(d)) - def test_SetState_on_invalid_path(self): + def test_set_state_on_invalid_path(self): d = Directories() try: - d.SetState(Path('foobar',),STATE_NORMAL) + d.set_state(Path('foobar',),STATE_NORMAL) except LookupError: self.fail() @@ -184,7 +184,7 @@ class TCDirectories(TestCase): d.add_path(testpath) self.assert_(isinstance(d[0], MySpecialDirclass)) - def test_LoadFromFile_with_invalid_path(self): + def test_load_from_file_with_invalid_path(self): #This test simulates a load from file resulting in a #InvalidPath raise. Other directories must be loaded. d1 = Directories() @@ -192,12 +192,12 @@ class TCDirectories(TestCase): #Will raise InvalidPath upon loading d1.add_path(self.tmppath()).name = 'does_not_exist' tmpxml = op.join(self.tmpdir(), 'directories_testunit.xml') - d1.SaveToFile(tmpxml) + d1.save_to_file(tmpxml) d2 = Directories() - d2.LoadFromFile(tmpxml) + d2.load_from_file(tmpxml) self.assertEqual(1, len(d2)) - def test_LoadFromFile_with_same_paths(self): + def test_load_from_file_with_same_paths(self): #This test simulates a load from file resulting in a #AlreadyExists raise. Other directories must be loaded. d1 = Directories() @@ -208,29 +208,21 @@ class TCDirectories(TestCase): #Will raise AlreadyExists upon loading d1.add_path(self.tmppath()).name = unicode(p1) tmpxml = op.join(self.tmpdir(), 'directories_testunit.xml') - d1.SaveToFile(tmpxml) + d1.save_to_file(tmpxml) d2 = Directories() - d2.LoadFromFile(tmpxml) + d2.load_from_file(tmpxml) self.assertEqual(2, len(d2)) - def test_Remove(self): - d = Directories() - d1 = d.add_path(self.tmppath()) - d2 = d.add_path(self.tmppath()) - d.Remove(d1) - self.assertEqual(1, len(d)) - self.assert_(d[0] is d2) - def test_unicode_save(self): d = Directories() p1 = self.tmppath() + u'hello\xe9' io.mkdir(p1) io.mkdir(p1 + u'foo\xe9') d.add_path(p1) - d.SetState(d[0][0].path, STATE_EXCLUDED) + d.set_state(d[0][0].path, STATE_EXCLUDED) tmpxml = op.join(self.tmpdir(), 'directories_testunit.xml') try: - d.SaveToFile(tmpxml) + d.save_to_file(tmpxml) except UnicodeDecodeError: self.fail() @@ -252,16 +244,16 @@ class TCDirectories(TestCase): io.rmtree(p) self.assertEqual([], list(d.get_files())) - def test_GetState_returns_excluded_by_default_for_hidden_directories(self): + def test_get_state_returns_excluded_by_default_for_hidden_directories(self): d = Directories() p = Path(self.tmpdir()) hidden_dir_path = p + '.foo' io.mkdir(p + '.foo') d.add_path(p) - self.assertEqual(d.GetState(hidden_dir_path), STATE_EXCLUDED) + self.assertEqual(d.get_state(hidden_dir_path), STATE_EXCLUDED) # But it can be overriden - d.SetState(hidden_dir_path, STATE_NORMAL) - self.assertEqual(d.GetState(hidden_dir_path), STATE_NORMAL) + d.set_state(hidden_dir_path, STATE_NORMAL) + self.assertEqual(d.get_state(hidden_dir_path), STATE_NORMAL) def test_special_dirclasses(self): # if a path is in special_dirclasses, use this class instead @@ -287,11 +279,11 @@ class TCDirectories(TestCase): io.mkdir(p1 + 'foobaz') io.open(p1 + 'foobaz/somefile', 'w').close() d.add_path(p1) - eq_(d.GetState(p1 + 'foobaz'), STATE_NORMAL) - eq_(d.GetState(p1 + 'foobar'), STATE_EXCLUDED) + eq_(d.get_state(p1 + 'foobaz'), STATE_NORMAL) + eq_(d.get_state(p1 + 'foobar'), STATE_EXCLUDED) eq_(len(list(d.get_files())), 1) # only the 'foobaz' file is there # However, the default state can be changed - d.SetState(p1 + 'foobar', STATE_NORMAL) - eq_(d.GetState(p1 + 'foobar'), STATE_NORMAL) + d.set_state(p1 + 'foobar', STATE_NORMAL) + eq_(d.get_state(p1 + 'foobar'), STATE_NORMAL) eq_(len(list(d.get_files())), 2) From 87a56bb53766a7235eea377e8948f0c91fd1108a Mon Sep 17 00:00:00 2001 From: hsoft Date: Thu, 18 Jun 2009 18:40:22 +0000 Subject: [PATCH 073/275] base qt: Adjusted to pep8ifying in r72. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4073 --- base/qt/directories_model.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/base/qt/directories_model.py b/base/qt/directories_model.py index cae88f39..f66f798d 100644 --- a/base/qt/directories_model.py +++ b/base/qt/directories_model.py @@ -72,11 +72,11 @@ class DirectoriesModel(TreeModel): if index.column() == 0: return QVariant(node.ref.name) else: - return QVariant(STATES[self._dirs.GetState(node.ref.path)]) + return QVariant(STATES[self._dirs.get_state(node.ref.path)]) elif role == Qt.EditRole and index.column() == 1: - return QVariant(self._dirs.GetState(node.ref.path)) + return QVariant(self._dirs.get_state(node.ref.path)) elif role == Qt.ForegroundRole: - state = self._dirs.GetState(node.ref.path) + state = self._dirs.get_state(node.ref.path) if state == 1: return QVariant(QBrush(Qt.blue)) elif state == 2: @@ -103,6 +103,6 @@ class DirectoriesModel(TreeModel): node = index.internalPointer() state, ok = value.toInt() assert ok - self._dirs.SetState(node.ref.path, state) + self._dirs.set_state(node.ref.path, state) return True From d1f460b091a325fcf6b92533535e05882f09b3b1 Mon Sep 17 00:00:00 2001 From: hsoft Date: Thu, 18 Jun 2009 18:55:00 +0000 Subject: [PATCH 074/275] Moving the 'py' folder into 'base'. --HG-- rename : py/__init__.py => base/py/__init__.py rename : py/app.py => base/py/app.py rename : py/app_cocoa.py => base/py/app_cocoa.py rename : py/app_me_cocoa.py => base/py/app_me_cocoa.py rename : py/app_pe_cocoa.py => base/py/app_pe_cocoa.py rename : py/app_se_cocoa.py => base/py/app_se_cocoa.py rename : py/data.py => base/py/data.py rename : py/data_me.py => base/py/data_me.py rename : py/data_pe.py => base/py/data_pe.py rename : py/directories.py => base/py/directories.py rename : py/engine.py => base/py/engine.py rename : py/export.py => base/py/export.py rename : py/gen.py => base/py/gen.py rename : py/ignore.py => base/py/ignore.py rename : py/modules/block/block.pyx => base/py/modules/block/block.pyx rename : py/modules/block/setup.py => base/py/modules/block/setup.py rename : py/modules/cache/cache.pyx => base/py/modules/cache/cache.pyx rename : py/modules/cache/setup.py => base/py/modules/cache/setup.py rename : py/picture/__init__.py => base/py/picture/__init__.py rename : py/picture/block.py => base/py/picture/block.py rename : py/picture/cache.py => base/py/picture/cache.py rename : py/picture/matchbase.py => base/py/picture/matchbase.py rename : py/results.py => base/py/results.py rename : py/scanner.py => base/py/scanner.py rename : py/tests/__init__.py => base/py/tests/__init__.py rename : py/tests/app_cocoa_test.py => base/py/tests/app_cocoa_test.py rename : py/tests/app_test.py => base/py/tests/app_test.py rename : py/tests/block_test.py => base/py/tests/block_test.py rename : py/tests/cache_test.py => base/py/tests/cache_test.py rename : py/tests/directories_test.py => base/py/tests/directories_test.py rename : py/tests/engine_test.py => base/py/tests/engine_test.py rename : py/tests/export_test.py => base/py/tests/export_test.py rename : py/tests/ignore_test.py => base/py/tests/ignore_test.py rename : py/tests/results_test.py => base/py/tests/results_test.py rename : py/tests/scanner_test.py => base/py/tests/scanner_test.py extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4074 --- {py => base/py}/__init__.py | 0 {py => base/py}/app.py | 0 {py => base/py}/app_cocoa.py | 0 {py => base/py}/app_me_cocoa.py | 0 {py => base/py}/app_pe_cocoa.py | 0 {py => base/py}/app_se_cocoa.py | 0 {py => base/py}/data.py | 0 {py => base/py}/data_me.py | 0 {py => base/py}/data_pe.py | 0 {py => base/py}/directories.py | 0 {py => base/py}/engine.py | 0 {py => base/py}/export.py | 0 {py => base/py}/gen.py | 0 {py => base/py}/ignore.py | 0 {py => base/py}/modules/block/block.pyx | 0 {py => base/py}/modules/block/setup.py | 0 {py => base/py}/modules/cache/cache.pyx | 0 {py => base/py}/modules/cache/setup.py | 0 {py => base/py}/picture/__init__.py | 0 {py => base/py}/picture/block.py | 0 {py => base/py}/picture/cache.py | 0 {py => base/py}/picture/matchbase.py | 0 {py => base/py}/results.py | 0 {py => base/py}/scanner.py | 0 {py => base/py}/tests/__init__.py | 0 {py => base/py}/tests/app_cocoa_test.py | 0 {py => base/py}/tests/app_test.py | 0 {py => base/py}/tests/block_test.py | 0 {py => base/py}/tests/cache_test.py | 0 {py => base/py}/tests/directories_test.py | 0 {py => base/py}/tests/engine_test.py | 0 {py => base/py}/tests/export_test.py | 0 {py => base/py}/tests/ignore_test.py | 0 {py => base/py}/tests/results_test.py | 0 {py => base/py}/tests/scanner_test.py | 0 35 files changed, 0 insertions(+), 0 deletions(-) rename {py => base/py}/__init__.py (100%) rename {py => base/py}/app.py (100%) rename {py => base/py}/app_cocoa.py (100%) rename {py => base/py}/app_me_cocoa.py (100%) rename {py => base/py}/app_pe_cocoa.py (100%) rename {py => base/py}/app_se_cocoa.py (100%) rename {py => base/py}/data.py (100%) rename {py => base/py}/data_me.py (100%) rename {py => base/py}/data_pe.py (100%) rename {py => base/py}/directories.py (100%) rename {py => base/py}/engine.py (100%) rename {py => base/py}/export.py (100%) rename {py => base/py}/gen.py (100%) rename {py => base/py}/ignore.py (100%) rename {py => base/py}/modules/block/block.pyx (100%) rename {py => base/py}/modules/block/setup.py (100%) rename {py => base/py}/modules/cache/cache.pyx (100%) rename {py => base/py}/modules/cache/setup.py (100%) rename {py => base/py}/picture/__init__.py (100%) rename {py => base/py}/picture/block.py (100%) rename {py => base/py}/picture/cache.py (100%) rename {py => base/py}/picture/matchbase.py (100%) rename {py => base/py}/results.py (100%) rename {py => base/py}/scanner.py (100%) rename {py => base/py}/tests/__init__.py (100%) rename {py => base/py}/tests/app_cocoa_test.py (100%) rename {py => base/py}/tests/app_test.py (100%) rename {py => base/py}/tests/block_test.py (100%) rename {py => base/py}/tests/cache_test.py (100%) rename {py => base/py}/tests/directories_test.py (100%) rename {py => base/py}/tests/engine_test.py (100%) rename {py => base/py}/tests/export_test.py (100%) rename {py => base/py}/tests/ignore_test.py (100%) rename {py => base/py}/tests/results_test.py (100%) rename {py => base/py}/tests/scanner_test.py (100%) diff --git a/py/__init__.py b/base/py/__init__.py similarity index 100% rename from py/__init__.py rename to base/py/__init__.py diff --git a/py/app.py b/base/py/app.py similarity index 100% rename from py/app.py rename to base/py/app.py diff --git a/py/app_cocoa.py b/base/py/app_cocoa.py similarity index 100% rename from py/app_cocoa.py rename to base/py/app_cocoa.py diff --git a/py/app_me_cocoa.py b/base/py/app_me_cocoa.py similarity index 100% rename from py/app_me_cocoa.py rename to base/py/app_me_cocoa.py diff --git a/py/app_pe_cocoa.py b/base/py/app_pe_cocoa.py similarity index 100% rename from py/app_pe_cocoa.py rename to base/py/app_pe_cocoa.py diff --git a/py/app_se_cocoa.py b/base/py/app_se_cocoa.py similarity index 100% rename from py/app_se_cocoa.py rename to base/py/app_se_cocoa.py diff --git a/py/data.py b/base/py/data.py similarity index 100% rename from py/data.py rename to base/py/data.py diff --git a/py/data_me.py b/base/py/data_me.py similarity index 100% rename from py/data_me.py rename to base/py/data_me.py diff --git a/py/data_pe.py b/base/py/data_pe.py similarity index 100% rename from py/data_pe.py rename to base/py/data_pe.py diff --git a/py/directories.py b/base/py/directories.py similarity index 100% rename from py/directories.py rename to base/py/directories.py diff --git a/py/engine.py b/base/py/engine.py similarity index 100% rename from py/engine.py rename to base/py/engine.py diff --git a/py/export.py b/base/py/export.py similarity index 100% rename from py/export.py rename to base/py/export.py diff --git a/py/gen.py b/base/py/gen.py similarity index 100% rename from py/gen.py rename to base/py/gen.py diff --git a/py/ignore.py b/base/py/ignore.py similarity index 100% rename from py/ignore.py rename to base/py/ignore.py diff --git a/py/modules/block/block.pyx b/base/py/modules/block/block.pyx similarity index 100% rename from py/modules/block/block.pyx rename to base/py/modules/block/block.pyx diff --git a/py/modules/block/setup.py b/base/py/modules/block/setup.py similarity index 100% rename from py/modules/block/setup.py rename to base/py/modules/block/setup.py diff --git a/py/modules/cache/cache.pyx b/base/py/modules/cache/cache.pyx similarity index 100% rename from py/modules/cache/cache.pyx rename to base/py/modules/cache/cache.pyx diff --git a/py/modules/cache/setup.py b/base/py/modules/cache/setup.py similarity index 100% rename from py/modules/cache/setup.py rename to base/py/modules/cache/setup.py diff --git a/py/picture/__init__.py b/base/py/picture/__init__.py similarity index 100% rename from py/picture/__init__.py rename to base/py/picture/__init__.py diff --git a/py/picture/block.py b/base/py/picture/block.py similarity index 100% rename from py/picture/block.py rename to base/py/picture/block.py diff --git a/py/picture/cache.py b/base/py/picture/cache.py similarity index 100% rename from py/picture/cache.py rename to base/py/picture/cache.py diff --git a/py/picture/matchbase.py b/base/py/picture/matchbase.py similarity index 100% rename from py/picture/matchbase.py rename to base/py/picture/matchbase.py diff --git a/py/results.py b/base/py/results.py similarity index 100% rename from py/results.py rename to base/py/results.py diff --git a/py/scanner.py b/base/py/scanner.py similarity index 100% rename from py/scanner.py rename to base/py/scanner.py diff --git a/py/tests/__init__.py b/base/py/tests/__init__.py similarity index 100% rename from py/tests/__init__.py rename to base/py/tests/__init__.py diff --git a/py/tests/app_cocoa_test.py b/base/py/tests/app_cocoa_test.py similarity index 100% rename from py/tests/app_cocoa_test.py rename to base/py/tests/app_cocoa_test.py diff --git a/py/tests/app_test.py b/base/py/tests/app_test.py similarity index 100% rename from py/tests/app_test.py rename to base/py/tests/app_test.py diff --git a/py/tests/block_test.py b/base/py/tests/block_test.py similarity index 100% rename from py/tests/block_test.py rename to base/py/tests/block_test.py diff --git a/py/tests/cache_test.py b/base/py/tests/cache_test.py similarity index 100% rename from py/tests/cache_test.py rename to base/py/tests/cache_test.py diff --git a/py/tests/directories_test.py b/base/py/tests/directories_test.py similarity index 100% rename from py/tests/directories_test.py rename to base/py/tests/directories_test.py diff --git a/py/tests/engine_test.py b/base/py/tests/engine_test.py similarity index 100% rename from py/tests/engine_test.py rename to base/py/tests/engine_test.py diff --git a/py/tests/export_test.py b/base/py/tests/export_test.py similarity index 100% rename from py/tests/export_test.py rename to base/py/tests/export_test.py diff --git a/py/tests/ignore_test.py b/base/py/tests/ignore_test.py similarity index 100% rename from py/tests/ignore_test.py rename to base/py/tests/ignore_test.py diff --git a/py/tests/results_test.py b/base/py/tests/results_test.py similarity index 100% rename from py/tests/results_test.py rename to base/py/tests/results_test.py diff --git a/py/tests/scanner_test.py b/base/py/tests/scanner_test.py similarity index 100% rename from py/tests/scanner_test.py rename to base/py/tests/scanner_test.py From e5b93a74fa4c710e8fd8523f4b75869b54a02f13 Mon Sep 17 00:00:00 2001 From: hsoft Date: Thu, 18 Jun 2009 19:05:35 +0000 Subject: [PATCH 075/275] Changed all externals to the new 'py' location. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4075 From 137c33439d2eac0542bc2903ba0f85358c1e7c3a Mon Sep 17 00:00:00 2001 From: hsoft Date: Thu, 18 Jun 2009 19:11:10 +0000 Subject: [PATCH 076/275] Moved dupeguru.picture into 'pe/py' --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4076 --- {base/py/picture => pe/py}/__init__.py | 0 {base/py/picture => pe/py}/block.py | 0 {base/py/picture => pe/py}/cache.py | 0 {base => pe}/py/gen.py | 8 ++++---- {base/py/picture => pe/py}/matchbase.py | 0 {base => pe}/py/modules/block/block.pyx | 0 {base => pe}/py/modules/block/setup.py | 0 {base => pe}/py/modules/cache/cache.pyx | 0 {base => pe}/py/modules/cache/setup.py | 0 9 files changed, 4 insertions(+), 4 deletions(-) rename {base/py/picture => pe/py}/__init__.py (100%) rename {base/py/picture => pe/py}/block.py (100%) rename {base/py/picture => pe/py}/cache.py (100%) rename {base => pe}/py/gen.py (63%) rename {base/py/picture => pe/py}/matchbase.py (100%) rename {base => pe}/py/modules/block/block.pyx (100%) rename {base => pe}/py/modules/block/setup.py (100%) rename {base => pe}/py/modules/cache/cache.pyx (100%) rename {base => pe}/py/modules/cache/setup.py (100%) diff --git a/base/py/picture/__init__.py b/pe/py/__init__.py similarity index 100% rename from base/py/picture/__init__.py rename to pe/py/__init__.py diff --git a/base/py/picture/block.py b/pe/py/block.py similarity index 100% rename from base/py/picture/block.py rename to pe/py/block.py diff --git a/base/py/picture/cache.py b/pe/py/cache.py similarity index 100% rename from base/py/picture/cache.py rename to pe/py/cache.py diff --git a/base/py/gen.py b/pe/py/gen.py similarity index 63% rename from base/py/gen.py rename to pe/py/gen.py index 0a842372..097f592c 100644 --- a/base/py/gen.py +++ b/pe/py/gen.py @@ -22,7 +22,7 @@ os.system('python setup.py build_ext --inplace') os.chdir(op.join('..', 'cache')) os.system('python setup.py build_ext --inplace') os.chdir(op.join('..', '..')) -move(op.join('modules', 'block', '_block.so'), op.join('picture', '_block.so')) -move(op.join('modules', 'block', '_block.pyd'), op.join('picture', '_block.pyd')) -move(op.join('modules', 'cache', '_cache.so'), op.join('picture', '_cache.so')) -move(op.join('modules', 'cache', '_cache.pyd'), op.join('picture', '_cache.pyd')) +move(op.join('modules', 'block', '_block.so'), '.') +move(op.join('modules', 'block', '_block.pyd'), '.') +move(op.join('modules', 'cache', '_cache.so'), '.') +move(op.join('modules', 'cache', '_cache.pyd'), '.') diff --git a/base/py/picture/matchbase.py b/pe/py/matchbase.py similarity index 100% rename from base/py/picture/matchbase.py rename to pe/py/matchbase.py diff --git a/base/py/modules/block/block.pyx b/pe/py/modules/block/block.pyx similarity index 100% rename from base/py/modules/block/block.pyx rename to pe/py/modules/block/block.pyx diff --git a/base/py/modules/block/setup.py b/pe/py/modules/block/setup.py similarity index 100% rename from base/py/modules/block/setup.py rename to pe/py/modules/block/setup.py diff --git a/base/py/modules/cache/cache.pyx b/pe/py/modules/cache/cache.pyx similarity index 100% rename from base/py/modules/cache/cache.pyx rename to pe/py/modules/cache/cache.pyx diff --git a/base/py/modules/cache/setup.py b/pe/py/modules/cache/setup.py similarity index 100% rename from base/py/modules/cache/setup.py rename to pe/py/modules/cache/setup.py From 34f1ff438e5e864fe4d43603ff5744b74fa89279 Mon Sep 17 00:00:00 2001 From: hsoft Date: Thu, 18 Jun 2009 19:13:25 +0000 Subject: [PATCH 077/275] Had forgotten to move app_pe_cocoa. --HG-- rename : base/py/app_pe_cocoa.py => pe/py/app_cocoa.py extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4077 --- base/py/app_pe_cocoa.py => pe/py/app_cocoa.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename base/py/app_pe_cocoa.py => pe/py/app_cocoa.py (100%) diff --git a/base/py/app_pe_cocoa.py b/pe/py/app_cocoa.py similarity index 100% rename from base/py/app_pe_cocoa.py rename to pe/py/app_cocoa.py From 2c555413b8aba0ef38a0572e9b6253188bd35bc2 Mon Sep 17 00:00:00 2001 From: hsoft Date: Thu, 18 Jun 2009 19:17:10 +0000 Subject: [PATCH 078/275] Had forgotten to remove the 'picture' folder from 'base/py'. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4078 From fdd796796241ce32ceb7e74b7d40d1c3cddb6d6d Mon Sep 17 00:00:00 2001 From: hsoft Date: Thu, 18 Jun 2009 19:20:11 +0000 Subject: [PATCH 079/275] Arg! and there's also 'data_pe' which I hadn't moved. --HG-- rename : base/py/data_pe.py => pe/py/data.py extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4079 --- base/py/data_pe.py => pe/py/data.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename base/py/data_pe.py => pe/py/data.py (100%) diff --git a/base/py/data_pe.py b/pe/py/data.py similarity index 100% rename from base/py/data_pe.py rename to pe/py/data.py From 72bdb4258a6708537b6223787ef14a0091a246a8 Mon Sep 17 00:00:00 2001 From: hsoft Date: Thu, 18 Jun 2009 19:42:27 +0000 Subject: [PATCH 080/275] pe py: Adjusted the code to its move from 'base/py' --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4080 --- pe/py/app_cocoa.py | 25 +++++++++++-------------- pe/py/block.py | 16 ++++++---------- pe/py/cache.py | 16 ++++++---------- pe/py/data.py | 18 +++++++----------- pe/py/gen.py | 8 ++++---- pe/py/matchbase.py | 20 ++++++++------------ 6 files changed, 42 insertions(+), 61 deletions(-) diff --git a/pe/py/app_cocoa.py b/pe/py/app_cocoa.py index 91c51dc1..72716c08 100644 --- a/pe/py/app_cocoa.py +++ b/pe/py/app_cocoa.py @@ -1,13 +1,9 @@ -#!/usr/bin/env python -""" -Unit Name: dupeguru.app_pe_cocoa -Created By: Virgil Dupras -Created On: 2006/11/13 -Last modified by:$Author: virgil $ -Last modified on:$Date: 2009-05-28 16:33:32 +0200 (Thu, 28 May 2009) $ - $Revision: 4392 $ -Copyright 2006 Hardcoded Software (http://www.hardcoded.net) -""" +# Unit Name: dupeguru_pe.app_cocoa +# Created By: Virgil Dupras +# Created On: 2006/11/13 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + import os import os.path as op import logging @@ -26,8 +22,9 @@ from hsutil.str import get_file_ext from hsutil.path import Path from hsutil.cocoa import as_fetch -import app_cocoa, data_pe, directories, picture.matchbase -from picture.cache import string_to_colors, Cache +from dupeguru import app_cocoa, directories +from . import data, matchbase +from .cache import string_to_colors, Cache mainBundle = NSBundle.mainBundle() PictureBlocks = mainBundle.classNamed_('PictureBlocks') @@ -123,8 +120,8 @@ class IPhotoLibrary(fs.Directory): class DupeGuruPE(app_cocoa.DupeGuru): def __init__(self): - app_cocoa.DupeGuru.__init__(self, data_pe, 'dupeguru_pe', appid=5) - self.scanner.match_factory = picture.matchbase.AsyncMatchFactory() + app_cocoa.DupeGuru.__init__(self, data, 'dupeguru_pe', appid=5) + self.scanner.match_factory = matchbase.AsyncMatchFactory() self.directories.dirclass = Directory self.directories.special_dirclasses[Path('iPhoto Library')] = lambda _, __: self._create_iphoto_library() p = op.join(self.appdata, 'cached_pictures.db') diff --git a/pe/py/block.py b/pe/py/block.py index 70015a50..a9aaa8d4 100644 --- a/pe/py/block.py +++ b/pe/py/block.py @@ -1,13 +1,9 @@ -#!/usr/bin/env python -""" -Unit Name: hs.picture.block -Created By: Virgil Dupras -Created On: 2006/09/01 -Last modified by:$Author: virgil $ -Last modified on:$Date: 2009-05-26 18:12:39 +0200 (Tue, 26 May 2009) $ - $Revision: 4365 $ -Copyright 2004-2006 Hardcoded Software (http://www.hardcoded.net) -""" +# Unit Name: dupeguru_pe.block +# Created By: Virgil Dupras +# Created On: 2006/09/01 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + from _block import NoBlocksError, DifferentBlockCountError, avgdiff, getblocks2 # Converted to Cython diff --git a/pe/py/cache.py b/pe/py/cache.py index 6ff0d2d1..428eedcd 100644 --- a/pe/py/cache.py +++ b/pe/py/cache.py @@ -1,13 +1,9 @@ -#!/usr/bin/env python -""" -Unit Name: hs.picture.cache -Created By: Virgil Dupras -Created On: 2006/09/14 -Last modified by:$Author: virgil $ -Last modified on:$Date: 2009-05-28 16:33:32 +0200 (Thu, 28 May 2009) $ - $Revision: 4392 $ -Copyright 2004-2006 Hardcoded Software (http://www.hardcoded.net) -""" +# Unit Name: dupeguru_pe.cache +# Created By: Virgil Dupras +# Created On: 2006/09/14 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + import os import logging import sqlite3 as sqlite diff --git a/pe/py/data.py b/pe/py/data.py index 94bdd99d..cd35a902 100644 --- a/pe/py/data.py +++ b/pe/py/data.py @@ -1,15 +1,11 @@ -#!/usr/bin/env python -""" -Unit Name: dupeguru.data -Created By: Virgil Dupras -Created On: 2006/03/15 -Last modified by:$Author: virgil $ -Last modified on:$Date: 2009-05-28 15:22:39 +0200 (Thu, 28 May 2009) $ - $Revision: 4385 $ -Copyright 2004-2006 Hardcoded Software (http://www.hardcoded.net) -""" +# Unit Name: dupeguru_pe.data +# Created By: Virgil Dupras +# Created On: 2006/03/15 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + from hsutil.str import format_size -from .data import format_path, format_timestamp, format_perc, format_dupe_count, cmp_value +from dupeguru.data import format_path, format_timestamp, format_perc, format_dupe_count, cmp_value def format_dimensions(dimensions): return '%d x %d' % (dimensions[0], dimensions[1]) diff --git a/pe/py/gen.py b/pe/py/gen.py index 097f592c..2c86d9e7 100644 --- a/pe/py/gen.py +++ b/pe/py/gen.py @@ -22,7 +22,7 @@ os.system('python setup.py build_ext --inplace') os.chdir(op.join('..', 'cache')) os.system('python setup.py build_ext --inplace') os.chdir(op.join('..', '..')) -move(op.join('modules', 'block', '_block.so'), '.') -move(op.join('modules', 'block', '_block.pyd'), '.') -move(op.join('modules', 'cache', '_cache.so'), '.') -move(op.join('modules', 'cache', '_cache.pyd'), '.') +move(op.join('modules', 'block', '_block.so'), '_block.so') +move(op.join('modules', 'block', '_block.pyd'), '_block.pyd') +move(op.join('modules', 'cache', '_cache.so'), '_cache.so') +move(op.join('modules', 'cache', '_cache.pyd'), '_cache.pyd') diff --git a/pe/py/matchbase.py b/pe/py/matchbase.py index 1bc4d04a..a0bebe86 100644 --- a/pe/py/matchbase.py +++ b/pe/py/matchbase.py @@ -1,13 +1,9 @@ -#!/usr/bin/env python -""" -Unit Name: hs.picture._match -Created By: Virgil Dupras -Created On: 2007/02/25 -Last modified by:$Author: virgil $ -Last modified on:$Date: 2009-05-28 16:02:48 +0200 (Thu, 28 May 2009) $ - $Revision: 4388 $ -Copyright 2007 Hardcoded Software (http://www.hardcoded.net) -""" +# Unit Name: dupeguru_pe.matchbase +# Created By: Virgil Dupras +# Created On: 2007/02/25 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + import logging import multiprocessing from Queue import Empty @@ -17,8 +13,8 @@ from hsutil import job from hsutil.misc import dedupe from dupeguru.engine import Match -from block import avgdiff, DifferentBlockCountError, NoBlocksError -from cache import Cache +from .block import avgdiff, DifferentBlockCountError, NoBlocksError +from .cache import Cache MIN_ITERATIONS = 3 From 1706675ae44ef5bc13aadaffc8aedca53212d08d Mon Sep 17 00:00:00 2001 From: hsoft Date: Thu, 18 Jun 2009 19:43:16 +0000 Subject: [PATCH 081/275] pe cocoa: Adjusted to recent dupeguru_pe move. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4081 --- pe/cocoa/py/dg_cocoa.py | 4 ++-- pe/cocoa/py/gen.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pe/cocoa/py/dg_cocoa.py b/pe/cocoa/py/dg_cocoa.py index 995d12a9..595fbea9 100644 --- a/pe/cocoa/py/dg_cocoa.py +++ b/pe/cocoa/py/dg_cocoa.py @@ -2,11 +2,11 @@ import objc from AppKit import * -from dupeguru import app_pe_cocoa, scanner +from dupeguru_pe import app_cocoa as app_pe_cocoa # Fix py2app imports which chokes on relative imports from dupeguru import app, app_cocoa, data, directories, engine, export, ignore, results, scanner -from dupeguru.picture import block, cache, matchbase +from dupeguru_pe import block, cache, matchbase from hsfs import auto, manual, stats, tree, utils class PyApp(NSObject): diff --git a/pe/cocoa/py/gen.py b/pe/cocoa/py/gen.py index 6195927d..2ca126a4 100644 --- a/pe/cocoa/py/gen.py +++ b/pe/cocoa/py/gen.py @@ -6,7 +6,7 @@ import shutil from hsutil.build import print_and_do -os.chdir('dupeguru') +os.chdir('dupeguru_pe') print_and_do('python gen.py') os.chdir('..') @@ -15,4 +15,4 @@ if op.exists('build'): if op.exists('dist'): shutil.rmtree('dist') -print_and_do('python -u setup.py py2app') \ No newline at end of file +print_and_do('python -u setup.py py2app -A') \ No newline at end of file From 2bd92e961a80d3f51ffcc5329491135118adee7e Mon Sep 17 00:00:00 2001 From: hsoft Date: Thu, 18 Jun 2009 19:52:24 +0000 Subject: [PATCH 082/275] base qt: Adjusted to dupeguru_pe move --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4082 --- base/qt/app.py | 1 - 1 file changed, 1 deletion(-) diff --git a/base/qt/app.py b/base/qt/app.py index 60a01389..d621ae76 100644 --- a/base/qt/app.py +++ b/base/qt/app.py @@ -15,7 +15,6 @@ from PyQt4.QtGui import QProgressDialog, QDesktopServices, QFileDialog, QDialog, from hsutil import job from hsutil.reg import RegistrationRequired -from dupeguru import data_pe from dupeguru.app import (DupeGuru as DupeGuruBase, JOB_SCAN, JOB_LOAD, JOB_MOVE, JOB_COPY, JOB_DELETE) From cf2e176fdfa85d1ba500e045b2260cf884b03985 Mon Sep 17 00:00:00 2001 From: hsoft Date: Thu, 18 Jun 2009 19:52:38 +0000 Subject: [PATCH 083/275] pe qt: Adjusted to dupeguru_pe move --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4083 --- pe/qt/app.py | 6 +++--- pe/qt/gen.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pe/qt/app.py b/pe/qt/app.py index a3b693e2..742d4819 100644 --- a/pe/qt/app.py +++ b/pe/qt/app.py @@ -13,9 +13,9 @@ import PIL.Image from hsfs import phys, IT_ATTRS, IT_MD5, IT_EXTRA from hsutil.str import get_file_ext -from dupeguru import data_pe -from dupeguru.picture.cache import Cache -from dupeguru.picture.matchbase import AsyncMatchFactory +from dupeguru_pe import data as data_pe +from dupeguru_pe.cache import Cache +from dupeguru_pe.matchbase import AsyncMatchFactory from block import getblocks from base.app import DupeGuru as DupeGuruBase diff --git a/pe/qt/gen.py b/pe/qt/gen.py index 7da1519c..2449de44 100644 --- a/pe/qt/gen.py +++ b/pe/qt/gen.py @@ -20,7 +20,7 @@ def move(src, dst): print 'Moving %s --> %s' % (src, dst) os.rename(src, dst) -os.chdir('dupeguru') +os.chdir('dupeguru_pe') print_and_do('python gen.py') os.chdir('..') From a49791173d5a76a38d39d66bfed0499e70400ad8 Mon Sep 17 00:00:00 2001 From: hsoft Date: Thu, 18 Jun 2009 20:00:00 +0000 Subject: [PATCH 084/275] Had forgotten to move the pe related tests to 'pe/py'. --HG-- rename : base/py/tests/block_test.py => pe/py/tests/block_test.py rename : base/py/tests/cache_test.py => pe/py/tests/cache_test.py extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4084 --- pe/py/tests/__init__.py | 0 {base => pe}/py/tests/block_test.py | 4 ++-- {base => pe}/py/tests/cache_test.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) create mode 100644 pe/py/tests/__init__.py rename {base => pe}/py/tests/block_test.py (99%) rename {base => pe}/py/tests/cache_test.py (98%) diff --git a/pe/py/tests/__init__.py b/pe/py/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/base/py/tests/block_test.py b/pe/py/tests/block_test.py similarity index 99% rename from base/py/tests/block_test.py rename to pe/py/tests/block_test.py index 25d97783..1ee02faf 100644 --- a/base/py/tests/block_test.py +++ b/pe/py/tests/block_test.py @@ -1,4 +1,4 @@ -# Unit Name: tests.picture.block +# Unit Name: dupeguru_pe.tests.block_test # Created By: Virgil Dupras # Created On: 2006/09/01 # $Id$ @@ -7,7 +7,7 @@ import unittest -from ..picture.block import * +from ..block import * def my_avgdiff(first, second, limit=768, min_iter=3): # this is so I don't have to re-write every call return avgdiff(first, second, limit, min_iter) diff --git a/base/py/tests/cache_test.py b/pe/py/tests/cache_test.py similarity index 98% rename from base/py/tests/cache_test.py rename to pe/py/tests/cache_test.py index c16f330e..3fd70759 100644 --- a/base/py/tests/cache_test.py +++ b/pe/py/tests/cache_test.py @@ -1,4 +1,4 @@ -# Unit Name: dupeguru.tests.cache_test +# Unit Name: dupeguru_pe.tests.cache_test # Created By: Virgil Dupras # Created On: 2006/09/14 # $Id$ @@ -11,7 +11,7 @@ import threading from hsutil.testcase import TestCase -from ..picture.cache import * +from ..cache import * class TCcolors_to_string(TestCase): def test_no_color(self): From e618a9bc55762ffe53050c5172e411b44bb98790 Mon Sep 17 00:00:00 2001 From: hsoft Date: Thu, 18 Jun 2009 20:06:06 +0000 Subject: [PATCH 085/275] 'gen.py' in dupeguru no longer needs to be called in non-pe editions. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4085 --- me/cocoa/py/gen.py | 4 ---- me/qt/gen.py | 4 ---- se/cocoa/py/gen.py | 4 ---- se/qt/gen.py | 4 ---- 4 files changed, 16 deletions(-) diff --git a/me/cocoa/py/gen.py b/me/cocoa/py/gen.py index 6195927d..997214dd 100644 --- a/me/cocoa/py/gen.py +++ b/me/cocoa/py/gen.py @@ -6,10 +6,6 @@ import shutil from hsutil.build import print_and_do -os.chdir('dupeguru') -print_and_do('python gen.py') -os.chdir('..') - if op.exists('build'): shutil.rmtree('build') if op.exists('dist'): diff --git a/me/qt/gen.py b/me/qt/gen.py index 9edb6040..618100a6 100644 --- a/me/qt/gen.py +++ b/me/qt/gen.py @@ -9,10 +9,6 @@ import os from hsutil.build import print_and_do -os.chdir('dupeguru') -print_and_do('python gen.py') -os.chdir('..') - os.chdir('base') print_and_do('python gen.py') os.chdir('..') diff --git a/se/cocoa/py/gen.py b/se/cocoa/py/gen.py index 6195927d..997214dd 100644 --- a/se/cocoa/py/gen.py +++ b/se/cocoa/py/gen.py @@ -6,10 +6,6 @@ import shutil from hsutil.build import print_and_do -os.chdir('dupeguru') -print_and_do('python gen.py') -os.chdir('..') - if op.exists('build'): shutil.rmtree('build') if op.exists('dist'): diff --git a/se/qt/gen.py b/se/qt/gen.py index b712a5b6..f90bc791 100644 --- a/se/qt/gen.py +++ b/se/qt/gen.py @@ -11,10 +11,6 @@ def print_and_do(cmd): print cmd os.system(cmd) -os.chdir('dupeguru') -print_and_do('python gen.py') -os.chdir('..') - os.chdir('base') print_and_do('python gen.py') os.chdir('..') From de178a5aba9af885e89ca3e3523e8bdb1fcf84bf Mon Sep 17 00:00:00 2001 From: hsoft Date: Fri, 19 Jun 2009 10:28:36 +0000 Subject: [PATCH 086/275] [#37 state:fixed] Ignore AlreadyExistsError in IPhotoLibrary. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4086 --- pe/py/app_cocoa.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pe/py/app_cocoa.py b/pe/py/app_cocoa.py index 72716c08..37b54bbd 100644 --- a/pe/py/app_cocoa.py +++ b/pe/py/app_cocoa.py @@ -103,7 +103,11 @@ class IPhotoLibrary(fs.Directory): subdir = subdir[element] except KeyError: subdir = fs.Directory(subdir, element) - IPhoto(subdir, photo_path) + try: + IPhoto(subdir, photo_path) + except fs.AlreadyExistsError: + # it's possible for 2 entries in the plist to point to the same path. Ignore one of them. + pass def update(self): self.clear() From fa66bea92fcfd46db8222405b0787926b509a0cc Mon Sep 17 00:00:00 2001 From: hsoft Date: Fri, 19 Jun 2009 10:39:14 +0000 Subject: [PATCH 087/275] [#36 state:fixed] Don't add pictures to dimensions2pictures if they don't have a cache_id (and moved some code around). --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4087 --- pe/py/matchbase.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pe/py/matchbase.py b/pe/py/matchbase.py index a0bebe86..93c4252c 100644 --- a/pe/py/matchbase.py +++ b/pe/py/matchbase.py @@ -95,14 +95,15 @@ class AsyncMatchFactory(MatchFactory): cache = self.cached_blocks id2picture = {} dimensions2pictures = defaultdict(set) - for picture in pictures[:]: + for picture in pictures: try: picture.cache_id = cache.get_id(picture.unicode_path) id2picture[picture.cache_id] = picture + if not self.match_scaled: + dimensions2pictures[picture.dimensions].add(picture) except ValueError: - pictures.remove(picture) - if not self.match_scaled: - dimensions2pictures[picture.dimensions].add(picture) + pass + pictures = [p for p in pictures if hasattr(p, 'cache_id')] pool = multiprocessing.Pool() async_results = [] pictures_copy = set(pictures) From 3f5a5687f9d162a142311b8da03dbb57d12e5bf3 Mon Sep 17 00:00:00 2001 From: hsoft Date: Fri, 19 Jun 2009 10:51:34 +0000 Subject: [PATCH 088/275] [#19 state:fixed] Now there are "timeout=0" args in *every* AS calls. It *has* to work. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4088 --- pe/py/app_cocoa.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pe/py/app_cocoa.py b/pe/py/app_cocoa.py index 37b54bbd..77db0208 100644 --- a/pe/py/app_cocoa.py +++ b/pe/py/app_cocoa.py @@ -150,6 +150,7 @@ class DupeGuruPE(app_cocoa.DupeGuru): if any(isinstance(dupe, IPhoto) for dupe in marked): j = j.start_subjob([6, 4], "Probing iPhoto. Don\'t touch it during the operation!") a = app('iPhoto') + a.activate(timeout=0) a.select(a.photo_library_album(timeout=0), timeout=0) photos = as_fetch(a.photo_library_album().photos, k.item) for photo in j.iter_with_progress(photos): @@ -162,7 +163,8 @@ class DupeGuruPE(app_cocoa.DupeGuru): if isinstance(dupe, IPhoto): if unicode(dupe.path) in self.path2iphoto: photo = self.path2iphoto[unicode(dupe.path)] - app('iPhoto').remove(photo) + a = app('iPhoto') + a.remove(photo, timeout=0) return True else: logging.warning("Could not find photo {0} in iPhoto Library", unicode(dupe.path)) From 8946ad02aced4598bfba03ed0d08a9a7828b6fa7 Mon Sep 17 00:00:00 2001 From: hsoft Date: Fri, 19 Jun 2009 11:14:27 +0000 Subject: [PATCH 089/275] [#34 state:fixed] qt base: Implemented "open on double click". --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4089 --- base/qt/app.py | 1 + base/qt/main_window.py | 4 ++++ base/qt/results_model.py | 4 ++++ 3 files changed, 9 insertions(+) diff --git a/base/qt/app.py b/base/qt/app.py index d621ae76..76632ee0 100644 --- a/base/qt/app.py +++ b/base/qt/app.py @@ -12,6 +12,7 @@ import traceback from PyQt4.QtCore import Qt, QTimer, QObject, QCoreApplication, QUrl, SIGNAL from PyQt4.QtGui import QProgressDialog, QDesktopServices, QFileDialog, QDialog, QMessageBox +import hsfs as fs from hsutil import job from hsutil.reg import RegistrationRequired diff --git a/base/qt/main_window.py b/base/qt/main_window.py index f79b14e0..91908b0f 100644 --- a/base/qt/main_window.py +++ b/base/qt/main_window.py @@ -39,6 +39,7 @@ class MainWindow(QMainWindow, Ui_MainWindow): self.connect(self.menuColumns, SIGNAL('triggered(QAction*)'), self.columnToggled) self.connect(QCoreApplication.instance(), SIGNAL('aboutToQuit()'), self.application_will_terminate) self.connect(self.resultsModel, SIGNAL('modelReset()'), self.resultsReset) + self.connect(self.resultsView, SIGNAL('doubleClicked()'), self.resultsDoubleClicked) def _setupUi(self): self.setupUi(self) @@ -293,6 +294,9 @@ class MainWindow(QMainWindow, Ui_MainWindow): def resultsChanged(self): self.resultsView.model().reset() + def resultsDoubleClicked(self): + self.app.open_selected() + def resultsReset(self): self.resultsView.expandAll() dupe = self.app.selected_dupe diff --git a/base/qt/results_model.py b/base/qt/results_model.py index cbffdfb8..a83e777a 100644 --- a/base/qt/results_model.py +++ b/base/qt/results_model.py @@ -189,6 +189,10 @@ class ResultsView(QTreeView): return QTreeView.keyPressEvent(self, event) + def mouseDoubleClickEvent(self, event): + self.emit(SIGNAL('doubleClicked()')) + # We don't call the superclass' method because the default behavior is to rename the cell. + def setModel(self, model): assert isinstance(model, ResultsModel) QTreeView.setModel(self, model) From 018f2a03dcb230a3e57462e13a6ffd1c518bc95f Mon Sep 17 00:00:00 2001 From: hsoft Date: Fri, 19 Jun 2009 11:32:58 +0000 Subject: [PATCH 090/275] [#35 state:fixed] base qt: Added contextual menu to the main window (the Actions menu). --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4090 --- base/qt/main_window.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/base/qt/main_window.py b/base/qt/main_window.py index 91908b0f..ba8612e7 100644 --- a/base/qt/main_window.py +++ b/base/qt/main_window.py @@ -287,6 +287,9 @@ class MainWindow(QMainWindow, Ui_MainWindow): h.setSectionHidden(colid, not h.isSectionHidden(colid)) self._update_column_actions_status() + def contextMenuEvent(self, event): + self.actionActions.menu().exec_(event.globalPos()) + def dupeMarkingChanged(self): self._redraw_results() self._update_status_line() From dd992462586b5fba9ffa5337be90071d28d5aaeb Mon Sep 17 00:00:00 2001 From: hsoft Date: Fri, 19 Jun 2009 11:44:18 +0000 Subject: [PATCH 091/275] me help: v5.6.3. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4091 --- me/help/changelog.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/me/help/changelog.yaml b/me/help/changelog.yaml index 39e03ff0..61f0a9c4 100644 --- a/me/help/changelog.yaml +++ b/me/help/changelog.yaml @@ -1,3 +1,11 @@ +- date: 2009-06-19 + version: 5.6.3 + description: | + * Fixed bugs with selection being jumpy during "Make Reference" actions and Power Marker + switches. + * Fixed crash happening when a file with non-roman characters couldn't be analyzed. + * Fixed crash sometimes happening during the file collection phase in scanning. + * Restored double-click and right-click behavior lost in the PyQt move (Windows). - date: 2009-06-10 version: 5.6.2 description: | From a864b0be0655c1b6f9fefb677cc5a8e9aa8ad704 Mon Sep 17 00:00:00 2001 From: hsoft Date: Fri, 19 Jun 2009 14:17:44 +0000 Subject: [PATCH 092/275] me cocoa: v5.6.3 --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4092 --- me/cocoa/Info.plist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/me/cocoa/Info.plist b/me/cocoa/Info.plist index 92004dd8..622bed33 100644 --- a/me/cocoa/Info.plist +++ b/me/cocoa/Info.plist @@ -23,7 +23,7 @@ CFBundleSignature hsft CFBundleVersion - 5.6.2 + 5.6.3 NSMainNibFile MainMenu NSPrincipalClass From e0cd47bd00a572eaae09fe7a24179dfe9ed5802c Mon Sep 17 00:00:00 2001 From: hsoft Date: Sat, 20 Jun 2009 09:18:13 +0000 Subject: [PATCH 093/275] me qt: v5.6.3 --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4093 --- me/qt/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/me/qt/app.py b/me/qt/app.py index c20259ab..8cf9e896 100644 --- a/me/qt/app.py +++ b/me/qt/app.py @@ -17,7 +17,7 @@ from preferences_dialog import PreferencesDialog class DupeGuru(DupeGuruBase): LOGO_NAME = 'logo_me' NAME = 'dupeGuru Music Edition' - VERSION = '5.6.2' + VERSION = '5.6.3' DELTA_COLUMNS = frozenset([2, 3, 4, 5, 7, 8]) def __init__(self): From f2b1382921c5e516e084e7e1f7502701e9d87144 Mon Sep 17 00:00:00 2001 From: hsoft Date: Sat, 20 Jun 2009 09:36:25 +0000 Subject: [PATCH 094/275] pe help: v1.7.4 --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4094 --- pe/help/changelog.yaml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pe/help/changelog.yaml b/pe/help/changelog.yaml index e324a783..149fc233 100644 --- a/pe/help/changelog.yaml +++ b/pe/help/changelog.yaml @@ -1,3 +1,12 @@ +- date: 2009-06-20 + version: 1.7.4 + description: | + * Fixed bugs with selection being jumpy during "Make Reference" actions and Power Marker + switches. + * Fixed crash happening when a file with non-roman characters couldn't be analyzed. + * Fixed crash sometimes happening during the file collection phase in scanning. + * Fixed bugs with iPhoto integration. + * Restored double-click and right-click behavior lost in the PyQt move (Windows). - date: 2009-06-07 version: 1.7.3 description: | From 9fbd840231885b47271f567caff67f5114a83107 Mon Sep 17 00:00:00 2001 From: hsoft Date: Sat, 20 Jun 2009 09:36:45 +0000 Subject: [PATCH 095/275] pe cocoa: v1.7.4 --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4095 --- pe/cocoa/Info.plist | 2 +- pe/cocoa/py/dg_cocoa.py | 2 +- pe/cocoa/py/gen.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pe/cocoa/Info.plist b/pe/cocoa/Info.plist index 3e7b42f7..9f51e9f2 100644 --- a/pe/cocoa/Info.plist +++ b/pe/cocoa/Info.plist @@ -23,7 +23,7 @@ CFBundleSignature hsft CFBundleVersion - 1.7.3 + 1.7.4 NSMainNibFile MainMenu NSPrincipalClass diff --git a/pe/cocoa/py/dg_cocoa.py b/pe/cocoa/py/dg_cocoa.py index 595fbea9..cad81108 100644 --- a/pe/cocoa/py/dg_cocoa.py +++ b/pe/cocoa/py/dg_cocoa.py @@ -6,7 +6,7 @@ from dupeguru_pe import app_cocoa as app_pe_cocoa # Fix py2app imports which chokes on relative imports from dupeguru import app, app_cocoa, data, directories, engine, export, ignore, results, scanner -from dupeguru_pe import block, cache, matchbase +from dupeguru_pe import block, cache, matchbase, data from hsfs import auto, manual, stats, tree, utils class PyApp(NSObject): diff --git a/pe/cocoa/py/gen.py b/pe/cocoa/py/gen.py index 2ca126a4..787b4f8e 100644 --- a/pe/cocoa/py/gen.py +++ b/pe/cocoa/py/gen.py @@ -15,4 +15,4 @@ if op.exists('build'): if op.exists('dist'): shutil.rmtree('dist') -print_and_do('python -u setup.py py2app -A') \ No newline at end of file +print_and_do('python -u setup.py py2app') \ No newline at end of file From 36b05ff6010bea79aeae5e2aea20293a6037c9a2 Mon Sep 17 00:00:00 2001 From: hsoft Date: Sat, 20 Jun 2009 09:44:26 +0000 Subject: [PATCH 096/275] pe qt: v1.7.4 --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4096 --- pe/qt/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pe/qt/app.py b/pe/qt/app.py index 742d4819..d601fb51 100644 --- a/pe/qt/app.py +++ b/pe/qt/app.py @@ -65,7 +65,7 @@ class Directory(phys.Directory): class DupeGuru(DupeGuruBase): LOGO_NAME = 'logo_pe' NAME = 'dupeGuru Picture Edition' - VERSION = '1.7.3' + VERSION = '1.7.4' DELTA_COLUMNS = frozenset([2, 5, 6]) def __init__(self): From 61767c04e7a11d885a36fb6acd35608d0a2feaba Mon Sep 17 00:00:00 2001 From: hsoft Date: Sat, 20 Jun 2009 15:52:45 +0000 Subject: [PATCH 097/275] se help: v2.7.3 --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4097 --- se/help/changelog.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/se/help/changelog.yaml b/se/help/changelog.yaml index c7a17838..47acc9c2 100644 --- a/se/help/changelog.yaml +++ b/se/help/changelog.yaml @@ -1,3 +1,11 @@ +- date: 2009-06-20 + version: 2.7.3 + description: | + * Fixed bugs with selection being jumpy during "Make Reference" actions and Power Marker + switches. + * Fixed crash happening when a file with non-roman characters couldn't be analyzed. + * Fixed crash sometimes happening during the file collection phase in scanning. + * Restored double-click and right-click behavior lost in the PyQt move (Windows). - date: 2009-06-10 version: 2.7.2 description: | From 7ece120bbdeacbe29499e3946829335fe5a4def6 Mon Sep 17 00:00:00 2001 From: hsoft Date: Sat, 20 Jun 2009 15:53:00 +0000 Subject: [PATCH 098/275] se cocoa: v2.7.3 --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4098 --- se/cocoa/Info.plist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/se/cocoa/Info.plist b/se/cocoa/Info.plist index 37ce5973..b7bd3c6d 100644 --- a/se/cocoa/Info.plist +++ b/se/cocoa/Info.plist @@ -23,7 +23,7 @@ CFBundleSignature hsft CFBundleVersion - 2.7.2 + 2.7.3 NSMainNibFile MainMenu NSPrincipalClass From 611b58951f2718a919960b151b199bb2b9baba7c Mon Sep 17 00:00:00 2001 From: hsoft Date: Sat, 20 Jun 2009 16:52:00 +0000 Subject: [PATCH 099/275] se qt: v2.7.3 --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%4099 --- se/qt/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/se/qt/app.py b/se/qt/app.py index daf00010..9d2f1e27 100644 --- a/se/qt/app.py +++ b/se/qt/app.py @@ -25,7 +25,7 @@ class Directories(DirectoriesBase): class DupeGuru(DupeGuruBase): LOGO_NAME = 'logo_se' NAME = 'dupeGuru' - VERSION = '2.7.2' + VERSION = '2.7.3' DELTA_COLUMNS = frozenset([2, 4, 5]) def __init__(self): From a8a601ec362eacb58746cd04c622aed6bd7d6e41 Mon Sep 17 00:00:00 2001 From: hsoft Date: Wed, 5 Aug 2009 08:59:46 +0000 Subject: [PATCH 100/275] Relicensed to HS License. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40100 --- base/cocoa/AppDelegate.h | 8 ++++++++ base/cocoa/AppDelegate.m | 8 ++++++++ base/cocoa/Consts.h | 8 ++++++++ base/cocoa/DetailsPanel.h | 8 ++++++++ base/cocoa/DetailsPanel.m | 8 ++++++++ base/cocoa/DirectoryPanel.h | 8 ++++++++ base/cocoa/DirectoryPanel.m | 8 ++++++++ base/cocoa/LICENSE | 11 +++++++++++ base/cocoa/PyDupeGuru.h | 8 ++++++++ base/cocoa/ResultWindow.h | 8 ++++++++ base/cocoa/ResultWindow.m | 8 ++++++++ base/py/LICENSE | 11 +++++++++++ base/py/app.py | 18 +++++++++--------- base/py/app_cocoa.py | 19 +++++++++---------- base/py/app_me_cocoa.py | 19 +++++++++---------- base/py/app_se_cocoa.py | 6 ++++-- base/py/data.py | 18 ++++++++---------- base/py/data_me.py | 18 ++++++++---------- base/py/directories.py | 5 ++++- base/py/engine.py | 19 +++++++++---------- base/py/export.py | 19 +++++++++---------- base/py/ignore.py | 19 +++++++++---------- base/py/results.py | 19 +++++++++---------- base/py/scanner.py | 19 +++++++++---------- base/py/tests/app_cocoa_test.py | 5 ++++- base/py/tests/app_test.py | 5 ++++- base/py/tests/directories_test.py | 5 ++++- base/py/tests/engine_test.py | 5 ++++- base/py/tests/export_test.py | 5 ++++- base/py/tests/ignore_test.py | 5 ++++- base/py/tests/results_test.py | 5 ++++- base/py/tests/scanner_test.py | 5 ++++- base/qt/LICENSE | 11 +++++++++++ base/qt/about_box.py | 6 ++++-- base/qt/app.py | 6 ++++-- base/qt/details_table.py | 6 ++++-- base/qt/directories_dialog.py | 6 ++++-- base/qt/directories_model.py | 6 ++++-- base/qt/error_report_dialog.py | 6 ++++-- base/qt/gen.py | 5 ++++- base/qt/main_window.py | 6 ++++-- base/qt/preferences.py | 6 ++++-- base/qt/reg.py | 6 ++++-- base/qt/reg_demo_dialog.py | 6 ++++-- base/qt/reg_submit_dialog.py | 6 ++++-- base/qt/results_model.py | 6 ++++-- base/qt/tree_model.py | 6 ++++-- me/cocoa/AppDelegate.h | 8 ++++++++ me/cocoa/AppDelegate.m | 8 ++++++++ me/cocoa/Consts.h | 8 ++++++++ me/cocoa/DetailsPanel.h | 8 ++++++++ me/cocoa/DetailsPanel.m | 8 ++++++++ me/cocoa/DirectoryPanel.h | 8 ++++++++ me/cocoa/DirectoryPanel.m | 8 ++++++++ me/cocoa/LICENSE | 11 +++++++++++ me/cocoa/PyDupeGuru.h | 8 ++++++++ me/cocoa/ResultWindow.h | 8 ++++++++ me/cocoa/ResultWindow.m | 8 ++++++++ me/cocoa/gen.py | 5 +++++ me/cocoa/main.m | 14 +++++++------- me/cocoa/py/dg_cocoa.py | 8 +++++++- me/cocoa/py/gen.py | 5 +++++ me/cocoa/py/setup.py | 6 +++++- me/help/LICENSE | 11 +++++++++++ me/help/gen.py | 5 +++++ me/qt/LICENSE | 11 +++++++++++ me/qt/app.py | 6 ++++-- me/qt/app_win.py | 6 ++++-- me/qt/build.py | 5 ++++- me/qt/details_dialog.py | 6 ++++-- me/qt/gen.py | 5 ++++- me/qt/preferences.py | 6 ++++-- me/qt/preferences_dialog.py | 6 ++++-- me/qt/profile.py | 7 +++++++ me/qt/start.py | 7 +++++++ pe/cocoa/AppDelegate.h | 8 ++++++++ pe/cocoa/AppDelegate.m | 8 ++++++++ pe/cocoa/Consts.h | 8 ++++++++ pe/cocoa/DetailsPanel.h | 8 ++++++++ pe/cocoa/DetailsPanel.m | 8 ++++++++ pe/cocoa/DirectoryPanel.h | 8 ++++++++ pe/cocoa/DirectoryPanel.m | 8 ++++++++ pe/cocoa/LICENSE | 11 +++++++++++ pe/cocoa/PictureBlocks.h | 8 ++++++++ pe/cocoa/PictureBlocks.m | 8 ++++++++ pe/cocoa/PyDupeGuru.h | 8 ++++++++ pe/cocoa/ResultWindow.h | 8 ++++++++ pe/cocoa/ResultWindow.m | 8 ++++++++ pe/cocoa/gen.py | 5 +++++ pe/cocoa/main.m | 14 +++++++------- pe/cocoa/py/dg_cocoa.py | 7 ++++++- pe/cocoa/py/gen.py | 5 +++++ pe/cocoa/py/setup.py | 6 +++++- pe/help/LICENSE | 11 +++++++++++ pe/help/gen.py | 5 +++++ pe/py/LICENSE | 11 +++++++++++ pe/py/app_cocoa.py | 5 ++++- pe/py/block.py | 5 ++++- pe/py/cache.py | 5 ++++- pe/py/data.py | 5 ++++- pe/py/gen.py | 5 ++++- pe/py/matchbase.py | 5 ++++- pe/py/modules/block/block.pyx | 4 ++++ pe/py/modules/block/setup.py | 5 ++++- pe/py/modules/cache/cache.pyx | 5 ++++- pe/py/modules/cache/setup.py | 5 ++++- pe/py/tests/block_test.py | 5 ++++- pe/py/tests/cache_test.py | 5 ++++- pe/qt/LICENSE | 11 +++++++++++ pe/qt/app.py | 6 ++++-- pe/qt/app_win.py | 6 ++++-- pe/qt/block.py | 6 ++++-- pe/qt/build.py | 5 ++++- pe/qt/details_dialog.py | 6 ++++-- pe/qt/gen.py | 5 ++++- pe/qt/main_window.py | 6 ++++-- pe/qt/modules/block/block.pyx | 7 +++++++ pe/qt/modules/block/setup.py | 6 +++++- pe/qt/preferences.py | 6 ++++-- pe/qt/preferences_dialog.py | 6 ++++-- pe/qt/profile.py | 7 +++++++ pe/qt/start.py | 7 +++++++ se/cocoa/AppDelegate.h | 8 ++++++++ se/cocoa/AppDelegate.m | 8 ++++++++ se/cocoa/Consts.h | 8 ++++++++ se/cocoa/DetailsPanel.h | 8 ++++++++ se/cocoa/DetailsPanel.m | 8 ++++++++ se/cocoa/DirectoryPanel.h | 8 ++++++++ se/cocoa/DirectoryPanel.m | 8 ++++++++ se/cocoa/LICENSE | 11 +++++++++++ se/cocoa/PyDupeGuru.h | 8 ++++++++ se/cocoa/ResultWindow.h | 8 ++++++++ se/cocoa/ResultWindow.m | 8 ++++++++ se/cocoa/gen.py | 5 +++++ se/cocoa/main.m | 14 +++++++------- se/cocoa/py/dg_cocoa.py | 8 +++++++- se/cocoa/py/gen.py | 5 +++++ se/cocoa/py/setup.py | 6 +++++- se/help/LICENSE | 11 +++++++++++ se/help/gen.py | 5 +++++ se/qt/LICENSE | 11 +++++++++++ se/qt/app.py | 6 ++++-- se/qt/app_win.py | 6 ++++-- se/qt/build.py | 5 ++++- se/qt/details_dialog.py | 6 ++++-- se/qt/gen.py | 5 ++++- se/qt/preferences.py | 6 ++++-- se/qt/preferences_dialog.py | 6 ++++-- se/qt/profile.py | 7 +++++++ se/qt/start.py | 7 +++++++ 150 files changed, 958 insertions(+), 216 deletions(-) create mode 100644 base/cocoa/LICENSE create mode 100644 base/py/LICENSE create mode 100644 base/qt/LICENSE create mode 100644 me/cocoa/LICENSE create mode 100644 me/help/LICENSE create mode 100644 me/qt/LICENSE create mode 100644 pe/cocoa/LICENSE create mode 100644 pe/help/LICENSE create mode 100644 pe/py/LICENSE create mode 100644 pe/qt/LICENSE create mode 100644 se/cocoa/LICENSE create mode 100644 se/help/LICENSE create mode 100644 se/qt/LICENSE diff --git a/base/cocoa/AppDelegate.h b/base/cocoa/AppDelegate.h index 055e0a67..64fd6325 100644 --- a/base/cocoa/AppDelegate.h +++ b/base/cocoa/AppDelegate.h @@ -1,3 +1,11 @@ +/* +Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +This software is licensed under the "HS" License as described in the "LICENSE" file, +which should be included with this package. The terms are also available at +http://www.hardcoded.net/licenses/hs_license +*/ + #import #import "RecentDirectories.h" #import "PyDupeGuru.h" diff --git a/base/cocoa/AppDelegate.m b/base/cocoa/AppDelegate.m index ef3d9162..cc9f0647 100644 --- a/base/cocoa/AppDelegate.m +++ b/base/cocoa/AppDelegate.m @@ -1,3 +1,11 @@ +/* +Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +This software is licensed under the "HS" License as described in the "LICENSE" file, +which should be included with this package. The terms are also available at +http://www.hardcoded.net/licenses/hs_license +*/ + #import "AppDelegate.h" #import "ProgressController.h" #import "RegistrationInterface.h" diff --git a/base/cocoa/Consts.h b/base/cocoa/Consts.h index 81e19592..1d81ae82 100644 --- a/base/cocoa/Consts.h +++ b/base/cocoa/Consts.h @@ -1,3 +1,11 @@ +/* +Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +This software is licensed under the "HS" License as described in the "LICENSE" file, +which should be included with this package. The terms are also available at +http://www.hardcoded.net/licenses/hs_license +*/ + #import #define DuplicateSelectionChangedNotification @"DuplicateSelectionChangedNotification" diff --git a/base/cocoa/DetailsPanel.h b/base/cocoa/DetailsPanel.h index ca66455a..a9965268 100644 --- a/base/cocoa/DetailsPanel.h +++ b/base/cocoa/DetailsPanel.h @@ -1,3 +1,11 @@ +/* +Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +This software is licensed under the "HS" License as described in the "LICENSE" file, +which should be included with this package. The terms are also available at +http://www.hardcoded.net/licenses/hs_license +*/ + #import #import "PyApp.h" #import "Table.h" diff --git a/base/cocoa/DetailsPanel.m b/base/cocoa/DetailsPanel.m index 2c45ef45..bf9d252f 100644 --- a/base/cocoa/DetailsPanel.m +++ b/base/cocoa/DetailsPanel.m @@ -1,3 +1,11 @@ +/* +Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +This software is licensed under the "HS" License as described in the "LICENSE" file, +which should be included with this package. The terms are also available at +http://www.hardcoded.net/licenses/hs_license +*/ + #import "DetailsPanel.h" #import "Consts.h" diff --git a/base/cocoa/DirectoryPanel.h b/base/cocoa/DirectoryPanel.h index d5738b29..a606aebc 100644 --- a/base/cocoa/DirectoryPanel.h +++ b/base/cocoa/DirectoryPanel.h @@ -1,3 +1,11 @@ +/* +Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +This software is licensed under the "HS" License as described in the "LICENSE" file, +which should be included with this package. The terms are also available at +http://www.hardcoded.net/licenses/hs_license +*/ + #import #import "RecentDirectories.h" #import "Outline.h" diff --git a/base/cocoa/DirectoryPanel.m b/base/cocoa/DirectoryPanel.m index b62b7ae8..6cc822b8 100644 --- a/base/cocoa/DirectoryPanel.m +++ b/base/cocoa/DirectoryPanel.m @@ -1,3 +1,11 @@ +/* +Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +This software is licensed under the "HS" License as described in the "LICENSE" file, +which should be included with this package. The terms are also available at +http://www.hardcoded.net/licenses/hs_license +*/ + #import "DirectoryPanel.h" #import "Dialogs.h" #import "Utils.h" diff --git a/base/cocoa/LICENSE b/base/cocoa/LICENSE new file mode 100644 index 00000000..f8818048 --- /dev/null +++ b/base/cocoa/LICENSE @@ -0,0 +1,11 @@ +Copyright 2009 Hardcoded Software Inc. (http://www.hardcoded.net) +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + * Neither the name of Hardcoded Software Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + * If the source code has been published less than two years ago, any redistribution, in whole or in part, must retain full licensing functionality, without any attempt to change, obscure or in other ways circumvent its intent. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/base/cocoa/PyDupeGuru.h b/base/cocoa/PyDupeGuru.h index 6d7ef768..140cf355 100644 --- a/base/cocoa/PyDupeGuru.h +++ b/base/cocoa/PyDupeGuru.h @@ -1,3 +1,11 @@ +/* +Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +This software is licensed under the "HS" License as described in the "LICENSE" file, +which should be included with this package. The terms are also available at +http://www.hardcoded.net/licenses/hs_license +*/ + #import #import "PyApp.h" diff --git a/base/cocoa/ResultWindow.h b/base/cocoa/ResultWindow.h index dbbb1c61..d3ea27a9 100644 --- a/base/cocoa/ResultWindow.h +++ b/base/cocoa/ResultWindow.h @@ -1,3 +1,11 @@ +/* +Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +This software is licensed under the "HS" License as described in the "LICENSE" file, +which should be included with this package. The terms are also available at +http://www.hardcoded.net/licenses/hs_license +*/ + #import #import "Outline.h" #import "DirectoryPanel.h" diff --git a/base/cocoa/ResultWindow.m b/base/cocoa/ResultWindow.m index aa4b7775..3dc9bf1b 100644 --- a/base/cocoa/ResultWindow.m +++ b/base/cocoa/ResultWindow.m @@ -1,3 +1,11 @@ +/* +Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +This software is licensed under the "HS" License as described in the "LICENSE" file, +which should be included with this package. The terms are also available at +http://www.hardcoded.net/licenses/hs_license +*/ + #import "ResultWindow.h" #import "Dialogs.h" #import "ProgressController.h" diff --git a/base/py/LICENSE b/base/py/LICENSE new file mode 100644 index 00000000..f8818048 --- /dev/null +++ b/base/py/LICENSE @@ -0,0 +1,11 @@ +Copyright 2009 Hardcoded Software Inc. (http://www.hardcoded.net) +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + * Neither the name of Hardcoded Software Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + * If the source code has been published less than two years ago, any redistribution, in whole or in part, must retain full licensing functionality, without any attempt to change, obscure or in other ways circumvent its intent. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/base/py/app.py b/base/py/app.py index 7dccb474..5c2d0379 100644 --- a/base/py/app.py +++ b/base/py/app.py @@ -1,13 +1,13 @@ #!/usr/bin/env python -""" -Unit Name: dupeguru.app -Created By: Virgil Dupras -Created On: 2006/11/11 -Last modified by:$Author: virgil $ -Last modified on:$Date: 2009-05-28 16:02:48 +0200 (Thu, 28 May 2009) $ - $Revision: 4388 $ -Copyright 2006 Hardcoded Software (http://www.hardcoded.net) -""" +# Created By: Virgil Dupras +# Created On: 2006/11/11 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license + import os import os.path as op import logging diff --git a/base/py/app_cocoa.py b/base/py/app_cocoa.py index 814dbe2d..82b547f1 100644 --- a/base/py/app_cocoa.py +++ b/base/py/app_cocoa.py @@ -1,13 +1,12 @@ -#!/usr/bin/env python -""" -Unit Name: dupeguru.app_cocoa -Created By: Virgil Dupras -Created On: 2006/11/11 -Last modified by:$Author: virgil $ -Last modified on:$Date: 2009-05-28 16:33:32 +0200 (Thu, 28 May 2009) $ - $Revision: 4392 $ -Copyright 2006 Hardcoded Software (http://www.hardcoded.net) -""" +# Created By: Virgil Dupras +# Created On: 2006/11/11 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license + from AppKit import * import logging import os.path as op diff --git a/base/py/app_me_cocoa.py b/base/py/app_me_cocoa.py index f3f7096f..c06ff42f 100644 --- a/base/py/app_me_cocoa.py +++ b/base/py/app_me_cocoa.py @@ -1,13 +1,12 @@ -#!/usr/bin/env python -""" -Unit Name: dupeguru.app_me_cocoa -Created By: Virgil Dupras -Created On: 2006/11/16 -Last modified by:$Author: virgil $ -Last modified on:$Date: 2009-05-28 16:33:32 +0200 (Thu, 28 May 2009) $ - $Revision: 4392 $ -Copyright 2006 Hardcoded Software (http://www.hardcoded.net) -""" +# Created By: Virgil Dupras +# Created On: 2006/11/16 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license + import os.path as op import logging from appscript import app, k, CommandError diff --git a/base/py/app_se_cocoa.py b/base/py/app_se_cocoa.py index a07c7613..ce7b0707 100644 --- a/base/py/app_se_cocoa.py +++ b/base/py/app_se_cocoa.py @@ -1,9 +1,11 @@ -#!/usr/bin/env python -# Unit Name: app_se_cocoa # Created By: Virgil Dupras # Created On: 2009-05-24 # $Id$ # Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license from hsfs.phys import Directory as DirectoryBase from hsfs.phys.bundle import Bundle diff --git a/base/py/data.py b/base/py/data.py index 568a3400..d0faf479 100644 --- a/base/py/data.py +++ b/base/py/data.py @@ -1,13 +1,11 @@ -#!/usr/bin/env python -""" -Unit Name: dupeguru.data -Created By: Virgil Dupras -Created On: 2006/03/15 -Last modified by:$Author: virgil $ -Last modified on:$Date: 2009-05-28 15:22:39 +0200 (Thu, 28 May 2009) $ - $Revision: 4385 $ -Copyright 2004-2006 Hardcoded Software (http://www.hardcoded.net) -""" +# Created By: Virgil Dupras +# Created On: 2006/03/15 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license from hsutil.str import format_time, FT_DECIMAL, format_size diff --git a/base/py/data_me.py b/base/py/data_me.py index 70d3ae66..b05dcf18 100644 --- a/base/py/data_me.py +++ b/base/py/data_me.py @@ -1,13 +1,11 @@ -#!/usr/bin/env python -""" -Unit Name: dupeguru.data -Created By: Virgil Dupras -Created On: 2006/03/15 -Last modified by:$Author: virgil $ -Last modified on:$Date: 2009-05-28 15:22:39 +0200 (Thu, 28 May 2009) $ - $Revision: 4385 $ -Copyright 2004-2006 Hardcoded Software (http://www.hardcoded.net) -""" +# Created By: Virgil Dupras +# Created On: 2006/03/15 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license from hsutil.str import format_time, FT_MINUTES, format_size from .data import (format_path, format_timestamp, format_words, format_perc, diff --git a/base/py/directories.py b/base/py/directories.py index f61e5d70..4ff98c55 100644 --- a/base/py/directories.py +++ b/base/py/directories.py @@ -1,8 +1,11 @@ -# Unit Name: dupeguru.directories # Created By: Virgil Dupras # Created On: 2006/02/27 # $Id$ # Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license import xml.dom.minidom diff --git a/base/py/engine.py b/base/py/engine.py index a826902d..796c8c3d 100644 --- a/base/py/engine.py +++ b/base/py/engine.py @@ -1,13 +1,12 @@ -#!/usr/bin/env python -""" -Unit Name: dupeguru.engine -Created By: Virgil Dupras -Created On: 2006/01/29 -Last modified by:$Author: virgil $ -Last modified on:$Date: $ - $Revision: $ -Copyright 2007 Hardcoded Software (http://www.hardcoded.net) -""" +# Created By: Virgil Dupras +# Created On: 2006/01/29 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license + from __future__ import division import difflib import logging diff --git a/base/py/export.py b/base/py/export.py index c6293a5d..bcac23fb 100644 --- a/base/py/export.py +++ b/base/py/export.py @@ -1,13 +1,12 @@ -#!/usr/bin/env python -""" -Unit Name: dupeguru.export -Created By: Virgil Dupras -Created On: 2006/09/16 -Last modified by:$Author: virgil $ -Last modified on:$Date: 2009-05-28 15:22:39 +0200 (Thu, 28 May 2009) $ - $Revision: 4385 $ -Copyright 2004-2006 Hardcoded Software (http://www.hardcoded.net) -""" +# Created By: Virgil Dupras +# Created On: 2006/09/16 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license + from xml.dom import minidom import tempfile import os.path as op diff --git a/base/py/ignore.py b/base/py/ignore.py index 97060786..125e9aad 100644 --- a/base/py/ignore.py +++ b/base/py/ignore.py @@ -1,13 +1,12 @@ -#!/usr/bin/env python -""" -Unit Name: ignore -Created By: Virgil Dupras -Created On: 2006/05/02 -Last modified by:$Author: virgil $ -Last modified on:$Date: 2009-05-28 15:22:39 +0200 (Thu, 28 May 2009) $ - $Revision: 4385 $ -Copyright 2004-2006 Hardcoded Software (http://www.hardcoded.net) -""" +# Created By: Virgil Dupras +# Created On: 2006/05/02 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license + from hsutil.files import FileOrPath import xml.dom.minidom diff --git a/base/py/results.py b/base/py/results.py index a7ded5c0..9e9863af 100644 --- a/base/py/results.py +++ b/base/py/results.py @@ -1,13 +1,12 @@ -#!/usr/bin/env python -""" -Unit Name: dupeguru.results -Created By: Virgil Dupras -Created On: 2006/02/23 -Last modified by:$Author: virgil $ -Last modified on:$Date: 2009-05-28 16:33:32 +0200 (Thu, 28 May 2009) $ - $Revision: 4392 $ -Copyright 2004-2006 Hardcoded Software (http://www.hardcoded.net) -""" +# Created By: Virgil Dupras +# Created On: 2006/02/23 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license + import re from xml.sax import handler, make_parser, SAXException from xml.sax.saxutils import XMLGenerator diff --git a/base/py/scanner.py b/base/py/scanner.py index 69a03b38..0ac87dc2 100644 --- a/base/py/scanner.py +++ b/base/py/scanner.py @@ -1,13 +1,12 @@ -#!/usr/bin/env python -""" -Unit Name: dupeguru.scanner -Created By: Virgil Dupras -Created On: 2006/03/03 -Last modified by:$Author: virgil $ -Last modified on:$Date: 2009-05-28 15:22:39 +0200 (Thu, 28 May 2009) $ - $Revision: 4385 $ -Copyright 2004-2006 Hardcoded Software (http://www.hardcoded.net) -""" +# Created By: Virgil Dupras +# Created On: 2006/03/03 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license + import logging from ignore import IgnoreList diff --git a/base/py/tests/app_cocoa_test.py b/base/py/tests/app_cocoa_test.py index c1ded17a..f489ac38 100644 --- a/base/py/tests/app_cocoa_test.py +++ b/base/py/tests/app_cocoa_test.py @@ -1,8 +1,11 @@ -# Unit Name: dupeguru.tests.app_cocoa_test # Created By: Virgil Dupras # Created On: 2006/11/11 # $Id$ # Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license import tempfile import shutil diff --git a/base/py/tests/app_test.py b/base/py/tests/app_test.py index fbb6afc3..c3127bdd 100644 --- a/base/py/tests/app_test.py +++ b/base/py/tests/app_test.py @@ -1,8 +1,11 @@ -# Unit Name: dupeguru.tests.app_test # Created By: Virgil Dupras # Created On: 2007-06-23 # $Id$ # Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license import os diff --git a/base/py/tests/directories_test.py b/base/py/tests/directories_test.py index 846d2b6a..80c6b817 100644 --- a/base/py/tests/directories_test.py +++ b/base/py/tests/directories_test.py @@ -1,8 +1,11 @@ -# Unit Name: dupeguru.tests.directories_test # Created By: Virgil Dupras # Created On: 2006/02/27 # $Id$ # Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license import os.path as op import os diff --git a/base/py/tests/engine_test.py b/base/py/tests/engine_test.py index bd0851ec..a9ff33b1 100644 --- a/base/py/tests/engine_test.py +++ b/base/py/tests/engine_test.py @@ -1,8 +1,11 @@ -# Unit Name: dupeguru.tests.engine_test # Created By: Virgil Dupras # Created On: 2006/01/29 # $Id$ # Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license import sys diff --git a/base/py/tests/export_test.py b/base/py/tests/export_test.py index 9469936b..447d02ed 100644 --- a/base/py/tests/export_test.py +++ b/base/py/tests/export_test.py @@ -1,8 +1,11 @@ -# Unit Name: dupeguru.tests.export_test # Created By: Virgil Dupras # Created On: 2006/09/16 # $Id$ # Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license from xml.dom import minidom from StringIO import StringIO diff --git a/base/py/tests/ignore_test.py b/base/py/tests/ignore_test.py index b19283d4..73dac5c8 100644 --- a/base/py/tests/ignore_test.py +++ b/base/py/tests/ignore_test.py @@ -1,8 +1,11 @@ -# Unit Name: dupeguru.tests.ignore_test # Created By: Virgil Dupras # Created On: 2006/05/02 # $Id$ # Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license import cStringIO import xml.dom.minidom diff --git a/base/py/tests/results_test.py b/base/py/tests/results_test.py index 096e9d4e..a6d9f05a 100644 --- a/base/py/tests/results_test.py +++ b/base/py/tests/results_test.py @@ -1,8 +1,11 @@ -# Unit Name: dupeguru.tests.results_test # Created By: Virgil Dupras # Created On: 2006/02/23 # $Id$ # Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license import unittest import StringIO diff --git a/base/py/tests/scanner_test.py b/base/py/tests/scanner_test.py index 8a4510a1..8cd9587a 100644 --- a/base/py/tests/scanner_test.py +++ b/base/py/tests/scanner_test.py @@ -1,8 +1,11 @@ -# Unit Name: dupeguru.tests.scanner_test # Created By: Virgil Dupras # Created On: 2006/03/03 # $Id$ # Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license from hsutil import job from hsutil.path import Path diff --git a/base/qt/LICENSE b/base/qt/LICENSE new file mode 100644 index 00000000..f8818048 --- /dev/null +++ b/base/qt/LICENSE @@ -0,0 +1,11 @@ +Copyright 2009 Hardcoded Software Inc. (http://www.hardcoded.net) +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + * Neither the name of Hardcoded Software Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + * If the source code has been published less than two years ago, any redistribution, in whole or in part, must retain full licensing functionality, without any attempt to change, obscure or in other ways circumvent its intent. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/base/qt/about_box.py b/base/qt/about_box.py index 55a36eb1..c72a7dfb 100644 --- a/base/qt/about_box.py +++ b/base/qt/about_box.py @@ -1,9 +1,11 @@ -#!/usr/bin/env python -# Unit Name: about_box # Created By: Virgil Dupras # Created On: 2009-05-09 # $Id$ # Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license from PyQt4.QtCore import Qt, QCoreApplication, SIGNAL from PyQt4.QtGui import QDialog, QDialogButtonBox, QPixmap diff --git a/base/qt/app.py b/base/qt/app.py index 76632ee0..f8e9d180 100644 --- a/base/qt/app.py +++ b/base/qt/app.py @@ -1,9 +1,11 @@ -#!/usr/bin/env python -# Unit Name: app # Created By: Virgil Dupras # Created On: 2009-04-25 # $Id$ # Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license import logging import os.path as op diff --git a/base/qt/details_table.py b/base/qt/details_table.py index 0a27c2ae..7e9b5c5c 100644 --- a/base/qt/details_table.py +++ b/base/qt/details_table.py @@ -1,9 +1,11 @@ -#!/usr/bin/env python -# Unit Name: details_table # Created By: Virgil Dupras # Created On: 2009-05-17 # $Id$ # Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license from PyQt4.QtCore import Qt, SIGNAL, QAbstractTableModel, QVariant from PyQt4.QtGui import QHeaderView, QTableView diff --git a/base/qt/directories_dialog.py b/base/qt/directories_dialog.py index 9a559d45..a2c36a3a 100644 --- a/base/qt/directories_dialog.py +++ b/base/qt/directories_dialog.py @@ -1,9 +1,11 @@ -#!/usr/bin/env python -# Unit Name: directories_dialog # Created By: Virgil Dupras # Created On: 2009-04-25 # $Id$ # Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license from PyQt4.QtCore import SIGNAL, Qt from PyQt4.QtGui import QDialog, QFileDialog, QHeaderView diff --git a/base/qt/directories_model.py b/base/qt/directories_model.py index f66f798d..bf2f86e3 100644 --- a/base/qt/directories_model.py +++ b/base/qt/directories_model.py @@ -1,9 +1,11 @@ -#!/usr/bin/env python -# Unit Name: directories_model # Created By: Virgil Dupras # Created On: 2009-04-25 # $Id$ # Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license from PyQt4.QtCore import QVariant, QModelIndex, Qt, QRect, QEvent, QPoint from PyQt4.QtGui import QComboBox, QStyledItemDelegate, QMouseEvent, QApplication, QBrush diff --git a/base/qt/error_report_dialog.py b/base/qt/error_report_dialog.py index 4aa8f977..ec4a5677 100644 --- a/base/qt/error_report_dialog.py +++ b/base/qt/error_report_dialog.py @@ -1,9 +1,11 @@ -#!/usr/bin/env python -# Unit Name: error_report_dialog # Created By: Virgil Dupras # Created On: 2009-05-23 # $Id$ # Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license from PyQt4.QtCore import Qt, QUrl from PyQt4.QtGui import QDialog, QDesktopServices diff --git a/base/qt/gen.py b/base/qt/gen.py index 3b0df2fa..2843e82e 100644 --- a/base/qt/gen.py +++ b/base/qt/gen.py @@ -1,9 +1,12 @@ #!/usr/bin/env python -# Unit Name: gen # Created By: Virgil Dupras # Created On: 2009-05-22 # $Id$ # Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license import os diff --git a/base/qt/main_window.py b/base/qt/main_window.py index ba8612e7..e4218caf 100644 --- a/base/qt/main_window.py +++ b/base/qt/main_window.py @@ -1,9 +1,11 @@ -#!/usr/bin/env python -# Unit Name: main_window # Created By: Virgil Dupras # Created On: 2009-04-25 # $Id$ # Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license from PyQt4.QtCore import Qt, QCoreApplication, QProcess, SIGNAL from PyQt4.QtGui import (QMainWindow, QMenu, QPixmap, QIcon, QToolButton, QLabel, QHeaderView, diff --git a/base/qt/preferences.py b/base/qt/preferences.py index ab7653b9..c6d2d9aa 100644 --- a/base/qt/preferences.py +++ b/base/qt/preferences.py @@ -1,9 +1,11 @@ -#!/usr/bin/env python -# Unit Name: preferences # Created By: Virgil Dupras # Created On: 2009-05-03 # $Id$ # Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license from PyQt4.QtCore import QSettings, QVariant diff --git a/base/qt/reg.py b/base/qt/reg.py index 59fd0bc3..79653f08 100644 --- a/base/qt/reg.py +++ b/base/qt/reg.py @@ -1,9 +1,11 @@ -#!/usr/bin/env python -# Unit Name: reg # Created By: Virgil Dupras # Created On: 2009-05-09 # $Id$ # Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license from hashlib import md5 diff --git a/base/qt/reg_demo_dialog.py b/base/qt/reg_demo_dialog.py index 95280314..a038f7c5 100644 --- a/base/qt/reg_demo_dialog.py +++ b/base/qt/reg_demo_dialog.py @@ -1,9 +1,11 @@ -#!/usr/bin/env python -# Unit Name: reg_demo_dialog # Created By: Virgil Dupras # Created On: 2009-05-10 # $Id$ # Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license from PyQt4.QtCore import SIGNAL, Qt, QUrl, QCoreApplication from PyQt4.QtGui import QDialog, QMessageBox, QDesktopServices diff --git a/base/qt/reg_submit_dialog.py b/base/qt/reg_submit_dialog.py index 4ba680b6..2befc08f 100644 --- a/base/qt/reg_submit_dialog.py +++ b/base/qt/reg_submit_dialog.py @@ -1,9 +1,11 @@ -#!/usr/bin/env python -# Unit Name: reg_submit_dialog # Created By: Virgil Dupras # Created On: 2009-05-09 # $Id$ # Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license from PyQt4.QtCore import SIGNAL, Qt, QUrl, QCoreApplication from PyQt4.QtGui import QDialog, QMessageBox, QDesktopServices diff --git a/base/qt/results_model.py b/base/qt/results_model.py index a83e777a..004fca10 100644 --- a/base/qt/results_model.py +++ b/base/qt/results_model.py @@ -1,9 +1,11 @@ -#!/usr/bin/env python -# Unit Name: # Created By: Virgil Dupras # Created On: 2009-04-23 # $Id$ # Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license from PyQt4.QtCore import SIGNAL, Qt, QAbstractItemModel, QVariant, QModelIndex, QRect from PyQt4.QtGui import QBrush, QStyledItemDelegate, QFont, QTreeView, QColor diff --git a/base/qt/tree_model.py b/base/qt/tree_model.py index b3a994b3..d102fac7 100644 --- a/base/qt/tree_model.py +++ b/base/qt/tree_model.py @@ -1,9 +1,11 @@ -#!/usr/bin/env python -# Unit Name: tree_model # Created By: Virgil Dupras # Created On: 2009-05-04 # $Id$ # Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license from PyQt4.QtCore import Qt, QAbstractItemModel, QVariant, QModelIndex diff --git a/me/cocoa/AppDelegate.h b/me/cocoa/AppDelegate.h index 89fb5b0f..3b5388cf 100644 --- a/me/cocoa/AppDelegate.h +++ b/me/cocoa/AppDelegate.h @@ -1,3 +1,11 @@ +/* +Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +This software is licensed under the "HS" License as described in the "LICENSE" file, +which should be included with this package. The terms are also available at +http://www.hardcoded.net/licenses/hs_license +*/ + #import #import "dgbase/AppDelegate.h" #import "ResultWindow.h" diff --git a/me/cocoa/AppDelegate.m b/me/cocoa/AppDelegate.m index 804999de..db74b01f 100644 --- a/me/cocoa/AppDelegate.m +++ b/me/cocoa/AppDelegate.m @@ -1,3 +1,11 @@ +/* +Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +This software is licensed under the "HS" License as described in the "LICENSE" file, +which should be included with this package. The terms are also available at +http://www.hardcoded.net/licenses/hs_license +*/ + #import "AppDelegate.h" #import "cocoalib/ProgressController.h" #import "cocoalib/RegistrationInterface.h" diff --git a/me/cocoa/Consts.h b/me/cocoa/Consts.h index 94d203f7..ebc7c1f3 100644 --- a/me/cocoa/Consts.h +++ b/me/cocoa/Consts.h @@ -1,3 +1,11 @@ +/* +Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +This software is licensed under the "HS" License as described in the "LICENSE" file, +which should be included with this package. The terms are also available at +http://www.hardcoded.net/licenses/hs_license +*/ + #import "dgbase/Consts.h" #define APPNAME @"dupeGuru ME" diff --git a/me/cocoa/DetailsPanel.h b/me/cocoa/DetailsPanel.h index 07779bac..eca22c12 100644 --- a/me/cocoa/DetailsPanel.h +++ b/me/cocoa/DetailsPanel.h @@ -1,3 +1,11 @@ +/* +Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +This software is licensed under the "HS" License as described in the "LICENSE" file, +which should be included with this package. The terms are also available at +http://www.hardcoded.net/licenses/hs_license +*/ + #import #import "dgbase/DetailsPanel.h" diff --git a/me/cocoa/DetailsPanel.m b/me/cocoa/DetailsPanel.m index e676aef9..77e0a35e 100644 --- a/me/cocoa/DetailsPanel.m +++ b/me/cocoa/DetailsPanel.m @@ -1,3 +1,11 @@ +/* +Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +This software is licensed under the "HS" License as described in the "LICENSE" file, +which should be included with this package. The terms are also available at +http://www.hardcoded.net/licenses/hs_license +*/ + #import "DetailsPanel.h" @implementation DetailsPanel diff --git a/me/cocoa/DirectoryPanel.h b/me/cocoa/DirectoryPanel.h index 86b00388..536258af 100644 --- a/me/cocoa/DirectoryPanel.h +++ b/me/cocoa/DirectoryPanel.h @@ -1,3 +1,11 @@ +/* +Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +This software is licensed under the "HS" License as described in the "LICENSE" file, +which should be included with this package. The terms are also available at +http://www.hardcoded.net/licenses/hs_license +*/ + #import #import "dgbase/DirectoryPanel.h" diff --git a/me/cocoa/DirectoryPanel.m b/me/cocoa/DirectoryPanel.m index 0f9831ab..00f35f56 100644 --- a/me/cocoa/DirectoryPanel.m +++ b/me/cocoa/DirectoryPanel.m @@ -1,3 +1,11 @@ +/* +Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +This software is licensed under the "HS" License as described in the "LICENSE" file, +which should be included with this package. The terms are also available at +http://www.hardcoded.net/licenses/hs_license +*/ + #import "DirectoryPanel.h" @implementation DirectoryPanel diff --git a/me/cocoa/LICENSE b/me/cocoa/LICENSE new file mode 100644 index 00000000..f8818048 --- /dev/null +++ b/me/cocoa/LICENSE @@ -0,0 +1,11 @@ +Copyright 2009 Hardcoded Software Inc. (http://www.hardcoded.net) +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + * Neither the name of Hardcoded Software Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + * If the source code has been published less than two years ago, any redistribution, in whole or in part, must retain full licensing functionality, without any attempt to change, obscure or in other ways circumvent its intent. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/me/cocoa/PyDupeGuru.h b/me/cocoa/PyDupeGuru.h index 04dfce20..1d45ad3a 100644 --- a/me/cocoa/PyDupeGuru.h +++ b/me/cocoa/PyDupeGuru.h @@ -1,3 +1,11 @@ +/* +Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +This software is licensed under the "HS" License as described in the "LICENSE" file, +which should be included with this package. The terms are also available at +http://www.hardcoded.net/licenses/hs_license +*/ + #import #import "dgbase/PyDupeGuru.h" diff --git a/me/cocoa/ResultWindow.h b/me/cocoa/ResultWindow.h index 5b196907..a44959d4 100644 --- a/me/cocoa/ResultWindow.h +++ b/me/cocoa/ResultWindow.h @@ -1,3 +1,11 @@ +/* +Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +This software is licensed under the "HS" License as described in the "LICENSE" file, +which should be included with this package. The terms are also available at +http://www.hardcoded.net/licenses/hs_license +*/ + #import #import "cocoalib/Outline.h" #import "dgbase/ResultWindow.h" diff --git a/me/cocoa/ResultWindow.m b/me/cocoa/ResultWindow.m index 9887111d..31d1dc25 100644 --- a/me/cocoa/ResultWindow.m +++ b/me/cocoa/ResultWindow.m @@ -1,3 +1,11 @@ +/* +Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +This software is licensed under the "HS" License as described in the "LICENSE" file, +which should be included with this package. The terms are also available at +http://www.hardcoded.net/licenses/hs_license +*/ + #import "ResultWindow.h" #import "cocoalib/Dialogs.h" #import "cocoalib/ProgressController.h" diff --git a/me/cocoa/gen.py b/me/cocoa/gen.py index 45ae1e20..ad9be98e 100644 --- a/me/cocoa/gen.py +++ b/me/cocoa/gen.py @@ -1,4 +1,9 @@ #!/usr/bin/env python +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license import os diff --git a/me/cocoa/main.m b/me/cocoa/main.m index c5f30658..2f1187a0 100644 --- a/me/cocoa/main.m +++ b/me/cocoa/main.m @@ -1,10 +1,10 @@ -// -// main.m -// dupeguru -// -// Created by Virgil Dupras on 2006/02/01. -// Copyright __MyCompanyName__ 2006. All rights reserved. -// +/* +Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +This software is licensed under the "HS" License as described in the "LICENSE" file, +which should be included with this package. The terms are also available at +http://www.hardcoded.net/licenses/hs_license +*/ #import diff --git a/me/cocoa/py/dg_cocoa.py b/me/cocoa/py/dg_cocoa.py index db6af459..3f799c39 100644 --- a/me/cocoa/py/dg_cocoa.py +++ b/me/cocoa/py/dg_cocoa.py @@ -1,4 +1,10 @@ -#!/usr/bin/env python +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license + import objc from AppKit import * diff --git a/me/cocoa/py/gen.py b/me/cocoa/py/gen.py index 997214dd..d4991df3 100644 --- a/me/cocoa/py/gen.py +++ b/me/cocoa/py/gen.py @@ -1,4 +1,9 @@ #!/usr/bin/env python +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license import os import os.path as op diff --git a/me/cocoa/py/setup.py b/me/cocoa/py/setup.py index af81b3ed..c589447a 100644 --- a/me/cocoa/py/setup.py +++ b/me/cocoa/py/setup.py @@ -1,4 +1,8 @@ -#!/usr/bin/env python +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license from distutils.core import setup import py2app diff --git a/me/help/LICENSE b/me/help/LICENSE new file mode 100644 index 00000000..f8818048 --- /dev/null +++ b/me/help/LICENSE @@ -0,0 +1,11 @@ +Copyright 2009 Hardcoded Software Inc. (http://www.hardcoded.net) +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + * Neither the name of Hardcoded Software Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + * If the source code has been published less than two years ago, any redistribution, in whole or in part, must retain full licensing functionality, without any attempt to change, obscure or in other ways circumvent its intent. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/me/help/gen.py b/me/help/gen.py index 9b74a83a..eb45cfbb 100644 --- a/me/help/gen.py +++ b/me/help/gen.py @@ -1,4 +1,9 @@ #!/usr/bin/env python +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license import os diff --git a/me/qt/LICENSE b/me/qt/LICENSE new file mode 100644 index 00000000..f8818048 --- /dev/null +++ b/me/qt/LICENSE @@ -0,0 +1,11 @@ +Copyright 2009 Hardcoded Software Inc. (http://www.hardcoded.net) +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + * Neither the name of Hardcoded Software Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + * If the source code has been published less than two years ago, any redistribution, in whole or in part, must retain full licensing functionality, without any attempt to change, obscure or in other ways circumvent its intent. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/me/qt/app.py b/me/qt/app.py index 8cf9e896..53dbb174 100644 --- a/me/qt/app.py +++ b/me/qt/app.py @@ -1,9 +1,11 @@ -#!/usr/bin/env python -# Unit Name: app # Created By: Virgil Dupras # Created On: 2009-05-21 # $Id$ # Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license import hsfs.phys.music diff --git a/me/qt/app_win.py b/me/qt/app_win.py index 697f8335..a096fdf8 100644 --- a/me/qt/app_win.py +++ b/me/qt/app_win.py @@ -1,9 +1,11 @@ -#!/usr/bin/env python -# Unit Name: app_win # Created By: Virgil Dupras # Created On: 2009-05-21 # $Id$ # Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license import winshell diff --git a/me/qt/build.py b/me/qt/build.py index 6e7e8b5f..95ce6b9f 100644 --- a/me/qt/build.py +++ b/me/qt/build.py @@ -1,9 +1,12 @@ #!/usr/bin/env python -# Unit Name: build # Created By: Virgil Dupras # Created On: 2009-05-22 # $Id$ # Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license # On Windows, PyInstaller is used to build an exe (py2exe creates a very bad looking icon) # The release version is outdated. Use at least r672 on http://svn.pyinstaller.org/trunk diff --git a/me/qt/details_dialog.py b/me/qt/details_dialog.py index b680309c..0bf88d01 100644 --- a/me/qt/details_dialog.py +++ b/me/qt/details_dialog.py @@ -1,9 +1,11 @@ -#!/usr/bin/env python -# Unit Name: details_dialog # Created By: Virgil Dupras # Created On: 2009-04-27 # $Id$ # Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license from PyQt4.QtCore import Qt from PyQt4.QtGui import QDialog diff --git a/me/qt/gen.py b/me/qt/gen.py index 618100a6..00d60204 100644 --- a/me/qt/gen.py +++ b/me/qt/gen.py @@ -1,9 +1,12 @@ #!/usr/bin/env python -# Unit Name: gen # Created By: Virgil Dupras # Created On: 2009-05-22 # $Id$ # Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license import os diff --git a/me/qt/preferences.py b/me/qt/preferences.py index bdb4c914..43bf1306 100644 --- a/me/qt/preferences.py +++ b/me/qt/preferences.py @@ -1,9 +1,11 @@ -#!/usr/bin/env python -# Unit Name: preferences # Created By: Virgil Dupras # Created On: 2009-05-17 # $Id$ # Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license from dupeguru.scanner import (SCAN_TYPE_FILENAME, SCAN_TYPE_FIELDS, SCAN_TYPE_FIELDS_NO_ORDER, SCAN_TYPE_TAG, SCAN_TYPE_CONTENT, SCAN_TYPE_CONTENT_AUDIO) diff --git a/me/qt/preferences_dialog.py b/me/qt/preferences_dialog.py index 0ec53364..424314af 100644 --- a/me/qt/preferences_dialog.py +++ b/me/qt/preferences_dialog.py @@ -1,9 +1,11 @@ -#!/usr/bin/env python -# Unit Name: preferences_dialog # Created By: Virgil Dupras # Created On: 2009-04-29 # $Id$ # Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license from PyQt4.QtCore import SIGNAL, Qt from PyQt4.QtGui import QDialog, QDialogButtonBox diff --git a/me/qt/profile.py b/me/qt/profile.py index 195b1775..0b80126c 100644 --- a/me/qt/profile.py +++ b/me/qt/profile.py @@ -1,3 +1,10 @@ +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license + import sys import cProfile import pstats diff --git a/me/qt/start.py b/me/qt/start.py index d4315875..ca0783ea 100644 --- a/me/qt/start.py +++ b/me/qt/start.py @@ -1,3 +1,10 @@ +#!/usr/bin/env python +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license + import sys from PyQt4.QtCore import QCoreApplication diff --git a/pe/cocoa/AppDelegate.h b/pe/cocoa/AppDelegate.h index 70203dfb..498281c4 100644 --- a/pe/cocoa/AppDelegate.h +++ b/pe/cocoa/AppDelegate.h @@ -1,3 +1,11 @@ +/* +Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +This software is licensed under the "HS" License as described in the "LICENSE" file, +which should be included with this package. The terms are also available at +http://www.hardcoded.net/licenses/hs_license +*/ + #import #import "dgbase/AppDelegate.h" #import "ResultWindow.h" diff --git a/pe/cocoa/AppDelegate.m b/pe/cocoa/AppDelegate.m index 0a515656..fdb5cb45 100644 --- a/pe/cocoa/AppDelegate.m +++ b/pe/cocoa/AppDelegate.m @@ -1,3 +1,11 @@ +/* +Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +This software is licensed under the "HS" License as described in the "LICENSE" file, +which should be included with this package. The terms are also available at +http://www.hardcoded.net/licenses/hs_license +*/ + #import "AppDelegate.h" #import "ProgressController.h" #import "RegistrationInterface.h" diff --git a/pe/cocoa/Consts.h b/pe/cocoa/Consts.h index f8f8bc8a..badc3600 100644 --- a/pe/cocoa/Consts.h +++ b/pe/cocoa/Consts.h @@ -1,3 +1,11 @@ +/* +Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +This software is licensed under the "HS" License as described in the "LICENSE" file, +which should be included with this package. The terms are also available at +http://www.hardcoded.net/licenses/hs_license +*/ + #import "dgbase/Consts.h" #define APPNAME @"dupeGuru PE" diff --git a/pe/cocoa/DetailsPanel.h b/pe/cocoa/DetailsPanel.h index d7dbcaa8..7f43535f 100644 --- a/pe/cocoa/DetailsPanel.h +++ b/pe/cocoa/DetailsPanel.h @@ -1,3 +1,11 @@ +/* +Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +This software is licensed under the "HS" License as described in the "LICENSE" file, +which should be included with this package. The terms are also available at +http://www.hardcoded.net/licenses/hs_license +*/ + #import #import "dgbase/DetailsPanel.h" diff --git a/pe/cocoa/DetailsPanel.m b/pe/cocoa/DetailsPanel.m index 809c565c..c26384fe 100644 --- a/pe/cocoa/DetailsPanel.m +++ b/pe/cocoa/DetailsPanel.m @@ -1,3 +1,11 @@ +/* +Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +This software is licensed under the "HS" License as described in the "LICENSE" file, +which should be included with this package. The terms are also available at +http://www.hardcoded.net/licenses/hs_license +*/ + #import "Utils.h" #import "NSNotificationAdditions.h" #import "NSImageAdditions.h" diff --git a/pe/cocoa/DirectoryPanel.h b/pe/cocoa/DirectoryPanel.h index 6033367f..c57b8140 100644 --- a/pe/cocoa/DirectoryPanel.h +++ b/pe/cocoa/DirectoryPanel.h @@ -1,3 +1,11 @@ +/* +Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +This software is licensed under the "HS" License as described in the "LICENSE" file, +which should be included with this package. The terms are also available at +http://www.hardcoded.net/licenses/hs_license +*/ + #import #import "dgbase/DirectoryPanel.h" diff --git a/pe/cocoa/DirectoryPanel.m b/pe/cocoa/DirectoryPanel.m index 9347ddd5..a0074820 100644 --- a/pe/cocoa/DirectoryPanel.m +++ b/pe/cocoa/DirectoryPanel.m @@ -1,3 +1,11 @@ +/* +Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +This software is licensed under the "HS" License as described in the "LICENSE" file, +which should be included with this package. The terms are also available at +http://www.hardcoded.net/licenses/hs_license +*/ + #import "DirectoryPanel.h" #import "ProgressController.h" diff --git a/pe/cocoa/LICENSE b/pe/cocoa/LICENSE new file mode 100644 index 00000000..f8818048 --- /dev/null +++ b/pe/cocoa/LICENSE @@ -0,0 +1,11 @@ +Copyright 2009 Hardcoded Software Inc. (http://www.hardcoded.net) +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + * Neither the name of Hardcoded Software Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + * If the source code has been published less than two years ago, any redistribution, in whole or in part, must retain full licensing functionality, without any attempt to change, obscure or in other ways circumvent its intent. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/pe/cocoa/PictureBlocks.h b/pe/cocoa/PictureBlocks.h index 6b1a5160..8a230e06 100644 --- a/pe/cocoa/PictureBlocks.h +++ b/pe/cocoa/PictureBlocks.h @@ -1,3 +1,11 @@ +/* +Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +This software is licensed under the "HS" License as described in the "LICENSE" file, +which should be included with this package. The terms are also available at +http://www.hardcoded.net/licenses/hs_license +*/ + #import diff --git a/pe/cocoa/PictureBlocks.m b/pe/cocoa/PictureBlocks.m index bc743694..007ff246 100644 --- a/pe/cocoa/PictureBlocks.m +++ b/pe/cocoa/PictureBlocks.m @@ -1,3 +1,11 @@ +/* +Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +This software is licensed under the "HS" License as described in the "LICENSE" file, +which should be included with this package. The terms are also available at +http://www.hardcoded.net/licenses/hs_license +*/ + #import "PictureBlocks.h" #import "Utils.h" diff --git a/pe/cocoa/PyDupeGuru.h b/pe/cocoa/PyDupeGuru.h index 8ced12b6..89191538 100644 --- a/pe/cocoa/PyDupeGuru.h +++ b/pe/cocoa/PyDupeGuru.h @@ -1,3 +1,11 @@ +/* +Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +This software is licensed under the "HS" License as described in the "LICENSE" file, +which should be included with this package. The terms are also available at +http://www.hardcoded.net/licenses/hs_license +*/ + #import #import "dgbase/PyDupeGuru.h" diff --git a/pe/cocoa/ResultWindow.h b/pe/cocoa/ResultWindow.h index 941164f0..a4add0c9 100644 --- a/pe/cocoa/ResultWindow.h +++ b/pe/cocoa/ResultWindow.h @@ -1,3 +1,11 @@ +/* +Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +This software is licensed under the "HS" License as described in the "LICENSE" file, +which should be included with this package. The terms are also available at +http://www.hardcoded.net/licenses/hs_license +*/ + #import #import "Outline.h" #import "dgbase/ResultWindow.h" diff --git a/pe/cocoa/ResultWindow.m b/pe/cocoa/ResultWindow.m index 886d34e0..cab5701f 100644 --- a/pe/cocoa/ResultWindow.m +++ b/pe/cocoa/ResultWindow.m @@ -1,3 +1,11 @@ +/* +Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +This software is licensed under the "HS" License as described in the "LICENSE" file, +which should be included with this package. The terms are also available at +http://www.hardcoded.net/licenses/hs_license +*/ + #import "ResultWindow.h" #import "Dialogs.h" #import "ProgressController.h" diff --git a/pe/cocoa/gen.py b/pe/cocoa/gen.py index 69d7125d..06c73e35 100644 --- a/pe/cocoa/gen.py +++ b/pe/cocoa/gen.py @@ -1,4 +1,9 @@ #!/usr/bin/env python +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license import os diff --git a/pe/cocoa/main.m b/pe/cocoa/main.m index c5f30658..2f1187a0 100644 --- a/pe/cocoa/main.m +++ b/pe/cocoa/main.m @@ -1,10 +1,10 @@ -// -// main.m -// dupeguru -// -// Created by Virgil Dupras on 2006/02/01. -// Copyright __MyCompanyName__ 2006. All rights reserved. -// +/* +Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +This software is licensed under the "HS" License as described in the "LICENSE" file, +which should be included with this package. The terms are also available at +http://www.hardcoded.net/licenses/hs_license +*/ #import diff --git a/pe/cocoa/py/dg_cocoa.py b/pe/cocoa/py/dg_cocoa.py index cad81108..2c0accfa 100644 --- a/pe/cocoa/py/dg_cocoa.py +++ b/pe/cocoa/py/dg_cocoa.py @@ -1,4 +1,9 @@ -#!/usr/bin/env python +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license + import objc from AppKit import * diff --git a/pe/cocoa/py/gen.py b/pe/cocoa/py/gen.py index 787b4f8e..bd44475f 100644 --- a/pe/cocoa/py/gen.py +++ b/pe/cocoa/py/gen.py @@ -1,4 +1,9 @@ #!/usr/bin/env python +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license import os import os.path as op diff --git a/pe/cocoa/py/setup.py b/pe/cocoa/py/setup.py index af81b3ed..c589447a 100644 --- a/pe/cocoa/py/setup.py +++ b/pe/cocoa/py/setup.py @@ -1,4 +1,8 @@ -#!/usr/bin/env python +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license from distutils.core import setup import py2app diff --git a/pe/help/LICENSE b/pe/help/LICENSE new file mode 100644 index 00000000..f8818048 --- /dev/null +++ b/pe/help/LICENSE @@ -0,0 +1,11 @@ +Copyright 2009 Hardcoded Software Inc. (http://www.hardcoded.net) +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + * Neither the name of Hardcoded Software Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + * If the source code has been published less than two years ago, any redistribution, in whole or in part, must retain full licensing functionality, without any attempt to change, obscure or in other ways circumvent its intent. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/pe/help/gen.py b/pe/help/gen.py index 620f02eb..d1fa2ce3 100644 --- a/pe/help/gen.py +++ b/pe/help/gen.py @@ -1,4 +1,9 @@ #!/usr/bin/env python +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license import os diff --git a/pe/py/LICENSE b/pe/py/LICENSE new file mode 100644 index 00000000..f8818048 --- /dev/null +++ b/pe/py/LICENSE @@ -0,0 +1,11 @@ +Copyright 2009 Hardcoded Software Inc. (http://www.hardcoded.net) +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + * Neither the name of Hardcoded Software Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + * If the source code has been published less than two years ago, any redistribution, in whole or in part, must retain full licensing functionality, without any attempt to change, obscure or in other ways circumvent its intent. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/pe/py/app_cocoa.py b/pe/py/app_cocoa.py index 77db0208..5bc08923 100644 --- a/pe/py/app_cocoa.py +++ b/pe/py/app_cocoa.py @@ -1,8 +1,11 @@ -# Unit Name: dupeguru_pe.app_cocoa # Created By: Virgil Dupras # Created On: 2006/11/13 # $Id$ # Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license import os import os.path as op diff --git a/pe/py/block.py b/pe/py/block.py index a9aaa8d4..4189f6bd 100644 --- a/pe/py/block.py +++ b/pe/py/block.py @@ -1,8 +1,11 @@ -# Unit Name: dupeguru_pe.block # Created By: Virgil Dupras # Created On: 2006/09/01 # $Id$ # Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license from _block import NoBlocksError, DifferentBlockCountError, avgdiff, getblocks2 diff --git a/pe/py/cache.py b/pe/py/cache.py index 428eedcd..22616b4a 100644 --- a/pe/py/cache.py +++ b/pe/py/cache.py @@ -1,8 +1,11 @@ -# Unit Name: dupeguru_pe.cache # Created By: Virgil Dupras # Created On: 2006/09/14 # $Id$ # Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license import os import logging diff --git a/pe/py/data.py b/pe/py/data.py index cd35a902..5ad691c4 100644 --- a/pe/py/data.py +++ b/pe/py/data.py @@ -1,8 +1,11 @@ -# Unit Name: dupeguru_pe.data # Created By: Virgil Dupras # Created On: 2006/03/15 # $Id$ # Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license from hsutil.str import format_size from dupeguru.data import format_path, format_timestamp, format_perc, format_dupe_count, cmp_value diff --git a/pe/py/gen.py b/pe/py/gen.py index 2c86d9e7..d08a9c04 100644 --- a/pe/py/gen.py +++ b/pe/py/gen.py @@ -1,9 +1,12 @@ #!/usr/bin/env python -# Unit Name: gen # Created By: Virgil Dupras # Created On: 2009-05-26 # $Id$ # Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license import os import os.path as op diff --git a/pe/py/matchbase.py b/pe/py/matchbase.py index 93c4252c..d1f208d9 100644 --- a/pe/py/matchbase.py +++ b/pe/py/matchbase.py @@ -1,8 +1,11 @@ -# Unit Name: dupeguru_pe.matchbase # Created By: Virgil Dupras # Created On: 2007/02/25 # $Id$ # Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license import logging import multiprocessing diff --git a/pe/py/modules/block/block.pyx b/pe/py/modules/block/block.pyx index db4c7500..a33708ba 100644 --- a/pe/py/modules/block/block.pyx +++ b/pe/py/modules/block/block.pyx @@ -2,6 +2,10 @@ # Created On: 2009-04-23 # $Id$ # Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license cdef extern from "stdlib.h": int abs(int n) # required so that abs() is applied on ints, not python objects diff --git a/pe/py/modules/block/setup.py b/pe/py/modules/block/setup.py index 9d8f4cb5..0f22c490 100644 --- a/pe/py/modules/block/setup.py +++ b/pe/py/modules/block/setup.py @@ -1,8 +1,11 @@ -#!/usr/bin/env python # Created By: Virgil Dupras # Created On: 2009-04-23 # $Id$ # Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license from distutils.core import setup from distutils.extension import Extension diff --git a/pe/py/modules/cache/cache.pyx b/pe/py/modules/cache/cache.pyx index 7bd2407d..4b3fd19a 100644 --- a/pe/py/modules/cache/cache.pyx +++ b/pe/py/modules/cache/cache.pyx @@ -1,8 +1,11 @@ -#!/usr/bin/env python # Created By: Virgil Dupras # Created On: 2009-04-23 # $Id$ # Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license # ok, this is hacky and stuff, but I don't know C well enough to play with char buffers, copy # them around and stuff diff --git a/pe/py/modules/cache/setup.py b/pe/py/modules/cache/setup.py index 2b6cd31b..ae1b825c 100644 --- a/pe/py/modules/cache/setup.py +++ b/pe/py/modules/cache/setup.py @@ -1,8 +1,11 @@ -#!/usr/bin/env python # Created By: Virgil Dupras # Created On: 2009-04-23 # $Id$ # Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license from distutils.core import setup from distutils.extension import Extension diff --git a/pe/py/tests/block_test.py b/pe/py/tests/block_test.py index 1ee02faf..38bced11 100644 --- a/pe/py/tests/block_test.py +++ b/pe/py/tests/block_test.py @@ -1,8 +1,11 @@ -# Unit Name: dupeguru_pe.tests.block_test # Created By: Virgil Dupras # Created On: 2006/09/01 # $Id$ # Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license # The commented out tests are tests for function that have been converted to pure C for speed import unittest diff --git a/pe/py/tests/cache_test.py b/pe/py/tests/cache_test.py index 3fd70759..5eaa0275 100644 --- a/pe/py/tests/cache_test.py +++ b/pe/py/tests/cache_test.py @@ -1,8 +1,11 @@ -# Unit Name: dupeguru_pe.tests.cache_test # Created By: Virgil Dupras # Created On: 2006/09/14 # $Id$ # Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license from StringIO import StringIO import os.path as op diff --git a/pe/qt/LICENSE b/pe/qt/LICENSE new file mode 100644 index 00000000..f8818048 --- /dev/null +++ b/pe/qt/LICENSE @@ -0,0 +1,11 @@ +Copyright 2009 Hardcoded Software Inc. (http://www.hardcoded.net) +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + * Neither the name of Hardcoded Software Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + * If the source code has been published less than two years ago, any redistribution, in whole or in part, must retain full licensing functionality, without any attempt to change, obscure or in other ways circumvent its intent. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/pe/qt/app.py b/pe/qt/app.py index d601fb51..fdeac442 100644 --- a/pe/qt/app.py +++ b/pe/qt/app.py @@ -1,9 +1,11 @@ -#!/usr/bin/env python -# Unit Name: app # Created By: Virgil Dupras # Created On: 2009-04-25 # $Id$ # Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license import os.path as op diff --git a/pe/qt/app_win.py b/pe/qt/app_win.py index a18ce2e9..95f8b112 100644 --- a/pe/qt/app_win.py +++ b/pe/qt/app_win.py @@ -1,9 +1,11 @@ -#!/usr/bin/env python -# Unit Name: app_win # Created By: Virgil Dupras # Created On: 2009-05-02 # $Id$ # Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license import winshell diff --git a/pe/qt/block.py b/pe/qt/block.py index 0270aba1..cb3cbc2e 100644 --- a/pe/qt/block.py +++ b/pe/qt/block.py @@ -1,9 +1,11 @@ -#!/usr/bin/env python -# Unit Name: block # Created By: Virgil Dupras # Created On: 2009-05-10 # $Id$ # Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license from _block import getblocks diff --git a/pe/qt/build.py b/pe/qt/build.py index 9af18e34..306365cc 100644 --- a/pe/qt/build.py +++ b/pe/qt/build.py @@ -1,9 +1,12 @@ #!/usr/bin/env python -# Unit Name: build # Created By: Virgil Dupras # Created On: 2009-05-22 # $Id$ # Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license # On Windows, PyInstaller is used to build an exe (py2exe creates a very bad looking icon # The release version is outdated. Use at least r672 on http://svn.pyinstaller.org/trunk diff --git a/pe/qt/details_dialog.py b/pe/qt/details_dialog.py index 0c7503a6..a0702087 100644 --- a/pe/qt/details_dialog.py +++ b/pe/qt/details_dialog.py @@ -1,9 +1,11 @@ -#!/usr/bin/env python -# Unit Name: details_dialog # Created By: Virgil Dupras # Created On: 2009-04-27 # $Id$ # Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license from PyQt4.QtCore import Qt, SIGNAL, QAbstractTableModel, QVariant from PyQt4.QtGui import QDialog, QHeaderView, QPixmap diff --git a/pe/qt/gen.py b/pe/qt/gen.py index 2449de44..114e0ac4 100644 --- a/pe/qt/gen.py +++ b/pe/qt/gen.py @@ -1,9 +1,12 @@ #!/usr/bin/env python -# Unit Name: gen # Created By: Virgil Dupras # Created On: 2009-05-22 # $Id$ # Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license import os import os.path as op diff --git a/pe/qt/main_window.py b/pe/qt/main_window.py index c1ba71bd..e0ab90b1 100644 --- a/pe/qt/main_window.py +++ b/pe/qt/main_window.py @@ -1,9 +1,11 @@ -#!/usr/bin/env python -# Unit Name: main_window # Created By: Virgil Dupras # Created On: 2009-05-23 # $Id$ # Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license from PyQt4.QtCore import SIGNAL from PyQt4.QtGui import QMessageBox, QAction diff --git a/pe/qt/modules/block/block.pyx b/pe/qt/modules/block/block.pyx index 777ff723..22dc20d2 100644 --- a/pe/qt/modules/block/block.pyx +++ b/pe/qt/modules/block/block.pyx @@ -1,3 +1,10 @@ +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license + cdef object getblock(object image): cdef int width, height, pixel_count, red, green, blue, i, offset cdef char *s diff --git a/pe/qt/modules/block/setup.py b/pe/qt/modules/block/setup.py index e37aee94..7b9c9512 100644 --- a/pe/qt/modules/block/setup.py +++ b/pe/qt/modules/block/setup.py @@ -1,4 +1,8 @@ -#!/usr/bin/env python +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license from distutils.core import setup from distutils.extension import Extension diff --git a/pe/qt/preferences.py b/pe/qt/preferences.py index 4dd748fd..f766f889 100644 --- a/pe/qt/preferences.py +++ b/pe/qt/preferences.py @@ -1,9 +1,11 @@ -#!/usr/bin/env python -# Unit Name: preferences # Created By: Virgil Dupras # Created On: 2009-05-17 # $Id$ # Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license from PyQt4.QtCore import QSettings, QVariant diff --git a/pe/qt/preferences_dialog.py b/pe/qt/preferences_dialog.py index 12505f95..198a86cc 100644 --- a/pe/qt/preferences_dialog.py +++ b/pe/qt/preferences_dialog.py @@ -1,9 +1,11 @@ -#!/usr/bin/env python -# Unit Name: preferences_dialog # Created By: Virgil Dupras # Created On: 2009-04-29 # $Id$ # Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license from PyQt4.QtCore import SIGNAL, Qt from PyQt4.QtGui import QDialog, QDialogButtonBox diff --git a/pe/qt/profile.py b/pe/qt/profile.py index e285fafe..867ba791 100644 --- a/pe/qt/profile.py +++ b/pe/qt/profile.py @@ -1,3 +1,10 @@ +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license + import sys import cProfile import pstats diff --git a/pe/qt/start.py b/pe/qt/start.py index 6de46e8e..e3c523fb 100644 --- a/pe/qt/start.py +++ b/pe/qt/start.py @@ -1,3 +1,10 @@ +#!/usr/bin/env python +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license + import sys from PyQt4.QtCore import QCoreApplication diff --git a/se/cocoa/AppDelegate.h b/se/cocoa/AppDelegate.h index 77d3b57f..43264f9e 100644 --- a/se/cocoa/AppDelegate.h +++ b/se/cocoa/AppDelegate.h @@ -1,3 +1,11 @@ +/* +Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +This software is licensed under the "HS" License as described in the "LICENSE" file, +which should be included with this package. The terms are also available at +http://www.hardcoded.net/licenses/hs_license +*/ + #import #import "dgbase/AppDelegate.h" #import "ResultWindow.h" diff --git a/se/cocoa/AppDelegate.m b/se/cocoa/AppDelegate.m index c1378a8c..746d8abf 100644 --- a/se/cocoa/AppDelegate.m +++ b/se/cocoa/AppDelegate.m @@ -1,3 +1,11 @@ +/* +Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +This software is licensed under the "HS" License as described in the "LICENSE" file, +which should be included with this package. The terms are also available at +http://www.hardcoded.net/licenses/hs_license +*/ + #import "AppDelegate.h" #import "cocoalib/ProgressController.h" #import "cocoalib/RegistrationInterface.h" diff --git a/se/cocoa/Consts.h b/se/cocoa/Consts.h index b27af158..aaff95ae 100644 --- a/se/cocoa/Consts.h +++ b/se/cocoa/Consts.h @@ -1,3 +1,11 @@ +/* +Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +This software is licensed under the "HS" License as described in the "LICENSE" file, +which should be included with this package. The terms are also available at +http://www.hardcoded.net/licenses/hs_license +*/ + #import "dgbase/Consts.h" #define APPNAME @"dupeGuru" \ No newline at end of file diff --git a/se/cocoa/DetailsPanel.h b/se/cocoa/DetailsPanel.h index c3191a00..9fad6e39 100644 --- a/se/cocoa/DetailsPanel.h +++ b/se/cocoa/DetailsPanel.h @@ -1,3 +1,11 @@ +/* +Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +This software is licensed under the "HS" License as described in the "LICENSE" file, +which should be included with this package. The terms are also available at +http://www.hardcoded.net/licenses/hs_license +*/ + #import #import "dgbase/DetailsPanel.h" diff --git a/se/cocoa/DetailsPanel.m b/se/cocoa/DetailsPanel.m index e676aef9..77e0a35e 100644 --- a/se/cocoa/DetailsPanel.m +++ b/se/cocoa/DetailsPanel.m @@ -1,3 +1,11 @@ +/* +Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +This software is licensed under the "HS" License as described in the "LICENSE" file, +which should be included with this package. The terms are also available at +http://www.hardcoded.net/licenses/hs_license +*/ + #import "DetailsPanel.h" @implementation DetailsPanel diff --git a/se/cocoa/DirectoryPanel.h b/se/cocoa/DirectoryPanel.h index 6f8141b9..dafdec92 100644 --- a/se/cocoa/DirectoryPanel.h +++ b/se/cocoa/DirectoryPanel.h @@ -1,3 +1,11 @@ +/* +Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +This software is licensed under the "HS" License as described in the "LICENSE" file, +which should be included with this package. The terms are also available at +http://www.hardcoded.net/licenses/hs_license +*/ + #import #import "dgbase/DirectoryPanel.h" diff --git a/se/cocoa/DirectoryPanel.m b/se/cocoa/DirectoryPanel.m index dd07462d..b81b8c40 100644 --- a/se/cocoa/DirectoryPanel.m +++ b/se/cocoa/DirectoryPanel.m @@ -1,3 +1,11 @@ +/* +Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +This software is licensed under the "HS" License as described in the "LICENSE" file, +which should be included with this package. The terms are also available at +http://www.hardcoded.net/licenses/hs_license +*/ + #import "DirectoryPanel.h" @implementation DirectoryPanel diff --git a/se/cocoa/LICENSE b/se/cocoa/LICENSE new file mode 100644 index 00000000..f8818048 --- /dev/null +++ b/se/cocoa/LICENSE @@ -0,0 +1,11 @@ +Copyright 2009 Hardcoded Software Inc. (http://www.hardcoded.net) +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + * Neither the name of Hardcoded Software Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + * If the source code has been published less than two years ago, any redistribution, in whole or in part, must retain full licensing functionality, without any attempt to change, obscure or in other ways circumvent its intent. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/se/cocoa/PyDupeGuru.h b/se/cocoa/PyDupeGuru.h index 85874157..d5cc454c 100644 --- a/se/cocoa/PyDupeGuru.h +++ b/se/cocoa/PyDupeGuru.h @@ -1,3 +1,11 @@ +/* +Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +This software is licensed under the "HS" License as described in the "LICENSE" file, +which should be included with this package. The terms are also available at +http://www.hardcoded.net/licenses/hs_license +*/ + #import #import "dgbase/PyDupeGuru.h" diff --git a/se/cocoa/ResultWindow.h b/se/cocoa/ResultWindow.h index 68e24301..cd4172e9 100644 --- a/se/cocoa/ResultWindow.h +++ b/se/cocoa/ResultWindow.h @@ -1,3 +1,11 @@ +/* +Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +This software is licensed under the "HS" License as described in the "LICENSE" file, +which should be included with this package. The terms are also available at +http://www.hardcoded.net/licenses/hs_license +*/ + #import #import "cocoalib/Outline.h" #import "dgbase/ResultWindow.h" diff --git a/se/cocoa/ResultWindow.m b/se/cocoa/ResultWindow.m index 6ec6e324..f1fae4a3 100644 --- a/se/cocoa/ResultWindow.m +++ b/se/cocoa/ResultWindow.m @@ -1,3 +1,11 @@ +/* +Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +This software is licensed under the "HS" License as described in the "LICENSE" file, +which should be included with this package. The terms are also available at +http://www.hardcoded.net/licenses/hs_license +*/ + #import "ResultWindow.h" #import "cocoalib/Dialogs.h" #import "cocoalib/ProgressController.h" diff --git a/se/cocoa/gen.py b/se/cocoa/gen.py index 4101b2a0..7f6bc18e 100644 --- a/se/cocoa/gen.py +++ b/se/cocoa/gen.py @@ -1,4 +1,9 @@ #!/usr/bin/env python +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license import os diff --git a/se/cocoa/main.m b/se/cocoa/main.m index c5f30658..2f1187a0 100644 --- a/se/cocoa/main.m +++ b/se/cocoa/main.m @@ -1,10 +1,10 @@ -// -// main.m -// dupeguru -// -// Created by Virgil Dupras on 2006/02/01. -// Copyright __MyCompanyName__ 2006. All rights reserved. -// +/* +Copyright 2009 Hardcoded Software (http://www.hardcoded.net) + +This software is licensed under the "HS" License as described in the "LICENSE" file, +which should be included with this package. The terms are also available at +http://www.hardcoded.net/licenses/hs_license +*/ #import diff --git a/se/cocoa/py/dg_cocoa.py b/se/cocoa/py/dg_cocoa.py index 8bf26603..845e3dc7 100644 --- a/se/cocoa/py/dg_cocoa.py +++ b/se/cocoa/py/dg_cocoa.py @@ -1,4 +1,10 @@ -#!/usr/bin/env python +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license + import objc from AppKit import * diff --git a/se/cocoa/py/gen.py b/se/cocoa/py/gen.py index 997214dd..d4991df3 100644 --- a/se/cocoa/py/gen.py +++ b/se/cocoa/py/gen.py @@ -1,4 +1,9 @@ #!/usr/bin/env python +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license import os import os.path as op diff --git a/se/cocoa/py/setup.py b/se/cocoa/py/setup.py index 6d37e3c5..218376c7 100644 --- a/se/cocoa/py/setup.py +++ b/se/cocoa/py/setup.py @@ -1,4 +1,8 @@ -#!/usr/bin/env python +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license from distutils.core import setup import py2app diff --git a/se/help/LICENSE b/se/help/LICENSE new file mode 100644 index 00000000..f8818048 --- /dev/null +++ b/se/help/LICENSE @@ -0,0 +1,11 @@ +Copyright 2009 Hardcoded Software Inc. (http://www.hardcoded.net) +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + * Neither the name of Hardcoded Software Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + * If the source code has been published less than two years ago, any redistribution, in whole or in part, must retain full licensing functionality, without any attempt to change, obscure or in other ways circumvent its intent. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/se/help/gen.py b/se/help/gen.py index 4187233c..8bfd66c3 100644 --- a/se/help/gen.py +++ b/se/help/gen.py @@ -1,4 +1,9 @@ #!/usr/bin/env python +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license import os diff --git a/se/qt/LICENSE b/se/qt/LICENSE new file mode 100644 index 00000000..f8818048 --- /dev/null +++ b/se/qt/LICENSE @@ -0,0 +1,11 @@ +Copyright 2009 Hardcoded Software Inc. (http://www.hardcoded.net) +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + * Neither the name of Hardcoded Software Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + * If the source code has been published less than two years ago, any redistribution, in whole or in part, must retain full licensing functionality, without any attempt to change, obscure or in other ways circumvent its intent. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/se/qt/app.py b/se/qt/app.py index 9d2f1e27..dd523d69 100644 --- a/se/qt/app.py +++ b/se/qt/app.py @@ -1,9 +1,11 @@ -#!/usr/bin/env python -# Unit Name: app # Created By: Virgil Dupras # Created On: 2009-05-24 # $Id$ # Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license from dupeguru import data from dupeguru.directories import Directories as DirectoriesBase, STATE_EXCLUDED diff --git a/se/qt/app_win.py b/se/qt/app_win.py index 8088bce6..777d012c 100644 --- a/se/qt/app_win.py +++ b/se/qt/app_win.py @@ -1,9 +1,11 @@ -#!/usr/bin/env python -# Unit Name: app_win # Created By: Virgil Dupras # Created On: 2009-05-24 # $Id$ # Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license import winshell diff --git a/se/qt/build.py b/se/qt/build.py index ff2b3e69..1f11c5b2 100644 --- a/se/qt/build.py +++ b/se/qt/build.py @@ -1,9 +1,12 @@ #!/usr/bin/env python -# Unit Name: build # Created By: Virgil Dupras # Created On: 2009-05-24 # $Id$ # Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license # On Windows, PyInstaller is used to build an exe (py2exe creates a very bad looking icon # The release version is outdated. Use at least r672 on http://svn.pyinstaller.org/trunk diff --git a/se/qt/details_dialog.py b/se/qt/details_dialog.py index df72ee52..cf940c07 100644 --- a/se/qt/details_dialog.py +++ b/se/qt/details_dialog.py @@ -1,9 +1,11 @@ -#!/usr/bin/env python -# Unit Name: details_dialog # Created By: Virgil Dupras # Created On: 2009-05-24 # $Id$ # Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license from PyQt4.QtCore import Qt from PyQt4.QtGui import QDialog diff --git a/se/qt/gen.py b/se/qt/gen.py index f90bc791..87e6483a 100644 --- a/se/qt/gen.py +++ b/se/qt/gen.py @@ -1,9 +1,12 @@ #!/usr/bin/env python -# Unit Name: gen # Created By: Virgil Dupras # Created On: 2009-05-24 # $Id$ # Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license import os diff --git a/se/qt/preferences.py b/se/qt/preferences.py index b978bc6d..0ed1c954 100644 --- a/se/qt/preferences.py +++ b/se/qt/preferences.py @@ -1,9 +1,11 @@ -#!/usr/bin/env python -# Unit Name: preferences # Created By: Virgil Dupras # Created On: 2009-05-24 # $Id$ # Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license from dupeguru.scanner import SCAN_TYPE_FILENAME, SCAN_TYPE_CONTENT diff --git a/se/qt/preferences_dialog.py b/se/qt/preferences_dialog.py index 80be1ac9..3a1bf1b0 100644 --- a/se/qt/preferences_dialog.py +++ b/se/qt/preferences_dialog.py @@ -1,9 +1,11 @@ -#!/usr/bin/env python -# Unit Name: preferences_dialog # Created By: Virgil Dupras # Created On: 2009-05-24 # $Id$ # Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license from PyQt4.QtCore import SIGNAL, Qt from PyQt4.QtGui import QDialog, QDialogButtonBox diff --git a/se/qt/profile.py b/se/qt/profile.py index c1aefca9..0c764bf3 100644 --- a/se/qt/profile.py +++ b/se/qt/profile.py @@ -1,3 +1,10 @@ +#!/usr/bin/env python +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license + import sys import cProfile import pstats diff --git a/se/qt/start.py b/se/qt/start.py index 41118430..7ff9828a 100644 --- a/se/qt/start.py +++ b/se/qt/start.py @@ -1,3 +1,10 @@ +#!/usr/bin/env python +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license + import sys from PyQt4.QtCore import QCoreApplication From 4cebd142ed6dd5317268e0a9e23a57ce6923babe Mon Sep 17 00:00:00 2001 From: hsoft Date: Wed, 5 Aug 2009 09:12:07 +0000 Subject: [PATCH 101/275] Added PyQt licensing warnings. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40101 --- base/qt/WARNING | 11 +++++++++++ me/qt/WARNING | 11 +++++++++++ pe/qt/WARNING | 11 +++++++++++ se/qt/WARNING | 11 +++++++++++ 4 files changed, 44 insertions(+) create mode 100644 base/qt/WARNING create mode 100644 me/qt/WARNING create mode 100644 pe/qt/WARNING create mode 100644 se/qt/WARNING diff --git a/base/qt/WARNING b/base/qt/WARNING new file mode 100644 index 00000000..729666cc --- /dev/null +++ b/base/qt/WARNING @@ -0,0 +1,11 @@ +WARNING ABOUT THE HS LICENSE AND PyQt + +Although Qt is now LGPL licensed, PyQt still is dual licensed. Until Nokia buys Riverbank and +releases PyQt as LGPL, users of this part of the code (The PyQt-based GUI code) have to use the +GPL version of PyQt, unless they possess a commercial license to it. + +There is no problem to this AS LONG AS YOU DON'T REDISTRIBUTE HS LICENSED CODE. The GPL license, from the point of view of the user, is very permissive. You can do WHATEVER you want with the GPLed version of PyQt, as long as you don't redistribute any of the code, or code dependent on it. When you do, the code you distribute has to be GPL compliant. The HS license is NOT, I repeat, NOT compliant with the GPL. + +So, what does it all mean? You have no restriction on the usage of the PyQt-dependent-HS-licensed code, but unless you possess a commercial PyQt license, Hardcoded Software (or anyone) cannot accept any contribution from you for this part of the code. + +Note that this only affects the PyQt dependent code, and not any other part of HS licensed code (if it has "import PyQt4" in it, it's PyQt dependent code). For the rest of the code, the only restrictions that apply are the ones from the HS license. \ No newline at end of file diff --git a/me/qt/WARNING b/me/qt/WARNING new file mode 100644 index 00000000..729666cc --- /dev/null +++ b/me/qt/WARNING @@ -0,0 +1,11 @@ +WARNING ABOUT THE HS LICENSE AND PyQt + +Although Qt is now LGPL licensed, PyQt still is dual licensed. Until Nokia buys Riverbank and +releases PyQt as LGPL, users of this part of the code (The PyQt-based GUI code) have to use the +GPL version of PyQt, unless they possess a commercial license to it. + +There is no problem to this AS LONG AS YOU DON'T REDISTRIBUTE HS LICENSED CODE. The GPL license, from the point of view of the user, is very permissive. You can do WHATEVER you want with the GPLed version of PyQt, as long as you don't redistribute any of the code, or code dependent on it. When you do, the code you distribute has to be GPL compliant. The HS license is NOT, I repeat, NOT compliant with the GPL. + +So, what does it all mean? You have no restriction on the usage of the PyQt-dependent-HS-licensed code, but unless you possess a commercial PyQt license, Hardcoded Software (or anyone) cannot accept any contribution from you for this part of the code. + +Note that this only affects the PyQt dependent code, and not any other part of HS licensed code (if it has "import PyQt4" in it, it's PyQt dependent code). For the rest of the code, the only restrictions that apply are the ones from the HS license. \ No newline at end of file diff --git a/pe/qt/WARNING b/pe/qt/WARNING new file mode 100644 index 00000000..729666cc --- /dev/null +++ b/pe/qt/WARNING @@ -0,0 +1,11 @@ +WARNING ABOUT THE HS LICENSE AND PyQt + +Although Qt is now LGPL licensed, PyQt still is dual licensed. Until Nokia buys Riverbank and +releases PyQt as LGPL, users of this part of the code (The PyQt-based GUI code) have to use the +GPL version of PyQt, unless they possess a commercial license to it. + +There is no problem to this AS LONG AS YOU DON'T REDISTRIBUTE HS LICENSED CODE. The GPL license, from the point of view of the user, is very permissive. You can do WHATEVER you want with the GPLed version of PyQt, as long as you don't redistribute any of the code, or code dependent on it. When you do, the code you distribute has to be GPL compliant. The HS license is NOT, I repeat, NOT compliant with the GPL. + +So, what does it all mean? You have no restriction on the usage of the PyQt-dependent-HS-licensed code, but unless you possess a commercial PyQt license, Hardcoded Software (or anyone) cannot accept any contribution from you for this part of the code. + +Note that this only affects the PyQt dependent code, and not any other part of HS licensed code (if it has "import PyQt4" in it, it's PyQt dependent code). For the rest of the code, the only restrictions that apply are the ones from the HS license. \ No newline at end of file diff --git a/se/qt/WARNING b/se/qt/WARNING new file mode 100644 index 00000000..729666cc --- /dev/null +++ b/se/qt/WARNING @@ -0,0 +1,11 @@ +WARNING ABOUT THE HS LICENSE AND PyQt + +Although Qt is now LGPL licensed, PyQt still is dual licensed. Until Nokia buys Riverbank and +releases PyQt as LGPL, users of this part of the code (The PyQt-based GUI code) have to use the +GPL version of PyQt, unless they possess a commercial license to it. + +There is no problem to this AS LONG AS YOU DON'T REDISTRIBUTE HS LICENSED CODE. The GPL license, from the point of view of the user, is very permissive. You can do WHATEVER you want with the GPLed version of PyQt, as long as you don't redistribute any of the code, or code dependent on it. When you do, the code you distribute has to be GPL compliant. The HS license is NOT, I repeat, NOT compliant with the GPL. + +So, what does it all mean? You have no restriction on the usage of the PyQt-dependent-HS-licensed code, but unless you possess a commercial PyQt license, Hardcoded Software (or anyone) cannot accept any contribution from you for this part of the code. + +Note that this only affects the PyQt dependent code, and not any other part of HS licensed code (if it has "import PyQt4" in it, it's PyQt dependent code). For the rest of the code, the only restrictions that apply are the ones from the HS license. \ No newline at end of file From 047b4aff0c5fca59e7f9e91c864edb105d4067f3 Mon Sep 17 00:00:00 2001 From: hsoft Date: Mon, 31 Aug 2009 12:04:43 +0000 Subject: [PATCH 102/275] [#52] Added file recycling support on OS X. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40102 --- base/qt/app.py | 12 ++ base/qt/gen.py | 9 +- base/qt/osx/SendToTrashProject/SendToTrash.m | 23 +++ .../SendToTrash.xcodeproj/project.pbxproj | 183 ++++++++++++++++++ base/qt/osx/__init__.py | 19 ++ se/qt/app_win.py => base/qt/win/__init__.py | 12 +- se/qt/start.py | 5 +- 7 files changed, 250 insertions(+), 13 deletions(-) create mode 100644 base/qt/osx/SendToTrashProject/SendToTrash.m create mode 100644 base/qt/osx/SendToTrashProject/SendToTrash.xcodeproj/project.pbxproj create mode 100644 base/qt/osx/__init__.py rename se/qt/app_win.py => base/qt/win/__init__.py (64%) diff --git a/base/qt/app.py b/base/qt/app.py index f8e9d180..8bb7c24c 100644 --- a/base/qt/app.py +++ b/base/qt/app.py @@ -9,6 +9,7 @@ import logging import os.path as op +import sys import traceback from PyQt4.QtCore import Qt, QTimer, QObject, QCoreApplication, QUrl, SIGNAL @@ -20,6 +21,13 @@ from hsutil.reg import RegistrationRequired from dupeguru.app import (DupeGuru as DupeGuruBase, JOB_SCAN, JOB_LOAD, JOB_MOVE, JOB_COPY, JOB_DELETE) + +if sys.platform == 'win32': + from .win import recycle_file +elif sys.platform == 'darwin': + from .osx import recycle_file +else: + logging.warning("Unsupported Platform!!!") from main_window import MainWindow from directories_dialog import DirectoriesDialog @@ -146,6 +154,10 @@ class DupeGuru(DupeGuruBase, QObject): raise NotImplementedError() #--- Override + @staticmethod + def _recycle_dupe(dupe): + recycle_file(dupe.path) + def _start_job(self, jobid, func): title = JOBID2TITLE[jobid] try: diff --git a/base/qt/gen.py b/base/qt/gen.py index 2843e82e..754209f1 100644 --- a/base/qt/gen.py +++ b/base/qt/gen.py @@ -9,6 +9,7 @@ # http://www.hardcoded.net/licenses/hs_license import os +import sys def print_and_do(cmd): print cmd @@ -20,4 +21,10 @@ print_and_do("pyuic4 about_box.ui > about_box_ui.py") print_and_do("pyuic4 reg_submit_dialog.ui > reg_submit_dialog_ui.py") print_and_do("pyuic4 reg_demo_dialog.ui > reg_demo_dialog_ui.py") print_and_do("pyuic4 error_report_dialog.ui > error_report_dialog_ui.py") -print_and_do("pyrcc4 dg.qrc > dg_rc.py") \ No newline at end of file +print_and_do("pyrcc4 dg.qrc > dg_rc.py") + +if sys.platform == 'darwin': + os.chdir('osx/SendToTrashProject') + print_and_do('xcodebuild') + print_and_do('cp build/Release/SendToTrash ../') + os.chdir('../..') \ No newline at end of file diff --git a/base/qt/osx/SendToTrashProject/SendToTrash.m b/base/qt/osx/SendToTrashProject/SendToTrash.m new file mode 100644 index 00000000..26401bcf --- /dev/null +++ b/base/qt/osx/SendToTrashProject/SendToTrash.m @@ -0,0 +1,23 @@ +#import + +int main (int argc, const char * argv[]) { + if(argc == 1){ + NSLog(@"A file path to send to trash is needed"); + return 1; + } + NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init]; + NSString *filepath = [NSString stringWithCString:argv[1] encoding:NSUTF8StringEncoding]; + NSLog(@"%@",filepath); + NSMutableArray *split = [NSMutableArray arrayWithArray:[filepath componentsSeparatedByString:@"/"]]; + NSString *filename = [split lastObject]; + [split removeLastObject]; + NSString *dirpath = [split componentsJoinedByString:@"/"]; + int result; + [[NSWorkspace sharedWorkspace] performFileOperation:NSWorkspaceRecycleOperation + source:dirpath + destination:@"" + files:[NSArray arrayWithObject:filename] + tag:&result]; + [pool drain]; + return result; +} diff --git a/base/qt/osx/SendToTrashProject/SendToTrash.xcodeproj/project.pbxproj b/base/qt/osx/SendToTrashProject/SendToTrash.xcodeproj/project.pbxproj new file mode 100644 index 00000000..43f432d4 --- /dev/null +++ b/base/qt/osx/SendToTrashProject/SendToTrash.xcodeproj/project.pbxproj @@ -0,0 +1,183 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 45; + objects = { + +/* Begin PBXBuildFile section */ + 8DD76F9A0486AA7600D96B5E /* SendToTrash.m in Sources */ = {isa = PBXBuildFile; fileRef = 08FB7796FE84155DC02AAC07 /* SendToTrash.m */; settings = {ATTRIBUTES = (); }; }; + 8DD76F9C0486AA7600D96B5E /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 08FB779EFE84155DC02AAC07 /* Foundation.framework */; }; + CEFA8C60104BD73200E2A946 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CEFA8C5F104BD73200E2A946 /* Cocoa.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 8DD76F9E0486AA7600D96B5E /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 8; + dstPath = /usr/share/man/man1/; + dstSubfolderSpec = 0; + files = ( + ); + runOnlyForDeploymentPostprocessing = 1; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 08FB7796FE84155DC02AAC07 /* SendToTrash.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SendToTrash.m; sourceTree = ""; }; + 08FB779EFE84155DC02AAC07 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = /System/Library/Frameworks/Foundation.framework; sourceTree = ""; }; + 8DD76FA10486AA7600D96B5E /* SendToTrash */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = SendToTrash; sourceTree = BUILT_PRODUCTS_DIR; }; + CEFA8C5F104BD73200E2A946 /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = /System/Library/Frameworks/Cocoa.framework; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 8DD76F9B0486AA7600D96B5E /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 8DD76F9C0486AA7600D96B5E /* Foundation.framework in Frameworks */, + CEFA8C60104BD73200E2A946 /* Cocoa.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 08FB7794FE84155DC02AAC07 /* SendToTrash */ = { + isa = PBXGroup; + children = ( + 08FB7795FE84155DC02AAC07 /* Source */, + 08FB779DFE84155DC02AAC07 /* External Frameworks and Libraries */, + 1AB674ADFE9D54B511CA2CBB /* Products */, + ); + name = SendToTrash; + sourceTree = ""; + }; + 08FB7795FE84155DC02AAC07 /* Source */ = { + isa = PBXGroup; + children = ( + 08FB7796FE84155DC02AAC07 /* SendToTrash.m */, + ); + name = Source; + sourceTree = ""; + }; + 08FB779DFE84155DC02AAC07 /* External Frameworks and Libraries */ = { + isa = PBXGroup; + children = ( + 08FB779EFE84155DC02AAC07 /* Foundation.framework */, + CEFA8C5F104BD73200E2A946 /* Cocoa.framework */, + ); + name = "External Frameworks and Libraries"; + sourceTree = ""; + }; + 1AB674ADFE9D54B511CA2CBB /* Products */ = { + isa = PBXGroup; + children = ( + 8DD76FA10486AA7600D96B5E /* SendToTrash */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 8DD76F960486AA7600D96B5E /* SendToTrash */ = { + isa = PBXNativeTarget; + buildConfigurationList = 1DEB927408733DD40010E9CD /* Build configuration list for PBXNativeTarget "SendToTrash" */; + buildPhases = ( + 8DD76F990486AA7600D96B5E /* Sources */, + 8DD76F9B0486AA7600D96B5E /* Frameworks */, + 8DD76F9E0486AA7600D96B5E /* CopyFiles */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = SendToTrash; + productInstallPath = "$(HOME)/bin"; + productName = SendToTrash; + productReference = 8DD76FA10486AA7600D96B5E /* SendToTrash */; + productType = "com.apple.product-type.tool"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 08FB7793FE84155DC02AAC07 /* Project object */ = { + isa = PBXProject; + buildConfigurationList = 1DEB927808733DD40010E9CD /* Build configuration list for PBXProject "SendToTrash" */; + compatibilityVersion = "Xcode 3.1"; + hasScannedForEncodings = 1; + mainGroup = 08FB7794FE84155DC02AAC07 /* SendToTrash */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 8DD76F960486AA7600D96B5E /* SendToTrash */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXSourcesBuildPhase section */ + 8DD76F990486AA7600D96B5E /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 8DD76F9A0486AA7600D96B5E /* SendToTrash.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 1DEB927608733DD40010E9CD /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + GCC_MODEL_TUNING = G5; + GCC_PRECOMPILE_PREFIX_HEADER = NO; + GCC_PREFIX_HEADER = ""; + INSTALL_PATH = /usr/local/bin; + PRODUCT_NAME = SendToTrash; + }; + name = Release; + }; + 1DEB927A08733DD40010E9CD /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD_32_BIT)"; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_ENABLE_SYMBOL_SEPARATION = NO; + GCC_THREADSAFE_STATICS = NO; + GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.4; + PREBINDING = NO; + PRECOMPS_INCLUDE_HEADERS_FROM_BUILT_PRODUCTS_DIR = NO; + SDKROOT = macosx10.5; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 1DEB927408733DD40010E9CD /* Build configuration list for PBXNativeTarget "SendToTrash" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 1DEB927608733DD40010E9CD /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 1DEB927808733DD40010E9CD /* Build configuration list for PBXProject "SendToTrash" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 1DEB927A08733DD40010E9CD /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 08FB7793FE84155DC02AAC07 /* Project object */; +} diff --git a/base/qt/osx/__init__.py b/base/qt/osx/__init__.py new file mode 100644 index 00000000..768bcadc --- /dev/null +++ b/base/qt/osx/__init__.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Created By: Virgil Dupras +# Created On: 2009-08-31 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license + +import os +import os.path as op + +CMD = unicode(op.join(op.dirname(__file__), 'SendToTrash')) +print CMD + +def recycle_file(path): + print u'%s "%s"' % (CMD, unicode(path)) + os.system(u'%s "%s"' % (CMD, unicode(path))) diff --git a/se/qt/app_win.py b/base/qt/win/__init__.py similarity index 64% rename from se/qt/app_win.py rename to base/qt/win/__init__.py index 777d012c..960cbfb7 100644 --- a/se/qt/app_win.py +++ b/base/qt/win/__init__.py @@ -1,5 +1,6 @@ +# -*- coding: utf-8 -*- # Created By: Virgil Dupras -# Created On: 2009-05-24 +# Created On: 2009-08-31 # $Id$ # Copyright 2009 Hardcoded Software (http://www.hardcoded.net) # @@ -9,10 +10,5 @@ import winshell -import app - -class DupeGuru(app.DupeGuru): - @staticmethod - def _recycle_dupe(dupe): - winshell.delete_file(unicode(dupe.path), no_confirm=True) - +def recycle_file(path): + winshell.delete_file(unicode(path), no_confirm=True) diff --git a/se/qt/start.py b/se/qt/start.py index 7ff9828a..8f97430f 100644 --- a/se/qt/start.py +++ b/se/qt/start.py @@ -12,10 +12,7 @@ from PyQt4.QtGui import QApplication, QIcon, QPixmap import base.dg_rc -if sys.platform == 'win32': - from app_win import DupeGuru -else: - from app import DupeGuru +from app import DupeGuru if __name__ == "__main__": app = QApplication(sys.argv) From f606cc37d2a5ac39911877ef65de8ae9f29b6de1 Mon Sep 17 00:00:00 2001 From: hsoft Date: Mon, 31 Aug 2009 15:33:44 +0000 Subject: [PATCH 103/275] [#47] Added debug logging to Results.load_to_xml() so that when it happens again, it's easier to figure out the cause of this --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40103 --- base/py/results.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/base/py/results.py b/base/py/results.py index 9e9863af..659c0562 100644 --- a/base/py/results.py +++ b/base/py/results.py @@ -7,6 +7,7 @@ # which should be included with this package. The terms are also available at # http://www.hardcoded.net/licenses/hs_license +import logging import re from xml.sax import handler, make_parser, SAXException from xml.sax.saxutils import XMLGenerator @@ -164,7 +165,20 @@ class Results(Markable): def load_from_xml(self, infile, get_file, j=nulljob): self.apply_filter(None) handler = _ResultsHandler(get_file) - parser = make_parser() + try: + parser = make_parser() + except Exception as e: + # This special handling is to try to figure out the cause of #47 + # We don't silently return, because we want the user to send error report. + logging.exception(e) + try: + import xml.parsers.expat + logging.warning('importing xml.parsers.expat went ok, WTF?') + except Exception as e: + # This log should give a little more details about the cause of this all + logging.exception(e) + raise + raise parser.setContentHandler(handler) try: infile, must_close = open_if_filename(infile) From 51b1b539148b2c19d3a73de64c759589fd298717 Mon Sep 17 00:00:00 2001 From: hsoft Date: Tue, 1 Sep 2009 06:53:58 +0000 Subject: [PATCH 104/275] [#46 state:fixed] Fixed crash in Directories panel. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40104 --- base/py/app_cocoa.py | 17 ++++++++++------- base/py/tests/app_cocoa_test.py | 22 ++++++++++++++++++++-- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/base/py/app_cocoa.py b/base/py/app_cocoa.py index 82b547f1..90d83d3c 100644 --- a/base/py/app_cocoa.py +++ b/base/py/app_cocoa.py @@ -253,8 +253,11 @@ class DupeGuru(app.DupeGuru): assert not node_path # no other value is possible return [len(g.dupes) for g in self.results.groups] elif tag == 1: #Directories - dirs = self.GetDirectory(node_path).dirs if node_path else self.directories - return [d.dircount for d in dirs] + try: + dirs = self.GetDirectory(node_path).dirs if node_path else self.directories + return [d.dircount for d in dirs] + except IndexError: # node_path out of range + return [] else: #Power Marker assert not node_path # no other value is possible return [0 for d in self.results.dupes] @@ -275,11 +278,11 @@ class DupeGuru(app.DupeGuru): result = self.data.GetDisplayInfo(d, g, self.display_delta_values) return result elif tag == 1: #Directories - d = self.GetDirectory(node_path) - return [ - d.name, - self.directories.get_state(d.path) - ] + try: + d = self.GetDirectory(node_path) + return [d.name, self.directories.get_state(d.path)] + except IndexError: # node_path out of range + return [] def GetOutlineViewMarked(self, tag, node_path): # 0=unmarked 1=marked 2=unmarkable diff --git a/base/py/tests/app_cocoa_test.py b/base/py/tests/app_cocoa_test.py index f489ac38..4a89b356 100644 --- a/base/py/tests/app_cocoa_test.py +++ b/base/py/tests/app_cocoa_test.py @@ -17,6 +17,7 @@ from nose.tools import eq_ from hsutil.path import Path from hsutil.testcase import TestCase from hsutil.decorators import log_calls +from hsutil import io import hsfs.phys from .results_test import GetTestGroups @@ -44,6 +45,10 @@ class TCDupeGuru(TestCase): self.app = DupeGuru() self.objects,self.matches,self.groups = GetTestGroups() self.app.results.groups = self.groups + tmppath = self.tmppath() + io.mkdir(tmppath + 'foo') + io.mkdir(tmppath + 'bar') + self.app.directories.add_path(tmppath) def test_GetObjects(self): app = self.app @@ -241,9 +246,10 @@ class TCDupeGuru(TestCase): self.assertEqual(0,len(app.results.dupes)) def test_addDirectory_simple(self): + # There's already a directory in self.app, so adding another once makes 2 of em app = self.app - self.assertEqual(0,app.add_directory(self.datadirpath())) - self.assertEqual(1,len(app.directories)) + eq_(app.add_directory(self.datadirpath()), 0) + eq_(len(app.directories), 2) def test_addDirectory_already_there(self): app = self.app @@ -289,6 +295,18 @@ class TCDupeGuru(TestCase): app.SelectPowerMarkerNodePaths(r2np([2])) #The dupe of the second, 2 sized group app.AddSelectedToIgnoreList() + def test_GetOutlineViewChildCounts_out_of_range(self): + # Out of range requests don't crash and return an empty value + app = self.app + # [0, 2] is out of range + eq_(app.GetOutlineViewChildCounts(1, [0, 2]), []) # no crash + + def test_GetOutlineViewValues_out_of_range(self): + # Out of range requests don't crash and return an empty value + app = self.app + # [0, 2] is out of range + eq_(app.GetOutlineViewValues(1, [0, 2]), []) # no crash + class TCDupeGuru_renameSelected(TestCase): def setUp(self): From 7112c57b92ab60aed906123641d70588858ecb22 Mon Sep 17 00:00:00 2001 From: hsoft Date: Tue, 1 Sep 2009 12:42:43 +0000 Subject: [PATCH 105/275] [#44] Added debug logging to try to figure out the cause of the crash. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40105 --- base/py/results.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/base/py/results.py b/base/py/results.py index 659c0562..464717e5 100644 --- a/base/py/results.py +++ b/base/py/results.py @@ -61,6 +61,9 @@ class Results(Markable): def __get_dupe_list(self): if self.__dupes is None: self.__dupes = flatten(group.dupes for group in self.groups) + if None in self.__dupes: + # This is debug logging to try to figure out #44 + logging.warning("There is a None value in the Results' dupe list. dupes: %r groups: %r", self.__dupes, self.groups) if self.__filtered_dupes: self.__dupes = [dupe for dupe in self.__dupes if dupe in self.__filtered_dupes] sd = self.__dupes_sort_descriptor From 4eebc7bb2cbfd8aa44c76d163a6313e9573292f1 Mon Sep 17 00:00:00 2001 From: hsoft Date: Tue, 1 Sep 2009 14:05:00 +0000 Subject: [PATCH 106/275] [#14 state:port] Changed the results export method to a pure python (and simpler) one. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40106 --- base/cocoa/PyDupeGuru.h | 2 +- base/cocoa/ResultWindow.h | 3 + base/cocoa/ResultWindow.m | 37 +++++ base/py/app.py | 16 +- base/py/app_cocoa.py | 12 +- base/py/export.py | 174 ++++++++++++++------ base/py/results.py | 10 +- base/py/tests/export_test.py | 86 ---------- base/py/tests/results_test.py | 24 --- se/cocoa/ResultWindow.h | 3 - se/cocoa/ResultWindow.m | 39 ----- se/cocoa/dupeguru.xcodeproj/project.pbxproj | 23 +-- se/cocoa/py/dg_cocoa.py | 4 +- se/cocoa/w3/dg.xsl | 75 --------- se/cocoa/w3/hardcoded.css | 71 -------- 15 files changed, 186 insertions(+), 393 deletions(-) delete mode 100644 base/py/tests/export_test.py delete mode 100644 se/cocoa/w3/dg.xsl delete mode 100644 se/cocoa/w3/hardcoded.css diff --git a/base/cocoa/PyDupeGuru.h b/base/cocoa/PyDupeGuru.h index 140cf355..e859b581 100644 --- a/base/cocoa/PyDupeGuru.h +++ b/base/cocoa/PyDupeGuru.h @@ -20,7 +20,7 @@ http://www.hardcoded.net/licenses/hs_license - (void)saveIgnoreList; - (void)clearIgnoreList; - (void)purgeIgnoreList; -- (NSString *)exportToXHTMLwithColumns:(NSArray *)aColIds xslt:(NSString *)xsltPath css:(NSString *)cssPath; +- (NSString *)exportToXHTMLwithColumns:(NSArray *)aColIds; - (NSNumber *)doScan; diff --git a/base/cocoa/ResultWindow.h b/base/cocoa/ResultWindow.h index d3ea27a9..65e42ff7 100644 --- a/base/cocoa/ResultWindow.h +++ b/base/cocoa/ResultWindow.h @@ -36,6 +36,8 @@ http://www.hardcoded.net/licenses/hs_license - (NSString *)logoImageName; /* Helpers */ +- (NSArray *)getColumnsOrder; +- (NSDictionary *)getColumnsWidth; - (NSArray *)getSelected:(BOOL)aDupesOnly; - (NSArray *)getSelectedPaths:(BOOL)aDupesOnly; - (void)updatePySelection; @@ -48,6 +50,7 @@ http://www.hardcoded.net/licenses/hs_license - (IBAction)copyMarked:(id)sender; - (IBAction)deleteMarked:(id)sender; - (IBAction)expandAll:(id)sender; +- (IBAction)exportToXHTML:(id)sender; - (IBAction)moveMarked:(id)sender; - (IBAction)switchSelected:(id)sender; - (IBAction)togglePowerMarker:(id)sender; diff --git a/base/cocoa/ResultWindow.m b/base/cocoa/ResultWindow.m index 3dc9bf1b..9ee36361 100644 --- a/base/cocoa/ResultWindow.m +++ b/base/cocoa/ResultWindow.m @@ -75,6 +75,37 @@ http://www.hardcoded.net/licenses/hs_license } /* Helpers */ +//Returns an array of identifiers, in order. +- (NSArray *)getColumnsOrder +{ + NSTableColumn *col; + NSString *colId; + NSMutableArray *result = [NSMutableArray array]; + NSEnumerator *e = [[matches tableColumns] objectEnumerator]; + while (col = [e nextObject]) + { + colId = [col identifier]; + [result addObject:colId]; + } + return result; +} + +- (NSDictionary *)getColumnsWidth +{ + NSMutableDictionary *result = [NSMutableDictionary dictionary]; + NSTableColumn *col; + NSString *colId; + NSNumber *width; + NSEnumerator *e = [[matches tableColumns] objectEnumerator]; + while (col = [e nextObject]) + { + colId = [col identifier]; + width = [NSNumber numberWithFloat:[col width]]; + [result setObject:width forKey:colId]; + } + return result; +} + - (NSArray *)getSelected:(BOOL)aDupesOnly { if (_powerMode) @@ -185,6 +216,12 @@ http://www.hardcoded.net/licenses/hs_license [matches expandItem:[matches itemAtRow:i]]; } +- (IBAction)exportToXHTML:(id)sender +{ + NSString *exported = [py exportToXHTMLwithColumns:[self getColumnsOrder]]; + [[NSWorkspace sharedWorkspace] openFile:exported]; +} + - (IBAction)moveMarked:(id)sender { int mark_count = [[py getMarkCount] intValue]; diff --git a/base/py/app.py b/base/py/app.py index 5c2d0379..5e36eaad 100644 --- a/base/py/app.py +++ b/base/py/app.py @@ -19,7 +19,7 @@ from hsutil.reg import RegistrableApplication, RegistrationRequired from hsutil.misc import flatten, first from hsutil.str import escape -from . import directories, results, scanner +from . import directories, results, scanner, export JOB_SCAN = 'job_scan' JOB_LOAD = 'job_load' @@ -177,6 +177,20 @@ class DupeGuru(RegistrableApplication): self._demo_check() self._start_job(JOB_DELETE, self._do_delete) + def export_to_xhtml(self, column_ids): + column_ids = [colid for colid in column_ids if colid.isdigit()] + column_ids = map(int, column_ids) + column_ids.sort() + colnames = [col['display'] for i, col in enumerate(self.data.COLUMNS) if i in column_ids] + rows = [] + for group in self.results.groups: + for dupe in group: + data = self.data.GetDisplayInfo(dupe, group) + row = [data[colid] for colid in column_ids] + row.insert(0, dupe is not group.ref) + rows.append(row) + return export.export_to_xhtml(colnames, rows) + def load(self): self._start_job(JOB_LOAD, self._do_load) self.load_ignore_list() diff --git a/base/py/app_cocoa.py b/base/py/app_cocoa.py index 90d83d3c..48b23a11 100644 --- a/base/py/app_cocoa.py +++ b/base/py/app_cocoa.py @@ -17,7 +17,7 @@ from hsutil.cocoa import install_exception_hook from hsutil.misc import stripnone from hsutil.reg import RegistrationRequired -import export, app, data +import app, data JOBID2TITLE = { app.JOB_SCAN: "Scanning for duplicates", @@ -116,16 +116,6 @@ class DupeGuru(app.DupeGuru): copy_or_move_marked = demo_method(app.DupeGuru.copy_or_move_marked) delete_marked = demo_method(app.DupeGuru.delete_marked) - def ExportToXHTML(self,column_ids,xslt_path,css_path): - columns = [] - for index,column in enumerate(self.data.COLUMNS): - display = column['display'] - enabled = str(index) in column_ids - columns.append((display,enabled)) - xml_path = op.join(self.appdata,'results_export.xml') - self.results.save_to_xml(xml_path,self.data.GetDisplayInfo) - return export.export_to_xhtml(xml_path,xslt_path,css_path,columns) - def MakeSelectedReference(self): self.make_reference(self.selected_dupes) diff --git a/base/py/export.py b/base/py/export.py index bcac23fb..c28f0487 100644 --- a/base/py/export.py +++ b/base/py/export.py @@ -7,60 +7,132 @@ # which should be included with this package. The terms are also available at # http://www.hardcoded.net/licenses/hs_license -from xml.dom import minidom import tempfile import os.path as op -import os -from StringIO import StringIO +from tempfile import mkdtemp -from hsutil.files import FileOrPath +# Yes, this is a very low-tech solution, but at least it doesn't have all these annoying dependency +# and resource problems. -def output_column_xml(outfile, columns): - """Creates a xml file outfile with the supplied columns. - - outfile can be a filename or a file object. - columns is a list of 2 sized tuples (display,enabled) - """ - doc = minidom.Document() - root = doc.appendChild(doc.createElement('columns')) - for display,enabled in columns: - col_node = root.appendChild(doc.createElement('column')) - col_node.setAttribute('display', display) - col_node.setAttribute('enabled', {True:'y',False:'n'}[enabled]) - with FileOrPath(outfile, 'wb') as fp: - doc.writexml(fp, '\t','\t','\n', encoding='utf-8') +MAIN_TEMPLATE = u""" + + + + + + dupeGuru Results + + + +

dupeGuru Results

+ +$colheaders +$rows +
+ + +""" + +COLHEADERS_TEMPLATE = u"{name}" + +ROW_TEMPLATE = u""" + + {filename}{cells} + +""" + +CELL_TEMPLATE = u"""{value}""" + +def export_to_xhtml(colnames, rows): + # a row is a list of values with the first value being a flag indicating if the row should be indented + if rows: + assert len(rows[0]) == len(colnames) + 1 # + 1 is for the "indented" flag + colheaders = u''.join(COLHEADERS_TEMPLATE.format(name=name) for name in colnames) + rendered_rows = [] + for row in rows: + # [2:] is to remove the indented flag + filename + indented = u'indented' if row[0] else u'' + filename = row[1] + cells = u''.join(CELL_TEMPLATE.format(value=value) for value in row[2:]) + rendered_rows.append(ROW_TEMPLATE.format(indented=indented, filename=filename, cells=cells)) + rendered_rows = u''.join(rendered_rows) + # The main template can't use format because the css code uses {} + content = MAIN_TEMPLATE.replace('$colheaders', colheaders).replace('$rows', rendered_rows) + folder = mkdtemp() + destpath = op.join(folder, u'export.htm') + fp = open(destpath, 'w') + fp.write(content.encode('utf-8')) + fp.close() + return destpath diff --git a/base/py/results.py b/base/py/results.py index 464717e5..15debc0e 100644 --- a/base/py/results.py +++ b/base/py/results.py @@ -252,7 +252,7 @@ class Results(Markable): group.clean_matches() self.__dupes = None - def save_to_xml(self, outfile, with_data=False): + def save_to_xml(self, outfile): self.apply_filter(None) outfile, must_close = open_if_filename(outfile, 'wb') writer = XMLGenerator(outfile, 'utf-8') @@ -275,14 +275,6 @@ class Results(Markable): 'marked': cond(self.is_marked(d), 'y', 'n') }) writer.startElement('file', attrs) - if with_data: - data_list = self.data.GetDisplayInfo(d, g) - for data in data_list: - attrs = AttributesImpl({ - 'value': data, - }) - writer.startElement('data', attrs) - writer.endElement('data') writer.endElement('file') for match in g.matches: attrs = AttributesImpl({ diff --git a/base/py/tests/export_test.py b/base/py/tests/export_test.py deleted file mode 100644 index 447d02ed..00000000 --- a/base/py/tests/export_test.py +++ /dev/null @@ -1,86 +0,0 @@ -# Created By: Virgil Dupras -# Created On: 2006/09/16 -# $Id$ -# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "HS" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at -# http://www.hardcoded.net/licenses/hs_license - -from xml.dom import minidom -from StringIO import StringIO - -from hsutil.testcase import TestCase - -from .. import export -from ..export import * - -class TCoutput_columns_xml(TestCase): - def test_empty_columns(self): - f = StringIO() - output_column_xml(f,[]) - f.seek(0) - doc = minidom.parse(f) - root = doc.documentElement - self.assertEqual('columns',root.nodeName) - self.assertEqual(0,len(root.childNodes)) - - def test_some_columns(self): - f = StringIO() - output_column_xml(f,[('foo',True),('bar',False),('baz',True)]) - f.seek(0) - doc = minidom.parse(f) - columns = doc.getElementsByTagName('column') - self.assertEqual(3,len(columns)) - c1,c2,c3 = columns - self.assertEqual('foo',c1.getAttribute('display')) - self.assertEqual('bar',c2.getAttribute('display')) - self.assertEqual('baz',c3.getAttribute('display')) - self.assertEqual('y',c1.getAttribute('enabled')) - self.assertEqual('n',c2.getAttribute('enabled')) - self.assertEqual('y',c3.getAttribute('enabled')) - - -class TCmerge_css_into_xhtml(TestCase): - def test_main(self): - css = StringIO() - css.write('foobar') - css.seek(0) - xhtml = StringIO() - xhtml.write(""" - - - - - dupeGuru - Duplicate file scanner - - - - - - """) - xhtml.seek(0) - self.assert_(merge_css_into_xhtml(xhtml,css)) - xhtml.seek(0) - doc = minidom.parse(xhtml) - head = doc.getElementsByTagName('head')[0] - #A style node should have been added in head. - styles = head.getElementsByTagName('style') - self.assertEqual(1,len(styles)) - style = styles[0] - self.assertEqual('text/css',style.getAttribute('type')) - self.assertEqual('foobar',style.firstChild.nodeValue.strip()) - #all should be removed - self.assertEqual(1,len(head.getElementsByTagName('link'))) - - def test_empty(self): - self.assert_(not merge_css_into_xhtml(StringIO(),StringIO())) - - def test_malformed(self): - xhtml = StringIO() - xhtml.write(""" - - """) - xhtml.seek(0) - self.assert_(not merge_css_into_xhtml(xhtml,StringIO())) - diff --git a/base/py/tests/results_test.py b/base/py/tests/results_test.py index a6d9f05a..e71e7680 100644 --- a/base/py/tests/results_test.py +++ b/base/py/tests/results_test.py @@ -395,30 +395,6 @@ class TCResultsXML(TestCase): self.assertEqual('ibabtu',d1.getAttributeNode('words').nodeValue) self.assertEqual('ibabtu',d2.getAttributeNode('words').nodeValue) - def test_save_to_xml_with_columns(self): - class FakeDataModule: - def GetDisplayInfo(self,dupe,group): - return [str(dupe.size),dupe.foo.upper()] - - for i,object in enumerate(self.objects): - object.size = i - object.foo = u'bar\u00e9' - f = StringIO.StringIO() - self.results.data = FakeDataModule() - self.results.save_to_xml(f,True) - f.seek(0) - doc = xml.dom.minidom.parse(f) - root = doc.documentElement - g1,g2 = root.getElementsByTagName('group') - d1,d2,d3 = g1.getElementsByTagName('file') - d4,d5 = g2.getElementsByTagName('file') - self.assertEqual('0',d1.getElementsByTagName('data')[0].getAttribute('value')) - self.assertEqual(u'BAR\u00c9',d1.getElementsByTagName('data')[1].getAttribute('value')) #\u00c9 is upper of \u00e9 - self.assertEqual('1',d2.getElementsByTagName('data')[0].getAttribute('value')) - self.assertEqual('2',d3.getElementsByTagName('data')[0].getAttribute('value')) - self.assertEqual('3',d4.getElementsByTagName('data')[0].getAttribute('value')) - self.assertEqual('4',d5.getElementsByTagName('data')[0].getAttribute('value')) - def test_LoadXML(self): def get_file(path): return [f for f in self.objects if str(f.path) == path][0] diff --git a/se/cocoa/ResultWindow.h b/se/cocoa/ResultWindow.h index cd4172e9..b8646aaf 100644 --- a/se/cocoa/ResultWindow.h +++ b/se/cocoa/ResultWindow.h @@ -25,7 +25,6 @@ http://www.hardcoded.net/licenses/hs_license NSMutableIndexSet *_deltaColumns; } - (IBAction)clearIgnoreList:(id)sender; -- (IBAction)exportToXHTML:(id)sender; - (IBAction)filter:(id)sender; - (IBAction)ignoreSelected:(id)sender; - (IBAction)markAll:(id)sender; @@ -47,8 +46,6 @@ http://www.hardcoded.net/licenses/hs_license - (IBAction)toggleDetailsPanel:(id)sender; - (NSTableColumn *)getColumnForIdentifier:(int)aIdentifier title:(NSString *)aTitle width:(int)aWidth refCol:(NSTableColumn *)aColumn; -- (NSArray *)getColumnsOrder; -- (NSDictionary *)getColumnsWidth; - (void)initResultColumns; - (void)restoreColumnsPosition:(NSArray *)aColumnsOrder widths:(NSDictionary *)aColumnsWidth; @end diff --git a/se/cocoa/ResultWindow.m b/se/cocoa/ResultWindow.m index f1fae4a3..a8bf6ec4 100644 --- a/se/cocoa/ResultWindow.m +++ b/se/cocoa/ResultWindow.m @@ -58,14 +58,6 @@ http://www.hardcoded.net/licenses/hs_license [py clearIgnoreList]; } -- (IBAction)exportToXHTML:(id)sender -{ - NSString *xsltPath = [[NSBundle mainBundle] pathForResource:@"dg" ofType:@"xsl"]; - NSString *cssPath = [[NSBundle mainBundle] pathForResource:@"hardcoded" ofType:@"css"]; - NSString *exported = [py exportToXHTMLwithColumns:[self getColumnsOrder] xslt:xsltPath css:cssPath]; - [[NSWorkspace sharedWorkspace] openFile:exported]; -} - - (IBAction)filter:(id)sender { NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; @@ -272,37 +264,6 @@ http://www.hardcoded.net/licenses/hs_license return col; } -//Returns an array of identifiers, in order. -- (NSArray *)getColumnsOrder -{ - NSTableColumn *col; - NSString *colId; - NSMutableArray *result = [NSMutableArray array]; - NSEnumerator *e = [[matches tableColumns] objectEnumerator]; - while (col = [e nextObject]) - { - colId = [col identifier]; - [result addObject:colId]; - } - return result; -} - -- (NSDictionary *)getColumnsWidth -{ - NSMutableDictionary *result = [NSMutableDictionary dictionary]; - NSTableColumn *col; - NSString *colId; - NSNumber *width; - NSEnumerator *e = [[matches tableColumns] objectEnumerator]; - while (col = [e nextObject]) - { - colId = [col identifier]; - width = [NSNumber numberWithFloat:[col width]]; - [result setObject:width forKey:colId]; - } - return result; -} - - (void)initResultColumns { NSTableColumn *refCol = [matches tableColumnWithIdentifier:@"0"]; diff --git a/se/cocoa/dupeguru.xcodeproj/project.pbxproj b/se/cocoa/dupeguru.xcodeproj/project.pbxproj index 334ad249..60e297be 100644 --- a/se/cocoa/dupeguru.xcodeproj/project.pbxproj +++ b/se/cocoa/dupeguru.xcodeproj/project.pbxproj @@ -12,8 +12,6 @@ 8D11072D0486CEB800E47090 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 29B97316FDCFA39411CA2CEA /* main.m */; settings = {ATTRIBUTES = (); }; }; 8D11072F0486CEB800E47090 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */; }; CE073F6309CAE1A3005C1D2F /* dupeguru_help in Resources */ = {isa = PBXBuildFile; fileRef = CE073F5409CAE1A3005C1D2F /* dupeguru_help */; }; - CE0D67640ABC2D3E00E2FFD9 /* dg.xsl in Resources */ = {isa = PBXBuildFile; fileRef = CE0D67620ABC2D3E00E2FFD9 /* dg.xsl */; }; - CE0D67650ABC2D3E00E2FFD9 /* hardcoded.css in Resources */ = {isa = PBXBuildFile; fileRef = CE0D67630ABC2D3E00E2FFD9 /* hardcoded.css */; }; CE381C9609914ACE003581CE /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = CE381C9409914ACE003581CE /* AppDelegate.m */; }; CE381C9C09914ADF003581CE /* ResultWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = CE381C9A09914ADF003581CE /* ResultWindow.m */; }; CE381D0509915304003581CE /* dg_cocoa.plugin in Resources */ = {isa = PBXBuildFile; fileRef = CE381CF509915304003581CE /* dg_cocoa.plugin */; }; @@ -77,8 +75,6 @@ 8D1107310486CEB800E47090 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = SOURCE_ROOT; }; 8D1107320486CEB800E47090 /* dupeGuru.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = dupeGuru.app; sourceTree = BUILT_PRODUCTS_DIR; }; CE073F5409CAE1A3005C1D2F /* dupeguru_help */ = {isa = PBXFileReference; lastKnownFileType = folder; name = dupeguru_help; path = help/dupeguru_help; sourceTree = ""; }; - CE0D67620ABC2D3E00E2FFD9 /* dg.xsl */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = text.xml; name = dg.xsl; path = w3/dg.xsl; sourceTree = SOURCE_ROOT; }; - CE0D67630ABC2D3E00E2FFD9 /* hardcoded.css */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = text; name = hardcoded.css; path = w3/hardcoded.css; sourceTree = SOURCE_ROOT; }; CE381C9409914ACE003581CE /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = SOURCE_ROOT; }; CE381C9509914ACE003581CE /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = SOURCE_ROOT; }; CE381C9A09914ADF003581CE /* ResultWindow.m */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.objc; path = ResultWindow.m; sourceTree = SOURCE_ROOT; }; @@ -215,7 +211,6 @@ CE073F5409CAE1A3005C1D2F /* dupeguru_help */, CE381CF509915304003581CE /* dg_cocoa.plugin */, CEFC294309C89E0000D9F998 /* images */, - CE0D67610ABC2D3E00E2FFD9 /* w3 */, CEEB135109C837A2004D2330 /* dupeguru.icns */, 8D1107310486CEB800E47090 /* Info.plist */, 089C165CFE840E0CC02AAC07 /* InfoPlist.strings */, @@ -235,15 +230,6 @@ name = Frameworks; sourceTree = ""; }; - CE0D67610ABC2D3E00E2FFD9 /* w3 */ = { - isa = PBXGroup; - children = ( - CE0D67620ABC2D3E00E2FFD9 /* dg.xsl */, - CE0D67630ABC2D3E00E2FFD9 /* hardcoded.css */, - ); - path = w3; - sourceTree = ""; - }; CEDD92D50FDD01640031C7B7 /* brsinglelineformatter */ = { isa = PBXGroup; children = ( @@ -375,8 +361,6 @@ CEF7823809C8AA0200EF38FF /* gear.png in Resources */, CECA899909DB12CA00A3D774 /* Details.nib in Resources */, CE3AA46709DB207900DB3A21 /* Directories.nib in Resources */, - CE0D67640ABC2D3E00E2FFD9 /* dg.xsl in Resources */, - CE0D67650ABC2D3E00E2FFD9 /* hardcoded.css in Resources */, CEFC7FAD0FC9518A00CD5728 /* ErrorReportWindow.xib in Resources */, CEFC7FAE0FC9518A00CD5728 /* progress.nib in Resources */, CEFC7FAF0FC9518A00CD5728 /* registration.nib in Resources */, @@ -534,12 +518,11 @@ C01FCF5008A954540054247B /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - ARCHS = ( - ppc, - i386, - ); + ARCHS = "$(ARCHS_STANDARD_32_BIT_PRE_XCODE_3_1)"; + ARCHS_STANDARD_32_BIT_PRE_XCODE_3_1 = "ppc i386"; FRAMEWORK_SEARCH_PATHS = "@executable_path/../Frameworks"; GCC_C_LANGUAGE_STANDARD = c99; + GCC_VERSION = 4.0; GCC_WARN_ABOUT_RETURN_TYPE = YES; GCC_WARN_UNUSED_VARIABLE = YES; MACOSX_DEPLOYMENT_TARGET = 10.4; diff --git a/se/cocoa/py/dg_cocoa.py b/se/cocoa/py/dg_cocoa.py index 845e3dc7..1dffaa32 100644 --- a/se/cocoa/py/dg_cocoa.py +++ b/se/cocoa/py/dg_cocoa.py @@ -41,8 +41,8 @@ class PyDupeGuru(PyApp): def doScan(self): return self.app.start_scanning() - def exportToXHTMLwithColumns_xslt_css_(self,column_ids,xslt_path,css_path): - return self.app.ExportToXHTML(column_ids,xslt_path,css_path) + def exportToXHTMLwithColumns_(self, column_ids): + return self.app.export_to_xhtml(column_ids) def loadIgnoreList(self): self.app.load_ignore_list() diff --git a/se/cocoa/w3/dg.xsl b/se/cocoa/w3/dg.xsl deleted file mode 100644 index 4f982fce..00000000 --- a/se/cocoa/w3/dg.xsl +++ /dev/null @@ -1,75 +0,0 @@ - - - - - - - - - - - - - - - - - - indented - - - - - - - - - - - - - - - - - - - - - - - - - - - - dupeGuru Results - - - -

dupeGuru Results

- - - - - -
- - -
- -
\ No newline at end of file diff --git a/se/cocoa/w3/hardcoded.css b/se/cocoa/w3/hardcoded.css deleted file mode 100644 index ed243bcc..00000000 --- a/se/cocoa/w3/hardcoded.css +++ /dev/null @@ -1,71 +0,0 @@ -BODY -{ - background-color:white; -} - -BODY,A,P,UL,TABLE,TR,TD -{ - font-family:Tahoma,Arial,sans-serif; - font-size:10pt; - color: #4477AA; -} - -TABLE -{ - background-color: #225588; - margin-left: auto; - margin-right: auto; - width: 90%; -} - -TR -{ - background-color: white; -} - -TH -{ - font-weight: bold; - color: black; - background-color: #C8D6E5; -} - -TH TD -{ - color:black; -} - -TD -{ - padding-left: 2pt; -} - -TD.rightelem -{ - text-align:right; - /*padding-left:0pt;*/ - padding-right: 2pt; - width: 17%; -} - -TD.indented -{ - padding-left: 12pt; -} - -H1 -{ - font-family:"Courier New",monospace; - color:#6699CC; - font-size:18pt; - color:#6da500; - border-color: #70A0CF; - border-width: 1pt; - border-style: solid; - margin-top: 16pt; - margin-left: 5%; - margin-right: 5%; - padding-top: 2pt; - padding-bottom:2pt; - text-align: center; -} \ No newline at end of file From 900f21b2f357e4273967ef73ee90fcfba23e2f7e Mon Sep 17 00:00:00 2001 From: hsoft Date: Tue, 1 Sep 2009 14:12:02 +0000 Subject: [PATCH 107/275] [#14] Adjusted ME-specific code to recent export changes. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40107 --- me/cocoa/ResultWindow.h | 3 - me/cocoa/ResultWindow.m | 39 ----------- me/cocoa/dupeguru.xcodeproj/project.pbxproj | 23 +------ me/cocoa/py/dg_cocoa.py | 4 +- me/cocoa/w3/dg.xsl | 75 --------------------- me/cocoa/w3/hardcoded.css | 71 ------------------- 6 files changed, 5 insertions(+), 210 deletions(-) delete mode 100644 me/cocoa/w3/dg.xsl delete mode 100644 me/cocoa/w3/hardcoded.css diff --git a/me/cocoa/ResultWindow.h b/me/cocoa/ResultWindow.h index a44959d4..d46b40b3 100644 --- a/me/cocoa/ResultWindow.h +++ b/me/cocoa/ResultWindow.h @@ -25,7 +25,6 @@ http://www.hardcoded.net/licenses/hs_license NSMutableIndexSet *_deltaColumns; } - (IBAction)clearIgnoreList:(id)sender; -- (IBAction)exportToXHTML:(id)sender; - (IBAction)filter:(id)sender; - (IBAction)ignoreSelected:(id)sender; - (IBAction)markAll:(id)sender; @@ -48,8 +47,6 @@ http://www.hardcoded.net/licenses/hs_license - (IBAction)toggleDetailsPanel:(id)sender; - (NSTableColumn *)getColumnForIdentifier:(int)aIdentifier title:(NSString *)aTitle width:(int)aWidth refCol:(NSTableColumn *)aColumn; -- (NSArray *)getColumnsOrder; -- (NSDictionary *)getColumnsWidth; - (void)initResultColumns; - (void)restoreColumnsPosition:(NSArray *)aColumnsOrder widths:(NSDictionary *)aColumnsWidth; @end diff --git a/me/cocoa/ResultWindow.m b/me/cocoa/ResultWindow.m index 31d1dc25..2f80b33c 100644 --- a/me/cocoa/ResultWindow.m +++ b/me/cocoa/ResultWindow.m @@ -59,14 +59,6 @@ http://www.hardcoded.net/licenses/hs_license [py clearIgnoreList]; } -- (IBAction)exportToXHTML:(id)sender -{ - NSString *xsltPath = [[NSBundle mainBundle] pathForResource:@"dg" ofType:@"xsl"]; - NSString *cssPath = [[NSBundle mainBundle] pathForResource:@"hardcoded" ofType:@"css"]; - NSString *exported = [py exportToXHTMLwithColumns:[self getColumnsOrder] xslt:xsltPath css:cssPath]; - [[NSWorkspace sharedWorkspace] openFile:exported]; -} - - (IBAction)filter:(id)sender { NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; @@ -280,37 +272,6 @@ http://www.hardcoded.net/licenses/hs_license return col; } -//Returns an array of identifiers, in order. -- (NSArray *)getColumnsOrder -{ - NSTableColumn *col; - NSString *colId; - NSMutableArray *result = [NSMutableArray array]; - NSEnumerator *e = [[matches tableColumns] objectEnumerator]; - while (col = [e nextObject]) - { - colId = [col identifier]; - [result addObject:colId]; - } - return result; -} - -- (NSDictionary *)getColumnsWidth -{ - NSMutableDictionary *result = [NSMutableDictionary dictionary]; - NSTableColumn *col; - NSString *colId; - NSNumber *width; - NSEnumerator *e = [[matches tableColumns] objectEnumerator]; - while (col = [e nextObject]) - { - colId = [col identifier]; - width = [NSNumber numberWithFloat:[col width]]; - [result setObject:width forKey:colId]; - } - return result; -} - - (void)initResultColumns { NSTableColumn *refCol = [matches tableColumnWithIdentifier:@"0"]; diff --git a/me/cocoa/dupeguru.xcodeproj/project.pbxproj b/me/cocoa/dupeguru.xcodeproj/project.pbxproj index 81740288..5ec33272 100644 --- a/me/cocoa/dupeguru.xcodeproj/project.pbxproj +++ b/me/cocoa/dupeguru.xcodeproj/project.pbxproj @@ -24,8 +24,6 @@ 8D11072D0486CEB800E47090 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 29B97316FDCFA39411CA2CEA /* main.m */; settings = {ATTRIBUTES = (); }; }; 8D11072F0486CEB800E47090 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */; }; CE073F6309CAE1A3005C1D2F /* dupeguru_me_help in Resources */ = {isa = PBXBuildFile; fileRef = CE073F5409CAE1A3005C1D2F /* dupeguru_me_help */; }; - CE12149E0AC86DB900E93983 /* dg.xsl in Resources */ = {isa = PBXBuildFile; fileRef = CE12149C0AC86DB900E93983 /* dg.xsl */; }; - CE12149F0AC86DB900E93983 /* hardcoded.css in Resources */ = {isa = PBXBuildFile; fileRef = CE12149D0AC86DB900E93983 /* hardcoded.css */; }; CE1425890AFB718500BD5167 /* Sparkle.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE1425880AFB718500BD5167 /* Sparkle.framework */; }; CE14259F0AFB719300BD5167 /* Sparkle.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = CE1425880AFB718500BD5167 /* Sparkle.framework */; }; CE381C9609914ACE003581CE /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = CE381C9409914ACE003581CE /* AppDelegate.m */; }; @@ -90,8 +88,6 @@ 8D1107310486CEB800E47090 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = SOURCE_ROOT; }; 8D1107320486CEB800E47090 /* dupeGuru ME.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "dupeGuru ME.app"; sourceTree = BUILT_PRODUCTS_DIR; }; CE073F5409CAE1A3005C1D2F /* dupeguru_me_help */ = {isa = PBXFileReference; lastKnownFileType = folder; name = dupeguru_me_help; path = help/dupeguru_me_help; sourceTree = ""; }; - CE12149C0AC86DB900E93983 /* dg.xsl */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = text.xml; name = dg.xsl; path = w3/dg.xsl; sourceTree = SOURCE_ROOT; }; - CE12149D0AC86DB900E93983 /* hardcoded.css */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = text; name = hardcoded.css; path = w3/hardcoded.css; sourceTree = SOURCE_ROOT; }; CE1425880AFB718500BD5167 /* Sparkle.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Sparkle.framework; path = /Library/Frameworks/Sparkle.framework; sourceTree = ""; }; CE381C9409914ACE003581CE /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = SOURCE_ROOT; }; CE381C9509914ACE003581CE /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = SOURCE_ROOT; }; @@ -236,7 +232,6 @@ CE073F5409CAE1A3005C1D2F /* dupeguru_me_help */, CE381CF509915304003581CE /* dg_cocoa.plugin */, CEFC294309C89E0000D9F998 /* images */, - CE12149B0AC86DB900E93983 /* w3 */, CEEB135109C837A2004D2330 /* dupeguru.icns */, 8D1107310486CEB800E47090 /* Info.plist */, 089C165CFE840E0CC02AAC07 /* InfoPlist.strings */, @@ -256,15 +251,6 @@ name = Frameworks; sourceTree = ""; }; - CE12149B0AC86DB900E93983 /* w3 */ = { - isa = PBXGroup; - children = ( - CE12149C0AC86DB900E93983 /* dg.xsl */, - CE12149D0AC86DB900E93983 /* hardcoded.css */, - ); - path = w3; - sourceTree = SOURCE_ROOT; - }; CE49DEF10FDFEB810098617B /* brsinglelineformatter */ = { isa = PBXGroup; children = ( @@ -394,8 +380,6 @@ CECA899909DB12CA00A3D774 /* Details.nib in Resources */, CE3AA46709DB207900DB3A21 /* Directories.nib in Resources */, CED2A6880A05102700AC4C3F /* power_marker32.png in Resources */, - CE12149E0AC86DB900E93983 /* dg.xsl in Resources */, - CE12149F0AC86DB900E93983 /* hardcoded.css in Resources */, CE515E020FC6C13E00EC695D /* ErrorReportWindow.xib in Resources */, CE515E030FC6C13E00EC695D /* progress.nib in Resources */, CE515E040FC6C13E00EC695D /* registration.nib in Resources */, @@ -548,12 +532,11 @@ C01FCF5008A954540054247B /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - ARCHS = ( - i386, - ppc, - ); + ARCHS = "$(ARCHS_STANDARD_32_BIT_PRE_XCODE_3_1)"; + ARCHS_STANDARD_32_BIT_PRE_XCODE_3_1 = "ppc i386"; COPY_PHASE_STRIP = NO; GCC_C_LANGUAGE_STANDARD = c99; + GCC_VERSION = 4.0; GCC_WARN_ABOUT_RETURN_TYPE = YES; GCC_WARN_UNUSED_VARIABLE = YES; MACOSX_DEPLOYMENT_TARGET = 10.4; diff --git a/me/cocoa/py/dg_cocoa.py b/me/cocoa/py/dg_cocoa.py index 3f799c39..613f9044 100644 --- a/me/cocoa/py/dg_cocoa.py +++ b/me/cocoa/py/dg_cocoa.py @@ -42,8 +42,8 @@ class PyDupeGuru(PyApp): def doScan(self): return self.app.start_scanning() - def exportToXHTMLwithColumns_xslt_css_(self,column_ids,xslt_path,css_path): - return self.app.ExportToXHTML(column_ids,xslt_path,css_path) + def exportToXHTMLwithColumns_(self, column_ids): + return self.app.export_to_xhtml(column_ids) def loadIgnoreList(self): self.app.load_ignore_list() diff --git a/me/cocoa/w3/dg.xsl b/me/cocoa/w3/dg.xsl deleted file mode 100644 index 4f982fce..00000000 --- a/me/cocoa/w3/dg.xsl +++ /dev/null @@ -1,75 +0,0 @@ - - - - - - - - - - - - - - - - - - indented - - - - - - - - - - - - - - - - - - - - - - - - - - - - dupeGuru Results - - - -

dupeGuru Results

- - - - - -
- - -
- -
\ No newline at end of file diff --git a/me/cocoa/w3/hardcoded.css b/me/cocoa/w3/hardcoded.css deleted file mode 100644 index ed243bcc..00000000 --- a/me/cocoa/w3/hardcoded.css +++ /dev/null @@ -1,71 +0,0 @@ -BODY -{ - background-color:white; -} - -BODY,A,P,UL,TABLE,TR,TD -{ - font-family:Tahoma,Arial,sans-serif; - font-size:10pt; - color: #4477AA; -} - -TABLE -{ - background-color: #225588; - margin-left: auto; - margin-right: auto; - width: 90%; -} - -TR -{ - background-color: white; -} - -TH -{ - font-weight: bold; - color: black; - background-color: #C8D6E5; -} - -TH TD -{ - color:black; -} - -TD -{ - padding-left: 2pt; -} - -TD.rightelem -{ - text-align:right; - /*padding-left:0pt;*/ - padding-right: 2pt; - width: 17%; -} - -TD.indented -{ - padding-left: 12pt; -} - -H1 -{ - font-family:"Courier New",monospace; - color:#6699CC; - font-size:18pt; - color:#6da500; - border-color: #70A0CF; - border-width: 1pt; - border-style: solid; - margin-top: 16pt; - margin-left: 5%; - margin-right: 5%; - padding-top: 2pt; - padding-bottom:2pt; - text-align: center; -} \ No newline at end of file From d8f9a3f0545cd361d9fac9a90be44108af2ac01c Mon Sep 17 00:00:00 2001 From: hsoft Date: Tue, 1 Sep 2009 14:18:09 +0000 Subject: [PATCH 108/275] [#14] Adjusted PE-specific code to recent export changes. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40108 --- pe/cocoa/ResultWindow.h | 3 - pe/cocoa/ResultWindow.m | 39 ----------- pe/cocoa/dupeguru.xcodeproj/project.pbxproj | 23 +------ pe/cocoa/py/dg_cocoa.py | 4 +- pe/cocoa/w3/dg.xsl | 75 --------------------- pe/cocoa/w3/hardcoded.css | 71 ------------------- 6 files changed, 5 insertions(+), 210 deletions(-) delete mode 100644 pe/cocoa/w3/dg.xsl delete mode 100644 pe/cocoa/w3/hardcoded.css diff --git a/pe/cocoa/ResultWindow.h b/pe/cocoa/ResultWindow.h index a4add0c9..80cd2a28 100644 --- a/pe/cocoa/ResultWindow.h +++ b/pe/cocoa/ResultWindow.h @@ -23,7 +23,6 @@ http://www.hardcoded.net/licenses/hs_license } - (IBAction)clearIgnoreList:(id)sender; - (IBAction)clearPictureCache:(id)sender; -- (IBAction)exportToXHTML:(id)sender; - (IBAction)filter:(id)sender; - (IBAction)ignoreSelected:(id)sender; - (IBAction)markAll:(id)sender; @@ -46,8 +45,6 @@ http://www.hardcoded.net/licenses/hs_license - (IBAction)toggleDirectories:(id)sender; - (NSTableColumn *)getColumnForIdentifier:(int)aIdentifier title:(NSString *)aTitle width:(int)aWidth refCol:(NSTableColumn *)aColumn; -- (NSArray *)getColumnsOrder; -- (NSDictionary *)getColumnsWidth; - (void)initResultColumns; - (void)restoreColumnsPosition:(NSArray *)aColumnsOrder widths:(NSDictionary *)aColumnsWidth; @end diff --git a/pe/cocoa/ResultWindow.m b/pe/cocoa/ResultWindow.m index cab5701f..f5d6153a 100644 --- a/pe/cocoa/ResultWindow.m +++ b/pe/cocoa/ResultWindow.m @@ -66,14 +66,6 @@ http://www.hardcoded.net/licenses/hs_license [(PyDupeGuru *)py clearPictureCache]; } -- (IBAction)exportToXHTML:(id)sender -{ - NSString *xsltPath = [[NSBundle mainBundle] pathForResource:@"dg" ofType:@"xsl"]; - NSString *cssPath = [[NSBundle mainBundle] pathForResource:@"hardcoded" ofType:@"css"]; - NSString *exported = [py exportToXHTMLwithColumns:[self getColumnsOrder] xslt:xsltPath css:cssPath]; - [[NSWorkspace sharedWorkspace] openFile:exported]; -} - - (IBAction)filter:(id)sender { NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; @@ -277,37 +269,6 @@ http://www.hardcoded.net/licenses/hs_license return col; } -//Returns an array of identifiers, in order. -- (NSArray *)getColumnsOrder -{ - NSTableColumn *col; - NSString *colId; - NSMutableArray *result = [NSMutableArray array]; - NSEnumerator *e = [[matches tableColumns] objectEnumerator]; - while (col = [e nextObject]) - { - colId = [col identifier]; - [result addObject:colId]; - } - return result; -} - -- (NSDictionary *)getColumnsWidth -{ - NSMutableDictionary *result = [NSMutableDictionary dictionary]; - NSTableColumn *col; - NSString *colId; - NSNumber *width; - NSEnumerator *e = [[matches tableColumns] objectEnumerator]; - while (col = [e nextObject]) - { - colId = [col identifier]; - width = [NSNumber numberWithFloat:[col width]]; - [result setObject:width forKey:colId]; - } - return result; -} - - (void)initResultColumns { NSTableColumn *refCol = [matches tableColumnWithIdentifier:@"0"]; diff --git a/pe/cocoa/dupeguru.xcodeproj/project.pbxproj b/pe/cocoa/dupeguru.xcodeproj/project.pbxproj index 703c7c9d..808d08ad 100644 --- a/pe/cocoa/dupeguru.xcodeproj/project.pbxproj +++ b/pe/cocoa/dupeguru.xcodeproj/project.pbxproj @@ -44,8 +44,6 @@ CECA899909DB12CA00A3D774 /* Details.nib in Resources */ = {isa = PBXBuildFile; fileRef = CECA899709DB12CA00A3D774 /* Details.nib */; }; CECA899C09DB132E00A3D774 /* DetailsPanel.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = CECA899A09DB132E00A3D774 /* DetailsPanel.h */; }; CECA899D09DB132E00A3D774 /* DetailsPanel.m in Sources */ = {isa = PBXBuildFile; fileRef = CECA899B09DB132E00A3D774 /* DetailsPanel.m */; }; - CEDA432E0B07C6E600B3091A /* dg.xsl in Resources */ = {isa = PBXBuildFile; fileRef = CEDA432C0B07C6E600B3091A /* dg.xsl */; }; - CEDA432F0B07C6E600B3091A /* hardcoded.css in Resources */ = {isa = PBXBuildFile; fileRef = CEDA432D0B07C6E600B3091A /* hardcoded.css */; }; CEEB135209C837A2004D2330 /* dupeguru.icns in Resources */ = {isa = PBXBuildFile; fileRef = CEEB135109C837A2004D2330 /* dupeguru.icns */; }; CEF7823809C8AA0200EF38FF /* gear.png in Resources */ = {isa = PBXBuildFile; fileRef = CEF7823709C8AA0200EF38FF /* gear.png */; }; CEFC294609C89E3D00D9F998 /* folder32.png in Resources */ = {isa = PBXBuildFile; fileRef = CEFC294509C89E3D00D9F998 /* folder32.png */; }; @@ -135,8 +133,6 @@ CECA899809DB12CA00A3D774 /* English */ = {isa = PBXFileReference; lastKnownFileType = wrapper.nib; name = English; path = English.lproj/Details.nib; sourceTree = ""; }; CECA899A09DB132E00A3D774 /* DetailsPanel.h */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.h; path = DetailsPanel.h; sourceTree = ""; }; CECA899B09DB132E00A3D774 /* DetailsPanel.m */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.objc; path = DetailsPanel.m; sourceTree = ""; }; - CEDA432C0B07C6E600B3091A /* dg.xsl */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = text.xml; name = dg.xsl; path = w3/dg.xsl; sourceTree = SOURCE_ROOT; }; - CEDA432D0B07C6E600B3091A /* hardcoded.css */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = text; name = hardcoded.css; path = w3/hardcoded.css; sourceTree = SOURCE_ROOT; }; CEEB135109C837A2004D2330 /* dupeguru.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; path = dupeguru.icns; sourceTree = ""; }; CEF7823709C8AA0200EF38FF /* gear.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = gear.png; path = images/gear.png; sourceTree = ""; }; CEFC294509C89E3D00D9F998 /* folder32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = folder32.png; path = images/folder32.png; sourceTree = SOURCE_ROOT; }; @@ -233,7 +229,6 @@ CE073F5409CAE1A3005C1D2F /* dupeguru_pe_help */, CE381CF509915304003581CE /* dg_cocoa.plugin */, CEFC294309C89E0000D9F998 /* images */, - CEDA432B0B07C6E600B3091A /* w3 */, CEEB135109C837A2004D2330 /* dupeguru.icns */, 8D1107310486CEB800E47090 /* Info.plist */, 089C165CFE840E0CC02AAC07 /* InfoPlist.strings */, @@ -316,15 +311,6 @@ path = cocoalib/brsinglelineformatter; sourceTree = SOURCE_ROOT; }; - CEDA432B0B07C6E600B3091A /* w3 */ = { - isa = PBXGroup; - children = ( - CEDA432C0B07C6E600B3091A /* dg.xsl */, - CEDA432D0B07C6E600B3091A /* hardcoded.css */, - ); - path = w3; - sourceTree = SOURCE_ROOT; - }; CEFC294309C89E0000D9F998 /* images */ = { isa = PBXGroup; children = ( @@ -393,8 +379,6 @@ CECA899909DB12CA00A3D774 /* Details.nib in Resources */, CE3AA46709DB207900DB3A21 /* Directories.nib in Resources */, CEFCDE2D0AB0418600C33A93 /* dgpe_logo_32.png in Resources */, - CEDA432E0B07C6E600B3091A /* dg.xsl in Resources */, - CEDA432F0B07C6E600B3091A /* hardcoded.css in Resources */, CE80DB760FC194760086DCA6 /* ErrorReportWindow.xib in Resources */, CE80DB770FC194760086DCA6 /* progress.nib in Resources */, CE80DB780FC194760086DCA6 /* registration.nib in Resources */, @@ -553,12 +537,11 @@ C01FCF5008A954540054247B /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - ARCHS = ( - ppc, - i386, - ); + ARCHS = "$(ARCHS_STANDARD_32_BIT_PRE_XCODE_3_1)"; + ARCHS_STANDARD_32_BIT_PRE_XCODE_3_1 = "ppc i386"; FRAMEWORK_SEARCH_PATHS = ""; GCC_C_LANGUAGE_STANDARD = c99; + GCC_VERSION = 4.0; GCC_WARN_ABOUT_RETURN_TYPE = YES; GCC_WARN_UNUSED_VARIABLE = YES; MACOSX_DEPLOYMENT_TARGET = 10.4; diff --git a/pe/cocoa/py/dg_cocoa.py b/pe/cocoa/py/dg_cocoa.py index 2c0accfa..aa2131d1 100644 --- a/pe/cocoa/py/dg_cocoa.py +++ b/pe/cocoa/py/dg_cocoa.py @@ -43,8 +43,8 @@ class PyDupeGuru(PyApp): def doScan(self): return self.app.start_scanning() - def exportToXHTMLwithColumns_xslt_css_(self,column_ids,xslt_path,css_path): - return self.app.ExportToXHTML(column_ids,xslt_path,css_path) + def exportToXHTMLwithColumns_(self, column_ids): + return self.app.export_to_xhtml(column_ids) def loadIgnoreList(self): self.app.load_ignore_list() diff --git a/pe/cocoa/w3/dg.xsl b/pe/cocoa/w3/dg.xsl deleted file mode 100644 index 4f982fce..00000000 --- a/pe/cocoa/w3/dg.xsl +++ /dev/null @@ -1,75 +0,0 @@ - - - - - - - - - - - - - - - - - - indented - - - - - - - - - - - - - - - - - - - - - - - - - - - - dupeGuru Results - - - -

dupeGuru Results

- - - - - -
- - -
- -
\ No newline at end of file diff --git a/pe/cocoa/w3/hardcoded.css b/pe/cocoa/w3/hardcoded.css deleted file mode 100644 index ed243bcc..00000000 --- a/pe/cocoa/w3/hardcoded.css +++ /dev/null @@ -1,71 +0,0 @@ -BODY -{ - background-color:white; -} - -BODY,A,P,UL,TABLE,TR,TD -{ - font-family:Tahoma,Arial,sans-serif; - font-size:10pt; - color: #4477AA; -} - -TABLE -{ - background-color: #225588; - margin-left: auto; - margin-right: auto; - width: 90%; -} - -TR -{ - background-color: white; -} - -TH -{ - font-weight: bold; - color: black; - background-color: #C8D6E5; -} - -TH TD -{ - color:black; -} - -TD -{ - padding-left: 2pt; -} - -TD.rightelem -{ - text-align:right; - /*padding-left:0pt;*/ - padding-right: 2pt; - width: 17%; -} - -TD.indented -{ - padding-left: 12pt; -} - -H1 -{ - font-family:"Courier New",monospace; - color:#6699CC; - font-size:18pt; - color:#6da500; - border-color: #70A0CF; - border-width: 1pt; - border-style: solid; - margin-top: 16pt; - margin-left: 5%; - margin-right: 5%; - padding-top: 2pt; - padding-bottom:2pt; - text-align: center; -} \ No newline at end of file From 44ecae26574f051dc9ee7080f8bf1cd0347ac28f Mon Sep 17 00:00:00 2001 From: hsoft Date: Tue, 1 Sep 2009 16:40:29 +0000 Subject: [PATCH 109/275] [#14 state:fixed] Added a Export to XHTML menu to the Qt port of DG. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40109 --- base/qt/main_window.py | 16 +++++++++++++--- base/qt/main_window.ui | 23 +++++++++++++++++++++++ 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/base/qt/main_window.py b/base/qt/main_window.py index e4218caf..a6442b9c 100644 --- a/base/qt/main_window.py +++ b/base/qt/main_window.py @@ -7,9 +7,9 @@ # which should be included with this package. The terms are also available at # http://www.hardcoded.net/licenses/hs_license -from PyQt4.QtCore import Qt, QCoreApplication, QProcess, SIGNAL +from PyQt4.QtCore import Qt, QCoreApplication, QProcess, SIGNAL, QUrl from PyQt4.QtGui import (QMainWindow, QMenu, QPixmap, QIcon, QToolButton, QLabel, QHeaderView, - QMessageBox, QInputDialog, QLineEdit, QItemSelectionModel) + QMessageBox, QInputDialog, QLineEdit, QItemSelectionModel, QDesktopServices) from hsutil.misc import nonone @@ -200,7 +200,17 @@ class MainWindow(QMainWindow, Ui_MainWindow): def directoriesTriggered(self): self.app.show_directories() - + + def exportTriggered(self): + h = self.resultsView.header() + column_ids = [] + for i in range(len(self.app.data.COLUMNS)): + if not h.isSectionHidden(i): + column_ids.append(str(i)) + exported_path = self.app.export_to_xhtml(column_ids) + url = QUrl.fromLocalFile(exported_path) + QDesktopServices.openUrl(url) + def makeReferenceTriggered(self): self.app.make_reference(self.resultsView.selectedDupes()) diff --git a/base/qt/main_window.ui b/base/qt/main_window.ui index 754f265c..8d949c2f 100644 --- a/base/qt/main_window.ui +++ b/base/qt/main_window.ui @@ -121,6 +121,7 @@ + @@ -416,6 +417,11 @@ Check for Update + + + Export To XHTML + + @@ -876,6 +882,22 @@ + + actionExport + triggered() + MainWindow + exportTriggered() + + + -1 + -1 + + + 314 + 256 + + + directoriesTriggered() @@ -907,5 +929,6 @@ aboutTriggered() registerTrigerred() checkForUpdateTriggered() + exportTriggered() From e5015d85b99fd2cea9c923da11e56fab4ac68fe6 Mon Sep 17 00:00:00 2001 From: hsoft Date: Wed, 2 Sep 2009 10:21:11 +0000 Subject: [PATCH 110/275] [#41 state:fixed] Added error handling + logging for GetDisplayInfo() --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40110 --- base/py/app.py | 11 ++++++++++- base/py/app_cocoa.py | 6 +++--- base/py/data.py | 4 +--- base/py/data_me.py | 4 +--- 4 files changed, 15 insertions(+), 10 deletions(-) diff --git a/base/py/app.py b/base/py/app.py index 5e36eaad..004e31d7 100644 --- a/base/py/app.py +++ b/base/py/app.py @@ -87,6 +87,15 @@ class DupeGuru(RegistrableApplication): for file in j.iter_with_progress(files, 'Reading metadata %d/%d'): file._read_all_info(sections=[IT_ATTRS, IT_EXTRA]) + def _get_display_info(self, dupe, group, delta=False): + if (dupe is None) or (group is None): + return ['---'] * len(self.data.COLUMNS) + try: + return self.data.GetDisplayInfo(dupe, group, delta) + except Exception as e: + logging.warning(u'Exception on GetDisplayInfo for %s: %s', unicode(dupe.path), unicode(e)) + return ['---'] * len(self.data.COLUMNS) + def _get_file(self, str_path): p = Path(str_path) for d in self.directories: @@ -185,7 +194,7 @@ class DupeGuru(RegistrableApplication): rows = [] for group in self.results.groups: for dupe in group: - data = self.data.GetDisplayInfo(dupe, group) + data = self._get_display_info(dupe, group) row = [data[colid] for colid in column_ids] row.insert(0, dupe is not group.ref) rows.append(row) diff --git a/base/py/app_cocoa.py b/base/py/app_cocoa.py index 48b23a11..7e4e68d3 100644 --- a/base/py/app_cocoa.py +++ b/base/py/app_cocoa.py @@ -100,9 +100,9 @@ class DupeGuru(app.DupeGuru): return self.GetDirectory(node_path[1:],d) def RefreshDetailsTable(self,dupe,group): - l1 = self.data.GetDisplayInfo(dupe,group,False) + l1 = self._get_display_info(dupe, group, False) if group is not None: - l2 = self.data.GetDisplayInfo(group.ref,group,False) + l2 = self._get_display_info(group.ref, group, False) else: l2 = l1 #To have a list of empty '---' values names = [c['display'] for c in self.data.COLUMNS] @@ -265,7 +265,7 @@ class DupeGuru(app.DupeGuru): else: d = self.results.dupes[node_path[0]] g = self.results.get_group_of_duplicate(d) - result = self.data.GetDisplayInfo(d, g, self.display_delta_values) + result = self._get_display_info(d, g, self.display_delta_values) return result elif tag == 1: #Directories try: diff --git a/base/py/data.py b/base/py/data.py index d0faf479..49f5fbf0 100644 --- a/base/py/data.py +++ b/base/py/data.py @@ -53,9 +53,7 @@ COLUMNS = [ {'attr':'dupe_count','display':'Dupe Count'}, ] -def GetDisplayInfo(dupe, group, delta=False): - if (dupe is None) or (group is None): - return ['---'] * len(COLUMNS) +def GetDisplayInfo(dupe, group, delta): size = dupe.size ctime = dupe.ctime mtime = dupe.mtime diff --git a/base/py/data_me.py b/base/py/data_me.py index b05dcf18..ab4d9dbf 100644 --- a/base/py/data_me.py +++ b/base/py/data_me.py @@ -33,9 +33,7 @@ COLUMNS = [ {'attr':'dupe_count','display':'Dupe Count'}, ] -def GetDisplayInfo(dupe, group, delta=False): - if (dupe is None) or (group is None): - return ['---'] * len(COLUMNS) +def GetDisplayInfo(dupe, group, delta): size = dupe.size duration = dupe.duration bitrate = dupe.bitrate From b8e6b8ba8b44442ea0bf7cafec6a418cf1fc1cda Mon Sep 17 00:00:00 2001 From: hsoft Date: Wed, 2 Sep 2009 11:08:36 +0000 Subject: [PATCH 111/275] [#50 state:port] When a ref file is selected, only show info one one side of the details panel. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40111 --- base/py/app_cocoa.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/base/py/app_cocoa.py b/base/py/app_cocoa.py index 7e4e68d3..5e1cb8e6 100644 --- a/base/py/app_cocoa.py +++ b/base/py/app_cocoa.py @@ -101,10 +101,9 @@ class DupeGuru(app.DupeGuru): def RefreshDetailsTable(self,dupe,group): l1 = self._get_display_info(dupe, group, False) - if group is not None: - l2 = self._get_display_info(group.ref, group, False) - else: - l2 = l1 #To have a list of empty '---' values + # we don't want the two sides of the table to display the stats for the same file + ref = group.ref if group is not None and group.ref is not dupe else None + l2 = self._get_display_info(ref, group, False) names = [c['display'] for c in self.data.COLUMNS] self.details_table = zip(names,l1,l2) From b8bad7a3399f2b448f63aae32478d575b73cca2e Mon Sep 17 00:00:00 2001 From: hsoft Date: Wed, 2 Sep 2009 11:14:24 +0000 Subject: [PATCH 112/275] [#50 state:fixed] When a ref file is selected, only show info one one side of the details panel. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40112 --- base/qt/details_table.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/base/qt/details_table.py b/base/qt/details_table.py index 7e9b5c5c..b568e459 100644 --- a/base/qt/details_table.py +++ b/base/qt/details_table.py @@ -51,9 +51,11 @@ class DetailsModel(QAbstractTableModel): def duplicateSelected(self): dupe = self._app.selected_dupe if dupe is None: - return - group = self._app.results.get_group_of_duplicate(dupe) - ref = group.ref + group = None + ref = None + else: + group = self._app.results.get_group_of_duplicate(dupe) + ref = group.ref if group.ref is not dupe else None self._dupe_data = self._data.GetDisplayInfo(dupe, group) self._ref_data = self._data.GetDisplayInfo(ref, group) self.reset() From 36f9e211848398901a91ffe10bcecebc0cd410e8 Mon Sep 17 00:00:00 2001 From: hsoft Date: Wed, 2 Sep 2009 13:05:17 +0000 Subject: [PATCH 113/275] [#11 state:fixed] dupeGuru SE now handles any kind of bundle, not just applications. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40113 --- base/py/app_se_cocoa.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/base/py/app_se_cocoa.py b/base/py/app_se_cocoa.py index ce7b0707..abd11d2e 100644 --- a/base/py/app_se_cocoa.py +++ b/base/py/app_se_cocoa.py @@ -7,20 +7,30 @@ # which should be included with this package. The terms are also available at # http://www.hardcoded.net/licenses/hs_license +import logging + +from AppKit import * + from hsfs.phys import Directory as DirectoryBase from hsfs.phys.bundle import Bundle from hsutil.path import Path from hsutil.misc import extract from hsutil.str import get_file_ext - from . import app_cocoa, data from .directories import Directories as DirectoriesBase, STATE_EXCLUDED +def is_bundle(path): + sw = NSWorkspace.sharedWorkspace() + uti, error = sw.typeOfFile_error_(path) + if error is not None: + logging.warning(u'There was an error trying to detect the UTI of %s', path) + return sw.type_conformsToType_(uti, 'com.apple.bundle') or sw.type_conformsToType_(uti, 'com.apple.package') + class DGDirectory(DirectoryBase): def _create_sub_file(self, name, with_parent=True): ext = get_file_ext(name) - if ext == 'app': + if is_bundle(unicode(self.path + name)): parent = self if with_parent else None return Bundle(parent, name) else: @@ -28,7 +38,7 @@ class DGDirectory(DirectoryBase): def _fetch_subitems(self): subdirs, subfiles = super(DGDirectory, self)._fetch_subitems() - apps, normal_dirs = extract(lambda name: get_file_ext(name) == 'app', subdirs) + apps, normal_dirs = extract(lambda name: is_bundle(unicode(self.path + name)), subdirs) subfiles += apps return normal_dirs, subfiles From 497a29118a9c5b5f6f7fbbd3e775b9a092bd4053 Mon Sep 17 00:00:00 2001 From: hsoft Date: Wed, 2 Sep 2009 14:51:05 +0000 Subject: [PATCH 114/275] [#48 state:fixed] Gracefully handle cases where the library doesn't exist in Add iPhoto Library. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40114 --- pe/py/app_cocoa.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pe/py/app_cocoa.py b/pe/py/app_cocoa.py index 5bc08923..1e5fbd1c 100644 --- a/pe/py/app_cocoa.py +++ b/pe/py/app_cocoa.py @@ -19,7 +19,7 @@ from appscript import app, k from hsutil import job, io import hsfs as fs -from hsfs import phys +from hsfs import phys, InvalidPath from hsutil import files from hsutil.str import get_file_ext from hsutil.path import Path @@ -94,6 +94,8 @@ class IPhotoLibrary(fs.Directory): self.refpath = plistpath[:-1] # the AlbumData.xml file lives right in the library path super(IPhotoLibrary, self).__init__(None, 'iPhoto Library') + if not io.exists(plistpath): + raise InvalidPath(self) def _update_photo(self, photo_data): if photo_data['MediaType'] != 'Image': From 536c43006d47e84f38e42ae4981fbc8c60abc281 Mon Sep 17 00:00:00 2001 From: hsoft Date: Wed, 2 Sep 2009 15:35:01 +0000 Subject: [PATCH 115/275] Fixed the Cython extensions gen script so that it works on Snow Leopard (what the heck is *up* with SL? Can't compile crap). --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40115 --- pe/py/gen.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pe/py/gen.py b/pe/py/gen.py index d08a9c04..bf3ebb12 100644 --- a/pe/py/gen.py +++ b/pe/py/gen.py @@ -19,11 +19,11 @@ def move(src, dst): print 'Moving %s --> %s' % (src, dst) os.rename(src, dst) - +# The CC=gcc-4.0 thing is because, in Snow Leopard, gcc-4.2 can't compile these units. os.chdir(op.join('modules', 'block')) -os.system('python setup.py build_ext --inplace') +os.system('CC=gcc-4.0;python setup.py build_ext --inplace') os.chdir(op.join('..', 'cache')) -os.system('python setup.py build_ext --inplace') +os.system('CC=gcc-4.0;python setup.py build_ext --inplace') os.chdir(op.join('..', '..')) move(op.join('modules', 'block', '_block.so'), '_block.so') move(op.join('modules', 'block', '_block.pyd'), '_block.pyd') From 6d5ae9950970cba0cfe533767f8999b20d5c2177 Mon Sep 17 00:00:00 2001 From: hsoft Date: Sat, 5 Sep 2009 14:58:35 +0000 Subject: [PATCH 116/275] [#51 state:fixed] Improved the grouping algorithm to reduce the number of discarded matches in a scan. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40116 --- base/py/engine.py | 20 +++++++++++++++----- base/py/results.py | 2 +- base/py/tests/engine_test.py | 23 +++++++++++++++++++++-- 3 files changed, 37 insertions(+), 8 deletions(-) diff --git a/base/py/engine.py b/base/py/engine.py index 796c8c3d..5f5bc07c 100644 --- a/base/py/engine.py +++ b/base/py/engine.py @@ -14,6 +14,7 @@ import string from collections import defaultdict, namedtuple from unicodedata import normalize +from hsutil.misc import flatten from hsutil.str import multi_replace from hsutil import job @@ -260,9 +261,11 @@ class Group(object): self._percentage = None self._matches_for_ref = None - def clean_matches(self): - self.matches = set(m for m in self.matches if (m.first in self.unordered) and (m.second in self.unordered)) + def discard_matches(self): + discarded = set(m for m in self.matches if not all(obj in self.unordered for obj in [m.first, m.second])) + self.matches -= discarded self.candidates = defaultdict(set) + return discarded def get_match_of(self, item): if item is self.ref: @@ -286,14 +289,14 @@ class Group(object): if ref is not self.ref: self.switch_ref(ref) - def remove_dupe(self, item, clean_matches=True): + def remove_dupe(self, item, discard_matches=True): try: self.ordered.remove(item) self.unordered.remove(item) self._percentage = None self._matches_for_ref = None if (len(self) > 1) and any(not getattr(item, 'is_ref', False) for item in self): - if clean_matches: + if discard_matches: self.matches = set(m for m in self.matches if item not in m) else: self._clear() @@ -354,6 +357,13 @@ def get_groups(matches, j=job.nulljob): dupe2group[first] = target_group dupe2group[second] = target_group target_group.add_match(match) + # Now that we have a group, we have to discard groups' matches and see if there're any "orphan" + # matches, that is, matches that were candidate in a group but that none of their 2 files were + # accepted in the group. With these orphan groups, it's safe to build additional groups + matched_files = set(flatten(groups)) + orphan_matches = [] for group in groups: - group.clean_matches() + orphan_matches += set(m for m in group.discard_matches() if not any(obj in matched_files for obj in [m.first, m.second])) + if groups and orphan_matches: + groups += get_groups(orphan_matches) # no job, as it isn't supposed to take a long time return groups diff --git a/base/py/results.py b/base/py/results.py index 15debc0e..dee24920 100644 --- a/base/py/results.py +++ b/base/py/results.py @@ -249,7 +249,7 @@ class Results(Markable): else: affected_groups.add(group) for group in affected_groups: - group.clean_matches() + group.discard_matches() self.__dupes = None def save_to_xml(self, outfile): diff --git a/base/py/tests/engine_test.py b/base/py/tests/engine_test.py index a9ff33b1..2111618f 100644 --- a/base/py/tests/engine_test.py +++ b/base/py/tests/engine_test.py @@ -9,6 +9,8 @@ import sys +from nose.tools import eq_ + from hsutil import job from hsutil.decorators import log_calls from hsutil.testcase import TestCase @@ -719,12 +721,12 @@ class TCGroup(TestCase): self.assert_(g[0] is o1) self.assert_(g[1] is o2) - def test_clean_matches(self): + def test_discard_matches(self): g = Group() o1,o2,o3 = (NamedObject("foo",True),NamedObject("bar",True),NamedObject("baz",True)) g.add_match(get_match(o1,o2)) g.add_match(get_match(o1,o3)) - g.clean_matches() + g.discard_matches() self.assertEqual(1,len(g.matches)) self.assertEqual(0,len(g.candidates)) @@ -815,3 +817,20 @@ class TCget_groups(TestCase): self.assertEqual(0,self.log[0]) self.assertEqual(100,self.log[-1]) + def test_group_admissible_discarded_dupes(self): + # If, with a (A, B, C, D) set, all match with A, but C and D don't match with B and that the + # (A, B) match is the highest (thus resulting in an (A, B) group), still match C and D + # in a separate group instead of discarding them. + A, B, C, D = [NamedObject() for _ in range(4)] + m1 = Match(A, B, 90) # This is the strongest "A" match + m2 = Match(A, C, 80) # Because C doesn't match with B, it won't be in the group + m3 = Match(A, D, 80) # Same thing for D + m4 = Match(C, D, 70) # However, because C and D match, they should have their own group. + groups = get_groups([m1, m2, m3, m4]) + eq_(len(groups), 2) + g1, g2 = groups + assert A in g1 + assert B in g1 + assert C in g2 + assert D in g2 + From 42ebef15dd774b8be4bc1f8d536bd7c05451e7ab Mon Sep 17 00:00:00 2001 From: hsoft Date: Sat, 5 Sep 2009 15:28:10 +0000 Subject: [PATCH 117/275] Refactoring: modernized scaner_test and got rid of the obsolete SCAN_TYPE_TAG_WITH_ALBUM scan type const. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40117 --- base/py/scanner.py | 8 +- base/py/tests/scanner_test.py | 872 +++++++++++++++++----------------- 2 files changed, 440 insertions(+), 440 deletions(-) diff --git a/base/py/scanner.py b/base/py/scanner.py index 0ac87dc2..320151c2 100644 --- a/base/py/scanner.py +++ b/base/py/scanner.py @@ -9,21 +9,20 @@ import logging -from ignore import IgnoreList from hsutil import job from hsutil.misc import dedupe from hsutil.str import get_file_ext, rem_file_ext from . import engine +from .ignore import IgnoreList (SCAN_TYPE_FILENAME, SCAN_TYPE_FIELDS, SCAN_TYPE_FIELDS_NO_ORDER, SCAN_TYPE_TAG, -SCAN_TYPE_TAG_WITH_ALBUM, # Obsolete SCAN_TYPE_CONTENT, -SCAN_TYPE_CONTENT_AUDIO) = range(7) +SCAN_TYPE_CONTENT_AUDIO) = range(6) SCANNABLE_TAGS = ['track', 'artist', 'album', 'title', 'genre', 'year'] @@ -42,9 +41,6 @@ class Scanner(object): if self.scan_type == SCAN_TYPE_FIELDS_NO_ORDER: self.scan_type = SCAN_TYPE_FIELDS mf.no_field_order = True - if self.scan_type == SCAN_TYPE_TAG_WITH_ALBUM: - self.scan_type = SCAN_TYPE_TAG - self.scanned_tags = set(['artist', 'album', 'title']) func = { SCAN_TYPE_FILENAME: lambda f: engine.getwords(rem_file_ext(f.name)), SCAN_TYPE_FIELDS: lambda f: engine.getfields(rem_file_ext(f.name)), diff --git a/base/py/tests/scanner_test.py b/base/py/tests/scanner_test.py index 8cd9587a..5356d030 100644 --- a/base/py/tests/scanner_test.py +++ b/base/py/tests/scanner_test.py @@ -7,9 +7,10 @@ # which should be included with this package. The terms are also available at # http://www.hardcoded.net/licenses/hs_license +from nose.tools import eq_ + from hsutil import job from hsutil.path import Path -from hsutil.testcase import TestCase from ..engine import getwords, Match from ..ignore import IgnoreList @@ -25,438 +26,441 @@ class NamedObject(object): no = NamedObject -class TCScanner(TestCase): - def test_empty(self): - s = Scanner() - r = s.GetDupeGroups([]) - self.assertEqual([],r) - - def test_default_settings(self): - s = Scanner() - self.assertEqual(80,s.min_match_percentage) - self.assertEqual(SCAN_TYPE_FILENAME,s.scan_type) - self.assertEqual(True,s.mix_file_kind) - self.assertEqual(False,s.word_weighting) - self.assertEqual(False,s.match_similar_words) - self.assert_(isinstance(s.ignore_list,IgnoreList)) - - def test_simple_with_default_settings(self): - s = Scanner() - f = [no('foo bar'),no('foo bar'),no('foo bleh')] - r = s.GetDupeGroups(f) - self.assertEqual(1,len(r)) - g = r[0] - #'foo bleh' cannot be in the group because the default min match % is 80 - self.assertEqual(2,len(g)) - self.assert_(g.ref in f[:2]) - self.assert_(g.dupes[0] in f[:2]) - - def test_simple_with_lower_min_match(self): - s = Scanner() - s.min_match_percentage = 50 - f = [no('foo bar'),no('foo bar'),no('foo bleh')] - r = s.GetDupeGroups(f) - self.assertEqual(1,len(r)) - g = r[0] - self.assertEqual(3,len(g)) - - def test_trim_all_ref_groups(self): - s = Scanner() - f = [no('foo'),no('foo'),no('bar'),no('bar')] - f[2].is_ref = True - f[3].is_ref = True - r = s.GetDupeGroups(f) - self.assertEqual(1,len(r)) - - def test_priorize(self): - s = Scanner() - f = [no('foo'),no('foo'),no('bar'),no('bar')] - f[1].size = 2 - f[2].size = 3 - f[3].is_ref = True - r = s.GetDupeGroups(f) - g1,g2 = r - self.assert_(f[1] in (g1.ref,g2.ref)) - self.assert_(f[0] in (g1.dupes[0],g2.dupes[0])) - self.assert_(f[3] in (g1.ref,g2.ref)) - self.assert_(f[2] in (g1.dupes[0],g2.dupes[0])) - - def test_content_scan(self): - s = Scanner() - s.scan_type = SCAN_TYPE_CONTENT - f = [no('foo'), no('bar'), no('bleh')] - f[0].md5 = 'foobar' - f[1].md5 = 'foobar' - f[2].md5 = 'bleh' - r = s.GetDupeGroups(f) - self.assertEqual(len(r), 1) - self.assertEqual(len(r[0]), 2) - self.assertEqual(s.discarded_file_count, 0) # don't count the different md5 as discarded! - - def test_content_scan_compare_sizes_first(self): - class MyFile(no): - def get_md5(file): - self.fail() - md5 = property(get_md5) - - s = Scanner() - s.scan_type = SCAN_TYPE_CONTENT - f = [MyFile('foo',1),MyFile('bar',2)] - self.assertEqual(0,len(s.GetDupeGroups(f))) - - def test_min_match_perc_doesnt_matter_for_content_scan(self): - s = Scanner() - s.scan_type = SCAN_TYPE_CONTENT - f = [no('foo'),no('bar'),no('bleh')] - f[0].md5 = 'foobar' - f[1].md5 = 'foobar' - f[2].md5 = 'bleh' - s.min_match_percentage = 101 - r = s.GetDupeGroups(f) - self.assertEqual(1,len(r)) - self.assertEqual(2,len(r[0])) - s.min_match_percentage = 0 - r = s.GetDupeGroups(f) - self.assertEqual(1,len(r)) - self.assertEqual(2,len(r[0])) - - def test_content_scan_puts_md5_in_words_at_the_end(self): - s = Scanner() - s.scan_type = SCAN_TYPE_CONTENT - f = [no('foo'),no('bar')] - f[0].md5 = '\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f' - f[1].md5 = '\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f' - r = s.GetDupeGroups(f) - g = r[0] - self.assertEqual(['--'],g.ref.words) - self.assertEqual(['--'],g.dupes[0].words) - - def test_extension_is_not_counted_in_filename_scan(self): - s = Scanner() - s.min_match_percentage = 100 - f = [no('foo.bar'),no('foo.bleh')] - r = s.GetDupeGroups(f) - self.assertEqual(1,len(r)) - self.assertEqual(2,len(r[0])) - - def test_job(self): - def do_progress(progress,desc=''): - log.append(progress) - return True - s = Scanner() - log = [] - f = [no('foo bar'),no('foo bar'),no('foo bleh')] - r = s.GetDupeGroups(f, job.Job(1,do_progress)) - self.assertEqual(0,log[0]) - self.assertEqual(100,log[-1]) - - def test_mix_file_kind(self): - s = Scanner() - s.mix_file_kind = False - f = [no('foo.1'),no('foo.2')] - r = s.GetDupeGroups(f) - self.assertEqual(0,len(r)) - - def test_word_weighting(self): - s = Scanner() - s.min_match_percentage = 75 - s.word_weighting = True - f = [no('foo bar'),no('foo bar bleh')] - r = s.GetDupeGroups(f) - self.assertEqual(1,len(r)) - g = r[0] - m = g.get_match_of(g.dupes[0]) - self.assertEqual(75,m.percentage) # 16 letters, 12 matching - - def test_similar_words(self): - s = Scanner() - s.match_similar_words = True - f = [no('The White Stripes'),no('The Whites Stripe'),no('Limp Bizkit'),no('Limp Bizkitt')] - r = s.GetDupeGroups(f) - self.assertEqual(2,len(r)) - - def test_fields(self): - s = Scanner() - s.scan_type = SCAN_TYPE_FIELDS - f = [no('The White Stripes - Little Ghost'),no('The White Stripes - Little Acorn')] - r = s.GetDupeGroups(f) - self.assertEqual(0,len(r)) - - def test_fields_no_order(self): - s = Scanner() - s.scan_type = SCAN_TYPE_FIELDS_NO_ORDER - f = [no('The White Stripes - Little Ghost'),no('Little Ghost - The White Stripes')] - r = s.GetDupeGroups(f) - self.assertEqual(1,len(r)) - - def test_tag_scan(self): - s = Scanner() - s.scan_type = SCAN_TYPE_TAG - o1 = no('foo') - o2 = no('bar') - o1.artist = 'The White Stripes' - o1.title = 'The Air Near My Fingers' - o2.artist = 'The White Stripes' - o2.title = 'The Air Near My Fingers' - r = s.GetDupeGroups([o1,o2]) - self.assertEqual(1,len(r)) - - def test_tag_with_album_scan(self): - s = Scanner() - s.scan_type = SCAN_TYPE_TAG_WITH_ALBUM - o1 = no('foo') - o2 = no('bar') - o3 = no('bleh') - o1.artist = 'The White Stripes' - o1.title = 'The Air Near My Fingers' - o1.album = 'Elephant' - o2.artist = 'The White Stripes' - o2.title = 'The Air Near My Fingers' - o2.album = 'Elephant' - o3.artist = 'The White Stripes' - o3.title = 'The Air Near My Fingers' - o3.album = 'foobar' - r = s.GetDupeGroups([o1,o2,o3]) - self.assertEqual(1,len(r)) - - def test_that_dash_in_tags_dont_create_new_fields(self): - s = Scanner() - s.scan_type = SCAN_TYPE_TAG_WITH_ALBUM - s.min_match_percentage = 50 - o1 = no('foo') - o2 = no('bar') - o1.artist = 'The White Stripes - a' - o1.title = 'The Air Near My Fingers - a' - o1.album = 'Elephant - a' - o2.artist = 'The White Stripes - b' - o2.title = 'The Air Near My Fingers - b' - o2.album = 'Elephant - b' - r = s.GetDupeGroups([o1,o2]) - self.assertEqual(1,len(r)) - - def test_tag_scan_with_different_scanned(self): - s = Scanner() - s.scan_type = SCAN_TYPE_TAG - s.scanned_tags = set(['track', 'year']) - o1 = no('foo') - o2 = no('bar') - o1.artist = 'The White Stripes' - o1.title = 'some title' - o1.track = 'foo' - o1.year = 'bar' - o2.artist = 'The White Stripes' - o2.title = 'another title' - o2.track = 'foo' - o2.year = 'bar' - r = s.GetDupeGroups([o1, o2]) - self.assertEqual(1, len(r)) - - def test_tag_scan_only_scans_existing_tags(self): - s = Scanner() - s.scan_type = SCAN_TYPE_TAG - s.scanned_tags = set(['artist', 'foo']) - o1 = no('foo') - o2 = no('bar') - o1.artist = 'The White Stripes' - o1.foo = 'foo' - o2.artist = 'The White Stripes' - o2.foo = 'bar' - r = s.GetDupeGroups([o1, o2]) - self.assertEqual(1, len(r)) # Because 'foo' is not scanned, they match - - def test_tag_scan_converts_to_str(self): - s = Scanner() - s.scan_type = SCAN_TYPE_TAG - s.scanned_tags = set(['track']) - o1 = no('foo') - o2 = no('bar') - o1.track = 42 - o2.track = 42 - try: - r = s.GetDupeGroups([o1, o2]) - except TypeError: - self.fail() - self.assertEqual(1, len(r)) - - def test_tag_scan_non_ascii(self): - s = Scanner() - s.scan_type = SCAN_TYPE_TAG - s.scanned_tags = set(['title']) - o1 = no('foo') - o2 = no('bar') - o1.title = u'foobar\u00e9' - o2.title = u'foobar\u00e9' - try: - r = s.GetDupeGroups([o1, o2]) - except UnicodeEncodeError: - self.fail() - self.assertEqual(1, len(r)) - - def test_audio_content_scan(self): - s = Scanner() - s.scan_type = SCAN_TYPE_CONTENT_AUDIO - f = [no('foo'),no('bar'),no('bleh')] - f[0].md5 = 'foo' - f[1].md5 = 'bar' - f[2].md5 = 'bleh' - f[0].md5partial = 'foo' - f[1].md5partial = 'foo' - f[2].md5partial = 'bleh' - f[0].audiosize = 1 - f[1].audiosize = 1 - f[2].audiosize = 1 - r = s.GetDupeGroups(f) - self.assertEqual(1,len(r)) - self.assertEqual(2,len(r[0])) - - def test_audio_content_scan_compare_sizes_first(self): - class MyFile(no): - def get_md5(file): - self.fail() - md5partial = property(get_md5) - - s = Scanner() - s.scan_type = SCAN_TYPE_CONTENT_AUDIO - f = [MyFile('foo'),MyFile('bar')] - f[0].audiosize = 1 - f[1].audiosize = 2 - self.assertEqual(0,len(s.GetDupeGroups(f))) - - def test_ignore_list(self): - s = Scanner() - f1 = no('foobar') - f2 = no('foobar') - f3 = no('foobar') - f1.path = Path('dir1/foobar') - f2.path = Path('dir2/foobar') - f3.path = Path('dir3/foobar') - s.ignore_list.Ignore(str(f1.path),str(f2.path)) - s.ignore_list.Ignore(str(f1.path),str(f3.path)) - r = s.GetDupeGroups([f1,f2,f3]) - self.assertEqual(1,len(r)) - g = r[0] - self.assertEqual(1,len(g.dupes)) - self.assert_(f1 not in g) - self.assert_(f2 in g) - self.assert_(f3 in g) - # Ignored matches are not counted as discarded - self.assertEqual(s.discarded_file_count, 0) - - def test_ignore_list_checks_for_unicode(self): - #scanner was calling path_str for ignore list checks. Since the Path changes, it must - #be unicode(path) - s = Scanner() - f1 = no('foobar') - f2 = no('foobar') - f3 = no('foobar') - f1.path = Path(u'foo1\u00e9') - f2.path = Path(u'foo2\u00e9') - f3.path = Path(u'foo3\u00e9') - s.ignore_list.Ignore(unicode(f1.path),unicode(f2.path)) - s.ignore_list.Ignore(unicode(f1.path),unicode(f3.path)) - r = s.GetDupeGroups([f1,f2,f3]) - self.assertEqual(1,len(r)) - g = r[0] - self.assertEqual(1,len(g.dupes)) - self.assert_(f1 not in g) - self.assert_(f2 in g) - self.assert_(f3 in g) - - def test_custom_match_factory(self): - class MatchFactory(object): - def getmatches(self,objects,j=None): - return [Match(objects[0], objects[1], 420)] - - - s = Scanner() - s.match_factory = MatchFactory() - o1,o2 = no('foo'),no('bar') - groups = s.GetDupeGroups([o1,o2]) - self.assertEqual(1,len(groups)) - g = groups[0] - self.assertEqual(2,len(g)) - g.switch_ref(o1) - m = g.get_match_of(o2) - self.assertEqual((o1,o2,420),m) - - def test_file_evaluates_to_false(self): - # A very wrong way to use any() was added at some point, causing resulting group list - # to be empty. - class FalseNamedObject(NamedObject): - def __nonzero__(self): - return False - - - s = Scanner() - f1 = FalseNamedObject('foobar') - f2 = FalseNamedObject('foobar') - r = s.GetDupeGroups([f1,f2]) - self.assertEqual(1,len(r)) - - def test_size_threshold(self): - # Only file equal or higher than the size_threshold in size are scanned - s = Scanner() - f1 = no('foo', 1) - f2 = no('foo', 2) - f3 = no('foo', 3) - s.size_threshold = 2 - groups = s.GetDupeGroups([f1,f2,f3]) - self.assertEqual(len(groups), 1) - [group] = groups - self.assertEqual(len(group), 2) - self.assertTrue(f1 not in group) - self.assertTrue(f2 in group) - self.assertTrue(f3 in group) - - def test_tie_breaker_path_deepness(self): - # If there is a tie in prioritization, path deepness is used as a tie breaker - s = Scanner() - o1, o2 = no('foo'), no('foo') - o1.path = Path('foo') - o2.path = Path('foo/bar') - [group] = s.GetDupeGroups([o1, o2]) - self.assertTrue(group.ref is o2) - - def test_tie_breaker_copy(self): - # if copy is in the words used (even if it has a deeper path), it becomes a dupe - s = Scanner() - o1, o2 = no('foo bar Copy'), no('foo bar') - o1.path = Path('deeper/path') - o2.path = Path('foo') - [group] = s.GetDupeGroups([o1, o2]) - self.assertTrue(group.ref is o2) - - def test_tie_breaker_same_name_plus_digit(self): - # if ref has the same words as dupe, but has some just one extra word which is a digit, it - # becomes a dupe - s = Scanner() - o1, o2 = no('foo bar 42'), no('foo bar') - o1.path = Path('deeper/path') - o2.path = Path('foo') - [group] = s.GetDupeGroups([o1, o2]) - self.assertTrue(group.ref is o2) - - def test_partial_group_match(self): - # Count the number od discarded matches (when a file doesn't match all other dupes of the - # group) in Scanner.discarded_file_count - s = Scanner() - o1, o2, o3 = no('a b'), no('a'), no('b') - s.min_match_percentage = 50 - [group] = s.GetDupeGroups([o1, o2, o3]) - self.assertEqual(len(group), 2) - self.assertTrue(o1 in group) - self.assertTrue(o2 in group) - self.assertTrue(o3 not in group) - self.assertEqual(s.discarded_file_count, 1) - +#--- Scanner +def test_empty(): + s = Scanner() + r = s.GetDupeGroups([]) + eq_(r, []) -class TCScannerME(TestCase): - def test_priorize(self): - # in ScannerME, bitrate goes first (right after is_ref) in priorization - s = ScannerME() - o1, o2 = no('foo'), no('foo') - o1.bitrate = 1 - o2.bitrate = 2 - [group] = s.GetDupeGroups([o1, o2]) - self.assertTrue(group.ref is o2) +def test_default_settings(): + s = Scanner() + eq_(s.min_match_percentage, 80) + eq_(s.scan_type, SCAN_TYPE_FILENAME) + eq_(s.mix_file_kind, True) + eq_(s.word_weighting, False) + eq_(s.match_similar_words, False) + assert isinstance(s.ignore_list, IgnoreList) + +def test_simple_with_default_settings(): + s = Scanner() + f = [no('foo bar'), no('foo bar'), no('foo bleh')] + r = s.GetDupeGroups(f) + eq_(len(r), 1) + g = r[0] + #'foo bleh' cannot be in the group because the default min match % is 80 + eq_(len(g), 2) + assert g.ref in f[:2] + assert g.dupes[0] in f[:2] + +def test_simple_with_lower_min_match(): + s = Scanner() + s.min_match_percentage = 50 + f = [no('foo bar'), no('foo bar'), no('foo bleh')] + r = s.GetDupeGroups(f) + eq_(len(r), 1) + g = r[0] + eq_(len(g), 3) + +def test_trim_all_ref_groups(): + s = Scanner() + f = [no('foo'), no('foo'), no('bar'), no('bar')] + f[2].is_ref = True + f[3].is_ref = True + r = s.GetDupeGroups(f) + eq_(len(r), 1) + +def test_priorize(): + s = Scanner() + f = [no('foo'), no('foo'), no('bar'), no('bar')] + f[1].size = 2 + f[2].size = 3 + f[3].is_ref = True + r = s.GetDupeGroups(f) + g1, g2 = r + assert f[1] in (g1.ref,g2.ref) + assert f[0] in (g1.dupes[0],g2.dupes[0]) + assert f[3] in (g1.ref,g2.ref) + assert f[2] in (g1.dupes[0],g2.dupes[0]) + +def test_content_scan(): + s = Scanner() + s.scan_type = SCAN_TYPE_CONTENT + f = [no('foo'), no('bar'), no('bleh')] + f[0].md5 = 'foobar' + f[1].md5 = 'foobar' + f[2].md5 = 'bleh' + r = s.GetDupeGroups(f) + eq_(len(r), 1) + eq_(len(r[0]), 2) + eq_(s.discarded_file_count, 0) # don't count the different md5 as discarded! + +def test_content_scan_compare_sizes_first(): + class MyFile(no): + @property + def md5(file): + raise AssertionError() + s = Scanner() + s.scan_type = SCAN_TYPE_CONTENT + f = [MyFile('foo', 1), MyFile('bar', 2)] + eq_(len(s.GetDupeGroups(f)), 0) + +def test_min_match_perc_doesnt_matter_for_content_scan(): + s = Scanner() + s.scan_type = SCAN_TYPE_CONTENT + f = [no('foo'), no('bar'), no('bleh')] + f[0].md5 = 'foobar' + f[1].md5 = 'foobar' + f[2].md5 = 'bleh' + s.min_match_percentage = 101 + r = s.GetDupeGroups(f) + eq_(len(r), 1) + eq_(len(r[0]), 2) + s.min_match_percentage = 0 + r = s.GetDupeGroups(f) + eq_(len(r), 1) + eq_(len(r[0]), 2) + +def test_content_scan_puts_md5_in_words_at_the_end(): + s = Scanner() + s.scan_type = SCAN_TYPE_CONTENT + f = [no('foo'),no('bar')] + f[0].md5 = '\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f' + f[1].md5 = '\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f' + r = s.GetDupeGroups(f) + g = r[0] + eq_(g.ref.words, ['--']) + eq_(g.dupes[0].words, ['--']) + +def test_extension_is_not_counted_in_filename_scan(): + s = Scanner() + s.min_match_percentage = 100 + f = [no('foo.bar'), no('foo.bleh')] + r = s.GetDupeGroups(f) + eq_(len(r), 1) + eq_(len(r[0]), 2) + +def test_job(): + def do_progress(progress, desc=''): + log.append(progress) + return True + + s = Scanner() + log = [] + f = [no('foo bar'), no('foo bar'), no('foo bleh')] + r = s.GetDupeGroups(f, job.Job(1, do_progress)) + eq_(log[0], 0) + eq_(log[-1], 100) + +def test_mix_file_kind(): + s = Scanner() + s.mix_file_kind = False + f = [no('foo.1'), no('foo.2')] + r = s.GetDupeGroups(f) + eq_(len(r), 0) + +def test_word_weighting(): + s = Scanner() + s.min_match_percentage = 75 + s.word_weighting = True + f = [no('foo bar'), no('foo bar bleh')] + r = s.GetDupeGroups(f) + eq_(len(r), 1) + g = r[0] + m = g.get_match_of(g.dupes[0]) + eq_(m.percentage, 75) # 16 letters, 12 matching + +def test_similar_words(): + s = Scanner() + s.match_similar_words = True + f = [no('The White Stripes'), no('The Whites Stripe'), no('Limp Bizkit'), no('Limp Bizkitt')] + r = s.GetDupeGroups(f) + eq_(len(r), 2) + +def test_fields(): + s = Scanner() + s.scan_type = SCAN_TYPE_FIELDS + f = [no('The White Stripes - Little Ghost'), no('The White Stripes - Little Acorn')] + r = s.GetDupeGroups(f) + eq_(len(r), 0) + +def test_fields_no_order(): + s = Scanner() + s.scan_type = SCAN_TYPE_FIELDS_NO_ORDER + f = [no('The White Stripes - Little Ghost'), no('Little Ghost - The White Stripes')] + r = s.GetDupeGroups(f) + eq_(len(r), 1) + +def test_tag_scan(): + s = Scanner() + s.scan_type = SCAN_TYPE_TAG + o1 = no('foo') + o2 = no('bar') + o1.artist = 'The White Stripes' + o1.title = 'The Air Near My Fingers' + o2.artist = 'The White Stripes' + o2.title = 'The Air Near My Fingers' + r = s.GetDupeGroups([o1,o2]) + eq_(len(r), 1) + +def test_tag_with_album_scan(): + s = Scanner() + s.scan_type = SCAN_TYPE_TAG + s.scanned_tags = set(['artist', 'album', 'title']) + o1 = no('foo') + o2 = no('bar') + o3 = no('bleh') + o1.artist = 'The White Stripes' + o1.title = 'The Air Near My Fingers' + o1.album = 'Elephant' + o2.artist = 'The White Stripes' + o2.title = 'The Air Near My Fingers' + o2.album = 'Elephant' + o3.artist = 'The White Stripes' + o3.title = 'The Air Near My Fingers' + o3.album = 'foobar' + r = s.GetDupeGroups([o1,o2,o3]) + eq_(len(r), 1) + +def test_that_dash_in_tags_dont_create_new_fields(): + s = Scanner() + s.scan_type = SCAN_TYPE_TAG + s.scanned_tags = set(['artist', 'album', 'title']) + s.min_match_percentage = 50 + o1 = no('foo') + o2 = no('bar') + o1.artist = 'The White Stripes - a' + o1.title = 'The Air Near My Fingers - a' + o1.album = 'Elephant - a' + o2.artist = 'The White Stripes - b' + o2.title = 'The Air Near My Fingers - b' + o2.album = 'Elephant - b' + r = s.GetDupeGroups([o1,o2]) + eq_(len(r), 1) + +def test_tag_scan_with_different_scanned(): + s = Scanner() + s.scan_type = SCAN_TYPE_TAG + s.scanned_tags = set(['track', 'year']) + o1 = no('foo') + o2 = no('bar') + o1.artist = 'The White Stripes' + o1.title = 'some title' + o1.track = 'foo' + o1.year = 'bar' + o2.artist = 'The White Stripes' + o2.title = 'another title' + o2.track = 'foo' + o2.year = 'bar' + r = s.GetDupeGroups([o1, o2]) + eq_(len(r), 1) + +def test_tag_scan_only_scans_existing_tags(): + s = Scanner() + s.scan_type = SCAN_TYPE_TAG + s.scanned_tags = set(['artist', 'foo']) + o1 = no('foo') + o2 = no('bar') + o1.artist = 'The White Stripes' + o1.foo = 'foo' + o2.artist = 'The White Stripes' + o2.foo = 'bar' + r = s.GetDupeGroups([o1, o2]) + eq_(len(r), 1) # Because 'foo' is not scanned, they match + +def test_tag_scan_converts_to_str(): + s = Scanner() + s.scan_type = SCAN_TYPE_TAG + s.scanned_tags = set(['track']) + o1 = no('foo') + o2 = no('bar') + o1.track = 42 + o2.track = 42 + try: + r = s.GetDupeGroups([o1, o2]) + except TypeError: + raise AssertionError() + eq_(len(r), 1) + +def test_tag_scan_non_ascii(): + s = Scanner() + s.scan_type = SCAN_TYPE_TAG + s.scanned_tags = set(['title']) + o1 = no('foo') + o2 = no('bar') + o1.title = u'foobar\u00e9' + o2.title = u'foobar\u00e9' + try: + r = s.GetDupeGroups([o1, o2]) + except UnicodeEncodeError: + raise AssertionError() + eq_(len(r), 1) + +def test_audio_content_scan(): + s = Scanner() + s.scan_type = SCAN_TYPE_CONTENT_AUDIO + f = [no('foo'), no('bar'), no('bleh')] + f[0].md5 = 'foo' + f[1].md5 = 'bar' + f[2].md5 = 'bleh' + f[0].md5partial = 'foo' + f[1].md5partial = 'foo' + f[2].md5partial = 'bleh' + f[0].audiosize = 1 + f[1].audiosize = 1 + f[2].audiosize = 1 + r = s.GetDupeGroups(f) + eq_(len(r), 1) + eq_(len(r[0]), 2) + +def test_audio_content_scan_compare_sizes_first(): + class MyFile(no): + @property + def md5partial(file): + raise AssertionError() + + s = Scanner() + s.scan_type = SCAN_TYPE_CONTENT_AUDIO + f = [MyFile('foo'), MyFile('bar')] + f[0].audiosize = 1 + f[1].audiosize = 2 + eq_(len(s.GetDupeGroups(f)), 0) + +def test_ignore_list(): + s = Scanner() + f1 = no('foobar') + f2 = no('foobar') + f3 = no('foobar') + f1.path = Path('dir1/foobar') + f2.path = Path('dir2/foobar') + f3.path = Path('dir3/foobar') + s.ignore_list.Ignore(str(f1.path),str(f2.path)) + s.ignore_list.Ignore(str(f1.path),str(f3.path)) + r = s.GetDupeGroups([f1,f2,f3]) + eq_(len(r), 1) + g = r[0] + eq_(len(g.dupes), 1) + assert f1 not in g + assert f2 in g + assert f3 in g + # Ignored matches are not counted as discarded + eq_(s.discarded_file_count, 0) + +def test_ignore_list_checks_for_unicode(): + #scanner was calling path_str for ignore list checks. Since the Path changes, it must + #be unicode(path) + s = Scanner() + f1 = no('foobar') + f2 = no('foobar') + f3 = no('foobar') + f1.path = Path(u'foo1\u00e9') + f2.path = Path(u'foo2\u00e9') + f3.path = Path(u'foo3\u00e9') + s.ignore_list.Ignore(unicode(f1.path),unicode(f2.path)) + s.ignore_list.Ignore(unicode(f1.path),unicode(f3.path)) + r = s.GetDupeGroups([f1,f2,f3]) + eq_(len(r), 1) + g = r[0] + eq_(len(g.dupes), 1) + assert f1 not in g + assert f2 in g + assert f3 in g + +def test_custom_match_factory(): + class MatchFactory(object): + def getmatches(self, objects, j=None): + return [Match(objects[0], objects[1], 420)] + + + s = Scanner() + s.match_factory = MatchFactory() + o1, o2 = no('foo'), no('bar') + groups = s.GetDupeGroups([o1, o2]) + eq_(len(groups), 1) + g = groups[0] + eq_(len(g), 2) + g.switch_ref(o1) + m = g.get_match_of(o2) + eq_(m, (o1, o2, 420)) + +def test_file_evaluates_to_false(): + # A very wrong way to use any() was added at some point, causing resulting group list + # to be empty. + class FalseNamedObject(NamedObject): + def __nonzero__(self): + return False + + + s = Scanner() + f1 = FalseNamedObject('foobar') + f2 = FalseNamedObject('foobar') + r = s.GetDupeGroups([f1, f2]) + eq_(len(r), 1) + +def test_size_threshold(): + # Only file equal or higher than the size_threshold in size are scanned + s = Scanner() + f1 = no('foo', 1) + f2 = no('foo', 2) + f3 = no('foo', 3) + s.size_threshold = 2 + groups = s.GetDupeGroups([f1,f2,f3]) + eq_(len(groups), 1) + [group] = groups + eq_(len(group), 2) + assert f1 not in group + assert f2 in group + assert f3 in group + +def test_tie_breaker_path_deepness(): + # If there is a tie in prioritization, path deepness is used as a tie breaker + s = Scanner() + o1, o2 = no('foo'), no('foo') + o1.path = Path('foo') + o2.path = Path('foo/bar') + [group] = s.GetDupeGroups([o1, o2]) + assert group.ref is o2 + +def test_tie_breaker_copy(): + # if copy is in the words used (even if it has a deeper path), it becomes a dupe + s = Scanner() + o1, o2 = no('foo bar Copy'), no('foo bar') + o1.path = Path('deeper/path') + o2.path = Path('foo') + [group] = s.GetDupeGroups([o1, o2]) + assert group.ref is o2 + +def test_tie_breaker_same_name_plus_digit(): + # if ref has the same words as dupe, but has some just one extra word which is a digit, it + # becomes a dupe + s = Scanner() + o1, o2 = no('foo bar 42'), no('foo bar') + o1.path = Path('deeper/path') + o2.path = Path('foo') + [group] = s.GetDupeGroups([o1, o2]) + assert group.ref is o2 + +def test_partial_group_match(): + # Count the number od discarded matches (when a file doesn't match all other dupes of the + # group) in Scanner.discarded_file_count + s = Scanner() + o1, o2, o3 = no('a b'), no('a'), no('b') + s.min_match_percentage = 50 + [group] = s.GetDupeGroups([o1, o2, o3]) + eq_(len(group), 2) + assert o1 in group + assert o2 in group + assert o3 not in group + eq_(s.discarded_file_count, 1) + + +#--- Scanner ME +def test_priorize_me(): + # in ScannerME, bitrate goes first (right after is_ref) in priorization + s = ScannerME() + o1, o2 = no('foo'), no('foo') + o1.bitrate = 1 + o2.bitrate = 2 + [group] = s.GetDupeGroups([o1, o2]) + assert group.ref is o2 + From 65944ef81382be19210332c062bf913b00f9d6f7 Mon Sep 17 00:00:00 2001 From: hsoft Date: Sat, 5 Sep 2009 16:27:50 +0000 Subject: [PATCH 118/275] [#33 state:fixed] md5partial is now used before md5 in Contents scans. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40118 --- base/py/scanner.py | 27 ++++++++++++++++----------- base/py/tests/scanner_test.py | 18 +++++++++--------- 2 files changed, 25 insertions(+), 20 deletions(-) diff --git a/base/py/scanner.py b/base/py/scanner.py index 320151c2..a4a8891f 100644 --- a/base/py/scanner.py +++ b/base/py/scanner.py @@ -31,6 +31,16 @@ class Scanner(object): self.ignore_list = IgnoreList() self.discarded_file_count = 0 + @staticmethod + def _filter_matches_by_content(matches, partial, j): + matched_files = dedupe([m.first for m in matches] + [m.second for m in matches]) + md5attrname = 'md5partial' if partial else 'md5' + md5 = lambda f: getattr(f, md5attrname) + for matched_file in j.iter_with_progress(matched_files, 'Analyzed %d/%d matching files'): + md5(matched_file) + j.set_progress(100, 'Removing false matches') + return [m for m in matches if md5(m.first) == md5(m.second)] + def _getmatches(self, files, j): j = j.start_subjob(2) mf = engine.MatchFactory() @@ -88,19 +98,14 @@ class Scanner(object): iter_matches = j.iter_with_progress(matches, 'Processed %d/%d matches against the ignore list') matches = [m for m in iter_matches if not self.ignore_list.AreIgnored(unicode(m.first.path), unicode(m.second.path))] - matched_files = dedupe([m.first for m in matches] + [m.second for m in matches]) if self.scan_type in (SCAN_TYPE_CONTENT, SCAN_TYPE_CONTENT_AUDIO): - md5attrname = 'md5partial' if self.scan_type == SCAN_TYPE_CONTENT_AUDIO else 'md5' - md5 = lambda f: getattr(f, md5attrname) - j = j.start_subjob(2) - for matched_file in j.iter_with_progress(matched_files, 'Analyzed %d/%d matching files'): - md5(matched_file) - j.set_progress(100, 'Removing false matches') - matches = [m for m in matches if md5(m.first) == md5(m.second)] - words_for_content = ['--'] # We compared md5. No words were involved. + j = j.start_subjob(3 if self.scan_type == SCAN_TYPE_CONTENT else 2) + matches = self._filter_matches_by_content(matches, partial=True, j=j) + if self.scan_type == SCAN_TYPE_CONTENT: + matches = self._filter_matches_by_content(matches, partial=False, j=j) + # We compared md5. No words were involved. for m in matches: - m.first.words = words_for_content - m.second.words = words_for_content + m.first.words = m.second.words = ['--'] logging.info('Grouping matches') groups = engine.get_groups(matches, j) groups = [g for g in groups if any(not f.is_ref for f in g)] diff --git a/base/py/tests/scanner_test.py b/base/py/tests/scanner_test.py index 5356d030..d683e405 100644 --- a/base/py/tests/scanner_test.py +++ b/base/py/tests/scanner_test.py @@ -86,9 +86,9 @@ def test_content_scan(): s = Scanner() s.scan_type = SCAN_TYPE_CONTENT f = [no('foo'), no('bar'), no('bleh')] - f[0].md5 = 'foobar' - f[1].md5 = 'foobar' - f[2].md5 = 'bleh' + f[0].md5 = f[0].md5partial = 'foobar' + f[1].md5 = f[1].md5partial = 'foobar' + f[2].md5 = f[2].md5partial = 'bleh' r = s.GetDupeGroups(f) eq_(len(r), 1) eq_(len(r[0]), 2) @@ -109,9 +109,9 @@ def test_min_match_perc_doesnt_matter_for_content_scan(): s = Scanner() s.scan_type = SCAN_TYPE_CONTENT f = [no('foo'), no('bar'), no('bleh')] - f[0].md5 = 'foobar' - f[1].md5 = 'foobar' - f[2].md5 = 'bleh' + f[0].md5 = f[0].md5partial = 'foobar' + f[1].md5 = f[1].md5partial = 'foobar' + f[2].md5 = f[2].md5partial = 'bleh' s.min_match_percentage = 101 r = s.GetDupeGroups(f) eq_(len(r), 1) @@ -121,12 +121,12 @@ def test_min_match_perc_doesnt_matter_for_content_scan(): eq_(len(r), 1) eq_(len(r[0]), 2) -def test_content_scan_puts_md5_in_words_at_the_end(): +def test_content_scan_doesnt_put_md5_in_words_at_the_end(): s = Scanner() s.scan_type = SCAN_TYPE_CONTENT f = [no('foo'),no('bar')] - f[0].md5 = '\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f' - f[1].md5 = '\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f' + f[0].md5 = f[0].md5partial = '\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f' + f[1].md5 = f[1].md5partial = '\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f' r = s.GetDupeGroups(f) g = r[0] eq_(g.ref.words, ['--']) From 4e7e4c98500d8cb8b5004d45cb8ae8e7053d1ce5 Mon Sep 17 00:00:00 2001 From: hsoft Date: Sun, 6 Sep 2009 08:07:50 +0000 Subject: [PATCH 119/275] [#54 state:fixed] Implemented enable/disable mechanism for pref pane widget based on selected scan type. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40119 --- me/qt/preferences_dialog.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/me/qt/preferences_dialog.py b/me/qt/preferences_dialog.py index 424314af..11905db1 100644 --- a/me/qt/preferences_dialog.py +++ b/me/qt/preferences_dialog.py @@ -33,6 +33,7 @@ class PreferencesDialog(QDialog, Ui_PreferencesDialog): self._setupUi() self.connect(self.buttonBox, SIGNAL('clicked(QAbstractButton*)'), self.buttonClicked) + self.connect(self.scanTypeComboBox, SIGNAL('currentIndexChanged(int)'), self.scanTypeChanged) def _setupUi(self): self.setupUi(self) @@ -85,3 +86,18 @@ class PreferencesDialog(QDialog, Ui_PreferencesDialog): if role == QDialogButtonBox.ResetRole: self.resetToDefaults() + def scanTypeChanged(self, index): + scan_type = SCAN_TYPE_ORDER[self.scanTypeComboBox.currentIndex()] + word_based = scan_type in [SCAN_TYPE_FILENAME, SCAN_TYPE_FIELDS, SCAN_TYPE_FIELDS_NO_ORDER, + SCAN_TYPE_TAG] + tag_based = scan_type == SCAN_TYPE_TAG + self.filterHardnessSlider.setEnabled(word_based) + self.matchSimilarBox.setEnabled(word_based) + self.wordWeightingBox.setEnabled(word_based) + self.tagTrackBox.setEnabled(tag_based) + self.tagArtistBox.setEnabled(tag_based) + self.tagAlbumBox.setEnabled(tag_based) + self.tagTitleBox.setEnabled(tag_based) + self.tagGenreBox.setEnabled(tag_based) + self.tagYearBox.setEnabled(tag_based) + From 2c3aa54c3f8745935574b201bb2a0b86a6af72fb Mon Sep 17 00:00:00 2001 From: hsoft Date: Sun, 6 Sep 2009 08:17:19 +0000 Subject: [PATCH 120/275] [#52] Improved the still shaky os x support for dg-qt. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40121 --- base/qt/osx/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/base/qt/osx/__init__.py b/base/qt/osx/__init__.py index 768bcadc..d5db85d1 100644 --- a/base/qt/osx/__init__.py +++ b/base/qt/osx/__init__.py @@ -12,7 +12,6 @@ import os import os.path as op CMD = unicode(op.join(op.dirname(__file__), 'SendToTrash')) -print CMD def recycle_file(path): print u'%s "%s"' % (CMD, unicode(path)) From c9d5d6fe000b503f9d00817a55cf8a977cd6408f Mon Sep 17 00:00:00 2001 From: hsoft Date: Sun, 6 Sep 2009 08:22:33 +0000 Subject: [PATCH 121/275] [#52] Removed OS X support for dg-qt from the main branch (it is kept in the qt-osx-support branch). --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40122 --- base/qt/app.py | 2 - base/qt/osx/SendToTrashProject/SendToTrash.m | 23 --- .../SendToTrash.xcodeproj/project.pbxproj | 183 ------------------ base/qt/osx/__init__.py | 18 -- 4 files changed, 226 deletions(-) delete mode 100644 base/qt/osx/SendToTrashProject/SendToTrash.m delete mode 100644 base/qt/osx/SendToTrashProject/SendToTrash.xcodeproj/project.pbxproj delete mode 100644 base/qt/osx/__init__.py diff --git a/base/qt/app.py b/base/qt/app.py index 8bb7c24c..fc471a90 100644 --- a/base/qt/app.py +++ b/base/qt/app.py @@ -24,8 +24,6 @@ from dupeguru.app import (DupeGuru as DupeGuruBase, JOB_SCAN, JOB_LOAD, JOB_MOVE if sys.platform == 'win32': from .win import recycle_file -elif sys.platform == 'darwin': - from .osx import recycle_file else: logging.warning("Unsupported Platform!!!") diff --git a/base/qt/osx/SendToTrashProject/SendToTrash.m b/base/qt/osx/SendToTrashProject/SendToTrash.m deleted file mode 100644 index 26401bcf..00000000 --- a/base/qt/osx/SendToTrashProject/SendToTrash.m +++ /dev/null @@ -1,23 +0,0 @@ -#import - -int main (int argc, const char * argv[]) { - if(argc == 1){ - NSLog(@"A file path to send to trash is needed"); - return 1; - } - NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init]; - NSString *filepath = [NSString stringWithCString:argv[1] encoding:NSUTF8StringEncoding]; - NSLog(@"%@",filepath); - NSMutableArray *split = [NSMutableArray arrayWithArray:[filepath componentsSeparatedByString:@"/"]]; - NSString *filename = [split lastObject]; - [split removeLastObject]; - NSString *dirpath = [split componentsJoinedByString:@"/"]; - int result; - [[NSWorkspace sharedWorkspace] performFileOperation:NSWorkspaceRecycleOperation - source:dirpath - destination:@"" - files:[NSArray arrayWithObject:filename] - tag:&result]; - [pool drain]; - return result; -} diff --git a/base/qt/osx/SendToTrashProject/SendToTrash.xcodeproj/project.pbxproj b/base/qt/osx/SendToTrashProject/SendToTrash.xcodeproj/project.pbxproj deleted file mode 100644 index 43f432d4..00000000 --- a/base/qt/osx/SendToTrashProject/SendToTrash.xcodeproj/project.pbxproj +++ /dev/null @@ -1,183 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 45; - objects = { - -/* Begin PBXBuildFile section */ - 8DD76F9A0486AA7600D96B5E /* SendToTrash.m in Sources */ = {isa = PBXBuildFile; fileRef = 08FB7796FE84155DC02AAC07 /* SendToTrash.m */; settings = {ATTRIBUTES = (); }; }; - 8DD76F9C0486AA7600D96B5E /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 08FB779EFE84155DC02AAC07 /* Foundation.framework */; }; - CEFA8C60104BD73200E2A946 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CEFA8C5F104BD73200E2A946 /* Cocoa.framework */; }; -/* End PBXBuildFile section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 8DD76F9E0486AA7600D96B5E /* CopyFiles */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 8; - dstPath = /usr/share/man/man1/; - dstSubfolderSpec = 0; - files = ( - ); - runOnlyForDeploymentPostprocessing = 1; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 08FB7796FE84155DC02AAC07 /* SendToTrash.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SendToTrash.m; sourceTree = ""; }; - 08FB779EFE84155DC02AAC07 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = /System/Library/Frameworks/Foundation.framework; sourceTree = ""; }; - 8DD76FA10486AA7600D96B5E /* SendToTrash */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = SendToTrash; sourceTree = BUILT_PRODUCTS_DIR; }; - CEFA8C5F104BD73200E2A946 /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = /System/Library/Frameworks/Cocoa.framework; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 8DD76F9B0486AA7600D96B5E /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 8DD76F9C0486AA7600D96B5E /* Foundation.framework in Frameworks */, - CEFA8C60104BD73200E2A946 /* Cocoa.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 08FB7794FE84155DC02AAC07 /* SendToTrash */ = { - isa = PBXGroup; - children = ( - 08FB7795FE84155DC02AAC07 /* Source */, - 08FB779DFE84155DC02AAC07 /* External Frameworks and Libraries */, - 1AB674ADFE9D54B511CA2CBB /* Products */, - ); - name = SendToTrash; - sourceTree = ""; - }; - 08FB7795FE84155DC02AAC07 /* Source */ = { - isa = PBXGroup; - children = ( - 08FB7796FE84155DC02AAC07 /* SendToTrash.m */, - ); - name = Source; - sourceTree = ""; - }; - 08FB779DFE84155DC02AAC07 /* External Frameworks and Libraries */ = { - isa = PBXGroup; - children = ( - 08FB779EFE84155DC02AAC07 /* Foundation.framework */, - CEFA8C5F104BD73200E2A946 /* Cocoa.framework */, - ); - name = "External Frameworks and Libraries"; - sourceTree = ""; - }; - 1AB674ADFE9D54B511CA2CBB /* Products */ = { - isa = PBXGroup; - children = ( - 8DD76FA10486AA7600D96B5E /* SendToTrash */, - ); - name = Products; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 8DD76F960486AA7600D96B5E /* SendToTrash */ = { - isa = PBXNativeTarget; - buildConfigurationList = 1DEB927408733DD40010E9CD /* Build configuration list for PBXNativeTarget "SendToTrash" */; - buildPhases = ( - 8DD76F990486AA7600D96B5E /* Sources */, - 8DD76F9B0486AA7600D96B5E /* Frameworks */, - 8DD76F9E0486AA7600D96B5E /* CopyFiles */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = SendToTrash; - productInstallPath = "$(HOME)/bin"; - productName = SendToTrash; - productReference = 8DD76FA10486AA7600D96B5E /* SendToTrash */; - productType = "com.apple.product-type.tool"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 08FB7793FE84155DC02AAC07 /* Project object */ = { - isa = PBXProject; - buildConfigurationList = 1DEB927808733DD40010E9CD /* Build configuration list for PBXProject "SendToTrash" */; - compatibilityVersion = "Xcode 3.1"; - hasScannedForEncodings = 1; - mainGroup = 08FB7794FE84155DC02AAC07 /* SendToTrash */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 8DD76F960486AA7600D96B5E /* SendToTrash */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXSourcesBuildPhase section */ - 8DD76F990486AA7600D96B5E /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 8DD76F9A0486AA7600D96B5E /* SendToTrash.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin XCBuildConfiguration section */ - 1DEB927608733DD40010E9CD /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - GCC_MODEL_TUNING = G5; - GCC_PRECOMPILE_PREFIX_HEADER = NO; - GCC_PREFIX_HEADER = ""; - INSTALL_PATH = /usr/local/bin; - PRODUCT_NAME = SendToTrash; - }; - name = Release; - }; - 1DEB927A08733DD40010E9CD /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ARCHS = "$(ARCHS_STANDARD_32_BIT)"; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_ENABLE_SYMBOL_SEPARATION = NO; - GCC_THREADSAFE_STATICS = NO; - GCC_WARN_ABOUT_RETURN_TYPE = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.4; - PREBINDING = NO; - PRECOMPS_INCLUDE_HEADERS_FROM_BUILT_PRODUCTS_DIR = NO; - SDKROOT = macosx10.5; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 1DEB927408733DD40010E9CD /* Build configuration list for PBXNativeTarget "SendToTrash" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 1DEB927608733DD40010E9CD /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 1DEB927808733DD40010E9CD /* Build configuration list for PBXProject "SendToTrash" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 1DEB927A08733DD40010E9CD /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 08FB7793FE84155DC02AAC07 /* Project object */; -} diff --git a/base/qt/osx/__init__.py b/base/qt/osx/__init__.py deleted file mode 100644 index d5db85d1..00000000 --- a/base/qt/osx/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -# -*- coding: utf-8 -*- -# Created By: Virgil Dupras -# Created On: 2009-08-31 -# $Id$ -# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "HS" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at -# http://www.hardcoded.net/licenses/hs_license - -import os -import os.path as op - -CMD = unicode(op.join(op.dirname(__file__), 'SendToTrash')) - -def recycle_file(path): - print u'%s "%s"' % (CMD, unicode(path)) - os.system(u'%s "%s"' % (CMD, unicode(path))) From 4ade9f1413228008c75e30670d9072567a291391 Mon Sep 17 00:00:00 2001 From: hsoft Date: Sun, 6 Sep 2009 08:25:56 +0000 Subject: [PATCH 122/275] Adapted the ME port to the latest qt changes. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40123 --- me/qt/app_win.py | 18 ------------------ me/qt/start.py | 5 +---- 2 files changed, 1 insertion(+), 22 deletions(-) delete mode 100644 me/qt/app_win.py diff --git a/me/qt/app_win.py b/me/qt/app_win.py deleted file mode 100644 index a096fdf8..00000000 --- a/me/qt/app_win.py +++ /dev/null @@ -1,18 +0,0 @@ -# Created By: Virgil Dupras -# Created On: 2009-05-21 -# $Id$ -# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "HS" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at -# http://www.hardcoded.net/licenses/hs_license - -import winshell - -import app - -class DupeGuru(app.DupeGuru): - @staticmethod - def _recycle_dupe(dupe): - winshell.delete_file(unicode(dupe.path), no_confirm=True) - diff --git a/me/qt/start.py b/me/qt/start.py index ca0783ea..ed37e0a5 100644 --- a/me/qt/start.py +++ b/me/qt/start.py @@ -12,10 +12,7 @@ from PyQt4.QtGui import QApplication, QIcon, QPixmap import base.dg_rc -if sys.platform == 'win32': - from app_win import DupeGuru -else: - from app import DupeGuru +from app import DupeGuru if __name__ == "__main__": app = QApplication(sys.argv) From b5b86bd89d34f6cd61a5c01170ba8a84a55aae0c Mon Sep 17 00:00:00 2001 From: hsoft Date: Sun, 6 Sep 2009 08:27:44 +0000 Subject: [PATCH 123/275] Adapted the PE port to the latest qt changes. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40124 --- pe/qt/app_win.py | 18 ------------------ pe/qt/start.py | 5 +---- 2 files changed, 1 insertion(+), 22 deletions(-) delete mode 100644 pe/qt/app_win.py diff --git a/pe/qt/app_win.py b/pe/qt/app_win.py deleted file mode 100644 index 95f8b112..00000000 --- a/pe/qt/app_win.py +++ /dev/null @@ -1,18 +0,0 @@ -# Created By: Virgil Dupras -# Created On: 2009-05-02 -# $Id$ -# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "HS" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at -# http://www.hardcoded.net/licenses/hs_license - -import winshell - -import app - -class DupeGuru(app.DupeGuru): - @staticmethod - def _recycle_dupe(dupe): - winshell.delete_file(unicode(dupe.path), no_confirm=True) - diff --git a/pe/qt/start.py b/pe/qt/start.py index e3c523fb..7fb5a367 100644 --- a/pe/qt/start.py +++ b/pe/qt/start.py @@ -12,10 +12,7 @@ from PyQt4.QtGui import QApplication, QIcon, QPixmap import base.dg_rc -if sys.platform == 'win32': - from app_win import DupeGuru -else: - from app import DupeGuru +from app import DupeGuru if __name__ == "__main__": app = QApplication(sys.argv) From eea31b3c3fac3aae5ede6b7ad344966cc2acef3a Mon Sep 17 00:00:00 2001 From: hsoft Date: Sun, 6 Sep 2009 12:25:49 +0000 Subject: [PATCH 124/275] Changed the PE gen script so it works on windows. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40125 --- pe/py/gen.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pe/py/gen.py b/pe/py/gen.py index bf3ebb12..37a5a422 100644 --- a/pe/py/gen.py +++ b/pe/py/gen.py @@ -20,10 +20,11 @@ def move(src, dst): os.rename(src, dst) # The CC=gcc-4.0 thing is because, in Snow Leopard, gcc-4.2 can't compile these units. +os.environ['CC'] = 'gcc-4.0' os.chdir(op.join('modules', 'block')) -os.system('CC=gcc-4.0;python setup.py build_ext --inplace') +os.system('python setup.py build_ext --inplace') os.chdir(op.join('..', 'cache')) -os.system('CC=gcc-4.0;python setup.py build_ext --inplace') +os.system('python setup.py build_ext --inplace') os.chdir(op.join('..', '..')) move(op.join('modules', 'block', '_block.so'), '_block.so') move(op.join('modules', 'block', '_block.pyd'), '_block.pyd') From a197403a44282d3397d9eb11eef0200d1ca5079f Mon Sep 17 00:00:00 2001 From: hsoft Date: Sun, 6 Sep 2009 12:36:25 +0000 Subject: [PATCH 125/275] Put back a placeholder for the obsolete scan type that had recently been removed. scan preference use those constants, so they can't be changed. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40126 --- base/py/scanner.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/base/py/scanner.py b/base/py/scanner.py index a4a8891f..18b83444 100644 --- a/base/py/scanner.py +++ b/base/py/scanner.py @@ -21,8 +21,9 @@ from .ignore import IgnoreList SCAN_TYPE_FIELDS, SCAN_TYPE_FIELDS_NO_ORDER, SCAN_TYPE_TAG, +UNUSED, # Must not be removed. Constants here are what scan_type in the prefs are. SCAN_TYPE_CONTENT, -SCAN_TYPE_CONTENT_AUDIO) = range(6) +SCAN_TYPE_CONTENT_AUDIO) = range(7) SCANNABLE_TAGS = ['track', 'artist', 'album', 'title', 'genre', 'year'] From ae6f5d27d82abe84c725b2d880494cbcb61abae4 Mon Sep 17 00:00:00 2001 From: hsoft Date: Sun, 6 Sep 2009 13:16:52 +0000 Subject: [PATCH 126/275] Adjusted the qt codebase to the recent GetDisplayInfo refactoring. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40127 --- base/qt/details_table.py | 9 ++++----- base/qt/results_model.py | 4 ++-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/base/qt/details_table.py b/base/qt/details_table.py index b568e459..532e3549 100644 --- a/base/qt/details_table.py +++ b/base/qt/details_table.py @@ -16,7 +16,6 @@ class DetailsModel(QAbstractTableModel): def __init__(self, app): QAbstractTableModel.__init__(self) self._app = app - self._data = app.data self._dupe_data = None self._ref_data = None self.connect(app, SIGNAL('duplicateSelected()'), self.duplicateSelected) @@ -32,7 +31,7 @@ class DetailsModel(QAbstractTableModel): column = index.column() row = index.row() if column == 0: - return QVariant(self._data.COLUMNS[row]['display']) + return QVariant(self._app.data.COLUMNS[row]['display']) elif column == 1 and self._dupe_data: return QVariant(self._dupe_data[row]) elif column == 2 and self._ref_data: @@ -45,7 +44,7 @@ class DetailsModel(QAbstractTableModel): return QVariant() def rowCount(self, parent): - return len(self._data.COLUMNS) + return len(self._app.data.COLUMNS) #--- Events def duplicateSelected(self): @@ -56,8 +55,8 @@ class DetailsModel(QAbstractTableModel): else: group = self._app.results.get_group_of_duplicate(dupe) ref = group.ref if group.ref is not dupe else None - self._dupe_data = self._data.GetDisplayInfo(dupe, group) - self._ref_data = self._data.GetDisplayInfo(ref, group) + self._dupe_data = self._app._get_display_info(dupe, group) + self._ref_data = self._app._get_display_info(ref, group) self.reset() diff --git a/base/qt/results_model.py b/base/qt/results_model.py index 004fca10..3ec8a095 100644 --- a/base/qt/results_model.py +++ b/base/qt/results_model.py @@ -35,13 +35,13 @@ class ResultNode(TreeNode): @property def normalData(self): if self._normalData is None: - self._normalData = self.model._data.GetDisplayInfo(self.dupe, self.group, delta=False) + self._normalData = self.model._app._get_display_info(self.dupe, self.group, delta=False) return self._normalData @property def deltaData(self): if self._deltaData is None: - self._deltaData = self.model._data.GetDisplayInfo(self.dupe, self.group, delta=True) + self._deltaData = self.model._app._get_display_info(self.dupe, self.group, delta=True) return self._deltaData From 5508609fdc22e637a98a62de905fc34b01f4a5f4 Mon Sep 17 00:00:00 2001 From: hsoft Date: Sun, 6 Sep 2009 15:24:17 +0000 Subject: [PATCH 127/275] Fixed a bug where groups discarded because all its files are ref would count in the "X discarded" message. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40128 --- base/py/scanner.py | 4 ++-- base/py/tests/scanner_test.py | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/base/py/scanner.py b/base/py/scanner.py index 18b83444..ff59d523 100644 --- a/base/py/scanner.py +++ b/base/py/scanner.py @@ -109,13 +109,13 @@ class Scanner(object): m.first.words = m.second.words = ['--'] logging.info('Grouping matches') groups = engine.get_groups(matches, j) + matched_files = dedupe([m.first for m in matches] + [m.second for m in matches]) + self.discarded_file_count = len(matched_files) - sum(len(g) for g in groups) groups = [g for g in groups if any(not f.is_ref for f in g)] logging.info('Created %d groups' % len(groups)) j.set_progress(100, 'Doing group prioritization') for g in groups: g.prioritize(self._key_func, self._tie_breaker) - matched_files = dedupe([m.first for m in matches] + [m.second for m in matches]) - self.discarded_file_count = len(matched_files) - sum(len(g) for g in groups) return groups match_factory = None diff --git a/base/py/tests/scanner_test.py b/base/py/tests/scanner_test.py index d683e405..7356d658 100644 --- a/base/py/tests/scanner_test.py +++ b/base/py/tests/scanner_test.py @@ -62,12 +62,15 @@ def test_simple_with_lower_min_match(): eq_(len(g), 3) def test_trim_all_ref_groups(): + # When all files of a group are ref, don't include that group in the results, but also don't + # count the files from that group as discarded. s = Scanner() f = [no('foo'), no('foo'), no('bar'), no('bar')] f[2].is_ref = True f[3].is_ref = True r = s.GetDupeGroups(f) eq_(len(r), 1) + eq_(s.discarded_file_count, 0) def test_priorize(): s = Scanner() From badd20e540cff407f793a9ed06c4ca5ff188a005 Mon Sep 17 00:00:00 2001 From: hsoft Date: Mon, 7 Sep 2009 07:17:28 +0000 Subject: [PATCH 128/275] Added auto-update public key to dgse's resources. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40129 --- base/cocoa/dsa_pub.pem | 20 ++++++++++++++++++++ se/cocoa/Info.plist | 2 ++ se/cocoa/dupeguru.xcodeproj/project.pbxproj | 4 ++++ 3 files changed, 26 insertions(+) create mode 100644 base/cocoa/dsa_pub.pem diff --git a/base/cocoa/dsa_pub.pem b/base/cocoa/dsa_pub.pem new file mode 100644 index 00000000..18e81668 --- /dev/null +++ b/base/cocoa/dsa_pub.pem @@ -0,0 +1,20 @@ +-----BEGIN PUBLIC KEY----- +MIIDOjCCAi0GByqGSM44BAEwggIgAoIBAQDSurIL+HKbw+jsppG6tp3+WOcA4W71 +nhwR/DD2Se076AtCXJcssAhuCDUm+AVkQ3l34D++aYWtLR575rCrwU4lZXfQe+b9 +plHK02oOuqAY8lO5y02xoHEh7XeGunZ0u8wOVZw8MI999vIJ8rtCdvIF3r26wkjx +9sieSxVpzJHDV5JHVdK3ObkXp/ts99dOD5B3CWGS8UiroMgS0FmRl7uPuADRRn2G +srHTBYMwJvq8HFzQmDxcLldGQMAKvRKchtH+nH6ci1unSnpDUyrsCd+7qv1cSTse +qc4OgXBDQ94MfVEh6Bs0S9stYfJf8cp6iV18J0sqMb9rbP4qC56iBsXfAhUAj6tx +gwima7VaNI4YiC69jpLod3MCggEAYx+/mbU8P/xGooV9MgA3nI2v2vVNkwZVFcPa +ROLQHg+R7bAftF3+1M9AnSP2O+PnXL65DwyTOab/Z/zM/vof3LLCGLYCmzPL+xvB +6PxlqO374kFsKHEaaw66nnFWzPSdks/il0rauAiEbO8Gn/a8F2HFdA/OCCzq83l6 +cOhya7kGXZxdjeIfpfiNjDqZXi+8VRNDcDXx5u/T4vpkliQ+4O8ZXjwE4z2dPHfu +Bw/N7DUalkzhZygYqcgx3tUxu3x/Pso+inmIBbk/As0uZv2nEll2CkEI6CSJIpfn +pLKNQb4E4G7h+u+8kfHcwQ59RU1uGh0PU5uM+DOPg6HsC41RwgOCAQUAAoIBABLY +T8gN8KdxWheESorvgksdG+Fizhkafpac08MCwJFF24v5a8AvZbhcCMLhChrloKcQ +19qHshRIuWbSma/OqCmQKH752PTOKxRKsmqAfO0Rej2aDJrd0s7YBMY72DqeSYPP +peLlwv0gkgRW7/EbDvBI18iTbrQLZtdqs9Xajc3dyIG5wrMtAf/Gta2oWChHlBLZ +S45++Y9ou+LtW7dMc7c+aTxbzeLG36S57kAenRzjfP8zOi3P+Cc+5b9+SZgqfFrz +/ch/HjB2zYAKq9AZSmgp9qIlOIuXnctJUD9hHivuEXFDr6xi1cxj7Q8WnX4+C58/ +QyGS4lebbLQ35x6fTQ8= +-----END PUBLIC KEY----- diff --git a/se/cocoa/Info.plist b/se/cocoa/Info.plist index b7bd3c6d..b8bd305e 100644 --- a/se/cocoa/Info.plist +++ b/se/cocoa/Info.plist @@ -30,5 +30,7 @@ NSApplication SUFeedURL http://www.hardcoded.net/updates/dupeguru.appcast + SUPublicDSAKeyFile + dsa_pub.pem diff --git a/se/cocoa/dupeguru.xcodeproj/project.pbxproj b/se/cocoa/dupeguru.xcodeproj/project.pbxproj index 60e297be..e5583252 100644 --- a/se/cocoa/dupeguru.xcodeproj/project.pbxproj +++ b/se/cocoa/dupeguru.xcodeproj/project.pbxproj @@ -19,6 +19,7 @@ CE45579B0AE3BC2B005A9546 /* Sparkle.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE45579A0AE3BC2B005A9546 /* Sparkle.framework */; }; CE4557B40AE3BC50005A9546 /* Sparkle.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = CE45579A0AE3BC2B005A9546 /* Sparkle.framework */; }; CE68EE6809ABC48000971085 /* DirectoryPanel.m in Sources */ = {isa = PBXBuildFile; fileRef = CE68EE6609ABC48000971085 /* DirectoryPanel.m */; }; + CE6E0DFE1054E9EF008D9390 /* dsa_pub.pem in Resources */ = {isa = PBXBuildFile; fileRef = CE6E0DFD1054E9EF008D9390 /* dsa_pub.pem */; }; CE848A1909DD85810004CB44 /* Consts.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = CE848A1809DD85810004CB44 /* Consts.h */; }; CECA899909DB12CA00A3D774 /* Details.nib in Resources */ = {isa = PBXBuildFile; fileRef = CECA899709DB12CA00A3D774 /* Details.nib */; }; CECA899C09DB132E00A3D774 /* DetailsPanel.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = CECA899A09DB132E00A3D774 /* DetailsPanel.h */; }; @@ -84,6 +85,7 @@ CE45579A0AE3BC2B005A9546 /* Sparkle.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Sparkle.framework; path = /Library/Frameworks/Sparkle.framework; sourceTree = ""; }; CE68EE6509ABC48000971085 /* DirectoryPanel.h */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.h; path = DirectoryPanel.h; sourceTree = SOURCE_ROOT; }; CE68EE6609ABC48000971085 /* DirectoryPanel.m */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.objc; path = DirectoryPanel.m; sourceTree = SOURCE_ROOT; }; + CE6E0DFD1054E9EF008D9390 /* dsa_pub.pem */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = dsa_pub.pem; path = dgbase/dsa_pub.pem; sourceTree = ""; }; CE848A1809DD85810004CB44 /* Consts.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Consts.h; sourceTree = ""; }; CECA899809DB12CA00A3D774 /* English */ = {isa = PBXFileReference; lastKnownFileType = wrapper.nib; name = English; path = English.lproj/Details.nib; sourceTree = ""; }; CECA899A09DB132E00A3D774 /* DetailsPanel.h */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.h; path = DetailsPanel.h; sourceTree = ""; }; @@ -214,6 +216,7 @@ CEEB135109C837A2004D2330 /* dupeguru.icns */, 8D1107310486CEB800E47090 /* Info.plist */, 089C165CFE840E0CC02AAC07 /* InfoPlist.strings */, + CE6E0DFD1054E9EF008D9390 /* dsa_pub.pem */, CECA899709DB12CA00A3D774 /* Details.nib */, CE3AA46509DB207900DB3A21 /* Directories.nib */, 29B97318FDCFA39411CA2CEA /* MainMenu.nib */, @@ -364,6 +367,7 @@ CEFC7FAD0FC9518A00CD5728 /* ErrorReportWindow.xib in Resources */, CEFC7FAE0FC9518A00CD5728 /* progress.nib in Resources */, CEFC7FAF0FC9518A00CD5728 /* registration.nib in Resources */, + CE6E0DFE1054E9EF008D9390 /* dsa_pub.pem in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; From e03ece4930a7fcfcb229133e0165508d02a1ff47 Mon Sep 17 00:00:00 2001 From: hsoft Date: Mon, 7 Sep 2009 07:22:31 +0000 Subject: [PATCH 129/275] Added auto-update public key to dgme's resources. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40130 --- me/cocoa/Info.plist | 2 ++ me/cocoa/dupeguru.xcodeproj/project.pbxproj | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/me/cocoa/Info.plist b/me/cocoa/Info.plist index 622bed33..d82d7a82 100644 --- a/me/cocoa/Info.plist +++ b/me/cocoa/Info.plist @@ -30,5 +30,7 @@ NSApplication SUFeedURL http://www.hardcoded.net/updates/dupeguru_me.appcast + SUPublicDSAKeyFile + dsa_pub.pem diff --git a/me/cocoa/dupeguru.xcodeproj/project.pbxproj b/me/cocoa/dupeguru.xcodeproj/project.pbxproj index 5ec33272..52be0bd5 100644 --- a/me/cocoa/dupeguru.xcodeproj/project.pbxproj +++ b/me/cocoa/dupeguru.xcodeproj/project.pbxproj @@ -49,6 +49,7 @@ CE515E1F0FC6C19300EC695D /* ResultWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = CE515E1C0FC6C19300EC695D /* ResultWindow.m */; }; CE6032C00FE6784C007E33FF /* DetailsPanel.m in Sources */ = {isa = PBXBuildFile; fileRef = CE6032BF0FE6784C007E33FF /* DetailsPanel.m */; }; CE68EE6809ABC48000971085 /* DirectoryPanel.m in Sources */ = {isa = PBXBuildFile; fileRef = CE68EE6609ABC48000971085 /* DirectoryPanel.m */; }; + CE6E0E9F1054EB97008D9390 /* dsa_pub.pem in Resources */ = {isa = PBXBuildFile; fileRef = CE6E0E9E1054EB97008D9390 /* dsa_pub.pem */; }; CE848A1909DD85810004CB44 /* Consts.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = CE848A1809DD85810004CB44 /* Consts.h */; }; CEA7D2C50FDFED340037CD8C /* dgme_logo_32.png in Resources */ = {isa = PBXBuildFile; fileRef = CEA7D2C40FDFED340037CD8C /* dgme_logo_32.png */; }; CECA899909DB12CA00A3D774 /* Details.nib in Resources */ = {isa = PBXBuildFile; fileRef = CECA899709DB12CA00A3D774 /* Details.nib */; }; @@ -133,6 +134,7 @@ CE6032BF0FE6784C007E33FF /* DetailsPanel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = DetailsPanel.m; path = dgbase/DetailsPanel.m; sourceTree = SOURCE_ROOT; }; CE68EE6509ABC48000971085 /* DirectoryPanel.h */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.h; path = DirectoryPanel.h; sourceTree = SOURCE_ROOT; }; CE68EE6609ABC48000971085 /* DirectoryPanel.m */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.objc; path = DirectoryPanel.m; sourceTree = SOURCE_ROOT; }; + CE6E0E9E1054EB97008D9390 /* dsa_pub.pem */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = dsa_pub.pem; path = dgbase/dsa_pub.pem; sourceTree = ""; }; CE848A1809DD85810004CB44 /* Consts.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Consts.h; sourceTree = ""; }; CEA7D2C40FDFED340037CD8C /* dgme_logo_32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = dgme_logo_32.png; path = images/dgme_logo_32.png; sourceTree = SOURCE_ROOT; }; CECA899809DB12CA00A3D774 /* English */ = {isa = PBXFileReference; lastKnownFileType = wrapper.nib; name = English; path = English.lproj/Details.nib; sourceTree = ""; }; @@ -235,6 +237,7 @@ CEEB135109C837A2004D2330 /* dupeguru.icns */, 8D1107310486CEB800E47090 /* Info.plist */, 089C165CFE840E0CC02AAC07 /* InfoPlist.strings */, + CE6E0E9E1054EB97008D9390 /* dsa_pub.pem */, CECA899709DB12CA00A3D774 /* Details.nib */, CE3AA46509DB207900DB3A21 /* Directories.nib */, 29B97318FDCFA39411CA2CEA /* MainMenu.nib */, @@ -384,6 +387,7 @@ CE515E030FC6C13E00EC695D /* progress.nib in Resources */, CE515E040FC6C13E00EC695D /* registration.nib in Resources */, CEA7D2C50FDFED340037CD8C /* dgme_logo_32.png in Resources */, + CE6E0E9F1054EB97008D9390 /* dsa_pub.pem in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; From cc1bb76c078b7f3d3b539d65c6f463d58381c6b2 Mon Sep 17 00:00:00 2001 From: hsoft Date: Mon, 7 Sep 2009 07:25:26 +0000 Subject: [PATCH 130/275] Added auto-update public key to dgpe's resources. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40131 --- pe/cocoa/Info.plist | 2 ++ pe/cocoa/dupeguru.xcodeproj/project.pbxproj | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/pe/cocoa/Info.plist b/pe/cocoa/Info.plist index 9f51e9f2..a8d12d58 100644 --- a/pe/cocoa/Info.plist +++ b/pe/cocoa/Info.plist @@ -30,5 +30,7 @@ NSApplication SUFeedURL http://www.hardcoded.net/updates/dupeguru_pe.appcast + SUPublicDSAKeyFile + dsa_pub.pem diff --git a/pe/cocoa/dupeguru.xcodeproj/project.pbxproj b/pe/cocoa/dupeguru.xcodeproj/project.pbxproj index 808d08ad..3ae2ed9c 100644 --- a/pe/cocoa/dupeguru.xcodeproj/project.pbxproj +++ b/pe/cocoa/dupeguru.xcodeproj/project.pbxproj @@ -21,6 +21,7 @@ CE3AA46709DB207900DB3A21 /* Directories.nib in Resources */ = {isa = PBXBuildFile; fileRef = CE3AA46509DB207900DB3A21 /* Directories.nib */; }; CE6044EC0FE6796200B71262 /* DetailsPanel.m in Sources */ = {isa = PBXBuildFile; fileRef = CE6044EB0FE6796200B71262 /* DetailsPanel.m */; }; CE68EE6809ABC48000971085 /* DirectoryPanel.m in Sources */ = {isa = PBXBuildFile; fileRef = CE68EE6609ABC48000971085 /* DirectoryPanel.m */; }; + CE6E0F3D1054EC62008D9390 /* dsa_pub.pem in Resources */ = {isa = PBXBuildFile; fileRef = CE6E0F3C1054EC62008D9390 /* dsa_pub.pem */; }; CE80DB2E0FC192D60086DCA6 /* Dialogs.m in Sources */ = {isa = PBXBuildFile; fileRef = CE80DB1C0FC192D60086DCA6 /* Dialogs.m */; }; CE80DB2F0FC192D60086DCA6 /* HSErrorReportWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = CE80DB1E0FC192D60086DCA6 /* HSErrorReportWindow.m */; }; CE80DB300FC192D60086DCA6 /* Outline.m in Sources */ = {isa = PBXBuildFile; fileRef = CE80DB200FC192D60086DCA6 /* Outline.m */; }; @@ -91,6 +92,7 @@ CE6044EB0FE6796200B71262 /* DetailsPanel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = DetailsPanel.m; path = dgbase/DetailsPanel.m; sourceTree = SOURCE_ROOT; }; CE68EE6509ABC48000971085 /* DirectoryPanel.h */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.h; path = DirectoryPanel.h; sourceTree = SOURCE_ROOT; }; CE68EE6609ABC48000971085 /* DirectoryPanel.m */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.objc; path = DirectoryPanel.m; sourceTree = SOURCE_ROOT; }; + CE6E0F3C1054EC62008D9390 /* dsa_pub.pem */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = dsa_pub.pem; path = dgbase/dsa_pub.pem; sourceTree = ""; }; CE80DB1B0FC192D60086DCA6 /* Dialogs.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Dialogs.h; path = cocoalib/Dialogs.h; sourceTree = SOURCE_ROOT; }; CE80DB1C0FC192D60086DCA6 /* Dialogs.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Dialogs.m; path = cocoalib/Dialogs.m; sourceTree = SOURCE_ROOT; }; CE80DB1D0FC192D60086DCA6 /* HSErrorReportWindow.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = HSErrorReportWindow.h; path = cocoalib/HSErrorReportWindow.h; sourceTree = SOURCE_ROOT; }; @@ -232,6 +234,7 @@ CEEB135109C837A2004D2330 /* dupeguru.icns */, 8D1107310486CEB800E47090 /* Info.plist */, 089C165CFE840E0CC02AAC07 /* InfoPlist.strings */, + CE6E0F3C1054EC62008D9390 /* dsa_pub.pem */, CECA899709DB12CA00A3D774 /* Details.nib */, CE3AA46509DB207900DB3A21 /* Directories.nib */, 29B97318FDCFA39411CA2CEA /* MainMenu.nib */, @@ -382,6 +385,7 @@ CE80DB760FC194760086DCA6 /* ErrorReportWindow.xib in Resources */, CE80DB770FC194760086DCA6 /* progress.nib in Resources */, CE80DB780FC194760086DCA6 /* registration.nib in Resources */, + CE6E0F3D1054EC62008D9390 /* dsa_pub.pem in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; From 3cf2839dc34aea44b0a67335066266dc3b932848 Mon Sep 17 00:00:00 2001 From: hsoft Date: Mon, 7 Sep 2009 10:06:41 +0000 Subject: [PATCH 131/275] Added partial is_bundle() support for Tiger (I hadn't noticed that typeOfFile:error: was Leopard only). --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40132 --- base/py/app_se_cocoa.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/base/py/app_se_cocoa.py b/base/py/app_se_cocoa.py index abd11d2e..e24b105c 100644 --- a/base/py/app_se_cocoa.py +++ b/base/py/app_se_cocoa.py @@ -20,16 +20,19 @@ from hsutil.str import get_file_ext from . import app_cocoa, data from .directories import Directories as DirectoriesBase, STATE_EXCLUDED -def is_bundle(path): - sw = NSWorkspace.sharedWorkspace() - uti, error = sw.typeOfFile_error_(path) - if error is not None: - logging.warning(u'There was an error trying to detect the UTI of %s', path) - return sw.type_conformsToType_(uti, 'com.apple.bundle') or sw.type_conformsToType_(uti, 'com.apple.package') +if NSWorkspace.sharedWorkspace().respondsToSelector_('typeOfFile:error:'): # Only from 10.5 + def is_bundle(str_path): + sw = NSWorkspace.sharedWorkspace() + uti, error = sw.typeOfFile_error_(str_path) + if error is not None: + logging.warning(u'There was an error trying to detect the UTI of %s', str_path) + return sw.type_conformsToType_(uti, 'com.apple.bundle') or sw.type_conformsToType_(uti, 'com.apple.package') +else: # Tiger + def is_bundle(str_path): # just return a list of a few known bundle extensions. + return get_file_ext(str_path) in ('app', 'pages', 'numbers') class DGDirectory(DirectoryBase): def _create_sub_file(self, name, with_parent=True): - ext = get_file_ext(name) if is_bundle(unicode(self.path + name)): parent = self if with_parent else None return Bundle(parent, name) From 28ec1785aabace47c460d36886453f47e5a35abe Mon Sep 17 00:00:00 2001 From: hsoft Date: Mon, 7 Sep 2009 10:07:18 +0000 Subject: [PATCH 132/275] dgse 2.8.0 + changelog. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40133 --- se/cocoa/Info.plist | 2 +- se/help/changelog.yaml | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/se/cocoa/Info.plist b/se/cocoa/Info.plist index b8bd305e..2e01d45c 100644 --- a/se/cocoa/Info.plist +++ b/se/cocoa/Info.plist @@ -23,7 +23,7 @@ CFBundleSignature hsft CFBundleVersion - 2.7.3 + 2.8.0 NSMainNibFile MainMenu NSPrincipalClass diff --git a/se/help/changelog.yaml b/se/help/changelog.yaml index 47acc9c2..8c3d6cd8 100644 --- a/se/help/changelog.yaml +++ b/se/help/changelog.yaml @@ -1,3 +1,13 @@ +- date: 2009-09-07 + version: 2.8.0 + description: | + * Added support for all kinds of bundle (not just applications) (Mac OS X) + * Re-introduced the Export to XHTML feature to Windows. + * Improved Export to XHTML speed. + * Improved Contents scanning speed for large files. + * Improved the grouping algorithm to reduce the number of discarded files in non-exact scans. + * Stopped showing the same file on the 2 sides of the details panel when a ref file is selected. + * Fixed crashes in the Directories panel. - date: 2009-06-20 version: 2.7.3 description: | From 3cf06644a5bd0e0f8b27f201028b3410a5e013a6 Mon Sep 17 00:00:00 2001 From: hsoft Date: Mon, 7 Sep 2009 10:10:08 +0000 Subject: [PATCH 133/275] dgse qt v2.8.0 --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40134 --- se/qt/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/se/qt/app.py b/se/qt/app.py index dd523d69..adbb75e9 100644 --- a/se/qt/app.py +++ b/se/qt/app.py @@ -27,7 +27,7 @@ class Directories(DirectoriesBase): class DupeGuru(DupeGuruBase): LOGO_NAME = 'logo_se' NAME = 'dupeGuru' - VERSION = '2.7.3' + VERSION = '2.8.0' DELTA_COLUMNS = frozenset([2, 4, 5]) def __init__(self): From 783c83f7c568505955de6da658807efb5326e17a Mon Sep 17 00:00:00 2001 From: hsoft Date: Mon, 7 Sep 2009 13:23:14 +0000 Subject: [PATCH 134/275] dgme cocoa v5.6.4 + changelog. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40135 --- me/cocoa/Info.plist | 2 +- me/help/changelog.yaml | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/me/cocoa/Info.plist b/me/cocoa/Info.plist index d82d7a82..b263a8d8 100644 --- a/me/cocoa/Info.plist +++ b/me/cocoa/Info.plist @@ -23,7 +23,7 @@ CFBundleSignature hsft CFBundleVersion - 5.6.3 + 5.6.4 NSMainNibFile MainMenu NSPrincipalClass diff --git a/me/help/changelog.yaml b/me/help/changelog.yaml index 61f0a9c4..e1adeb75 100644 --- a/me/help/changelog.yaml +++ b/me/help/changelog.yaml @@ -1,3 +1,11 @@ +- date: 2009-09-07 + version: 5.6.4 + description: | + * Re-introduced the Export to XHTML feature to Windows. + * Improved Export to XHTML speed. + * Improved the grouping algorithm to reduce the number of discarded files in non-exact scans. + * Stopped showing the same file on the 2 sides of the details panel when a ref file is selected. + * Fixed crashes in the Directories panel. - date: 2009-06-19 version: 5.6.3 description: | From e9eb5dd5335f8a3d6fc88091e96f581e0fbbf7f0 Mon Sep 17 00:00:00 2001 From: hsoft Date: Mon, 7 Sep 2009 13:24:37 +0000 Subject: [PATCH 135/275] dgme qt v5.6.4 --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40136 --- me/qt/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/me/qt/app.py b/me/qt/app.py index 53dbb174..51488a8e 100644 --- a/me/qt/app.py +++ b/me/qt/app.py @@ -19,7 +19,7 @@ from preferences_dialog import PreferencesDialog class DupeGuru(DupeGuruBase): LOGO_NAME = 'logo_me' NAME = 'dupeGuru Music Edition' - VERSION = '5.6.3' + VERSION = '5.6.4' DELTA_COLUMNS = frozenset([2, 3, 4, 5, 7, 8]) def __init__(self): From 55ff281f562afac85a115dae618024ff91c18fac Mon Sep 17 00:00:00 2001 From: hsoft Date: Mon, 7 Sep 2009 14:43:58 +0000 Subject: [PATCH 136/275] dgpe cocoa v1.7.5 + changelog. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40137 --- pe/cocoa/Info.plist | 2 +- pe/help/changelog.yaml | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/pe/cocoa/Info.plist b/pe/cocoa/Info.plist index a8d12d58..99b9a2b5 100644 --- a/pe/cocoa/Info.plist +++ b/pe/cocoa/Info.plist @@ -23,7 +23,7 @@ CFBundleSignature hsft CFBundleVersion - 1.7.4 + 1.7.5 NSMainNibFile MainMenu NSPrincipalClass diff --git a/pe/help/changelog.yaml b/pe/help/changelog.yaml index 149fc233..a48e6836 100644 --- a/pe/help/changelog.yaml +++ b/pe/help/changelog.yaml @@ -1,3 +1,11 @@ +- date: 2009-09-07 + version: 1.7.5 + description: | + * Re-introduced the Export to XHTML feature to Windows. + * Improved Export to XHTML speed. + * Improved the grouping algorithm to reduce the number of discarded files in non-exact scans. + * Stopped showing the same file on the 2 sides of the details panel when a ref file is selected. + * Fixed crashes in the Directories panel. - date: 2009-06-20 version: 1.7.4 description: | From a4d295297099a1069864bcd3f5e516e521d0ffc9 Mon Sep 17 00:00:00 2001 From: hsoft Date: Mon, 7 Sep 2009 14:45:20 +0000 Subject: [PATCH 137/275] dgpe qt v1.7.5 --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40138 --- pe/qt/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pe/qt/app.py b/pe/qt/app.py index fdeac442..b99de5fd 100644 --- a/pe/qt/app.py +++ b/pe/qt/app.py @@ -67,7 +67,7 @@ class Directory(phys.Directory): class DupeGuru(DupeGuruBase): LOGO_NAME = 'logo_pe' NAME = 'dupeGuru Picture Edition' - VERSION = '1.7.4' + VERSION = '1.7.5' DELTA_COLUMNS = frozenset([2, 5, 6]) def __init__(self): From 541bf66cb46346174a418326f1485e52a43e58c2 Mon Sep 17 00:00:00 2001 From: hsoft Date: Sat, 12 Sep 2009 08:41:02 +0000 Subject: [PATCH 138/275] Removed stuff that belongs to the qt-osx branch --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40139 --- base/qt/gen.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/base/qt/gen.py b/base/qt/gen.py index 754209f1..cb1c2de7 100644 --- a/base/qt/gen.py +++ b/base/qt/gen.py @@ -21,10 +21,4 @@ print_and_do("pyuic4 about_box.ui > about_box_ui.py") print_and_do("pyuic4 reg_submit_dialog.ui > reg_submit_dialog_ui.py") print_and_do("pyuic4 reg_demo_dialog.ui > reg_demo_dialog_ui.py") print_and_do("pyuic4 error_report_dialog.ui > error_report_dialog_ui.py") -print_and_do("pyrcc4 dg.qrc > dg_rc.py") - -if sys.platform == 'darwin': - os.chdir('osx/SendToTrashProject') - print_and_do('xcodebuild') - print_and_do('cp build/Release/SendToTrash ../') - os.chdir('../..') \ No newline at end of file +print_and_do("pyrcc4 dg.qrc > dg_rc.py") \ No newline at end of file From dc4a10cc266534896e043abc06cdc317a55030eb Mon Sep 17 00:00:00 2001 From: hsoft Date: Thu, 17 Sep 2009 10:37:12 +0000 Subject: [PATCH 139/275] Moved externals to the new lib repository. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40140 From 657eca1bb8302d2bd0d0f964aba94e1d95dc6e23 Mon Sep 17 00:00:00 2001 From: hsoft Date: Sun, 27 Sep 2009 08:41:27 +0000 Subject: [PATCH 140/275] se help: added tixgen support. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40141 --- se/help/changelog.yaml | 28 ++++++++++++++-------------- se/help/gen.py | 6 ++++-- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/se/help/changelog.yaml b/se/help/changelog.yaml index 8c3d6cd8..b629350d 100644 --- a/se/help/changelog.yaml +++ b/se/help/changelog.yaml @@ -1,27 +1,27 @@ - date: 2009-09-07 version: 2.8.0 description: | - * Added support for all kinds of bundle (not just applications) (Mac OS X) - * Re-introduced the Export to XHTML feature to Windows. - * Improved Export to XHTML speed. - * Improved Contents scanning speed for large files. - * Improved the grouping algorithm to reduce the number of discarded files in non-exact scans. - * Stopped showing the same file on the 2 sides of the details panel when a ref file is selected. - * Fixed crashes in the Directories panel. + * Added support for all kinds of bundle (not just applications) (Mac OS X) (#11) + * Re-introduced the Export to XHTML feature to Windows. (#14) + * Improved Export to XHTML speed. (#14) + * Improved Contents scanning speed for large files. (#33) + * Improved the grouping algorithm to reduce the number of discarded files in non-exact scans. (#51) + * Stopped showing the same file on the 2 sides of the details panel when a ref file is selected. (#50) + * Fixed crashes in the Directories panel. (#46) - date: 2009-06-20 version: 2.7.3 description: | * Fixed bugs with selection being jumpy during "Make Reference" actions and Power Marker - switches. - * Fixed crash happening when a file with non-roman characters couldn't be analyzed. - * Fixed crash sometimes happening during the file collection phase in scanning. - * Restored double-click and right-click behavior lost in the PyQt move (Windows). + switches. (#3) + * Fixed crash happening when a file with non-roman characters couldn't be analyzed. (#30) + * Fixed crash sometimes happening during the file collection phase in scanning. (#38) + * Restored double-click and right-click behavior lost in the PyQt move (Windows). (#34 #35) - date: 2009-06-10 version: 2.7.2 description: | - * Fixed an occasional crash on Copy/Move operations. - * Added automatic exclusion for sensible folders (like system folders). - * Fixed an occasional crash when application files were part of the results (Mac OS X). + * Fixed an occasional crash on Copy/Move operations. (#16) + * Added automatic exclusion for sensible folders (like system folders). (#20) + * Fixed an occasional crash when application files were part of the results (Mac OS X). (#25) - date: 2009-05-29 version: 2.7.1 description: | diff --git a/se/help/gen.py b/se/help/gen.py index 8bfd66c3..375aaee8 100644 --- a/se/help/gen.py +++ b/se/help/gen.py @@ -7,6 +7,8 @@ import os -from hsdocgen import generate_help +from hsdocgen import generate_help, filters -generate_help.main('.', 'dupeguru_help', force_render=True) +tix = filters.tixgen("https://hardcoded.lighthouseapp.com/projects/31699-dupeguru/tickets/{0}") + +generate_help.main('.', 'dupeguru_help', force_render=True, tix=tix) From a508a86ce41bb0ab561a45feef28f6504db54625 Mon Sep 17 00:00:00 2001 From: hsoft Date: Sun, 27 Sep 2009 08:44:06 +0000 Subject: [PATCH 141/275] qt base: Adjusted codebase to the latest dev trends. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40142 --- base/qt/app.py | 61 ++-------- base/qt/directories_model.py | 56 +++++---- base/qt/error_report_dialog.py | 27 ----- base/qt/error_report_dialog.ui | 117 ------------------- base/qt/platform.py | 17 +++ base/qt/{win/__init__.py => platform_win.py} | 0 base/qt/results_model.py | 57 +++++---- base/qt/tree_model.py | 68 ----------- 8 files changed, 79 insertions(+), 324 deletions(-) delete mode 100644 base/qt/error_report_dialog.py delete mode 100644 base/qt/error_report_dialog.ui create mode 100644 base/qt/platform.py rename base/qt/{win/__init__.py => platform_win.py} (100%) delete mode 100644 base/qt/tree_model.py diff --git a/base/qt/app.py b/base/qt/app.py index fc471a90..9a94b540 100644 --- a/base/qt/app.py +++ b/base/qt/app.py @@ -9,8 +9,6 @@ import logging import os.path as op -import sys -import traceback from PyQt4.QtCore import Qt, QTimer, QObject, QCoreApplication, QUrl, SIGNAL from PyQt4.QtGui import QProgressDialog, QDesktopServices, QFileDialog, QDialog, QMessageBox @@ -22,16 +20,14 @@ from hsutil.reg import RegistrationRequired from dupeguru.app import (DupeGuru as DupeGuruBase, JOB_SCAN, JOB_LOAD, JOB_MOVE, JOB_COPY, JOB_DELETE) -if sys.platform == 'win32': - from .win import recycle_file -else: - logging.warning("Unsupported Platform!!!") +from qtlib.progress import Progress -from main_window import MainWindow -from directories_dialog import DirectoriesDialog -from about_box import AboutBox -from reg import Registration -from error_report_dialog import ErrorReportDialog +from . import platform + +from .main_window import MainWindow +from .directories_dialog import DirectoriesDialog +from .about_box import AboutBox +from .reg import Registration JOBID2TITLE = { JOB_SCAN: "Scanning for duplicates", @@ -41,47 +37,6 @@ JOBID2TITLE = { JOB_DELETE: "Sending files to the recycle bin", } -class Progress(QProgressDialog, job.ThreadedJobPerformer): - def __init__(self, parent): - flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint - QProgressDialog.__init__(self, '', u"Cancel", 0, 100, parent, flags) - self.setModal(True) - self.setAutoReset(False) - self.setAutoClose(False) - self._timer = QTimer() - self._jobid = '' - self.connect(self._timer, SIGNAL('timeout()'), self.updateProgress) - - def updateProgress(self): - # the values might change before setValue happens - last_progress = self.last_progress - last_desc = self.last_desc - if not self._job_running or last_progress is None: - self._timer.stop() - self.close() - self.emit(SIGNAL('finished(QString)'), self._jobid) - if self._last_error is not None: - s = ''.join(traceback.format_exception(*self._last_error)) - dialog = ErrorReportDialog(self.parent(), s) - dialog.exec_() - return - if self.wasCanceled(): - self.job_cancelled = True - return - if last_desc: - self.setLabelText(last_desc) - self.setValue(last_progress) - - def run(self, jobid, title, target, args=()): - self._jobid = jobid - self.reset() - self.setLabelText('') - self.run_threaded(target, args) - self.setWindowTitle(title) - self.show() - self._timer.start(500) - - def demo_method(method): def wrapper(self, *args, **kwargs): try: @@ -154,7 +109,7 @@ class DupeGuru(DupeGuruBase, QObject): #--- Override @staticmethod def _recycle_dupe(dupe): - recycle_file(dupe.path) + platform.recycle_file(dupe.path) def _start_job(self, jobid, func): title = JOBID2TITLE[jobid] diff --git a/base/qt/directories_model.py b/base/qt/directories_model.py index bf2f86e3..ac77fa0c 100644 --- a/base/qt/directories_model.py +++ b/base/qt/directories_model.py @@ -7,10 +7,10 @@ # which should be included with this package. The terms are also available at # http://www.hardcoded.net/licenses/hs_license -from PyQt4.QtCore import QVariant, QModelIndex, Qt, QRect, QEvent, QPoint +from PyQt4.QtCore import QModelIndex, Qt, QRect, QEvent, QPoint from PyQt4.QtGui import QComboBox, QStyledItemDelegate, QMouseEvent, QApplication, QBrush -from tree_model import TreeNode, TreeModel +from qtlib.tree_model import TreeNode, TreeModel HEADERS = ['Name', 'State'] STATES = ['Normal', 'Reference', 'Excluded'] @@ -22,8 +22,7 @@ class DirectoriesDelegate(QStyledItemDelegate): return editor def setEditorData(self, editor, index): - value, ok = index.model().data(index, Qt.EditRole).toInt() - assert ok + value = index.model().data(index, Qt.EditRole) editor.setCurrentIndex(value); press = QMouseEvent(QEvent.MouseButtonPress, QPoint(0, 0), Qt.LeftButton, Qt.LeftButton, Qt.NoModifier) release = QMouseEvent(QEvent.MouseButtonRelease, QPoint(0, 0), Qt.LeftButton, Qt.LeftButton, Qt.NoModifier) @@ -32,7 +31,7 @@ class DirectoriesDelegate(QStyledItemDelegate): # editor.showPopup() # this causes a weird glitch. the ugly workaround is above. def setModelData(self, editor, model, index): - value = QVariant(editor.currentIndex()) + value = editor.currentIndex() model.setData(index, value, Qt.EditRole) def updateEditorGeometry(self, editor, option, index): @@ -40,16 +39,15 @@ class DirectoriesDelegate(QStyledItemDelegate): class DirectoryNode(TreeNode): - def __init__(self, parent, ref, row): - TreeNode.__init__(self, parent, row) + def __init__(self, model, parent, ref, row): + TreeNode.__init__(self, model, parent, row) self.ref = ref - def _get_children(self): - children = [] - for index, directory in enumerate(self.ref.dirs): - node = DirectoryNode(self, directory, index) - children.append(node) - return children + def _createNode(self, ref, row): + return DirectoryNode(self.model, self, ref, row) + + def _getChildren(self): + return self.ref.dirs class DirectoriesModel(TreeModel): @@ -57,33 +55,33 @@ class DirectoriesModel(TreeModel): self._dirs = app.directories TreeModel.__init__(self) - def _root_nodes(self): - nodes = [] - for index, directory in enumerate(self._dirs): - nodes.append(DirectoryNode(None, directory, index)) - return nodes + def _createNode(self, ref, row): + return DirectoryNode(self, None, ref, row) + + def _getChildren(self): + return self._dirs def columnCount(self, parent): return 2 def data(self, index, role): if not index.isValid(): - return QVariant() + return None node = index.internalPointer() if role == Qt.DisplayRole: if index.column() == 0: - return QVariant(node.ref.name) + return node.ref.name else: - return QVariant(STATES[self._dirs.get_state(node.ref.path)]) + return STATES[self._dirs.get_state(node.ref.path)] elif role == Qt.EditRole and index.column() == 1: - return QVariant(self._dirs.get_state(node.ref.path)) + return self._dirs.get_state(node.ref.path) elif role == Qt.ForegroundRole: state = self._dirs.get_state(node.ref.path) if state == 1: - return QVariant(QBrush(Qt.blue)) + return QBrush(Qt.blue) elif state == 2: - return QVariant(QBrush(Qt.red)) - return QVariant() + return QBrush(Qt.red) + return None def flags(self, index): if not index.isValid(): @@ -96,15 +94,13 @@ class DirectoriesModel(TreeModel): def headerData(self, section, orientation, role): if orientation == Qt.Horizontal: if role == Qt.DisplayRole and section < len(HEADERS): - return QVariant(HEADERS[section]) - return QVariant() + return HEADERS[section] + return None def setData(self, index, value, role): if not index.isValid() or role != Qt.EditRole or index.column() != 1: return False node = index.internalPointer() - state, ok = value.toInt() - assert ok - self._dirs.set_state(node.ref.path, state) + self._dirs.set_state(node.ref.path, value) return True diff --git a/base/qt/error_report_dialog.py b/base/qt/error_report_dialog.py deleted file mode 100644 index ec4a5677..00000000 --- a/base/qt/error_report_dialog.py +++ /dev/null @@ -1,27 +0,0 @@ -# Created By: Virgil Dupras -# Created On: 2009-05-23 -# $Id$ -# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "HS" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at -# http://www.hardcoded.net/licenses/hs_license - -from PyQt4.QtCore import Qt, QUrl -from PyQt4.QtGui import QDialog, QDesktopServices - -from error_report_dialog_ui import Ui_ErrorReportDialog - -class ErrorReportDialog(QDialog, Ui_ErrorReportDialog): - def __init__(self, parent, error): - flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint - QDialog.__init__(self, parent, flags) - self.setupUi(self) - self.errorTextEdit.setPlainText(error) - - def accept(self): - text = self.errorTextEdit.toPlainText() - url = QUrl("mailto:support@hardcoded.net?SUBJECT=Error Report&BODY=%s" % text) - QDesktopServices.openUrl(url) - QDialog.accept(self) - diff --git a/base/qt/error_report_dialog.ui b/base/qt/error_report_dialog.ui deleted file mode 100644 index 0974dd2f..00000000 --- a/base/qt/error_report_dialog.ui +++ /dev/null @@ -1,117 +0,0 @@ - - - ErrorReportDialog - - - - 0 - 0 - 553 - 349 - - - - Error Report - - - - - - Something went wrong. Would you like to send the error report to Hardcoded Software? - - - true - - - - - - - true - - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - 110 - 0 - - - - Don't Send - - - - - - - - 110 - 0 - - - - Send - - - true - - - - - - - - - - - sendButton - clicked() - ErrorReportDialog - accept() - - - 485 - 320 - - - 276 - 174 - - - - - dontSendButton - clicked() - ErrorReportDialog - reject() - - - 373 - 320 - - - 276 - 174 - - - - - diff --git a/base/qt/platform.py b/base/qt/platform.py new file mode 100644 index 00000000..c0668794 --- /dev/null +++ b/base/qt/platform.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# Created By: Virgil Dupras +# Created On: 2009-09-27 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license + +import logging +import sys + +if sys.platform == 'win32': + from platform_win import * +else: + logging.warning("Unsupported Platform!!") \ No newline at end of file diff --git a/base/qt/win/__init__.py b/base/qt/platform_win.py similarity index 100% rename from base/qt/win/__init__.py rename to base/qt/platform_win.py diff --git a/base/qt/results_model.py b/base/qt/results_model.py index 3ec8a095..ccd84f7b 100644 --- a/base/qt/results_model.py +++ b/base/qt/results_model.py @@ -7,30 +7,29 @@ # which should be included with this package. The terms are also available at # http://www.hardcoded.net/licenses/hs_license -from PyQt4.QtCore import SIGNAL, Qt, QAbstractItemModel, QVariant, QModelIndex, QRect +from PyQt4.QtCore import SIGNAL, Qt, QAbstractItemModel, QModelIndex, QRect from PyQt4.QtGui import QBrush, QStyledItemDelegate, QFont, QTreeView, QColor -from tree_model import TreeNode, TreeModel +from qtlib.tree_model import TreeNode, TreeModel class ResultNode(TreeNode): def __init__(self, model, parent, row, dupe, group): - TreeNode.__init__(self, parent, row) - self.model = model + TreeNode.__init__(self, model, parent, row) self.dupe = dupe self.group = group self._normalData = None self._deltaData = None - def _get_children(self): - children = [] - if self.dupe is self.group.ref: - for index, dupe in enumerate(self.group.dupes): - children.append(ResultNode(self.model, self, index, dupe, self.group)) - return children + def _createNode(self, ref, row): + return ResultNode(self.model, self, row, ref, self.group) - def reset(self): + def _getChildren(self): + return self.group.dupes if self.dupe is self.group.ref else [] + + def invalidate(self): self._normalData = None self._deltaData = None + TreeNode.invalidate(self) @property def normalData(self): @@ -65,40 +64,41 @@ class ResultsModel(TreeModel): self._power_marker = False TreeModel.__init__(self) - def _root_nodes(self): - nodes = [] + def _createNode(self, ref, row): if self.power_marker: - for index, dupe in enumerate(self._results.dupes): - group = self._results.get_group_of_duplicate(dupe) - nodes.append(ResultNode(self, None, index, dupe, group)) + # ref is a dupe + group = self._results.get_group_of_duplicate(ref) + return ResultNode(self, None, row, ref, group) else: - for index, group in enumerate(self._results.groups): - nodes.append(ResultNode(self, None, index, group.ref, group)) - return nodes + # ref is a group + return ResultNode(self, None, row, ref.ref, ref) + + def _getChildren(self): + return self._results.dupes if self.power_marker else self._results.groups def columnCount(self, parent): return len(self._data.COLUMNS) def data(self, index, role): if not index.isValid(): - return QVariant() + return None node = index.internalPointer() if role == Qt.DisplayRole: data = node.deltaData if self.delta else node.normalData - return QVariant(data[index.column()]) + return data[index.column()] elif role == Qt.CheckStateRole: if index.column() == 0 and node.dupe is not node.group.ref: state = Qt.Checked if self._results.is_marked(node.dupe) else Qt.Unchecked - return QVariant(state) + return state elif role == Qt.ForegroundRole: if node.dupe is node.group.ref or node.dupe.is_ref: - return QVariant(QBrush(Qt.blue)) + return QBrush(Qt.blue) elif self.delta and index.column() in self._delta_columns: - return QVariant(QBrush(QColor(255, 142, 40))) # orange + return QBrush(QColor(255, 142, 40)) # orange elif role == Qt.EditRole: if index.column() == 0: - return QVariant(node.normalData[index.column()]) - return QVariant() + return node.normalData[index.column()] + return None def dupesForIndexes(self, indexes): nodes = [index.internalPointer() for index in indexes] @@ -138,9 +138,8 @@ class ResultsModel(TreeModel): def headerData(self, section, orientation, role): if orientation == Qt.Horizontal and role == Qt.DisplayRole and section < len(self._data.COLUMNS): - return QVariant(self._data.COLUMNS[section]['display']) - - return QVariant() + return self._data.COLUMNS[section]['display'] + return None def setData(self, index, value, role): if not index.isValid(): diff --git a/base/qt/tree_model.py b/base/qt/tree_model.py deleted file mode 100644 index d102fac7..00000000 --- a/base/qt/tree_model.py +++ /dev/null @@ -1,68 +0,0 @@ -# Created By: Virgil Dupras -# Created On: 2009-05-04 -# $Id$ -# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "HS" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at -# http://www.hardcoded.net/licenses/hs_license - -from PyQt4.QtCore import Qt, QAbstractItemModel, QVariant, QModelIndex - -class TreeNode(object): - def __init__(self, parent, row): - self.parent = parent - self.row = row - self._children = None - - def _get_children(self): - raise NotImplementedError() - - @property - def children(self): - if self._children is None: - self._children = self._get_children() - return self._children - - -class TreeModel(QAbstractItemModel): - def __init__(self): - QAbstractItemModel.__init__(self) - self._nodes = None - - def _root_nodes(self): - raise NotImplementedError() - - def index(self, row, column, parent): - if not self.nodes: - return QModelIndex() - if not parent.isValid(): - return self.createIndex(row, column, self.nodes[row]) - node = parent.internalPointer() - return self.createIndex(row, column, node.children[row]) - - def parent(self, index): - if not index.isValid(): - return QModelIndex() - node = index.internalPointer() - if node.parent is None: - return QModelIndex() - else: - return self.createIndex(node.parent.row, 0, node.parent) - - def reset(self): - self._nodes = None - QAbstractItemModel.reset(self) - - def rowCount(self, parent): - if not parent.isValid(): - return len(self.nodes) - node = parent.internalPointer() - return len(node.children) - - @property - def nodes(self): - if self._nodes is None: - self._nodes = self._root_nodes() - return self._nodes - From e0ed1414fbdc64146b5341b4c3ea0749d075c0a4 Mon Sep 17 00:00:00 2001 From: hsoft Date: Sun, 27 Sep 2009 08:44:35 +0000 Subject: [PATCH 142/275] se qt: Adjusted codebase to the latest dev trends. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40143 --- se/qt/gen.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/se/qt/gen.py b/se/qt/gen.py index 87e6483a..6ae54431 100644 --- a/se/qt/gen.py +++ b/se/qt/gen.py @@ -9,17 +9,13 @@ # http://www.hardcoded.net/licenses/hs_license import os +import os.path as op -def print_and_do(cmd): - print cmd - os.system(cmd) +from hsutil.build import print_and_do, build_all_qt_ui -os.chdir('base') -print_and_do('python gen.py') -os.chdir('..') - -print_and_do("pyuic4 details_dialog.ui > details_dialog_ui.py") -print_and_do("pyuic4 preferences_dialog.ui > preferences_dialog_ui.py") +build_all_qt_ui(op.join('qtlib', 'ui')) +build_all_qt_ui('base') +build_all_qt_ui('.') os.chdir('help') print_and_do('python gen.py') From d9344a6d63be56979544c3a95a50516791a97324 Mon Sep 17 00:00:00 2001 From: hsoft Date: Sun, 27 Sep 2009 09:26:54 +0000 Subject: [PATCH 143/275] dg base: Adjusted to hsfs section removal part 1. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40144 --- base/py/app.py | 2 +- base/py/data.py | 3 ++- base/py/data_me.py | 3 +++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/base/py/app.py b/base/py/app.py index 004e31d7..336d0f79 100644 --- a/base/py/app.py +++ b/base/py/app.py @@ -85,7 +85,7 @@ class DupeGuru(RegistrableApplication): self.results.load_from_xml(op.join(self.appdata, 'last_results.xml'), self._get_file, j) files = flatten(g[:] for g in self.results.groups) for file in j.iter_with_progress(files, 'Reading metadata %d/%d'): - file._read_all_info(sections=[IT_ATTRS, IT_EXTRA]) + file._read_all_info(attrnames=self.data.METADATA_TO_READ) def _get_display_info(self, dupe, group, delta=False): if (dupe is None) or (group is None): diff --git a/base/py/data.py b/base/py/data.py index 49f5fbf0..3b1d1017 100644 --- a/base/py/data.py +++ b/base/py/data.py @@ -53,6 +53,8 @@ COLUMNS = [ {'attr':'dupe_count','display':'Dupe Count'}, ] +METADATA_TO_READ = ['size', 'ctime', 'mtime'] + def GetDisplayInfo(dupe, group, delta): size = dupe.size ctime = dupe.ctime @@ -98,4 +100,3 @@ def GetGroupSortKey(group, key): if key == 8: return len(group) return cmp_value(getattr(group.ref, COLUMNS[key]['attr'])) - diff --git a/base/py/data_me.py b/base/py/data_me.py index ab4d9dbf..41ce0f85 100644 --- a/base/py/data_me.py +++ b/base/py/data_me.py @@ -33,6 +33,9 @@ COLUMNS = [ {'attr':'dupe_count','display':'Dupe Count'}, ] +METADATA_TO_READ = ['size', 'ctime', 'mtime', 'duration', 'bitrate', 'samplerate', 'title', 'artist', + 'album', 'genre', 'year', 'track', 'comment'] + def GetDisplayInfo(dupe, group, delta): size = dupe.size duration = dupe.duration From a7f70495fdd545e5baa9b62d5ef159b10cc470b5 Mon Sep 17 00:00:00 2001 From: hsoft Date: Sun, 27 Sep 2009 09:31:57 +0000 Subject: [PATCH 144/275] dgpe base: Adjusted to hsfs section removal part 1. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40145 --- pe/py/data.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pe/py/data.py b/pe/py/data.py index 5ad691c4..f13cf14b 100644 --- a/pe/py/data.py +++ b/pe/py/data.py @@ -25,6 +25,8 @@ COLUMNS = [ {'attr':'dupe_count','display':'Dupe Count'}, ] +METADATA_TO_READ = ['size', 'ctime', 'mtime', 'dimensions'] + def GetDisplayInfo(dupe,group,delta=False): if (dupe is None) or (group is None): return ['---'] * len(COLUMNS) From 5a94c82f95adc675f01b0961e2a90b2aa287d84b Mon Sep 17 00:00:00 2001 From: hsoft Date: Sun, 27 Sep 2009 10:16:15 +0000 Subject: [PATCH 145/275] dg base: Adjusted to hsfs section removal part 4. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40146 --- base/py/app.py | 1 - 1 file changed, 1 deletion(-) diff --git a/base/py/app.py b/base/py/app.py index 336d0f79..97356bd6 100644 --- a/base/py/app.py +++ b/base/py/app.py @@ -12,7 +12,6 @@ import os import os.path as op import logging -from hsfs import IT_ATTRS, IT_EXTRA from hsutil import job, io, files from hsutil.path import Path from hsutil.reg import RegistrableApplication, RegistrationRequired From b816955d9574af985df2e5a1b33f5e679aba286d Mon Sep 17 00:00:00 2001 From: hsoft Date: Sun, 27 Sep 2009 17:16:14 +0000 Subject: [PATCH 146/275] dgpe help: added tix support --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40147 --- pe/help/gen.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pe/help/gen.py b/pe/help/gen.py index d1fa2ce3..bb523f62 100644 --- a/pe/help/gen.py +++ b/pe/help/gen.py @@ -7,6 +7,8 @@ import os -from hsdocgen import generate_help +from hsdocgen import generate_help, filters -generate_help.main('.', 'dupeguru_pe_help', force_render=True) +tix = filters.tixgen("https://hardcoded.lighthouseapp.com/projects/31699-dupeguru/tickets/{0}") + +generate_help.main('.', 'dupeguru_pe_help', force_render=True, tix=tix) From 355c617de4e4eac05ea0fffce9f3a29608241528 Mon Sep 17 00:00:00 2001 From: hsoft Date: Sun, 27 Sep 2009 17:16:52 +0000 Subject: [PATCH 147/275] dgpe qt: adjusted to the latest developements. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40148 --- pe/qt/app.py | 29 +++++++++-------------------- pe/qt/gen.py | 17 +++++++---------- 2 files changed, 16 insertions(+), 30 deletions(-) diff --git a/pe/qt/app.py b/pe/qt/app.py index b99de5fd..d00606dd 100644 --- a/pe/qt/app.py +++ b/pe/qt/app.py @@ -12,7 +12,7 @@ import os.path as op from PyQt4.QtGui import QImage import PIL.Image -from hsfs import phys, IT_ATTRS, IT_MD5, IT_EXTRA +from hsfs import phys from hsutil.str import get_file_ext from dupeguru_pe import data as data_pe @@ -27,27 +27,16 @@ from preferences import Preferences from preferences_dialog import PreferencesDialog class File(phys.File): - cls_info_map = { - 'size': IT_ATTRS, - 'ctime': IT_ATTRS, - 'mtime': IT_ATTRS, - 'md5': IT_MD5, - 'md5partial': IT_MD5, - 'dimensions': IT_EXTRA, - } + INITIAL_INFO = phys.File.INITIAL_INFO.copy() + INITIAL_INFO.update({ + 'dimensions': (0,0), + }) - def _initialize_info(self, section): - super(File, self)._initialize_info(section) - if section == IT_EXTRA: - self._info.update({ - 'dimensions': (0,0), - }) - - def _read_info(self, section): - super(File, self)._read_info(section) - if section == IT_EXTRA: + def _read_info(self, field): + super(File, self)._read_info(field) + if field == 'dimensions': im = PIL.Image.open(unicode(self.path)) - self._info['dimensions'] = im.size + self.dimensions = im.size def get_blocks(self, block_count_per_side): image = QImage(unicode(self.path)) diff --git a/pe/qt/gen.py b/pe/qt/gen.py index 114e0ac4..183aec92 100644 --- a/pe/qt/gen.py +++ b/pe/qt/gen.py @@ -11,9 +11,11 @@ import os import os.path as op -def print_and_do(cmd): - print cmd - os.system(cmd) +from hsutil.build import print_and_do, build_all_qt_ui + +build_all_qt_ui(op.join('qtlib', 'ui')) +build_all_qt_ui('base') +build_all_qt_ui('.') def move(src, dst): if not op.exists(src): @@ -27,19 +29,14 @@ os.chdir('dupeguru_pe') print_and_do('python gen.py') os.chdir('..') -os.chdir('base') -print_and_do('python gen.py') -os.chdir('..') - +# The CC=gcc-4.0 thing is because, in Snow Leopard, gcc-4.2 can't compile these units. +os.environ['CC'] = 'gcc-4.0' os.chdir(op.join('modules', 'block')) os.system('python setup.py build_ext --inplace') os.chdir(op.join('..', '..')) move(op.join('modules', 'block', '_block.so'), op.join('.', '_block.so')) move(op.join('modules', 'block', '_block.pyd'), op.join('.', '_block.pyd')) -print_and_do("pyuic4 details_dialog.ui > details_dialog_ui.py") -print_and_do("pyuic4 preferences_dialog.ui > preferences_dialog_ui.py") - os.chdir('help') print_and_do('python gen.py') os.chdir('..') \ No newline at end of file From 9a44956d8f8d61dfb28511c8c24299c4094a156a Mon Sep 17 00:00:00 2001 From: hsoft Date: Mon, 28 Sep 2009 15:31:54 +0000 Subject: [PATCH 148/275] dgpe cocoa: adjusted to the latest developments. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40149 --- pe/cocoa/py/dg_cocoa.py | 2 +- pe/py/app_cocoa.py | 29 +++++++++-------------------- 2 files changed, 10 insertions(+), 21 deletions(-) diff --git a/pe/cocoa/py/dg_cocoa.py b/pe/cocoa/py/dg_cocoa.py index aa2131d1..66e1ec01 100644 --- a/pe/cocoa/py/dg_cocoa.py +++ b/pe/cocoa/py/dg_cocoa.py @@ -12,7 +12,7 @@ from dupeguru_pe import app_cocoa as app_pe_cocoa # Fix py2app imports which chokes on relative imports from dupeguru import app, app_cocoa, data, directories, engine, export, ignore, results, scanner from dupeguru_pe import block, cache, matchbase, data -from hsfs import auto, manual, stats, tree, utils +from hsfs import auto, stats, tree class PyApp(NSObject): pass #fake class diff --git a/pe/py/app_cocoa.py b/pe/py/app_cocoa.py index 1e5fbd1c..249c6966 100644 --- a/pe/py/app_cocoa.py +++ b/pe/py/app_cocoa.py @@ -34,31 +34,20 @@ PictureBlocks = mainBundle.classNamed_('PictureBlocks') assert PictureBlocks is not None class Photo(phys.File): - cls_info_map = { - 'size': fs.IT_ATTRS, - 'ctime': fs.IT_ATTRS, - 'mtime': fs.IT_ATTRS, - 'md5': fs.IT_MD5, - 'md5partial': fs.IT_MD5, - 'dimensions': fs.IT_EXTRA, - } + INITIAL_INFO = phys.File.INITIAL_INFO.copy() + INITIAL_INFO.update({ + 'dimensions': (0,0), + }) - def _initialize_info(self,section): - super(Photo, self)._initialize_info(section) - if section == fs.IT_EXTRA: - self._info.update({ - 'dimensions': (0,0), - }) - - def _read_info(self,section): - super(Photo, self)._read_info(section) - if section == fs.IT_EXTRA: + def _read_info(self, field): + super(Photo, self)._read_info(field) + if field == 'dimensions': size = PictureBlocks.getImageSize_(unicode(self.path)) - self._info['dimensions'] = (size.width, size.height) + self.dimensions = (size.width, size.height) def get_blocks(self, block_count_per_side): try: - blocks = PictureBlocks.getBlocksFromImagePath_blockCount_scanArea_(unicode(self.path), block_count_per_side, 0) + blocks = PictureBlocks.getBlocksFromImagePath_blockCount_(unicode(self.path), block_count_per_side) except Exception, e: raise IOError('The reading of "%s" failed with "%s"' % (unicode(self.path), unicode(e))) if not blocks: From 7e45f422e1b330381a7f03a27b4749dd95ce4517 Mon Sep 17 00:00:00 2001 From: hsoft Date: Mon, 28 Sep 2009 15:33:25 +0000 Subject: [PATCH 149/275] [#64 state:fixed] fixed a bug in PictureBlocks where transparency could make two equal pictures return two different sets of blocks. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40150 --- pe/cocoa/PictureBlocks.h | 4 ++-- pe/cocoa/PictureBlocks.m | 17 ++++++++--------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/pe/cocoa/PictureBlocks.h b/pe/cocoa/PictureBlocks.h index 8a230e06..54eaec7f 100644 --- a/pe/cocoa/PictureBlocks.h +++ b/pe/cocoa/PictureBlocks.h @@ -11,9 +11,9 @@ http://www.hardcoded.net/licenses/hs_license @interface PictureBlocks : NSObject { } -+ (NSString *)getBlocksFromImagePath:(NSString *)imagePath blockCount:(NSNumber *)blockCount scanArea:(NSNumber *)scanArea; ++ (NSString *)getBlocksFromImagePath:(NSString *)imagePath blockCount:(NSNumber *)blockCount; + (NSSize)getImageSize:(NSString *)imagePath; @end -NSString* GetBlocks(NSString *filePath, int blockCount, int scanSize); \ No newline at end of file +NSString* GetBlocks(NSString *filePath, int blockCount); \ No newline at end of file diff --git a/pe/cocoa/PictureBlocks.m b/pe/cocoa/PictureBlocks.m index 007ff246..394da6eb 100644 --- a/pe/cocoa/PictureBlocks.m +++ b/pe/cocoa/PictureBlocks.m @@ -9,11 +9,10 @@ http://www.hardcoded.net/licenses/hs_license #import "PictureBlocks.h" #import "Utils.h" - @implementation PictureBlocks -+ (NSString *)getBlocksFromImagePath:(NSString *)imagePath blockCount:(NSNumber *)blockCount scanArea:(NSNumber *)scanArea ++ (NSString *)getBlocksFromImagePath:(NSString *)imagePath blockCount:(NSNumber *)blockCount { - return GetBlocks(imagePath, n2i(blockCount), n2i(scanArea)); + return GetBlocks(imagePath, n2i(blockCount)); } + (NSSize)getImageSize:(NSString *)imagePath @@ -47,7 +46,11 @@ http://www.hardcoded.net/licenses/hs_license colorSpace = CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB); - bitmapData = malloc( bitmapByteCount ); + // calloc() must be used to allocate bitmapData here because the buffer has to be zeroed. + // If it's not zeroes, when images with transparency are drawn in the context, this buffer + // will stay with undefined pixels, which means that two pictures with the same pixels will + // most likely have different blocks (which is not supposed to happen). + bitmapData = calloc(bitmapByteCount, 1); if (bitmapData == NULL) { fprintf (stderr, "Memory not allocated!"); @@ -90,7 +93,7 @@ http://www.hardcoded.net/licenses/hs_license return result; } - NSString* GetBlocks (NSString* filePath, int blockCount, int scanSize) + NSString* GetBlocks (NSString* filePath, int blockCount) { CFURLRef fileURL = CFURLCreateWithFileSystemPath(NULL, (CFStringRef)filePath, kCFURLPOSIXPathStyle, FALSE); CGImageSourceRef source = CGImageSourceCreateWithURL(fileURL, NULL); @@ -101,10 +104,6 @@ http://www.hardcoded.net/licenses/hs_license return NULL; size_t width = CGImageGetWidth(image); size_t height = CGImageGetHeight(image); - if ((scanSize > 0) && (width > scanSize)) - width = scanSize; - if ((scanSize > 0) && (height > scanSize)) - height = scanSize; CGContextRef myContext = MyCreateBitmapContext(width, height); CGRect myBoundingBox = CGRectMake (0, 0, width, height); CGContextDrawImage(myContext, myBoundingBox, image); From 9210a8dd6e77190a1d1965b227dff0c46277f2ce Mon Sep 17 00:00:00 2001 From: hsoft Date: Mon, 28 Sep 2009 15:52:41 +0000 Subject: [PATCH 150/275] dgme help: adjusted to the latest developments. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40151 --- me/help/gen.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/me/help/gen.py b/me/help/gen.py index eb45cfbb..7fc7a854 100644 --- a/me/help/gen.py +++ b/me/help/gen.py @@ -7,6 +7,8 @@ import os -from hsdocgen import generate_help +from hsdocgen import generate_help, filters -generate_help.main('.', 'dupeguru_me_help', force_render=True) \ No newline at end of file +tix = filters.tixgen("https://hardcoded.lighthouseapp.com/projects/31699-dupeguru/tickets/{0}") + +generate_help.main('.', 'dupeguru_me_help', force_render=True, tix=tix) \ No newline at end of file From e0f52c9b805d45438be609318297bddecf1d61cd Mon Sep 17 00:00:00 2001 From: hsoft Date: Mon, 28 Sep 2009 15:52:55 +0000 Subject: [PATCH 151/275] dgme qt: adjusted to the latest developments. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40152 --- me/qt/gen.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/me/qt/gen.py b/me/qt/gen.py index 00d60204..9510c5dc 100644 --- a/me/qt/gen.py +++ b/me/qt/gen.py @@ -9,15 +9,13 @@ # http://www.hardcoded.net/licenses/hs_license import os +import os.path as op -from hsutil.build import print_and_do +from hsutil.build import print_and_do, build_all_qt_ui -os.chdir('base') -print_and_do('python gen.py') -os.chdir('..') - -print_and_do("pyuic4 details_dialog.ui > details_dialog_ui.py") -print_and_do("pyuic4 preferences_dialog.ui > preferences_dialog_ui.py") +build_all_qt_ui(op.join('qtlib', 'ui')) +build_all_qt_ui('base') +build_all_qt_ui('.') os.chdir('help') print_and_do('python gen.py') From bb2dab505fa0d18a6652cb33e101791be60eaa58 Mon Sep 17 00:00:00 2001 From: hsoft Date: Mon, 28 Sep 2009 16:08:09 +0000 Subject: [PATCH 152/275] dgme cocoa: adjusted to the latest developments. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40153 --- me/cocoa/py/dg_cocoa.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/me/cocoa/py/dg_cocoa.py b/me/cocoa/py/dg_cocoa.py index 613f9044..87c723c0 100644 --- a/me/cocoa/py/dg_cocoa.py +++ b/me/cocoa/py/dg_cocoa.py @@ -12,9 +12,10 @@ from dupeguru import app_me_cocoa, scanner # Fix py2app imports which chokes on relative imports from dupeguru import app, app_cocoa, data, directories, engine, export, ignore, results, scanner -from hsfs import auto, manual, stats, tree, utils, music +from hsfs import auto, stats, tree, music from hsfs.phys import music from hsmedia import aiff, flac, genres, id3v1, id3v2, mp4, mpeg, ogg, wma +from hsutil import conflict class PyApp(NSObject): pass #fake class From 25207831ccd2c31b9ba5282301d53d50006438f9 Mon Sep 17 00:00:00 2001 From: hsoft Date: Mon, 28 Sep 2009 16:19:27 +0000 Subject: [PATCH 153/275] dgse cocoa: added "/dev" to the list of folder to auto-ignore --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40154 --- base/py/app_se_cocoa.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base/py/app_se_cocoa.py b/base/py/app_se_cocoa.py index e24b105c..22fc7bec 100644 --- a/base/py/app_se_cocoa.py +++ b/base/py/app_se_cocoa.py @@ -47,7 +47,7 @@ class DGDirectory(DirectoryBase): class Directories(DirectoriesBase): - ROOT_PATH_TO_EXCLUDE = map(Path, ['/Library', '/Volumes', '/System', '/bin', '/sbin', '/opt', '/private']) + ROOT_PATH_TO_EXCLUDE = map(Path, ['/Library', '/Volumes', '/System', '/bin', '/sbin', '/opt', '/private', '/dev']) HOME_PATH_TO_EXCLUDE = [Path('Library')] def __init__(self): DirectoriesBase.__init__(self) From e5f3976b89a8b3c581ad472bb036a02b9b4f9ec1 Mon Sep 17 00:00:00 2001 From: hsoft Date: Mon, 28 Sep 2009 16:24:03 +0000 Subject: [PATCH 154/275] dgse cocoa: adjusted to the latest developments. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40155 --- se/cocoa/py/dg_cocoa.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/se/cocoa/py/dg_cocoa.py b/se/cocoa/py/dg_cocoa.py index 1dffaa32..93eded52 100644 --- a/se/cocoa/py/dg_cocoa.py +++ b/se/cocoa/py/dg_cocoa.py @@ -12,8 +12,9 @@ from dupeguru import app_se_cocoa, scanner # Fix py2app imports with chokes on relative imports from dupeguru import app, app_cocoa, data, directories, engine, export, ignore, results, scanner -from hsfs import auto, manual, stats, tree, utils +from hsfs import auto, stats, tree from hsfs.phys import bundle +from hsutil import conflict class PyApp(NSObject): pass #fake class From 01b06c90b219dc7eb382e2fa5de16dab61ccc81b Mon Sep 17 00:00:00 2001 From: hsoft Date: Tue, 29 Sep 2009 11:15:36 +0000 Subject: [PATCH 155/275] [#60 state:fixed] Ignore invalid regexp in filter field --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40156 --- base/py/results.py | 5 ++++- base/py/tests/results_test.py | 5 +++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/base/py/results.py b/base/py/results.py index dee24920..5b631724 100644 --- a/base/py/results.py +++ b/base/py/results.py @@ -142,8 +142,11 @@ class Results(Markable): else: if not self.__filters: self.__filters = [] + try: + filter_re = re.compile(filter_str, re.IGNORECASE) + except re.error: + return # don't apply this filter. self.__filters.append(filter_str) - filter_re = re.compile(filter_str, re.IGNORECASE) if self.__filtered_dupes is None: self.__filtered_dupes = flatten(g[:] for g in self.groups) self.__filtered_dupes = set(dupe for dupe in self.__filtered_dupes if filter_re.search(dupe.name)) diff --git a/base/py/tests/results_test.py b/base/py/tests/results_test.py index e71e7680..b49303a9 100644 --- a/base/py/tests/results_test.py +++ b/base/py/tests/results_test.py @@ -48,6 +48,11 @@ class TCResultsEmpty(TestCase): def setUp(self): self.results = Results(data) + def test_apply_invalid_filter(self): + # If the applied filter is an invalid regexp, just ignore the filter. + self.results.apply_filter('[') # invalid + self.test_stat_line() # make sure that the stats line isn't saying we applied a '[' filter + def test_stat_line(self): self.assertEqual("0 / 0 (0.00 B / 0.00 B) duplicates marked.",self.results.stat_line) From 0c1854e02a2466f5adcc84b96b5b7494b71cf7a3 Mon Sep 17 00:00:00 2001 From: hsoft Date: Tue, 29 Sep 2009 13:05:19 +0000 Subject: [PATCH 156/275] [#61 state:fixed] Fixed parsing of iPhoto libraries where iPhoto didn't properly escape "&" characters. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40157 --- pe/py/app_cocoa.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pe/py/app_cocoa.py b/pe/py/app_cocoa.py index 249c6966..52849c97 100644 --- a/pe/py/app_cocoa.py +++ b/pe/py/app_cocoa.py @@ -11,6 +11,7 @@ import os import os.path as op import logging import plistlib +import re import objc from Foundation import * @@ -108,6 +109,12 @@ class IPhotoLibrary(fs.Directory): s = open(unicode(self.plistpath)).read() # There was a case where a guy had 0x10 chars in his plist, causing expat errors on loading s = s.replace('\x10', '') + # It seems that iPhoto sometimes doesn't properly escape & chars. The regexp below is to find + # any & char that is not a &-based entity (&, ", 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) plist = plistlib.readPlistFromString(s) for photo_data in plist['Master Image List'].values(): self._update_photo(photo_data) From f792d959e2dba155581ba651b4a356534c1f8459 Mon Sep 17 00:00:00 2001 From: hsoft Date: Tue, 29 Sep 2009 14:07:50 +0000 Subject: [PATCH 157/275] [#8] Changed the appdata base path (and PE's) --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40158 --- base/py/app_cocoa.py | 4 +++- pe/py/app_cocoa.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/base/py/app_cocoa.py b/base/py/app_cocoa.py index 5e1cb8e6..de9522db 100644 --- a/base/py/app_cocoa.py +++ b/base/py/app_cocoa.py @@ -7,6 +7,7 @@ # which should be included with this package. The terms are also available at # http://www.hardcoded.net/licenses/hs_license +from Foundation import * from AppKit import * import logging import os.path as op @@ -44,7 +45,8 @@ class DupeGuru(app.DupeGuru): install_exception_hook() if data_module is None: data_module = data - appdata = op.expanduser(op.join('~', '.hsoftdata', appdata_subdir)) + appsupport = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, True)[0] + appdata = op.join(appsupport, appdata_subdir) app.DupeGuru.__init__(self, data_module, appdata, appid) self.progress = cocoa.ThreadedJobPerformer() self.display_delta_values = False diff --git a/pe/py/app_cocoa.py b/pe/py/app_cocoa.py index 52849c97..4880b2e2 100644 --- a/pe/py/app_cocoa.py +++ b/pe/py/app_cocoa.py @@ -125,7 +125,7 @@ class IPhotoLibrary(fs.Directory): class DupeGuruPE(app_cocoa.DupeGuru): def __init__(self): - app_cocoa.DupeGuru.__init__(self, data, 'dupeguru_pe', appid=5) + app_cocoa.DupeGuru.__init__(self, data, 'dupeGuru Picture Edition', appid=5) self.scanner.match_factory = matchbase.AsyncMatchFactory() self.directories.dirclass = Directory self.directories.special_dirclasses[Path('iPhoto Library')] = lambda _, __: self._create_iphoto_library() From d8ecf954978b9bea2dc0b3e6daec1f9ebeb2f12b Mon Sep 17 00:00:00 2001 From: hsoft Date: Tue, 29 Sep 2009 14:19:45 +0000 Subject: [PATCH 158/275] [#8 state:fixed] Changed SE and ME's appdata path. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40159 --- base/py/app_me_cocoa.py | 2 +- base/py/app_se_cocoa.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/base/py/app_me_cocoa.py b/base/py/app_me_cocoa.py index c06ff42f..d9850c2d 100644 --- a/base/py/app_me_cocoa.py +++ b/base/py/app_me_cocoa.py @@ -27,7 +27,7 @@ app_cocoa.JOBID2TITLE.update({ class DupeGuruME(app_cocoa.DupeGuru): def __init__(self): - app_cocoa.DupeGuru.__init__(self, data_me, 'dupeguru_me', appid=1) + app_cocoa.DupeGuru.__init__(self, data_me, 'dupeGuru Music Edition', appid=1) self.scanner = scanner.ScannerME() self.directories.dirclass = hsfs.phys.music.Directory self.dead_tracks = [] diff --git a/base/py/app_se_cocoa.py b/base/py/app_se_cocoa.py index 22fc7bec..c431ab05 100644 --- a/base/py/app_se_cocoa.py +++ b/base/py/app_se_cocoa.py @@ -65,6 +65,6 @@ class Directories(DirectoriesBase): class DupeGuru(app_cocoa.DupeGuru): def __init__(self): - app_cocoa.DupeGuru.__init__(self, data, 'dupeguru', appid=4) + app_cocoa.DupeGuru.__init__(self, data, 'dupeGuru', appid=4) self.directories = Directories() From e7cdec93c0c8bf047a5f7b0e76d3cf61cb5b402b Mon Sep 17 00:00:00 2001 From: hsoft Date: Tue, 29 Sep 2009 14:37:01 +0000 Subject: [PATCH 159/275] [#66 state:fixed] Fixed crash when changing the state of an expanded directory. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40160 --- base/cocoa/DirectoryPanel.m | 1 + 1 file changed, 1 insertion(+) diff --git a/base/cocoa/DirectoryPanel.m b/base/cocoa/DirectoryPanel.m index 6cc822b8..f2a0d0fe 100644 --- a/base/cocoa/DirectoryPanel.m +++ b/base/cocoa/DirectoryPanel.m @@ -58,6 +58,7 @@ http://www.hardcoded.net/licenses/hs_license OVNode *node = [directories itemAtRow:[directories clickedRow]]; [_py setDirectory:p2a([node indexPath]) state:i2n([sender tag])]; [node resetAllBuffers]; + [directories reloadItem:node reloadChildren:YES]; [directories display]; } From f739580db0f01e06275817c61d9041c599959730 Mon Sep 17 00:00:00 2001 From: hsoft Date: Wed, 30 Sep 2009 16:29:50 +0000 Subject: [PATCH 160/275] [#55 state:fixed] fixed a crash after makeReference when the action would make the dupe list smaller. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40161 --- base/cocoa/ResultWindow.m | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/base/cocoa/ResultWindow.m b/base/cocoa/ResultWindow.m index 9ee36361..800daabc 100644 --- a/base/cocoa/ResultWindow.m +++ b/base/cocoa/ResultWindow.m @@ -244,9 +244,19 @@ http://www.hardcoded.net/licenses/hs_license - (IBAction)switchSelected:(id)sender { + // It might look like a complicated way to get the length of the current dupe list on the py side + // but after a lot of fussing around, believe it or not, it actually is. + int matchesTag = _powerMode ? 2 : 0; + int startLen = [[py getOutlineView:matchesTag childCountsForPath:[NSArray array]] count]; [self performPySelection:[self getSelectedPaths:YES]]; [py makeSelectedReference]; - [[NSNotificationCenter defaultCenter] postNotificationName:ResultsUpdatedNotification object:self]; + // In some cases (when in a filtered view in Power Marker mode, it's possible that the demoted + // ref is not a part of the filter, making the table smaller. In those cases, we want to do a + // complete reload of the table to avoid a crash. + if ([[py getOutlineView:matchesTag childCountsForPath:[NSArray array]] count] == startLen) + [[NSNotificationCenter defaultCenter] postNotificationName:ResultsUpdatedNotification object:self]; + else + [[NSNotificationCenter defaultCenter] postNotificationName:ResultsChangedNotification object:self]; } - (IBAction)togglePowerMarker:(id)sender From fa32880fdcf757492291de95fe26f94d3a4e247a Mon Sep 17 00:00:00 2001 From: hsoft Date: Thu, 1 Oct 2009 09:45:49 +0000 Subject: [PATCH 161/275] Make the nag window show after the application's event loop is started. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40162 --- base/qt/app.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/base/qt/app.py b/base/qt/app.py index 9a94b540..69d23f56 100644 --- a/base/qt/app.py +++ b/base/qt/app.py @@ -74,7 +74,13 @@ class DupeGuru(DupeGuruBase, QObject): self.reg = Registration(self) self.set_registration(self.prefs.registration_code, self.prefs.registration_email) if not self.registered: - self.reg.show_nag() + # The timer scheme is because if the nag is not shown before the application is + # completely initialized, the nag will be shown before the app shows up in the task bar + # In some circumstances, the nag is hidden by other window, which may make the user think + # that the application haven't launched. + self._nagTimer = QTimer() + self.connect(self._nagTimer, SIGNAL('timeout()'), self.mustShowNag) + self._nagTimer.start(0) self.main_window.show() self.load() @@ -226,6 +232,10 @@ class DupeGuru(DupeGuruBase, QObject): self.save() self.save_ignore_list() + def mustShowNag(self): + self._nagTimer.stop() # must be shown only once + self.reg.show_nag() + def job_finished(self, jobid): self.emit(SIGNAL('resultsChanged()')) if jobid == JOB_LOAD: From 354babc4175949d72773d2dbefa40763a8658ba7 Mon Sep 17 00:00:00 2001 From: hsoft Date: Thu, 1 Oct 2009 11:08:18 +0000 Subject: [PATCH 162/275] dgbase qt: Fixed a crash in the ResultsModel caused by the recent TreeModel changes. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40163 --- base/qt/results_model.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/base/qt/results_model.py b/base/qt/results_model.py index ccd84f7b..6b020fd5 100644 --- a/base/qt/results_model.py +++ b/base/qt/results_model.py @@ -109,13 +109,13 @@ class ResultsModel(TreeModel): try: if self.power_marker: row = self._results.dupes.index(dupe) - node = self.nodes[row] + node = self.subnodes[row] assert node.dupe is dupe return self.createIndex(row, 0, node) else: group = self._results.get_group_of_duplicate(dupe) row = self._results.groups.index(group) - node = self.nodes[row] + node = self.subnodes[row] if dupe is group.ref: assert node.dupe is dupe return self.createIndex(row, 0, node) From ff86624c3e1e044c0876ca293b202c3934688db4 Mon Sep 17 00:00:00 2001 From: hsoft Date: Thu, 1 Oct 2009 11:13:58 +0000 Subject: [PATCH 163/275] [#62 state:fixed] Wrapped winshell exception. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40164 --- base/qt/platform_win.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/base/qt/platform_win.py b/base/qt/platform_win.py index 960cbfb7..f1a777ae 100644 --- a/base/qt/platform_win.py +++ b/base/qt/platform_win.py @@ -8,7 +8,14 @@ # which should be included with this package. The terms are also available at # http://www.hardcoded.net/licenses/hs_license +from __future__ import unicode_literals + +import logging + import winshell def recycle_file(path): - winshell.delete_file(unicode(path), no_confirm=True) + try: + winshell.delete_file(unicode(path), no_confirm=True, silent=True) + except winshell.x_winshell as e: + logging.warning("winshell error: %s", e) From 61af22403e506f382d6985b9aff3dfcf0cf8a015 Mon Sep 17 00:00:00 2001 From: hsoft Date: Fri, 2 Oct 2009 11:12:25 +0000 Subject: [PATCH 164/275] [#63 state:fixed] Added a "Open Debug Log" menu item in dg qt's menu. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40165 --- base/qt/app.py | 9 +++++++++ base/qt/gen.py | 24 ------------------------ base/qt/main_window.py | 3 +++ base/qt/main_window.ui | 23 +++++++++++++++++++++++ base/qt/platform.py | 2 +- 5 files changed, 36 insertions(+), 25 deletions(-) delete mode 100644 base/qt/gen.py diff --git a/base/qt/app.py b/base/qt/app.py index 69d23f56..0631bb06 100644 --- a/base/qt/app.py +++ b/base/qt/app.py @@ -7,6 +7,8 @@ # which should be included with this package. The terms are also available at # http://www.hardcoded.net/licenses/hs_license +from __future__ import unicode_literals + import logging import os.path as op @@ -54,6 +56,8 @@ class DupeGuru(DupeGuruBase, QObject): def __init__(self, data_module, appid): appdata = unicode(QDesktopServices.storageLocation(QDesktopServices.DataLocation)) + # For basicConfig() to work, we have to be sure that no logging has taken place before this call. + logging.basicConfig(filename=op.join(appdata, 'debug.log'), level=logging.WARNING) DupeGuruBase.__init__(self, data_module, appdata, appid) QObject.__init__(self) self._setup() @@ -169,6 +173,11 @@ class DupeGuru(DupeGuruBase, QObject): self.results.mark_none() self.emit(SIGNAL('dupeMarkingChanged()')) + def openDebugLog(self): + debugLogPath = op.join(self.appdata, 'debug.log') + url = QUrl.fromLocalFile(debugLogPath) + QDesktopServices.openUrl(url) + def open_selected(self): if self.selected_dupe is None: return diff --git a/base/qt/gen.py b/base/qt/gen.py deleted file mode 100644 index cb1c2de7..00000000 --- a/base/qt/gen.py +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env python -# Created By: Virgil Dupras -# Created On: 2009-05-22 -# $Id$ -# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "HS" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at -# http://www.hardcoded.net/licenses/hs_license - -import os -import sys - -def print_and_do(cmd): - print cmd - os.system(cmd) - -print_and_do("pyuic4 main_window.ui > main_window_ui.py") -print_and_do("pyuic4 directories_dialog.ui > directories_dialog_ui.py") -print_and_do("pyuic4 about_box.ui > about_box_ui.py") -print_and_do("pyuic4 reg_submit_dialog.ui > reg_submit_dialog_ui.py") -print_and_do("pyuic4 reg_demo_dialog.ui > reg_demo_dialog_ui.py") -print_and_do("pyuic4 error_report_dialog.ui > error_report_dialog_ui.py") -print_and_do("pyrcc4 dg.qrc > dg_rc.py") \ No newline at end of file diff --git a/base/qt/main_window.py b/base/qt/main_window.py index a6442b9c..c19416e7 100644 --- a/base/qt/main_window.py +++ b/base/qt/main_window.py @@ -230,6 +230,9 @@ class MainWindow(QMainWindow, Ui_MainWindow): def moveTriggered(self): self.app.copy_or_move_marked(False) + def openDebugLogTriggered(self): + self.app.openDebugLog() + def openTriggered(self): self.app.open_selected() diff --git a/base/qt/main_window.ui b/base/qt/main_window.ui index 8d949c2f..cb34d01f 100644 --- a/base/qt/main_window.ui +++ b/base/qt/main_window.ui @@ -113,6 +113,7 @@ + @@ -422,6 +423,11 @@ Export To XHTML + + + Open Debug Log + + @@ -898,6 +904,22 @@ + + actionOpenDebugLog + triggered() + MainWindow + openDebugLogTriggered() + + + -1 + -1 + + + 314 + 256 + + + directoriesTriggered() @@ -930,5 +952,6 @@ registerTrigerred() checkForUpdateTriggered() exportTriggered() + openDebugLogTriggered() diff --git a/base/qt/platform.py b/base/qt/platform.py index c0668794..98e612b7 100644 --- a/base/qt/platform.py +++ b/base/qt/platform.py @@ -14,4 +14,4 @@ import sys if sys.platform == 'win32': from platform_win import * else: - logging.warning("Unsupported Platform!!") \ No newline at end of file + pass # unsupported platform From 0b968ac1ced9c8f883caf07b70164be5b4567bb9 Mon Sep 17 00:00:00 2001 From: hsoft Date: Fri, 2 Oct 2009 11:31:46 +0000 Subject: [PATCH 165/275] [#65 state:fixed] Handle shutil.Error. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40166 --- base/py/app.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/base/py/app.py b/base/py/app.py index 97356bd6..6fb6f1e2 100644 --- a/base/py/app.py +++ b/base/py/app.py @@ -8,6 +8,8 @@ # which should be included with this package. The terms are also available at # http://www.hardcoded.net/licenses/hs_license +from __future__ import unicode_literals + import os import os.path as op import logging @@ -75,7 +77,7 @@ class DupeGuru(RegistrableApplication): if not io.exists(dupe.path): dupe.parent = None return True - logging.warning(u"Could not send {0} to trash.".format(unicode(dupe.path))) + logging.warning("Could not send {0} to trash.".format(unicode(dupe.path))) return False def _do_load(self, j): @@ -92,7 +94,7 @@ class DupeGuru(RegistrableApplication): try: return self.data.GetDisplayInfo(dupe, group, delta) except Exception as e: - logging.warning(u'Exception on GetDisplayInfo for %s: %s', unicode(dupe.path), unicode(e)) + logging.warning("Exception on GetDisplayInfo for %s: %s", unicode(dupe.path), unicode(e)) return ['---'] * len(self.data.COLUMNS) def _get_file(self, str_path): @@ -162,7 +164,7 @@ class DupeGuru(RegistrableApplication): else: files.move(source_path, dest_path) self.clean_empty_dirs(source_path[:-1]) - except (IOError, OSError) as e: + except EnvironmentError as e: operation = 'Copy' if copy else 'Move' logging.warning('%s operation failed on %s. Error: %s' % (operation, unicode(dupe.path), unicode(e))) return False From fab04de69026aa6067e06c84616944637390ff8c Mon Sep 17 00:00:00 2001 From: hsoft Date: Fri, 2 Oct 2009 11:44:39 +0000 Subject: [PATCH 166/275] dgse qt v2.8.1 --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40167 --- se/help/changelog.yaml | 7 +++++++ se/qt/app.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/se/help/changelog.yaml b/se/help/changelog.yaml index b629350d..f03450c8 100644 --- a/se/help/changelog.yaml +++ b/se/help/changelog.yaml @@ -1,3 +1,10 @@ +- date: 2009-10-02 + version: 2.8.1 + description: | + * Fixed crash with filtering when regular expressions were enabled. (#60) + * Fixed crash when setting directories' state. (Mac OS X) (#66) + * Fixed crash with Make Reference when certain filters are applied. (Mac OS X) (#55) + * Improved error handling during delete/move/copy actions. (#62 #65) - date: 2009-09-07 version: 2.8.0 description: | diff --git a/se/qt/app.py b/se/qt/app.py index adbb75e9..29edcf05 100644 --- a/se/qt/app.py +++ b/se/qt/app.py @@ -27,7 +27,7 @@ class Directories(DirectoriesBase): class DupeGuru(DupeGuruBase): LOGO_NAME = 'logo_se' NAME = 'dupeGuru' - VERSION = '2.8.0' + VERSION = '2.8.1' DELTA_COLUMNS = frozenset([2, 4, 5]) def __init__(self): From 627fac0d2ed47dda534e3b324b74b67707150829 Mon Sep 17 00:00:00 2001 From: hsoft Date: Fri, 2 Oct 2009 13:56:56 +0000 Subject: [PATCH 167/275] dgse cocoa v2.8.1 --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40168 --- se/cocoa/Info.plist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/se/cocoa/Info.plist b/se/cocoa/Info.plist index 2e01d45c..c2787340 100644 --- a/se/cocoa/Info.plist +++ b/se/cocoa/Info.plist @@ -23,7 +23,7 @@ CFBundleSignature hsft CFBundleVersion - 2.8.0 + 2.8.1 NSMainNibFile MainMenu NSPrincipalClass From f5c20b149b1defa9293af41b99ddc575d980c2be Mon Sep 17 00:00:00 2001 From: hsoft Date: Sat, 3 Oct 2009 10:27:32 +0000 Subject: [PATCH 168/275] [#39 state:fixed] Wrapped the LookupError in DupeGuru.save() --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40169 --- base/py/app.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/base/py/app.py b/base/py/app.py index 6fb6f1e2..0748ac2c 100644 --- a/base/py/app.py +++ b/base/py/app.py @@ -218,8 +218,13 @@ class DupeGuru(RegistrableApplication): changed_groups.add(g) def save(self): - self.directories.save_to_file(op.join(self.appdata, 'last_directories.xml')) - self.results.save_to_xml(op.join(self.appdata, 'last_results.xml')) + try: + self.directories.save_to_file(op.join(self.appdata, 'last_directories.xml')) + self.results.save_to_xml(op.join(self.appdata, 'last_results.xml')) + except LookupError: + # This is that weird issue from #39 that sometimes happens when auto-updating with + # Sparkle. Just ignore it. + pass def save_ignore_list(self): p = op.join(self.appdata, 'ignore_list.xml') From 22d9db28ff130e0c2b2e0c88d7f1acdba96f6f47 Mon Sep 17 00:00:00 2001 From: hsoft Date: Sat, 3 Oct 2009 10:32:18 +0000 Subject: [PATCH 169/275] dgpe cocoa v1.7.6 --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40170 --- pe/cocoa/Info.plist | 2 +- pe/cocoa/py/dg_cocoa.py | 1 + pe/help/changelog.yaml | 9 +++++++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/pe/cocoa/Info.plist b/pe/cocoa/Info.plist index 99b9a2b5..6960d416 100644 --- a/pe/cocoa/Info.plist +++ b/pe/cocoa/Info.plist @@ -23,7 +23,7 @@ CFBundleSignature hsft CFBundleVersion - 1.7.5 + 1.7.6 NSMainNibFile MainMenu NSPrincipalClass diff --git a/pe/cocoa/py/dg_cocoa.py b/pe/cocoa/py/dg_cocoa.py index 66e1ec01..150998b1 100644 --- a/pe/cocoa/py/dg_cocoa.py +++ b/pe/cocoa/py/dg_cocoa.py @@ -13,6 +13,7 @@ from dupeguru_pe import app_cocoa as app_pe_cocoa from dupeguru import app, app_cocoa, data, directories, engine, export, ignore, results, scanner from dupeguru_pe import block, cache, matchbase, data from hsfs import auto, stats, tree +from hsutil import conflict class PyApp(NSObject): pass #fake class diff --git a/pe/help/changelog.yaml b/pe/help/changelog.yaml index a48e6836..e2f6186f 100644 --- a/pe/help/changelog.yaml +++ b/pe/help/changelog.yaml @@ -1,3 +1,12 @@ +- date: 2009-10-03 + version: 1.7.6 + description: | + * Fixed a bug preventing some duplicates to be found if transparency was involved. (Mac OS X) (#64) + * Fixed a bug preventing some iPhoto Libraries to be read. (Mac OS X) (#61) + * Fixed crash with filtering when regular expressions were enabled. (#60) + * Fixed crash when setting directories' state. (Mac OS X) (#66) + * Fixed crash with Make Reference when certain filters were applied. (Mac OS X) (#55) + * Improved error handling during delete/move/copy actions. (#62 #65) - date: 2009-09-07 version: 1.7.5 description: | From df6c9b3740e01134318cffb83bbc7930653d6806 Mon Sep 17 00:00:00 2001 From: hsoft Date: Sat, 3 Oct 2009 10:48:08 +0000 Subject: [PATCH 170/275] Fixed Results model, which still had invalid code since the latest qtlib tree_model changes. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40171 --- base/qt/results_model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base/qt/results_model.py b/base/qt/results_model.py index 6b020fd5..d29f73bc 100644 --- a/base/qt/results_model.py +++ b/base/qt/results_model.py @@ -120,7 +120,7 @@ class ResultsModel(TreeModel): assert node.dupe is dupe return self.createIndex(row, 0, node) subrow = group.dupes.index(dupe) - subnode = node.children[subrow] + subnode = node.subnodes[subrow] assert subnode.dupe is dupe return self.createIndex(subrow, 0, subnode) except ValueError: # the dupe is not there anymore From 90e2e43f3e27c107b6e134e220d6f255f61e66b3 Mon Sep 17 00:00:00 2001 From: hsoft Date: Sat, 3 Oct 2009 11:49:53 +0000 Subject: [PATCH 171/275] [#67 state:fixed] Fixed block.pyx so it takes Qt pixel alignment into account. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40172 --- pe/qt/modules/block/block.pyx | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/pe/qt/modules/block/block.pyx b/pe/qt/modules/block/block.pyx index 22dc20d2..2f2c6825 100644 --- a/pe/qt/modules/block/block.pyx +++ b/pe/qt/modules/block/block.pyx @@ -6,7 +6,7 @@ # http://www.hardcoded.net/licenses/hs_license cdef object getblock(object image): - cdef int width, height, pixel_count, red, green, blue, i, offset + cdef int width, height, pixel_count, red, green, blue, i, offset, bytes_per_line cdef char *s cdef unsigned char r, g, b width = image.width() @@ -14,16 +14,21 @@ cdef object getblock(object image): if width: pixel_count = width * height red = green = blue = 0 + bytes_per_line = image.bytesPerLine() tmp = image.bits().asstring(image.numBytes()) s = tmp - for i in range(pixel_count): - offset = i * 3 - r = s[offset] - g = s[offset + 1] - b = s[offset + 2] - red += r - green += g - blue += b + # Qt aligns all its lines on 32bit, which means that if the number of bytes per + # line for image is not divisible by 4, there's going to be crap inserted in "s" + # We have to take this into account when calculating offsets + for i in range(height): + for j in range(width): + offset = i * bytes_per_line + j * 3 + r = s[offset] + g = s[offset + 1] + b = s[offset + 2] + red += r + green += g + blue += b return (red // pixel_count, green // pixel_count, blue // pixel_count) else: return (0, 0, 0) @@ -38,9 +43,9 @@ def getblocks(image, int block_count_per_side): block_height = max(height // block_count_per_side, 1) result = [] for ih in range(block_count_per_side): - top = min(ih * block_height, height - block_height) + top = min(ih*block_height, height-block_height-1) for iw in range(block_count_per_side): - left = min(iw * block_width, width - block_width) + left = min(iw*block_width, width-block_width-1) crop = image.copy(left, top, block_width, block_height) result.append(getblock(crop)) return result From b7acc791657f70bda99422df7c2f7f0510127da7 Mon Sep 17 00:00:00 2001 From: hsoft Date: Sat, 3 Oct 2009 12:29:16 +0000 Subject: [PATCH 172/275] dgpe qt: The reference image now longer show anything if the reference side is empty in the details panel. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40173 --- pe/help/changelog.yaml | 2 +- pe/qt/details_dialog.py | 6 +++++- pe/qt/details_dialog.ui | 32 ++++++++++++++++---------------- 3 files changed, 22 insertions(+), 18 deletions(-) diff --git a/pe/help/changelog.yaml b/pe/help/changelog.yaml index e2f6186f..1bfc740a 100644 --- a/pe/help/changelog.yaml +++ b/pe/help/changelog.yaml @@ -1,7 +1,7 @@ - date: 2009-10-03 version: 1.7.6 description: | - * Fixed a bug preventing some duplicates to be found if transparency was involved. (Mac OS X) (#64) + * Fixed bugs preventing some duplicates to be found. (#64 #67) * Fixed a bug preventing some iPhoto Libraries to be read. (Mac OS X) (#61) * Fixed crash with filtering when regular expressions were enabled. (#60) * Fixed crash when setting directories' state. (Mac OS X) (#66) diff --git a/pe/qt/details_dialog.py b/pe/qt/details_dialog.py index a0702087..b4a73c4c 100644 --- a/pe/qt/details_dialog.py +++ b/pe/qt/details_dialog.py @@ -33,7 +33,7 @@ class DetailsDialog(QDialog, Ui_DetailsDialog): self.selectedPixmap = QPixmap(unicode(dupe.path)) if ref is dupe: - self.referencePixmap = self.selectedPixmap + self.referencePixmap = None else: self.referencePixmap = QPixmap(unicode(ref.path)) self._updateImages() @@ -43,10 +43,14 @@ class DetailsDialog(QDialog, Ui_DetailsDialog): target_size = self.selectedImage.size() scaledPixmap = self.selectedPixmap.scaled(target_size, Qt.KeepAspectRatio, Qt.SmoothTransformation) self.selectedImage.setPixmap(scaledPixmap) + else: + self.selectedImage.setPixmap(QPixmap()) if self.referencePixmap is not None: target_size = self.referenceImage.size() scaledPixmap = self.referencePixmap.scaled(target_size, Qt.KeepAspectRatio, Qt.SmoothTransformation) self.referenceImage.setPixmap(scaledPixmap) + else: + self.referenceImage.setPixmap(QPixmap()) #--- Override def resizeEvent(self, event): diff --git a/pe/qt/details_dialog.ui b/pe/qt/details_dialog.ui index cee1adb1..aef34876 100644 --- a/pe/qt/details_dialog.ui +++ b/pe/qt/details_dialog.ui @@ -31,22 +31,6 @@ 4 - - - - - 0 - 0 - - - - - - - Qt::AlignCenter - - - @@ -66,6 +50,22 @@ + + + + + 0 + 0 + + + + + + + Qt::AlignCenter + + + From 2a6124eacdbb35a686dd604768b355993ba7ba2a Mon Sep 17 00:00:00 2001 From: hsoft Date: Sat, 3 Oct 2009 15:37:53 +0000 Subject: [PATCH 173/275] [#58 state:fixed] Moved the async results collection into the same loops as the async filler phase to avoid getting memory errors. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40174 --- pe/py/matchbase.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/pe/py/matchbase.py b/pe/py/matchbase.py index d1f208d9..34966a50 100644 --- a/pe/py/matchbase.py +++ b/pe/py/matchbase.py @@ -21,6 +21,11 @@ from .cache import Cache MIN_ITERATIONS = 3 +# Enough so that we're sure that the main thread will not wait after a result.get() call +# cpucount*2 should be enough to be sure that the spawned process will not wait after the results +# collection made by the main process. +RESULTS_QUEUE_LIMIT = multiprocessing.cpu_count() * 2 + def get_match(first,second,percentage): if percentage < 0: percentage = 0 @@ -40,7 +45,7 @@ class MatchFactory(object): # there is enough memory left to carry on the operation because it is assumed that the # MemoryError happens when trying to read an image file, which is freed from memory by the # time that MemoryError is raised. - j = j.start_subjob([2, 8]) + j = j.start_subjob([3, 7]) logging.info('Preparing %d files' % len(files)) prepared = self.prepare_files(files, j) logging.info('Finished preparing %d files' % len(prepared)) @@ -94,7 +99,7 @@ class AsyncMatchFactory(MatchFactory): except Empty: pass - j = j.start_subjob([1, 8, 1], 'Preparing for matching') + j = j.start_subjob([9, 1], 'Preparing for matching') cache = self.cached_blocks id2picture = {} dimensions2pictures = defaultdict(set) @@ -109,18 +114,18 @@ class AsyncMatchFactory(MatchFactory): pictures = [p for p in pictures if hasattr(p, 'cache_id')] pool = multiprocessing.Pool() async_results = [] + matches = [] pictures_copy = set(pictures) - for ref in j.iter_with_progress(pictures): + for ref in j.iter_with_progress(pictures, 'Matched %d/%d pictures'): others = pictures_copy if self.match_scaled else dimensions2pictures[ref.dimensions] others.remove(ref) if others: cache_ids = [f.cache_id for f in others] args = (ref.cache_id, cache_ids, self.cached_blocks.dbname, self.threshold) async_results.append(pool.apply_async(async_compare, args)) - - matches = [] - for result in j.iter_with_progress(async_results, 'Matched %d/%d pictures'): - matches.extend(result.get()) + if len(async_results) > RESULTS_QUEUE_LIMIT: + result = async_results.pop(0) + matches.extend(result.get()) result = [] for ref_id, other_id, percentage in j.iter_with_progress(matches, 'Verified %d/%d matches', every=10): From 723590abf8642b0b74c4254838a6c8a132fa14df Mon Sep 17 00:00:00 2001 From: hsoft Date: Sat, 3 Oct 2009 15:38:28 +0000 Subject: [PATCH 174/275] dgpe help: updated the help file to add the fix for #58 --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40175 --- pe/help/changelog.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/pe/help/changelog.yaml b/pe/help/changelog.yaml index 1bfc740a..3d0f3898 100644 --- a/pe/help/changelog.yaml +++ b/pe/help/changelog.yaml @@ -1,6 +1,7 @@ - date: 2009-10-03 version: 1.7.6 description: | + * Fixed a bug causing memory error when scanning a lot of pictures. (#58) * Fixed bugs preventing some duplicates to be found. (#64 #67) * Fixed a bug preventing some iPhoto Libraries to be read. (Mac OS X) (#61) * Fixed crash with filtering when regular expressions were enabled. (#60) From d71d8e6da91bb0e070dc67a6dae9f747b110a9fe Mon Sep 17 00:00:00 2001 From: hsoft Date: Sat, 3 Oct 2009 15:54:07 +0000 Subject: [PATCH 175/275] dgpe qt v1.7.6 --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40176 --- pe/qt/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pe/qt/app.py b/pe/qt/app.py index d00606dd..358add29 100644 --- a/pe/qt/app.py +++ b/pe/qt/app.py @@ -56,7 +56,7 @@ class Directory(phys.Directory): class DupeGuru(DupeGuruBase): LOGO_NAME = 'logo_pe' NAME = 'dupeGuru Picture Edition' - VERSION = '1.7.5' + VERSION = '1.7.6' DELTA_COLUMNS = frozenset([2, 5, 6]) def __init__(self): From 4780ce6daaac0fa76d1f2cc051c6af5edf9db2d5 Mon Sep 17 00:00:00 2001 From: hsoft Date: Sun, 4 Oct 2009 16:11:06 +0000 Subject: [PATCH 176/275] dgme cocoa v5.6.5 --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40177 --- me/cocoa/Info.plist | 2 +- me/help/changelog.yaml | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/me/cocoa/Info.plist b/me/cocoa/Info.plist index b263a8d8..0557a93e 100644 --- a/me/cocoa/Info.plist +++ b/me/cocoa/Info.plist @@ -23,7 +23,7 @@ CFBundleSignature hsft CFBundleVersion - 5.6.4 + 5.6.5 NSMainNibFile MainMenu NSPrincipalClass diff --git a/me/help/changelog.yaml b/me/help/changelog.yaml index e1adeb75..b81ca5ff 100644 --- a/me/help/changelog.yaml +++ b/me/help/changelog.yaml @@ -1,3 +1,10 @@ +- date: 2009-10-04 + version: 5.6.5 + description: | + * Fixed crash with filtering when regular expressions were enabled. (#60) + * Fixed crash when setting directories' state. (Mac OS X) (#66) + * Fixed crash with Make Reference when certain filters were applied. (Mac OS X) (#55) + * Improved error handling during delete/move/copy actions. (#62 #65) - date: 2009-09-07 version: 5.6.4 description: | From e3fb879f9f139c3b8dfa1748c6b60419593f3013 Mon Sep 17 00:00:00 2001 From: hsoft Date: Sun, 4 Oct 2009 16:32:24 +0000 Subject: [PATCH 177/275] dgme qt v5.6.5 --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40178 --- me/qt/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/me/qt/app.py b/me/qt/app.py index 51488a8e..414f434d 100644 --- a/me/qt/app.py +++ b/me/qt/app.py @@ -19,7 +19,7 @@ from preferences_dialog import PreferencesDialog class DupeGuru(DupeGuruBase): LOGO_NAME = 'logo_me' NAME = 'dupeGuru Music Edition' - VERSION = '5.6.4' + VERSION = '5.6.5' DELTA_COLUMNS = frozenset([2, 3, 4, 5, 7, 8]) def __init__(self): From 37abac14016cb6ae8fdbadd9377f6df783ba1b12 Mon Sep 17 00:00:00 2001 From: hsoft Date: Tue, 13 Oct 2009 14:59:19 +0000 Subject: [PATCH 178/275] [#68 state:fixed] Create the appdata folder before trying to create a debug log. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40179 --- base/qt/app.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/base/qt/app.py b/base/qt/app.py index 0631bb06..12171c52 100644 --- a/base/qt/app.py +++ b/base/qt/app.py @@ -10,6 +10,7 @@ from __future__ import unicode_literals import logging +import os import os.path as op from PyQt4.QtCore import Qt, QTimer, QObject, QCoreApplication, QUrl, SIGNAL @@ -56,6 +57,8 @@ class DupeGuru(DupeGuruBase, QObject): def __init__(self, data_module, appid): appdata = unicode(QDesktopServices.storageLocation(QDesktopServices.DataLocation)) + if not op.exists(appdata): + os.makedirs(appdata) # For basicConfig() to work, we have to be sure that no logging has taken place before this call. logging.basicConfig(filename=op.join(appdata, 'debug.log'), level=logging.WARNING) DupeGuruBase.__init__(self, data_module, appdata, appid) From 288601b30c8b5017dc4b72a77e27f1e267b4c72e Mon Sep 17 00:00:00 2001 From: hsoft Date: Tue, 13 Oct 2009 15:34:25 +0000 Subject: [PATCH 179/275] [#69] Added a vcredist prereq to dgse_qt's installer project. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40180 --- se/qt/build.py | 1 - se/qt/installer.aip | 83 ++++++++++++++++++++++----------------------- 2 files changed, 41 insertions(+), 43 deletions(-) diff --git a/se/qt/build.py b/se/qt/build.py index 1f11c5b2..4c17550a 100644 --- a/se/qt/build.py +++ b/se/qt/build.py @@ -36,7 +36,6 @@ fp.close() print_and_do("python C:\\Python26\\pyinstaller\\Build.py dgse.spec") os.remove('verinfo_tmp') -print_and_do("xcopy /Y C:\\src\\vs_comp\\msvcrt dist") print_and_do("xcopy /Y /S /I help\\dupeguru_help dist\\help") aicom = '"\\Program Files\\Caphyon\\Advanced Installer\\AdvancedInstaller.com"' diff --git a/se/qt/installer.aip b/se/qt/installer.aip index c60f88ae..729665cf 100644 --- a/se/qt/installer.aip +++ b/se/qt/installer.aip @@ -54,7 +54,6 @@ - @@ -67,7 +66,6 @@ - @@ -75,13 +73,12 @@ - + - @@ -90,52 +87,50 @@ - - - + + + - - - - + + + + - + - - - - + + + - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + - - - - - - + + + + + + @@ -246,8 +241,12 @@ + + + + From 3ffbd2b242183719e3a16af08068c5e7691eae69 Mon Sep 17 00:00:00 2001 From: hsoft Date: Wed, 14 Oct 2009 12:03:00 +0000 Subject: [PATCH 180/275] [#69] Added a vcredist prereq to dgse_qt's installer project. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40181 --- me/qt/build.py | 2 +- me/qt/installer.aip | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/me/qt/build.py b/me/qt/build.py index 95ce6b9f..91560469 100644 --- a/me/qt/build.py +++ b/me/qt/build.py @@ -34,7 +34,7 @@ fp.close() print_and_do("python C:\\Python26\\pyinstaller\\Build.py dgme.spec") os.remove('verinfo_tmp') -print_and_do("xcopy /Y C:\\src\\vs_comp\\msvcrt dist") +print_and_do("del *90.dll") # They're in vcredist, no need to include them print_and_do("xcopy /Y /S /I help\\dupeguru_me_help dist\\help") aicom = '"\\Program Files\\Caphyon\\Advanced Installer\\AdvancedInstaller.com"' diff --git a/me/qt/installer.aip b/me/qt/installer.aip index 51cfc4d9..dfc69c3f 100644 --- a/me/qt/installer.aip +++ b/me/qt/installer.aip @@ -230,8 +230,12 @@ + + + + From 3f78ee56a25ecca552106f1a746b93b37937c318 Mon Sep 17 00:00:00 2001 From: hsoft Date: Wed, 14 Oct 2009 12:04:34 +0000 Subject: [PATCH 181/275] [#69] The build script now removes *90.dll from the dist package. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40182 --- se/qt/build.py | 1 + 1 file changed, 1 insertion(+) diff --git a/se/qt/build.py b/se/qt/build.py index 4c17550a..63d73fea 100644 --- a/se/qt/build.py +++ b/se/qt/build.py @@ -36,6 +36,7 @@ fp.close() print_and_do("python C:\\Python26\\pyinstaller\\Build.py dgse.spec") os.remove('verinfo_tmp') +print_and_do("del *90.dll") # They're in vcredist, no need to include them print_and_do("xcopy /Y /S /I help\\dupeguru_help dist\\help") aicom = '"\\Program Files\\Caphyon\\Advanced Installer\\AdvancedInstaller.com"' From 6c185bc96006a1a9f0f4df4e10a2611c98046705 Mon Sep 17 00:00:00 2001 From: hsoft Date: Wed, 14 Oct 2009 12:17:27 +0000 Subject: [PATCH 182/275] [#69 state:fixed] Added a vcredist prereq to dgpe_qt's installer project. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40183 --- pe/qt/build.py | 2 +- pe/qt/installer.aip | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/pe/qt/build.py b/pe/qt/build.py index 306365cc..20dc9023 100644 --- a/pe/qt/build.py +++ b/pe/qt/build.py @@ -36,7 +36,7 @@ fp.close() print_and_do("python C:\\Python26\\pyinstaller\\Build.py dgpe.spec") os.remove('verinfo_tmp') -print_and_do("xcopy /Y C:\\src\\vs_comp\\msvcrt dist") +print_and_do("del *90.dll") # They're in vcredist, no need to include them print_and_do("xcopy /Y /S /I help\\dupeguru_pe_help dist\\help") aicom = '"\\Program Files\\Caphyon\\Advanced Installer\\AdvancedInstaller.com"' diff --git a/pe/qt/installer.aip b/pe/qt/installer.aip index d7f3ed6c..a3676b3e 100644 --- a/pe/qt/installer.aip +++ b/pe/qt/installer.aip @@ -238,8 +238,12 @@ + + + + From 46a27d5988a37ffeb35b8733228ba3f5a82eb41a Mon Sep 17 00:00:00 2001 From: hsoft Date: Wed, 14 Oct 2009 13:29:46 +0000 Subject: [PATCH 183/275] [#56 state:fixed] Remember the last added folder in the Directories panel. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40184 --- base/qt/directories_dialog.py | 9 ++++++--- base/qt/platform.py | 2 ++ base/qt/platform_osx.py | 16 ++++++++++++++++ base/qt/platform_win.py | 2 ++ 4 files changed, 26 insertions(+), 3 deletions(-) create mode 100644 base/qt/platform_osx.py diff --git a/base/qt/directories_dialog.py b/base/qt/directories_dialog.py index a2c36a3a..587f5d54 100644 --- a/base/qt/directories_dialog.py +++ b/base/qt/directories_dialog.py @@ -10,14 +10,16 @@ from PyQt4.QtCore import SIGNAL, Qt from PyQt4.QtGui import QDialog, QFileDialog, QHeaderView -from directories_dialog_ui import Ui_DirectoriesDialog -from directories_model import DirectoriesModel, DirectoriesDelegate +from . import platform +from .directories_dialog_ui import Ui_DirectoriesDialog +from .directories_model import DirectoriesModel, DirectoriesDelegate class DirectoriesDialog(QDialog, Ui_DirectoriesDialog): def __init__(self, parent, app): flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint QDialog.__init__(self, parent, flags) self.app = app + self.lastAddedFolder = platform.INITIAL_FOLDER_IN_DIALOGS self._setupUi() self._updateRemoveButton() @@ -54,9 +56,10 @@ class DirectoriesDialog(QDialog, Ui_DirectoriesDialog): def addButtonClicked(self): title = u"Select a directory to add to the scanning list" flags = QFileDialog.ShowDirsOnly - dirpath = unicode(QFileDialog.getExistingDirectory(self, title, '', flags)) + dirpath = unicode(QFileDialog.getExistingDirectory(self, title, self.lastAddedFolder, flags)) if not dirpath: return + self.lastAddedFolder = dirpath self.app.add_directory(dirpath) self.directoriesModel.reset() diff --git a/base/qt/platform.py b/base/qt/platform.py index 98e612b7..5962018c 100644 --- a/base/qt/platform.py +++ b/base/qt/platform.py @@ -13,5 +13,7 @@ import sys if sys.platform == 'win32': from platform_win import * +elif sys.platform == 'darwin': + from platform_osx import * else: pass # unsupported platform diff --git a/base/qt/platform_osx.py b/base/qt/platform_osx.py new file mode 100644 index 00000000..0385f8d4 --- /dev/null +++ b/base/qt/platform_osx.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# Created By: Virgil Dupras +# Created On: 2009-10-14 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license + +# dummy unit to allow the app to run under OSX during development + +INITIAL_FOLDER_IN_DIALOGS = '/' + +def recycle_file(path): + pass diff --git a/base/qt/platform_win.py b/base/qt/platform_win.py index f1a777ae..ead79783 100644 --- a/base/qt/platform_win.py +++ b/base/qt/platform_win.py @@ -14,6 +14,8 @@ import logging import winshell +INITIAL_FOLDER_IN_DIALOGS = 'C:\\' + def recycle_file(path): try: winshell.delete_file(unicode(path), no_confirm=True, silent=True) From 7f34389b8e8d938f4fd0c016eee9d8b68a167d58 Mon Sep 17 00:00:00 2001 From: hsoft Date: Wed, 14 Oct 2009 14:42:18 +0000 Subject: [PATCH 184/275] [#70 state:fixed] Wrapped get_groups() into a try..except MemoryError --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40185 --- base/py/engine.py | 48 ++++++++++++++++++++++++++--------------------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/base/py/engine.py b/base/py/engine.py index 5f5bc07c..19626f55 100644 --- a/base/py/engine.py +++ b/base/py/engine.py @@ -334,29 +334,35 @@ def get_groups(matches, j=job.nulljob): matches.sort(key=lambda match: -match.percentage) dupe2group = {} groups = [] - for match in j.iter_with_progress(matches, 'Grouped %d/%d matches', JOB_REFRESH_RATE): - first, second, _ = match - first_group = dupe2group.get(first) - second_group = dupe2group.get(second) - if first_group: - if second_group: - if first_group is second_group: - target_group = first_group + try: + for match in j.iter_with_progress(matches, 'Grouped %d/%d matches', JOB_REFRESH_RATE): + first, second, _ = match + first_group = dupe2group.get(first) + second_group = dupe2group.get(second) + if first_group: + if second_group: + if first_group is second_group: + target_group = first_group + else: + continue else: - continue + target_group = first_group + dupe2group[second] = target_group else: - target_group = first_group - dupe2group[second] = target_group - else: - if second_group: - target_group = second_group - dupe2group[first] = target_group - else: - target_group = Group() - groups.append(target_group) - dupe2group[first] = target_group - dupe2group[second] = target_group - target_group.add_match(match) + if second_group: + target_group = second_group + dupe2group[first] = target_group + else: + target_group = Group() + groups.append(target_group) + dupe2group[first] = target_group + dupe2group[second] = target_group + target_group.add_match(match) + except MemoryError: + del dupe2group + del matches + # should free enough memory to continue + logging.warning('Memory Overflow. Groups: {0}'.format(len(groups))) # Now that we have a group, we have to discard groups' matches and see if there're any "orphan" # matches, that is, matches that were candidate in a group but that none of their 2 files were # accepted in the group. With these orphan groups, it's safe to build additional groups From 10af16614ec60aeb1f2bb7f76292fa44fdf0bd0a Mon Sep 17 00:00:00 2001 From: hsoft Date: Wed, 14 Oct 2009 15:18:26 +0000 Subject: [PATCH 185/275] dgse cocoa 2.8.2 --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40186 --- se/cocoa/Info.plist | 2 +- se/help/changelog.yaml | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/se/cocoa/Info.plist b/se/cocoa/Info.plist index c2787340..9263d518 100644 --- a/se/cocoa/Info.plist +++ b/se/cocoa/Info.plist @@ -23,7 +23,7 @@ CFBundleSignature hsft CFBundleVersion - 2.8.1 + 2.8.2 NSMainNibFile MainMenu NSPrincipalClass diff --git a/se/help/changelog.yaml b/se/help/changelog.yaml index f03450c8..3f4b1845 100644 --- a/se/help/changelog.yaml +++ b/se/help/changelog.yaml @@ -1,3 +1,9 @@ +- date: 2009-10-14 + version: 2.8.2 + description: | + * Improved directory selection in the Directories panel (Windows). (#56) + * Fixed a bug preventing dupeGuru from starting on certain machines (Windows). (#68) + * Fixed a crash during very big scans. (#70) - date: 2009-10-02 version: 2.8.1 description: | From ab6cb5e3d77c0cabace48ad345e7686aa5c48afe Mon Sep 17 00:00:00 2001 From: hsoft Date: Wed, 14 Oct 2009 15:38:19 +0000 Subject: [PATCH 186/275] dgse qt v2.8.2 --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40187 --- se/qt/app.py | 2 +- se/qt/build.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/se/qt/app.py b/se/qt/app.py index 29edcf05..cf245b38 100644 --- a/se/qt/app.py +++ b/se/qt/app.py @@ -27,7 +27,7 @@ class Directories(DirectoriesBase): class DupeGuru(DupeGuruBase): LOGO_NAME = 'logo_se' NAME = 'dupeGuru' - VERSION = '2.8.1' + VERSION = '2.8.2' DELTA_COLUMNS = frozenset([2, 4, 5]) def __init__(self): diff --git a/se/qt/build.py b/se/qt/build.py index 63d73fea..f62cc936 100644 --- a/se/qt/build.py +++ b/se/qt/build.py @@ -36,7 +36,7 @@ fp.close() print_and_do("python C:\\Python26\\pyinstaller\\Build.py dgse.spec") os.remove('verinfo_tmp') -print_and_do("del *90.dll") # They're in vcredist, no need to include them +print_and_do("del dist\\*90.dll") # They're in vcredist, no need to include them print_and_do("xcopy /Y /S /I help\\dupeguru_help dist\\help") aicom = '"\\Program Files\\Caphyon\\Advanced Installer\\AdvancedInstaller.com"' From 9300e030903c0b59247eb478268f2de9f49b6122 Mon Sep 17 00:00:00 2001 From: hsoft Date: Wed, 14 Oct 2009 15:48:11 +0000 Subject: [PATCH 187/275] dgme cocoa 5.6.6 --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40188 --- me/cocoa/Info.plist | 2 +- me/help/changelog.yaml | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/me/cocoa/Info.plist b/me/cocoa/Info.plist index 0557a93e..e96f2561 100644 --- a/me/cocoa/Info.plist +++ b/me/cocoa/Info.plist @@ -23,7 +23,7 @@ CFBundleSignature hsft CFBundleVersion - 5.6.5 + 5.6.6 NSMainNibFile MainMenu NSPrincipalClass diff --git a/me/help/changelog.yaml b/me/help/changelog.yaml index b81ca5ff..20ec0064 100644 --- a/me/help/changelog.yaml +++ b/me/help/changelog.yaml @@ -1,3 +1,9 @@ +- date: 2009-10-14 + version: 5.6.6 + description: | + * Improved directory selection in the Directories panel (Windows). (#56) + * Fixed a bug preventing dupeGuru from starting on certain machines (Windows). (#68) + * Fixed a crash during very big scans. (#70) - date: 2009-10-04 version: 5.6.5 description: | From 15609922768835277e729149c86020fa5a6e1367 Mon Sep 17 00:00:00 2001 From: hsoft Date: Wed, 14 Oct 2009 15:52:31 +0000 Subject: [PATCH 188/275] dgpe cocoa 1.7.7 --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40189 --- pe/cocoa/Info.plist | 2 +- pe/help/changelog.yaml | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/pe/cocoa/Info.plist b/pe/cocoa/Info.plist index 6960d416..ffd62ad8 100644 --- a/pe/cocoa/Info.plist +++ b/pe/cocoa/Info.plist @@ -23,7 +23,7 @@ CFBundleSignature hsft CFBundleVersion - 1.7.6 + 1.7.7 NSMainNibFile MainMenu NSPrincipalClass diff --git a/pe/help/changelog.yaml b/pe/help/changelog.yaml index 3d0f3898..21347b44 100644 --- a/pe/help/changelog.yaml +++ b/pe/help/changelog.yaml @@ -1,3 +1,9 @@ +- date: 2009-10-14 + version: 1.7.7 + description: | + * Improved directory selection in the Directories panel (Windows). (#56) + * Fixed a bug preventing dupeGuru from starting on certain machines (Windows). (#68) + * Fixed a crash during very big scans. (#70) - date: 2009-10-03 version: 1.7.6 description: | From 2c339751b95cdd8dc679fca116736314eb082c5d Mon Sep 17 00:00:00 2001 From: hsoft Date: Wed, 14 Oct 2009 15:58:51 +0000 Subject: [PATCH 189/275] dgme qt v5.6.6 --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40190 --- me/qt/app.py | 2 +- me/qt/build.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/me/qt/app.py b/me/qt/app.py index 414f434d..87359304 100644 --- a/me/qt/app.py +++ b/me/qt/app.py @@ -19,7 +19,7 @@ from preferences_dialog import PreferencesDialog class DupeGuru(DupeGuruBase): LOGO_NAME = 'logo_me' NAME = 'dupeGuru Music Edition' - VERSION = '5.6.5' + VERSION = '5.6.6' DELTA_COLUMNS = frozenset([2, 3, 4, 5, 7, 8]) def __init__(self): diff --git a/me/qt/build.py b/me/qt/build.py index 91560469..64c7fbdc 100644 --- a/me/qt/build.py +++ b/me/qt/build.py @@ -34,7 +34,7 @@ fp.close() print_and_do("python C:\\Python26\\pyinstaller\\Build.py dgme.spec") os.remove('verinfo_tmp') -print_and_do("del *90.dll") # They're in vcredist, no need to include them +print_and_do("del dist\\*90.dll") # They're in vcredist, no need to include them print_and_do("xcopy /Y /S /I help\\dupeguru_me_help dist\\help") aicom = '"\\Program Files\\Caphyon\\Advanced Installer\\AdvancedInstaller.com"' From 3f34dab881c44fd9819a51de79019137023591cc Mon Sep 17 00:00:00 2001 From: hsoft Date: Wed, 14 Oct 2009 16:01:24 +0000 Subject: [PATCH 190/275] dgpe qt v1.7.7 --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40191 --- pe/qt/app.py | 2 +- pe/qt/build.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pe/qt/app.py b/pe/qt/app.py index 358add29..dced3764 100644 --- a/pe/qt/app.py +++ b/pe/qt/app.py @@ -56,7 +56,7 @@ class Directory(phys.Directory): class DupeGuru(DupeGuruBase): LOGO_NAME = 'logo_pe' NAME = 'dupeGuru Picture Edition' - VERSION = '1.7.6' + VERSION = '1.7.7' DELTA_COLUMNS = frozenset([2, 5, 6]) def __init__(self): diff --git a/pe/qt/build.py b/pe/qt/build.py index 20dc9023..63fb23cd 100644 --- a/pe/qt/build.py +++ b/pe/qt/build.py @@ -36,7 +36,7 @@ fp.close() print_and_do("python C:\\Python26\\pyinstaller\\Build.py dgpe.spec") os.remove('verinfo_tmp') -print_and_do("del *90.dll") # They're in vcredist, no need to include them +print_and_do("del dist\\*90.dll") # They're in vcredist, no need to include them print_and_do("xcopy /Y /S /I help\\dupeguru_pe_help dist\\help") aicom = '"\\Program Files\\Caphyon\\Advanced Installer\\AdvancedInstaller.com"' From 7228adf433e2b365a8a1474cd267031681dc35b8 Mon Sep 17 00:00:00 2001 From: hsoft Date: Sun, 18 Oct 2009 08:46:00 +0000 Subject: [PATCH 191/275] Changed the MatchFactory into a simple getmatch method, and added a separate getmatches_by_contents() method for contents scan, which results in faster and less memory hungry scans. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40192 --- base/py/data.py | 2 +- base/py/data_me.py | 2 +- base/py/engine.py | 114 ++++++++++++++++++-------------- base/py/scanner.py | 67 +++++++------------ base/py/tests/app_cocoa_test.py | 2 +- base/py/tests/engine_test.py | 94 +++++++++----------------- base/py/tests/results_test.py | 4 +- base/py/tests/scanner_test.py | 17 ----- 8 files changed, 123 insertions(+), 179 deletions(-) diff --git a/base/py/data.py b/base/py/data.py index 3b1d1017..2f81084e 100644 --- a/base/py/data.py +++ b/base/py/data.py @@ -79,7 +79,7 @@ def GetDisplayInfo(dupe, group, delta): format_timestamp(ctime, delta and m), format_timestamp(mtime, delta and m), format_perc(percentage), - format_words(dupe.words), + format_words(dupe.words) if hasattr(dupe, 'words') else '', format_dupe_count(dupe_count) ] diff --git a/base/py/data_me.py b/base/py/data_me.py index 41ce0f85..4fc74069 100644 --- a/base/py/data_me.py +++ b/base/py/data_me.py @@ -76,7 +76,7 @@ def GetDisplayInfo(dupe, group, delta): str(dupe.track), dupe.comment, format_perc(percentage), - format_words(dupe.words), + format_words(dupe.words) if hasattr(dupe, 'words') else '', format_dupe_count(dupe_count) ] diff --git a/base/py/engine.py b/base/py/engine.py index 19626f55..b34f2edd 100644 --- a/base/py/engine.py +++ b/base/py/engine.py @@ -9,6 +9,7 @@ from __future__ import division import difflib +import itertools import logging import string from collections import defaultdict, namedtuple @@ -156,58 +157,69 @@ def get_match(first, second, flags=()): percentage = compare(first.words, second.words, flags) return Match(first, second, percentage) -class MatchFactory(object): - common_word_threshold = 50 - match_similar_words = False - min_match_percentage = 0 - weight_words = False - no_field_order = False - limit = 5000000 - - def getmatches(self, objects, j=job.nulljob): - j = j.start_subjob(2) - sj = j.start_subjob(2) - for o in objects: - if not hasattr(o, 'words'): - o.words = getwords(o.name) - word_dict = build_word_dict(objects, sj) - reduce_common_words(word_dict, self.common_word_threshold) - if self.match_similar_words: - merge_similar_words(word_dict) - match_flags = [] - if self.weight_words: - match_flags.append(WEIGHT_WORDS) - if self.match_similar_words: - match_flags.append(MATCH_SIMILAR_WORDS) - if self.no_field_order: - match_flags.append(NO_FIELD_ORDER) - j.start_job(len(word_dict), '0 matches found') - compared = defaultdict(set) - result = [] - try: - # This whole 'popping' thing is there to avoid taking too much memory at the same time. - while word_dict: - items = word_dict.popitem()[1] - while items: - ref = items.pop() - compared_already = compared[ref] - to_compare = items - compared_already - compared_already |= to_compare - for other in to_compare: - m = get_match(ref, other, match_flags) - if m.percentage >= self.min_match_percentage: - result.append(m) - if len(result) >= self.limit: - return result - j.add_progress(desc='%d matches found' % len(result)) - except MemoryError: - # This is the place where the memory usage is at its peak during the scan. - # Just continue the process with an incomplete list of matches. - del compared # This should give us enough room to call logging. - logging.warning('Memory Overflow. Matches: %d. Word dict: %d' % (len(result), len(word_dict))) - return result +def getmatches(objects, min_match_percentage=0, match_similar_words=False, weight_words=False, + no_field_order=False, j=job.nulljob): + COMMON_WORD_THRESHOLD = 50 + LIMIT = 5000000 + j = j.start_subjob(2) + sj = j.start_subjob(2) + for o in objects: + if not hasattr(o, 'words'): + o.words = getwords(o.name) + word_dict = build_word_dict(objects, sj) + reduce_common_words(word_dict, COMMON_WORD_THRESHOLD) + if match_similar_words: + merge_similar_words(word_dict) + match_flags = [] + if weight_words: + match_flags.append(WEIGHT_WORDS) + if match_similar_words: + match_flags.append(MATCH_SIMILAR_WORDS) + if no_field_order: + match_flags.append(NO_FIELD_ORDER) + j.start_job(len(word_dict), '0 matches found') + compared = defaultdict(set) + result = [] + try: + # This whole 'popping' thing is there to avoid taking too much memory at the same time. + while word_dict: + items = word_dict.popitem()[1] + while items: + ref = items.pop() + compared_already = compared[ref] + to_compare = items - compared_already + compared_already |= to_compare + for other in to_compare: + m = get_match(ref, other, match_flags) + if m.percentage >= min_match_percentage: + result.append(m) + if len(result) >= LIMIT: + return result + j.add_progress(desc='%d matches found' % len(result)) + except MemoryError: + # This is the place where the memory usage is at its peak during the scan. + # Just continue the process with an incomplete list of matches. + del compared # This should give us enough room to call logging. + logging.warning('Memory Overflow. Matches: %d. Word dict: %d' % (len(result), len(word_dict))) return result - + return result + +def getmatches_by_contents(files, sizeattr='size', partial=False, j=job.nulljob): + j = j.start_subjob([2, 8]) + size2files = defaultdict(set) + for file in j.iter_with_progress(files, 'Read size of %d/%d files'): + size2files[getattr(file, sizeattr)].add(file) + possible_matches = [files for files in size2files.values() if len(files) > 1] + del size2files + result = [] + j.start_job(len(possible_matches), '0 matches found') + for group in possible_matches: + for first, second in itertools.combinations(group, 2): + if first.md5partial == second.md5partial: + if partial or first.md5 == second.md5: + result.append(Match(first, second, 100)) + j.add_progress(desc='%d matches found' % len(result)) + return result class Group(object): #---Override diff --git a/base/py/scanner.py b/base/py/scanner.py index ff59d523..0ac41d23 100644 --- a/base/py/scanner.py +++ b/base/py/scanner.py @@ -32,40 +32,32 @@ class Scanner(object): self.ignore_list = IgnoreList() self.discarded_file_count = 0 - @staticmethod - def _filter_matches_by_content(matches, partial, j): - matched_files = dedupe([m.first for m in matches] + [m.second for m in matches]) - md5attrname = 'md5partial' if partial else 'md5' - md5 = lambda f: getattr(f, md5attrname) - for matched_file in j.iter_with_progress(matched_files, 'Analyzed %d/%d matching files'): - md5(matched_file) - j.set_progress(100, 'Removing false matches') - return [m for m in matches if md5(m.first) == md5(m.second)] - def _getmatches(self, files, j): - j = j.start_subjob(2) - mf = engine.MatchFactory() - if self.scan_type != SCAN_TYPE_CONTENT: - mf.match_similar_words = self.match_similar_words - mf.weight_words = self.word_weighting - mf.min_match_percentage = self.min_match_percentage - if self.scan_type == SCAN_TYPE_FIELDS_NO_ORDER: - self.scan_type = SCAN_TYPE_FIELDS - mf.no_field_order = True - func = { - SCAN_TYPE_FILENAME: lambda f: engine.getwords(rem_file_ext(f.name)), - SCAN_TYPE_FIELDS: lambda f: engine.getfields(rem_file_ext(f.name)), - SCAN_TYPE_TAG: lambda f: [engine.getwords(unicode(getattr(f, attrname))) for attrname in SCANNABLE_TAGS if attrname in self.scanned_tags], - SCAN_TYPE_CONTENT: lambda f: [str(f.size)], - SCAN_TYPE_CONTENT_AUDIO: lambda f: [str(f.audiosize)] - }[self.scan_type] - for f in j.iter_with_progress(files, 'Read metadata of %d/%d files'): - if self.size_threshold: + if not self.size_threshold: + j = j.start_subjob([2, 8]) + for f in j.iter_with_progress(files, 'Read size of %d/%d files'): f.size # pre-read, makes a smoother progress if read here (especially for bundles) - f.words = func(f) - if self.size_threshold: files = [f for f in files if f.size >= self.size_threshold] - return mf.getmatches(files, j) + if self.scan_type in (SCAN_TYPE_CONTENT, SCAN_TYPE_CONTENT_AUDIO): + sizeattr = 'size' if self.scan_type == SCAN_TYPE_CONTENT else 'audiosize' + return engine.getmatches_by_contents(files, sizeattr, partial=self.scan_type==SCAN_TYPE_CONTENT_AUDIO, j=j) + else: + j = j.start_subjob([2, 8]) + kw = {} + kw['match_similar_words'] = self.match_similar_words + kw['weight_words'] = self.word_weighting + kw['min_match_percentage'] = self.min_match_percentage + if self.scan_type == SCAN_TYPE_FIELDS_NO_ORDER: + self.scan_type = SCAN_TYPE_FIELDS + kw['no_field_order'] = True + func = { + SCAN_TYPE_FILENAME: lambda f: engine.getwords(rem_file_ext(f.name)), + SCAN_TYPE_FIELDS: lambda f: engine.getfields(rem_file_ext(f.name)), + SCAN_TYPE_TAG: lambda f: [engine.getwords(unicode(getattr(f, attrname))) for attrname in SCANNABLE_TAGS if attrname in self.scanned_tags], + }[self.scan_type] + for f in j.iter_with_progress(files, 'Read metadata of %d/%d files'): + f.words = func(f) + return engine.getmatches(files, j=j, **kw) @staticmethod def _key_func(dupe): @@ -86,10 +78,7 @@ class Scanner(object): for f in [f for f in files if not hasattr(f, 'is_ref')]: f.is_ref = False logging.info('Getting matches') - if self.match_factory is None: - matches = self._getmatches(files, j) - else: - matches = self.match_factory.getmatches(files, j) + matches = self._getmatches(files, j) logging.info('Found %d matches' % len(matches)) if not self.mix_file_kind: j.set_progress(100, 'Removing false matches') @@ -99,14 +88,6 @@ class Scanner(object): iter_matches = j.iter_with_progress(matches, 'Processed %d/%d matches against the ignore list') matches = [m for m in iter_matches if not self.ignore_list.AreIgnored(unicode(m.first.path), unicode(m.second.path))] - if self.scan_type in (SCAN_TYPE_CONTENT, SCAN_TYPE_CONTENT_AUDIO): - j = j.start_subjob(3 if self.scan_type == SCAN_TYPE_CONTENT else 2) - matches = self._filter_matches_by_content(matches, partial=True, j=j) - if self.scan_type == SCAN_TYPE_CONTENT: - matches = self._filter_matches_by_content(matches, partial=False, j=j) - # We compared md5. No words were involved. - for m in matches: - m.first.words = m.second.words = ['--'] logging.info('Grouping matches') groups = engine.get_groups(matches, j) matched_files = dedupe([m.first for m in matches] + [m.second for m in matches]) diff --git a/base/py/tests/app_cocoa_test.py b/base/py/tests/app_cocoa_test.py index 4a89b356..4f8ca34e 100644 --- a/base/py/tests/app_cocoa_test.py +++ b/base/py/tests/app_cocoa_test.py @@ -318,7 +318,7 @@ class TCDupeGuru_renameSelected(TestCase): fp = open(str(p + 'foo bar 3'),mode='w') fp.close() refdir = hsfs.phys.Directory(None,str(p)) - matches = engine.MatchFactory().getmatches(refdir.files) + matches = engine.getmatches(refdir.files) groups = engine.get_groups(matches) g = groups[0] g.prioritize(lambda x:x.name) diff --git a/base/py/tests/engine_test.py b/base/py/tests/engine_test.py index 2111618f..1c3366bc 100644 --- a/base/py/tests/engine_test.py +++ b/base/py/tests/engine_test.py @@ -340,21 +340,13 @@ class TCget_match(TestCase): self.assertEqual(int((6.0 / 13.0) * 100),get_match(NamedObject("foo bar",True),NamedObject("bar bleh",True),(WEIGHT_WORDS,)).percentage) -class TCMatchFactory(TestCase): +class GetMatches(TestCase): def test_empty(self): - self.assertEqual([],MatchFactory().getmatches([])) - - def test_defaults(self): - mf = MatchFactory() - self.assertEqual(50,mf.common_word_threshold) - self.assertEqual(False,mf.weight_words) - self.assertEqual(False,mf.match_similar_words) - self.assertEqual(False,mf.no_field_order) - self.assertEqual(0,mf.min_match_percentage) + eq_(getmatches([]), []) def test_simple(self): l = [NamedObject("foo bar"),NamedObject("bar bleh"),NamedObject("a b c foo")] - r = MatchFactory().getmatches(l) + r = getmatches(l) self.assertEqual(2,len(r)) seek = [m for m in r if m.percentage == 50] #"foo bar" and "bar bleh" m = seek[0] @@ -367,7 +359,7 @@ class TCMatchFactory(TestCase): def test_null_and_unrelated_objects(self): l = [NamedObject("foo bar"),NamedObject("bar bleh"),NamedObject(""),NamedObject("unrelated object")] - r = MatchFactory().getmatches(l) + r = getmatches(l) self.assertEqual(1,len(r)) m = r[0] self.assertEqual(50,m.percentage) @@ -376,34 +368,33 @@ class TCMatchFactory(TestCase): def test_twice_the_same_word(self): l = [NamedObject("foo foo bar"),NamedObject("bar bleh")] - r = MatchFactory().getmatches(l) + r = getmatches(l) self.assertEqual(1,len(r)) def test_twice_the_same_word_when_preworded(self): l = [NamedObject("foo foo bar",True),NamedObject("bar bleh",True)] - r = MatchFactory().getmatches(l) + r = getmatches(l) self.assertEqual(1,len(r)) def test_two_words_match(self): l = [NamedObject("foo bar"),NamedObject("foo bar bleh")] - r = MatchFactory().getmatches(l) + r = getmatches(l) self.assertEqual(1,len(r)) def test_match_files_with_only_common_words(self): #If a word occurs more than 50 times, it is excluded from the matching process #The problem with the common_word_threshold is that the files containing only common #words will never be matched together. We *should* match them. - mf = MatchFactory() - mf.common_word_threshold = 50 + # This test assumes that the common word threashold const is 50 l = [NamedObject("foo") for i in range(50)] - r = mf.getmatches(l) + r = getmatches(l) self.assertEqual(1225,len(r)) def test_use_words_already_there_if_there(self): o1 = NamedObject('foo') o2 = NamedObject('bar') o2.words = ['foo'] - self.assertEqual(1,len(MatchFactory().getmatches([o1,o2]))) + eq_(1, len(getmatches([o1,o2]))) def test_job(self): def do_progress(p,d=''): @@ -413,75 +404,62 @@ class TCMatchFactory(TestCase): j = job.Job(1,do_progress) self.log = [] s = "foo bar" - MatchFactory().getmatches([NamedObject(s),NamedObject(s),NamedObject(s)],j) + getmatches([NamedObject(s), NamedObject(s), NamedObject(s)], j=j) self.assert_(len(self.log) > 2) self.assertEqual(0,self.log[0]) self.assertEqual(100,self.log[-1]) def test_weight_words(self): - mf = MatchFactory() - mf.weight_words = True l = [NamedObject("foo bar"),NamedObject("bar bleh")] - m = mf.getmatches(l)[0] + m = getmatches(l, weight_words=True)[0] self.assertEqual(int((6.0 / 13.0) * 100),m.percentage) def test_similar_word(self): - mf = MatchFactory() - mf.match_similar_words = True l = [NamedObject("foobar"),NamedObject("foobars")] - self.assertEqual(1,len(mf.getmatches(l))) - self.assertEqual(100,mf.getmatches(l)[0].percentage) + eq_(len(getmatches(l, match_similar_words=True)), 1) + eq_(getmatches(l, match_similar_words=True)[0].percentage, 100) l = [NamedObject("foobar"),NamedObject("foo")] - self.assertEqual(0,len(mf.getmatches(l))) #too far + eq_(len(getmatches(l, match_similar_words=True)), 0) #too far l = [NamedObject("bizkit"),NamedObject("bizket")] - self.assertEqual(1,len(mf.getmatches(l))) + eq_(len(getmatches(l, match_similar_words=True)), 1) l = [NamedObject("foobar"),NamedObject("foosbar")] - self.assertEqual(1,len(mf.getmatches(l))) + eq_(len(getmatches(l, match_similar_words=True)), 1) def test_single_object_with_similar_words(self): - mf = MatchFactory() - mf.match_similar_words = True l = [NamedObject("foo foos")] - self.assertEqual(0,len(mf.getmatches(l))) + eq_(len(getmatches(l, match_similar_words=True)), 0) def test_double_words_get_counted_only_once(self): - mf = MatchFactory() l = [NamedObject("foo bar foo bleh"),NamedObject("foo bar bleh bar")] - m = mf.getmatches(l)[0] + m = getmatches(l)[0] self.assertEqual(75,m.percentage) def test_with_fields(self): - mf = MatchFactory() o1 = NamedObject("foo bar - foo bleh") o2 = NamedObject("foo bar - bleh bar") o1.words = getfields(o1.name) o2.words = getfields(o2.name) - m = mf.getmatches([o1, o2])[0] + m = getmatches([o1, o2])[0] self.assertEqual(50, m.percentage) def test_with_fields_no_order(self): - mf = MatchFactory() - mf.no_field_order = True o1 = NamedObject("foo bar - foo bleh") o2 = NamedObject("bleh bang - foo bar") o1.words = getfields(o1.name) o2.words = getfields(o2.name) - m = mf.getmatches([o1, o2])[0] - self.assertEqual(50 ,m.percentage) + m = getmatches([o1, o2], no_field_order=True)[0] + eq_(m.percentage, 50) def test_only_match_similar_when_the_option_is_set(self): - mf = MatchFactory() - mf.match_similar_words = False l = [NamedObject("foobar"),NamedObject("foobars")] - self.assertEqual(0,len(mf.getmatches(l))) + eq_(len(getmatches(l, match_similar_words=False)), 0) def test_dont_recurse_do_match(self): # with nosetests, the stack is increased. The number has to be high enough not to be failing falsely sys.setrecursionlimit(100) - mf = MatchFactory() files = [NamedObject('foo bar') for i in range(101)] try: - mf.getmatches(files) + getmatches(files) except RuntimeError: self.fail() finally: @@ -489,18 +467,9 @@ class TCMatchFactory(TestCase): def test_min_match_percentage(self): l = [NamedObject("foo bar"),NamedObject("bar bleh"),NamedObject("a b c foo")] - mf = MatchFactory() - mf.min_match_percentage = 50 - r = mf.getmatches(l) + r = getmatches(l, min_match_percentage=50) self.assertEqual(1,len(r)) #Only "foo bar" / "bar bleh" should match - def test_limit(self): - l = [NamedObject(),NamedObject(),NamedObject()] - mf = MatchFactory() - mf.limit = 2 - r = mf.getmatches(l) - self.assertEqual(2,len(r)) - def test_MemoryError(self): @log_calls def mocked_match(first, second, flags): @@ -510,9 +479,8 @@ class TCMatchFactory(TestCase): objects = [NamedObject() for i in range(10)] # results in 45 matches self.mock(engine, 'get_match', mocked_match) - mf = MatchFactory() try: - r = mf.getmatches(objects) + r = getmatches(objects) except MemoryError: self.fail('MemorryError must be handled') self.assertEqual(42, len(r)) @@ -738,7 +706,7 @@ class TCget_groups(TestCase): def test_simple(self): l = [NamedObject("foo bar"),NamedObject("bar bleh")] - matches = MatchFactory().getmatches(l) + matches = getmatches(l) m = matches[0] r = get_groups(matches) self.assertEqual(1,len(r)) @@ -749,7 +717,7 @@ class TCget_groups(TestCase): def test_group_with_multiple_matches(self): #This results in 3 matches l = [NamedObject("foo"),NamedObject("foo"),NamedObject("foo")] - matches = MatchFactory().getmatches(l) + matches = getmatches(l) r = get_groups(matches) self.assertEqual(1,len(r)) g = r[0] @@ -759,7 +727,7 @@ class TCget_groups(TestCase): l = [NamedObject("a b"),NamedObject("a b"),NamedObject("b c"),NamedObject("c d"),NamedObject("c d")] #There will be 2 groups here: group "a b" and group "c d" #"b c" can go either of them, but not both. - matches = MatchFactory().getmatches(l) + matches = getmatches(l) r = get_groups(matches) self.assertEqual(2,len(r)) self.assertEqual(5,len(r[0])+len(r[1])) @@ -768,7 +736,7 @@ class TCget_groups(TestCase): l = [NamedObject("a b"),NamedObject("a b"),NamedObject("a b"),NamedObject("a b")] #There will be 2 groups here: group "a b" and group "c d" #"b c" can fit in both, but it must be in only one of them - matches = MatchFactory().getmatches(l) + matches = getmatches(l) r = get_groups(matches) self.assertEqual(1,len(r)) @@ -788,7 +756,7 @@ class TCget_groups(TestCase): def test_four_sized_group(self): l = [NamedObject("foobar") for i in xrange(4)] - m = MatchFactory().getmatches(l) + m = getmatches(l) r = get_groups(m) self.assertEqual(1,len(r)) self.assertEqual(4,len(r[0])) diff --git a/base/py/tests/results_test.py b/base/py/tests/results_test.py index b49303a9..ef24a81a 100644 --- a/base/py/tests/results_test.py +++ b/base/py/tests/results_test.py @@ -37,7 +37,7 @@ class NamedObject(engine_test.NamedObject): def GetTestGroups(): objects = [NamedObject("foo bar"),NamedObject("bar bleh"),NamedObject("foo bleh"),NamedObject("ibabtu"),NamedObject("ibabtu")] objects[1].size = 1024 - matches = engine.MatchFactory().getmatches(objects) #we should have 5 matches + matches = engine.getmatches(objects) #we should have 5 matches groups = engine.get_groups(matches) #We should have 2 groups for g in groups: g.prioritize(lambda x:objects.index(x)) #We want the dupes to be in the same order as the list is @@ -505,7 +505,7 @@ class TCResultsXML(TestCase): return objects[1] objects = [NamedObject(u"\xe9foo bar",True),NamedObject("bar bleh",True)] - matches = engine.MatchFactory().getmatches(objects) #we should have 5 matches + matches = engine.getmatches(objects) #we should have 5 matches groups = engine.get_groups(matches) #We should have 2 groups for g in groups: g.prioritize(lambda x:objects.index(x)) #We want the dupes to be in the same order as the list is diff --git a/base/py/tests/scanner_test.py b/base/py/tests/scanner_test.py index 7356d658..39d5eaf4 100644 --- a/base/py/tests/scanner_test.py +++ b/base/py/tests/scanner_test.py @@ -369,23 +369,6 @@ def test_ignore_list_checks_for_unicode(): assert f2 in g assert f3 in g -def test_custom_match_factory(): - class MatchFactory(object): - def getmatches(self, objects, j=None): - return [Match(objects[0], objects[1], 420)] - - - s = Scanner() - s.match_factory = MatchFactory() - o1, o2 = no('foo'), no('bar') - groups = s.GetDupeGroups([o1, o2]) - eq_(len(groups), 1) - g = groups[0] - eq_(len(g), 2) - g.switch_ref(o1) - m = g.get_match_of(o2) - eq_(m, (o1, o2, 420)) - def test_file_evaluates_to_false(): # A very wrong way to use any() was added at some point, causing resulting group list # to be empty. From 11977c6533dd8911b718b3db35b78cf6b3d85ac0 Mon Sep 17 00:00:00 2001 From: hsoft Date: Sun, 18 Oct 2009 09:26:04 +0000 Subject: [PATCH 192/275] dgpe: adjusted to the MatchFactory removal. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40193 --- base/py/scanner.py | 1 - pe/py/app_cocoa.py | 7 +- pe/py/matchbase.py | 166 +++++++++++++++++++------------------------ pe/py/scanner.py | 22 ++++++ pe/qt/app.py | 10 +-- pe/qt/main_window.py | 2 +- 6 files changed, 107 insertions(+), 101 deletions(-) create mode 100644 pe/py/scanner.py diff --git a/base/py/scanner.py b/base/py/scanner.py index 0ac41d23..39f1984a 100644 --- a/base/py/scanner.py +++ b/base/py/scanner.py @@ -99,7 +99,6 @@ class Scanner(object): g.prioritize(self._key_func, self._tie_breaker) return groups - match_factory = None match_similar_words = False min_match_percentage = 80 mix_file_kind = True diff --git a/pe/py/app_cocoa.py b/pe/py/app_cocoa.py index 4880b2e2..fa619838 100644 --- a/pe/py/app_cocoa.py +++ b/pe/py/app_cocoa.py @@ -27,8 +27,9 @@ from hsutil.path import Path from hsutil.cocoa import as_fetch from dupeguru import app_cocoa, directories -from . import data, matchbase +from . import data from .cache import string_to_colors, Cache +from .scanner import ScannerPE mainBundle = NSBundle.mainBundle() PictureBlocks = mainBundle.classNamed_('PictureBlocks') @@ -126,11 +127,11 @@ class IPhotoLibrary(fs.Directory): class DupeGuruPE(app_cocoa.DupeGuru): def __init__(self): app_cocoa.DupeGuru.__init__(self, data, 'dupeGuru Picture Edition', appid=5) - self.scanner.match_factory = matchbase.AsyncMatchFactory() + self.scanner = ScannerPE() self.directories.dirclass = Directory self.directories.special_dirclasses[Path('iPhoto Library')] = lambda _, __: self._create_iphoto_library() p = op.join(self.appdata, 'cached_pictures.db') - self.scanner.match_factory.cached_blocks = Cache(p) + self.scanner.cached_blocks = Cache(p) def _create_iphoto_library(self): ud = NSUserDefaults.standardUserDefaults() diff --git a/pe/py/matchbase.py b/pe/py/matchbase.py index 34966a50..3490620b 100644 --- a/pe/py/matchbase.py +++ b/pe/py/matchbase.py @@ -20,58 +20,42 @@ from .block import avgdiff, DifferentBlockCountError, NoBlocksError from .cache import Cache MIN_ITERATIONS = 3 +BLOCK_COUNT_PER_SIDE = 15 # Enough so that we're sure that the main thread will not wait after a result.get() call # cpucount*2 should be enough to be sure that the spawned process will not wait after the results # collection made by the main process. RESULTS_QUEUE_LIMIT = multiprocessing.cpu_count() * 2 -def get_match(first,second,percentage): +def prepare_pictures(pictures, cached_blocks, j=job.nulljob): + # The MemoryError handlers in there use logging without first caring about whether or not + # there is enough memory left to carry on the operation because it is assumed that the + # MemoryError happens when trying to read an image file, which is freed from memory by the + # time that MemoryError is raised. + prepared = [] # only pictures for which there was no error getting blocks + try: + for picture in j.iter_with_progress(pictures, 'Analyzed %d/%d pictures'): + picture.dimensions + picture.unicode_path = unicode(picture.path) + try: + if picture.unicode_path not in cached_blocks: + blocks = picture.get_blocks(BLOCK_COUNT_PER_SIDE) + cached_blocks[picture.unicode_path] = blocks + prepared.append(picture) + except IOError as e: + logging.warning(unicode(e)) + except MemoryError: + logging.warning(u'Ran out of memory while reading %s of size %d' % (picture.unicode_path, picture.size)) + if picture.size < 10 * 1024 * 1024: # We're really running out of memory + raise + except MemoryError: + logging.warning('Ran out of memory while preparing pictures') + return prepared + +def get_match(first, second, percentage): if percentage < 0: percentage = 0 - return Match(first,second,percentage) - -class MatchFactory(object): - cached_blocks = None - block_count_per_side = 15 - threshold = 75 - match_scaled = False - - def _do_getmatches(self, files, j): - raise NotImplementedError() - - def getmatches(self, files, j=job.nulljob): - # The MemoryError handlers in there use logging without first caring about whether or not - # there is enough memory left to carry on the operation because it is assumed that the - # MemoryError happens when trying to read an image file, which is freed from memory by the - # time that MemoryError is raised. - j = j.start_subjob([3, 7]) - logging.info('Preparing %d files' % len(files)) - prepared = self.prepare_files(files, j) - logging.info('Finished preparing %d files' % len(prepared)) - return self._do_getmatches(prepared, j) - - def prepare_files(self, files, j=job.nulljob): - prepared = [] # only files for which there was no error getting blocks - try: - for picture in j.iter_with_progress(files, 'Analyzed %d/%d pictures'): - picture.dimensions - picture.unicode_path = unicode(picture.path) - try: - if picture.unicode_path not in self.cached_blocks: - blocks = picture.get_blocks(self.block_count_per_side) - self.cached_blocks[picture.unicode_path] = blocks - prepared.append(picture) - except IOError as e: - logging.warning(unicode(e)) - except MemoryError: - logging.warning(u'Ran out of memory while reading %s of size %d' % (picture.unicode_path, picture.size)) - if picture.size < 10 * 1024 * 1024: # We're really running out of memory - raise - except MemoryError: - logging.warning('Ran out of memory while preparing files') - return prepared - + return Match(first, second, percentage) def async_compare(ref_id, other_ids, dbname, threshold): cache = Cache(dbname, threaded=False) @@ -89,53 +73,53 @@ def async_compare(ref_id, other_ids, dbname, threshold): results.append((ref_id, other_id, percentage)) cache.con.close() return results - -class AsyncMatchFactory(MatchFactory): - def _do_getmatches(self, pictures, j): - def empty_out_queue(queue, into): - try: - while True: - into.append(queue.get(block=False)) - except Empty: - pass - - j = j.start_subjob([9, 1], 'Preparing for matching') - cache = self.cached_blocks - id2picture = {} - dimensions2pictures = defaultdict(set) - for picture in pictures: - try: - picture.cache_id = cache.get_id(picture.unicode_path) - id2picture[picture.cache_id] = picture - if not self.match_scaled: - dimensions2pictures[picture.dimensions].add(picture) - except ValueError: - pass - pictures = [p for p in pictures if hasattr(p, 'cache_id')] - pool = multiprocessing.Pool() - async_results = [] - matches = [] - pictures_copy = set(pictures) - for ref in j.iter_with_progress(pictures, 'Matched %d/%d pictures'): - others = pictures_copy if self.match_scaled else dimensions2pictures[ref.dimensions] - others.remove(ref) - if others: - cache_ids = [f.cache_id for f in others] - args = (ref.cache_id, cache_ids, self.cached_blocks.dbname, self.threshold) - async_results.append(pool.apply_async(async_compare, args)) - if len(async_results) > RESULTS_QUEUE_LIMIT: - result = async_results.pop(0) - matches.extend(result.get()) - - result = [] - for ref_id, other_id, percentage in j.iter_with_progress(matches, 'Verified %d/%d matches', every=10): - ref = id2picture[ref_id] - other = id2picture[other_id] - if percentage == 100 and ref.md5 != other.md5: - percentage = 99 - if percentage >= self.threshold: - result.append(get_match(ref, other, percentage)) - return result +def getmatches(pictures, cached_blocks, threshold=75, match_scaled=False, j=job.nulljob): + def empty_out_queue(queue, into): + try: + while True: + into.append(queue.get(block=False)) + except Empty: + pass + + j = j.start_subjob([3, 7]) + pictures = prepare_pictures(pictures, cached_blocks, j) + j = j.start_subjob([9, 1], 'Preparing for matching') + cache = cached_blocks + id2picture = {} + dimensions2pictures = defaultdict(set) + for picture in pictures: + try: + picture.cache_id = cache.get_id(picture.unicode_path) + id2picture[picture.cache_id] = picture + if not match_scaled: + dimensions2pictures[picture.dimensions].add(picture) + except ValueError: + pass + pictures = [p for p in pictures if hasattr(p, 'cache_id')] + pool = multiprocessing.Pool() + async_results = [] + matches = [] + pictures_copy = set(pictures) + for ref in j.iter_with_progress(pictures, 'Matched %d/%d pictures'): + others = pictures_copy if match_scaled else dimensions2pictures[ref.dimensions] + others.remove(ref) + if others: + cache_ids = [f.cache_id for f in others] + args = (ref.cache_id, cache_ids, cached_blocks.dbname, threshold) + async_results.append(pool.apply_async(async_compare, args)) + if len(async_results) > RESULTS_QUEUE_LIMIT: + result = async_results.pop(0) + matches.extend(result.get()) + + result = [] + for ref_id, other_id, percentage in j.iter_with_progress(matches, 'Verified %d/%d matches', every=10): + ref = id2picture[ref_id] + other = id2picture[other_id] + if percentage == 100 and ref.md5 != other.md5: + percentage = 99 + if percentage >= threshold: + result.append(get_match(ref, other, percentage)) + return result multiprocessing.freeze_support() \ No newline at end of file diff --git a/pe/py/scanner.py b/pe/py/scanner.py new file mode 100644 index 00000000..b25f0011 --- /dev/null +++ b/pe/py/scanner.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Created By: Virgil Dupras +# Created On: 2009-10-18 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license + +from dupeguru.scanner import Scanner + +from . import matchbase + +class ScannerPE(Scanner): + cached_blocks = None + match_scaled = False + threshold = 75 + + def _getmatches(self, files, j): + return matchbase.getmatches(files, self.cached_blocks, self.threshold, self.match_scaled, j) + diff --git a/pe/qt/app.py b/pe/qt/app.py index dced3764..cc6296c7 100644 --- a/pe/qt/app.py +++ b/pe/qt/app.py @@ -17,7 +17,7 @@ from hsutil.str import get_file_ext from dupeguru_pe import data as data_pe from dupeguru_pe.cache import Cache -from dupeguru_pe.matchbase import AsyncMatchFactory +from dupeguru_pe.scanner import ScannerPE from block import getblocks from base.app import DupeGuru as DupeGuruBase @@ -63,15 +63,15 @@ class DupeGuru(DupeGuruBase): DupeGuruBase.__init__(self, data_pe, appid=5) def _setup(self): - self.scanner.match_factory = AsyncMatchFactory() + self.scanner = ScannerPE() self.directories.dirclass = Directory - self.scanner.match_factory.cached_blocks = Cache(op.join(self.appdata, 'cached_pictures.db')) + self.scanner.cached_blocks = Cache(op.join(self.appdata, 'cached_pictures.db')) DupeGuruBase._setup(self) def _update_options(self): DupeGuruBase._update_options(self) - self.scanner.match_factory.match_scaled = self.prefs.match_scaled - self.scanner.match_factory.threshold = self.prefs.filter_hardness + self.scanner.match_scaled = self.prefs.match_scaled + self.scanner.threshold = self.prefs.filter_hardness def _create_details_dialog(self, parent): return DetailsDialog(parent, self) diff --git a/pe/qt/main_window.py b/pe/qt/main_window.py index e0ab90b1..f3d7d990 100644 --- a/pe/qt/main_window.py +++ b/pe/qt/main_window.py @@ -23,6 +23,6 @@ class MainWindow(MainWindowBase): title = "Clear Picture Cache" msg = "Do you really want to remove all your cached picture analysis?" if self._confirm(title, msg, QMessageBox.No): - self.app.scanner.match_factory.cached_blocks.clear() + self.app.scanner.cached_blocks.clear() QMessageBox.information(self, title, "Picture cache cleared.") \ No newline at end of file From 371cdda9110a4e86d6bf8aeb353f48f883376539 Mon Sep 17 00:00:00 2001 From: hsoft Date: Sun, 18 Oct 2009 09:29:33 +0000 Subject: [PATCH 193/275] dgpe cocoa: adjusted to MatchFactory removal. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40194 --- pe/cocoa/py/dg_cocoa.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pe/cocoa/py/dg_cocoa.py b/pe/cocoa/py/dg_cocoa.py index 150998b1..296178ce 100644 --- a/pe/cocoa/py/dg_cocoa.py +++ b/pe/cocoa/py/dg_cocoa.py @@ -39,7 +39,7 @@ class PyDupeGuru(PyApp): self.app.scanner.ignore_list.Clear() def clearPictureCache(self): - self.app.scanner.match_factory.cached_blocks.clear() + self.app.scanner.cached_blocks.clear() def doScan(self): return self.app.start_scanning() @@ -172,10 +172,10 @@ class PyDupeGuru(PyApp): #---Properties def setMatchScaled_(self,match_scaled): - self.app.scanner.match_factory.match_scaled = match_scaled + self.app.scanner.match_scaled = match_scaled def setMinMatchPercentage_(self,percentage): - self.app.scanner.match_factory.threshold = int(percentage) + self.app.scanner.threshold = int(percentage) def setMixFileKind_(self,mix_file_kind): self.app.scanner.mix_file_kind = mix_file_kind From b167a51243c181871368e6b69784a30ba19befa6 Mon Sep 17 00:00:00 2001 From: hsoft Date: Thu, 22 Oct 2009 15:23:32 +0000 Subject: [PATCH 194/275] Added dupeguru.fs, which is a simpler fork of hsfs and aims to replace it in the dupeguru project. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40195 --- base/py/fs.py | 190 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 base/py/fs.py diff --git a/base/py/fs.py b/base/py/fs.py new file mode 100644 index 00000000..38652f07 --- /dev/null +++ b/base/py/fs.py @@ -0,0 +1,190 @@ +# -*- coding: utf-8 -*- +# Created By: Virgil Dupras +# Created On: 2009-10-22 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license + +# This is a fork from hsfs. The reason for this fork is that hsfs has been designed for musicGuru +# and was re-used for dupeGuru. The problem is that hsfs is way over-engineered for dupeGuru, +# resulting needless complexity and memory usage. It's been a while since I wanted to do that fork, +# and I'm doing it now. + +from __future__ import unicode_literals + +import hashlib +import logging + +from hsutil import io +from hsutil.misc import nonone +from hsutil.str import get_file_ext + +class FSError(Exception): + cls_message = "An error has occured on '{name}' in '{parent}'" + def __init__(self, fsobject, parent=None): + message = self.cls_message + if isinstance(fsobject, basestring): + name = fsobject + elif isinstance(fsobject, File): + name = fsobject.name + else: + name = '' + parentname = unicode(parent) if parent is not None else '' + Exception.__init__(self, message.format(name=name, parent=parentname)) + + +class AlreadyExistsError(FSError): + "The directory or file name we're trying to add already exists" + cls_message = "'{name}' already exists in '{parent}'" + +class InvalidPath(FSError): + "The path of self is invalid, and cannot be worked with." + cls_message = "'{name}' is invalid." + +class InvalidDestinationError(FSError): + """A copy/move operation has been called, but the destination is invalid.""" + cls_message = "'{name}' is an invalid destination for this operation." + +class OperationError(FSError): + """A copy/move/delete operation has been called, but the checkup after the + operation shows that it didn't work.""" + cls_message = "Operation on '{name}' failed." + +class File(object): + INITIAL_INFO = { + 'size': 0, + 'ctime': 0, + 'mtime': 0, + 'md5': '', + 'md5partial': '', + } + + def __init__(self, path): + self.path = path + #This offset is where we should start reading the file to get a partial md5 + #For audio file, it should be where audio data starts + self._md5partial_offset = 0x4000 #16Kb + self._md5partial_size = 0x4000 #16Kb + + def __getattr__(self, attrname): + # Only called when attr is not there + if attrname in self.INITIAL_INFO: + try: + self._read_info(attrname) + except Exception as e: + logging.warning("An error '%s' was raised while decoding '%s'", e, repr(self.path)) + try: + return self.__dict__[attrname] + except KeyError: + return self.INITIAL_INFO[attrname] + raise AttributeError() + + def _read_info(self, field): + if field in ('size', 'ctime', 'mtime'): + stats = io.stat(self.path) + self.size = nonone(stats.st_size, 0) + self.ctime = nonone(stats.st_ctime, 0) + self.mtime = nonone(stats.st_mtime, 0) + elif field == 'md5partial': + try: + fp = io.open(self.path, 'rb') + offset = self._md5partial_offset + size = self._md5partial_size + fp.seek(offset) + partialdata = fp.read(size) + md5 = hashlib.md5(partialdata) + self.md5partial = md5.digest() + fp.close() + except Exception: + pass + elif field == 'md5': + try: + fp = io.open(self.path, 'rb') + filedata = fp.read() + md5 = hashlib.md5(filedata) + self.md5 = md5.digest() + fp.close() + except Exception: + pass + + def _invalidate_info(self): + for attrname in self.INITIAL_INFO: + if attrname in self.__dict__: + delattr(self, attrname) + + def _read_all_info(self, attrnames=None): + """Cache all possible info. + + If `attrnames` is not None, caches only attrnames. + """ + if attrnames is None: + attrnames = self.INITIAL_INFO.keys() + for attrname in attrnames: + if attrname not in self.__dict__: + self._read_info(attrname) + + #--- Public + @classmethod + def can_handle(cls, path): + return io.isfile(path) + + def copy(self, destpath, newname=None, force=False): + if newname is None: + newname = self.name + destpath = destpath + newname + if (not force) and (io.exists(destpath)): + raise AlreadyExistsError(self, destpath[:-1]) + try: + io.copy(self.path, destpath) + except EnvironmentError: + raise OperationError(self) + if not io.exists(destpath): + raise OperationError(self) + + def move(self, destpath, newname=None, force=False): + if newname is None: + newname = self.name + destpath = destpath + newname + if io.exists(destpath): + if force: + io.remove(destpath) + else: + raise AlreadyExistsError(self, destpath[:-1]) + try: + io.move(self.path, destpath) + except EnvironmentError: + raise OperationError(self) + if not io.exists(destpath): + raise OperationError(self) + self.path = destpath + + def rename(self, newname): + newpath = self.path[:-1] + newname + if io.exists(newpath): + raise AlreadyExistsError(newname, self.path[:-1]) + try: + io.rename(self.path, newpath) + except OSError: + raise OperationError(self) + self.path = newpath + + #--- Properties + @property + def extension(self): + return get_file_ext(self.name) + + @property + def name(self): + return self.path[-1] + + +def get_files(path, fileclass=File): + assert issubclass(fileclass, File) + try: + paths = [path + name for name in io.listdir(path)] + return [fileclass(path) for path in paths if not io.islink(path) and io.isfile(path)] + except EnvironmentError: + raise InvalidPath(path) From f9abc3b35df04f43fa3a5e6f0a267d86a120f3d2 Mon Sep 17 00:00:00 2001 From: hsoft Date: Fri, 23 Oct 2009 08:02:43 +0000 Subject: [PATCH 195/275] Added a dupeguru_se sub-package. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40196 From 0aff7f16e5020f21d6ac135f1c186541eee88b68 Mon Sep 17 00:00:00 2001 From: hsoft Date: Fri, 23 Oct 2009 08:17:35 +0000 Subject: [PATCH 196/275] dg qt: Added the dupeguru_se external. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40197 --- se/qt/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/se/qt/app.py b/se/qt/app.py index cf245b38..de3240f5 100644 --- a/se/qt/app.py +++ b/se/qt/app.py @@ -7,7 +7,7 @@ # which should be included with this package. The terms are also available at # http://www.hardcoded.net/licenses/hs_license -from dupeguru import data +from dupeguru_se import data from dupeguru.directories import Directories as DirectoriesBase, STATE_EXCLUDED from base.app import DupeGuru as DupeGuruBase From 54ac0fd19ef2f7624b1316d6a740365c0475e21a Mon Sep 17 00:00:00 2001 From: hsoft Date: Fri, 23 Oct 2009 08:19:02 +0000 Subject: [PATCH 197/275] dg qt: oops, *now* I added the external ref. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40198 From 49165125e47ae010e98c01ec698d936eb3f81575 Mon Sep 17 00:00:00 2001 From: hsoft Date: Fri, 23 Oct 2009 08:19:48 +0000 Subject: [PATCH 198/275] dg se: Moved se-specific code from dupeguru to dupeguru_se. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40199 --- base/py/data.py | 60 ---------------- base/qt/app.py | 1 + se/py/LICENSE | 11 +++ se/py/__init__.py | 1 + base/py/app_se_cocoa.py => se/py/app_cocoa.py | 7 +- se/py/data.py | 72 +++++++++++++++++++ 6 files changed, 89 insertions(+), 63 deletions(-) create mode 100644 se/py/LICENSE create mode 100644 se/py/__init__.py rename base/py/app_se_cocoa.py => se/py/app_cocoa.py (93%) create mode 100644 se/py/data.py diff --git a/base/py/data.py b/base/py/data.py index 2f81084e..46589c62 100644 --- a/base/py/data.py +++ b/base/py/data.py @@ -40,63 +40,3 @@ def format_dupe_count(c): def cmp_value(value): return value.lower() if isinstance(value, basestring) else value - -COLUMNS = [ - {'attr':'name','display':'Filename'}, - {'attr':'path','display':'Directory'}, - {'attr':'size','display':'Size (KB)'}, - {'attr':'extension','display':'Kind'}, - {'attr':'ctime','display':'Creation'}, - {'attr':'mtime','display':'Modification'}, - {'attr':'percentage','display':'Match %'}, - {'attr':'words','display':'Words Used'}, - {'attr':'dupe_count','display':'Dupe Count'}, -] - -METADATA_TO_READ = ['size', 'ctime', 'mtime'] - -def GetDisplayInfo(dupe, group, delta): - size = dupe.size - ctime = dupe.ctime - mtime = dupe.mtime - m = group.get_match_of(dupe) - if m: - percentage = m.percentage - dupe_count = 0 - if delta: - r = group.ref - size -= r.size - ctime -= r.ctime - mtime -= r.mtime - else: - percentage = group.percentage - dupe_count = len(group.dupes) - return [ - dupe.name, - format_path(dupe.path), - format_size(size, 0, 1, False), - dupe.extension, - format_timestamp(ctime, delta and m), - format_timestamp(mtime, delta and m), - format_perc(percentage), - format_words(dupe.words) if hasattr(dupe, 'words') else '', - format_dupe_count(dupe_count) - ] - -def GetDupeSortKey(dupe, get_group, key, delta): - if key == 6: - m = get_group().get_match_of(dupe) - return m.percentage - if key == 8: - return 0 - r = cmp_value(getattr(dupe, COLUMNS[key]['attr'])) - if delta and (key in (2, 4, 5)): - r -= cmp_value(getattr(get_group().ref, COLUMNS[key]['attr'])) - return r - -def GetGroupSortKey(group, key): - if key == 6: - return group.percentage - if key == 8: - return len(group) - return cmp_value(getattr(group.ref, COLUMNS[key]['attr'])) diff --git a/base/qt/app.py b/base/qt/app.py index 12171c52..f8c2063f 100644 --- a/base/qt/app.py +++ b/base/qt/app.py @@ -145,6 +145,7 @@ class DupeGuru(DupeGuruBase, QObject): def ask_for_reg_code(self): if self.reg.ask_for_code(): + #XXX bug??? self._setup_ui_as_registered() @demo_method diff --git a/se/py/LICENSE b/se/py/LICENSE new file mode 100644 index 00000000..f8818048 --- /dev/null +++ b/se/py/LICENSE @@ -0,0 +1,11 @@ +Copyright 2009 Hardcoded Software Inc. (http://www.hardcoded.net) +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + * Neither the name of Hardcoded Software Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + * If the source code has been published less than two years ago, any redistribution, in whole or in part, must retain full licensing functionality, without any attempt to change, obscure or in other ways circumvent its intent. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/se/py/__init__.py b/se/py/__init__.py new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/se/py/__init__.py @@ -0,0 +1 @@ + diff --git a/base/py/app_se_cocoa.py b/se/py/app_cocoa.py similarity index 93% rename from base/py/app_se_cocoa.py rename to se/py/app_cocoa.py index c431ab05..4ba82ac2 100644 --- a/base/py/app_se_cocoa.py +++ b/se/py/app_cocoa.py @@ -17,8 +17,9 @@ from hsutil.path import Path from hsutil.misc import extract from hsutil.str import get_file_ext -from . import app_cocoa, data -from .directories import Directories as DirectoriesBase, STATE_EXCLUDED +from dupeguru.app_cocoa import DupeGuru as DupeGuruBase +from dupeguru.directories import Directories as DirectoriesBase, STATE_EXCLUDED +from . import data if NSWorkspace.sharedWorkspace().respondsToSelector_('typeOfFile:error:'): # Only from 10.5 def is_bundle(str_path): @@ -63,7 +64,7 @@ class Directories(DirectoriesBase): return STATE_EXCLUDED -class DupeGuru(app_cocoa.DupeGuru): +class DupeGuru(DupeGuruBase): def __init__(self): app_cocoa.DupeGuru.__init__(self, data, 'dupeGuru', appid=4) self.directories = Directories() diff --git a/se/py/data.py b/se/py/data.py new file mode 100644 index 00000000..dc353319 --- /dev/null +++ b/se/py/data.py @@ -0,0 +1,72 @@ +# Created By: Virgil Dupras +# Created On: 2006/03/15 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license + +from hsutil.str import format_size +from dupeguru.data import (format_path, format_timestamp, format_words, format_perc, + format_dupe_count, cmp_value) + +COLUMNS = [ + {'attr':'name','display':'Filename'}, + {'attr':'path','display':'Directory'}, + {'attr':'size','display':'Size (KB)'}, + {'attr':'extension','display':'Kind'}, + {'attr':'ctime','display':'Creation'}, + {'attr':'mtime','display':'Modification'}, + {'attr':'percentage','display':'Match %'}, + {'attr':'words','display':'Words Used'}, + {'attr':'dupe_count','display':'Dupe Count'}, +] + +METADATA_TO_READ = ['size', 'ctime', 'mtime'] + +def GetDisplayInfo(dupe, group, delta): + size = dupe.size + ctime = dupe.ctime + mtime = dupe.mtime + m = group.get_match_of(dupe) + if m: + percentage = m.percentage + dupe_count = 0 + if delta: + r = group.ref + size -= r.size + ctime -= r.ctime + mtime -= r.mtime + else: + percentage = group.percentage + dupe_count = len(group.dupes) + return [ + dupe.name, + format_path(dupe.path), + format_size(size, 0, 1, False), + dupe.extension, + format_timestamp(ctime, delta and m), + format_timestamp(mtime, delta and m), + format_perc(percentage), + format_words(dupe.words) if hasattr(dupe, 'words') else '', + format_dupe_count(dupe_count) + ] + +def GetDupeSortKey(dupe, get_group, key, delta): + if key == 6: + m = get_group().get_match_of(dupe) + return m.percentage + if key == 8: + return 0 + r = cmp_value(getattr(dupe, COLUMNS[key]['attr'])) + if delta and (key in (2, 4, 5)): + r -= cmp_value(getattr(get_group().ref, COLUMNS[key]['attr'])) + return r + +def GetGroupSortKey(group, key): + if key == 6: + return group.percentage + if key == 8: + return len(group) + return cmp_value(getattr(group.ref, COLUMNS[key]['attr'])) From b2b316b642958ddf6f4349e896b635b9bb1b6717 Mon Sep 17 00:00:00 2001 From: hsoft Date: Fri, 23 Oct 2009 12:56:52 +0000 Subject: [PATCH 199/275] dgse qt: removed all hsfs usages. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40200 --- base/py/app.py | 15 ++--- base/py/app_cocoa.py | 42 +++++++------- base/py/directories.py | 81 ++++++++++++++------------- base/py/fs.py | 63 +++++++++------------ base/py/scanner.py | 2 +- base/py/tests/app_cocoa_test.py | 57 +++++++++---------- base/py/tests/app_test.py | 28 ++++++---- base/py/tests/data.py | 45 +++++++++++++++ base/py/tests/directories_test.py | 91 +++++++++++++------------------ base/py/tests/results_test.py | 4 +- base/py/tests/scanner_test.py | 2 - base/qt/app.py | 2 +- base/qt/directories_model.py | 19 +++++-- se/py/app_cocoa.py | 25 +++------ se/py/fs.py | 43 +++++++++++++++ se/py/tests/__init__.py | 0 se/py/tests/fs_test.py | 48 ++++++++++++++++ 17 files changed, 334 insertions(+), 233 deletions(-) create mode 100644 base/py/tests/data.py create mode 100644 se/py/fs.py create mode 100644 se/py/tests/__init__.py create mode 100644 se/py/tests/fs_test.py diff --git a/base/py/app.py b/base/py/app.py index 0748ac2c..f21cb4e4 100644 --- a/base/py/app.py +++ b/base/py/app.py @@ -14,13 +14,13 @@ import os import os.path as op import logging -from hsutil import job, io, files +from hsutil import io, files from hsutil.path import Path from hsutil.reg import RegistrableApplication, RegistrationRequired from hsutil.misc import flatten, first from hsutil.str import escape -from . import directories, results, scanner, export +from . import directories, results, scanner, export, fs JOB_SCAN = 'job_scan' JOB_LOAD = 'job_load' @@ -98,13 +98,8 @@ class DupeGuru(RegistrableApplication): return ['---'] * len(self.data.COLUMNS) def _get_file(self, str_path): - p = Path(str_path) - for d in self.directories: - if p not in d.path: - continue - result = d.find_path(p[d.path:]) - if result is not None: - return result + path = Path(str_path) + return fs.get_file(path, self.directories.fileclasses) @staticmethod def _recycle_dupe(dupe): @@ -150,7 +145,7 @@ class DupeGuru(RegistrableApplication): 2 = absolute re-creation. """ source_path = dupe.path - location_path = dupe.root.path + location_path = first(p for p in self.directories if dupe.path in p) dest_path = Path(destination) if dest_type == 2: dest_path = dest_path + source_path[1:-1] #Remove drive letter and filename diff --git a/base/py/app_cocoa.py b/base/py/app_cocoa.py index de9522db..20ad41e8 100644 --- a/base/py/app_cocoa.py +++ b/base/py/app_cocoa.py @@ -12,13 +12,12 @@ from AppKit import * import logging import os.path as op -import hsfs as fs from hsutil import io, cocoa, job from hsutil.cocoa import install_exception_hook from hsutil.misc import stripnone from hsutil.reg import RegistrationRequired -import app, data +from . import app, fs JOBID2TITLE = { app.JOB_SCAN: "Scanning for duplicates", @@ -43,8 +42,6 @@ class DupeGuru(app.DupeGuru): logging.basicConfig(level=LOGGING_LEVEL, format='%(levelname)s %(message)s') logging.debug('started in debug mode') install_exception_hook() - if data_module is None: - data_module = data appsupport = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, True)[0] appdata = op.join(appsupport, appdata_subdir) app.DupeGuru.__init__(self, data_module, appdata, appid) @@ -91,15 +88,15 @@ class DupeGuru(app.DupeGuru): except IndexError: return (None,None) - def GetDirectory(self,node_path,curr_dir=None): + def get_folder_path(self, node_path, curr_path=None): if not node_path: - return curr_dir - if curr_dir is not None: - l = curr_dir.dirs + return curr_path + current_index = node_path[0] + if curr_path is None: + curr_path = self.directories[current_index] else: - l = self.directories - d = l[node_path[0]] - return self.GetDirectory(node_path[1:],d) + curr_path = self.directories.get_subfolders(curr_path)[current_index] + return self.get_folder_path(node_path[1:], curr_path) def RefreshDetailsTable(self,dupe,group): l1 = self._get_display_info(dupe, group, False) @@ -146,13 +143,13 @@ class DupeGuru(app.DupeGuru): def RemoveSelected(self): self.results.remove_duplicates(self.selected_dupes) - def RenameSelected(self,newname): + def RenameSelected(self, newname): try: d = self.selected_dupes[0] - d = d.move(d.parent,newname) + d.rename(newname) return True - except (IndexError,fs.FSError),e: - logging.warning("dupeGuru Warning: %s" % str(e)) + except (IndexError, fs.FSError) as e: + logging.warning("dupeGuru Warning: %s" % unicode(e)) return False def RevealSelected(self): @@ -214,9 +211,9 @@ class DupeGuru(app.DupeGuru): self.results.dupes[row] for row in rows if row in xrange(len(self.results.dupes)) ] - def SetDirectoryState(self,node_path,state): - d = self.GetDirectory(node_path) - self.directories.set_state(d.path,state) + def SetDirectoryState(self, node_path, state): + p = self.get_folder_path(node_path) + self.directories.set_state(p, state) def sort_dupes(self,key,asc): self.results.sort_dupes(key,asc,self.display_delta_values) @@ -245,8 +242,9 @@ class DupeGuru(app.DupeGuru): return [len(g.dupes) for g in self.results.groups] elif tag == 1: #Directories try: - dirs = self.GetDirectory(node_path).dirs if node_path else self.directories - return [d.dircount for d in dirs] + path = self.get_folder_path(node_path) + subfolders = self.directories.get_subfolders(path) + return [len(self.directories.get_subfolders(path)) for path in subfolders] except IndexError: # node_path out of range return [] else: #Power Marker @@ -270,8 +268,8 @@ class DupeGuru(app.DupeGuru): return result elif tag == 1: #Directories try: - d = self.GetDirectory(node_path) - return [d.name, self.directories.get_state(d.path)] + path = self.get_folder_path(node_path) + return [path[-1], self.directories.get_state(path)] except IndexError: # node_path out of range return [] diff --git a/base/py/directories.py b/base/py/directories.py index 4ff98c55..9a47b1ac 100644 --- a/base/py/directories.py +++ b/base/py/directories.py @@ -9,11 +9,12 @@ import xml.dom.minidom -from hsfs import phys -import hsfs as fs +from hsutil import io from hsutil.files import FileOrPath from hsutil.path import Path +from . import fs + (STATE_NORMAL, STATE_REFERENCE, STATE_EXCLUDED) = range(3) @@ -26,15 +27,14 @@ class InvalidPathError(Exception): class Directories(object): #---Override - def __init__(self): + def __init__(self, fileclasses=[fs.File]): self._dirs = [] self.states = {} - self.dirclass = phys.Directory - self.special_dirclasses = {} + self.fileclasses = fileclasses - def __contains__(self,path): - for d in self._dirs: - if path in d.path: + def __contains__(self, path): + for p in self._dirs: + if path in p: return True return False @@ -53,8 +53,7 @@ class Directories(object): if path[-1].startswith('.'): # hidden return STATE_EXCLUDED - def _get_files(self, from_dir): - from_path = from_dir.path + def _get_files(self, from_path): state = self.get_state(from_path) if state == STATE_EXCLUDED: # Recursively get files from folders with lots of subfolder is expensive. However, there @@ -62,14 +61,17 @@ class Directories(object): # through self.states and see if we must continue, or we can stop right here to save time if not any(p[:len(from_path)] == from_path for p in self.states): return - result = [] - for subdir in from_dir.dirs: - for file in self._get_files(subdir): - yield file - if state != STATE_EXCLUDED: - for file in from_dir.files: - file.is_ref = state == STATE_REFERENCE - yield file + try: + subdir_paths = [from_path + name for name in io.listdir(from_path) if io.isdir(from_path + name)] + for subdir_path in subdir_paths: + for file in self._get_files(subdir_path): + yield file + if state != STATE_EXCLUDED: + for file in fs.get_files(from_path, fileclasses=self.fileclasses): + file.is_ref = state == STATE_REFERENCE + yield file + except (EnvironmentError, fs.InvalidPath): + pass #---Public def add_path(self, path): @@ -80,29 +82,30 @@ class Directories(object): under it will be removed. Can also raise InvalidPathError if 'path' does not exist. """ if path in self: - raise AlreadyThereError - self._dirs = [d for d in self._dirs if d.path not in path] - try: - dirclass = self.special_dirclasses.get(path, self.dirclass) - d = dirclass(None, unicode(path)) - d[:] #If an InvalidPath exception has to be raised, it will be raised here - self._dirs.append(d) - return d - except fs.InvalidPath: + raise AlreadyThereError() + if not io.exists(path): raise InvalidPathError() + self._dirs = [p for p in self._dirs if p not in path] + self._dirs.append(path) + + @staticmethod + def get_subfolders(path): + """returns a sorted list of paths corresponding to subfolders in `path`""" + try: + names = [name for name in io.listdir(path) if io.isdir(path + name)] + names.sort(key=lambda x:x.lower()) + return [path + name for name in names] + except EnvironmentError: + return [] def get_files(self): """Returns a list of all files that are not excluded. Returned files also have their 'is_ref' attr set. """ - for d in self._dirs: - d.force_update() - try: - for file in self._get_files(d): - yield file - except fs.InvalidPath: - pass + for path in self._dirs: + for file in self._get_files(path): + yield file def get_state(self, path): """Returns the state of 'path' (One of the STATE_* const.) @@ -123,8 +126,8 @@ class Directories(object): doc = xml.dom.minidom.parse(infile) except: return - root_dir_nodes = doc.getElementsByTagName('root_directory') - for rdn in root_dir_nodes: + root_path_nodes = doc.getElementsByTagName('root_directory') + for rdn in root_path_nodes: if not rdn.getAttributeNode('path'): continue path = rdn.getAttributeNode('path').nodeValue @@ -144,9 +147,9 @@ class Directories(object): with FileOrPath(outfile, 'wb') as fp: doc = xml.dom.minidom.Document() root = doc.appendChild(doc.createElement('directories')) - for root_dir in self: - root_dir_node = root.appendChild(doc.createElement('root_directory')) - root_dir_node.setAttribute('path', unicode(root_dir.path).encode('utf-8')) + for root_path in self: + root_path_node = root.appendChild(doc.createElement('root_directory')) + root_path_node.setAttribute('path', unicode(root_path).encode('utf-8')) for path, state in self.states.iteritems(): state_node = root.appendChild(doc.createElement('state')) state_node.setAttribute('path', unicode(path).encode('utf-8')) diff --git a/base/py/fs.py b/base/py/fs.py index 38652f07..e962c38d 100644 --- a/base/py/fs.py +++ b/base/py/fs.py @@ -19,7 +19,7 @@ import hashlib import logging from hsutil import io -from hsutil.misc import nonone +from hsutil.misc import nonone, flatten from hsutil.str import get_file_ext class FSError(Exception): @@ -129,48 +129,22 @@ class File(object): #--- Public @classmethod def can_handle(cls, path): - return io.isfile(path) + return not io.islink(path) and io.isfile(path) - def copy(self, destpath, newname=None, force=False): - if newname is None: - newname = self.name - destpath = destpath + newname - if (not force) and (io.exists(destpath)): - raise AlreadyExistsError(self, destpath[:-1]) - try: - io.copy(self.path, destpath) - except EnvironmentError: - raise OperationError(self) - if not io.exists(destpath): - raise OperationError(self) - - def move(self, destpath, newname=None, force=False): - if newname is None: - newname = self.name - destpath = destpath + newname + def rename(self, newname): + if newname == self.name: + return + destpath = self.path[:-1] + newname if io.exists(destpath): - if force: - io.remove(destpath) - else: - raise AlreadyExistsError(self, destpath[:-1]) + raise AlreadyExistsError(newname, self.path[:-1]) try: - io.move(self.path, destpath) + io.rename(self.path, destpath) except EnvironmentError: raise OperationError(self) if not io.exists(destpath): raise OperationError(self) self.path = destpath - def rename(self, newname): - newpath = self.path[:-1] + newname - if io.exists(newpath): - raise AlreadyExistsError(newname, self.path[:-1]) - try: - io.rename(self.path, newpath) - except OSError: - raise OperationError(self) - self.path = newpath - #--- Properties @property def extension(self): @@ -181,10 +155,25 @@ class File(object): return self.path[-1] -def get_files(path, fileclass=File): - assert issubclass(fileclass, File) +def get_file(path, fileclasses=[File]): + for fileclass in fileclasses: + if fileclass.can_handle(path): + return fileclass(path) + +def get_files(path, fileclasses=[File]): + assert all(issubclass(fileclass, File) for fileclass in fileclasses) try: paths = [path + name for name in io.listdir(path)] - return [fileclass(path) for path in paths if not io.islink(path) and io.isfile(path)] + result = [] + for path in paths: + file = get_file(path, fileclasses=fileclasses) + if file is not None: + result.append(file) + return result except EnvironmentError: raise InvalidPath(path) + +def get_all_files(path, fileclasses=[File]): + subfolders = [path + name for name in io.listdir(path) if not io.islink(path + name) and io.isdir(path + name)] + subfiles = flatten(get_all_files(subpath, fileclasses=fileclasses) for subpath in subfolders) + return subfiles + get_files(path, fileclasses=fileclasses) diff --git a/base/py/scanner.py b/base/py/scanner.py index 39f1984a..1a6b3389 100644 --- a/base/py/scanner.py +++ b/base/py/scanner.py @@ -33,7 +33,7 @@ class Scanner(object): self.discarded_file_count = 0 def _getmatches(self, files, j): - if not self.size_threshold: + if self.size_threshold: j = j.start_subjob([2, 8]) for f in j.iter_with_progress(files, 'Read size of %d/%d files'): f.size # pre-read, makes a smoother progress if read here (especially for bundles) diff --git a/base/py/tests/app_cocoa_test.py b/base/py/tests/app_cocoa_test.py index 4f8ca34e..e501aa84 100644 --- a/base/py/tests/app_cocoa_test.py +++ b/base/py/tests/app_cocoa_test.py @@ -18,10 +18,10 @@ from hsutil.path import Path from hsutil.testcase import TestCase from hsutil.decorators import log_calls from hsutil import io -import hsfs.phys +from . import data from .results_test import GetTestGroups -from .. import engine, data +from .. import engine, fs try: from ..app_cocoa import DupeGuru as DupeGuruBase except ImportError: @@ -35,7 +35,6 @@ class DupeGuru(DupeGuruBase): def _start_job(self, jobid, func): func(nulljob) - def r2np(rows): #Transforms a list of rows [1,2,3] into a list of node paths [[1],[2],[3]] return [[i] for i in rows] @@ -310,15 +309,15 @@ class TCDupeGuru(TestCase): class TCDupeGuru_renameSelected(TestCase): def setUp(self): - p = Path(tempfile.mkdtemp()) - fp = open(str(p + 'foo bar 1'),mode='w') + p = self.tmppath() + fp = open(unicode(p + 'foo bar 1'),mode='w') fp.close() - fp = open(str(p + 'foo bar 2'),mode='w') + fp = open(unicode(p + 'foo bar 2'),mode='w') fp.close() - fp = open(str(p + 'foo bar 3'),mode='w') + fp = open(unicode(p + 'foo bar 3'),mode='w') fp.close() - refdir = hsfs.phys.Directory(None,str(p)) - matches = engine.getmatches(refdir.files) + files = fs.get_files(p) + matches = engine.getmatches(files) groups = engine.get_groups(matches) g = groups[0] g.prioritize(lambda x:x.name) @@ -327,45 +326,41 @@ class TCDupeGuru_renameSelected(TestCase): self.app = app self.groups = groups self.p = p - self.refdir = refdir - - def tearDown(self): - shutil.rmtree(str(self.p)) + self.files = files def test_simple(self): app = self.app - refdir = self.refdir g = self.groups[0] app.SelectPowerMarkerNodePaths(r2np([0])) - self.assert_(app.RenameSelected('renamed')) - self.assert_('renamed' in refdir) - self.assert_('foo bar 2' not in refdir) - self.assert_(g.dupes[0] is refdir['renamed']) - self.assert_(g.dupes[0] in refdir) + assert app.RenameSelected('renamed') + names = io.listdir(self.p) + assert 'renamed' in names + assert 'foo bar 2' not in names + eq_(g.dupes[0].name, 'renamed') def test_none_selected(self): app = self.app - refdir = self.refdir g = self.groups[0] app.SelectPowerMarkerNodePaths([]) self.mock(logging, 'warning', log_calls(lambda msg: None)) - self.assert_(not app.RenameSelected('renamed')) + assert not app.RenameSelected('renamed') msg = logging.warning.calls[0]['msg'] - self.assertEqual('dupeGuru Warning: list index out of range', msg) - self.assert_('renamed' not in refdir) - self.assert_('foo bar 2' in refdir) - self.assert_(g.dupes[0] is refdir['foo bar 2']) + eq_('dupeGuru Warning: list index out of range', msg) + names = io.listdir(self.p) + assert 'renamed' not in names + assert 'foo bar 2' in names + eq_(g.dupes[0].name, 'foo bar 2') def test_name_already_exists(self): app = self.app - refdir = self.refdir g = self.groups[0] app.SelectPowerMarkerNodePaths(r2np([0])) self.mock(logging, 'warning', log_calls(lambda msg: None)) - self.assert_(not app.RenameSelected('foo bar 1')) + assert not app.RenameSelected('foo bar 1') msg = logging.warning.calls[0]['msg'] - self.assert_(msg.startswith('dupeGuru Warning: \'foo bar 2\' already exists in')) - self.assert_('foo bar 1' in refdir) - self.assert_('foo bar 2' in refdir) - self.assert_(g.dupes[0] is refdir['foo bar 2']) + assert msg.startswith('dupeGuru Warning: \'foo bar 1\' already exists in') + names = io.listdir(self.p) + assert 'foo bar 1' in names + assert 'foo bar 2' in names + eq_(g.dupes[0].name, 'foo bar 2') diff --git a/base/py/tests/app_test.py b/base/py/tests/app_test.py index c3127bdd..1a468e0d 100644 --- a/base/py/tests/app_test.py +++ b/base/py/tests/app_test.py @@ -13,12 +13,11 @@ from hsutil.testcase import TestCase from hsutil import io from hsutil.path import Path from hsutil.decorators import log_calls -import hsfs as fs -import hsfs.phys import hsutil.files from hsutil.job import nulljob -from .. import data, app +from . import data +from .. import app, fs from ..app import DupeGuru as DupeGuruBase class DupeGuru(DupeGuruBase): @@ -59,27 +58,27 @@ class TCDupeGuru(TestCase): # The goal here is just to have a test for a previous blowup I had. I know my test coverage # for this unit is pathetic. What's done is done. My approach now is to add tests for # every change I want to make. The blowup was caused by a missing import. - dupe_parent = fs.Directory(None, 'foo') - dupe = fs.File(dupe_parent, 'bar') - dupe.copy = log_calls(lambda dest, newname: None) + p = self.tmppath() + io.open(p + 'foo', 'w').close() self.mock(hsutil.files, 'copy', log_calls(lambda source_path, dest_path: None)) self.mock(os, 'makedirs', lambda path: None) # We don't want the test to create that fake directory - self.mock(fs.phys, 'Directory', fs.Directory) # We don't want an error because makedirs didn't work app = DupeGuru() - app.copy_or_move(dupe, True, 'some_destination', 0) + app.directories.add_path(p) + [f] = app.directories.get_files() + app.copy_or_move(f, True, 'some_destination', 0) self.assertEqual(1, len(hsutil.files.copy.calls)) call = hsutil.files.copy.calls[0] self.assertEqual('some_destination', call['dest_path']) - self.assertEqual(dupe.path, call['source_path']) + self.assertEqual(f.path, call['source_path']) def test_copy_or_move_clean_empty_dirs(self): tmppath = Path(self.tmpdir()) sourcepath = tmppath + 'source' io.mkdir(sourcepath) io.open(sourcepath + 'myfile', 'w') - tmpdir = hsfs.phys.Directory(None, unicode(tmppath)) - myfile = tmpdir['source']['myfile'] app = DupeGuru() + app.directories.add_path(tmppath) + [myfile] = app.directories.get_files() self.mock(app, 'clean_empty_dirs', log_calls(lambda path: None)) app.copy_or_move(myfile, False, tmppath + 'dest', 0) calls = app.clean_empty_dirs.calls @@ -87,9 +86,14 @@ class TCDupeGuru(TestCase): self.assertEqual(sourcepath, calls[0]['path']) def test_Scan_with_objects_evaluating_to_false(self): + class FakeFile(fs.File): + def __nonzero__(self): + return False + + # At some point, any() was used in a wrong way that made Scan() wrongly return 1 app = DupeGuru() - f1, f2 = [fs.File(None, 'foo') for i in range(2)] + f1, f2 = [FakeFile('foo') for i in range(2)] f1.is_ref, f2.is_ref = (False, False) assert not (bool(f1) and bool(f2)) app.directories.get_files = lambda: [f1, f2] diff --git a/base/py/tests/data.py b/base/py/tests/data.py new file mode 100644 index 00000000..d71582c5 --- /dev/null +++ b/base/py/tests/data.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +# Created By: Virgil Dupras +# Created On: 2009-10-23 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license + +# data module for tests + +from hsutil.str import format_size +from dupeguru.data import format_path, cmp_value + +COLUMNS = [ + {'attr':'name','display':'Filename'}, + {'attr':'path','display':'Directory'}, + {'attr':'size','display':'Size (KB)'}, + {'attr':'extension','display':'Kind'}, +] + +METADATA_TO_READ = ['size'] + +def GetDisplayInfo(dupe, group, delta): + size = dupe.size + m = group.get_match_of(dupe) + if m and delta: + r = group.ref + size -= r.size + return [ + dupe.name, + format_path(dupe.path), + format_size(size, 0, 1, False), + dupe.extension, + ] + +def GetDupeSortKey(dupe, get_group, key, delta): + r = cmp_value(getattr(dupe, COLUMNS[key]['attr'])) + if delta and (key == 2): + r -= cmp_value(getattr(get_group().ref, COLUMNS[key]['attr'])) + return r + +def GetGroupSortKey(group, key): + return cmp_value(getattr(group.ref, COLUMNS[key]['attr'])) \ No newline at end of file diff --git a/base/py/tests/directories_test.py b/base/py/tests/directories_test.py index 80c6b817..4a550f7a 100644 --- a/base/py/tests/directories_test.py +++ b/base/py/tests/directories_test.py @@ -10,20 +10,43 @@ import os.path as op import os import time -import shutil from nose.tools import eq_ -from hsutil import job, io +from hsutil import io from hsutil.path import Path from hsutil.testcase import TestCase -import hsfs.phys -from hsfs.tests import phys_test from ..directories import * testpath = Path(TestCase.datadirpath()) +def create_fake_fs(rootpath): + rootpath = rootpath + 'fs' + io.mkdir(rootpath) + io.mkdir(rootpath + 'dir1') + io.mkdir(rootpath + 'dir2') + io.mkdir(rootpath + 'dir3') + fp = io.open(rootpath + 'file1.test', 'w') + fp.write('1') + fp.close() + fp = io.open(rootpath + 'file2.test', 'w') + fp.write('12') + fp.close() + fp = io.open(rootpath + 'file3.test', 'w') + fp.write('123') + fp.close() + fp = io.open(rootpath + ('dir1', 'file1.test'), 'w') + fp.write('1') + fp.close() + fp = io.open(rootpath + ('dir2', 'file2.test'), 'w') + fp.write('12') + fp.close() + fp = io.open(rootpath + ('dir3', 'file3.test'), 'w') + fp.write('123') + fp.close() + return rootpath + class TCDirectories(TestCase): def test_empty(self): d = Directories() @@ -33,13 +56,11 @@ class TCDirectories(TestCase): def test_add_path(self): d = Directories() p = testpath + 'utils' - added = d.add_path(p) + d.add_path(p) self.assertEqual(1,len(d)) self.assert_(p in d) self.assert_((p + 'foobar') in d) self.assert_(p[:-1] not in d) - self.assertEqual(p,added.path) - self.assert_(d[0] is added) p = self.tmppath() d.add_path(p) self.assertEqual(2,len(d)) @@ -53,13 +74,13 @@ class TCDirectories(TestCase): self.assertRaises(AlreadyThereError, d.add_path, p + 'foobar') self.assertEqual(1, len(d)) - def test_AddPath_containing_paths_already_there(self): + def test_add_path_containing_paths_already_there(self): d = Directories() d.add_path(testpath + 'utils') self.assertEqual(1, len(d)) - added = d.add_path(testpath) - self.assertEqual(1, len(d)) - self.assert_(added is d[0]) + d.add_path(testpath) + eq_(len(d), 1) + eq_(d[0], testpath) def test_AddPath_non_latin(self): p = Path(self.tmpdir()) @@ -114,7 +135,7 @@ class TCDirectories(TestCase): def test_set_state_keep_state_dict_size_to_minimum(self): d = Directories() - p = Path(phys_test.create_fake_fs(self.tmpdir())) + p = create_fake_fs(self.tmppath()) d.add_path(p) d.set_state(p,STATE_REFERENCE) d.set_state(p + 'dir1',STATE_REFERENCE) @@ -129,7 +150,7 @@ class TCDirectories(TestCase): def test_get_files(self): d = Directories() - p = Path(phys_test.create_fake_fs(self.tmpdir())) + p = create_fake_fs(self.tmppath()) d.add_path(p) d.set_state(p + 'dir1',STATE_REFERENCE) d.set_state(p + 'dir2',STATE_EXCLUDED) @@ -177,52 +198,28 @@ class TCDirectories(TestCase): except LookupError: self.fail() - def test_default_dirclass(self): - self.assert_(Directories().dirclass is hsfs.phys.Directory) - - def test_dirclass(self): - class MySpecialDirclass(hsfs.phys.Directory): pass - d = Directories() - d.dirclass = MySpecialDirclass - d.add_path(testpath) - self.assert_(isinstance(d[0], MySpecialDirclass)) - def test_load_from_file_with_invalid_path(self): #This test simulates a load from file resulting in a #InvalidPath raise. Other directories must be loaded. d1 = Directories() d1.add_path(testpath + 'utils') #Will raise InvalidPath upon loading - d1.add_path(self.tmppath()).name = 'does_not_exist' + p = self.tmppath() + d1.add_path(p) + io.rmdir(p) tmpxml = op.join(self.tmpdir(), 'directories_testunit.xml') d1.save_to_file(tmpxml) d2 = Directories() d2.load_from_file(tmpxml) self.assertEqual(1, len(d2)) - def test_load_from_file_with_same_paths(self): - #This test simulates a load from file resulting in a - #AlreadyExists raise. Other directories must be loaded. - d1 = Directories() - p1 = self.tmppath() - p2 = self.tmppath() - d1.add_path(p1) - d1.add_path(p2) - #Will raise AlreadyExists upon loading - d1.add_path(self.tmppath()).name = unicode(p1) - tmpxml = op.join(self.tmpdir(), 'directories_testunit.xml') - d1.save_to_file(tmpxml) - d2 = Directories() - d2.load_from_file(tmpxml) - self.assertEqual(2, len(d2)) - def test_unicode_save(self): d = Directories() p1 = self.tmppath() + u'hello\xe9' io.mkdir(p1) io.mkdir(p1 + u'foo\xe9') d.add_path(p1) - d.set_state(d[0][0].path, STATE_EXCLUDED) + d.set_state(p1 + u'foo\xe9', STATE_EXCLUDED) tmpxml = op.join(self.tmpdir(), 'directories_testunit.xml') try: d.save_to_file(tmpxml) @@ -231,7 +228,7 @@ class TCDirectories(TestCase): def test_get_files_refreshes_its_directories(self): d = Directories() - p = Path(phys_test.create_fake_fs(self.tmpdir())) + p = create_fake_fs(self.tmppath()) d.add_path(p) files = d.get_files() self.assertEqual(6, len(list(files))) @@ -258,16 +255,6 @@ class TCDirectories(TestCase): d.set_state(hidden_dir_path, STATE_NORMAL) self.assertEqual(d.get_state(hidden_dir_path), STATE_NORMAL) - def test_special_dirclasses(self): - # if a path is in special_dirclasses, use this class instead - class MySpecialDirclass(hsfs.phys.Directory): pass - d = Directories() - p1 = self.tmppath() - p2 = self.tmppath() - d.special_dirclasses[p1] = MySpecialDirclass - self.assert_(isinstance(d.add_path(p2), hsfs.phys.Directory)) - self.assert_(isinstance(d.add_path(p1), MySpecialDirclass)) - def test_default_path_state_override(self): # It's possible for a subclass to override the default state of a path class MyDirectories(Directories): diff --git a/base/py/tests/results_test.py b/base/py/tests/results_test.py index ef24a81a..f3602b7c 100644 --- a/base/py/tests/results_test.py +++ b/base/py/tests/results_test.py @@ -16,8 +16,8 @@ from hsutil.path import Path from hsutil.testcase import TestCase from hsutil.misc import first -from . import engine_test -from .. import data, engine +from . import engine_test, data +from .. import engine from ..results import * class NamedObject(engine_test.NamedObject): diff --git a/base/py/tests/scanner_test.py b/base/py/tests/scanner_test.py index 39d5eaf4..7ae50715 100644 --- a/base/py/tests/scanner_test.py +++ b/base/py/tests/scanner_test.py @@ -132,8 +132,6 @@ def test_content_scan_doesnt_put_md5_in_words_at_the_end(): f[1].md5 = f[1].md5partial = '\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f' r = s.GetDupeGroups(f) g = r[0] - eq_(g.ref.words, ['--']) - eq_(g.dupes[0].words, ['--']) def test_extension_is_not_counted_in_filename_scan(): s = Scanner() diff --git a/base/qt/app.py b/base/qt/app.py index f8c2063f..93a5e293 100644 --- a/base/qt/app.py +++ b/base/qt/app.py @@ -16,10 +16,10 @@ import os.path as op from PyQt4.QtCore import Qt, QTimer, QObject, QCoreApplication, QUrl, SIGNAL from PyQt4.QtGui import QProgressDialog, QDesktopServices, QFileDialog, QDialog, QMessageBox -import hsfs as fs from hsutil import job from hsutil.reg import RegistrationRequired +from dupeguru import fs from dupeguru.app import (DupeGuru as DupeGuruBase, JOB_SCAN, JOB_LOAD, JOB_MOVE, JOB_COPY, JOB_DELETE) diff --git a/base/qt/directories_model.py b/base/qt/directories_model.py index ac77fa0c..e04c913f 100644 --- a/base/qt/directories_model.py +++ b/base/qt/directories_model.py @@ -47,7 +47,14 @@ class DirectoryNode(TreeNode): return DirectoryNode(self.model, self, ref, row) def _getChildren(self): - return self.ref.dirs + return self.model._dirs.get_subfolders(self.ref) + + @property + def name(self): + if self.parent is not None: + return self.ref[-1] + else: + return unicode(self.ref) class DirectoriesModel(TreeModel): @@ -70,13 +77,13 @@ class DirectoriesModel(TreeModel): node = index.internalPointer() if role == Qt.DisplayRole: if index.column() == 0: - return node.ref.name + return node.name else: - return STATES[self._dirs.get_state(node.ref.path)] + return STATES[self._dirs.get_state(node.ref)] elif role == Qt.EditRole and index.column() == 1: - return self._dirs.get_state(node.ref.path) + return self._dirs.get_state(node.ref) elif role == Qt.ForegroundRole: - state = self._dirs.get_state(node.ref.path) + state = self._dirs.get_state(node.ref) if state == 1: return QBrush(Qt.blue) elif state == 2: @@ -101,6 +108,6 @@ class DirectoriesModel(TreeModel): if not index.isValid() or role != Qt.EditRole or index.column() != 1: return False node = index.internalPointer() - self._dirs.set_state(node.ref.path, value) + self._dirs.set_state(node.ref, value) return True diff --git a/se/py/app_cocoa.py b/se/py/app_cocoa.py index 4ba82ac2..cffc8134 100644 --- a/se/py/app_cocoa.py +++ b/se/py/app_cocoa.py @@ -11,12 +11,11 @@ import logging from AppKit import * -from hsfs.phys import Directory as DirectoryBase -from hsfs.phys.bundle import Bundle +from hsutil import io from hsutil.path import Path -from hsutil.misc import extract from hsutil.str import get_file_ext +from dupeguru import fs from dupeguru.app_cocoa import DupeGuru as DupeGuruBase from dupeguru.directories import Directories as DirectoriesBase, STATE_EXCLUDED from . import data @@ -32,27 +31,17 @@ else: # Tiger def is_bundle(str_path): # just return a list of a few known bundle extensions. return get_file_ext(str_path) in ('app', 'pages', 'numbers') -class DGDirectory(DirectoryBase): - def _create_sub_file(self, name, with_parent=True): - if is_bundle(unicode(self.path + name)): - parent = self if with_parent else None - return Bundle(parent, name) - else: - return super(DGDirectory, self)._create_sub_file(name, with_parent) - - def _fetch_subitems(self): - subdirs, subfiles = super(DGDirectory, self)._fetch_subitems() - apps, normal_dirs = extract(lambda name: is_bundle(unicode(self.path + name)), subdirs) - subfiles += apps - return normal_dirs, subfiles +class Bundle(BundleBase): + @classmethod + def can_handle(cls, path): + return not io.islink(path) and io.isdir(path) and is_bundle(unicode(path)) class Directories(DirectoriesBase): ROOT_PATH_TO_EXCLUDE = map(Path, ['/Library', '/Volumes', '/System', '/bin', '/sbin', '/opt', '/private', '/dev']) HOME_PATH_TO_EXCLUDE = [Path('Library')] def __init__(self): - DirectoriesBase.__init__(self) - self.dirclass = DGDirectory + DirectoriesBase.__init__(self, fileclasses=[Bundle, fs.File]) def _default_state_for_path(self, path): result = DirectoriesBase._default_state_for_path(self, path) diff --git a/se/py/fs.py b/se/py/fs.py new file mode 100644 index 00000000..dc7d0025 --- /dev/null +++ b/se/py/fs.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# Created By: Virgil Dupras +# Created On: 2009-10-23 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license + +import hashlib + +from hsutil import io +from hsutil.misc import nonone + +from dupeguru import fs + +class Bundle(fs.File): + """This class is for Mac OSX bundles (.app). Bundles are seen by the OS as + normal directories, but I don't want that in dupeGuru. I want dupeGuru + to see them as files. + """ + def _read_info(self, field): + if field in ('size', 'ctime', 'mtime'): + files = fs.get_all_files(self.path) + size = sum((file.size for file in files), 0) + self.size = size + stats = io.stat(self.path) + self.ctime = nonone(stats.st_ctime, 0) + self.mtime = nonone(stats.st_mtime, 0) + elif field in ('md5', 'md5partial'): + # What's sensitive here is that we must make sure that subfiles' + # md5 are always added up in the same order, but we also want a + # different md5 if a file gets moved in a different subdirectory. + def get_dir_md5_concat(): + files = fs.get_all_files(self.path) + files.sort(key=lambda f:f.path) + md5s = [getattr(f, field) for f in files] + return ''.join(md5s) + + md5 = hashlib.md5(get_dir_md5_concat()) + digest = md5.digest() + setattr(self, field, digest) diff --git a/se/py/tests/__init__.py b/se/py/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/se/py/tests/fs_test.py b/se/py/tests/fs_test.py new file mode 100644 index 00000000..c948ede7 --- /dev/null +++ b/se/py/tests/fs_test.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +# Created By: Virgil Dupras +# Created On: 2009-10-23 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license + +import hashlib + +from nose.tools import eq_ + +from hsutil.testcase import TestCase +from dupeguru.fs import File +from dupeguru.tests.directories_test import create_fake_fs + +from .. import fs + +class TCBundle(TestCase): + def test_size_aggregates_subfiles(self): + p = create_fake_fs(self.tmppath()) + b = fs.Bundle(p) + eq_(b.size, 12) + + def test_md5_aggregate_subfiles_sorted(self): + #dir.allfiles can return child in any order. Thus, bundle.md5 must aggregate + #all files' md5 it contains, but it must make sure that it does so in the + #same order everytime. + p = create_fake_fs(self.tmppath()) + b = fs.Bundle(p) + md5s = File(p + ('dir1', 'file1.test')).md5 + md5s += File(p + ('dir2', 'file2.test')).md5 + md5s += File(p + ('dir3', 'file3.test')).md5 + md5s += File(p + 'file1.test').md5 + md5s += File(p + 'file2.test').md5 + md5s += File(p + 'file3.test').md5 + md5 = hashlib.md5(md5s) + eq_(b.md5, md5.digest()) + + def test_has_file_attrs(self): + #a Bundle must behave like a file, so it must have ctime and mtime attributes + b = fs.Bundle(self.tmppath()) + assert b.mtime > 0 + assert b.ctime > 0 + eq_(b.extension, '') + \ No newline at end of file From 787cbcd01f09f21b49a2c4b56b24d7aab82bc132 Mon Sep 17 00:00:00 2001 From: hsoft Date: Fri, 23 Oct 2009 12:59:29 +0000 Subject: [PATCH 200/275] dgse qt: removed hsfs external --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40201 From cf44c93013407c0ebd79618d3d3dcdcf933e5b23 Mon Sep 17 00:00:00 2001 From: hsoft Date: Fri, 23 Oct 2009 13:45:15 +0000 Subject: [PATCH 201/275] dgse cocoa: added the dupeguru_se external and removed the hsfs one. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40202 From 4d7f03288916352a1e8702b6ac92b3c34b5763bc Mon Sep 17 00:00:00 2001 From: hsoft Date: Fri, 23 Oct 2009 13:46:18 +0000 Subject: [PATCH 202/275] dgse cocoa: fixed quirks created by the hsfs move. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40203 --- base/py/app_cocoa.py | 10 +++++++--- base/py/directories.py | 12 ++++++++---- base/py/fs.py | 8 ++++++-- se/cocoa/py/dg_cocoa.py | 10 +++++----- se/py/app_cocoa.py | 3 ++- 5 files changed, 28 insertions(+), 15 deletions(-) diff --git a/base/py/app_cocoa.py b/base/py/app_cocoa.py index 20ad41e8..780388bb 100644 --- a/base/py/app_cocoa.py +++ b/base/py/app_cocoa.py @@ -242,8 +242,11 @@ class DupeGuru(app.DupeGuru): return [len(g.dupes) for g in self.results.groups] elif tag == 1: #Directories try: - path = self.get_folder_path(node_path) - subfolders = self.directories.get_subfolders(path) + if node_path: + path = self.get_folder_path(node_path) + subfolders = self.directories.get_subfolders(path) + else: + subfolders = self.directories return [len(self.directories.get_subfolders(path)) for path in subfolders] except IndexError: # node_path out of range return [] @@ -269,7 +272,8 @@ class DupeGuru(app.DupeGuru): elif tag == 1: #Directories try: path = self.get_folder_path(node_path) - return [path[-1], self.directories.get_state(path)] + name = unicode(path) if len(node_path) == 1 else path[-1] + return [name, self.directories.get_state(path)] except IndexError: # node_path out of range return [] diff --git a/base/py/directories.py b/base/py/directories.py index 9a47b1ac..d61ff1a7 100644 --- a/base/py/directories.py +++ b/base/py/directories.py @@ -62,13 +62,17 @@ class Directories(object): if not any(p[:len(from_path)] == from_path for p in self.states): return try: - subdir_paths = [from_path + name for name in io.listdir(from_path) if io.isdir(from_path + name)] - for subdir_path in subdir_paths: - for file in self._get_files(subdir_path): - yield file + filepaths = set() if state != STATE_EXCLUDED: for file in fs.get_files(from_path, fileclasses=self.fileclasses): file.is_ref = state == STATE_REFERENCE + filepaths.add(file.path) + yield file + subpaths = [from_path + name for name in io.listdir(from_path)] + # it's possible that a folder (bundle) gets into the file list. in that case, we don't want to recurse into it + subfolders = [p for p in subpaths if not io.islink(p) and io.isdir(p) and p not in filepaths] + for subfolder in subfolders: + for file in self._get_files(subfolder): yield file except (EnvironmentError, fs.InvalidPath): pass diff --git a/base/py/fs.py b/base/py/fs.py index e962c38d..93bc1d4d 100644 --- a/base/py/fs.py +++ b/base/py/fs.py @@ -174,6 +174,10 @@ def get_files(path, fileclasses=[File]): raise InvalidPath(path) def get_all_files(path, fileclasses=[File]): - subfolders = [path + name for name in io.listdir(path) if not io.islink(path + name) and io.isdir(path + name)] + files = get_files(path, fileclasses=fileclasses) + filepaths = set(f.path for f in files) + subpaths = [path + name for name in io.listdir(path)] + # it's possible that a folder (bundle) gets into the file list. in that case, we don't want to recurse into it + subfolders = [p for p in subpaths if not io.islink(p) and io.isdir(p) and p not in filepaths] subfiles = flatten(get_all_files(subpath, fileclasses=fileclasses) for subpath in subfolders) - return subfiles + get_files(path, fileclasses=fileclasses) + return subfiles + files diff --git a/se/cocoa/py/dg_cocoa.py b/se/cocoa/py/dg_cocoa.py index 93eded52..11ce95b6 100644 --- a/se/cocoa/py/dg_cocoa.py +++ b/se/cocoa/py/dg_cocoa.py @@ -8,12 +8,12 @@ import objc from AppKit import * -from dupeguru import app_se_cocoa, scanner +from dupeguru_se.app_cocoa import DupeGuru +from dupeguru import scanner # Fix py2app imports with chokes on relative imports -from dupeguru import app, app_cocoa, data, directories, engine, export, ignore, results, scanner -from hsfs import auto, stats, tree -from hsfs.phys import bundle +from dupeguru_se import fs, data +from dupeguru import app, app_cocoa, data, directories, engine, export, ignore, results, fs from hsutil import conflict class PyApp(NSObject): @@ -22,7 +22,7 @@ class PyApp(NSObject): class PyDupeGuru(PyApp): def init(self): self = super(PyDupeGuru,self).init() - self.app = app_se_cocoa.DupeGuru() + self.app = DupeGuru() return self #---Directories diff --git a/se/py/app_cocoa.py b/se/py/app_cocoa.py index cffc8134..4eb58820 100644 --- a/se/py/app_cocoa.py +++ b/se/py/app_cocoa.py @@ -19,6 +19,7 @@ from dupeguru import fs from dupeguru.app_cocoa import DupeGuru as DupeGuruBase from dupeguru.directories import Directories as DirectoriesBase, STATE_EXCLUDED from . import data +from .fs import Bundle as BundleBase if NSWorkspace.sharedWorkspace().respondsToSelector_('typeOfFile:error:'): # Only from 10.5 def is_bundle(str_path): @@ -55,6 +56,6 @@ class Directories(DirectoriesBase): class DupeGuru(DupeGuruBase): def __init__(self): - app_cocoa.DupeGuru.__init__(self, data, 'dupeGuru', appid=4) + DupeGuruBase.__init__(self, data, 'dupeGuru', appid=4) self.directories = Directories() From 085311d5594e276fa2aeb74304dff5376d123b87 Mon Sep 17 00:00:00 2001 From: hsoft Date: Fri, 23 Oct 2009 14:05:06 +0000 Subject: [PATCH 203/275] Added the folder me/py --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40204 From a281931b16b4e01e6927225d1a3687d4cf4970fd Mon Sep 17 00:00:00 2001 From: hsoft Date: Fri, 23 Oct 2009 14:34:59 +0000 Subject: [PATCH 204/275] dgme qt: added the dupeguru_me external and removed the hsfs one. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40205 From 385768a69bdc7001ff3ea558f2a7448ca50bd143 Mon Sep 17 00:00:00 2001 From: hsoft Date: Fri, 23 Oct 2009 14:35:51 +0000 Subject: [PATCH 205/275] dgme qt: adjusted code to the hsfs move. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40206 --- base/py/fs.py | 5 - base/py/scanner.py | 6 - base/py/tests/scanner_test.py | 12 -- me/py/__init__.py | 0 base/py/app_me_cocoa.py => me/py/app_cocoa.py | 14 +- base/py/data_me.py => me/py/data.py | 2 +- me/py/fs.py | 183 ++++++++++++++++++ me/py/scanner.py | 16 ++ me/py/tests/__init__.py | 0 me/py/tests/scanner_test.py | 33 ++++ me/qt/app.py | 8 +- 11 files changed, 243 insertions(+), 36 deletions(-) create mode 100644 me/py/__init__.py rename base/py/app_me_cocoa.py => me/py/app_cocoa.py (86%) rename base/py/data_me.py => me/py/data.py (97%) create mode 100644 me/py/fs.py create mode 100644 me/py/scanner.py create mode 100644 me/py/tests/__init__.py create mode 100644 me/py/tests/scanner_test.py diff --git a/base/py/fs.py b/base/py/fs.py index 93bc1d4d..1f01349f 100644 --- a/base/py/fs.py +++ b/base/py/fs.py @@ -110,11 +110,6 @@ class File(object): except Exception: pass - def _invalidate_info(self): - for attrname in self.INITIAL_INFO: - if attrname in self.__dict__: - delattr(self, attrname) - def _read_all_info(self, attrnames=None): """Cache all possible info. diff --git a/base/py/scanner.py b/base/py/scanner.py index 1a6b3389..3f999920 100644 --- a/base/py/scanner.py +++ b/base/py/scanner.py @@ -106,9 +106,3 @@ class Scanner(object): scanned_tags = set(['artist', 'title']) size_threshold = 0 word_weighting = False - -class ScannerME(Scanner): # Scanner for Music Edition - @staticmethod - def _key_func(dupe): - return (not dupe.is_ref, -dupe.bitrate, -dupe.size) - diff --git a/base/py/tests/scanner_test.py b/base/py/tests/scanner_test.py index 7ae50715..1ce0f8f7 100644 --- a/base/py/tests/scanner_test.py +++ b/base/py/tests/scanner_test.py @@ -436,15 +436,3 @@ def test_partial_group_match(): assert o2 in group assert o3 not in group eq_(s.discarded_file_count, 1) - - -#--- Scanner ME -def test_priorize_me(): - # in ScannerME, bitrate goes first (right after is_ref) in priorization - s = ScannerME() - o1, o2 = no('foo'), no('foo') - o1.bitrate = 1 - o2.bitrate = 2 - [group] = s.GetDupeGroups([o1, o2]) - assert group.ref is o2 - diff --git a/me/py/__init__.py b/me/py/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/base/py/app_me_cocoa.py b/me/py/app_cocoa.py similarity index 86% rename from base/py/app_me_cocoa.py rename to me/py/app_cocoa.py index d9850c2d..692f847f 100644 --- a/base/py/app_me_cocoa.py +++ b/me/py/app_cocoa.py @@ -7,29 +7,29 @@ # which should be included with this package. The terms are also available at # http://www.hardcoded.net/licenses/hs_license -import os.path as op import logging from appscript import app, k, CommandError import time from hsutil.cocoa import as_fetch -import hsfs.phys.music -import app_cocoa, data_me, scanner +from dupeguru.app_cocoa import JOBID2TITLE, DupeGuru as DupeGuruBase + +from . import data, scanner, fs JOB_REMOVE_DEAD_TRACKS = 'jobRemoveDeadTracks' JOB_SCAN_DEAD_TRACKS = 'jobScanDeadTracks' -app_cocoa.JOBID2TITLE.update({ +JOBID2TITLE.update({ JOB_REMOVE_DEAD_TRACKS: "Removing dead tracks from your iTunes Library", JOB_SCAN_DEAD_TRACKS: "Scanning the iTunes Library", }) -class DupeGuruME(app_cocoa.DupeGuru): +class DupeGuruME(DupeGuruBase): def __init__(self): - app_cocoa.DupeGuru.__init__(self, data_me, 'dupeGuru Music Edition', appid=1) + DupeGuruBase.__init__(self, data, 'dupeGuru Music Edition', appid=1) self.scanner = scanner.ScannerME() - self.directories.dirclass = hsfs.phys.music.Directory + self.directories.fileclasses = [fs.Mp3File, fs.Mp4File, fs.WmaFile, fs.OggFile, fs.FlacFile, fs.AiffFile] self.dead_tracks = [] def remove_dead_tracks(self): diff --git a/base/py/data_me.py b/me/py/data.py similarity index 97% rename from base/py/data_me.py rename to me/py/data.py index 4fc74069..ad9edda5 100644 --- a/base/py/data_me.py +++ b/me/py/data.py @@ -8,7 +8,7 @@ # http://www.hardcoded.net/licenses/hs_license from hsutil.str import format_time, FT_MINUTES, format_size -from .data import (format_path, format_timestamp, format_words, format_perc, +from dupeguru.data import (format_path, format_timestamp, format_words, format_perc, format_dupe_count, cmp_value) COLUMNS = [ diff --git a/me/py/fs.py b/me/py/fs.py new file mode 100644 index 00000000..0a47e709 --- /dev/null +++ b/me/py/fs.py @@ -0,0 +1,183 @@ +# -*- coding: utf-8 -*- +# Created By: Virgil Dupras +# Created On: 2009-10-23 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license + +from hsmedia import mpeg, wma, mp4, ogg, flac, aiff +from hsutil.str import get_file_ext +from dupeguru import fs + +TAG_FIELDS = ['audiosize', 'duration', 'bitrate', 'samplerate', 'title', 'artist', + 'album', 'genre', 'year', 'track', 'comment'] + +class MusicFile(fs.File): + INITIAL_INFO = fs.File.INITIAL_INFO.copy() + INITIAL_INFO.update({ + 'audiosize': 0, + 'bitrate' : 0, + 'duration' : 0, + 'samplerate':0, + 'artist' : '', + 'album' : '', + 'title' : '', + 'genre' : '', + 'comment' : '', + 'year' : '', + 'track' : 0, + }) + HANDLED_EXTS = set() + + @classmethod + def can_handle(cls, path): + if not fs.File.can_handle(path): + return False + return get_file_ext(path[-1]) in cls.HANDLED_EXTS + + +class Mp3File(MusicFile): + HANDLED_EXTS = set(['mp3']) + def _read_info(self, field): + if field == 'md5partial': + fileinfo = mpeg.Mpeg(unicode(self.path)) + self._md5partial_offset = fileinfo.audio_offset + self._md5partial_size = fileinfo.audio_size + MusicFile._read_info(self, field) + if field in TAG_FIELDS: + fileinfo = mpeg.Mpeg(unicode(self.path)) + self.audiosize = fileinfo.audio_size + self.bitrate = fileinfo.bitrate + self.duration = fileinfo.duration + self.samplerate = fileinfo.sample_rate + i1 = fileinfo.id3v1 + # id3v1, even when non-existant, gives empty values. not id3v2. if id3v2 don't exist, + # just replace it with id3v1 + i2 = fileinfo.id3v2 + if not i2.exists: + i2 = i1 + self.artist = i2.artist or i1.artist + self.album = i2.album or i1.album + self.title = i2.title or i1.title + self.genre = i2.genre or i1.genre + self.comment = i2.comment or i1.comment + self.year = i2.year or i1.year + self.track = i2.track or i1.track + +class WmaFile(MusicFile): + HANDLED_EXTS = set(['wma']) + def _read_info(self, field): + if field == 'md5partial': + dec = wma.WMADecoder(unicode(self.path)) + self._md5partial_offset = dec.audio_offset + self._md5partial_size = dec.audio_size + MusicFile._read_info(self, field) + if field in TAG_FIELDS: + dec = wma.WMADecoder(unicode(self.path)) + self.audiosize = dec.audio_size + self.bitrate = dec.bitrate + self.duration = dec.duration + self.samplerate = dec.sample_rate + self.artist = dec.artist + self.album = dec.album + self.title = dec.title + self.genre = dec.genre + self.comment = dec.comment + self.year = dec.year + self.track = dec.track + +class Mp4File(MusicFile): + HANDLED_EXTS = set(['m4a', 'm4p']) + def _read_info(self, field): + if field == 'md5partial': + dec = mp4.File(unicode(self.path)) + self._md5partial_offset = dec.audio_offset + self._md5partial_size = dec.audio_size + dec.close() + MusicFile._read_info(self, field) + if field in TAG_FIELDS: + dec = mp4.File(unicode(self.path)) + self.audiosize = dec.audio_size + self.bitrate = dec.bitrate + self.duration = dec.duration + self.samplerate = dec.sample_rate + self.artist = dec.artist + self.album = dec.album + self.title = dec.title + self.genre = dec.genre + self.comment = dec.comment + self.year = dec.year + self.track = dec.track + dec.close() + +class OggFile(MusicFile): + HANDLED_EXTS = set(['ogg']) + def _read_info(self, field): + if field == 'md5partial': + dec = ogg.Vorbis(unicode(self.path)) + self._md5partial_offset = dec.audio_offset + self._md5partial_size = dec.audio_size + MusicFile._read_info(self, field) + if field in TAG_FIELDS: + dec = ogg.Vorbis(unicode(self.path)) + self.audiosize = dec.audio_size + self.bitrate = dec.bitrate + self.duration = dec.duration + self.samplerate = dec.sample_rate + self.artist = dec.artist + self.album = dec.album + self.title = dec.title + self.genre = dec.genre + self.comment = dec.comment + self.year = dec.year + self.track = dec.track + +class FlacFile(MusicFile): + HANDLED_EXTS = set(['flac']) + def _read_info(self, field): + if field == 'md5partial': + dec = flac.FLAC(unicode(self.path)) + self._md5partial_offset = dec.audio_offset + self._md5partial_size = dec.audio_size + MusicFile._read_info(self, field) + if field in TAG_FIELDS: + dec = flac.FLAC(unicode(self.path)) + self.audiosize = dec.audio_size + self.bitrate = dec.bitrate + self.duration = dec.duration + self.samplerate = dec.sample_rate + self.artist = dec.artist + self.album = dec.album + self.title = dec.title + self.genre = dec.genre + self.comment = dec.comment + self.year = dec.year + self.track = dec.track + +class AiffFile(MusicFile): + HANDLED_EXTS = set(['aif', 'aiff', 'aifc']) + def _read_info(self, field): + if field == 'md5partial': + dec = aiff.File(unicode(self.path)) + self._md5partial_offset = dec.audio_offset + self._md5partial_size = dec.audio_size + MusicFile._read_info(self, field) + if field in TAG_FIELDS: + dec = aiff.File(unicode(self.path)) + self.audiosize = dec.audio_size + self.bitrate = dec.bitrate + self.duration = dec.duration + self.samplerate = dec.sample_rate + tag = dec.tag + if tag is not None: + self.artist = tag.artist + self.album = tag.album + self.title = tag.title + self.genre = tag.genre + self.comment = tag.comment + self.year = tag.year + self.track = tag.track + diff --git a/me/py/scanner.py b/me/py/scanner.py new file mode 100644 index 00000000..7fce8427 --- /dev/null +++ b/me/py/scanner.py @@ -0,0 +1,16 @@ +# Created By: Virgil Dupras +# Created On: 2006/03/03 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license + +from dupeguru.scanner import Scanner as ScannerBase + +class ScannerME(ScannerBase): + @staticmethod + def _key_func(dupe): + return (not dupe.is_ref, -dupe.bitrate, -dupe.size) + diff --git a/me/py/tests/__init__.py b/me/py/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/me/py/tests/scanner_test.py b/me/py/tests/scanner_test.py new file mode 100644 index 00000000..6ab32a6d --- /dev/null +++ b/me/py/tests/scanner_test.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# Created By: Virgil Dupras +# Created On: 2009-10-23 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license + +from hsutil.path import Path + +from dupeguru.engine import getwords +from ..scanner import * + +class NamedObject(object): + def __init__(self, name="foobar", size=1): + self.name = name + self.size = size + self.path = Path('') + self.words = getwords(name) + + +no = NamedObject + +def test_priorize_me(): + # in ScannerME, bitrate goes first (right after is_ref) in priorization + s = ScannerME() + o1, o2 = no('foo'), no('foo') + o1.bitrate = 1 + o2.bitrate = 2 + [group] = s.GetDupeGroups([o1, o2]) + assert group.ref is o2 \ No newline at end of file diff --git a/me/qt/app.py b/me/qt/app.py index 87359304..5234c8fa 100644 --- a/me/qt/app.py +++ b/me/qt/app.py @@ -7,9 +7,7 @@ # which should be included with this package. The terms are also available at # http://www.hardcoded.net/licenses/hs_license -import hsfs.phys.music - -from dupeguru import data_me, scanner +from dupeguru_me import data, scanner, fs from base.app import DupeGuru as DupeGuruBase from details_dialog import DetailsDialog @@ -23,11 +21,11 @@ class DupeGuru(DupeGuruBase): DELTA_COLUMNS = frozenset([2, 3, 4, 5, 7, 8]) def __init__(self): - DupeGuruBase.__init__(self, data_me, appid=1) + DupeGuruBase.__init__(self, data, appid=1) def _setup(self): self.scanner = scanner.ScannerME() - self.directories.dirclass = hsfs.phys.music.Directory + self.directories.fileclasses = [fs.Mp3File, fs.Mp4File, fs.WmaFile, fs.OggFile, fs.FlacFile, fs.AiffFile] DupeGuruBase._setup(self) def _update_options(self): From 794192835da6722e76bd254fdf884f9aa246cd97 Mon Sep 17 00:00:00 2001 From: hsoft Date: Fri, 23 Oct 2009 14:46:00 +0000 Subject: [PATCH 206/275] dgme cocoa: added dupeguru_me external and removed the hsfs one. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40207 --- me/cocoa/py/dg_cocoa.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/me/cocoa/py/dg_cocoa.py b/me/cocoa/py/dg_cocoa.py index 87c723c0..0bb65ba4 100644 --- a/me/cocoa/py/dg_cocoa.py +++ b/me/cocoa/py/dg_cocoa.py @@ -8,12 +8,13 @@ import objc from AppKit import * -from dupeguru import app_me_cocoa, scanner +from dupeguru_me.app_cocoa import DupeGuruME +from dupeguru.scanner import (SCAN_TYPE_FILENAME, SCAN_TYPE_FIELDS, SCAN_TYPE_FIELDS_NO_ORDER, + SCAN_TYPE_TAG, SCAN_TYPE_CONTENT, SCAN_TYPE_CONTENT_AUDIO) # Fix py2app imports which chokes on relative imports -from dupeguru import app, app_cocoa, data, directories, engine, export, ignore, results, scanner -from hsfs import auto, stats, tree, music -from hsfs.phys import music +from dupeguru_me import app_cocoa, data, fs, scanner +from dupeguru import app, app_cocoa, data, directories, engine, export, ignore, results, scanner, fs from hsmedia import aiff, flac, genres, id3v1, id3v2, mp4, mpeg, ogg, wma from hsutil import conflict @@ -23,7 +24,7 @@ class PyApp(NSObject): class PyDupeGuru(PyApp): def init(self): self = super(PyDupeGuru,self).init() - self.app = app_me_cocoa.DupeGuruME() + self.app = DupeGuruME() return self #---Directories @@ -180,12 +181,12 @@ class PyDupeGuru(PyApp): def setScanType_(self, scan_type): try: self.app.scanner.scan_type = [ - scanner.SCAN_TYPE_FILENAME, - scanner.SCAN_TYPE_FIELDS, - scanner.SCAN_TYPE_FIELDS_NO_ORDER, - scanner.SCAN_TYPE_TAG, - scanner.SCAN_TYPE_CONTENT, - scanner.SCAN_TYPE_CONTENT_AUDIO + SCAN_TYPE_FILENAME, + SCAN_TYPE_FIELDS, + SCAN_TYPE_FIELDS_NO_ORDER, + SCAN_TYPE_TAG, + SCAN_TYPE_CONTENT, + SCAN_TYPE_CONTENT_AUDIO ][scan_type] except IndexError: pass From a3ab314378ef2777b4ffc66160adcbf24e1dcf7e Mon Sep 17 00:00:00 2001 From: hsoft Date: Fri, 23 Oct 2009 15:04:37 +0000 Subject: [PATCH 207/275] dgpe qt: adjusted to the hsfs move. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40208 --- pe/qt/app.py | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/pe/qt/app.py b/pe/qt/app.py index cc6296c7..a73914d7 100644 --- a/pe/qt/app.py +++ b/pe/qt/app.py @@ -12,9 +12,9 @@ import os.path as op from PyQt4.QtGui import QImage import PIL.Image -from hsfs import phys from hsutil.str import get_file_ext +from dupeguru import fs from dupeguru_pe import data as data_pe from dupeguru_pe.cache import Cache from dupeguru_pe.scanner import ScannerPE @@ -26,14 +26,19 @@ from main_window import MainWindow from preferences import Preferences from preferences_dialog import PreferencesDialog -class File(phys.File): - INITIAL_INFO = phys.File.INITIAL_INFO.copy() +class File(fs.File): + INITIAL_INFO = fs.File.INITIAL_INFO.copy() INITIAL_INFO.update({ 'dimensions': (0,0), }) + HANDLED_EXTS = set(['png', 'jpg', 'jpeg', 'gif', 'bmp', 'tiff']) + + @classmethod + def can_handle(cls, path): + return fs.File.can_handle(path) and get_file_ext(path[-1]) in cls.HANDLED_EXTS def _read_info(self, field): - super(File, self)._read_info(field) + fs.File._read_info(self, field) if field == 'dimensions': im = PIL.Image.open(unicode(self.path)) self.dimensions = im.size @@ -44,15 +49,6 @@ class File(phys.File): return getblocks(image, block_count_per_side) -class Directory(phys.Directory): - cls_file_class = File - cls_supported_exts = ('png', 'jpg', 'jpeg', 'gif', 'bmp', 'tiff') - - def _fetch_subitems(self): - subdirs, subfiles = super(Directory, self)._fetch_subitems() - return subdirs, [name for name in subfiles if get_file_ext(name) in self.cls_supported_exts] - - class DupeGuru(DupeGuruBase): LOGO_NAME = 'logo_pe' NAME = 'dupeGuru Picture Edition' @@ -64,7 +60,7 @@ class DupeGuru(DupeGuruBase): def _setup(self): self.scanner = ScannerPE() - self.directories.dirclass = Directory + self.directories.fileclasses = [File] self.scanner.cached_blocks = Cache(op.join(self.appdata, 'cached_pictures.db')) DupeGuruBase._setup(self) From b8c11b5aae207d4d0a8daabdaaeaa56c761387b3 Mon Sep 17 00:00:00 2001 From: hsoft Date: Sat, 24 Oct 2009 12:21:09 +0000 Subject: [PATCH 208/275] dgpe cocoa: removed hsfs from externals. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40209 From 25dadc83ebeefa1c6fd82e2936d72e775c191a92 Mon Sep 17 00:00:00 2001 From: hsoft Date: Sat, 24 Oct 2009 12:21:39 +0000 Subject: [PATCH 209/275] sgpe cocoa: adjusted to hsfs removal. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40210 --- pe/cocoa/py/dg_cocoa.py | 1 - pe/py/app_cocoa.py | 172 +++++++++++++++++----------------------- 2 files changed, 71 insertions(+), 102 deletions(-) diff --git a/pe/cocoa/py/dg_cocoa.py b/pe/cocoa/py/dg_cocoa.py index 296178ce..32c5575f 100644 --- a/pe/cocoa/py/dg_cocoa.py +++ b/pe/cocoa/py/dg_cocoa.py @@ -12,7 +12,6 @@ from dupeguru_pe import app_cocoa as app_pe_cocoa # Fix py2app imports which chokes on relative imports from dupeguru import app, app_cocoa, data, directories, engine, export, ignore, results, scanner from dupeguru_pe import block, cache, matchbase, data -from hsfs import auto, stats, tree from hsutil import conflict class PyApp(NSObject): diff --git a/pe/py/app_cocoa.py b/pe/py/app_cocoa.py index fa619838..74e89701 100644 --- a/pe/py/app_cocoa.py +++ b/pe/py/app_cocoa.py @@ -7,25 +7,21 @@ # which should be included with this package. The terms are also available at # http://www.hardcoded.net/licenses/hs_license -import os import os.path as op import logging import plistlib import re -import objc from Foundation import * from AppKit import * from appscript import app, k -from hsutil import job, io -import hsfs as fs -from hsfs import phys, InvalidPath -from hsutil import files +from hsutil import io from hsutil.str import get_file_ext from hsutil.path import Path from hsutil.cocoa import as_fetch +from dupeguru import fs from dupeguru import app_cocoa, directories from . import data from .cache import string_to_colors, Cache @@ -35,14 +31,19 @@ mainBundle = NSBundle.mainBundle() PictureBlocks = mainBundle.classNamed_('PictureBlocks') assert PictureBlocks is not None -class Photo(phys.File): - INITIAL_INFO = phys.File.INITIAL_INFO.copy() +class Photo(fs.File): + INITIAL_INFO = fs.File.INITIAL_INFO.copy() INITIAL_INFO.update({ 'dimensions': (0,0), }) + HANDLED_EXTS = set(['png', 'jpg', 'jpeg', 'gif', 'psd', 'bmp', 'tiff', 'nef', 'cr2']) + + @classmethod + def can_handle(cls, path): + return fs.File.can_handle(path) and get_file_ext(path[-1]) in cls.HANDLED_EXTS def _read_info(self, field): - super(Photo, self)._read_info(field) + fs.File._read_info(self, field) if field == 'dimensions': size = PictureBlocks.getImageSize_(unicode(self.path)) self.dimensions = (size.width, size.height) @@ -50,7 +51,7 @@ class Photo(phys.File): def get_blocks(self, block_count_per_side): try: blocks = PictureBlocks.getBlocksFromImagePath_blockCount_(unicode(self.path), block_count_per_side) - except Exception, e: + except Exception as e: raise IOError('The reading of "%s" failed with "%s"' % (unicode(self.path), unicode(e))) if not blocks: raise IOError('The picture %s could not be read' % unicode(self.path)) @@ -58,90 +59,80 @@ class Photo(phys.File): class IPhoto(Photo): - def __init__(self, parent, whole_path): - super(IPhoto, self).__init__(parent, whole_path[-1]) - self.whole_path = whole_path - - def _build_path(self): - return self.whole_path - @property def display_path(self): - return super(IPhoto, self)._build_path() + return Path(('iPhoto Library', self.name)) +def get_iphoto_database_path(): + ud = NSUserDefaults.standardUserDefaults() + prefs = ud.persistentDomainForName_('com.apple.iApps') + if 'iPhotoRecentDatabases' not in prefs: + raise directories.InvalidPathError() + plisturl = NSURL.URLWithString_(prefs['iPhotoRecentDatabases'][0]) + return Path(plisturl.path()) -class Directory(phys.Directory): - cls_file_class = Photo - cls_supported_exts = ('png', 'jpg', 'jpeg', 'gif', 'psd', 'bmp', 'tiff', 'nef', 'cr2') - - def _fetch_subitems(self): - subdirs, subfiles = super(Directory,self)._fetch_subitems() - return subdirs, [name for name in subfiles if get_file_ext(name) in self.cls_supported_exts] - - -class IPhotoLibrary(fs.Directory): - def __init__(self, plistpath): - self.plistpath = plistpath - self.refpath = plistpath[:-1] - # the AlbumData.xml file lives right in the library path - super(IPhotoLibrary, self).__init__(None, 'iPhoto Library') - if not io.exists(plistpath): - raise InvalidPath(self) - - def _update_photo(self, photo_data): +def get_iphoto_pictures(plistpath): + if not io.exists(plistpath): + raise InvalidPath(self) + s = io.open(plistpath).read() + # There was a case where a guy had 0x10 chars in his plist, causing expat errors on loading + s = s.replace('\x10', '') + # It seems that iPhoto sometimes doesn't properly escape & chars. The regexp below is to find + # any & char that is not a &-based entity (&, ", 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) + plist = plistlib.readPlistFromString(s) + result = [] + for photo_data in plist['Master Image List'].values(): if photo_data['MediaType'] != 'Image': - return + continue photo_path = Path(photo_data['ImagePath']) - subpath = photo_path[len(self.refpath):-1] - subdir = self - for element in subpath: - try: - subdir = subdir[element] - except KeyError: - subdir = fs.Directory(subdir, element) - try: - IPhoto(subdir, photo_path) - except fs.AlreadyExistsError: - # it's possible for 2 entries in the plist to point to the same path. Ignore one of them. - pass + photo = IPhoto(photo_path) + result.append(photo) + return result + +class Directories(directories.Directories): + def __init__(self): + directories.Directories.__init__(self, fileclasses=[Photo]) + self.iphoto_libpath = get_iphoto_database_path() + self.set_state(self.iphoto_libpath[:-1], directories.STATE_EXCLUDED) - def update(self): - self.clear() - s = open(unicode(self.plistpath)).read() - # There was a case where a guy had 0x10 chars in his plist, causing expat errors on loading - s = s.replace('\x10', '') - # It seems that iPhoto sometimes doesn't properly escape & chars. The regexp below is to find - # any & char that is not a &-based entity (&, ", 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) - plist = plistlib.readPlistFromString(s) - for photo_data in plist['Master Image List'].values(): - self._update_photo(photo_data) + def _get_files(self, from_path): + if from_path == Path('iPhoto Library'): + is_ref = self.get_state(from_path) == directories.STATE_REFERENCE + photos = get_iphoto_pictures(self.iphoto_libpath) + for photo in photos: + photo.is_ref = is_ref + return photos + else: + return directories.Directories._get_files(self, from_path) - def force_update(self): # Don't update - pass + @staticmethod + def get_subfolders(path): + if path == Path('iPhoto Library'): + return [] + else: + return directories.Directories.get_subfolders(path) + + def add_path(self, path): + if path == Path('iPhoto Library'): + if path in self: + raise AlreadyThereError() + self._dirs.append(path) + else: + directories.Directories.add_path(self, path) class DupeGuruPE(app_cocoa.DupeGuru): def __init__(self): app_cocoa.DupeGuru.__init__(self, data, 'dupeGuru Picture Edition', appid=5) self.scanner = ScannerPE() - self.directories.dirclass = Directory - self.directories.special_dirclasses[Path('iPhoto Library')] = lambda _, __: self._create_iphoto_library() + self.directories = Directories() p = op.join(self.appdata, 'cached_pictures.db') self.scanner.cached_blocks = Cache(p) - def _create_iphoto_library(self): - ud = NSUserDefaults.standardUserDefaults() - prefs = ud.persistentDomainForName_('com.apple.iApps') - if 'iPhotoRecentDatabases' not in prefs: - raise directories.InvalidPathError - plisturl = NSURL.URLWithString_(prefs['iPhotoRecentDatabases'][0]) - plistpath = Path(plisturl.path()) - return IPhotoLibrary(plistpath) - def _do_delete(self, j): def op(dupe): j.add_progress() @@ -175,40 +166,19 @@ class DupeGuruPE(app_cocoa.DupeGuru): def _do_load(self, j): self.directories.load_from_file(op.join(self.appdata, 'last_directories.xml')) - for d in self.directories: - if isinstance(d, IPhotoLibrary): - d.update() self.results.load_from_xml(op.join(self.appdata, 'last_results.xml'), self._get_file, j) def _get_file(self, str_path): p = Path(str_path) - for d in self.directories: - result = None - if p in d.path: - result = d.find_path(p[d.path:]) - if isinstance(d, IPhotoLibrary) and p in d.refpath: - result = d.find_path(p[d.refpath:]) - if result is not None: - return result - - def add_directory(self, d): - result = app_cocoa.DupeGuru.add_directory(self, d) - if (result == 0) and (d == 'iPhoto Library'): - [iphotolib] = [dir for dir in self.directories if dir.path == d] - iphotolib.update() - return result + if p in self.directories.iphoto_libpath[:-1]: + return IPhoto(p) + return app_cocoa.DupeGuru._get_file(self, str_path) def copy_or_move(self, dupe, copy, destination, dest_type): if isinstance(dupe, IPhoto): copy = True return app_cocoa.DupeGuru.copy_or_move(self, dupe, copy, destination, dest_type) - def start_scanning(self): - for directory in self.directories: - if isinstance(directory, IPhotoLibrary): - self.directories.set_state(directory.refpath, directories.STATE_EXCLUDED) - return app_cocoa.DupeGuru.start_scanning(self) - def selected_dupe_path(self): if not self.selected_dupes: return None From 37a40040b34458f95dae6555643ab116e3395510 Mon Sep 17 00:00:00 2001 From: hsoft Date: Sat, 24 Oct 2009 13:54:57 +0000 Subject: [PATCH 210/275] [#73 state:port] Fixed a bug causing some matches to be ignored in the new pe match algo. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40212 --- pe/py/app_cocoa.py | 2 +- pe/py/matchbase.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/pe/py/app_cocoa.py b/pe/py/app_cocoa.py index 74e89701..8b9bd23b 100644 --- a/pe/py/app_cocoa.py +++ b/pe/py/app_cocoa.py @@ -36,7 +36,7 @@ class Photo(fs.File): INITIAL_INFO.update({ 'dimensions': (0,0), }) - HANDLED_EXTS = set(['png', 'jpg', 'jpeg', 'gif', 'psd', 'bmp', 'tiff', 'nef', 'cr2']) + HANDLED_EXTS = set(['png', 'jpg', 'jpeg', 'gif', 'psd', 'bmp', 'tiff', 'tif', 'nef', 'cr2']) @classmethod def can_handle(cls, path): diff --git a/pe/py/matchbase.py b/pe/py/matchbase.py index 3490620b..8cfae038 100644 --- a/pe/py/matchbase.py +++ b/pe/py/matchbase.py @@ -111,6 +111,8 @@ def getmatches(pictures, cached_blocks, threshold=75, match_scaled=False, j=job. if len(async_results) > RESULTS_QUEUE_LIMIT: result = async_results.pop(0) matches.extend(result.get()) + for result in async_results: # process the rest of the results + matches.extend(result.get()) result = [] for ref_id, other_id, percentage in j.iter_with_progress(matches, 'Verified %d/%d matches', every=10): From b25c1c3a3bc29592b30af32e14c3d3c25ba05a42 Mon Sep 17 00:00:00 2001 From: hsoft Date: Sat, 24 Oct 2009 14:18:36 +0000 Subject: [PATCH 211/275] Added dgpe 1.7.8 to the changelog. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40215 --- pe/help/changelog.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pe/help/changelog.yaml b/pe/help/changelog.yaml index 21347b44..eda6590d 100644 --- a/pe/help/changelog.yaml +++ b/pe/help/changelog.yaml @@ -1,3 +1,7 @@ +- date: 2009-10-24 + version: 1.7.8 + description: | + * Fixed a bug sometimes causing some duplicates to be ignored during the scans. (#73) - date: 2009-10-14 version: 1.7.7 description: | From 911521d8e04985653b4c0c229a3570e2e65223a4 Mon Sep 17 00:00:00 2001 From: hsoft Date: Sat, 24 Oct 2009 16:30:37 +0000 Subject: [PATCH 212/275] dgpe qt: build related fixes. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40217 --- pe/qt/app.py | 2 +- pe/qt/dgpe.spec | 2 +- pe/qt/gen.py | 1 + pe/qt/start.py | 3 +++ 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/pe/qt/app.py b/pe/qt/app.py index a73914d7..88eb35e4 100644 --- a/pe/qt/app.py +++ b/pe/qt/app.py @@ -31,7 +31,7 @@ class File(fs.File): INITIAL_INFO.update({ 'dimensions': (0,0), }) - HANDLED_EXTS = set(['png', 'jpg', 'jpeg', 'gif', 'bmp', 'tiff']) + HANDLED_EXTS = set(['png', 'jpg', 'jpeg', 'gif', 'bmp', 'tiff', 'tif']) @classmethod def can_handle(cls, path): diff --git a/pe/qt/dgpe.spec b/pe/qt/dgpe.spec index 06e92f4e..673d9039 100644 --- a/pe/qt/dgpe.spec +++ b/pe/qt/dgpe.spec @@ -1,6 +1,6 @@ # -*- mode: python -*- a = Analysis([os.path.join(HOMEPATH,'support\\_mountzlib.py'), os.path.join(HOMEPATH,'support\\useUnicode.py'), 'start.py'], - pathex=['C:\\src\\dupeguru\\pe\\qt']) + pathex=[]) pyz = PYZ(a.pure) exe = EXE(pyz, a.scripts, diff --git a/pe/qt/gen.py b/pe/qt/gen.py index 183aec92..caa7557e 100644 --- a/pe/qt/gen.py +++ b/pe/qt/gen.py @@ -16,6 +16,7 @@ from hsutil.build import print_and_do, build_all_qt_ui build_all_qt_ui(op.join('qtlib', 'ui')) build_all_qt_ui('base') build_all_qt_ui('.') +print_and_do("pyrcc4 base\\dg.qrc > base\\dg_rc.py") def move(src, dst): if not op.exists(src): diff --git a/pe/qt/start.py b/pe/qt/start.py index 7fb5a367..ace0fc0e 100644 --- a/pe/qt/start.py +++ b/pe/qt/start.py @@ -14,6 +14,9 @@ import base.dg_rc from app import DupeGuru +# This is a workaround for a pyinstaller problem where compiled dupeguru can't read tiff files +from PIL import TiffImagePlugin, TiffTags + if __name__ == "__main__": app = QApplication(sys.argv) app.setWindowIcon(QIcon(QPixmap(":/logo_pe"))) From f0a38a2b3f10165e7f7afa320b22edefd6a55f8c Mon Sep 17 00:00:00 2001 From: hsoft Date: Sun, 25 Oct 2009 10:42:00 +0000 Subject: [PATCH 213/275] dgse cocoa: dropped tiger support. Moved Details.nib and Directories.nib to dgbase. Added toolbar creation in the MainMenu nib. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40219 --- base/cocoa/DetailsPanel.m | 2 +- base/cocoa/DirectoryPanel.m | 2 +- base/cocoa/ResultWindow.h | 7 - base/cocoa/ResultWindow.m | 116 - base/cocoa/xib/DetailsPanel.xib | 1083 +++ base/cocoa/xib/DirectoryPanel.xib | 1491 +++++ images/gear.png | Bin 394 -> 0 bytes .../English.lproj/Details.nib/classes.nib | 18 - se/cocoa/English.lproj/Details.nib/info.nib | 16 - .../Details.nib/keyedobjects.nib | Bin 6122 -> 0 bytes .../English.lproj/Directories.nib/classes.nib | 62 - .../English.lproj/Directories.nib/info.nib | 20 - .../Directories.nib/keyedobjects.nib | Bin 9698 -> 0 bytes se/cocoa/English.lproj/InfoPlist.strings | Bin 204 -> 0 bytes .../English.lproj/MainMenu.nib/classes.nib | 229 - se/cocoa/English.lproj/MainMenu.nib/info.nib | 20 - .../MainMenu.nib/keyedobjects.nib | Bin 48638 -> 0 bytes se/cocoa/Info.plist | 2 + se/cocoa/ResultWindow.m | 13 - se/cocoa/dupeguru.xcodeproj/project.pbxproj | 115 +- se/cocoa/xib/MainMenu.xib | 5892 +++++++++++++++++ 21 files changed, 8492 insertions(+), 596 deletions(-) create mode 100644 base/cocoa/xib/DetailsPanel.xib create mode 100644 base/cocoa/xib/DirectoryPanel.xib delete mode 100755 images/gear.png delete mode 100644 se/cocoa/English.lproj/Details.nib/classes.nib delete mode 100644 se/cocoa/English.lproj/Details.nib/info.nib delete mode 100644 se/cocoa/English.lproj/Details.nib/keyedobjects.nib delete mode 100644 se/cocoa/English.lproj/Directories.nib/classes.nib delete mode 100644 se/cocoa/English.lproj/Directories.nib/info.nib delete mode 100644 se/cocoa/English.lproj/Directories.nib/keyedobjects.nib delete mode 100644 se/cocoa/English.lproj/InfoPlist.strings delete mode 100644 se/cocoa/English.lproj/MainMenu.nib/classes.nib delete mode 100644 se/cocoa/English.lproj/MainMenu.nib/info.nib delete mode 100644 se/cocoa/English.lproj/MainMenu.nib/keyedobjects.nib create mode 100644 se/cocoa/xib/MainMenu.xib diff --git a/base/cocoa/DetailsPanel.m b/base/cocoa/DetailsPanel.m index bf9d252f..859a6d42 100644 --- a/base/cocoa/DetailsPanel.m +++ b/base/cocoa/DetailsPanel.m @@ -12,7 +12,7 @@ http://www.hardcoded.net/licenses/hs_license @implementation DetailsPanelBase - (id)initWithPy:(PyApp *)aPy { - self = [super initWithWindowNibName:@"Details"]; + self = [super initWithWindowNibName:@"DetailsPanel"]; [self window]; //So the detailsTable is initialized. [detailsTable setPy:aPy]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(duplicateSelectionChanged:) name:DuplicateSelectionChangedNotification object:nil]; diff --git a/base/cocoa/DirectoryPanel.m b/base/cocoa/DirectoryPanel.m index f2a0d0fe..66f99a29 100644 --- a/base/cocoa/DirectoryPanel.m +++ b/base/cocoa/DirectoryPanel.m @@ -14,7 +14,7 @@ http://www.hardcoded.net/licenses/hs_license @implementation DirectoryPanelBase - (id)initWithParentApp:(id)aParentApp { - self = [super initWithWindowNibName:@"Directories"]; + self = [super initWithWindowNibName:@"DirectoryPanel"]; [self window]; AppDelegateBase *app = aParentApp; _py = [app py]; diff --git a/base/cocoa/ResultWindow.h b/base/cocoa/ResultWindow.h index 65e42ff7..5b062a52 100644 --- a/base/cocoa/ResultWindow.h +++ b/base/cocoa/ResultWindow.h @@ -20,21 +20,14 @@ http://www.hardcoded.net/licenses/hs_license @protected IBOutlet PyDupeGuruBase *py; IBOutlet id app; - IBOutlet NSView *actionMenuView; IBOutlet NSSegmentedControl *deltaSwitch; - IBOutlet NSView *deltaSwitchView; - IBOutlet NSView *filterFieldView; IBOutlet MatchesView *matches; IBOutlet NSSegmentedControl *pmSwitch; - IBOutlet NSView *pmSwitchView; IBOutlet NSTextField *stats; BOOL _powerMode; BOOL _displayDelta; } -/* Override */ -- (NSString *)logoImageName; - /* Helpers */ - (NSArray *)getColumnsOrder; - (NSDictionary *)getColumnsWidth; diff --git a/base/cocoa/ResultWindow.m b/base/cocoa/ResultWindow.m index 800daabc..42be291a 100644 --- a/base/cocoa/ResultWindow.m +++ b/base/cocoa/ResultWindow.m @@ -14,15 +14,6 @@ http://www.hardcoded.net/licenses/hs_license #import "AppDelegate.h" #import "Consts.h" -#define tbbDirectories @"tbbDirectories" -#define tbbDetails @"tbbDetail" -#define tbbPreferences @"tbbPreferences" -#define tbbPowerMarker @"tbbPowerMarker" -#define tbbScan @"tbbScan" -#define tbbAction @"tbbAction" -#define tbbDelta @"tbbDelta" -#define tbbFilter @"tbbFilter" - @implementation MatchesView - (void)keyDown:(NSEvent *)theEvent { @@ -68,12 +59,6 @@ http://www.hardcoded.net/licenses/hs_license [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(resultsUpdated:) name:ResultsUpdatedNotification object:nil]; } -/* Virtual */ -- (NSString *)logoImageName -{ - return @"dg_logo32"; -} - /* Helpers */ //Returns an array of identifiers, in order. - (NSArray *)getColumnsOrder @@ -355,107 +340,6 @@ http://www.hardcoded.net/licenses/hs_license [matches invalidateBuffers]; } -/* Toolbar */ -- (NSToolbarItem *)toolbar:(NSToolbar *)toolbar itemForItemIdentifier:(NSString *)itemIdentifier willBeInsertedIntoToolbar:(BOOL)flag -{ - NSToolbarItem *tbi = [[[NSToolbarItem alloc] initWithItemIdentifier:itemIdentifier] autorelease]; - if ([itemIdentifier isEqualTo:tbbDirectories]) - { - [tbi setLabel: @"Directories"]; - [tbi setToolTip: @"Show/Hide the directories panel."]; - [tbi setImage: [NSImage imageNamed: @"folder32"]]; - [tbi setTarget: app]; - [tbi setAction: @selector(toggleDirectories:)]; - } - else if ([itemIdentifier isEqualTo:tbbDetails]) - { - [tbi setLabel: @"Details"]; - [tbi setToolTip: @"Show/Hide the details panel."]; - [tbi setImage: [NSImage imageNamed: @"details32"]]; - [tbi setTarget: self]; - [tbi setAction: @selector(toggleDetailsPanel:)]; - } - else if ([itemIdentifier isEqualTo:tbbPreferences]) - { - [tbi setLabel: @"Preferences"]; - [tbi setToolTip: @"Show the preferences panel."]; - [tbi setImage: [NSImage imageNamed: @"preferences32"]]; - [tbi setTarget: self]; - [tbi setAction: @selector(showPreferencesPanel:)]; - } - else if ([itemIdentifier isEqualTo:tbbPowerMarker]) - { - [tbi setLabel: @"Power Marker"]; - [tbi setToolTip: @"When enabled, only the duplicates are shown, not the references."]; - [tbi setView:pmSwitchView]; - [tbi setMinSize:[pmSwitchView frame].size]; - [tbi setMaxSize:[pmSwitchView frame].size]; - } - else if ([itemIdentifier isEqualTo:tbbScan]) - { - [tbi setLabel: @"Start Scanning"]; - [tbi setToolTip: @"Start scanning for duplicates in the selected directories."]; - [tbi setImage: [NSImage imageNamed:[self logoImageName]]]; - [tbi setTarget: self]; - [tbi setAction: @selector(startDuplicateScan:)]; - } - else if ([itemIdentifier isEqualTo:tbbAction]) - { - [tbi setLabel: @"Action"]; - [tbi setView:actionMenuView]; - [tbi setMinSize:[actionMenuView frame].size]; - [tbi setMaxSize:[actionMenuView frame].size]; - } - else if ([itemIdentifier isEqualTo:tbbDelta]) - { - [tbi setLabel: @"Delta Values"]; - [tbi setToolTip: @"When enabled, this option makes dupeGuru display, where applicable, delta values instead of absolute values."]; - [tbi setView:deltaSwitchView]; - [tbi setMinSize:[deltaSwitchView frame].size]; - [tbi setMaxSize:[deltaSwitchView frame].size]; - } - else if ([itemIdentifier isEqualTo:tbbFilter]) - { - [tbi setLabel: @"Filter"]; - [tbi setToolTip: @"Filters the results using regular expression."]; - [tbi setView:filterFieldView]; - [tbi setMinSize:[filterFieldView frame].size]; - [tbi setMaxSize:NSMakeSize(1000, [filterFieldView frame].size.height)]; - } - [tbi setPaletteLabel: [tbi label]]; - return tbi; -} - -- (NSArray *)toolbarAllowedItemIdentifiers:(NSToolbar *)toolbar -{ - return [NSArray arrayWithObjects: - tbbDirectories, - tbbDetails, - tbbPreferences, - tbbPowerMarker, - tbbScan, - tbbAction, - tbbDelta, - tbbFilter, - NSToolbarSeparatorItemIdentifier, - NSToolbarSpaceItemIdentifier, - NSToolbarFlexibleSpaceItemIdentifier, - nil]; -} - -- (NSArray *)toolbarDefaultItemIdentifiers:(NSToolbar *)toolbar -{ - return [NSArray arrayWithObjects: - tbbScan, - tbbAction, - tbbDirectories, - tbbDetails, - tbbPowerMarker, - tbbDelta, - tbbFilter, - nil]; -} - - (BOOL)validateToolbarItem:(NSToolbarItem *)theItem { return ![[ProgressController mainProgressController] isShown]; diff --git a/base/cocoa/xib/DetailsPanel.xib b/base/cocoa/xib/DetailsPanel.xib new file mode 100644 index 00000000..3b00ffa6 --- /dev/null +++ b/base/cocoa/xib/DetailsPanel.xib @@ -0,0 +1,1083 @@ + + + + 1050 + 10B504 + 740 + 1038.2 + 437.00 + + com.apple.InterfaceBuilder.CocoaPlugin + 740 + + + YES + + + + YES + com.apple.InterfaceBuilder.CocoaPlugin + + + YES + + YES + + + YES + + + + YES + + DetailsPanel + + + FirstResponder + + + NSApplication + + + 155 + 2 + {{33, 261}, {451, 161}} + -260571136 + Details of Selected File + + NSPanel + + + View + + {1.79769e+308, 1.79769e+308} + {451, 161} + + + 256 + + YES + + + 274 + + YES + + + 2304 + + YES + + + 256 + {449, 143} + + 2 + YES + + + 256 + {449, 17} + + + + + + -2147483392 + {{-26, 0}, {16, 17}} + + + + YES + + 0 + 70 + 40 + 1000 + + 75628096 + 2048 + Attribute + + LucidaGrande + 11 + 3100 + + + 3 + MC4zMzMzMzI5OQA + + + 6 + System + headerTextColor + + 3 + MAA + + + + + 337772096 + 2048 + + + + 6 + System + controlBackgroundColor + + 3 + MC42NjY2NjY2NjY3AA + + + + 6 + System + controlTextColor + + + + 2 + YES + + + + 1 + 198 + 40 + 1000 + + 75628096 + 2048 + Selected + + + + + + 337772096 + 2048 + + + + + + 3 + YES + + + + 2 + 172 + 56.4755859375 + 1000 + + 75628096 + 2048 + Reference + + + 6 + System + headerColor + + 3 + MQA + + + + + + 337772096 + 2048 + + + + + + 3 + YES + + + + 3 + 2 + + + 6 + System + gridColor + + 3 + MC41AA + + + 14 + 1111523328 + + + 1 + 15 + 0 + YES + 0 + + + {{1, 17}, {449, 143}} + + + + + 4 + + + + -2147483392 + {{-30, 17}, {15, 129}} + + + _doScroller: + 0.89375001192092896 + + + + -2147483392 + {{-100, -100}, {394, 15}} + + 1 + + _doScroller: + 0.96332520246505737 + + + + 2304 + + YES + + + {{1, 0}, {449, 17}} + + + + + 4 + + + + {451, 161} + + + 530 + + + + + + QSAAAEEgAABBgAAAQYAAAA + + + {451, 161} + + + {{0, 0}, {1024, 746}} + {451, 177} + {1.79769e+308, 1.79769e+308} + + + + + YES + + + window + + + + 12 + + + + detailsTable + + + + 13 + + + + + YES + + 0 + + + + + + -2 + + + File's Owner + + + -1 + + + First Responder + + + 5 + + + YES + + + + details + + + 6 + + + YES + + + + + + 7 + + + YES + + + + + + + + + 8 + + + YES + + + + + + + + 9 + + + YES + + + + + + 10 + + + YES + + + + + + 11 + + + YES + + + + + + 15 + + + + + 16 + + + + + 17 + + + + + 18 + + + + + 19 + + + + + 20 + + + + + -3 + + + Application + + + + + YES + + YES + 10.IBPluginDependency + 10.ImportedFromIB2 + 11.IBPluginDependency + 11.ImportedFromIB2 + 15.IBShouldRemoveOnLegacySave + 16.IBShouldRemoveOnLegacySave + 17.IBShouldRemoveOnLegacySave + 18.IBShouldRemoveOnLegacySave + 19.IBShouldRemoveOnLegacySave + 20.IBShouldRemoveOnLegacySave + 5.IBEditorWindowLastContentRect + 5.IBPluginDependency + 5.IBWindowTemplateEditedContentRect + 5.ImportedFromIB2 + 5.windowTemplate.hasMinSize + 5.windowTemplate.minSize + 6.IBPluginDependency + 6.ImportedFromIB2 + 7.IBPluginDependency + 7.ImportedFromIB2 + 8.CustomClassName + 8.IBPluginDependency + 8.ImportedFromIB2 + 9.IBPluginDependency + 9.ImportedFromIB2 + + + YES + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + + + + + + + {{109, 656}, {451, 161}} + com.apple.InterfaceBuilder.CocoaPlugin + {{109, 656}, {451, 161}} + + + {451, 161} + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + TableView + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + + + + YES + + + YES + + + + + YES + + + YES + + + + 20 + + + + YES + + DetailsPanel + DetailsPanelBase + + IBProjectSource + DetailsPanel.h + + + + DetailsPanel + DetailsPanelBase + + detailsTable + NSTableView + + + IBUserSource + + + + + DetailsPanelBase + NSWindowController + + detailsTable + TableView + + + IBProjectSource + dgbase/DetailsPanel.h + + + + FirstResponder + + IBUserSource + + + + + PyApp + PyRegistrable + + IBProjectSource + cocoalib/PyApp.h + + + + PyRegistrable + NSObject + + IBProjectSource + cocoalib/PyRegistrable.h + + + + TableView + NSTableView + + py + PyApp + + + IBProjectSource + cocoalib/Table.h + + + + TableView + NSTableView + + IBUserSource + + + + + + YES + + NSActionCell + NSCell + + IBFrameworkSource + AppKit.framework/Headers/NSActionCell.h + + + + NSApplication + NSResponder + + IBFrameworkSource + AppKit.framework/Headers/NSApplication.h + + + + NSApplication + + IBFrameworkSource + AppKit.framework/Headers/NSApplicationScripting.h + + + + NSApplication + + IBFrameworkSource + AppKit.framework/Headers/NSColorPanel.h + + + + NSApplication + + IBFrameworkSource + AppKit.framework/Headers/NSHelpManager.h + + + + NSApplication + + IBFrameworkSource + AppKit.framework/Headers/NSPageLayout.h + + + + NSApplication + + IBFrameworkSource + AppKit.framework/Headers/NSUserInterfaceItemSearching.h + + + + NSCell + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSCell.h + + + + NSControl + NSView + + IBFrameworkSource + AppKit.framework/Headers/NSControl.h + + + + NSFormatter + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSFormatter.h + + + + NSMenu + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSMenu.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSAccessibility.h + + + + NSObject + + + + NSObject + + + + NSObject + + + + NSObject + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSDictionaryController.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSDragging.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSFontManager.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSFontPanel.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSKeyValueBinding.h + + + + NSObject + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSNibLoading.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSOutlineView.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSPasteboard.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSSavePanel.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSTableView.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSToolbarItem.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSView.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSArchiver.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSClassDescription.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSError.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSFileManager.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSKeyValueCoding.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSKeyValueObserving.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSKeyedArchiver.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSObject.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSObjectScripting.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSPortCoder.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSRunLoop.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSScriptClassDescription.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSScriptKeyValueCoding.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSScriptObjectSpecifiers.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSScriptWhoseTests.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSThread.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSURL.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSURLConnection.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSURLDownload.h + + + + NSObject + + IBFrameworkSource + Sparkle.framework/Headers/SUAppcast.h + + + + NSObject + + IBFrameworkSource + Sparkle.framework/Headers/SUUpdater.h + + + + NSPanel + NSWindow + + IBFrameworkSource + AppKit.framework/Headers/NSPanel.h + + + + NSResponder + + IBFrameworkSource + AppKit.framework/Headers/NSInterfaceStyle.h + + + + NSResponder + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSResponder.h + + + + NSScrollView + NSView + + IBFrameworkSource + AppKit.framework/Headers/NSScrollView.h + + + + NSScroller + NSControl + + IBFrameworkSource + AppKit.framework/Headers/NSScroller.h + + + + NSTableColumn + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSTableColumn.h + + + + NSTableHeaderView + NSView + + IBFrameworkSource + AppKit.framework/Headers/NSTableHeaderView.h + + + + NSTableView + NSControl + + + + NSTextFieldCell + NSActionCell + + IBFrameworkSource + AppKit.framework/Headers/NSTextFieldCell.h + + + + NSView + + IBFrameworkSource + AppKit.framework/Headers/NSClipView.h + + + + NSView + + IBFrameworkSource + AppKit.framework/Headers/NSMenuItem.h + + + + NSView + + IBFrameworkSource + AppKit.framework/Headers/NSRulerView.h + + + + NSView + NSResponder + + + + NSWindow + + IBFrameworkSource + AppKit.framework/Headers/NSDrawer.h + + + + NSWindow + NSResponder + + IBFrameworkSource + AppKit.framework/Headers/NSWindow.h + + + + NSWindow + + IBFrameworkSource + AppKit.framework/Headers/NSWindowScripting.h + + + + NSWindowController + NSResponder + + showWindow: + id + + + IBFrameworkSource + AppKit.framework/Headers/NSWindowController.h + + + + + 0 + + com.apple.InterfaceBuilder.CocoaPlugin.macosx + + + + com.apple.InterfaceBuilder.CocoaPlugin.macosx + + + + com.apple.InterfaceBuilder.CocoaPlugin.InterfaceBuilder3 + + + YES + ../dupeguru.xcodeproj + 3 + + diff --git a/base/cocoa/xib/DirectoryPanel.xib b/base/cocoa/xib/DirectoryPanel.xib new file mode 100644 index 00000000..9f8f8e7a --- /dev/null +++ b/base/cocoa/xib/DirectoryPanel.xib @@ -0,0 +1,1491 @@ + + + + 1050 + 10B504 + 740 + 1038.2 + 437.00 + + com.apple.InterfaceBuilder.CocoaPlugin + 740 + + + YES + + + + YES + com.apple.InterfaceBuilder.CocoaPlugin + + + YES + + YES + + + YES + + + + YES + + DirectoryPanel + + + FirstResponder + + + NSApplication + + + 27 + 2 + {{387, 290}, {369, 269}} + -260571136 + Directories + + NSPanel + + + View + + {1.79769e+308, 1.79769e+308} + {213, 113} + + + 256 + + YES + + + 274 + + YES + + + 2304 + + YES + + + 256 + {327, 165} + + 1 + YES + + + 256 + {327, 17} + + + + + + -2147483392 + {{-26, 0}, {16, 17}} + + + + YES + + 0 + 236 + 16 + 1000 + + 75628096 + 2048 + Name + + LucidaGrande + 11 + 3100 + + + 3 + MC4zMzMzMzI5OQA + + + 6 + System + headerTextColor + + 3 + MAA + + + + + 337772096 + 2048 + Text Cell + + + + 6 + System + controlBackgroundColor + + 3 + MC42NjY2NjY2NjY3AA + + + + 6 + System + controlTextColor + + + + 3 + YES + + + + 1 + 85.35595703125 + 30.35595703125 + 1000 + + 75628096 + 2048 + State + + + 6 + System + headerColor + + 3 + MQA + + + + + + 71433792 + 2048 + + + -2046672641 + 1 + + + 400 + 75 + + YES + + + + YES + + + -1 + 3 + YES + YES + 1 + + 2 + YES + + + + 3 + 2 + + + 6 + System + gridColor + + 3 + MC41AA + + + 14 + 1379958784 + + + 1 + 15 + 0 + YES + 0 + + + {{1, 17}, {327, 165}} + + + + + 4 + + + + -2147483392 + {{-30, 17}, {15, 150}} + + + _doScroller: + 0.7366071343421936 + + + + -2147483392 + {{1, -30}, {312, 15}} + + 1 + + _doScroller: + 0.9541284441947937 + + + + 2304 + + YES + + + {{1, 0}, {327, 17}} + + + + + 4 + + + + {{20, 66}, {329, 183}} + + + 562 + + + + + + QSAAAEEgAABBgAAAQYAAAA + + + + -2147483392 + {{17, 22}, {46, 26}} + + YES + + -2076049856 + 2048 + + LucidaGrande + 13 + 1044 + + + 109199615 + 1 + + LucidaGrande + 13 + 16 + + + + + + 400 + 75 + + + IA + + 1048576 + 2147483647 + 1 + + NSImage + NSMenuCheckmark + + + NSImage + NSMenuMixedState + + _popUpItemAction: + + + YES + + + OtherViews + + + YES + + + + 3 + YES + YES + 1 + + + + + 289 + {{152, 18}, {90, 32}} + + YES + + 67239424 + 134217728 + Add + + + -2038284033 + 1 + + + + + + 200 + 25 + + + + + 289 + {{62, 18}, {90, 32}} + + YES + + 67239424 + 134217728 + Remove + + + -2038284033 + 1 + + + + + + 200 + 25 + + + + + 289 + {{265, 18}, {90, 32}} + + YES + + 67239424 + 134217728 + Done + + + -2038284033 + 1 + + + DQ + 200 + 25 + + + + {369, 269} + + + {{0, 0}, {1440, 878}} + {213, 129} + {1.79769e+308, 1.79769e+308} + + + + + YES + + + initialFirstResponder + + + + 19 + + + + nextKeyView + + + + 20 + + + + nextKeyView + + + + 21 + + + + nextKeyView + + + + 22 + + + + popupAddDirectoryMenu: + + + + 23 + + + + removeSelectedDirectory: + + + + 24 + + + + window + + + + 25 + + + + addButtonPopUp + + + + 26 + + + + directories + + + + 27 + + + + delegate + + + + 29 + + + + performClose: + + + + 32 + + + + removeButton + + + + 43 + + + + + YES + + 0 + + + + + + -2 + + + File's Owner + + + -1 + + + First Responder + + + -3 + + + Application + + + 5 + + + YES + + + + directories + + + 6 + + + YES + + + + + + + + + + 7 + + + YES + + + + + + 10 + + + YES + + + + + + + + + 11 + + + YES + + + + + + + 13 + + + YES + + + + + + 14 + + + YES + + + + + + 15 + + + YES + + + + + + 17 + + + YES + + + + + + 18 + + + YES + + + + + + 31 + + + YES + + + + + + 45 + + + YES + + + + + + 46 + + + + + 47 + + + + + 48 + + + + + 49 + + + + + 8 + + + YES + + + + + + 9 + + + + + 50 + + + + + 51 + + + + + 52 + + + + + 53 + + + + + + + YES + + YES + -3.IBPluginDependency + -3.ImportedFromIB2 + 10.IBPluginDependency + 10.ImportedFromIB2 + 11.CustomClassName + 11.IBPluginDependency + 11.ImportedFromIB2 + 13.IBPluginDependency + 13.ImportedFromIB2 + 14.IBPluginDependency + 14.ImportedFromIB2 + 15.IBPluginDependency + 15.ImportedFromIB2 + 17.IBPluginDependency + 17.ImportedFromIB2 + 18.IBPluginDependency + 18.ImportedFromIB2 + 31.IBPluginDependency + 31.ImportedFromIB2 + 49.IBShouldRemoveOnLegacySave + 5.IBEditorWindowLastContentRect + 5.IBPluginDependency + 5.IBWindowTemplateEditedContentRect + 5.ImportedFromIB2 + 5.windowTemplate.hasMinSize + 5.windowTemplate.minSize + 51.IBShouldRemoveOnLegacySave + 52.IBShouldRemoveOnLegacySave + 53.IBShouldRemoveOnLegacySave + 6.IBPluginDependency + 6.ImportedFromIB2 + 7.IBPluginDependency + 7.ImportedFromIB2 + 8.IBPluginDependency + 8.ImportedFromIB2 + 9.IBPluginDependency + 9.ImportedFromIB2 + + + YES + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + OutlineView + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + + {{88, 571}, {369, 269}} + com.apple.InterfaceBuilder.CocoaPlugin + {{88, 571}, {369, 269}} + + + {213, 113} + + + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + + + + YES + + + YES + + + + + YES + + + YES + + + + 53 + + + + YES + + DirectoryPanel + DirectoryPanelBase + + IBProjectSource + DirectoryPanel.h + + + + DirectoryPanel + DirectoryPanelBase + + YES + + YES + askForDirectory: + changeDirectoryState: + popupAddDirectoryMenu: + removeSelectedDirectory: + toggleVisible: + + + YES + id + id + id + id + id + + + + YES + + YES + addButtonPopUp + directories + removeButton + + + YES + NSPopUpButton + NSOutlineView + NSButton + + + + IBUserSource + + + + + DirectoryPanelBase + NSWindowController + + YES + + YES + askForDirectory: + changeDirectoryState: + popupAddDirectoryMenu: + removeSelectedDirectory: + toggleVisible: + + + YES + id + id + id + id + id + + + + YES + + YES + addButtonPopUp + directories + removeButton + + + YES + NSPopUpButton + OutlineView + NSButton + + + + IBProjectSource + dgbase/DirectoryPanel.h + + + + FirstResponder + + IBUserSource + + + + + OutlineView + NSOutlineView + + py + PyApp + + + IBProjectSource + cocoalib/Outline.h + + + + OutlineView + NSOutlineView + + IBUserSource + + + + + PyApp + PyRegistrable + + IBProjectSource + cocoalib/PyApp.h + + + + PyRegistrable + NSObject + + IBProjectSource + cocoalib/PyRegistrable.h + + + + + YES + + NSActionCell + NSCell + + IBFrameworkSource + AppKit.framework/Headers/NSActionCell.h + + + + NSApplication + NSResponder + + IBFrameworkSource + AppKit.framework/Headers/NSApplication.h + + + + NSApplication + + IBFrameworkSource + AppKit.framework/Headers/NSApplicationScripting.h + + + + NSApplication + + IBFrameworkSource + AppKit.framework/Headers/NSColorPanel.h + + + + NSApplication + + IBFrameworkSource + AppKit.framework/Headers/NSHelpManager.h + + + + NSApplication + + IBFrameworkSource + AppKit.framework/Headers/NSPageLayout.h + + + + NSApplication + + IBFrameworkSource + AppKit.framework/Headers/NSUserInterfaceItemSearching.h + + + + NSButton + NSControl + + IBFrameworkSource + AppKit.framework/Headers/NSButton.h + + + + NSButtonCell + NSActionCell + + IBFrameworkSource + AppKit.framework/Headers/NSButtonCell.h + + + + NSCell + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSCell.h + + + + NSControl + NSView + + IBFrameworkSource + AppKit.framework/Headers/NSControl.h + + + + NSFormatter + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSFormatter.h + + + + NSMenu + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSMenu.h + + + + NSMenuItem + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSMenuItem.h + + + + NSMenuItemCell + NSButtonCell + + IBFrameworkSource + AppKit.framework/Headers/NSMenuItemCell.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSAccessibility.h + + + + NSObject + + + + NSObject + + + + NSObject + + + + NSObject + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSDictionaryController.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSDragging.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSFontManager.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSFontPanel.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSKeyValueBinding.h + + + + NSObject + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSNibLoading.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSOutlineView.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSPasteboard.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSSavePanel.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSTableView.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSToolbarItem.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSView.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSArchiver.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSClassDescription.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSError.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSFileManager.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSKeyValueCoding.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSKeyValueObserving.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSKeyedArchiver.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSObject.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSObjectScripting.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSPortCoder.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSRunLoop.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSScriptClassDescription.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSScriptKeyValueCoding.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSScriptObjectSpecifiers.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSScriptWhoseTests.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSThread.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSURL.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSURLConnection.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSURLDownload.h + + + + NSObject + + IBFrameworkSource + Sparkle.framework/Headers/SUAppcast.h + + + + NSObject + + IBFrameworkSource + Sparkle.framework/Headers/SUUpdater.h + + + + NSOutlineView + NSTableView + + + + NSPanel + NSWindow + + IBFrameworkSource + AppKit.framework/Headers/NSPanel.h + + + + NSPopUpButton + NSButton + + IBFrameworkSource + AppKit.framework/Headers/NSPopUpButton.h + + + + NSPopUpButtonCell + NSMenuItemCell + + IBFrameworkSource + AppKit.framework/Headers/NSPopUpButtonCell.h + + + + NSResponder + + IBFrameworkSource + AppKit.framework/Headers/NSInterfaceStyle.h + + + + NSResponder + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSResponder.h + + + + NSScrollView + NSView + + IBFrameworkSource + AppKit.framework/Headers/NSScrollView.h + + + + NSScroller + NSControl + + IBFrameworkSource + AppKit.framework/Headers/NSScroller.h + + + + NSTableColumn + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSTableColumn.h + + + + NSTableHeaderView + NSView + + IBFrameworkSource + AppKit.framework/Headers/NSTableHeaderView.h + + + + NSTableView + NSControl + + + + NSTextFieldCell + NSActionCell + + IBFrameworkSource + AppKit.framework/Headers/NSTextFieldCell.h + + + + NSView + + IBFrameworkSource + AppKit.framework/Headers/NSClipView.h + + + + NSView + + + + NSView + + IBFrameworkSource + AppKit.framework/Headers/NSRulerView.h + + + + NSView + NSResponder + + + + NSWindow + + IBFrameworkSource + AppKit.framework/Headers/NSDrawer.h + + + + NSWindow + NSResponder + + IBFrameworkSource + AppKit.framework/Headers/NSWindow.h + + + + NSWindow + + IBFrameworkSource + AppKit.framework/Headers/NSWindowScripting.h + + + + NSWindowController + NSResponder + + showWindow: + id + + + IBFrameworkSource + AppKit.framework/Headers/NSWindowController.h + + + + + 0 + + com.apple.InterfaceBuilder.CocoaPlugin.macosx + + + + com.apple.InterfaceBuilder.CocoaPlugin.macosx + + + + com.apple.InterfaceBuilder.CocoaPlugin.InterfaceBuilder3 + + + YES + ../dupeguru.xcodeproj + 3 + + diff --git a/images/gear.png b/images/gear.png deleted file mode 100755 index 41ff2dd6d78b9827ab401185d625f35184687699..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 394 zcmeAS@N?(olHy`uVBq!ia0vp^JRr=$1|-8uW1a&kwj^(N7l!{JxM1({$v_d#0*}aI z1_o|n5N2eUHAey{$X?><>&kwWhn-hLtURzvhk=1n+|$J|MB{w!#e-f~3I*6doPYCA zR5WthRol+0h;3G_;z!LtNbZuns_b!IL1bS;(uI}T0&liP-(8ZtU_&XVbw`}jtz0XM zq!#AGH9u?X57(scyW_h1l~P=1MeL7wztvg%{u?`4bxy}BwrGp#p)m@*T*{pZ$N2B8h+e&7oveb317H3V|%s+_Ln#?%p zU9;-$wvF+^YCmo!?As)JX4;fI(~`WMlkDyvOYc$2;i%;)=nLJWP$hV7LpwjO-bahe z28VRy9xl+cQ2d`#)xlnIf0cGlqGfRNdew!Y70YaH>?{2+eRlQvhZ65UJUPa8zOYGK m=FuVBjZOViwf|49XOukW9JuC4h#N4}7(8A5T-G@yGywqT51()V diff --git a/se/cocoa/English.lproj/Details.nib/classes.nib b/se/cocoa/English.lproj/Details.nib/classes.nib deleted file mode 100644 index e1b7cb92..00000000 --- a/se/cocoa/English.lproj/Details.nib/classes.nib +++ /dev/null @@ -1,18 +0,0 @@ -{ - IBClasses = ( - { - CLASS = DetailsPanel; - LANGUAGE = ObjC; - OUTLETS = {detailsTable = NSTableView; }; - SUPERCLASS = NSWindowController; - }, - {CLASS = FirstResponder; LANGUAGE = ObjC; SUPERCLASS = NSObject; }, - { - CLASS = TableView; - LANGUAGE = ObjC; - OUTLETS = {py = PyApp; }; - SUPERCLASS = NSTableView; - } - ); - IBVersion = 1; -} \ No newline at end of file diff --git a/se/cocoa/English.lproj/Details.nib/info.nib b/se/cocoa/English.lproj/Details.nib/info.nib deleted file mode 100644 index 3f14ee77..00000000 --- a/se/cocoa/English.lproj/Details.nib/info.nib +++ /dev/null @@ -1,16 +0,0 @@ - - - - - IBDocumentLocation - 432 54 356 240 0 0 1024 746 - IBFramework Version - 443.0 - IBOpenObjects - - 5 - - IBSystem Version - 8I127 - - diff --git a/se/cocoa/English.lproj/Details.nib/keyedobjects.nib b/se/cocoa/English.lproj/Details.nib/keyedobjects.nib deleted file mode 100644 index d4df1a7889bd47136abada1956a01f83a9ab97d2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6122 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 tUi(V>yY{2@lXf{xOq0@FX{|C-a6WIU& diff --git a/se/cocoa/English.lproj/Directories.nib/classes.nib b/se/cocoa/English.lproj/Directories.nib/classes.nib deleted file mode 100644 index 3ebaa96a..00000000 --- a/se/cocoa/English.lproj/Directories.nib/classes.nib +++ /dev/null @@ -1,62 +0,0 @@ - - - - - IBClasses - - - CLASS - FirstResponder - LANGUAGE - ObjC - SUPERCLASS - NSObject - - - ACTIONS - - askForDirectory - id - changeDirectoryState - id - popupAddDirectoryMenu - id - removeSelectedDirectory - id - toggleVisible - id - - CLASS - DirectoryPanel - LANGUAGE - ObjC - OUTLETS - - addButtonPopUp - NSPopUpButton - directories - NSOutlineView - removeButton - NSButton - - SUPERCLASS - DirectoryPanelBase - - - CLASS - OutlineView - LANGUAGE - ObjC - OUTLETS - - py - PyApp - - SUPERCLASS - NSOutlineView - - - IBVersion - 1 - - diff --git a/se/cocoa/English.lproj/Directories.nib/info.nib b/se/cocoa/English.lproj/Directories.nib/info.nib deleted file mode 100644 index 5c508e04..00000000 --- a/se/cocoa/English.lproj/Directories.nib/info.nib +++ /dev/null @@ -1,20 +0,0 @@ - - - - - IBFramework Version - 629 - IBLastKnownRelativeProjectPath - ../../dupeguru.xcodeproj - IBOldestOS - 5 - IBOpenObjects - - 6 - - IBSystem Version - 9B18 - targetFramework - IBCocoaFramework - - diff --git a/se/cocoa/English.lproj/Directories.nib/keyedobjects.nib b/se/cocoa/English.lproj/Directories.nib/keyedobjects.nib deleted file mode 100644 index 2275d202d0e2f6eaaa97521edfef9371f56b2f95..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9698 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 Baclqp diff --git a/se/cocoa/English.lproj/InfoPlist.strings b/se/cocoa/English.lproj/InfoPlist.strings deleted file mode 100644 index d224a14bd2062242e455ea370c921c67e7045c94..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 204 zcmW-bOA5k35Cv=PDT2!&D(*yFxrrby%n4#XD*i$e6}^#{RLh~Ed*=0fHS_s0A|_(R zm7I(d2VRsEYIkQtt8(SyjGUEy>8yVS6Mq diff --git a/se/cocoa/English.lproj/MainMenu.nib/classes.nib b/se/cocoa/English.lproj/MainMenu.nib/classes.nib deleted file mode 100644 index 3a85cf2a..00000000 --- a/se/cocoa/English.lproj/MainMenu.nib/classes.nib +++ /dev/null @@ -1,229 +0,0 @@ - - - - - IBClasses - - - CLASS - NSSegmentedControl - LANGUAGE - ObjC - SUPERCLASS - NSControl - - - ACTIONS - - openWebsite - id - toggleDirectories - id - unlockApp - id - - CLASS - AppDelegate - LANGUAGE - ObjC - OUTLETS - - py - PyDupeGuru - recentDirectories - RecentDirectories - result - ResultWindow - unlockMenuItem - NSMenuItem - - SUPERCLASS - NSObject - - - CLASS - PyApp - LANGUAGE - ObjC - SUPERCLASS - NSObject - - - CLASS - MatchesView - LANGUAGE - ObjC - SUPERCLASS - OutlineView - - - CLASS - PyDupeGuru - LANGUAGE - ObjC - SUPERCLASS - PyApp - - - ACTIONS - - changeDelta - id - changePowerMarker - id - clearIgnoreList - id - collapseAll - id - copyMarked - id - deleteMarked - id - expandAll - id - exportToXHTML - id - filter - id - ignoreSelected - id - markAll - id - markInvert - id - markNone - id - markSelected - id - markToggle - id - moveMarked - id - openSelected - id - refresh - id - removeMarked - id - removeSelected - id - renameSelected - id - resetColumnsToDefault - id - revealSelected - id - showPreferencesPanel - id - startDuplicateScan - id - switchSelected - id - toggleColumn - id - toggleDelta - id - toggleDetailsPanel - id - togglePowerMarker - id - - CLASS - ResultWindow - LANGUAGE - ObjC - OUTLETS - - actionMenu - NSPopUpButton - actionMenuView - NSView - app - id - columnsMenu - NSMenu - deltaSwitch - NSSegmentedControl - deltaSwitchView - NSView - filterField - NSSearchField - filterFieldView - NSView - matches - MatchesView - pmSwitch - NSSegmentedControl - pmSwitchView - NSView - preferencesPanel - NSWindow - py - PyDupeGuru - stats - NSTextField - - SUPERCLASS - NSWindowController - - - CLASS - FirstResponder - LANGUAGE - ObjC - SUPERCLASS - NSObject - - - ACTIONS - - checkForUpdates - id - - CLASS - SUUpdater - LANGUAGE - ObjC - SUPERCLASS - NSObject - - - ACTIONS - - clearMenu - id - menuClick - id - - CLASS - RecentDirectories - LANGUAGE - ObjC - OUTLETS - - delegate - id - menu - NSMenu - - SUPERCLASS - NSObject - - - CLASS - OutlineView - LANGUAGE - ObjC - OUTLETS - - py - PyApp - - SUPERCLASS - NSOutlineView - - - IBVersion - 1 - - diff --git a/se/cocoa/English.lproj/MainMenu.nib/info.nib b/se/cocoa/English.lproj/MainMenu.nib/info.nib deleted file mode 100644 index 6799cea9..00000000 --- a/se/cocoa/English.lproj/MainMenu.nib/info.nib +++ /dev/null @@ -1,20 +0,0 @@ - - - - - IBFramework Version - 629 - IBLastKnownRelativeProjectPath - ../../dupeguru.xcodeproj - IBOldestOS - 5 - IBOpenObjects - - 524 - - IBSystem Version - 9E17 - targetFramework - IBCocoaFramework - - diff --git a/se/cocoa/English.lproj/MainMenu.nib/keyedobjects.nib b/se/cocoa/English.lproj/MainMenu.nib/keyedobjects.nib deleted file mode 100644 index 7136407cef8a5712c05487ac7165ec08616e1cfd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 48638 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𝔗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 diff --git a/se/cocoa/Info.plist b/se/cocoa/Info.plist index 9263d518..648f0f39 100644 --- a/se/cocoa/Info.plist +++ b/se/cocoa/Info.plist @@ -28,6 +28,8 @@ MainMenu NSPrincipalClass NSApplication + NSHumanReadableCopyright + © Hardcoded Software, 2009 SUFeedURL http://www.hardcoded.net/updates/dupeguru.appcast SUPublicDSAKeyFile diff --git a/se/cocoa/ResultWindow.m b/se/cocoa/ResultWindow.m index a8bf6ec4..ad1554d7 100644 --- a/se/cocoa/ResultWindow.m +++ b/se/cocoa/ResultWindow.m @@ -32,19 +32,6 @@ http://www.hardcoded.net/licenses/hs_license [self initResultColumns]; [self refreshStats]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(resultsMarkingChanged:) name:ResultsMarkingChangedNotification object:nil]; - - NSToolbar *t = [[[NSToolbar alloc] initWithIdentifier:@"ResultWindowToolbar"] autorelease]; - [t setAllowsUserCustomization:YES]; - [t setAutosavesConfiguration:YES]; - [t setDisplayMode:NSToolbarDisplayModeIconAndLabel]; - [t setDelegate:self]; - [[self window] setToolbar:t]; -} - -/* Override */ -- (NSString *)logoImageName -{ - return @"dgse_logo_32"; } /* Actions */ diff --git a/se/cocoa/dupeguru.xcodeproj/project.pbxproj b/se/cocoa/dupeguru.xcodeproj/project.pbxproj index e5583252..650e1593 100644 --- a/se/cocoa/dupeguru.xcodeproj/project.pbxproj +++ b/se/cocoa/dupeguru.xcodeproj/project.pbxproj @@ -7,28 +7,26 @@ objects = { /* Begin PBXBuildFile section */ - 8D11072A0486CEB800E47090 /* MainMenu.nib in Resources */ = {isa = PBXBuildFile; fileRef = 29B97318FDCFA39411CA2CEA /* MainMenu.nib */; }; - 8D11072B0486CEB800E47090 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 089C165CFE840E0CC02AAC07 /* InfoPlist.strings */; }; 8D11072D0486CEB800E47090 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 29B97316FDCFA39411CA2CEA /* main.m */; settings = {ATTRIBUTES = (); }; }; 8D11072F0486CEB800E47090 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */; }; CE073F6309CAE1A3005C1D2F /* dupeguru_help in Resources */ = {isa = PBXBuildFile; fileRef = CE073F5409CAE1A3005C1D2F /* dupeguru_help */; }; CE381C9609914ACE003581CE /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = CE381C9409914ACE003581CE /* AppDelegate.m */; }; CE381C9C09914ADF003581CE /* ResultWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = CE381C9A09914ADF003581CE /* ResultWindow.m */; }; CE381D0509915304003581CE /* dg_cocoa.plugin in Resources */ = {isa = PBXBuildFile; fileRef = CE381CF509915304003581CE /* dg_cocoa.plugin */; }; - CE3AA46709DB207900DB3A21 /* Directories.nib in Resources */ = {isa = PBXBuildFile; fileRef = CE3AA46509DB207900DB3A21 /* Directories.nib */; }; CE45579B0AE3BC2B005A9546 /* Sparkle.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE45579A0AE3BC2B005A9546 /* Sparkle.framework */; }; CE4557B40AE3BC50005A9546 /* Sparkle.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = CE45579A0AE3BC2B005A9546 /* Sparkle.framework */; }; CE68EE6809ABC48000971085 /* DirectoryPanel.m in Sources */ = {isa = PBXBuildFile; fileRef = CE68EE6609ABC48000971085 /* DirectoryPanel.m */; }; CE6E0DFE1054E9EF008D9390 /* dsa_pub.pem in Resources */ = {isa = PBXBuildFile; fileRef = CE6E0DFD1054E9EF008D9390 /* dsa_pub.pem */; }; CE848A1909DD85810004CB44 /* Consts.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = CE848A1809DD85810004CB44 /* Consts.h */; }; - CECA899909DB12CA00A3D774 /* Details.nib in Resources */ = {isa = PBXBuildFile; fileRef = CECA899709DB12CA00A3D774 /* Details.nib */; }; CECA899C09DB132E00A3D774 /* DetailsPanel.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = CECA899A09DB132E00A3D774 /* DetailsPanel.h */; }; CECA899D09DB132E00A3D774 /* DetailsPanel.m in Sources */ = {isa = PBXBuildFile; fileRef = CECA899B09DB132E00A3D774 /* DetailsPanel.m */; }; CEDD92DA0FDD01640031C7B7 /* BRSingleLineFormatter.m in Sources */ = {isa = PBXBuildFile; fileRef = CEDD92D70FDD01640031C7B7 /* BRSingleLineFormatter.m */; }; CEDD92DB0FDD01640031C7B7 /* NSCharacterSet_Extensions.m in Sources */ = {isa = PBXBuildFile; fileRef = CEDD92D90FDD01640031C7B7 /* NSCharacterSet_Extensions.m */; }; CEE7EA130FE675C80004E467 /* DetailsPanel.m in Sources */ = {isa = PBXBuildFile; fileRef = CEE7EA120FE675C80004E467 /* DetailsPanel.m */; }; CEEB135209C837A2004D2330 /* dupeguru.icns in Resources */ = {isa = PBXBuildFile; fileRef = CEEB135109C837A2004D2330 /* dupeguru.icns */; }; - CEF7823809C8AA0200EF38FF /* gear.png in Resources */ = {isa = PBXBuildFile; fileRef = CEF7823709C8AA0200EF38FF /* gear.png */; }; + CEEFC0EF10944EDD001F3A39 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = CEEFC0EE10944EDD001F3A39 /* MainMenu.xib */; }; + CEEFC0F810945D9F001F3A39 /* DirectoryPanel.xib in Resources */ = {isa = PBXBuildFile; fileRef = CEEFC0F710945D9F001F3A39 /* DirectoryPanel.xib */; }; + CEEFC0FB10945E37001F3A39 /* DetailsPanel.xib in Resources */ = {isa = PBXBuildFile; fileRef = CEEFC0FA10945E37001F3A39 /* DetailsPanel.xib */; }; CEFC294609C89E3D00D9F998 /* folder32.png in Resources */ = {isa = PBXBuildFile; fileRef = CEFC294509C89E3D00D9F998 /* folder32.png */; }; CEFC295509C89FF200D9F998 /* details32.png in Resources */ = {isa = PBXBuildFile; fileRef = CEFC295309C89FF200D9F998 /* details32.png */; }; CEFC295609C89FF200D9F998 /* preferences32.png in Resources */ = {isa = PBXBuildFile; fileRef = CEFC295409C89FF200D9F998 /* preferences32.png */; }; @@ -66,11 +64,9 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 089C165DFE840E0CC02AAC07 /* English */ = {isa = PBXFileReference; fileEncoding = 10; lastKnownFileType = text.plist.strings; name = English; path = English.lproj/InfoPlist.strings; sourceTree = ""; }; 1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = /System/Library/Frameworks/Cocoa.framework; sourceTree = ""; }; 13E42FB307B3F0F600E4EEF1 /* CoreData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreData.framework; path = /System/Library/Frameworks/CoreData.framework; sourceTree = ""; }; 29B97316FDCFA39411CA2CEA /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; - 29B97319FDCFA39411CA2CEA /* English */ = {isa = PBXFileReference; lastKnownFileType = wrapper.nib; name = English; path = English.lproj/MainMenu.nib; sourceTree = ""; }; 29B97324FDCFA39411CA2CEA /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = /System/Library/Frameworks/AppKit.framework; sourceTree = ""; }; 29B97325FDCFA39411CA2CEA /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = /System/Library/Frameworks/Foundation.framework; sourceTree = ""; }; 8D1107310486CEB800E47090 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = SOURCE_ROOT; }; @@ -81,13 +77,11 @@ CE381C9A09914ADF003581CE /* ResultWindow.m */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.objc; path = ResultWindow.m; sourceTree = SOURCE_ROOT; }; CE381C9B09914ADF003581CE /* ResultWindow.h */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.h; path = ResultWindow.h; sourceTree = SOURCE_ROOT; }; CE381CF509915304003581CE /* dg_cocoa.plugin */ = {isa = PBXFileReference; lastKnownFileType = folder; name = dg_cocoa.plugin; path = py/dist/dg_cocoa.plugin; sourceTree = SOURCE_ROOT; }; - CE3AA46609DB207900DB3A21 /* English */ = {isa = PBXFileReference; lastKnownFileType = wrapper.nib; name = English; path = English.lproj/Directories.nib; sourceTree = ""; }; CE45579A0AE3BC2B005A9546 /* Sparkle.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Sparkle.framework; path = /Library/Frameworks/Sparkle.framework; sourceTree = ""; }; CE68EE6509ABC48000971085 /* DirectoryPanel.h */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.h; path = DirectoryPanel.h; sourceTree = SOURCE_ROOT; }; CE68EE6609ABC48000971085 /* DirectoryPanel.m */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.objc; path = DirectoryPanel.m; sourceTree = SOURCE_ROOT; }; CE6E0DFD1054E9EF008D9390 /* dsa_pub.pem */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = dsa_pub.pem; path = dgbase/dsa_pub.pem; sourceTree = ""; }; CE848A1809DD85810004CB44 /* Consts.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Consts.h; sourceTree = ""; }; - CECA899809DB12CA00A3D774 /* English */ = {isa = PBXFileReference; lastKnownFileType = wrapper.nib; name = English; path = English.lproj/Details.nib; sourceTree = ""; }; CECA899A09DB132E00A3D774 /* DetailsPanel.h */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.h; path = DetailsPanel.h; sourceTree = ""; }; CECA899B09DB132E00A3D774 /* DetailsPanel.m */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.objc; path = DetailsPanel.m; sourceTree = ""; }; CEDD92D60FDD01640031C7B7 /* BRSingleLineFormatter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BRSingleLineFormatter.h; path = cocoalib/brsinglelineformatter/BRSingleLineFormatter.h; sourceTree = SOURCE_ROOT; }; @@ -97,7 +91,9 @@ CEE7EA110FE675C80004E467 /* DetailsPanel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = DetailsPanel.h; path = dgbase/DetailsPanel.h; sourceTree = SOURCE_ROOT; }; CEE7EA120FE675C80004E467 /* DetailsPanel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = DetailsPanel.m; path = dgbase/DetailsPanel.m; sourceTree = SOURCE_ROOT; }; CEEB135109C837A2004D2330 /* dupeguru.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; path = dupeguru.icns; sourceTree = ""; }; - CEF7823709C8AA0200EF38FF /* gear.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = gear.png; path = images/gear.png; sourceTree = ""; }; + CEEFC0EE10944EDD001F3A39 /* MainMenu.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = MainMenu.xib; path = xib/MainMenu.xib; sourceTree = ""; }; + CEEFC0F710945D9F001F3A39 /* DirectoryPanel.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = DirectoryPanel.xib; path = dgbase/xib/DirectoryPanel.xib; sourceTree = ""; }; + CEEFC0FA10945E37001F3A39 /* DetailsPanel.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = DetailsPanel.xib; path = dgbase/xib/DetailsPanel.xib; sourceTree = ""; }; CEFC294509C89E3D00D9F998 /* folder32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = folder32.png; path = images/folder32.png; sourceTree = SOURCE_ROOT; }; CEFC295309C89FF200D9F998 /* details32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = details32.png; path = images/details32.png; sourceTree = SOURCE_ROOT; }; CEFC295409C89FF200D9F998 /* preferences32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = preferences32.png; path = images/preferences32.png; sourceTree = SOURCE_ROOT; }; @@ -213,13 +209,10 @@ CE073F5409CAE1A3005C1D2F /* dupeguru_help */, CE381CF509915304003581CE /* dg_cocoa.plugin */, CEFC294309C89E0000D9F998 /* images */, + CEEFC0CA10943849001F3A39 /* xib */, CEEB135109C837A2004D2330 /* dupeguru.icns */, 8D1107310486CEB800E47090 /* Info.plist */, - 089C165CFE840E0CC02AAC07 /* InfoPlist.strings */, CE6E0DFD1054E9EF008D9390 /* dsa_pub.pem */, - CECA899709DB12CA00A3D774 /* Details.nib */, - CE3AA46509DB207900DB3A21 /* Directories.nib */, - 29B97318FDCFA39411CA2CEA /* MainMenu.nib */, ); name = Resources; sourceTree = ""; @@ -245,10 +238,19 @@ path = cocoalib/brsinglelineformatter; sourceTree = SOURCE_ROOT; }; + CEEFC0CA10943849001F3A39 /* xib */ = { + isa = PBXGroup; + children = ( + CEEFC0EE10944EDD001F3A39 /* MainMenu.xib */, + CEEFC0F710945D9F001F3A39 /* DirectoryPanel.xib */, + CEEFC0FA10945E37001F3A39 /* DetailsPanel.xib */, + ); + name = xib; + sourceTree = ""; + }; CEFC294309C89E0000D9F998 /* images */ = { isa = PBXGroup; children = ( - CEF7823709C8AA0200EF38FF /* gear.png */, CEFC295D09C8A0B000D9F998 /* dgse_logo_32.png */, CEFC295309C89FF200D9F998 /* details32.png */, CEFC295409C89FF200D9F998 /* preferences32.png */, @@ -352,8 +354,6 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 8D11072A0486CEB800E47090 /* MainMenu.nib in Resources */, - 8D11072B0486CEB800E47090 /* InfoPlist.strings in Resources */, CE381D0509915304003581CE /* dg_cocoa.plugin in Resources */, CE073F6309CAE1A3005C1D2F /* dupeguru_help in Resources */, CEEB135209C837A2004D2330 /* dupeguru.icns in Resources */, @@ -361,13 +361,13 @@ CEFC295509C89FF200D9F998 /* details32.png in Resources */, CEFC295609C89FF200D9F998 /* preferences32.png in Resources */, CEFC295E09C8A0B000D9F998 /* dgse_logo_32.png in Resources */, - CEF7823809C8AA0200EF38FF /* gear.png in Resources */, - CECA899909DB12CA00A3D774 /* Details.nib in Resources */, - CE3AA46709DB207900DB3A21 /* Directories.nib in Resources */, CEFC7FAD0FC9518A00CD5728 /* ErrorReportWindow.xib in Resources */, CEFC7FAE0FC9518A00CD5728 /* progress.nib in Resources */, CEFC7FAF0FC9518A00CD5728 /* registration.nib in Resources */, CE6E0DFE1054E9EF008D9390 /* dsa_pub.pem in Resources */, + CEEFC0EF10944EDD001F3A39 /* MainMenu.xib in Resources */, + CEEFC0F810945D9F001F3A39 /* DirectoryPanel.xib in Resources */, + CEEFC0FB10945E37001F3A39 /* DetailsPanel.xib in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -404,38 +404,6 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXVariantGroup section */ - 089C165CFE840E0CC02AAC07 /* InfoPlist.strings */ = { - isa = PBXVariantGroup; - children = ( - 089C165DFE840E0CC02AAC07 /* English */, - ); - name = InfoPlist.strings; - sourceTree = ""; - }; - 29B97318FDCFA39411CA2CEA /* MainMenu.nib */ = { - isa = PBXVariantGroup; - children = ( - 29B97319FDCFA39411CA2CEA /* English */, - ); - name = MainMenu.nib; - sourceTree = SOURCE_ROOT; - }; - CE3AA46509DB207900DB3A21 /* Directories.nib */ = { - isa = PBXVariantGroup; - children = ( - CE3AA46609DB207900DB3A21 /* English */, - ); - name = Directories.nib; - sourceTree = ""; - }; - CECA899709DB12CA00A3D774 /* Details.nib */ = { - isa = PBXVariantGroup; - children = ( - CECA899809DB12CA00A3D774 /* English */, - ); - name = Details.nib; - sourceTree = ""; - }; CEFC7FA70FC9518A00CD5728 /* ErrorReportWindow.xib */ = { isa = PBXVariantGroup; children = ( @@ -463,28 +431,6 @@ /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ - C01FCF4B08A954540054247B /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - COPY_PHASE_STRIP = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(FRAMEWORK_SEARCH_PATHS)", - "$(SRCROOT)/../../../cocoalib/build/Release", - "$(FRAMEWORK_SEARCH_PATHS_QUOTED_FOR_TARGET_1)", - ); - FRAMEWORK_SEARCH_PATHS_QUOTED_FOR_TARGET_1 = "\"$(SRCROOT)/../../base/cocoa/build/Release\""; - GCC_DYNAMIC_NO_PIC = NO; - GCC_ENABLE_FIX_AND_CONTINUE = YES; - GCC_MODEL_TUNING = G5; - GCC_OPTIMIZATION_LEVEL = 0; - INFOPLIST_FILE = Info.plist; - INSTALL_PATH = "$(HOME)/Applications"; - PRODUCT_NAME = dupeGuru; - WRAPPER_EXTENSION = app; - ZERO_LINK = YES; - }; - name = Debug; - }; C01FCF4C08A954540054247B /* Release */ = { isa = XCBuildConfiguration; buildSettings = { @@ -507,31 +453,16 @@ }; name = Release; }; - C01FCF4F08A954540054247B /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - GCC_C_LANGUAGE_STANDARD = c99; - GCC_WARN_ABOUT_RETURN_TYPE = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.4; - PREBINDING = NO; - SDKROOT = "$(DEVELOPER_SDK_DIR)/MacOSX10.4u.sdk"; - }; - name = Debug; - }; C01FCF5008A954540054247B /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ARCHS = "$(ARCHS_STANDARD_32_BIT_PRE_XCODE_3_1)"; ARCHS_STANDARD_32_BIT_PRE_XCODE_3_1 = "ppc i386"; - FRAMEWORK_SEARCH_PATHS = "@executable_path/../Frameworks"; GCC_C_LANGUAGE_STANDARD = c99; - GCC_VERSION = 4.0; GCC_WARN_ABOUT_RETURN_TYPE = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.4; - PREBINDING = NO; - SDKROOT = "$(DEVELOPER_SDK_DIR)/MacOSX10.4u.sdk"; + MACOSX_DEPLOYMENT_TARGET = 10.5; + SDKROOT = "$(DEVELOPER_SDK_DIR)/MacOSX10.5.sdk"; }; name = Release; }; @@ -541,7 +472,6 @@ C01FCF4A08A954540054247B /* Build configuration list for PBXNativeTarget "dupeguru" */ = { isa = XCConfigurationList; buildConfigurations = ( - C01FCF4B08A954540054247B /* Debug */, C01FCF4C08A954540054247B /* Release */, ); defaultConfigurationIsVisible = 0; @@ -550,7 +480,6 @@ C01FCF4E08A954540054247B /* Build configuration list for PBXProject "dupeguru" */ = { isa = XCConfigurationList; buildConfigurations = ( - C01FCF4F08A954540054247B /* Debug */, C01FCF5008A954540054247B /* Release */, ); defaultConfigurationIsVisible = 0; diff --git a/se/cocoa/xib/MainMenu.xib b/se/cocoa/xib/MainMenu.xib new file mode 100644 index 00000000..80bf96cd --- /dev/null +++ b/se/cocoa/xib/MainMenu.xib @@ -0,0 +1,5892 @@ + + + + 1050 + 10B504 + 740 + 1038.2 + 437.00 + + com.apple.InterfaceBuilder.CocoaPlugin + 740 + + + + + + + + com.apple.InterfaceBuilder.CocoaPlugin + + + + + NSApplication + + + FirstResponder + + + NSApplication + + + 15 + 2 + {{47, 310}, {557, 400}} + 1886912512 + dupeGuru + NSWindow + + + 184FCE08-7704-43E1-B7CA-394621354414 + + + YES + YES + YES + YES + 1 + 1 + + + + 05CA01D3-49FE-42B7-B043-791FFDF37644 + + Power Marker + Power Marker + + + + 256 + {{7, 14}, {67, 24}} + + YES + + 67239424 + 0 + + LucidaGrande + 11 + 3100 + + + + + 30 + Off + 2 + + + 30 + On + 1 + 2 + + + 1 + + + + + + {67, 24} + {67, 24} + YES + YES + 0 + YES + 0 + + + + 3E17CA47-6688-44FC-963C-CF22D780305F + + Start Scanning + Start Scanning + + + + NSImage + dgse_logo_32 + + + + {0, 0} + {0, 0} + YES + YES + -1 + YES + 0 + + + + 8021CD05-A38E-4F80-99DD-7771914CEE06 + + Preferences + Preferences + + + + NSImage + preferences32 + + + + {0, 0} + {0, 0} + YES + YES + -1 + YES + 0 + + + + 8E5ADD0F-24AD-452A-BE68-464FE9E5E240 + + Filter + Filter + + + + 258 + {{0, 14}, {81, 22}} + + YES + + 343014976 + 268436480 + + + LucidaGrande + 13 + 1044 + + Filter + + YES + 1 + + 6 + System + textBackgroundColor + + 3 + MQA + + + + 6 + System + controlTextColor + + 3 + MAA + + + + 130560 + 0 + search + + _searchFieldSearch: + + 138690815 + 0 + + 400 + 75 + + + 130560 + 0 + clear + + + cancel + + + + + _searchFieldCancel: + + 138690815 + 0 + + 400 + 75 + + 10 + YES + + + + + + {81, 22} + {9999, 22} + YES + YES + 0 + YES + 0 + + + + BA65FFF2-9E56-4E88-AB2E-8FBE2B3D030F + + Directories + Directories + + + + NSImage + folder32 + + + + {0, 0} + {0, 0} + YES + YES + -1 + YES + 0 + + + + DAF37663-5062-4BEC-809F-0F335CC144ED + + Details + Details + + + + NSImage + details32 + + + + {0, 0} + {0, 0} + YES + YES + -1 + YES + 0 + + + + F37510C7-955F-4141-9D09-AC2881ADCCFA + + Action + Action + + + + 256 + {{0, 14}, {58, 26}} + + YES + + -2076049856 + 2048 + + + 109199615 + 1 + + + + + + 400 + 75 + + + YES + IA + + 1048576 + 2147483647 + 1 + + NSImage + NSActionTemplate + + + NSImage + NSMenuCheckmark + + + NSImage + NSMenuMixedState + + _popUpItemAction: + + + YES + + + OtherViews + + + + + + Send Marked to Trash + + 2147483647 + + + _popUpItemAction: + + + + + Move Marked to... + + 2147483647 + + + _popUpItemAction: + + + + + Copy Marked to... + + 2147483647 + + + _popUpItemAction: + + + + + Remove Marked from Results + + 2147483647 + + + _popUpItemAction: + + + + + YES + YES + + + 1048576 + 2147483647 + + + _popUpItemAction: + + + + + Remove Selected from Results + + 2147483647 + + + _popUpItemAction: + + + + + Add Selected to Ignore List + + 1048576 + 2147483647 + + + _popUpItemAction: + + + + + Make Selected Reference + + 2147483647 + + + _popUpItemAction: + + + + + YES + YES + + + 1048576 + 2147483647 + + + _popUpItemAction: + + + + + Open Selected with Default Application + + 2147483647 + + + _popUpItemAction: + + + + + Reveal Selected in Finder + + 2147483647 + + + _popUpItemAction: + + + + + Rename Selected + + 1048576 + 2147483647 + + + _popUpItemAction: + + + + + YES + 3 + YES + YES + 1 + + + + + + {58, 26} + {58, 26} + YES + YES + 0 + YES + 0 + + + + F813A7D3-0C98-4465-A6F8-799EF380A59F + + Delta Values + Delta Values + + + + 256 + {{4, 14}, {67, 24}} + + YES + + 67239424 + 0 + + + + + 30 + Off + 2 + + + 30 + On + 1 + 2 + + + 1 + + + + + + {67, 24} + {67, 24} + YES + YES + 0 + YES + 0 + + + NSToolbarFlexibleSpaceItem + + Flexible Space + + + + + + {1, 5} + {20000, 32} + YES + YES + -1 + YES + 0 + + YES + YES + + + 1048576 + 2147483647 + + + + + + NSToolbarSeparatorItem + + Separator + + + + + + {12, 5} + {12, 1000} + YES + YES + -1 + YES + 0 + + YES + YES + + + 1048576 + 2147483647 + + + + + + NSToolbarSpaceItem + + Space + + + + + + {32, 5} + {32, 32} + YES + YES + -1 + YES + 0 + + YES + YES + + + 1048576 + 2147483647 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {1.79769e+308, 1.79769e+308} + {340, 340} + + + 256 + + + + 274 + + + + 2304 + + + + 274 + {515, 317} + + YES + + + 256 + {515, 17} + + + + + + -2147483392 + {{-26, 0}, {16, 17}} + + + + + mark + 47 + 16 + 1000 + + 75628096 + 2048 + + + + 6 + System + headerColor + + + + 6 + System + headerTextColor + + + + + 67239424 + 131072 + + + LucidaGrande + 12 + 16 + + + 1211912703 + 2 + + NSSwitch + + + + 400 + 75 + + + + + 0 + 195 + 16 + 1000 + + 75628096 + 2048 + Name + + + 3 + MC4zMzMzMzI5OQA + + + + + 337772096 + 2048 + + + + 6 + System + controlBackgroundColor + + 3 + MC42NjY2NjY2NjY3AA + + + + + 2 + YES + + + 0 + YES + compare: + + + + 1 + 120 + 10 + 1000 + + 75628096 + 2048 + Directory + + + + + + 337772096 + 2048 + + + + + + 2 + YES + + + 1 + YES + compare: + + + + 2 + 63 + 10 + 1000 + + 75628096 + 2048 + Size (KB) + + + + + + 337772096 + 67110912 + + + + + + 2 + YES + + + 2 + YES + compare: + + + + 6 + 59.9580078125 + 46.9580078125 + 1000 + + 75628096 + 2048 + Match % + + + + + + 337772096 + 2048 + + + + + + 2 + YES + + + 6 + YES + compare: + + + + 3 + 2 + + + 6 + System + gridColor + + 3 + MC41AA + + + 14 + -895483904 + + + 2 + 0 + 15 + 0 + YES + 0 + + + {{1, 17}, {515, 317}} + + + + + 4 + + + + -2147483392 + {{-30, 17}, {15, 302}} + + + _doScroller: + 0.98739492893218994 + + + + -2147483392 + {{1, -30}, {500, 15}} + + 1 + + _doScroller: + 0.99806201457977295 + + + + 2304 + + + + {{1, 0}, {515, 17}} + + + + + 4 + + + + {{20, 45}, {517, 335}} + + + 562 + + + + + + QSAAAEEgAABBgAAAQYAAAA + + + + 290 + {{17, 20}, {523, 17}} + + YES + + 67239424 + 138412032 + Marked: 0 files, 0 B. Total: 0 files, 0 B. + + + + 6 + System + controlColor + + + + + + + {557, 400} + + + {{0, 0}, {1440, 878}} + {340, 418} + {1.79769e+308, 1.79769e+308} + + + MainMenu + + + + dupeGuru + + 1048576 + 2147483647 + + + submenuAction: + + dupeGuru + + + + About dupeGuru + + 2147483647 + + + + + + Unlock dupeGuru + + 1048576 + 2147483647 + + + + + + Check for update... + + 1048576 + 2147483647 + + + + + + YES + YES + + + 1048576 + 2147483647 + + + + + + Preferences... + , + 1048576 + 2147483647 + + + + + + YES + YES + + + 1048576 + 2147483647 + + + + + + Hide dupeGuru + h + 1048576 + 2147483647 + + + + + + Hide Others + h + 1572864 + 2147483647 + + + + + + Show All + + 1048576 + 2147483647 + + + + + + YES + YES + + + 1048576 + 2147483647 + + + + + + Quit dupeGuru + q + 1048576 + 2147483647 + + + + + _NSAppleMenu + + + + + Edit + + 1048576 + 2147483647 + + + submenuAction: + + + Edit + + + + + Mark All + a + 1048576 + 2147483647 + + + + + + Mark None + A + 1048576 + 2147483647 + + + + + + Invert Marking + a + 1572864 + 2147483647 + + + + + + Mark Selected + a + 1310720 + 2147483647 + + + + + + YES + YES + + + 1048576 + 2147483647 + + + + + + Cut + x + 1048576 + 2147483647 + + + + + + Copy + c + 1048576 + 2147483647 + + + + + + Paste + v + 1048576 + 2147483647 + + + + + + + + + Actions + + 1048576 + 2147483647 + + + submenuAction: + + Actions + + + + Start Duplicate Scan + s + 1048576 + 2147483647 + + + + + + Clear Ignore List + I + 1048576 + 2147483647 + + + + + + Export Results to XHTML + E + 1048576 + 2147483647 + + + + + + YES + YES + + + 1048576 + 2147483647 + + + + + + Send Marked to Trash + t + 1048576 + 2147483647 + + + + + + Move Marked to... + m + 1048576 + 2147483647 + + + + + + Copy Marked to... + m + 1572864 + 2147483647 + + + + + + Remove Marked from Results + r + 1048576 + 2147483647 + + + + + + YES + YES + + + 1048576 + 2147483647 + + + + + + Remove Selected from Results + R + 1048576 + 2147483647 + + + + + + Add Selected to Ignore List + i + 1048576 + 2147483647 + + + + + + Make Selected Reference +  + 1048576 + 2147483647 + + + + + + YES + YES + + + 1048576 + 2147483647 + + + + + + Open Selected with Default Application + DQ + 1048576 + 2147483647 + + + + + + Reveal Selected in Finder + DQ + 1572864 + 2147483647 + + + + + + Rename Selected + Aw + 2147483647 + + + + + + + + + Columns + + 1048576 + 2147483647 + + + submenuAction: + + Columns + + + + File Name + + 1048576 + 2147483647 + 1 + + + + + + Directory + + 1048576 + 2147483647 + 1 + + + 1 + + + + Size + + 1048576 + 2147483647 + 1 + + + 2 + + + + Kind + + 1048576 + 2147483647 + + + 3 + + + + Creation + + 1048576 + 2147483647 + + + 4 + + + + Modification + + 1048576 + 2147483647 + + + 5 + + + + Match % + + 1048576 + 2147483647 + 1 + + + 6 + + + + Words Used + + 1048576 + 2147483647 + + + 7 + + + + Dupe Count + + 1048576 + 2147483647 + + + 8 + + + + YES + YES + IA + + 1048576 + 2147483647 + + + -1 + + + + Reset to Default + + 1048576 + 2147483647 + + + -1 + + + + + + + Mode + + 1048576 + 2147483647 + + + submenuAction: + + Mode + + + + Power Marker + 1 + 1048576 + 2147483647 + + + + + + Delta Values + 2 + 1048576 + 2147483647 + + + + + + + + + Window + + 1048576 + 2147483647 + + + submenuAction: + + Window + + + + Directory Panel + 3 + 1048576 + 2147483647 + + + + + + Details Panel + 4 + 1048576 + 2147483647 + + + + + + YES + YES + + + 1048576 + 2147483647 + + + + + + Minimize + + 2147483647 + + + + + + Zoom + + 1048576 + 2147483647 + + + + + + Close Window + w + 1048576 + 2147483647 + + + + + + YES + YES + + + 1048576 + 2147483647 + + + + + + Bring All to Front + + 1048576 + 2147483647 + + + + + _NSWindowsMenu + + + + + Help + + 1048576 + 2147483647 + + + submenuAction: + + Help + + + + dupeGuru Help + ? + 1048576 + 2147483647 + + + + + + dupeGuru Website + + 1048576 + 2147483647 + + + + + + + + _NSMainMenu + + + AppDelegate + + + ResultWindow + + + YES + + + RecentDirectories + + + 3 + 2 + {{92, 276}, {352, 326}} + 1886912512 + dupeGuru Preferences + + NSWindow + + + View + + {1.79769e+308, 1.79769e+308} + {213, 107} + + + 256 + + + + 292 + {{120, 247}, {181, 21}} + + YES + + 67239424 + 0 + + + + + Helvetica + 12 + 16 + + + 100 + 1 + 80 + 0.0 + 0 + 1 + NO + NO + + + + + 292 + {{122, 230}, {80, 13}} + + YES + + 67239424 + 272629760 + More results + + LucidaGrande + 10 + 2843 + + + + + + + + + 289 + {{219, 230}, {80, 13}} + + YES + + 67239424 + 71303168 + Less results + + + + + + + + + 292 + {{17, 252}, {100, 14}} + + YES + + 67239424 + 272629760 + Filter hardness: + + + + + + + + + 292 + {{20, 293}, {85, 13}} + + YES + + 67239424 + 272629760 + Scan type: + + + + + + + + + 292 + {{119, 282}, {216, 26}} + + YES + + -2076049856 + 2048 + + + 109199615 + 1 + + + + + + 400 + 75 + + + Filename + + 1048576 + 2147483647 + 1 + + + _popUpItemAction: + + + YES + + + OtherViews + + + + + + Content + + 1048576 + 2147483647 + + + _popUpItemAction: + + + + + 3 + YES + YES + 1 + + + + + 256 + {{18, 206}, {214, 18}} + + YES + + 67239424 + 0 + Word weighting + + + 1211912703 + 2 + + + + 200 + 25 + + + + + 256 + {{18, 166}, {214, 18}} + + YES + + 67239424 + 0 + Can mix file kind + + + 1211912703 + 2 + + + + 200 + 25 + + + + + 256 + {{304, 252}, {31, 14}} + + YES + + 67239424 + -1874853888 + + + + + + + 0 + + + . + + , + -0 + 0 + + + 0 + -0 + + + + + + + + NaN + + + + 0 + 0 + YES + NO + 1 + AAAAAAAAAAAAAAAAAAAAAA + + + + . + , + NO + YES + YES + + + + + + + + + 256 + {{190, 12}, {148, 32}} + + YES + + 67239424 + 134217728 + Reset to Defaults + + + -2038284033 + 1 + + + + + + 200 + 25 + + + + + 256 + {{18, 186}, {214, 18}} + + YES + + 67239424 + 0 + Match similar words + + + 1211912703 + 2 + + + + 200 + 25 + + + + + 292 + {{20, 67}, {85, 13}} + + YES + + 67239424 + 272629760 + Copy and Move: + + + + + + + + + 292 + {{110, 56}, {216, 26}} + + YES + + -2076049856 + 2048 + + + 109199615 + 1 + + + + + + 400 + 75 + + + Right in destination + + 1048576 + 2147483647 + 1 + + + _popUpItemAction: + + + YES + + + OtherViews + + + + + + Recreate relative path + + 1048576 + 2147483647 + + + _popUpItemAction: + + + + + Recreate absolute path + + 1048576 + 2147483647 + + + _popUpItemAction: + + + + + 3 + YES + YES + 1 + + + + + 256 + {{18, 86}, {283, 18}} + + YES + + 67239424 + 0 + Check for update on startup + + + 1211912703 + 2 + + + + 200 + 25 + + + + + 256 + {{18, 146}, {228, 18}} + + YES + + 67239424 + 0 + Use regular expressions when filtering + + + 1211912703 + 2 + + + + 200 + 25 + + + + + 256 + {{18, 126}, {242, 18}} + + YES + + 67239424 + 0 + Remove empty folders on delete or move + + + 1211912703 + 2 + + + + 200 + 25 + + + + + 256 + {{18, 106}, {152, 18}} + + YES + + 67239424 + 0 + Ignore files smaller than + + + 1211912703 + 2 + + + + 200 + 25 + + + + + 268 + {{176, 104}, {59, 22}} + + YES + + -1804468671 + -1874852864 + + + + + + + + + + + + #0 + + + + #0 + #0 + + + + + + NaN + + + + + + 3 + YES + YES + YES + + . + , + NO + NO + NO + + + YES + + + 6 + System + textColor + + + + + + + 292 + {{243, 106}, {23, 17}} + + YES + + 67239424 + 272629760 + KB + + + + + + + + {352, 326} + + + {{0, 0}, {1440, 878}} + {213, 129} + {1.79769e+308, 1.79769e+308} + + + PyDupeGuru + + + Menu + + + + Remove Selected from Results + + 1048576 + 2147483647 + + + + + + Add Selected to Ignore List + + 1048576 + 2147483647 + + + + + + Make Selected Reference + + 1048576 + 2147483647 + + + + + + YES + YES + + + 1048576 + 2147483647 + + + + + + Open Selected with Default Application + + 1048576 + 2147483647 + + + + + + Reveal Selected in Finder + + 1048576 + 2147483647 + + + + + + Rename Selected + + 1048576 + 2147483647 + + + + + + + + SUUpdater + + + + + + + performMiniaturize: + + + + 37 + + + + arrangeInFront: + + + + 39 + + + + showHelp: + + + + 122 + + + + terminate: + + + + 139 + + + + orderFrontStandardAboutPanel: + + + + 142 + + + + hideOtherApplications: + + + + 146 + + + + hide: + + + + 152 + + + + unhideAllApplications: + + + + 153 + + + + performZoom: + + + + 198 + + + + delegate + + + + 207 + + + + delegate + + + + 208 + + + + window + + + + 210 + + + + result + + + + 211 + + + + delegate + + + + 212 + + + + matches + + + + 245 + + + + initialFirstResponder + + + + 279 + + + + delegate + + + + 410 + + + + markToggle: + + + + 414 + + + + stats + + + + 445 + + + + delegate + + + + 502 + + + + recentDirectories + + + + 503 + + + + makeKeyAndOrderFront: + + + + 543 + + + + value: values.minMatchPercentage + + + + + + value: values.minMatchPercentage + value + values.minMatchPercentage + 2 + + + 549 + + + + selectedIndex: values.scanType + + + + + + selectedIndex: values.scanType + selectedIndex + values.scanType + 2 + + + 551 + + + + enabled: values.scanType + + + + + + enabled: values.scanType + enabled + values.scanType + + NSValueTransformerName + vtScanTypeIsNotContent + + 2 + + + 554 + + + + toggleDetailsPanel: + + + + 596 + + + + deleteMarked: + + + + 606 + + + + moveMarked: + + + + 607 + + + + copyMarked: + + + + 608 + + + + removeMarked: + + + + 609 + + + + switchSelected: + + + + 610 + + + + removeSelected: + + + + 611 + + + + py + + + + 614 + + + + py + + + + 616 + + + + toggleColumn: + + + + 627 + + + + toggleColumn: + + + + 628 + + + + toggleColumn: + + + + 629 + + + + toggleColumn: + + + + 630 + + + + toggleColumn: + + + + 631 + + + + toggleColumn: + + + + 632 + + + + toggleColumn: + + + + 633 + + + + enabled: values.scanType + + + + + + enabled: values.scanType + enabled + values.scanType + + NSValueTransformerName + vtScanTypeIsNotContent + + 2 + + + 640 + + + + value: values.wordWeighting + + + + + + value: values.wordWeighting + value + values.wordWeighting + 2 + + + 642 + + + + toggleColumn: + + + + 647 + + + + value: values.mixFileKind + + + + + + value: values.mixFileKind + value + values.mixFileKind + 2 + + + 656 + + + + openSelected: + + + + 660 + + + + revealSelected: + + + + 661 + + + + menu + + + + 663 + + + + toggleColumn: + + + + 706 + + + + openSelected: + + + + 709 + + + + revealSelected: + + + + 711 + + + + value: values.minMatchPercentage + + + + + + value: values.minMatchPercentage + value + values.minMatchPercentage + 2 + + + 713 + + + + switchSelected: + + + + 716 + + + + preferencesPanel + + + + 718 + + + + actionMenu + + + + 726 + + + + deleteMarked: + + + + 741 + + + + moveMarked: + + + + 742 + + + + copyMarked: + + + + 743 + + + + removeMarked: + + + + 744 + + + + removeSelected: + + + + 745 + + + + switchSelected: + + + + 746 + + + + openSelected: + + + + 747 + + + + revealSelected: + + + + 748 + + + + unlockApp: + + + + 755 + + + + unlockMenuItem + + + + 756 + + + + app + + + + 757 + + + + toggleDirectories: + + + + 758 + + + + py + + + + 764 + + + + enabled: values.scanType + + + + + + enabled: values.scanType + enabled + values.scanType + + NSValueTransformerName + vtScanTypeIsNotContent + + 2 + + + 774 + + + + value: values.matchSimilarWords + + + + + + value: values.matchSimilarWords + value + values.matchSimilarWords + 2 + + + 775 + + + + removeSelected: + + + + 873 + + + + changeDelta: + + + + 882 + + + + deltaSwitch + + + + 883 + + + + selectedIndex: values.recreatePathType + + + + + + selectedIndex: values.recreatePathType + selectedIndex + values.recreatePathType + 2 + + + 914 + + + + ignoreSelected: + + + + 921 + + + + ignoreSelected: + + + + 923 + + + + performClose: + + + + 925 + + + + startDuplicateScan: + + + + 929 + + + + clearIgnoreList: + + + + 930 + + + + revertToInitialValues: + + + + 932 + + + + renameSelected: + + + + 934 + + + + renameSelected: + + + + 936 + + + + ignoreSelected: + + + + 940 + + + + renameSelected: + + + + 941 + + + + resetColumnsToDefault: + + + + 945 + + + + columnsMenu + + + + 946 + + + + exportToXHTML: + + + + 948 + + + + checkForUpdates: + + + + 951 + + + + value: values.SUCheckAtStartup + + + + + + value: values.SUCheckAtStartup + value + values.SUCheckAtStartup + 2 + + + 953 + + + + changePowerMarker: + + + + 956 + + + + pmSwitch + + + + 957 + + + + togglePowerMarker: + + + + 963 + + + + toggleDelta: + + + + 964 + + + + cut: + + + + 996 + + + + copy: + + + + 998 + + + + paste: + + + + 1005 + + + + markAll: + + + + 1019 + + + + markNone: + + + + 1020 + + + + markInvert: + + + + 1021 + + + + markSelected: + + + + 1022 + + + + openWebsite: + + + + 1024 + + + + value: values.useRegexpFilter + + + + + + value: values.useRegexpFilter + value + values.useRegexpFilter + 2 + + + 1026 + + + + filterField + + + + 1030 + + + + filter: + + + + 1031 + + + + nextKeyView + + + + 1062 + + + + value: values.removeEmptyFolders + + + + + + value: values.removeEmptyFolders + value + values.removeEmptyFolders + 2 + + + 1072 + + + + value: values.ignoreSmallFiles + + + + + + value: values.ignoreSmallFiles + value + values.ignoreSmallFiles + 2 + + + 1111 + + + + value: values.smallFileThreshold + + + + + + value: values.smallFileThreshold + value + values.smallFileThreshold + 2 + + + 1113 + + + + toggleDirectories: + + + + 1166 + + + + toggleDetailsPanel: + + + + 1167 + + + + showPreferencesPanel: + + + + 1168 + + + + startDuplicateScan: + + + + 1169 + + + + + + 0 + + + + + + -2 + + + File's Owner + + + -1 + + + First Responder + + + -3 + + + Application + + + 21 + + + + + + + Window + + + 2 + + + + + + + + + 219 + + + + + + + + + + + 220 + + + + + + + + + + + + 222 + + + + + + + + 223 + + + + + + + + 233 + + + + + + + + 406 + + + + + + + + 407 + + + + + 931 + + + + + + + + 291 + + + + + + + + 29 + + + + + + + + + + + + MainMenu + + + 19 + + + + + + + + 24 + + + + + + + + + + + + + + + 5 + + + + + 23 + + + + + 92 + + + + + 197 + + + + + 398 + + + + + 399 + + + + + 579 + + + + + 924 + + + + + 56 + + + + + + + + 57 + + + + + + + + + + + + + + + + + + 58 + + + + + 134 + + + + + 136 + + + + + 144 + + + + + 145 + + + + + 149 + + + + + 150 + + + + + 541 + + + + + 542 + + + + + 754 + + + + + 950 + + + + + 103 + + + + + + + + 106 + + + + + + + + + 111 + + + + + 1023 + + + + + 597 + + + + + + + + 598 + + + + + + + + + + + + + + + + + + + + + + + 599 + + + + + 600 + + + + + 601 + + + + + 602 + + + + + 603 + + + + + 604 + + + + + 605 + + + + + 707 + + + + + 708 + + + + + 710 + + + + + 922 + + + + + 926 + + + + + 927 + + + + + 928 + + + + + 933 + + + + + 947 + + + + + 618 + + + + + + + + 619 + + + + + + + + + + + + + + + + + + 620 + + + + + 621 + + + + + 622 + + + + + 623 + + + + + 624 + + + + + 625 + + + + + 626 + + + + + 646 + + + + + 705 + + + + + 943 + + + + + 944 + + + + + 959 + + + + + + + + 960 + + + + + + + + + 961 + + + + + 962 + + + + + 965 + + + + + + + + 966 + + + + + + + + + + + + + + + 985 + + + + + 986 + + + + + 991 + + + + + 1011 + + + + + 1012 + + + + + 1013 + + + + + 1014 + + + + + 1018 + + + + + 206 + + + AppDelegate + + + 209 + + + ResultWindow + + + 468 + + + Shared Defaults + + + 497 + + + RecentDirectoriesController + + + 523 + + + + + + preferences + + + 524 + + + + + + + + + + + + + + + + + + + + + + + + + + 531 + + + + + + + + 532 + + + + + + + + 533 + + + + + + + + 534 + + + + + + + + 535 + + + + + + + + 536 + + + + + + + + 635 + + + + + + + + 649 + + + + + + + + 712 + + + + + + + + 750 + + + + + + + + 772 + + + + + + + + 904 + + + + + + + + 905 + + + + + + + + 952 + + + + + + + + 1025 + + + + + + + + 1068 + + + + + + + + 1104 + + + + + + + + 1106 + + + + + + + + 1109 + + + + + + + + 613 + + + PyDupeGuru + + + 657 + + + + + + + + + + + + matches_context + + + 658 + + + + + 659 + + + + + 715 + + + + + 872 + + + + + 937 + + + + + 938 + + + + + 939 + + + + + 949 + + + SUUpdater + + + 1116 + + + + + 1117 + + + + + 1118 + + + + + 1119 + + + + + 1120 + + + + + 1121 + + + + + 1122 + + + + + + + + 1123 + + + + + 1124 + + + + + 1125 + + + + + + + + 1126 + + + + + 1127 + + + + + 1128 + + + + + 1129 + + + + + + + + 1130 + + + + + 1131 + + + + + 1132 + + + + + 1133 + + + + + 1134 + + + + + + + + 1135 + + + + + 1140 + + + + + 1141 + + + + + 1142 + + + + + 1143 + + + + + 714 + + + + + 1108 + + + + + 537 + + + + + + + + + 539 + + + + + 538 + + + + + 906 + + + + + + + + + + 913 + + + + + 909 + + + + + 908 + + + + + 1144 + + + + + 1145 + + + + + 1146 + + + + + 1147 + + + + + + + + + + + + + + + + + + 1150 + + + + + 1152 + + + + + 1153 + + + + + 1156 + + + + + 1158 + + + + + 1159 + + + + + 1160 + + + + + 1170 + + + + + + + + 720 + + + + + + + + 1136 + + + + + + + + 721 + + + + + + + + + + + + + + + + + + + + 935 + + + + + 740 + + + + + 739 + + + + + 737 + + + + + 738 + + + + + 920 + + + + + 736 + + + + + 735 + + + + + 734 + + + + + 733 + + + + + 732 + + + + + 731 + + + + + 723 + + + + + 1171 + + + + + + + + 955 + + + + + + + + 1138 + + + + + 1172 + + + + + + + + 880 + + + + + + + + 1137 + + + + + 1173 + + + + + + + + 1028 + + + + + + + + 1139 + + + + + + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + {{79, 539}, {617, 227}} + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + tbbScan + com.apple.InterfaceBuilder.CocoaPlugin + tbbDirectories + com.apple.InterfaceBuilder.CocoaPlugin + tbbDetail + com.apple.InterfaceBuilder.CocoaPlugin + tbbPreferences + com.apple.InterfaceBuilder.CocoaPlugin + tbbAction + com.apple.InterfaceBuilder.CocoaPlugin + tbbPowerMarker + com.apple.InterfaceBuilder.CocoaPlugin + tbbDelta + com.apple.InterfaceBuilder.CocoaPlugin + tbbFilter + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + + + {{109, 366}, {557, 400}} + com.apple.InterfaceBuilder.CocoaPlugin + {{109, 366}, {557, 400}} + + + + {340, 340} + com.apple.InterfaceBuilder.CocoaPlugin + + MatchesView + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + {{140, 768}, {481, 20}} + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + + com.apple.InterfaceBuilder.CocoaPlugin + + {{88, 519}, {352, 326}} + com.apple.InterfaceBuilder.CocoaPlugin + {{88, 519}, {352, 326}} + + + {213, 107} + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + {{286, 475}, {359, 293}} + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + {{182, 609}, {331, 133}} + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + {{94, 408}, {331, 243}} + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + + + + + + 1173 + + + + + AppDelegate + AppDelegateBase + + id + id + + + result + ResultWindow + + + IBProjectSource + AppDelegate.h + + + + AppDelegate + AppDelegateBase + + unlockApp: + id + + + PyDupeGuru + RecentDirectories + NSMenuItem + + + IBUserSource + + + + + AppDelegateBase + NSObject + + unlockApp: + id + + + PyDupeGuruBase + RecentDirectories + NSMenuItem + + + IBProjectSource + dgbase/AppDelegate.h + + + + FirstResponder + NSObject + + IBUserSource + + + + + MatchesView + OutlineView + + IBProjectSource + dgbase/ResultWindow.h + + + + MatchesView + OutlineView + + IBUserSource + + + + + NSSegmentedControl + NSControl + + IBUserSource + + + + + OutlineView + NSOutlineView + + py + PyApp + + + IBProjectSource + cocoalib/Outline.h + + + + OutlineView + NSOutlineView + + IBUserSource + + + + + PyApp + PyRegistrable + + IBProjectSource + cocoalib/PyApp.h + + + + PyApp + PyRegistrable + + IBUserSource + + + + + PyDupeGuru + PyDupeGuruBase + + IBProjectSource + PyDupeGuru.h + + + + PyDupeGuru + PyDupeGuruBase + + IBUserSource + + + + + PyDupeGuruBase + PyApp + + IBProjectSource + dgbase/PyDupeGuru.h + + + + PyRegistrable + NSObject + + IBProjectSource + cocoalib/PyRegistrable.h + + + + RecentDirectories + NSObject + + id + id + + + id + NSMenu + + + IBProjectSource + cocoalib/RecentDirectories.h + + + + RecentDirectories + NSObject + + IBUserSource + + + + + ResultWindow + ResultWindowBase + + id + id + id + id + id + id + id + id + id + id + id + id + id + id + id + id + id + id + id + id + + + NSPopUpButton + NSMenu + NSSearchField + NSWindow + + + IBProjectSource + ResultWindow.h + + + + ResultWindow + ResultWindowBase + + id + id + id + id + id + id + id + id + id + id + + + NSView + id + NSSegmentedControl + NSView + NSView + MatchesView + NSSegmentedControl + NSView + PyDupeGuru + NSTextField + + + IBUserSource + + + + + ResultWindowBase + NSWindowController + + id + id + id + id + id + id + id + id + id + + + id + NSSegmentedControl + MatchesView + NSSegmentedControl + PyDupeGuruBase + NSTextField + + + + + SUUpdater + NSObject + + IBUserSource + + + + + + + NSActionCell + NSCell + + IBFrameworkSource + AppKit.framework/Headers/NSActionCell.h + + + + NSApplication + NSResponder + + IBFrameworkSource + AppKit.framework/Headers/NSApplication.h + + + + NSApplication + + IBFrameworkSource + AppKit.framework/Headers/NSApplicationScripting.h + + + + NSApplication + + IBFrameworkSource + AppKit.framework/Headers/NSColorPanel.h + + + + NSApplication + + IBFrameworkSource + AppKit.framework/Headers/NSHelpManager.h + + + + NSApplication + + IBFrameworkSource + AppKit.framework/Headers/NSPageLayout.h + + + + NSApplication + + IBFrameworkSource + AppKit.framework/Headers/NSUserInterfaceItemSearching.h + + + + NSButton + NSControl + + IBFrameworkSource + AppKit.framework/Headers/NSButton.h + + + + NSButtonCell + NSActionCell + + IBFrameworkSource + AppKit.framework/Headers/NSButtonCell.h + + + + NSCell + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSCell.h + + + + NSControl + NSView + + IBFrameworkSource + AppKit.framework/Headers/NSControl.h + + + + NSController + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSController.h + + + + NSFormatter + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSFormatter.h + + + + NSMenu + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSMenu.h + + + + NSMenuItem + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSMenuItem.h + + + + NSMenuItemCell + NSButtonCell + + IBFrameworkSource + AppKit.framework/Headers/NSMenuItemCell.h + + + + NSMovieView + NSView + + IBFrameworkSource + AppKit.framework/Headers/NSMovieView.h + + + + NSNumberFormatter + NSFormatter + + IBFrameworkSource + Foundation.framework/Headers/NSNumberFormatter.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSAccessibility.h + + + + NSObject + + + + NSObject + + + + NSObject + + + + NSObject + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSDictionaryController.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSDragging.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSFontManager.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSFontPanel.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSKeyValueBinding.h + + + + NSObject + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSNibLoading.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSOutlineView.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSPasteboard.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSSavePanel.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSTableView.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSToolbarItem.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSView.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSArchiver.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSClassDescription.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSError.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSFileManager.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSKeyValueCoding.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSKeyValueObserving.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSKeyedArchiver.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSObject.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSObjectScripting.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSPortCoder.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSRunLoop.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSScriptClassDescription.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSScriptKeyValueCoding.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSScriptObjectSpecifiers.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSScriptWhoseTests.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSThread.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSURL.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSURLConnection.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSURLDownload.h + + + + NSObject + + IBFrameworkSource + Sparkle.framework/Headers/SUAppcast.h + + + + NSObject + + IBFrameworkSource + Sparkle.framework/Headers/SUUpdater.h + + + + NSOutlineView + NSTableView + + + + NSPopUpButton + NSButton + + IBFrameworkSource + AppKit.framework/Headers/NSPopUpButton.h + + + + NSPopUpButtonCell + NSMenuItemCell + + IBFrameworkSource + AppKit.framework/Headers/NSPopUpButtonCell.h + + + + NSResponder + + IBFrameworkSource + AppKit.framework/Headers/NSInterfaceStyle.h + + + + NSResponder + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSResponder.h + + + + NSScrollView + NSView + + IBFrameworkSource + AppKit.framework/Headers/NSScrollView.h + + + + NSScroller + NSControl + + IBFrameworkSource + AppKit.framework/Headers/NSScroller.h + + + + NSSearchField + NSTextField + + IBFrameworkSource + AppKit.framework/Headers/NSSearchField.h + + + + NSSearchFieldCell + NSTextFieldCell + + IBFrameworkSource + AppKit.framework/Headers/NSSearchFieldCell.h + + + + NSSegmentedCell + NSActionCell + + IBFrameworkSource + AppKit.framework/Headers/NSSegmentedCell.h + + + + NSSegmentedControl + NSControl + + IBFrameworkSource + AppKit.framework/Headers/NSSegmentedControl.h + + + + NSSlider + NSControl + + IBFrameworkSource + AppKit.framework/Headers/NSSlider.h + + + + NSSliderCell + NSActionCell + + IBFrameworkSource + AppKit.framework/Headers/NSSliderCell.h + + + + NSTableColumn + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSTableColumn.h + + + + NSTableHeaderView + NSView + + IBFrameworkSource + AppKit.framework/Headers/NSTableHeaderView.h + + + + NSTableView + NSControl + + + + NSText + NSView + + IBFrameworkSource + AppKit.framework/Headers/NSText.h + + + + NSTextField + NSControl + + IBFrameworkSource + AppKit.framework/Headers/NSTextField.h + + + + NSTextFieldCell + NSActionCell + + IBFrameworkSource + AppKit.framework/Headers/NSTextFieldCell.h + + + + NSToolbar + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSToolbar.h + + + + NSToolbarItem + NSObject + + + + NSUserDefaultsController + NSController + + IBFrameworkSource + AppKit.framework/Headers/NSUserDefaultsController.h + + + + NSView + + IBFrameworkSource + AppKit.framework/Headers/NSClipView.h + + + + NSView + + + + NSView + + IBFrameworkSource + AppKit.framework/Headers/NSRulerView.h + + + + NSView + NSResponder + + + + NSWindow + + IBFrameworkSource + AppKit.framework/Headers/NSDrawer.h + + + + NSWindow + NSResponder + + IBFrameworkSource + AppKit.framework/Headers/NSWindow.h + + + + NSWindow + + IBFrameworkSource + AppKit.framework/Headers/NSWindowScripting.h + + + + NSWindowController + NSResponder + + showWindow: + id + + + IBFrameworkSource + AppKit.framework/Headers/NSWindowController.h + + + + SUUpdater + NSObject + + checkForUpdates: + id + + + delegate + id + + + + + + 0 + + com.apple.InterfaceBuilder.CocoaPlugin.macosx + + + + com.apple.InterfaceBuilder.CocoaPlugin.macosx + + + YES + ../dupeguru.xcodeproj + 3 + + From 89bce95c27142a88c36202c4e02388c5ed316877 Mon Sep 17 00:00:00 2001 From: hsoft Date: Sun, 25 Oct 2009 11:17:13 +0000 Subject: [PATCH 214/275] dgse cocoa: dropped tiger support. Added toolbar creation in the MainMenu nib. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40220 --- .../English.lproj/Details.nib/classes.nib | 18 - me/cocoa/English.lproj/Details.nib/info.nib | 16 - .../Details.nib/keyedobjects.nib | Bin 6122 -> 0 bytes .../English.lproj/Directories.nib/classes.nib | 64 - .../English.lproj/Directories.nib/info.nib | 20 - .../Directories.nib/keyedobjects.nib | Bin 9698 -> 0 bytes me/cocoa/English.lproj/InfoPlist.strings | Bin 204 -> 0 bytes .../English.lproj/MainMenu.nib/classes.nib | 257 - me/cocoa/English.lproj/MainMenu.nib/info.nib | 20 - .../MainMenu.nib/keyedobjects.nib | Bin 57645 -> 0 bytes me/cocoa/Info.plist | 2 + me/cocoa/ResultWindow.h | 1 - me/cocoa/ResultWindow.m | 14 - me/cocoa/dupeguru.xcodeproj/project.pbxproj | 116 +- me/cocoa/xib/MainMenu.xib | 6950 +++++++++++++++++ 15 files changed, 6975 insertions(+), 503 deletions(-) delete mode 100644 me/cocoa/English.lproj/Details.nib/classes.nib delete mode 100644 me/cocoa/English.lproj/Details.nib/info.nib delete mode 100644 me/cocoa/English.lproj/Details.nib/keyedobjects.nib delete mode 100644 me/cocoa/English.lproj/Directories.nib/classes.nib delete mode 100644 me/cocoa/English.lproj/Directories.nib/info.nib delete mode 100644 me/cocoa/English.lproj/Directories.nib/keyedobjects.nib delete mode 100644 me/cocoa/English.lproj/InfoPlist.strings delete mode 100644 me/cocoa/English.lproj/MainMenu.nib/classes.nib delete mode 100644 me/cocoa/English.lproj/MainMenu.nib/info.nib delete mode 100644 me/cocoa/English.lproj/MainMenu.nib/keyedobjects.nib create mode 100644 me/cocoa/xib/MainMenu.xib diff --git a/me/cocoa/English.lproj/Details.nib/classes.nib b/me/cocoa/English.lproj/Details.nib/classes.nib deleted file mode 100644 index e1b7cb92..00000000 --- a/me/cocoa/English.lproj/Details.nib/classes.nib +++ /dev/null @@ -1,18 +0,0 @@ -{ - IBClasses = ( - { - CLASS = DetailsPanel; - LANGUAGE = ObjC; - OUTLETS = {detailsTable = NSTableView; }; - SUPERCLASS = NSWindowController; - }, - {CLASS = FirstResponder; LANGUAGE = ObjC; SUPERCLASS = NSObject; }, - { - CLASS = TableView; - LANGUAGE = ObjC; - OUTLETS = {py = PyApp; }; - SUPERCLASS = NSTableView; - } - ); - IBVersion = 1; -} \ No newline at end of file diff --git a/me/cocoa/English.lproj/Details.nib/info.nib b/me/cocoa/English.lproj/Details.nib/info.nib deleted file mode 100644 index 3f14ee77..00000000 --- a/me/cocoa/English.lproj/Details.nib/info.nib +++ /dev/null @@ -1,16 +0,0 @@ - - - - - IBDocumentLocation - 432 54 356 240 0 0 1024 746 - IBFramework Version - 443.0 - IBOpenObjects - - 5 - - IBSystem Version - 8I127 - - diff --git a/me/cocoa/English.lproj/Details.nib/keyedobjects.nib b/me/cocoa/English.lproj/Details.nib/keyedobjects.nib deleted file mode 100644 index e50621e9b70e67ef9ab1837bad20b6e5a12a053a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6122 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 - - - - IBClasses - - - CLASS - FirstResponder - LANGUAGE - ObjC - SUPERCLASS - NSObject - - - ACTIONS - - addiTunes - id - askForDirectory - id - changeDirectoryState - id - popupAddDirectoryMenu - id - removeSelectedDirectory - id - toggleVisible - id - - CLASS - DirectoryPanel - LANGUAGE - ObjC - OUTLETS - - addButtonPopUp - NSPopUpButton - directories - NSOutlineView - removeButton - NSButton - - SUPERCLASS - DirectoryPanelBase - - - CLASS - OutlineView - LANGUAGE - ObjC - OUTLETS - - py - PyApp - - SUPERCLASS - NSOutlineView - - - IBVersion - 1 - - diff --git a/me/cocoa/English.lproj/Directories.nib/info.nib b/me/cocoa/English.lproj/Directories.nib/info.nib deleted file mode 100644 index 77f19ce7..00000000 --- a/me/cocoa/English.lproj/Directories.nib/info.nib +++ /dev/null @@ -1,20 +0,0 @@ - - - - - IBFramework Version - 629 - IBLastKnownRelativeProjectPath - ../../dupeguru.xcodeproj - IBOldestOS - 5 - IBOpenObjects - - 5 - - IBSystem Version - 9B18 - targetFramework - IBCocoaFramework - - diff --git a/me/cocoa/English.lproj/Directories.nib/keyedobjects.nib b/me/cocoa/English.lproj/Directories.nib/keyedobjects.nib deleted file mode 100644 index c541159e828cdd61d88e04c1cd76746986f25db6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9698 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 diff --git a/me/cocoa/English.lproj/InfoPlist.strings b/me/cocoa/English.lproj/InfoPlist.strings deleted file mode 100644 index b0430081705b43f55219aa5ab386484da1f3f892..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 204 zcmW-bOA5k35Cv=PDT2!&M%;ffpr(+TGdEs?=PZk+ZTdy;YuvTm#?IsUD@Dfr?4!S!+@j+&G(iVdFkCH)E@- sWNyM$M`QkueaM)Z)8}np$TiZrR3ZKUPa59uc!XWaKyA#(n&_JH1B0<7GXMYp diff --git a/me/cocoa/English.lproj/MainMenu.nib/classes.nib b/me/cocoa/English.lproj/MainMenu.nib/classes.nib deleted file mode 100644 index b3d34484..00000000 --- a/me/cocoa/English.lproj/MainMenu.nib/classes.nib +++ /dev/null @@ -1,257 +0,0 @@ - - - - - IBClasses - - - CLASS - NSSegmentedControl - LANGUAGE - ObjC - SUPERCLASS - NSControl - - - ACTIONS - - openWebsite - id - popupPresets - id - toggleDirectories - id - unlockApp - id - usePreset - id - - CLASS - AppDelegate - LANGUAGE - ObjC - OUTLETS - - defaultsController - NSUserDefaultsController - presetsButton - NSButton - presetsPopup - NSPopUpButton - py - PyDupeGuru - recentDirectories - RecentDirectories - result - ResultWindow - unlockMenuItem - NSMenuItem - - SUPERCLASS - AppDelegateBase - - - CLASS - PyApp - LANGUAGE - ObjC - SUPERCLASS - NSObject - - - CLASS - MatchesView - LANGUAGE - ObjC - SUPERCLASS - OutlineView - - - CLASS - PyDupeGuruBase - LANGUAGE - ObjC - SUPERCLASS - PyApp - - - CLASS - PyDupeGuru - LANGUAGE - ObjC - SUPERCLASS - PyDupeGuruBase - - - ACTIONS - - changeDelta - id - changePowerMarker - id - clearIgnoreList - id - collapseAll - id - copyMarked - id - deleteMarked - id - expandAll - id - exportToXHTML - id - filter - id - ignoreSelected - id - markAll - id - markInvert - id - markNone - id - markSelected - id - markToggle - id - moveMarked - id - openSelected - id - refresh - id - removeDeadTracks - id - removeMarked - id - removeSelected - id - renameSelected - id - resetColumnsToDefault - id - revealSelected - id - showPreferencesPanel - id - startDuplicateScan - id - switchSelected - id - toggleColumn - id - toggleDelta - id - toggleDetailsPanel - id - togglePowerMarker - id - - CLASS - ResultWindow - LANGUAGE - ObjC - OUTLETS - - actionMenu - NSPopUpButton - actionMenuView - NSView - app - id - columnsMenu - NSMenu - deltaSwitch - NSSegmentedControl - deltaSwitchView - NSView - filterField - NSSearchField - filterFieldView - NSView - matches - MatchesView - pmSwitch - NSSegmentedControl - pmSwitchView - NSView - preferencesPanel - NSWindow - py - PyDupeGuru - stats - NSTextField - - SUPERCLASS - ResultWindowBase - - - CLASS - FirstResponder - LANGUAGE - ObjC - SUPERCLASS - NSObject - - - ACTIONS - - checkForUpdates - id - - CLASS - SUUpdater - LANGUAGE - ObjC - SUPERCLASS - NSObject - - - ACTIONS - - clearMenu - id - menuClick - id - - CLASS - RecentDirectories - LANGUAGE - ObjC - OUTLETS - - delegate - id - menu - NSMenu - - SUPERCLASS - NSObject - - - CLASS - ResultWindowBase - LANGUAGE - ObjC - SUPERCLASS - NSWindowController - - - CLASS - OutlineView - LANGUAGE - ObjC - OUTLETS - - py - PyApp - - SUPERCLASS - NSOutlineView - - - IBVersion - 1 - - diff --git a/me/cocoa/English.lproj/MainMenu.nib/info.nib b/me/cocoa/English.lproj/MainMenu.nib/info.nib deleted file mode 100644 index 7cd1eddb..00000000 --- a/me/cocoa/English.lproj/MainMenu.nib/info.nib +++ /dev/null @@ -1,20 +0,0 @@ - - - - - IBFramework Version - 629 - IBLastKnownRelativeProjectPath - ../../dupeguru.xcodeproj - IBOldestOS - 5 - IBOpenObjects - - 598 - - IBSystem Version - 9E17 - targetFramework - IBCocoaFramework - - diff --git a/me/cocoa/English.lproj/MainMenu.nib/keyedobjects.nib b/me/cocoa/English.lproj/MainMenu.nib/keyedobjects.nib deleted file mode 100644 index 649ce170df6fbe0c363a3f9127126dc86a6a10ef..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 57645 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 diff --git a/me/cocoa/Info.plist b/me/cocoa/Info.plist index e96f2561..f3e12846 100644 --- a/me/cocoa/Info.plist +++ b/me/cocoa/Info.plist @@ -28,6 +28,8 @@ MainMenu NSPrincipalClass NSApplication + NSHumanReadableCopyright + © Hardcoded Software, 2009 SUFeedURL http://www.hardcoded.net/updates/dupeguru_me.appcast SUPublicDSAKeyFile diff --git a/me/cocoa/ResultWindow.h b/me/cocoa/ResultWindow.h index d46b40b3..cbdaaeae 100644 --- a/me/cocoa/ResultWindow.h +++ b/me/cocoa/ResultWindow.h @@ -14,7 +14,6 @@ http://www.hardcoded.net/licenses/hs_license @interface ResultWindow : ResultWindowBase { - IBOutlet NSPopUpButton *actionMenu; IBOutlet NSMenu *columnsMenu; IBOutlet NSSearchField *filterField; IBOutlet NSWindow *preferencesPanel; diff --git a/me/cocoa/ResultWindow.m b/me/cocoa/ResultWindow.m index 2f80b33c..5aa0f34c 100644 --- a/me/cocoa/ResultWindow.m +++ b/me/cocoa/ResultWindow.m @@ -29,23 +29,9 @@ http://www.hardcoded.net/licenses/hs_license [py setDisplayDeltaValues:b2n(_displayDelta)]; [matches setTarget:self]; [matches setDoubleAction:@selector(openSelected:)]; - [[actionMenu itemAtIndex:0] setImage:[NSImage imageNamed: @"gear"]]; [self initResultColumns]; [self refreshStats]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(resultsMarkingChanged:) name:ResultsMarkingChangedNotification object:nil]; - - NSToolbar *t = [[[NSToolbar alloc] initWithIdentifier:@"ResultWindowToolbar"] autorelease]; - [t setAllowsUserCustomization:YES]; - [t setAutosavesConfiguration:YES]; - [t setDisplayMode:NSToolbarDisplayModeIconAndLabel]; - [t setDelegate:self]; - [[self window] setToolbar:t]; -} - -/* Overrides */ -- (NSString *)logoImageName -{ - return @"dgme_logo_32"; } /* Actions */ diff --git a/me/cocoa/dupeguru.xcodeproj/project.pbxproj b/me/cocoa/dupeguru.xcodeproj/project.pbxproj index 52be0bd5..940f8094 100644 --- a/me/cocoa/dupeguru.xcodeproj/project.pbxproj +++ b/me/cocoa/dupeguru.xcodeproj/project.pbxproj @@ -19,8 +19,6 @@ /* End PBXAppleScriptBuildPhase section */ /* Begin PBXBuildFile section */ - 8D11072A0486CEB800E47090 /* MainMenu.nib in Resources */ = {isa = PBXBuildFile; fileRef = 29B97318FDCFA39411CA2CEA /* MainMenu.nib */; }; - 8D11072B0486CEB800E47090 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 089C165CFE840E0CC02AAC07 /* InfoPlist.strings */; }; 8D11072D0486CEB800E47090 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 29B97316FDCFA39411CA2CEA /* main.m */; settings = {ATTRIBUTES = (); }; }; 8D11072F0486CEB800E47090 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */; }; CE073F6309CAE1A3005C1D2F /* dupeguru_me_help in Resources */ = {isa = PBXBuildFile; fileRef = CE073F5409CAE1A3005C1D2F /* dupeguru_me_help */; }; @@ -29,7 +27,9 @@ CE381C9609914ACE003581CE /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = CE381C9409914ACE003581CE /* AppDelegate.m */; }; CE381C9C09914ADF003581CE /* ResultWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = CE381C9A09914ADF003581CE /* ResultWindow.m */; }; CE381D0509915304003581CE /* dg_cocoa.plugin in Resources */ = {isa = PBXBuildFile; fileRef = CE381CF509915304003581CE /* dg_cocoa.plugin */; }; - CE3AA46709DB207900DB3A21 /* Directories.nib in Resources */ = {isa = PBXBuildFile; fileRef = CE3AA46509DB207900DB3A21 /* Directories.nib */; }; + CE3FBDD31094637800B72D77 /* DetailsPanel.xib in Resources */ = {isa = PBXBuildFile; fileRef = CE3FBDD11094637800B72D77 /* DetailsPanel.xib */; }; + CE3FBDD41094637800B72D77 /* DirectoryPanel.xib in Resources */ = {isa = PBXBuildFile; fileRef = CE3FBDD21094637800B72D77 /* DirectoryPanel.xib */; }; + CE3FBDD7109463AB00B72D77 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = CE3FBDD6109463AB00B72D77 /* MainMenu.xib */; }; CE49DEF60FDFEB810098617B /* BRSingleLineFormatter.m in Sources */ = {isa = PBXBuildFile; fileRef = CE49DEF30FDFEB810098617B /* BRSingleLineFormatter.m */; }; CE49DEF70FDFEB810098617B /* NSCharacterSet_Extensions.m in Sources */ = {isa = PBXBuildFile; fileRef = CE49DEF50FDFEB810098617B /* NSCharacterSet_Extensions.m */; }; CE515DF30FC6C12E00EC695D /* Dialogs.m in Sources */ = {isa = PBXBuildFile; fileRef = CE515DE10FC6C12E00EC695D /* Dialogs.m */; }; @@ -52,12 +52,10 @@ CE6E0E9F1054EB97008D9390 /* dsa_pub.pem in Resources */ = {isa = PBXBuildFile; fileRef = CE6E0E9E1054EB97008D9390 /* dsa_pub.pem */; }; CE848A1909DD85810004CB44 /* Consts.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = CE848A1809DD85810004CB44 /* Consts.h */; }; CEA7D2C50FDFED340037CD8C /* dgme_logo_32.png in Resources */ = {isa = PBXBuildFile; fileRef = CEA7D2C40FDFED340037CD8C /* dgme_logo_32.png */; }; - CECA899909DB12CA00A3D774 /* Details.nib in Resources */ = {isa = PBXBuildFile; fileRef = CECA899709DB12CA00A3D774 /* Details.nib */; }; CECA899C09DB132E00A3D774 /* DetailsPanel.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = CECA899A09DB132E00A3D774 /* DetailsPanel.h */; }; CECA899D09DB132E00A3D774 /* DetailsPanel.m in Sources */ = {isa = PBXBuildFile; fileRef = CECA899B09DB132E00A3D774 /* DetailsPanel.m */; }; CED2A6880A05102700AC4C3F /* power_marker32.png in Resources */ = {isa = PBXBuildFile; fileRef = CED2A6870A05102600AC4C3F /* power_marker32.png */; }; CEEB135209C837A2004D2330 /* dupeguru.icns in Resources */ = {isa = PBXBuildFile; fileRef = CEEB135109C837A2004D2330 /* dupeguru.icns */; }; - CEF7823809C8AA0200EF38FF /* gear.png in Resources */ = {isa = PBXBuildFile; fileRef = CEF7823709C8AA0200EF38FF /* gear.png */; }; CEFC294609C89E3D00D9F998 /* folder32.png in Resources */ = {isa = PBXBuildFile; fileRef = CEFC294509C89E3D00D9F998 /* folder32.png */; }; CEFC295509C89FF200D9F998 /* details32.png in Resources */ = {isa = PBXBuildFile; fileRef = CEFC295309C89FF200D9F998 /* details32.png */; }; CEFC295609C89FF200D9F998 /* preferences32.png in Resources */ = {isa = PBXBuildFile; fileRef = CEFC295409C89FF200D9F998 /* preferences32.png */; }; @@ -79,11 +77,9 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 089C165DFE840E0CC02AAC07 /* English */ = {isa = PBXFileReference; fileEncoding = 10; lastKnownFileType = text.plist.strings; name = English; path = English.lproj/InfoPlist.strings; sourceTree = ""; }; 1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = /System/Library/Frameworks/Cocoa.framework; sourceTree = ""; }; 13E42FB307B3F0F600E4EEF1 /* CoreData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreData.framework; path = /System/Library/Frameworks/CoreData.framework; sourceTree = ""; }; 29B97316FDCFA39411CA2CEA /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = SOURCE_ROOT; }; - 29B97319FDCFA39411CA2CEA /* English */ = {isa = PBXFileReference; lastKnownFileType = wrapper.nib; name = English; path = English.lproj/MainMenu.nib; sourceTree = ""; }; 29B97324FDCFA39411CA2CEA /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = /System/Library/Frameworks/AppKit.framework; sourceTree = ""; }; 29B97325FDCFA39411CA2CEA /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = /System/Library/Frameworks/Foundation.framework; sourceTree = ""; }; 8D1107310486CEB800E47090 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = SOURCE_ROOT; }; @@ -95,7 +91,9 @@ CE381C9A09914ADF003581CE /* ResultWindow.m */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.objc; path = ResultWindow.m; sourceTree = SOURCE_ROOT; }; CE381C9B09914ADF003581CE /* ResultWindow.h */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.h; path = ResultWindow.h; sourceTree = SOURCE_ROOT; }; CE381CF509915304003581CE /* dg_cocoa.plugin */ = {isa = PBXFileReference; lastKnownFileType = folder; name = dg_cocoa.plugin; path = py/dist/dg_cocoa.plugin; sourceTree = SOURCE_ROOT; }; - CE3AA46609DB207900DB3A21 /* English */ = {isa = PBXFileReference; lastKnownFileType = wrapper.nib; name = English; path = English.lproj/Directories.nib; sourceTree = ""; }; + CE3FBDD11094637800B72D77 /* DetailsPanel.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = DetailsPanel.xib; sourceTree = ""; }; + CE3FBDD21094637800B72D77 /* DirectoryPanel.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = DirectoryPanel.xib; sourceTree = ""; }; + CE3FBDD6109463AB00B72D77 /* MainMenu.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = MainMenu.xib; path = ../../xib/MainMenu.xib; sourceTree = ""; }; CE49DEF20FDFEB810098617B /* BRSingleLineFormatter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BRSingleLineFormatter.h; path = cocoalib/brsinglelineformatter/BRSingleLineFormatter.h; sourceTree = SOURCE_ROOT; }; CE49DEF30FDFEB810098617B /* BRSingleLineFormatter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BRSingleLineFormatter.m; path = cocoalib/brsinglelineformatter/BRSingleLineFormatter.m; sourceTree = SOURCE_ROOT; }; CE49DEF40FDFEB810098617B /* NSCharacterSet_Extensions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = NSCharacterSet_Extensions.h; path = cocoalib/brsinglelineformatter/NSCharacterSet_Extensions.h; sourceTree = SOURCE_ROOT; }; @@ -137,12 +135,10 @@ CE6E0E9E1054EB97008D9390 /* dsa_pub.pem */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = dsa_pub.pem; path = dgbase/dsa_pub.pem; sourceTree = ""; }; CE848A1809DD85810004CB44 /* Consts.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Consts.h; sourceTree = ""; }; CEA7D2C40FDFED340037CD8C /* dgme_logo_32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = dgme_logo_32.png; path = images/dgme_logo_32.png; sourceTree = SOURCE_ROOT; }; - CECA899809DB12CA00A3D774 /* English */ = {isa = PBXFileReference; lastKnownFileType = wrapper.nib; name = English; path = English.lproj/Details.nib; sourceTree = ""; }; CECA899A09DB132E00A3D774 /* DetailsPanel.h */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.h; path = DetailsPanel.h; sourceTree = ""; }; CECA899B09DB132E00A3D774 /* DetailsPanel.m */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.objc; path = DetailsPanel.m; sourceTree = ""; }; CED2A6870A05102600AC4C3F /* power_marker32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = power_marker32.png; path = images/power_marker32.png; sourceTree = SOURCE_ROOT; }; CEEB135109C837A2004D2330 /* dupeguru.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; path = dupeguru.icns; sourceTree = ""; }; - CEF7823709C8AA0200EF38FF /* gear.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = gear.png; path = images/gear.png; sourceTree = ""; }; CEFC294509C89E3D00D9F998 /* folder32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = folder32.png; path = images/folder32.png; sourceTree = SOURCE_ROOT; }; CEFC295309C89FF200D9F998 /* details32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = details32.png; path = images/details32.png; sourceTree = SOURCE_ROOT; }; CEFC295409C89FF200D9F998 /* preferences32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = preferences32.png; path = images/preferences32.png; sourceTree = SOURCE_ROOT; }; @@ -231,16 +227,13 @@ 29B97317FDCFA39411CA2CEA /* Resources */ = { isa = PBXGroup; children = ( + CE3FBDD01094637800B72D77 /* xib */, CE073F5409CAE1A3005C1D2F /* dupeguru_me_help */, CE381CF509915304003581CE /* dg_cocoa.plugin */, CEFC294309C89E0000D9F998 /* images */, CEEB135109C837A2004D2330 /* dupeguru.icns */, 8D1107310486CEB800E47090 /* Info.plist */, - 089C165CFE840E0CC02AAC07 /* InfoPlist.strings */, CE6E0E9E1054EB97008D9390 /* dsa_pub.pem */, - CECA899709DB12CA00A3D774 /* Details.nib */, - CE3AA46509DB207900DB3A21 /* Directories.nib */, - 29B97318FDCFA39411CA2CEA /* MainMenu.nib */, ); name = Resources; sourceTree = ""; @@ -254,6 +247,17 @@ name = Frameworks; sourceTree = ""; }; + CE3FBDD01094637800B72D77 /* xib */ = { + isa = PBXGroup; + children = ( + CE3FBDD6109463AB00B72D77 /* MainMenu.xib */, + CE3FBDD11094637800B72D77 /* DetailsPanel.xib */, + CE3FBDD21094637800B72D77 /* DirectoryPanel.xib */, + ); + name = xib; + path = dgbase/xib; + sourceTree = ""; + }; CE49DEF10FDFEB810098617B /* brsinglelineformatter */ = { isa = PBXGroup; children = ( @@ -318,7 +322,6 @@ children = ( CEA7D2C40FDFED340037CD8C /* dgme_logo_32.png */, CED2A6870A05102600AC4C3F /* power_marker32.png */, - CEF7823709C8AA0200EF38FF /* gear.png */, CEFC295309C89FF200D9F998 /* details32.png */, CEFC295409C89FF200D9F998 /* preferences32.png */, CEFC294509C89E3D00D9F998 /* folder32.png */, @@ -371,23 +374,21 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 8D11072A0486CEB800E47090 /* MainMenu.nib in Resources */, - 8D11072B0486CEB800E47090 /* InfoPlist.strings in Resources */, CE381D0509915304003581CE /* dg_cocoa.plugin in Resources */, CE073F6309CAE1A3005C1D2F /* dupeguru_me_help in Resources */, CEEB135209C837A2004D2330 /* dupeguru.icns in Resources */, CEFC294609C89E3D00D9F998 /* folder32.png in Resources */, CEFC295509C89FF200D9F998 /* details32.png in Resources */, CEFC295609C89FF200D9F998 /* preferences32.png in Resources */, - CEF7823809C8AA0200EF38FF /* gear.png in Resources */, - CECA899909DB12CA00A3D774 /* Details.nib in Resources */, - CE3AA46709DB207900DB3A21 /* Directories.nib in Resources */, CED2A6880A05102700AC4C3F /* power_marker32.png in Resources */, CE515E020FC6C13E00EC695D /* ErrorReportWindow.xib in Resources */, CE515E030FC6C13E00EC695D /* progress.nib in Resources */, CE515E040FC6C13E00EC695D /* registration.nib in Resources */, CEA7D2C50FDFED340037CD8C /* dgme_logo_32.png in Resources */, CE6E0E9F1054EB97008D9390 /* dsa_pub.pem in Resources */, + CE3FBDD31094637800B72D77 /* DetailsPanel.xib in Resources */, + CE3FBDD41094637800B72D77 /* DirectoryPanel.xib in Resources */, + CE3FBDD7109463AB00B72D77 /* MainMenu.xib in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -424,30 +425,6 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXVariantGroup section */ - 089C165CFE840E0CC02AAC07 /* InfoPlist.strings */ = { - isa = PBXVariantGroup; - children = ( - 089C165DFE840E0CC02AAC07 /* English */, - ); - name = InfoPlist.strings; - sourceTree = ""; - }; - 29B97318FDCFA39411CA2CEA /* MainMenu.nib */ = { - isa = PBXVariantGroup; - children = ( - 29B97319FDCFA39411CA2CEA /* English */, - ); - name = MainMenu.nib; - sourceTree = SOURCE_ROOT; - }; - CE3AA46509DB207900DB3A21 /* Directories.nib */ = { - isa = PBXVariantGroup; - children = ( - CE3AA46609DB207900DB3A21 /* English */, - ); - name = Directories.nib; - sourceTree = ""; - }; CE515DFC0FC6C13E00EC695D /* ErrorReportWindow.xib */ = { isa = PBXVariantGroup; children = ( @@ -472,38 +449,9 @@ name = registration.nib; sourceTree = SOURCE_ROOT; }; - CECA899709DB12CA00A3D774 /* Details.nib */ = { - isa = PBXVariantGroup; - children = ( - CECA899809DB12CA00A3D774 /* English */, - ); - name = Details.nib; - sourceTree = ""; - }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ - C01FCF4B08A954540054247B /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - COPY_PHASE_STRIP = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(FRAMEWORK_SEARCH_PATHS)", - "$(SRCROOT)/../../../cocoalib/build/Release", - "\"$(SRCROOT)/../../base/cocoa/build/Release\"", - ); - GCC_DYNAMIC_NO_PIC = NO; - GCC_ENABLE_FIX_AND_CONTINUE = YES; - GCC_MODEL_TUNING = G5; - GCC_OPTIMIZATION_LEVEL = 0; - INFOPLIST_FILE = Info.plist; - INSTALL_PATH = "$(HOME)/Applications"; - PRODUCT_NAME = dupeGuru; - WRAPPER_EXTENSION = app; - ZERO_LINK = YES; - }; - name = Debug; - }; C01FCF4C08A954540054247B /* Release */ = { isa = XCBuildConfiguration; buildSettings = { @@ -521,32 +469,16 @@ }; name = Release; }; - C01FCF4F08A954540054247B /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - GCC_C_LANGUAGE_STANDARD = c99; - GCC_WARN_ABOUT_RETURN_TYPE = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.4; - PREBINDING = NO; - SDKROOT = "$(DEVELOPER_SDK_DIR)/MacOSX10.4u.sdk"; - }; - name = Debug; - }; C01FCF5008A954540054247B /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ARCHS = "$(ARCHS_STANDARD_32_BIT_PRE_XCODE_3_1)"; ARCHS_STANDARD_32_BIT_PRE_XCODE_3_1 = "ppc i386"; - COPY_PHASE_STRIP = NO; GCC_C_LANGUAGE_STANDARD = c99; - GCC_VERSION = 4.0; GCC_WARN_ABOUT_RETURN_TYPE = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.4; - PREBINDING = NO; - SDKROOT = "$(DEVELOPER_SDK_DIR)/MacOSX10.4u.sdk"; - STRIP_INSTALLED_PRODUCT = NO; + MACOSX_DEPLOYMENT_TARGET = 10.5; + SDKROOT = "$(DEVELOPER_SDK_DIR)/MacOSX10.5.sdk"; }; name = Release; }; @@ -556,7 +488,6 @@ C01FCF4A08A954540054247B /* Build configuration list for PBXNativeTarget "dupeguru" */ = { isa = XCConfigurationList; buildConfigurations = ( - C01FCF4B08A954540054247B /* Debug */, C01FCF4C08A954540054247B /* Release */, ); defaultConfigurationIsVisible = 0; @@ -565,7 +496,6 @@ C01FCF4E08A954540054247B /* Build configuration list for PBXProject "dupeguru" */ = { isa = XCConfigurationList; buildConfigurations = ( - C01FCF4F08A954540054247B /* Debug */, C01FCF5008A954540054247B /* Release */, ); defaultConfigurationIsVisible = 0; diff --git a/me/cocoa/xib/MainMenu.xib b/me/cocoa/xib/MainMenu.xib new file mode 100644 index 00000000..b423cda2 --- /dev/null +++ b/me/cocoa/xib/MainMenu.xib @@ -0,0 +1,6950 @@ + + + + 1050 + 10B504 + 740 + 1038.2 + 437.00 + + com.apple.InterfaceBuilder.CocoaPlugin + 740 + + + + + + + + com.apple.InterfaceBuilder.CocoaPlugin + + + + + NSApplication + + + FirstResponder + + + NSApplication + + + 15 + 2 + {{47, 310}, {557, 400}} + 1886912512 + dupeGuru Music Edition + NSWindow + + + B570361B-ACC8-474D-BCD9-EE4A4EF88C48 + + + YES + YES + YES + YES + 1 + 1 + + + + 00156499-01C0-42C8-8A93-179A727D7695 + + Filter + Filter + + + + 258 + {{0, 14}, {81, 22}} + YES + + 343014976 + 268436480 + + + LucidaGrande + 13 + 1044 + + Filter + + YES + 1 + + 6 + System + textBackgroundColor + + 3 + MQA + + + + 6 + System + controlTextColor + + 3 + MAA + + + + 130560 + 0 + search + + _searchFieldSearch: + + 138690815 + 0 + + 400 + 75 + + + 130560 + 0 + clear + + + cancel + + + + + _searchFieldCancel: + + 138690815 + 0 + + 400 + 75 + + 10 + YES + + + + + + {81, 22} + {9999, 22} + YES + YES + 0 + YES + 0 + + + + 2EE12929-476B-49AF-846C-E3C1498BA2CC + + Start Scanning + Start Scanning + + + + NSImage + dgme_logo_32 + + + + {0, 0} + {0, 0} + YES + YES + -1 + YES + 0 + + + + 3017E624-E1D3-4BAD-9B9E-B7FFD6C99F00 + + Delta Values + Delta Values + + + + 256 + {{4, 14}, {67, 24}} + YES + + 67239424 + 0 + + LucidaGrande + 11 + 3100 + + + + + 30 + Off + 2 + + + 30 + On + 1 + 2 + + + 1 + + + + + + {67, 24} + {67, 24} + YES + YES + 0 + YES + 0 + + + + 3B3D4F13-9057-4C55-9748-ACDC8A39368D + + Action + Action + + + + 256 + {{0, 14}, {58, 26}} + YES + + -2076049856 + 2048 + + + 109199615 + 1 + + + + + + 400 + 75 + + + YES + IA + + 1048576 + 2147483647 + 1 + + NSImage + NSActionTemplate + + + NSImage + NSMenuCheckmark + + + NSImage + NSMenuMixedState + + _popUpItemAction: + + + YES + + + OtherViews + + + + + + Send Marked to Trash + + 2147483647 + + + _popUpItemAction: + + + + + Move Marked to... + + 2147483647 + + + _popUpItemAction: + + + + + Copy Marked to... + + 2147483647 + + + _popUpItemAction: + + + + + Remove Marked from Results + + 2147483647 + + + _popUpItemAction: + + + + + YES + YES + + + 1048576 + 2147483647 + + + _popUpItemAction: + + + + + Remove Selected from Results + + 2147483647 + + + _popUpItemAction: + + + + + Add Selected to Ignore List + + 1048576 + 2147483647 + + + _popUpItemAction: + + + + + Make Selected Reference + + 2147483647 + + + _popUpItemAction: + + + + + YES + YES + + + 1048576 + 2147483647 + + + _popUpItemAction: + + + + + Open Selected with Default Application + + 2147483647 + + + _popUpItemAction: + + + + + Reveal Selected in Finder + + 2147483647 + + + _popUpItemAction: + + + + + Rename Selected + + 1048576 + 2147483647 + + + _popUpItemAction: + + + + + YES + 3 + YES + YES + 1 + + + + + + {58, 26} + {58, 26} + YES + YES + 0 + YES + 0 + + + + 3CF40535-D531-4EBF-97BC-44491B2D99D8 + + Details + Details + + + + NSImage + details32 + + + + {0, 0} + {0, 0} + YES + YES + -1 + YES + 0 + + + + 4D2CC17D-F076-4811-A7EE-A0E07D024C12 + + Directories + Directories + + + + NSImage + folder32 + + + + {0, 0} + {0, 0} + YES + YES + -1 + YES + 0 + + + + 69655571-C90A-48A6-BE96-C70BF9E7BE5B + + Preferences + Preferences + + + + NSImage + preferences32 + + + + {0, 0} + {0, 0} + YES + YES + -1 + YES + 0 + + + + DAD5AB78-1216-48A1-B87C-D451B8C2EBD1 + + Power Marker + Power Marker + + + + 256 + {{7, 14}, {67, 24}} + YES + + 67239424 + 0 + + + + + 30 + Off + 2 + + + 30 + On + 1 + 2 + + + 1 + + + + + + {67, 24} + {67, 24} + YES + YES + 0 + YES + 0 + + + NSToolbarFlexibleSpaceItem + + Flexible Space + + + + + + {1, 5} + {20000, 32} + YES + YES + -1 + YES + 0 + + YES + YES + + + 1048576 + 2147483647 + + + + + + NSToolbarSeparatorItem + + Separator + + + + + + {12, 5} + {12, 1000} + YES + YES + -1 + YES + 0 + + YES + YES + + + 1048576 + 2147483647 + + + + + + NSToolbarSpaceItem + + Space + + + + + + {32, 5} + {32, 32} + YES + YES + -1 + YES + 0 + + YES + YES + + + 1048576 + 2147483647 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {1.79769e+308, 1.79769e+308} + {340, 340} + + + 256 + + + + 274 + + + + 2304 + + + + 274 + {515, 317} + + YES + + + 256 + {515, 17} + + + + + + -2147483392 + {{-26, 0}, {16, 17}} + + + + + mark + 47 + 16 + 1000 + + 75628096 + 2048 + + + + 6 + System + headerColor + + + + 6 + System + headerTextColor + + + + + 67239424 + 131072 + + + LucidaGrande + 12 + 16 + + + 1211912703 + 2 + + NSSwitch + + + + 400 + 75 + + + + + 0 + 230 + 16 + 1000 + + 75628096 + 2048 + Name + + + 3 + MC4zMzMzMzI5OQA + + + + + 337772096 + 2048 + + + + 6 + System + controlBackgroundColor + + 3 + MC42NjY2NjY2NjY3AA + + + + + 2 + YES + + + 0 + YES + compare: + + + + 2 + 63 + 10 + 1000 + + 75628096 + 2048 + Size (MB) + + + + + + 337772096 + 67110912 + + + + + + 2 + YES + + + 2 + YES + compare: + + + + 3 + 50 + 10 + 1000 + + 75628096 + 2048 + Time + + + + + + 337772096 + 67110912 + + + + + + 2 + YES + + + 3 + YES + compare: + + + + 4 + 50 + 10 + 1000 + + 75628096 + 2048 + Bitrate + + + + + + 337772096 + 67110912 + + + + + + 2 + YES + + + 4 + YES + compare: + + + + 16 + 56.9580078125 + 46.9580078125 + 1000 + + 75628096 + 2048 + Match % + + + + + + 337772096 + 2048 + + + + + + 2 + YES + + + 16 + YES + compare: + + + + 3 + 2 + + + 6 + System + gridColor + + 3 + MC41AA + + + 14 + -893386752 + + + 2 + 0 + 15 + 0 + YES + 0 + + + {{1, 17}, {515, 317}} + + + + + 4 + + + + -2147483392 + {{-30, 17}, {15, 302}} + + + _doScroller: + 0.95268136262893677 + + + + -2147483392 + {{1, -30}, {515, 15}} + + 1 + + _doScroller: + 0.96986818313598633 + + + + 2304 + + + + {{1, 0}, {515, 17}} + + + + + 4 + + + + {{20, 45}, {517, 335}} + + + 562 + + + + + + QSAAAEEgAABBgAAAQYAAAA + + + + 290 + {{17, 20}, {523, 17}} + + YES + + 67239424 + 138412032 + Marked: 0 files, 0 B. Total: 0 files, 0 B. + + + + 6 + System + controlColor + + + + + + + {557, 400} + + + {{0, 0}, {1440, 878}} + {340, 418} + {1.79769e+308, 1.79769e+308} + + + MainMenu + + + + dupeGuru ME + + 1048576 + 2147483647 + + + submenuAction: + + dupeGuru ME + + + + About dupeGuru ME + + 2147483647 + + + + + + Unlock dupeGuru ME + + 1048576 + 2147483647 + + + + + + Check for update... + + 1048576 + 2147483647 + + + + + + YES + YES + + + 1048576 + 2147483647 + + + + + + Preferences... + , + 1048576 + 2147483647 + + + + + + YES + YES + + + 1048576 + 2147483647 + + + + + + Hide dupeGuru ME + h + 1048576 + 2147483647 + + + + + + Hide Others + h + 1572864 + 2147483647 + + + + + + Show All + + 1048576 + 2147483647 + + + + + + YES + YES + + + 1048576 + 2147483647 + + + + + + Quit dupeGuru ME + q + 1048576 + 2147483647 + + + + + _NSAppleMenu + + + + + Edit + + 1048576 + 2147483647 + + + submenuAction: + + + Edit + + + + + Mark All + a + 1048576 + 2147483647 + + + + + + Mark None + A + 1048576 + 2147483647 + + + + + + Invert Marking + a + 1572864 + 2147483647 + + + + + + Mark Selected + a + 1310720 + 2147483647 + + + + + + YES + YES + + + 1048576 + 2147483647 + + + + + + Cut + x + 1048576 + 2147483647 + + + + + + Copy + c + 1048576 + 2147483647 + + + + + + Paste + v + 1048576 + 2147483647 + + + + + + + + + Actions + + 1048576 + 2147483647 + + + submenuAction: + + Actions + + + + Start Duplicate Scan + s + 1048576 + 2147483647 + + + + + + Clear Ignore List + I + 1048576 + 2147483647 + + + + + + Export Results to XHTML + E + 1048576 + 2147483647 + + + + + + Remove Dead Tracks in iTunes + + 1048576 + 2147483647 + + + + + + YES + YES + + + 1048576 + 2147483647 + + + + + + Send Marked to Trash + t + 1048576 + 2147483647 + + + + + + Move Marked to... + m + 1048576 + 2147483647 + + + + + + Copy Marked to... + m + 1572864 + 2147483647 + + + + + + Remove Marked from Results + r + 1048576 + 2147483647 + + + + + + YES + YES + + + 1048576 + 2147483647 + + + + + + Remove Selected from Results + R + 1048576 + 2147483647 + + + + + + Add Selected to Ignore List + i + 1048576 + 2147483647 + + + + + + Make Selected Reference +  + 1048576 + 2147483647 + + + + + + YES + YES + + + 1048576 + 2147483647 + + + + + + Open Selected with Default Application + DQ + 1048576 + 2147483647 + + + + + + Reveal Selected in Finder + DQ + 1572864 + 2147483647 + + + + + + Rename Selected + Aw + 2147483647 + + + + + + + + + Columns + + 1048576 + 2147483647 + + + submenuAction: + + Columns + + + + File Name + + 1048576 + 2147483647 + 1 + + + + + + Directory + + 1048576 + 2147483647 + + + 1 + + + + Size + + 1048576 + 2147483647 + 1 + + + 2 + + + + Time + + 1048576 + 2147483647 + 1 + + + 3 + + + + Bitrate + + 1048576 + 2147483647 + 1 + + + 4 + + + + Sample Rate + + 1048576 + 2147483647 + + + 5 + + + + Kind + + 1048576 + 2147483647 + + + 6 + + + + Creation + + 1048576 + 2147483647 + + + 7 + + + + Modification + + 1048576 + 2147483647 + + + 8 + + + + Title + + 1048576 + 2147483647 + + + 9 + + + + Artist + + 1048576 + 2147483647 + + + 10 + + + + Album + + 1048576 + 2147483647 + + + 11 + + + + Genre + + 1048576 + 2147483647 + + + 12 + + + + Year + + 1048576 + 2147483647 + + + 13 + + + + Track Number + + 1048576 + 2147483647 + + + 14 + + + + Comment + + 1048576 + 2147483647 + + + 15 + + + + Match % + + 1048576 + 2147483647 + 1 + + + 16 + + + + Words Used + + 1048576 + 2147483647 + + + 17 + + + + Dupe Count + + 1048576 + 2147483647 + + + 18 + + + + YES + YES + IA + + 1048576 + 2147483647 + + + -1 + + + + Reset to Default + + 1048576 + 2147483647 + + + -1 + + + + + + + Mode + + 1048576 + 2147483647 + + + submenuAction: + + Mode + + + + Power Marker + 1 + 1048576 + 2147483647 + + + + + + Delta Values + 2 + 1048576 + 2147483647 + + + + + + + + + Window + + 1048576 + 2147483647 + + + submenuAction: + + Window + + + + Directory Panel + 3 + 1048576 + 2147483647 + + + + + + Details Panel + 4 + 1048576 + 2147483647 + + + + + + YES + YES + + + 1048576 + 2147483647 + + + + + + Minimize + + 2147483647 + + + + + + Zoom + + 1048576 + 2147483647 + + + + + + Close Window + w + 1048576 + 2147483647 + + + + + + YES + YES + + + 1048576 + 2147483647 + + + + + + Bring All to Front + + 1048576 + 2147483647 + + + + + _NSWindowsMenu + + + + + Help + + 1048576 + 2147483647 + + + submenuAction: + + Help + + + + dupeGuru ME Help + ? + 1048576 + 2147483647 + + + + + + dupeGuru ME Website + + 1048576 + 2147483647 + + + + + + + + _NSMainMenu + + + AppDelegate + + + ResultWindow + + + YES + + + RecentDirectories + + + 3 + 2 + {{92, 259}, {361, 343}} + 1886912512 + dupeGuru ME Preferences + + NSWindow + + + View + + {1.79769e+308, 1.79769e+308} + {213, 107} + + + 256 + + + + 292 + {{120, 264}, {190, 21}} + + YES + + 67239424 + 0 + + + + + Helvetica + 12 + 16 + + + 100 + 1 + 80 + 0.0 + 0 + 1 + NO + NO + + + + + 292 + {{122, 247}, {80, 13}} + + YES + + 67239424 + 272629760 + More results + + LucidaGrande + 10 + 2843 + + + + + + + + + 289 + {{228, 247}, {80, 13}} + + YES + + 67239424 + 71303168 + Less results + + + + + + + + + 292 + {{17, 269}, {100, 14}} + + YES + + 67239424 + 272629760 + Filter hardness: + + + + + + + + + 292 + {{20, 310}, {85, 13}} + + YES + + 67239424 + 272629760 + Scan type: + + + + + + + + + 292 + {{113, 299}, {231, 26}} + + YES + + -2076049856 + 2048 + + + 109199615 + 1 + + + + + + 400 + 75 + + + Tags + + 1048576 + 2147483647 + 1 + + + _popUpItemAction: + + + YES + + + OtherViews + + + + + Filename + + 1048576 + 2147483647 + + + _popUpItemAction: + + + + + Filename - Fields + + 1048576 + 2147483647 + + + _popUpItemAction: + + + + + Filename - Fields (No Order) + + 1048576 + 2147483647 + + + _popUpItemAction: + + + + + + Content + + 1048576 + 2147483647 + + + _popUpItemAction: + + + + + Audio Content + + 1048576 + 2147483647 + + + _popUpItemAction: + + + + + 3 + 3 + YES + YES + 1 + + + + + 256 + {{18, 183}, {214, 18}} + + YES + + 67239424 + 0 + Word weighting + + + 1211912703 + 2 + + + + 200 + 25 + + + + + 256 + {{18, 143}, {214, 18}} + + YES + + 67239424 + 0 + Can mix file kind + + + 1211912703 + 2 + + + + 200 + 25 + + + + + 256 + {{316, 269}, {31, 14}} + + YES + + 67239424 + -1874853888 + + + + + + + 0 + + + . + + , + -0 + 0 + + + 0 + -0 + + + + + + + + NaN + + + + 0 + 0 + YES + NO + 1 + AAAAAAAAAAAAAAAAAAAAAA + + + + . + , + NO + YES + YES + + + + + + + + + 256 + {{234, 12}, {113, 32}} + + YES + + 67239424 + 134217728 + Presets + + + -2038284033 + 1 + + + + + + 200 + 25 + + + + + 256 + {{18, 163}, {214, 18}} + + YES + + 67239424 + 0 + Match similar words + + + 1211912703 + 2 + + + + 200 + 25 + + + + + -2147483392 + {{180, 16}, {46, 26}} + + YES + + -2076049856 + 2048 + + + 109199615 + 1 + + LucidaGrande + 13 + 16 + + + + + + 400 + 75 + + + Tight, 99.9% accurate scan (audio content) + + 1048576 + 2147483647 + 1 + + + _popUpItemAction: + + + YES + + + OtherViews + + + + + + Normal, very accurate scan (tag) + + 1048576 + 2147483647 + + + _popUpItemAction: + 1 + + + + + Scan for tag-less songs (filename - fields) + + 1048576 + 2147483647 + + + _popUpItemAction: + 2 + + + + + Broader scan, might have false positives (filename) + + 1048576 + 2147483647 + + + _popUpItemAction: + 3 + + + + + Deep scan, lots of false positive (filename) + + 1048576 + 2147483647 + + + _popUpItemAction: + 4 + + + + + 3 + YES + YES + 1 + + + + + 292 + {{20, 62}, {85, 13}} + + YES + + 67239424 + 272629760 + Copy and Move: + + + + + + + + + 292 + {{110, 51}, {234, 26}} + + YES + + -2076049856 + 2048 + + + 109199615 + 1 + + + + + + 400 + 75 + + + Right in destination + + 1048576 + 2147483647 + 1 + + + _popUpItemAction: + + + YES + + + OtherViews + + + + + + Recreate relative path + + 1048576 + 2147483647 + + + _popUpItemAction: + + + + + Recreate absolute path + + 1048576 + 2147483647 + + + _popUpItemAction: + + + + + 3 + YES + YES + 1 + + + + + 256 + {{18, 81}, {283, 18}} + + YES + + 67239424 + 0 + Check for update on startup + + + 1211912703 + 2 + + + + 200 + 25 + + + + + 256 + {{18, 123}, {262, 18}} + + YES + + 67239424 + 0 + Use regular expressions when filtering + + + 1211912703 + 2 + + + + 200 + 25 + + + + + 256 + {{18, 103}, {262, 18}} + + YES + + 67239424 + 0 + Remove empty folders after delete and move + + + 1211912703 + 2 + + + + 200 + 25 + + + + + 292 + {{17, 225}, {100, 17}} + + YES + + 67239424 + 272629760 + Tags to scan: + + + + + + + + + 256 + {{27, 205}, {55, 18}} + + YES + + 67239424 + 0 + Track + + + 1211912703 + 2 + + + + 200 + 25 + + + + + 256 + {{80, 205}, {55, 18}} + + YES + + 67239424 + 0 + Artist + + + 1211912703 + 2 + + + + 200 + 25 + + + + + 256 + {{132, 205}, {60, 18}} + + YES + + 67239424 + 0 + Album + + + 1211912703 + 2 + + + + 200 + 25 + + + + + 256 + {{190, 205}, {51, 18}} + + YES + + 67239424 + 0 + Title + + + 1211912703 + 2 + + + + 200 + 25 + + + + + 256 + {{238, 205}, {54, 18}} + + YES + + 67239424 + 0 + Genre + + + 1211912703 + 2 + + + + 200 + 25 + + + + + 256 + {{295, 205}, {54, 18}} + + YES + + 67239424 + 0 + Year + + + 1211912703 + 2 + + + + 200 + 25 + + + + {361, 343} + + + {{0, 0}, {1440, 878}} + {213, 129} + {1.79769e+308, 1.79769e+308} + + + PyDupeGuru + + + Menu + + + + Remove Selected from Results + + 1048576 + 2147483647 + + + + + + Add Selected to Ignore List + + 1048576 + 2147483647 + + + + + + Make Selected Reference + + 1048576 + 2147483647 + + + + + + YES + YES + + + 1048576 + 2147483647 + + + + + + Open Selected with Default Application + + 1048576 + 2147483647 + + + + + + Reveal Selected in Finder + + 1048576 + 2147483647 + + + + + + Rename Selected + + 1048576 + 2147483647 + + + + + + + + SUUpdater + + + + + + + performMiniaturize: + + + + 37 + + + + arrangeInFront: + + + + 39 + + + + showHelp: + + + + 122 + + + + terminate: + + + + 139 + + + + orderFrontStandardAboutPanel: + + + + 142 + + + + hideOtherApplications: + + + + 146 + + + + hide: + + + + 152 + + + + unhideAllApplications: + + + + 153 + + + + performZoom: + + + + 198 + + + + delegate + + + + 207 + + + + delegate + + + + 208 + + + + window + + + + 210 + + + + result + + + + 211 + + + + delegate + + + + 212 + + + + matches + + + + 245 + + + + initialFirstResponder + + + + 279 + + + + delegate + + + + 410 + + + + markToggle: + + + + 414 + + + + stats + + + + 445 + + + + delegate + + + + 502 + + + + recentDirectories + + + + 503 + + + + makeKeyAndOrderFront: + + + + 543 + + + + initialFirstResponder + + + + 544 + + + + value: values.minMatchPercentage + + + + + + value: values.minMatchPercentage + value + values.minMatchPercentage + 2 + + + 549 + + + + selectedIndex: values.scanType + + + + + + selectedIndex: values.scanType + selectedIndex + values.scanType + 2 + + + 551 + + + + enabled: values.scanType + + + + + + enabled: values.scanType + enabled + values.scanType + + NSValueTransformerName + vtScanTypeIsNotContent + + 2 + + + 554 + + + + toggleDetailsPanel: + + + + 596 + + + + deleteMarked: + + + + 606 + + + + moveMarked: + + + + 607 + + + + copyMarked: + + + + 608 + + + + removeMarked: + + + + 609 + + + + switchSelected: + + + + 610 + + + + removeSelected: + + + + 611 + + + + py + + + + 614 + + + + py + + + + 616 + + + + toggleColumn: + + + + 627 + + + + toggleColumn: + + + + 628 + + + + toggleColumn: + + + + 629 + + + + toggleColumn: + + + + 630 + + + + toggleColumn: + + + + 631 + + + + toggleColumn: + + + + 632 + + + + toggleColumn: + + + + 633 + + + + enabled: values.scanType + + + + + + enabled: values.scanType + enabled + values.scanType + + NSValueTransformerName + vtScanTypeIsNotContent + + 2 + + + 640 + + + + value: values.wordWeighting + + + + + + value: values.wordWeighting + value + values.wordWeighting + 2 + + + 642 + + + + toggleColumn: + + + + 647 + + + + value: values.mixFileKind + + + + + + value: values.mixFileKind + value + values.mixFileKind + 2 + + + 656 + + + + openSelected: + + + + 660 + + + + revealSelected: + + + + 661 + + + + menu + + + + 663 + + + + toggleColumn: + + + + 706 + + + + openSelected: + + + + 709 + + + + revealSelected: + + + + 711 + + + + value: values.minMatchPercentage + + + + + + value: values.minMatchPercentage + value + values.minMatchPercentage + 2 + + + 713 + + + + switchSelected: + + + + 716 + + + + preferencesPanel + + + + 718 + + + + deleteMarked: + + + + 741 + + + + moveMarked: + + + + 742 + + + + copyMarked: + + + + 743 + + + + removeMarked: + + + + 744 + + + + removeSelected: + + + + 745 + + + + switchSelected: + + + + 746 + + + + openSelected: + + + + 747 + + + + revealSelected: + + + + 748 + + + + defaultsController + + + + 753 + + + + unlockApp: + + + + 755 + + + + unlockMenuItem + + + + 756 + + + + app + + + + 757 + + + + toggleDirectories: + + + + 758 + + + + py + + + + 764 + + + + enabled: values.scanType + + + + + + enabled: values.scanType + enabled + values.scanType + + NSValueTransformerName + vtScanTypeIsNotContent + + 2 + + + 774 + + + + value: values.matchSimilarWords + + + + + + value: values.matchSimilarWords + value + values.matchSimilarWords + 2 + + + 775 + + + + nextKeyView + + + + 781 + + + + toggleColumn: + + + + 786 + + + + toggleColumn: + + + + 787 + + + + toggleColumn: + + + + 794 + + + + toggleColumn: + + + + 795 + + + + toggleColumn: + + + + 796 + + + + toggleColumn: + + + + 797 + + + + toggleColumn: + + + + 798 + + + + toggleColumn: + + + + 799 + + + + removeSelected: + + + + 873 + + + + toggleColumn: + + + + 875 + + + + changeDelta: + + + + 882 + + + + deltaSwitch + + + + 883 + + + + usePreset: + + + + 891 + + + + usePreset: + + + + 892 + + + + usePreset: + + + + 893 + + + + usePreset: + + + + 894 + + + + usePreset: + + + + 895 + + + + popupPresets: + + + + 896 + + + + presetsButton + + + + 897 + + + + presetsPopup + + + + 902 + + + + selectedIndex: values.recreatePathType + + + + + + selectedIndex: values.recreatePathType + selectedIndex + values.recreatePathType + 2 + + + 914 + + + + ignoreSelected: + + + + 921 + + + + ignoreSelected: + + + + 923 + + + + performClose: + + + + 925 + + + + startDuplicateScan: + + + + 929 + + + + clearIgnoreList: + + + + 930 + + + + renameSelected: + + + + 932 + + + + renameSelected: + + + + 934 + + + + renameSelected: + + + + 938 + + + + ignoreSelected: + + + + 939 + + + + toggleColumn: + + + + 941 + + + + columnsMenu + + + + 942 + + + + resetColumnsToDefault: + + + + 945 + + + + exportToXHTML: + + + + 947 + + + + checkForUpdates: + + + + 950 + + + + value: values.SUCheckAtStartup + + + + + + value: values.SUCheckAtStartup + value + values.SUCheckAtStartup + 2 + + + 952 + + + + openWebsite: + + + + 956 + + + + togglePowerMarker: + + + + 961 + + + + toggleDelta: + + + + 962 + + + + changePowerMarker: + + + + 965 + + + + pmSwitch + + + + 966 + + + + copy: + + + + 1006 + + + + cut: + + + + 1007 + + + + paste: + + + + 1011 + + + + markAll: + + + + 1019 + + + + markNone: + + + + 1020 + + + + markSelected: + + + + 1021 + + + + markInvert: + + + + 1022 + + + + filter: + + + + 1025 + + + + filterField + + + + 1027 + + + + value: values.useRegexpFilter + + + + + + value: values.useRegexpFilter + value + values.useRegexpFilter + 2 + + + 1031 + + + + nextKeyView + + + + 1064 + + + + nextKeyView + + + + 1066 + + + + nextKeyView + + + + 1067 + + + + nextKeyView + + + + 1068 + + + + nextKeyView + + + + 1069 + + + + nextKeyView + + + + 1070 + + + + nextKeyView + + + + 1071 + + + + nextKeyView + + + + 1072 + + + + value: values.removeEmptyFolders + + + + + + value: values.removeEmptyFolders + value + values.removeEmptyFolders + 2 + + + 1074 + + + + nextKeyView + + + + 1121 + + + + nextKeyView + + + + 1122 + + + + nextKeyView + + + + 1123 + + + + nextKeyView + + + + 1124 + + + + nextKeyView + + + + 1125 + + + + nextKeyView + + + + 1126 + + + + nextKeyView + + + + 1127 + + + + value: values.scanTagTrack + + + + + + value: values.scanTagTrack + value + values.scanTagTrack + 2 + + + 1129 + + + + value: values.scanTagArtist + + + + + + value: values.scanTagArtist + value + values.scanTagArtist + 2 + + + 1130 + + + + value: values.scanTagAlbum + + + + + + value: values.scanTagAlbum + value + values.scanTagAlbum + 2 + + + 1131 + + + + value: values.scanTagTitle + + + + + + value: values.scanTagTitle + value + values.scanTagTitle + 2 + + + 1132 + + + + value: values.scanTagGenre + + + + + + value: values.scanTagGenre + value + values.scanTagGenre + 2 + + + 1133 + + + + value: values.scanTagYear + + + + + + value: values.scanTagYear + value + values.scanTagYear + 2 + + + 1134 + + + + enabled: values.scanType + + + + + + enabled: values.scanType + enabled + values.scanType + + NSValueTransformerName + vtScanTypeIsTag + + 2 + + + 1136 + + + + enabled: values.scanType + + + + + + enabled: values.scanType + enabled + values.scanType + + NSValueTransformerName + vtScanTypeIsTag + + 2 + + + 1139 + + + + enabled: values.scanType + + + + + + enabled: values.scanType + enabled + values.scanType + + NSValueTransformerName + vtScanTypeIsTag + + 2 + + + 1141 + + + + enabled: values.scanType + + + + + + enabled: values.scanType + enabled + values.scanType + + NSValueTransformerName + vtScanTypeIsTag + + 2 + + + 1143 + + + + enabled: values.scanType + + + + + + enabled: values.scanType + enabled + values.scanType + + NSValueTransformerName + vtScanTypeIsTag + + 2 + + + 1145 + + + + enabled: values.scanType + + + + + + enabled: values.scanType + enabled + values.scanType + + NSValueTransformerName + vtScanTypeIsTag + + 2 + + + 1147 + + + + removeDeadTracks: + + + + 1187 + + + + startDuplicateScan: + + + + 1242 + + + + toggleDirectories: + + + + 1243 + + + + toggleDetailsPanel: + + + + 1244 + + + + showPreferencesPanel: + + + + 1245 + + + + + + 0 + + + + + + -2 + + + File's Owner + + + -1 + + + First Responder + + + -3 + + + Application + + + 21 + + + + + + + Window + + + 2 + + + + + + + + + 219 + + + + + + + + + + + 220 + + + + + + + + + + + + + 222 + + + + + + + + 223 + + + + + + + + 233 + + + + + + + + 406 + + + + + + + + 407 + + + + + 782 + + + + + + + + 783 + + + + + + + + 291 + + + + + + + + 29 + + + + + + + + + + + + MainMenu + + + 19 + + + + + + + + 24 + + + + + + + + + + + + + + + 5 + + + + + 23 + + + + + 92 + + + + + 197 + + + + + 398 + + + + + 399 + + + + + 579 + + + + + 924 + + + + + 56 + + + + + + + + 57 + + + + + + + + + + + + + + + + + + 58 + + + + + 134 + + + + + 136 + + + + + 144 + + + + + 145 + + + + + 149 + + + + + 150 + + + + + 541 + + + + + 542 + + + + + 754 + + + + + 949 + + + + + 103 + + + + + + + + 106 + + + + + + + + + 111 + + + + + 955 + + + + + 597 + + + + + + + + 598 + + + + + + + + + + + + + + + + + + + + + + + + 599 + + + + + 600 + + + + + 601 + + + + + 602 + + + + + 603 + + + + + 604 + + + + + 605 + + + + + 707 + + + + + 708 + + + + + 710 + + + + + 922 + + + + + 926 + + + + + 927 + + + + + 928 + + + + + 931 + + + + + 946 + + + + + 953 + + + + + 618 + + + + + + + + 619 + + + + + + + + + + + + + + + + + + + + + + + + + + + + 620 + + + + + 621 + + + + + 622 + + + + + 623 + + + + + 624 + + + + + 625 + + + + + 626 + + + + + 646 + + + + + 705 + + + + + 784 + + + + + 785 + + + + + 788 + + + + + 789 + + + + + 790 + + + + + 791 + + + + + 792 + + + + + 793 + + + + + 874 + + + + + 940 + + + + + 943 + + + + + 944 + + + + + 957 + + + + + + + + 958 + + + + + + + + + 959 + + + + + 960 + + + + + 968 + + + + + + + + 969 + + + + + + + + + + + + + + + 974 + + + + + 994 + + + + + 995 + + + + + 1014 + + + + + 1015 + + + + + 1016 + + + + + 1017 + + + + + 1018 + + + + + 206 + + + AppDelegate + + + 209 + + + ResultWindow + + + 468 + + + Shared Defaults + + + 497 + + + RecentDirectoriesController + + + 523 + + + + + + preferences + + + 524 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 531 + + + + + + + + 532 + + + + + + + + 533 + + + + + + + + 534 + + + + + + + + 535 + + + + + + + + 536 + + + + + + + + 635 + + + + + + + + 649 + + + + + + + + 712 + + + + + + + + 750 + + + + + + + + 772 + + + + + + + + 899 + + + + + + + + 904 + + + + + + + + 905 + + + + + + + + 951 + + + + + + + + 1028 + + + + + + + + 1062 + + + + + + + + 1107 + + + + + + + + 1109 + + + + + + + + 1111 + + + + + + + + 1113 + + + + + + + + 1115 + + + + + + + + 1117 + + + + + + + + 1119 + + + + + + + + 613 + + + PyDupeGuru + + + 657 + + + + + + + + + + + + matches_context + + + 658 + + + + + 659 + + + + + 715 + + + + + 872 + + + + + 935 + + + + + 936 + + + + + 937 + + + + + 948 + + + SUUpdater + + + 1189 + + + + + 1190 + + + + + 1191 + + + + + 1192 + + + + + 1193 + + + + + 1194 + + + + + 1195 + + + + + + + + 1196 + + + + + 1197 + + + + + 1198 + + + + + + + + 1199 + + + + + 1200 + + + + + 1201 + + + + + + + + 1202 + + + + + 1203 + + + + + + + + 1204 + + + + + 1205 + + + + + 1206 + + + + + 1207 + + + + + 1208 + + + + + 1209 + + + + + 1210 + + + + + 1211 + + + + + 1212 + + + + + 1213 + + + + + 1218 + + + + + 1219 + + + + + 1220 + + + + + 1221 + + + + + 1222 + + + + + 714 + + + + + 537 + + + + + + + + + + + + + 804 + + + + + 803 + + + + + 801 + + + + + 800 + + + + + 539 + + + + + 538 + + + + + 900 + + + + + + + + + + + + 906 + + + + + + + + + + 913 + + + + + 909 + + + + + 908 + + + + + 1223 + + + + + 1224 + + + + + 1225 + + + + + 1226 + + + + + + + + + + + + + + + + + + 1229 + + + + + 1231 + + + + + 1232 + + + + + 1234 + + + + + 1235 + + + + + 1236 + + + + + 1237 + + + + + 1238 + + + + + + + + 720 + + + + + + + + 1214 + + + + + + + + 721 + + + + + + + + + + + + + + + + + + + + 723 + + + + + 731 + + + + + 732 + + + + + 733 + + + + + 734 + + + + + 735 + + + + + 736 + + + + + 920 + + + + + 738 + + + + + 737 + + + + + 739 + + + + + 740 + + + + + 933 + + + + + 1239 + + + + + + + + 880 + + + + + + + + 1215 + + + + + 1240 + + + + + + + + 964 + + + + + + + + 1216 + + + + + 1241 + + + + + + + + 1024 + + + + + + + + 1217 + + + + + 886 + + + + + 887 + + + + + 889 + + + + + 888 + + + + + 890 + + + + + + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + {{58, 778}, {617, 0}} + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + tbbScan + com.apple.InterfaceBuilder.CocoaPlugin + tbbDirectories + com.apple.InterfaceBuilder.CocoaPlugin + tbbDetail + com.apple.InterfaceBuilder.CocoaPlugin + tbbPreferences + com.apple.InterfaceBuilder.CocoaPlugin + tbbAction + com.apple.InterfaceBuilder.CocoaPlugin + tbbDelta + com.apple.InterfaceBuilder.CocoaPlugin + tbbPowerMarker + com.apple.InterfaceBuilder.CocoaPlugin + tbbFilter + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + + + {{87, 377}, {557, 400}} + com.apple.InterfaceBuilder.CocoaPlugin + {{87, 377}, {557, 400}} + + + + {340, 340} + com.apple.InterfaceBuilder.CocoaPlugin + + MatchesView + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + {{88, 814}, {506, 20}} + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + + com.apple.InterfaceBuilder.CocoaPlugin + + {{88, 502}, {361, 343}} + com.apple.InterfaceBuilder.CocoaPlugin + {{88, 502}, {361, 343}} + + + {213, 107} + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + {{259, 501}, {359, 313}} + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + {{73, 457}, {331, 243}} + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + {{257, 441}, {411, 103}} + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + + + + + + 1245 + + + + + AppDelegate + AppDelegateBase + + id + id + id + id + + + NSButton + NSPopUpButton + ResultWindow + + + IBProjectSource + AppDelegate.h + + + + AppDelegate + AppDelegateBase + + unlockApp: + id + + + NSUserDefaultsController + PyDupeGuru + RecentDirectories + NSMenuItem + + + IBUserSource + + + + + AppDelegateBase + NSObject + + unlockApp: + id + + + PyDupeGuruBase + RecentDirectories + NSMenuItem + + + IBProjectSource + dgbase/AppDelegate.h + + + + FirstResponder + NSObject + + IBUserSource + + + + + MatchesView + OutlineView + + IBProjectSource + dgbase/ResultWindow.h + + + + MatchesView + OutlineView + + IBUserSource + + + + + NSSegmentedControl + NSControl + + IBUserSource + + + + + OutlineView + NSOutlineView + + py + PyApp + + + IBProjectSource + cocoalib/Outline.h + + + + OutlineView + NSOutlineView + + IBUserSource + + + + + PyApp + PyRegistrable + + IBProjectSource + cocoalib/PyApp.h + + + + PyApp + PyRegistrable + + IBUserSource + + + + + PyDupeGuru + PyDupeGuruBase + + IBProjectSource + PyDupeGuru.h + + + + PyDupeGuru + PyDupeGuruBase + + IBUserSource + + + + + PyDupeGuruBase + PyApp + + IBProjectSource + dgbase/PyDupeGuru.h + + + + PyDupeGuruBase + PyApp + + IBUserSource + + + + + RecentDirectories + NSObject + + id + id + + + id + NSMenu + + + IBProjectSource + cocoalib/RecentDirectories.h + + + + RecentDirectories + NSObject + + IBUserSource + + + + + ResultWindow + ResultWindowBase + + id + id + id + id + id + id + id + id + id + id + id + id + id + id + id + id + id + id + id + id + id + + + NSMenu + NSSearchField + NSWindow + + + IBProjectSource + ResultWindow.h + + + + ResultWindow + ResultWindowBase + + id + id + id + id + id + id + id + id + id + id + + + NSView + id + NSSegmentedControl + NSView + NSView + MatchesView + NSSegmentedControl + NSView + PyDupeGuru + NSTextField + + + IBUserSource + + + + + ResultWindowBase + NSWindowController + + id + id + id + id + id + id + id + id + id + + + id + NSSegmentedControl + MatchesView + NSSegmentedControl + PyDupeGuruBase + NSTextField + + + + + ResultWindowBase + NSWindowController + + IBUserSource + + + + + SUUpdater + NSObject + + IBUserSource + + + + + + + NSActionCell + NSCell + + IBFrameworkSource + AppKit.framework/Headers/NSActionCell.h + + + + NSApplication + NSResponder + + IBFrameworkSource + AppKit.framework/Headers/NSApplication.h + + + + NSApplication + + IBFrameworkSource + AppKit.framework/Headers/NSApplicationScripting.h + + + + NSApplication + + IBFrameworkSource + AppKit.framework/Headers/NSColorPanel.h + + + + NSApplication + + IBFrameworkSource + AppKit.framework/Headers/NSHelpManager.h + + + + NSApplication + + IBFrameworkSource + AppKit.framework/Headers/NSPageLayout.h + + + + NSApplication + + IBFrameworkSource + AppKit.framework/Headers/NSUserInterfaceItemSearching.h + + + + NSButton + NSControl + + IBFrameworkSource + AppKit.framework/Headers/NSButton.h + + + + NSButtonCell + NSActionCell + + IBFrameworkSource + AppKit.framework/Headers/NSButtonCell.h + + + + NSCell + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSCell.h + + + + NSControl + NSView + + IBFrameworkSource + AppKit.framework/Headers/NSControl.h + + + + NSController + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSController.h + + + + NSFormatter + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSFormatter.h + + + + NSMenu + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSMenu.h + + + + NSMenuItem + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSMenuItem.h + + + + NSMenuItemCell + NSButtonCell + + IBFrameworkSource + AppKit.framework/Headers/NSMenuItemCell.h + + + + NSMovieView + NSView + + IBFrameworkSource + AppKit.framework/Headers/NSMovieView.h + + + + NSNumberFormatter + NSFormatter + + IBFrameworkSource + Foundation.framework/Headers/NSNumberFormatter.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSAccessibility.h + + + + NSObject + + + + NSObject + + + + NSObject + + + + NSObject + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSDictionaryController.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSDragging.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSFontManager.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSFontPanel.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSKeyValueBinding.h + + + + NSObject + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSNibLoading.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSOutlineView.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSPasteboard.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSSavePanel.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSTableView.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSToolbarItem.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSView.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSArchiver.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSClassDescription.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSError.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSFileManager.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSKeyValueCoding.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSKeyValueObserving.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSKeyedArchiver.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSObject.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSObjectScripting.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSPortCoder.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSRunLoop.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSScriptClassDescription.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSScriptKeyValueCoding.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSScriptObjectSpecifiers.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSScriptWhoseTests.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSThread.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSURL.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSURLConnection.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSURLDownload.h + + + + NSObject + + IBFrameworkSource + Sparkle.framework/Headers/SUAppcast.h + + + + NSObject + + IBFrameworkSource + Sparkle.framework/Headers/SUUpdater.h + + + + NSOutlineView + NSTableView + + + + NSPopUpButton + NSButton + + IBFrameworkSource + AppKit.framework/Headers/NSPopUpButton.h + + + + NSPopUpButtonCell + NSMenuItemCell + + IBFrameworkSource + AppKit.framework/Headers/NSPopUpButtonCell.h + + + + NSResponder + + IBFrameworkSource + AppKit.framework/Headers/NSInterfaceStyle.h + + + + NSResponder + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSResponder.h + + + + NSScrollView + NSView + + IBFrameworkSource + AppKit.framework/Headers/NSScrollView.h + + + + NSScroller + NSControl + + IBFrameworkSource + AppKit.framework/Headers/NSScroller.h + + + + NSSearchField + NSTextField + + IBFrameworkSource + AppKit.framework/Headers/NSSearchField.h + + + + NSSearchFieldCell + NSTextFieldCell + + IBFrameworkSource + AppKit.framework/Headers/NSSearchFieldCell.h + + + + NSSegmentedCell + NSActionCell + + IBFrameworkSource + AppKit.framework/Headers/NSSegmentedCell.h + + + + NSSegmentedControl + NSControl + + IBFrameworkSource + AppKit.framework/Headers/NSSegmentedControl.h + + + + NSSlider + NSControl + + IBFrameworkSource + AppKit.framework/Headers/NSSlider.h + + + + NSSliderCell + NSActionCell + + IBFrameworkSource + AppKit.framework/Headers/NSSliderCell.h + + + + NSTableColumn + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSTableColumn.h + + + + NSTableHeaderView + NSView + + IBFrameworkSource + AppKit.framework/Headers/NSTableHeaderView.h + + + + NSTableView + NSControl + + + + NSText + NSView + + IBFrameworkSource + AppKit.framework/Headers/NSText.h + + + + NSTextField + NSControl + + IBFrameworkSource + AppKit.framework/Headers/NSTextField.h + + + + NSTextFieldCell + NSActionCell + + IBFrameworkSource + AppKit.framework/Headers/NSTextFieldCell.h + + + + NSToolbar + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSToolbar.h + + + + NSToolbarItem + NSObject + + + + NSUserDefaultsController + NSController + + IBFrameworkSource + AppKit.framework/Headers/NSUserDefaultsController.h + + + + NSView + + IBFrameworkSource + AppKit.framework/Headers/NSClipView.h + + + + NSView + + + + NSView + + IBFrameworkSource + AppKit.framework/Headers/NSRulerView.h + + + + NSView + NSResponder + + + + NSWindow + + IBFrameworkSource + AppKit.framework/Headers/NSDrawer.h + + + + NSWindow + NSResponder + + IBFrameworkSource + AppKit.framework/Headers/NSWindow.h + + + + NSWindow + + IBFrameworkSource + AppKit.framework/Headers/NSWindowScripting.h + + + + NSWindowController + NSResponder + + showWindow: + id + + + IBFrameworkSource + AppKit.framework/Headers/NSWindowController.h + + + + SUUpdater + NSObject + + checkForUpdates: + id + + + delegate + id + + + + + + 0 + + com.apple.InterfaceBuilder.CocoaPlugin.macosx + + + + com.apple.InterfaceBuilder.CocoaPlugin.macosx + + + YES + ../dupeguru.xcodeproj + 3 + + From d4d8917956f83420f347d740166ecb553b5201f8 Mon Sep 17 00:00:00 2001 From: hsoft Date: Sun, 25 Oct 2009 11:46:26 +0000 Subject: [PATCH 215/275] dgpe cocoa: dropped tiger support. Added toolbar creation in the MainMenu nib. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40221 --- .../English.lproj/Details.nib/classes.nib | 24 - pe/cocoa/English.lproj/Details.nib/info.nib | 16 - .../Details.nib/keyedobjects.nib | Bin 9001 -> 0 bytes .../English.lproj/Directories.nib/classes.nib | 62 - .../English.lproj/Directories.nib/info.nib | 20 - .../Directories.nib/keyedobjects.nib | Bin 9698 -> 0 bytes pe/cocoa/English.lproj/InfoPlist.strings | Bin 204 -> 0 bytes .../English.lproj/MainMenu.nib/classes.nib | 235 - pe/cocoa/English.lproj/MainMenu.nib/info.nib | 20 - .../MainMenu.nib/keyedobjects.nib | Bin 45275 -> 0 bytes pe/cocoa/Info.plist | 4 +- pe/cocoa/ResultWindow.h | 1 - pe/cocoa/ResultWindow.m | 14 - pe/cocoa/dupeguru.xcodeproj/project.pbxproj | 123 +- pe/cocoa/xib/DetailsPanel.xib | 1472 +++++ pe/cocoa/xib/MainMenu.xib | 5501 +++++++++++++++++ pe/py/app_cocoa.py | 2 + 17 files changed, 7004 insertions(+), 490 deletions(-) delete mode 100644 pe/cocoa/English.lproj/Details.nib/classes.nib delete mode 100644 pe/cocoa/English.lproj/Details.nib/info.nib delete mode 100644 pe/cocoa/English.lproj/Details.nib/keyedobjects.nib delete mode 100644 pe/cocoa/English.lproj/Directories.nib/classes.nib delete mode 100644 pe/cocoa/English.lproj/Directories.nib/info.nib delete mode 100644 pe/cocoa/English.lproj/Directories.nib/keyedobjects.nib delete mode 100644 pe/cocoa/English.lproj/InfoPlist.strings delete mode 100644 pe/cocoa/English.lproj/MainMenu.nib/classes.nib delete mode 100644 pe/cocoa/English.lproj/MainMenu.nib/info.nib delete mode 100644 pe/cocoa/English.lproj/MainMenu.nib/keyedobjects.nib create mode 100644 pe/cocoa/xib/DetailsPanel.xib create mode 100644 pe/cocoa/xib/MainMenu.xib diff --git a/pe/cocoa/English.lproj/Details.nib/classes.nib b/pe/cocoa/English.lproj/Details.nib/classes.nib deleted file mode 100644 index f39cb617..00000000 --- a/pe/cocoa/English.lproj/Details.nib/classes.nib +++ /dev/null @@ -1,24 +0,0 @@ -{ - IBClasses = ( - { - CLASS = DetailsPanel; - LANGUAGE = ObjC; - OUTLETS = { - detailsTable = NSTableView; - dupeImage = NSImageView; - dupeProgressIndicator = NSProgressIndicator; - refImage = NSImageView; - refProgressIndicator = NSProgressIndicator; - }; - SUPERCLASS = NSWindowController; - }, - {CLASS = FirstResponder; LANGUAGE = ObjC; SUPERCLASS = NSObject; }, - { - CLASS = TableView; - LANGUAGE = ObjC; - OUTLETS = {py = PyApp; }; - SUPERCLASS = NSTableView; - } - ); - IBVersion = 1; -} \ No newline at end of file diff --git a/pe/cocoa/English.lproj/Details.nib/info.nib b/pe/cocoa/English.lproj/Details.nib/info.nib deleted file mode 100644 index 3ad9dc9e..00000000 --- a/pe/cocoa/English.lproj/Details.nib/info.nib +++ /dev/null @@ -1,16 +0,0 @@ - - - - - IBDocumentLocation - 701 68 356 240 0 0 1440 878 - IBFramework Version - 446.1 - IBOpenObjects - - 5 - - IBSystem Version - 8R2232 - - diff --git a/pe/cocoa/English.lproj/Details.nib/keyedobjects.nib b/pe/cocoa/English.lproj/Details.nib/keyedobjects.nib deleted file mode 100644 index dfc61c70bdcc3191c61f1ae5160b485223253e9f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9001 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 AaR2}S diff --git a/pe/cocoa/English.lproj/Directories.nib/classes.nib b/pe/cocoa/English.lproj/Directories.nib/classes.nib deleted file mode 100644 index 3ebaa96a..00000000 --- a/pe/cocoa/English.lproj/Directories.nib/classes.nib +++ /dev/null @@ -1,62 +0,0 @@ - - - - - IBClasses - - - CLASS - FirstResponder - LANGUAGE - ObjC - SUPERCLASS - NSObject - - - ACTIONS - - askForDirectory - id - changeDirectoryState - id - popupAddDirectoryMenu - id - removeSelectedDirectory - id - toggleVisible - id - - CLASS - DirectoryPanel - LANGUAGE - ObjC - OUTLETS - - addButtonPopUp - NSPopUpButton - directories - NSOutlineView - removeButton - NSButton - - SUPERCLASS - DirectoryPanelBase - - - CLASS - OutlineView - LANGUAGE - ObjC - OUTLETS - - py - PyApp - - SUPERCLASS - NSOutlineView - - - IBVersion - 1 - - diff --git a/pe/cocoa/English.lproj/Directories.nib/info.nib b/pe/cocoa/English.lproj/Directories.nib/info.nib deleted file mode 100644 index 77f19ce7..00000000 --- a/pe/cocoa/English.lproj/Directories.nib/info.nib +++ /dev/null @@ -1,20 +0,0 @@ - - - - - IBFramework Version - 629 - IBLastKnownRelativeProjectPath - ../../dupeguru.xcodeproj - IBOldestOS - 5 - IBOpenObjects - - 5 - - IBSystem Version - 9B18 - targetFramework - IBCocoaFramework - - diff --git a/pe/cocoa/English.lproj/Directories.nib/keyedobjects.nib b/pe/cocoa/English.lproj/Directories.nib/keyedobjects.nib deleted file mode 100644 index 906ea9c34734659e9d70e20070a6b8f4dc797a98..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9698 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 LQhrn4zM20Aagm87 diff --git a/pe/cocoa/English.lproj/InfoPlist.strings b/pe/cocoa/English.lproj/InfoPlist.strings deleted file mode 100644 index d224a14bd2062242e455ea370c921c67e7045c94..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 204 zcmW-bOA5k35Cv=PDT2!&D(*yFxrrby%n4#XD*i$e6}^#{RLh~Ed*=0fHS_s0A|_(R zm7I(d2VRsEYIkQtt8(SyjGUEy>8yVS6Mq diff --git a/pe/cocoa/English.lproj/MainMenu.nib/classes.nib b/pe/cocoa/English.lproj/MainMenu.nib/classes.nib deleted file mode 100644 index fbce5b56..00000000 --- a/pe/cocoa/English.lproj/MainMenu.nib/classes.nib +++ /dev/null @@ -1,235 +0,0 @@ - - - - - IBClasses - - - CLASS - NSSegmentedControl - LANGUAGE - ObjC - SUPERCLASS - NSControl - - - ACTIONS - - openWebsite - id - toggleDetailsPanel - id - toggleDirectories - id - unlockApp - id - - CLASS - AppDelegate - LANGUAGE - ObjC - OUTLETS - - py - PyDupeGuru - recentDirectories - RecentDirectories - result - ResultWindow - unlockMenuItem - NSMenuItem - - SUPERCLASS - NSObject - - - CLASS - PyApp - LANGUAGE - ObjC - SUPERCLASS - NSObject - - - CLASS - MatchesView - LANGUAGE - ObjC - SUPERCLASS - OutlineView - - - CLASS - PyDupeGuru - LANGUAGE - ObjC - SUPERCLASS - PyApp - - - ACTIONS - - changeDelta - id - changePowerMarker - id - clearIgnoreList - id - clearPictureCache - id - collapseAll - id - copyMarked - id - deleteMarked - id - expandAll - id - exportToXHTML - id - filter - id - ignoreSelected - id - markAll - id - markInvert - id - markNone - id - markSelected - id - markToggle - id - moveMarked - id - openSelected - id - refresh - id - removeMarked - id - removeSelected - id - renameSelected - id - resetColumnsToDefault - id - revealSelected - id - showPreferencesPanel - id - startDuplicateScan - id - switchSelected - id - toggleColumn - id - toggleDelta - id - toggleDetailsPanel - id - toggleDirectories - id - togglePowerMarker - id - - CLASS - ResultWindow - LANGUAGE - ObjC - OUTLETS - - actionMenu - NSPopUpButton - actionMenuView - NSView - app - id - columnsMenu - NSMenu - deltaSwitch - NSSegmentedControl - deltaSwitchView - NSView - filterField - NSSearchField - filterFieldView - NSView - matches - MatchesView - pmSwitch - NSSegmentedControl - pmSwitchView - NSView - preferencesPanel - NSWindow - py - PyDupeGuru - stats - NSTextField - - SUPERCLASS - NSWindowController - - - CLASS - FirstResponder - LANGUAGE - ObjC - SUPERCLASS - NSObject - - - ACTIONS - - checkForUpdates - id - - CLASS - SUUpdater - LANGUAGE - ObjC - SUPERCLASS - NSObject - - - ACTIONS - - clearMenu - id - menuClick - id - - CLASS - RecentDirectories - LANGUAGE - ObjC - OUTLETS - - delegate - id - menu - NSMenu - - SUPERCLASS - NSObject - - - CLASS - OutlineView - LANGUAGE - ObjC - OUTLETS - - py - PyApp - - SUPERCLASS - NSOutlineView - - - IBVersion - 1 - - diff --git a/pe/cocoa/English.lproj/MainMenu.nib/info.nib b/pe/cocoa/English.lproj/MainMenu.nib/info.nib deleted file mode 100644 index a75b8270..00000000 --- a/pe/cocoa/English.lproj/MainMenu.nib/info.nib +++ /dev/null @@ -1,20 +0,0 @@ - - - - - IBFramework Version - 629 - IBLastKnownRelativeProjectPath - ../../dupeguru.xcodeproj - IBOldestOS - 4 - IBOpenObjects - - 524 - - IBSystem Version - 9B18 - targetFramework - IBCocoaFramework - - diff --git a/pe/cocoa/English.lproj/MainMenu.nib/keyedobjects.nib b/pe/cocoa/English.lproj/MainMenu.nib/keyedobjects.nib deleted file mode 100644 index 1c9f4e3c32d40623fd26a3a2d4f3bff499750da6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 45275 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

NS*?bdVCbz5HmH(L&L*Ij$7Si*|A*Zmt)+P9 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 hsft CFBundleVersion - 1.7.7 + 1.8.0b NSMainNibFile MainMenu NSPrincipalClass NSApplication + NSHumanReadableCopyright + © Hardcoded Software, 2009 SUFeedURL http://www.hardcoded.net/updates/dupeguru_pe.appcast SUPublicDSAKeyFile diff --git a/pe/cocoa/ResultWindow.h b/pe/cocoa/ResultWindow.h index 80cd2a28..fd3bf08f 100644 --- a/pe/cocoa/ResultWindow.h +++ b/pe/cocoa/ResultWindow.h @@ -13,7 +13,6 @@ http://www.hardcoded.net/licenses/hs_license @interface ResultWindow : ResultWindowBase { - IBOutlet NSPopUpButton *actionMenu; IBOutlet NSMenu *columnsMenu; IBOutlet NSSearchField *filterField; IBOutlet NSWindow *preferencesPanel; diff --git a/pe/cocoa/ResultWindow.m b/pe/cocoa/ResultWindow.m index f5d6153a..5bfa92e2 100644 --- a/pe/cocoa/ResultWindow.m +++ b/pe/cocoa/ResultWindow.m @@ -29,23 +29,9 @@ http://www.hardcoded.net/licenses/hs_license [py setDisplayDeltaValues:b2n(_displayDelta)]; [matches setTarget:self]; [matches setDoubleAction:@selector(openSelected:)]; - [[actionMenu itemAtIndex:0] setImage:[NSImage imageNamed: @"gear"]]; [self initResultColumns]; [self refreshStats]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(resultsMarkingChanged:) name:ResultsMarkingChangedNotification object:nil]; - - NSToolbar *t = [[[NSToolbar alloc] initWithIdentifier:@"ResultWindowToolbar"] autorelease]; - [t setAllowsUserCustomization:YES]; - [t setAutosavesConfiguration:YES]; - [t setDisplayMode:NSToolbarDisplayModeIconAndLabel]; - [t setDelegate:self]; - [[self window] setToolbar:t]; -} - -/* Overrides */ -- (NSString *)logoImageName -{ - return @"dgpe_logo_32"; } /* Actions */ diff --git a/pe/cocoa/dupeguru.xcodeproj/project.pbxproj b/pe/cocoa/dupeguru.xcodeproj/project.pbxproj index 3ae2ed9c..ec81dfc9 100644 --- a/pe/cocoa/dupeguru.xcodeproj/project.pbxproj +++ b/pe/cocoa/dupeguru.xcodeproj/project.pbxproj @@ -3,12 +3,10 @@ archiveVersion = 1; classes = { }; - objectVersion = 42; + objectVersion = 46; objects = { /* Begin PBXBuildFile section */ - 8D11072A0486CEB800E47090 /* MainMenu.nib in Resources */ = {isa = PBXBuildFile; fileRef = 29B97318FDCFA39411CA2CEA /* MainMenu.nib */; }; - 8D11072B0486CEB800E47090 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 089C165CFE840E0CC02AAC07 /* InfoPlist.strings */; }; 8D11072D0486CEB800E47090 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 29B97316FDCFA39411CA2CEA /* main.m */; settings = {ATTRIBUTES = (); }; }; 8D11072F0486CEB800E47090 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */; }; CE073F6309CAE1A3005C1D2F /* dupeguru_pe_help in Resources */ = {isa = PBXBuildFile; fileRef = CE073F5409CAE1A3005C1D2F /* dupeguru_pe_help */; }; @@ -18,10 +16,12 @@ CE381C9609914ACE003581CE /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = CE381C9409914ACE003581CE /* AppDelegate.m */; }; CE381C9C09914ADF003581CE /* ResultWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = CE381C9A09914ADF003581CE /* ResultWindow.m */; }; CE381D0509915304003581CE /* dg_cocoa.plugin in Resources */ = {isa = PBXBuildFile; fileRef = CE381CF509915304003581CE /* dg_cocoa.plugin */; }; - CE3AA46709DB207900DB3A21 /* Directories.nib in Resources */ = {isa = PBXBuildFile; fileRef = CE3AA46509DB207900DB3A21 /* Directories.nib */; }; CE6044EC0FE6796200B71262 /* DetailsPanel.m in Sources */ = {isa = PBXBuildFile; fileRef = CE6044EB0FE6796200B71262 /* DetailsPanel.m */; }; CE68EE6809ABC48000971085 /* DirectoryPanel.m in Sources */ = {isa = PBXBuildFile; fileRef = CE68EE6609ABC48000971085 /* DirectoryPanel.m */; }; CE6E0F3D1054EC62008D9390 /* dsa_pub.pem in Resources */ = {isa = PBXBuildFile; fileRef = CE6E0F3C1054EC62008D9390 /* dsa_pub.pem */; }; + CE77C89E10946C6D0078B0DB /* DirectoryPanel.xib in Resources */ = {isa = PBXBuildFile; fileRef = CE77C89C10946C6D0078B0DB /* DirectoryPanel.xib */; }; + CE77C8A110946C840078B0DB /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = CE77C8A010946C840078B0DB /* MainMenu.xib */; }; + CE77C8A810946CE20078B0DB /* DetailsPanel.xib in Resources */ = {isa = PBXBuildFile; fileRef = CE77C8A710946CE20078B0DB /* DetailsPanel.xib */; }; CE80DB2E0FC192D60086DCA6 /* Dialogs.m in Sources */ = {isa = PBXBuildFile; fileRef = CE80DB1C0FC192D60086DCA6 /* Dialogs.m */; }; CE80DB2F0FC192D60086DCA6 /* HSErrorReportWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = CE80DB1E0FC192D60086DCA6 /* HSErrorReportWindow.m */; }; CE80DB300FC192D60086DCA6 /* Outline.m in Sources */ = {isa = PBXBuildFile; fileRef = CE80DB200FC192D60086DCA6 /* Outline.m */; }; @@ -42,11 +42,9 @@ CE848A1909DD85810004CB44 /* Consts.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = CE848A1809DD85810004CB44 /* Consts.h */; }; CEBAE4270FDA97E000B7887D /* BRSingleLineFormatter.m in Sources */ = {isa = PBXBuildFile; fileRef = CEBAE4240FDA97E000B7887D /* BRSingleLineFormatter.m */; }; CEBAE4280FDA97E000B7887D /* NSCharacterSet_Extensions.m in Sources */ = {isa = PBXBuildFile; fileRef = CEBAE4260FDA97E000B7887D /* NSCharacterSet_Extensions.m */; }; - CECA899909DB12CA00A3D774 /* Details.nib in Resources */ = {isa = PBXBuildFile; fileRef = CECA899709DB12CA00A3D774 /* Details.nib */; }; CECA899C09DB132E00A3D774 /* DetailsPanel.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = CECA899A09DB132E00A3D774 /* DetailsPanel.h */; }; CECA899D09DB132E00A3D774 /* DetailsPanel.m in Sources */ = {isa = PBXBuildFile; fileRef = CECA899B09DB132E00A3D774 /* DetailsPanel.m */; }; CEEB135209C837A2004D2330 /* dupeguru.icns in Resources */ = {isa = PBXBuildFile; fileRef = CEEB135109C837A2004D2330 /* dupeguru.icns */; }; - CEF7823809C8AA0200EF38FF /* gear.png in Resources */ = {isa = PBXBuildFile; fileRef = CEF7823709C8AA0200EF38FF /* gear.png */; }; CEFC294609C89E3D00D9F998 /* folder32.png in Resources */ = {isa = PBXBuildFile; fileRef = CEFC294509C89E3D00D9F998 /* folder32.png */; }; CEFC295509C89FF200D9F998 /* details32.png in Resources */ = {isa = PBXBuildFile; fileRef = CEFC295309C89FF200D9F998 /* details32.png */; }; CEFC295609C89FF200D9F998 /* preferences32.png in Resources */ = {isa = PBXBuildFile; fileRef = CEFC295409C89FF200D9F998 /* preferences32.png */; }; @@ -69,11 +67,9 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 089C165DFE840E0CC02AAC07 /* English */ = {isa = PBXFileReference; fileEncoding = 10; lastKnownFileType = text.plist.strings; name = English; path = English.lproj/InfoPlist.strings; sourceTree = ""; }; 1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = /System/Library/Frameworks/Cocoa.framework; sourceTree = ""; }; 13E42FB307B3F0F600E4EEF1 /* CoreData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreData.framework; path = /System/Library/Frameworks/CoreData.framework; sourceTree = ""; }; 29B97316FDCFA39411CA2CEA /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = SOURCE_ROOT; }; - 29B97319FDCFA39411CA2CEA /* English */ = {isa = PBXFileReference; lastKnownFileType = wrapper.nib; name = English; path = English.lproj/MainMenu.nib; sourceTree = ""; }; 29B97324FDCFA39411CA2CEA /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = /System/Library/Frameworks/AppKit.framework; sourceTree = ""; }; 29B97325FDCFA39411CA2CEA /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = /System/Library/Frameworks/Foundation.framework; sourceTree = ""; }; 8D1107310486CEB800E47090 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = SOURCE_ROOT; }; @@ -87,12 +83,14 @@ CE381C9A09914ADF003581CE /* ResultWindow.m */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.objc; path = ResultWindow.m; sourceTree = SOURCE_ROOT; }; CE381C9B09914ADF003581CE /* ResultWindow.h */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.h; path = ResultWindow.h; sourceTree = SOURCE_ROOT; }; CE381CF509915304003581CE /* dg_cocoa.plugin */ = {isa = PBXFileReference; lastKnownFileType = folder; name = dg_cocoa.plugin; path = py/dist/dg_cocoa.plugin; sourceTree = SOURCE_ROOT; }; - CE3AA46609DB207900DB3A21 /* English */ = {isa = PBXFileReference; lastKnownFileType = wrapper.nib; name = English; path = English.lproj/Directories.nib; sourceTree = ""; }; CE6044EA0FE6796200B71262 /* DetailsPanel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = DetailsPanel.h; path = dgbase/DetailsPanel.h; sourceTree = SOURCE_ROOT; }; CE6044EB0FE6796200B71262 /* DetailsPanel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = DetailsPanel.m; path = dgbase/DetailsPanel.m; sourceTree = SOURCE_ROOT; }; CE68EE6509ABC48000971085 /* DirectoryPanel.h */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.h; path = DirectoryPanel.h; sourceTree = SOURCE_ROOT; }; CE68EE6609ABC48000971085 /* DirectoryPanel.m */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.objc; path = DirectoryPanel.m; sourceTree = SOURCE_ROOT; }; CE6E0F3C1054EC62008D9390 /* dsa_pub.pem */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = dsa_pub.pem; path = dgbase/dsa_pub.pem; sourceTree = ""; }; + CE77C89C10946C6D0078B0DB /* DirectoryPanel.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = DirectoryPanel.xib; sourceTree = ""; }; + CE77C8A010946C840078B0DB /* MainMenu.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = MainMenu.xib; path = ../../xib/MainMenu.xib; sourceTree = ""; }; + CE77C8A710946CE20078B0DB /* DetailsPanel.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = DetailsPanel.xib; path = ../../xib/DetailsPanel.xib; sourceTree = ""; }; CE80DB1B0FC192D60086DCA6 /* Dialogs.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Dialogs.h; path = cocoalib/Dialogs.h; sourceTree = SOURCE_ROOT; }; CE80DB1C0FC192D60086DCA6 /* Dialogs.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Dialogs.m; path = cocoalib/Dialogs.m; sourceTree = SOURCE_ROOT; }; CE80DB1D0FC192D60086DCA6 /* HSErrorReportWindow.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = HSErrorReportWindow.h; path = cocoalib/HSErrorReportWindow.h; sourceTree = SOURCE_ROOT; }; @@ -132,11 +130,9 @@ CEBAE4240FDA97E000B7887D /* BRSingleLineFormatter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BRSingleLineFormatter.m; path = cocoalib/brsinglelineformatter/BRSingleLineFormatter.m; sourceTree = SOURCE_ROOT; }; CEBAE4250FDA97E000B7887D /* NSCharacterSet_Extensions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = NSCharacterSet_Extensions.h; path = cocoalib/brsinglelineformatter/NSCharacterSet_Extensions.h; sourceTree = SOURCE_ROOT; }; CEBAE4260FDA97E000B7887D /* NSCharacterSet_Extensions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = NSCharacterSet_Extensions.m; path = cocoalib/brsinglelineformatter/NSCharacterSet_Extensions.m; sourceTree = SOURCE_ROOT; }; - CECA899809DB12CA00A3D774 /* English */ = {isa = PBXFileReference; lastKnownFileType = wrapper.nib; name = English; path = English.lproj/Details.nib; sourceTree = ""; }; CECA899A09DB132E00A3D774 /* DetailsPanel.h */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.h; path = DetailsPanel.h; sourceTree = ""; }; CECA899B09DB132E00A3D774 /* DetailsPanel.m */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.objc; path = DetailsPanel.m; sourceTree = ""; }; CEEB135109C837A2004D2330 /* dupeguru.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; path = dupeguru.icns; sourceTree = ""; }; - CEF7823709C8AA0200EF38FF /* gear.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = gear.png; path = images/gear.png; sourceTree = ""; }; CEFC294509C89E3D00D9F998 /* folder32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = folder32.png; path = images/folder32.png; sourceTree = SOURCE_ROOT; }; CEFC295309C89FF200D9F998 /* details32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = details32.png; path = images/details32.png; sourceTree = SOURCE_ROOT; }; CEFC295409C89FF200D9F998 /* preferences32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = preferences32.png; path = images/preferences32.png; sourceTree = SOURCE_ROOT; }; @@ -228,16 +224,13 @@ 29B97317FDCFA39411CA2CEA /* Resources */ = { isa = PBXGroup; children = ( + CE77C89A10946C6D0078B0DB /* xib */, CE073F5409CAE1A3005C1D2F /* dupeguru_pe_help */, CE381CF509915304003581CE /* dg_cocoa.plugin */, CEFC294309C89E0000D9F998 /* images */, CEEB135109C837A2004D2330 /* dupeguru.icns */, 8D1107310486CEB800E47090 /* Info.plist */, - 089C165CFE840E0CC02AAC07 /* InfoPlist.strings */, CE6E0F3C1054EC62008D9390 /* dsa_pub.pem */, - CECA899709DB12CA00A3D774 /* Details.nib */, - CE3AA46509DB207900DB3A21 /* Directories.nib */, - 29B97318FDCFA39411CA2CEA /* MainMenu.nib */, ); name = Resources; sourceTree = ""; @@ -251,6 +244,17 @@ name = Frameworks; sourceTree = ""; }; + CE77C89A10946C6D0078B0DB /* xib */ = { + isa = PBXGroup; + children = ( + CE77C8A010946C840078B0DB /* MainMenu.xib */, + CE77C8A710946CE20078B0DB /* DetailsPanel.xib */, + CE77C89C10946C6D0078B0DB /* DirectoryPanel.xib */, + ); + name = xib; + path = dgbase/xib; + sourceTree = ""; + }; CE80DB1A0FC192AB0086DCA6 /* cocoalib */ = { isa = PBXGroup; children = ( @@ -318,7 +322,6 @@ isa = PBXGroup; children = ( CEFCDE2C0AB0418600C33A93 /* dgpe_logo_32.png */, - CEF7823709C8AA0200EF38FF /* gear.png */, CEFC295309C89FF200D9F998 /* details32.png */, CEFC295409C89FF200D9F998 /* preferences32.png */, CEFC294509C89E3D00D9F998 /* folder32.png */, @@ -354,7 +357,7 @@ 29B97313FDCFA39411CA2CEA /* Project object */ = { isa = PBXProject; buildConfigurationList = C01FCF4E08A954540054247B /* Build configuration list for PBXProject "dupeguru" */; - compatibilityVersion = "Xcode 2.4"; + compatibilityVersion = "Xcode 3.2"; hasScannedForEncodings = 1; mainGroup = 29B97314FDCFA39411CA2CEA /* dupeguru */; projectDirPath = ""; @@ -370,22 +373,20 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 8D11072A0486CEB800E47090 /* MainMenu.nib in Resources */, - 8D11072B0486CEB800E47090 /* InfoPlist.strings in Resources */, CE381D0509915304003581CE /* dg_cocoa.plugin in Resources */, CE073F6309CAE1A3005C1D2F /* dupeguru_pe_help in Resources */, CEEB135209C837A2004D2330 /* dupeguru.icns in Resources */, CEFC294609C89E3D00D9F998 /* folder32.png in Resources */, CEFC295509C89FF200D9F998 /* details32.png in Resources */, CEFC295609C89FF200D9F998 /* preferences32.png in Resources */, - CEF7823809C8AA0200EF38FF /* gear.png in Resources */, - CECA899909DB12CA00A3D774 /* Details.nib in Resources */, - CE3AA46709DB207900DB3A21 /* Directories.nib in Resources */, CEFCDE2D0AB0418600C33A93 /* dgpe_logo_32.png in Resources */, CE80DB760FC194760086DCA6 /* ErrorReportWindow.xib in Resources */, CE80DB770FC194760086DCA6 /* progress.nib in Resources */, CE80DB780FC194760086DCA6 /* registration.nib in Resources */, CE6E0F3D1054EC62008D9390 /* dsa_pub.pem in Resources */, + CE77C89E10946C6D0078B0DB /* DirectoryPanel.xib in Resources */, + CE77C8A110946C840078B0DB /* MainMenu.xib in Resources */, + CE77C8A810946CE20078B0DB /* DetailsPanel.xib in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -425,30 +426,6 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXVariantGroup section */ - 089C165CFE840E0CC02AAC07 /* InfoPlist.strings */ = { - isa = PBXVariantGroup; - children = ( - 089C165DFE840E0CC02AAC07 /* English */, - ); - name = InfoPlist.strings; - sourceTree = ""; - }; - 29B97318FDCFA39411CA2CEA /* MainMenu.nib */ = { - isa = PBXVariantGroup; - children = ( - 29B97319FDCFA39411CA2CEA /* English */, - ); - name = MainMenu.nib; - sourceTree = SOURCE_ROOT; - }; - CE3AA46509DB207900DB3A21 /* Directories.nib */ = { - isa = PBXVariantGroup; - children = ( - CE3AA46609DB207900DB3A21 /* English */, - ); - name = Directories.nib; - sourceTree = ""; - }; CE80DB700FC194760086DCA6 /* ErrorReportWindow.xib */ = { isa = PBXVariantGroup; children = ( @@ -473,39 +450,9 @@ name = registration.nib; sourceTree = SOURCE_ROOT; }; - CECA899709DB12CA00A3D774 /* Details.nib */ = { - isa = PBXVariantGroup; - children = ( - CECA899809DB12CA00A3D774 /* English */, - ); - name = Details.nib; - sourceTree = ""; - }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ - C01FCF4B08A954540054247B /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - COPY_PHASE_STRIP = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(FRAMEWORK_SEARCH_PATHS)", - "$(SRCROOT)/cocoalib/build/Release", - "$(FRAMEWORK_SEARCH_PATHS_QUOTED_FOR_TARGET_1)", - ); - FRAMEWORK_SEARCH_PATHS_QUOTED_FOR_TARGET_1 = "\"$(SRCROOT)/dgbase/build/Release\""; - GCC_DYNAMIC_NO_PIC = NO; - GCC_ENABLE_FIX_AND_CONTINUE = YES; - GCC_MODEL_TUNING = G5; - GCC_OPTIMIZATION_LEVEL = 0; - INFOPLIST_FILE = Info.plist; - INSTALL_PATH = "$(HOME)/Applications"; - PRODUCT_NAME = dupeGuru; - WRAPPER_EXTENSION = app; - ZERO_LINK = YES; - }; - name = Debug; - }; C01FCF4C08A954540054247B /* Release */ = { isa = XCBuildConfiguration; buildSettings = { @@ -526,31 +473,15 @@ }; name = Release; }; - C01FCF4F08A954540054247B /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - GCC_C_LANGUAGE_STANDARD = c99; - GCC_WARN_ABOUT_RETURN_TYPE = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.4; - PREBINDING = NO; - SDKROOT = /Developer/SDKs/MacOSX10.4u.sdk; - }; - name = Debug; - }; C01FCF5008A954540054247B /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - ARCHS = "$(ARCHS_STANDARD_32_BIT_PRE_XCODE_3_1)"; - ARCHS_STANDARD_32_BIT_PRE_XCODE_3_1 = "ppc i386"; - FRAMEWORK_SEARCH_PATHS = ""; + ARCHS = "$(ARCHS_STANDARD_32_BIT)"; GCC_C_LANGUAGE_STANDARD = c99; - GCC_VERSION = 4.0; GCC_WARN_ABOUT_RETURN_TYPE = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.4; - PREBINDING = NO; - SDKROOT = /Developer/SDKs/MacOSX10.4u.sdk; + MACOSX_DEPLOYMENT_TARGET = 10.5; + SDKROOT = macosx10.5; }; name = Release; }; @@ -560,7 +491,6 @@ C01FCF4A08A954540054247B /* Build configuration list for PBXNativeTarget "dupeguru" */ = { isa = XCConfigurationList; buildConfigurations = ( - C01FCF4B08A954540054247B /* Debug */, C01FCF4C08A954540054247B /* Release */, ); defaultConfigurationIsVisible = 0; @@ -569,7 +499,6 @@ C01FCF4E08A954540054247B /* Build configuration list for PBXProject "dupeguru" */ = { isa = XCConfigurationList; buildConfigurations = ( - C01FCF4F08A954540054247B /* Debug */, C01FCF5008A954540054247B /* Release */, ); defaultConfigurationIsVisible = 0; diff --git a/pe/cocoa/xib/DetailsPanel.xib b/pe/cocoa/xib/DetailsPanel.xib new file mode 100644 index 00000000..cab70b29 --- /dev/null +++ b/pe/cocoa/xib/DetailsPanel.xib @@ -0,0 +1,1472 @@ + + + + 1050 + 10B504 + 740 + 1038.2 + 437.00 + + com.apple.InterfaceBuilder.CocoaPlugin + 740 + + + YES + + + + YES + com.apple.InterfaceBuilder.CocoaPlugin + + + YES + + YES + + + YES + + + + YES + + DetailsPanel + + + FirstResponder + + + NSApplication + + + 155 + 2 + {{634, 317}, {593, 398}} + -260571136 + Details of Selected File + + NSPanel + + + View + + {1.79769e+308, 1.79769e+308} + {451, 161} + + + 256 + + YES + + + 266 + + YES + + + 2304 + + YES + + + 256 + {591, 147} + + 2 + YES + + + 256 + {591, 17} + + + + + + -2147483392 + {{-26, 0}, {16, 17}} + + + + YES + + 0 + 74 + 40 + 1000 + + 75628096 + 2048 + Attribute + + LucidaGrande + 11 + 3100 + + + 3 + MC4zMzMzMzI5OQA + + + 6 + System + headerTextColor + + 3 + MAA + + + + + 337772096 + 2048 + + + + 6 + System + controlBackgroundColor + + 3 + MC42NjY2NjY2NjY3AA + + + + 6 + System + controlTextColor + + + + 2 + YES + + + + 1 + 260 + 40 + 1000 + + 75628096 + 2048 + Selected + + + + + + 337772096 + 2048 + + + + + + 3 + YES + + + + 2 + 248 + 56.4755859375 + 1000 + + 75628096 + 2048 + Reference + + + 6 + System + headerColor + + 3 + MQA + + + + + + 337772096 + 2048 + + + + + + 3 + YES + + + + 3 + 2 + + + 6 + System + gridColor + + 3 + MC41AA + + + 14 + 1111523328 + + + 1 + 15 + 0 + YES + 0 + + + {{1, 17}, {591, 147}} + + + + + 4 + + + + -2147483392 + {{-30, 17}, {15, 147}} + + + _doScroller: + 0.91874998807907104 + + + + -2147483392 + {{-100, -100}, {394, 15}} + + 1 + + _doScroller: + 0.96332520246505737 + + + + 2304 + + YES + + + {{1, 0}, {591, 17}} + + + + + 4 + + + + {{0, 233}, {593, 165}} + + + 530 + + + + + + QSAAAEEgAABBgAAAQYAAAA + + + + 274 + + YES + + + 256 + + YES + + + 266 + {{-3, 201}, {294, 17}} + + YES + + 67239424 + 138412032 + Selected + + LucidaGrande + 13 + 1044 + + + + 6 + System + controlColor + + + + + + + + 274 + + YES + + YES + Apple PDF pasteboard type + Apple PICT pasteboard type + Apple PNG pasteboard type + NSFilenamesPboardType + NeXT Encapsulated PostScript v1.2 pasteboard type + NeXT TIFF v4.0 pasteboard type + + + {288, 193} + + YES + + 130560 + 33554432 + + NSImage + NSApplicationIcon + + 0 + 0 + 0 + NO + + YES + + + + 1289 + + {{255, 201}, {16, 16}} + + 28938 + 100 + + + {291, 225} + + NSView + + + + 256 + + YES + + + 266 + {{-3, 203}, {295, 17}} + + YES + + 67239424 + 138412032 + Reference + + + + + + + + + 274 + + YES + + YES + Apple PDF pasteboard type + Apple PICT pasteboard type + Apple PNG pasteboard type + NSFilenamesPboardType + NeXT Encapsulated PostScript v1.2 pasteboard type + NeXT TIFF v4.0 pasteboard type + + + {289, 195} + + YES + + 130560 + 33554432 + + 0 + 0 + 0 + NO + + YES + + + + 1289 + + {{257, 203}, {16, 16}} + + 28938 + 100 + + + {{300, 0}, {293, 225}} + + NSView + + + {593, 225} + + YES + + + {593, 398} + + + {{0, 0}, {1440, 878}} + {451, 177} + {1.79769e+308, 1.79769e+308} + + + + + YES + + + window + + + + 12 + + + + detailsTable + + + + 13 + + + + refImage + + + + 25 + + + + dupeImage + + + + 26 + + + + dupeProgressIndicator + + + + 30 + + + + refProgressIndicator + + + + 31 + + + + + YES + + 0 + + + + + + -2 + + + File's Owner + + + -1 + + + First Responder + + + 5 + + + YES + + + + details + + + 6 + + + YES + + + + + + + 7 + + + YES + + + + + + + + + 8 + + + YES + + + + + + + + 9 + + + YES + + + + + + 10 + + + YES + + + + + + 11 + + + YES + + + + + + 20 + + + YES + + + + + + + 18 + + + YES + + + + + + + + 14 + + + YES + + + + + + 16 + + + YES + + + + + + 27 + + + + + 19 + + + YES + + + + + + + + 15 + + + YES + + + + + + 17 + + + YES + + + + + + 29 + + + + + 33 + + + + + 34 + + + + + 35 + + + + + 36 + + + + + 37 + + + + + 38 + + + + + 39 + + + + + 40 + + + + + 41 + + + + + 42 + + + + + -3 + + + Application + + + + + YES + + YES + -3.IBPluginDependency + 10.IBPluginDependency + 10.ImportedFromIB2 + 11.IBPluginDependency + 11.ImportedFromIB2 + 14.IBPluginDependency + 14.ImportedFromIB2 + 15.IBPluginDependency + 15.ImportedFromIB2 + 16.IBPluginDependency + 16.ImportedFromIB2 + 17.IBPluginDependency + 17.ImportedFromIB2 + 18.IBPluginDependency + 18.ImportedFromIB2 + 19.IBPluginDependency + 19.ImportedFromIB2 + 20.IBPluginDependency + 20.ImportedFromIB2 + 27.IBPluginDependency + 27.ImportedFromIB2 + 29.IBPluginDependency + 29.ImportedFromIB2 + 33.IBPluginDependency + 34.IBPluginDependency + 35.IBPluginDependency + 36.IBPluginDependency + 37.IBPluginDependency + 37.IBShouldRemoveOnLegacySave + 38.IBPluginDependency + 38.IBShouldRemoveOnLegacySave + 39.IBPluginDependency + 39.IBShouldRemoveOnLegacySave + 40.IBPluginDependency + 40.IBShouldRemoveOnLegacySave + 41.IBPluginDependency + 41.IBShouldRemoveOnLegacySave + 42.IBPluginDependency + 42.IBShouldRemoveOnLegacySave + 5.IBEditorWindowLastContentRect + 5.IBPluginDependency + 5.IBWindowTemplateEditedContentRect + 5.ImportedFromIB2 + 5.windowTemplate.hasMinSize + 5.windowTemplate.minSize + 6.IBPluginDependency + 6.ImportedFromIB2 + 7.IBPluginDependency + 7.ImportedFromIB2 + 8.CustomClassName + 8.IBPluginDependency + 8.ImportedFromIB2 + 9.IBPluginDependency + 9.ImportedFromIB2 + + + YES + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + {{88, 453}, {593, 398}} + com.apple.InterfaceBuilder.CocoaPlugin + {{88, 453}, {593, 398}} + + + {451, 161} + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + TableView + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + + + + YES + + + YES + + + + + YES + + + YES + + + + 42 + + + + YES + + DetailsPanel + DetailsPanelBase + + YES + + YES + dupeImage + dupeProgressIndicator + refImage + refProgressIndicator + + + YES + NSImageView + NSProgressIndicator + NSImageView + NSProgressIndicator + + + + IBProjectSource + DetailsPanel.h + + + + DetailsPanel + DetailsPanelBase + + detailsTable + NSTableView + + + IBUserSource + + + + + DetailsPanelBase + NSWindowController + + detailsTable + TableView + + + IBProjectSource + dgbase/DetailsPanel.h + + + + FirstResponder + NSObject + + IBUserSource + + + + + PyApp + PyRegistrable + + IBProjectSource + cocoalib/PyApp.h + + + + TableView + NSTableView + + py + PyApp + + + IBProjectSource + cocoalib/Table.h + + + + TableView + NSTableView + + IBUserSource + + + + + + YES + + NSActionCell + NSCell + + IBFrameworkSource + AppKit.framework/Headers/NSActionCell.h + + + + NSApplication + NSResponder + + IBFrameworkSource + AppKit.framework/Headers/NSApplication.h + + + + NSApplication + + IBFrameworkSource + AppKit.framework/Headers/NSApplicationScripting.h + + + + NSApplication + + IBFrameworkSource + AppKit.framework/Headers/NSColorPanel.h + + + + NSApplication + + IBFrameworkSource + AppKit.framework/Headers/NSHelpManager.h + + + + NSApplication + + IBFrameworkSource + AppKit.framework/Headers/NSPageLayout.h + + + + NSApplication + + IBFrameworkSource + AppKit.framework/Headers/NSUserInterfaceItemSearching.h + + + + NSCell + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSCell.h + + + + NSControl + NSView + + IBFrameworkSource + AppKit.framework/Headers/NSControl.h + + + + NSFormatter + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSFormatter.h + + + + NSImageCell + NSCell + + IBFrameworkSource + AppKit.framework/Headers/NSImageCell.h + + + + NSImageView + NSControl + + IBFrameworkSource + AppKit.framework/Headers/NSImageView.h + + + + NSMenu + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSMenu.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSAccessibility.h + + + + NSObject + + + + NSObject + + + + NSObject + + + + NSObject + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSDictionaryController.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSDragging.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSFontManager.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSFontPanel.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSKeyValueBinding.h + + + + NSObject + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSNibLoading.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSOutlineView.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSPasteboard.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSSavePanel.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSTableView.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSToolbarItem.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSView.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSArchiver.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSClassDescription.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSError.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSFileManager.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSKeyValueCoding.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSKeyValueObserving.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSKeyedArchiver.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSObject.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSObjectScripting.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSPortCoder.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSRunLoop.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSScriptClassDescription.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSScriptKeyValueCoding.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSScriptObjectSpecifiers.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSScriptWhoseTests.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSThread.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSURL.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSURLConnection.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSURLDownload.h + + + + NSObject + + IBFrameworkSource + Sparkle.framework/Headers/SUAppcast.h + + + + NSObject + + IBFrameworkSource + Sparkle.framework/Headers/SUUpdater.h + + + + NSPanel + NSWindow + + IBFrameworkSource + AppKit.framework/Headers/NSPanel.h + + + + NSProgressIndicator + NSView + + IBFrameworkSource + AppKit.framework/Headers/NSProgressIndicator.h + + + + NSResponder + + IBFrameworkSource + AppKit.framework/Headers/NSInterfaceStyle.h + + + + NSResponder + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSResponder.h + + + + NSScrollView + NSView + + IBFrameworkSource + AppKit.framework/Headers/NSScrollView.h + + + + NSScroller + NSControl + + IBFrameworkSource + AppKit.framework/Headers/NSScroller.h + + + + NSSplitView + NSView + + IBFrameworkSource + AppKit.framework/Headers/NSSplitView.h + + + + NSTableColumn + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSTableColumn.h + + + + NSTableHeaderView + NSView + + IBFrameworkSource + AppKit.framework/Headers/NSTableHeaderView.h + + + + NSTableView + NSControl + + + + NSTextField + NSControl + + IBFrameworkSource + AppKit.framework/Headers/NSTextField.h + + + + NSTextFieldCell + NSActionCell + + IBFrameworkSource + AppKit.framework/Headers/NSTextFieldCell.h + + + + NSView + + IBFrameworkSource + AppKit.framework/Headers/NSClipView.h + + + + NSView + + IBFrameworkSource + AppKit.framework/Headers/NSMenuItem.h + + + + NSView + + IBFrameworkSource + AppKit.framework/Headers/NSRulerView.h + + + + NSView + NSResponder + + + + NSWindow + + IBFrameworkSource + AppKit.framework/Headers/NSDrawer.h + + + + NSWindow + NSResponder + + IBFrameworkSource + AppKit.framework/Headers/NSWindow.h + + + + NSWindow + + IBFrameworkSource + AppKit.framework/Headers/NSWindowScripting.h + + + + NSWindowController + NSResponder + + showWindow: + id + + + IBFrameworkSource + AppKit.framework/Headers/NSWindowController.h + + + + + 0 + + com.apple.InterfaceBuilder.CocoaPlugin.macosx + + + + com.apple.InterfaceBuilder.CocoaPlugin.macosx + + + + com.apple.InterfaceBuilder.CocoaPlugin.InterfaceBuilder3 + + + YES + + 3 + + diff --git a/pe/cocoa/xib/MainMenu.xib b/pe/cocoa/xib/MainMenu.xib new file mode 100644 index 00000000..6a458727 --- /dev/null +++ b/pe/cocoa/xib/MainMenu.xib @@ -0,0 +1,5501 @@ + + + + 1050 + 10B504 + 740 + 1038.2 + 437.00 + + com.apple.InterfaceBuilder.CocoaPlugin + 740 + + + + + + com.apple.InterfaceBuilder.CocoaPlugin + + + + + NSApplication + + + FirstResponder + + + NSApplication + + + 15 + 2 + {{47, 310}, {557, 400}} + 1886912512 + dupeGuru Picture Edition + NSWindow + + + 2C450DD5-50E5-4BF2-9ED3-6113712C18E6 + + + YES + YES + YES + YES + 1 + 1 + + + + 092EDA0C-232E-4EC4-9334-68F62220C787 + + Directories + Directories + + + + NSImage + folder32 + + + + {0, 0} + {0, 0} + YES + YES + -1 + YES + 0 + + + + 0A3C1C35-6A27-453D-A627-C7ADA6CC26B3 + + Action + Action + + + + 256 + {{0, 14}, {58, 26}} + + + YES + + -2076049856 + 2048 + + LucidaGrande + 13 + 1044 + + + 109068543 + 1 + + NSImage + NSActionTemplate + + + + + + 400 + 75 + + + YES + IA + + 1048576 + 2147483647 + 1 + + + NSImage + NSMenuCheckmark + + + NSImage + NSMenuMixedState + + _popUpItemAction: + + + YES + + + OtherViews + + + + + + Send Marked to Trash + + 2147483647 + + + _popUpItemAction: + + + + + Move Marked to... + + 2147483647 + + + _popUpItemAction: + + + + + Copy Marked to... + + 2147483647 + + + _popUpItemAction: + + + + + Remove Marked from Results + + 2147483647 + + + _popUpItemAction: + + + + + YES + YES + + + 1048576 + 2147483647 + + + _popUpItemAction: + + + + + Remove Selected from Results + + 2147483647 + + + _popUpItemAction: + + + + + Add Selected to Ignore List + + 1048576 + 2147483647 + + + _popUpItemAction: + + + + + Make Selected Reference + + 2147483647 + + + _popUpItemAction: + + + + + YES + YES + + + 1048576 + 2147483647 + + + _popUpItemAction: + + + + + Open Selected with Default Application + + 2147483647 + + + _popUpItemAction: + + + + + Reveal Selected in Finder + + 2147483647 + + + _popUpItemAction: + + + + + Rename Selected + + 1048576 + 2147483647 + + + _popUpItemAction: + + + + + YES + 3 + YES + YES + 1 + + + + + + {58, 26} + {58, 26} + YES + YES + 0 + YES + 0 + + + + 26033E1E-95A5-4838-8727-1306C619AB37 + + Power Marker + Power Marker + + + + 256 + {{7, 14}, {67, 24}} + + + YES + + 67239424 + 0 + + LucidaGrande + 11 + 3100 + + + + + 30 + Off + 2 + + + 30 + On + 1 + 2 + + + 1 + + + + + + {67, 24} + {67, 24} + YES + YES + 0 + YES + 0 + + + + 45C339F5-66AC-4590-A395-8BF09951F9DE + + Preferences + Preferences + + + + NSImage + preferences32 + + + + {0, 0} + {0, 0} + YES + YES + -1 + YES + 0 + + + + 67845041-FE64-472F-B902-4364CE189365 + + Start Scanning + Start Scanning + + + + NSImage + dgpe_logo_32 + + + + {0, 0} + {0, 0} + YES + YES + -1 + YES + 0 + + + + 95D92C39-45B7-4C65-8B1C-F33A620227C3 + + Details + Details + + + + NSImage + details32 + + + + {0, 0} + {0, 0} + YES + YES + -1 + YES + 0 + + + + E1E3467D-6276-4BE7-8AEE-942275EEE54D + + Filter + Filter + + + + 258 + {{0, 14}, {81, 22}} + + + YES + + 343014976 + 268436480 + + + Filter + + YES + 1 + + 6 + System + textBackgroundColor + + 3 + MQA + + + + 6 + System + controlTextColor + + 3 + MAA + + + + 130560 + 0 + search + + _searchFieldSearch: + + 138690815 + 0 + + 400 + 75 + + + 130560 + 0 + clear + + + cancel + + + + + _searchFieldCancel: + + 138690815 + 0 + + 400 + 75 + + 10 + YES + + + + + + {81, 22} + {9999, 22} + YES + YES + 0 + YES + 0 + + + + FF475101-7082-44F5-8CF5-65A1ECE54BD2 + + Delta Values + Delta Values + + + + 256 + {{4, 14}, {67, 24}} + + + YES + + 67239424 + 0 + + + + + 30 + Off + 2 + + + 30 + On + 1 + 2 + + + 1 + + + + + + {67, 24} + {67, 24} + YES + YES + 0 + YES + 0 + + + NSToolbarFlexibleSpaceItem + + Flexible Space + + + + + + {1, 5} + {20000, 32} + YES + YES + -1 + YES + 0 + + YES + YES + + + 1048576 + 2147483647 + + + + + + NSToolbarSeparatorItem + + Separator + + + + + + {12, 5} + {12, 1000} + YES + YES + -1 + YES + 0 + + YES + YES + + + 1048576 + 2147483647 + + + + + + NSToolbarSpaceItem + + Space + + + + + + {32, 5} + {32, 32} + YES + YES + -1 + YES + 0 + + YES + YES + + + 1048576 + 2147483647 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {1.79769e+308, 1.79769e+308} + {340, 340} + + + 256 + + + + 274 + + + + 2304 + + + + 274 + {515, 317} + + YES + + + 256 + {515, 17} + + + + + + -2147483392 + {{-26, 0}, {16, 17}} + + + + + mark + 47 + 16 + 1000 + + 75628096 + 2048 + + + + 6 + System + headerColor + + + + 6 + System + headerTextColor + + + + + 67239424 + 131072 + + + LucidaGrande + 12 + 16 + + + 1211912703 + 2 + + NSSwitch + + + + 400 + 75 + + + + + 0 + 202 + 16 + 1000 + + 75628096 + 2048 + Name + + + 3 + MC4zMzMzMzI5OQA + + + + + 337772096 + 2048 + + + + 6 + System + controlBackgroundColor + + 3 + MC42NjY2NjY2NjY3AA + + + + + 3 + YES + + + 0 + YES + compare: + + + + 1 + 128 + 10 + 1000 + + 75628096 + 2048 + Directory + + + + + + 337772096 + 2048 + + + + + + 3 + YES + + + 1 + YES + compare: + + + + 2 + 63 + 10 + 1000 + + 75628096 + 2048 + Size (KB) + + + + + + 337772096 + 67110912 + + + + + + 2 + YES + + + 2 + YES + compare: + + + + 7 + 59.9580078125 + 46.9580078125 + 1000 + + 75628096 + 2048 + Match % + + + + + + 337772096 + 2048 + + + + + + 2 + YES + + + 7 + YES + compare: + + + + 3 + 2 + + + 6 + System + gridColor + + 3 + MC41AA + + + 14 + -901742592 + + + 2 + 1 + 15 + 0 + YES + 0 + + + {{1, 17}, {515, 317}} + + + + + 4 + + + + -2147483392 + {{-30, 17}, {15, 302}} + + + _doScroller: + 0.98739492893218994 + + + + -2147483392 + {{1, -30}, {500, 15}} + + 1 + + _doScroller: + 0.99806201457977295 + + + + 2304 + + + + {{1, 0}, {515, 17}} + + + + + 4 + + + + {{20, 45}, {517, 335}} + + + 562 + + + + + + QSAAAEEgAABBgAAAQYAAAA + + + + 290 + {{17, 20}, {523, 17}} + + YES + + 67239424 + 138412032 + Marked: 0 files, 0 B. Total: 0 files, 0 B. + + + + 6 + System + controlColor + + + + + + + {557, 400} + + + {{0, 0}, {1440, 878}} + {340, 418} + {1.79769e+308, 1.79769e+308} + + + MainMenu + + + + dupeGuru PE + + 1048576 + 2147483647 + + + submenuAction: + + dupeGuru PE + + + + About dupeGuru PE + + 2147483647 + + + + + + Unlock dupeGuru PE + + 1048576 + 2147483647 + + + + + + Check for update... + + 1048576 + 2147483647 + + + + + + YES + YES + + + 1048576 + 2147483647 + + + + + + Preferences... + , + 1048576 + 2147483647 + + + + + + YES + YES + + + 1048576 + 2147483647 + + + + + + Hide dupeGuru PE + h + 1048576 + 2147483647 + + + + + + Hide Others + h + 1572864 + 2147483647 + + + + + + Show All + + 1048576 + 2147483647 + + + + + + YES + YES + + + 1048576 + 2147483647 + + + + + + Quit dupeGuru PE + q + 1048576 + 2147483647 + + + + + _NSAppleMenu + + + + + Edit + + 1048576 + 2147483647 + + + submenuAction: + + + Edit + + + + + Mark All + a + 1048576 + 2147483647 + + + + + + Mark None + A + 1048576 + 2147483647 + + + + + + Invert Marking + a + 1572864 + 2147483647 + + + + + + Mark Selected + a + 1310720 + 2147483647 + + + + + + YES + YES + + + 1048576 + 2147483647 + + + + + + Cut + x + 1048576 + 2147483647 + + + + + + Copy + c + 1048576 + 2147483647 + + + + + + Paste + v + 1048576 + 2147483647 + + + + + + + + + Actions + + 1048576 + 2147483647 + + + submenuAction: + + Actions + + + + Start Duplicate Scan + s + 1048576 + 2147483647 + + + + + + Clear Ignore List + I + 1048576 + 2147483647 + + + + + + Clear Picture Cache + P + 1048576 + 2147483647 + + + + + + Export Results to XHTML + E + 1048576 + 2147483647 + + + + + + YES + YES + + + 1048576 + 2147483647 + + + + + + Send Marked to Trash + t + 1048576 + 2147483647 + + + + + + Move Marked to... + m + 1048576 + 2147483647 + + + + + + Copy Marked to... + m + 1572864 + 2147483647 + + + + + + Remove Marked from Results + r + 1048576 + 2147483647 + + + + + + YES + YES + + + 1048576 + 2147483647 + + + + + + Remove Selected from Results + R + 1048576 + 2147483647 + + + + + + Add Selected to Ignore List + i + 1048576 + 2147483647 + + + + + + Make Selected Reference +  + 1048576 + 2147483647 + + + + + + YES + YES + + + 1048576 + 2147483647 + + + + + + Open Selected with Default Application + DQ + 1048576 + 2147483647 + + + + + + Reveal Selected in Finder + DQ + 1572864 + 2147483647 + + + + + + Rename Selected + Aw + 2147483647 + + + + + + + + + Columns + + 1048576 + 2147483647 + + + submenuAction: + + Columns + + + + File Name + + 1048576 + 2147483647 + 1 + + + + + + Directory + + 1048576 + 2147483647 + 1 + + + 1 + + + + Size + + 1048576 + 2147483647 + 1 + + + 2 + + + + Kind + + 1048576 + 2147483647 + + + 3 + + + + Dimensions + + 1048576 + 2147483647 + + + 4 + + + + Creation + + 1048576 + 2147483647 + + + 5 + + + + Modification + + 1048576 + 2147483647 + + + 6 + + + + Match % + + 1048576 + 2147483647 + 1 + + + 7 + + + + Dupe Count + + 1048576 + 2147483647 + + + 8 + + + + YES + YES + IA + + 1048576 + 2147483647 + + + -1 + + + + Reset to Default + + 1048576 + 2147483647 + + + -1 + + + + + + + Modes + + 1048576 + 2147483647 + + + submenuAction: + + Modes + + + + Power Marker + 1 + 1048576 + 2147483647 + + + + + + Delta Values + 2 + 1048576 + 2147483647 + + + + + + + + + Window + + 1048576 + 2147483647 + + + submenuAction: + + Window + + + + Directory Panel + 3 + 1048576 + 2147483647 + + + + + + Details Panel + 4 + 1048576 + 2147483647 + + + + + + YES + YES + + + 1048576 + 2147483647 + + + + + + Minimize + + 2147483647 + + + + + + Zoom + + 1048576 + 2147483647 + + + + + + Close Window + w + 1048576 + 2147483647 + + + + + + YES + YES + + + 1048576 + 2147483647 + + + + + + Bring All to Front + + 1048576 + 2147483647 + + + + + _NSWindowsMenu + + + + + Help + + 1048576 + 2147483647 + + + submenuAction: + + Help + + + + dupeGuru PE Help + ? + 1048576 + 2147483647 + + + + + + dupeGuru PE Website + + 1048576 + 2147483647 + + + + + + + + _NSMainMenu + + + AppDelegate + + + ResultWindow + + + YES + + + RecentDirectories + + + 3 + 2 + {{92, 350}, {352, 252}} + 1886912512 + dupeGuru PE Preferences + + NSWindow + + + View + + {1.79769e+308, 1.79769e+308} + {213, 107} + + + 256 + + + + 292 + {{120, 213}, {181, 21}} + + YES + + 67239424 + 0 + + + + + Helvetica + 12 + 16 + + + 100 + 1 + 80 + 0.0 + 0 + 1 + NO + NO + + + + + 292 + {{122, 196}, {80, 13}} + + YES + + 67239424 + 272629760 + More results + + LucidaGrande + 10 + 2843 + + + + + + + + + 289 + {{219, 196}, {80, 13}} + + YES + + 67239424 + 71303168 + Less results + + + + + + + + + 292 + {{17, 218}, {100, 14}} + + YES + + 67239424 + 272629760 + Filter hardness: + + + + + + + + + 256 + {{18, 152}, {214, 18}} + + YES + + 67239424 + 0 + Can mix file kind + + + 1211912703 + 2 + + + + 200 + 25 + + + + + 256 + {{304, 218}, {31, 14}} + + YES + + 67239424 + -1874853888 + + + + + + + 0 + + + . + + , + -0 + 0 + + + 0 + -0 + + + + + + + + NaN + + + + 0 + 0 + YES + NO + 1 + AAAAAAAAAAAAAAAAAAAAAA + + + + . + , + NO + YES + YES + + + + + + + + + 256 + {{190, 16}, {148, 32}} + + YES + + 67239424 + 134217728 + Reset to Defaults + + + -2038284033 + 1 + + + + + + 200 + 25 + + + + + 292 + {{20, 71}, {85, 13}} + + YES + + 67239424 + 272629760 + Copy and Move: + + + + + + + + + 292 + {{110, 60}, {216, 26}} + + YES + + -2076049856 + 2048 + + + 109199615 + 1 + + + + + + 400 + 75 + + + Right in destination + + 1048576 + 2147483647 + 1 + + + _popUpItemAction: + + + YES + + + OtherViews + + + + + + Recreate relative path + + 1048576 + 2147483647 + + + _popUpItemAction: + + + + + Recreate absolute path + + 1048576 + 2147483647 + + + _popUpItemAction: + + + + + 3 + YES + YES + 1 + + + + + 256 + {{18, 172}, {214, 18}} + + YES + + 67239424 + 0 + Match scaled pictures together + + + 1211912703 + 2 + + + + 200 + 25 + + + + + 256 + {{18, 90}, {283, 18}} + + YES + + 67239424 + 0 + Check for update on startup + + + 1211912703 + 2 + + + + 200 + 25 + + + + + 256 + {{18, 112}, {242, 18}} + + YES + + 67239424 + 0 + Remove empty folders on delete or move + + + 1211912703 + 2 + + + + 200 + 25 + + + + + 256 + {{18, 132}, {228, 18}} + + YES + + 67239424 + 0 + Use regular expressions when filtering + + + 1211912703 + 2 + + + + 200 + 25 + + + + {352, 252} + + {{0, 0}, {1440, 878}} + {213, 129} + {1.79769e+308, 1.79769e+308} + + + PyDupeGuru + + + Menu + + + + Remove Selected from Results + + 1048576 + 2147483647 + + + + + + Add Selected to Ignore List + + 1048576 + 2147483647 + + + + + + Make Selected Reference + + 1048576 + 2147483647 + + + + + + YES + YES + + + 1048576 + 2147483647 + + + + + + Open Selected with Default Application + + 1048576 + 2147483647 + + + + + + Reveal Selected in Finder + + 1048576 + 2147483647 + + + + + + Rename Selected + + 1048576 + 2147483647 + + + + + + + + SUUpdater + + + + + + + performMiniaturize: + + + + 37 + + + + arrangeInFront: + + + + 39 + + + + showHelp: + + + + 122 + + + + terminate: + + + + 139 + + + + orderFrontStandardAboutPanel: + + + + 142 + + + + hideOtherApplications: + + + + 146 + + + + hide: + + + + 152 + + + + unhideAllApplications: + + + + 153 + + + + performZoom: + + + + 198 + + + + delegate + + + + 207 + + + + delegate + + + + 208 + + + + window + + + + 210 + + + + result + + + + 211 + + + + delegate + + + + 212 + + + + matches + + + + 245 + + + + initialFirstResponder + + + + 279 + + + + delegate + + + + 410 + + + + markToggle: + + + + 414 + + + + stats + + + + 445 + + + + delegate + + + + 502 + + + + recentDirectories + + + + 503 + + + + makeKeyAndOrderFront: + + + + 543 + + + + value: values.minMatchPercentage + + + + + + value: values.minMatchPercentage + value + values.minMatchPercentage + 2 + + + 549 + + + + deleteMarked: + + + + 606 + + + + moveMarked: + + + + 607 + + + + copyMarked: + + + + 608 + + + + removeMarked: + + + + 609 + + + + switchSelected: + + + + 610 + + + + removeSelected: + + + + 611 + + + + py + + + + 614 + + + + py + + + + 616 + + + + toggleColumn: + + + + 627 + + + + toggleColumn: + + + + 628 + + + + toggleColumn: + + + + 629 + + + + toggleColumn: + + + + 630 + + + + toggleColumn: + + + + 631 + + + + toggleColumn: + + + + 632 + + + + toggleColumn: + + + + 633 + + + + toggleColumn: + + + + 647 + + + + value: values.mixFileKind + + + + + + value: values.mixFileKind + value + values.mixFileKind + 2 + + + 656 + + + + openSelected: + + + + 660 + + + + revealSelected: + + + + 661 + + + + menu + + + + 663 + + + + toggleColumn: + + + + 706 + + + + openSelected: + + + + 709 + + + + revealSelected: + + + + 711 + + + + value: values.minMatchPercentage + + + + + + value: values.minMatchPercentage + value + values.minMatchPercentage + 2 + + + 713 + + + + switchSelected: + + + + 716 + + + + preferencesPanel + + + + 718 + + + + actionMenu + + + + 726 + + + + deleteMarked: + + + + 741 + + + + moveMarked: + + + + 742 + + + + copyMarked: + + + + 743 + + + + removeMarked: + + + + 744 + + + + removeSelected: + + + + 745 + + + + switchSelected: + + + + 746 + + + + openSelected: + + + + 747 + + + + revealSelected: + + + + 748 + + + + unlockApp: + + + + 755 + + + + unlockMenuItem + + + + 756 + + + + app + + + + 757 + + + + toggleDirectories: + + + + 758 + + + + py + + + + 764 + + + + removeSelected: + + + + 873 + + + + changeDelta: + + + + 882 + + + + deltaSwitch + + + + 883 + + + + selectedIndex: values.recreatePathType + + + + + + selectedIndex: values.recreatePathType + selectedIndex + values.recreatePathType + 2 + + + 914 + + + + ignoreSelected: + + + + 921 + + + + ignoreSelected: + + + + 923 + + + + performClose: + + + + 925 + + + + startDuplicateScan: + + + + 929 + + + + clearIgnoreList: + + + + 930 + + + + revertToInitialValues: + + + + 932 + + + + renameSelected: + + + + 934 + + + + renameSelected: + + + + 936 + + + + ignoreSelected: + + + + 940 + + + + renameSelected: + + + + 941 + + + + resetColumnsToDefault: + + + + 945 + + + + columnsMenu + + + + 946 + + + + toggleDetailsPanel: + + + + 947 + + + + openWebsite: + + + + 949 + + + + value: values.matchScaled + + + + + + value: values.matchScaled + value + values.matchScaled + 2 + + + 951 + + + + clearPictureCache: + + + + 953 + + + + checkForUpdates: + + + + 956 + + + + value: values.SUCheckAtStartup + + + + + + value: values.SUCheckAtStartup + value + values.SUCheckAtStartup + 2 + + + 959 + + + + pmSwitch + + + + 963 + + + + changePowerMarker: + + + + 964 + + + + togglePowerMarker: + + + + 969 + + + + toggleDelta: + + + + 970 + + + + exportToXHTML: + + + + 972 + + + + paste: + + + + 1003 + + + + cut: + + + + 1004 + + + + copy: + + + + 1006 + + + + markAll: + + + + 1024 + + + + markNone: + + + + 1025 + + + + markInvert: + + + + 1026 + + + + markSelected: + + + + 1027 + + + + filter: + + + + 1030 + + + + filterField + + + + 1032 + + + + value: values.useRegexpFilter + + + + + + value: values.useRegexpFilter + value + values.useRegexpFilter + 2 + + + 1065 + + + + value: values.removeEmptyFolders + + + + + + value: values.removeEmptyFolders + value + values.removeEmptyFolders + 2 + + + 1066 + + + + nextKeyView + + + + 1067 + + + + nextKeyView + + + + 1068 + + + + nextKeyView + + + + 1069 + + + + nextKeyView + + + + 1070 + + + + nextKeyView + + + + 1071 + + + + nextKeyView + + + + 1072 + + + + nextKeyView + + + + 1073 + + + + startDuplicateScan: + + + + 1117 + + + + toggleDirectories: + + + + 1118 + + + + toggleDetailsPanel: + + + + 1119 + + + + showPreferencesPanel: + + + + 1120 + + + + + + 0 + + + + + + -2 + + + File's Owner + + + -1 + + + First Responder + + + -3 + + + Application + + + 21 + + + + + + + Window + + + 2 + + + + + + + + + 219 + + + + + + + + + + + 220 + + + + + + + + + + + + 222 + + + + + + + + 223 + + + + + + + + 233 + + + + + + + + 406 + + + + + + + + 407 + + + + + 931 + + + + + + + + 291 + + + + + + + + 29 + + + + + + + + + + + + MainMenu + + + 19 + + + + + + + + 24 + + + + + + + + + + + + + + + 5 + + + + + 23 + + + + + 92 + + + + + 197 + + + + + 398 + + + + + 399 + + + + + 579 + + + + + 924 + + + + + 56 + + + + + + + + 57 + + + + + + + + + + + + + + + + + + 58 + + + + + 134 + + + + + 136 + + + + + 144 + + + + + 145 + + + + + 149 + + + + + 150 + + + + + 541 + + + + + 542 + + + + + 754 + + + + + 955 + + + + + 103 + + + + + + + + 106 + + + + + + + + + 111 + + + + + 948 + + + + + 597 + + + + + + + + 598 + + + + + + + + + + + + + + + + + + + + + + + + 599 + + + + + 600 + + + + + 601 + + + + + 602 + + + + + 603 + + + + + 604 + + + + + 605 + + + + + 707 + + + + + 708 + + + + + 710 + + + + + 922 + + + + + 926 + + + + + 927 + + + + + 928 + + + + + 933 + + + + + 952 + + + + + 971 + + + + + 618 + + + + + + + + 619 + + + + + + + + + + + + + + + + + + 620 + + + + + 621 + + + + + 622 + + + + + 623 + + + + + 624 + + + + + 625 + + + + + 626 + + + + + 646 + + + + + 705 + + + + + 943 + + + + + 944 + + + + + 965 + + + + + + + + 966 + + + + + + + + + 967 + + + + + 968 + + + + + 973 + + + + + + + + 974 + + + + + + + + + + + + + + + 990 + + + + + 991 + + + + + 996 + + + + + 1019 + + + + + 1020 + + + + + 1021 + + + + + 1022 + + + + + 1023 + + + + + 206 + + + AppDelegate + + + 209 + + + ResultWindow + + + 468 + + + Shared Defaults + + + 497 + + + RecentDirectoriesController + + + 523 + + + + + + preferences + + + 524 + + + + + + + + + + + + + + + + + + + + 531 + + + + + + + + 532 + + + + + + + + 533 + + + + + + + + 534 + + + + + + + + 649 + + + + + + + + 712 + + + + + + + + 750 + + + + + + + + 904 + + + + + + + + 905 + + + + + + + + 950 + + + + + + + + 958 + + + + + + + + 1059 + + + + + + + + 1060 + + + + + + + + 613 + + + PyDupeGuru + + + 657 + + + + + + + + + + + + matches_context + + + 658 + + + + + 659 + + + + + 715 + + + + + 872 + + + + + 937 + + + + + 938 + + + + + 939 + + + + + 954 + + + SUUpdater + + + 1076 + + + + + 1077 + + + + + 1078 + + + + + 1079 + + + + + 1080 + + + + + 1081 + + + + + 1082 + + + + + + + + 1083 + + + + + 1084 + + + + + 1085 + + + + + + + + 1086 + + + + + 1087 + + + + + 1088 + + + + + 1089 + + + + + 1094 + + + + + 1095 + + + + + 1096 + + + + + 1097 + + + + + 714 + + + + + 906 + + + + + + + + + + 913 + + + + + 909 + + + + + 908 + + + + + 1098 + + + + + 1099 + + + + + 1100 + + + + + 1101 + + + + + + + + + + + + + + + + + + 1104 + + + + + 1106 + + + + + 1107 + + + + + 1109 + + + + + 1110 + + + + + 1111 + + + + + 1112 + + + + + 1113 + + + + + + + + 720 + + + + + + + + 1090 + + + + + + + + 721 + + + + + + + + + + + + + + + + + + + + 723 + + + + + 731 + + + + + 732 + + + + + 733 + + + + + 734 + + + + + 735 + + + + + 736 + + + + + 920 + + + + + 738 + + + + + 737 + + + + + 739 + + + + + 740 + + + + + 935 + + + + + 1114 + + + + + + + + 961 + + + + + + + + 1092 + + + + + 1115 + + + + + + + + 880 + + + + + + + + 1091 + + + + + 1116 + + + + + + + + 1029 + + + + + + + + 1093 + + + + + + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + {{58, 789}, {617, 0}} + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + tbbScan + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + tbbDirectories + com.apple.InterfaceBuilder.CocoaPlugin + tbbDetail + com.apple.InterfaceBuilder.CocoaPlugin + tbbPreferences + tbbAction + tbbPowerMarker + tbbDelta + tbbFilter + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + + + {{88, 389}, {557, 400}} + com.apple.InterfaceBuilder.CocoaPlugin + {{88, 389}, {557, 400}} + + + + {340, 340} + com.apple.InterfaceBuilder.CocoaPlugin + + MatchesView + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + + com.apple.InterfaceBuilder.CocoaPlugin + + {{88, 593}, {352, 252}} + com.apple.InterfaceBuilder.CocoaPlugin + {{88, 593}, {352, 252}} + + + {213, 107} + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + {{73, 468}, {331, 243}} + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + + + + + + 1120 + + + + + AppDelegate + AppDelegateBase + + id + id + id + + + result + ResultWindow + + + IBProjectSource + AppDelegate.h + + + + AppDelegate + AppDelegateBase + + unlockApp: + id + + + PyDupeGuru + RecentDirectories + NSMenuItem + + + IBUserSource + + + + + AppDelegateBase + NSObject + + unlockApp: + id + + + PyDupeGuruBase + RecentDirectories + NSMenuItem + + + IBProjectSource + dgbase/AppDelegate.h + + + + FirstResponder + NSObject + + IBUserSource + + + + + MatchesView + OutlineView + + IBProjectSource + dgbase/ResultWindow.h + + + + MatchesView + OutlineView + + IBUserSource + + + + + NSSegmentedControl + NSControl + + IBUserSource + + + + + OutlineView + NSOutlineView + + py + PyApp + + + IBProjectSource + cocoalib/Outline.h + + + + OutlineView + NSOutlineView + + IBUserSource + + + + + PyApp + PyRegistrable + + IBProjectSource + cocoalib/PyApp.h + + + + PyApp + PyRegistrable + + IBUserSource + + + + + PyDupeGuru + PyDupeGuruBase + + IBProjectSource + PyDupeGuru.h + + + + PyDupeGuru + PyDupeGuruBase + + IBUserSource + + + + + PyDupeGuruBase + PyApp + + IBProjectSource + dgbase/PyDupeGuru.h + + + + RecentDirectories + NSObject + + id + id + + + id + NSMenu + + + IBProjectSource + cocoalib/RecentDirectories.h + + + + RecentDirectories + NSObject + + IBUserSource + + + + + ResultWindow + ResultWindowBase + + id + id + id + id + id + id + id + id + id + id + id + id + id + id + id + id + id + id + id + id + id + id + + + NSMenu + NSSearchField + NSWindow + + + IBProjectSource + ResultWindow.h + + + + ResultWindow + ResultWindowBase + + id + id + id + id + id + id + id + id + id + id + + + NSPopUpButton + NSView + id + NSSegmentedControl + NSView + NSView + MatchesView + NSSegmentedControl + NSView + PyDupeGuru + NSTextField + + + IBUserSource + + + + + ResultWindowBase + NSWindowController + + id + id + id + id + id + id + id + id + id + + + id + NSSegmentedControl + MatchesView + NSSegmentedControl + PyDupeGuruBase + NSTextField + + + + + SUUpdater + NSObject + + IBUserSource + + + + + + + NSActionCell + NSCell + + IBFrameworkSource + AppKit.framework/Headers/NSActionCell.h + + + + NSApplication + NSResponder + + IBFrameworkSource + AppKit.framework/Headers/NSApplication.h + + + + NSApplication + + IBFrameworkSource + AppKit.framework/Headers/NSApplicationScripting.h + + + + NSApplication + + IBFrameworkSource + AppKit.framework/Headers/NSColorPanel.h + + + + NSApplication + + IBFrameworkSource + AppKit.framework/Headers/NSHelpManager.h + + + + NSApplication + + IBFrameworkSource + AppKit.framework/Headers/NSPageLayout.h + + + + NSApplication + + IBFrameworkSource + AppKit.framework/Headers/NSUserInterfaceItemSearching.h + + + + NSButton + NSControl + + IBFrameworkSource + AppKit.framework/Headers/NSButton.h + + + + NSButtonCell + NSActionCell + + IBFrameworkSource + AppKit.framework/Headers/NSButtonCell.h + + + + NSCell + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSCell.h + + + + NSControl + NSView + + IBFrameworkSource + AppKit.framework/Headers/NSControl.h + + + + NSController + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSController.h + + + + NSFormatter + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSFormatter.h + + + + NSMenu + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSMenu.h + + + + NSMenuItem + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSMenuItem.h + + + + NSMenuItemCell + NSButtonCell + + IBFrameworkSource + AppKit.framework/Headers/NSMenuItemCell.h + + + + NSMovieView + NSView + + IBFrameworkSource + AppKit.framework/Headers/NSMovieView.h + + + + NSNumberFormatter + NSFormatter + + IBFrameworkSource + Foundation.framework/Headers/NSNumberFormatter.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSAccessibility.h + + + + NSObject + + + + NSObject + + + + NSObject + + + + NSObject + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSDictionaryController.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSDragging.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSFontManager.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSFontPanel.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSKeyValueBinding.h + + + + NSObject + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSNibLoading.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSOutlineView.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSPasteboard.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSSavePanel.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSTableView.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSToolbarItem.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSView.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSArchiver.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSClassDescription.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSError.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSFileManager.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSKeyValueCoding.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSKeyValueObserving.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSKeyedArchiver.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSObject.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSObjectScripting.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSPortCoder.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSRunLoop.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSScriptClassDescription.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSScriptKeyValueCoding.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSScriptObjectSpecifiers.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSScriptWhoseTests.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSThread.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSURL.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSURLConnection.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSURLDownload.h + + + + NSObject + + IBFrameworkSource + Sparkle.framework/Headers/SUAppcast.h + + + + NSObject + + IBFrameworkSource + Sparkle.framework/Headers/SUUpdater.h + + + + NSOutlineView + NSTableView + + + + NSPopUpButton + NSButton + + IBFrameworkSource + AppKit.framework/Headers/NSPopUpButton.h + + + + NSPopUpButtonCell + NSMenuItemCell + + IBFrameworkSource + AppKit.framework/Headers/NSPopUpButtonCell.h + + + + NSResponder + + IBFrameworkSource + AppKit.framework/Headers/NSInterfaceStyle.h + + + + NSResponder + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSResponder.h + + + + NSScrollView + NSView + + IBFrameworkSource + AppKit.framework/Headers/NSScrollView.h + + + + NSScroller + NSControl + + IBFrameworkSource + AppKit.framework/Headers/NSScroller.h + + + + NSSearchField + NSTextField + + IBFrameworkSource + AppKit.framework/Headers/NSSearchField.h + + + + NSSearchFieldCell + NSTextFieldCell + + IBFrameworkSource + AppKit.framework/Headers/NSSearchFieldCell.h + + + + NSSegmentedCell + NSActionCell + + IBFrameworkSource + AppKit.framework/Headers/NSSegmentedCell.h + + + + NSSegmentedControl + NSControl + + IBFrameworkSource + AppKit.framework/Headers/NSSegmentedControl.h + + + + NSSlider + NSControl + + IBFrameworkSource + AppKit.framework/Headers/NSSlider.h + + + + NSSliderCell + NSActionCell + + IBFrameworkSource + AppKit.framework/Headers/NSSliderCell.h + + + + NSTableColumn + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSTableColumn.h + + + + NSTableHeaderView + NSView + + IBFrameworkSource + AppKit.framework/Headers/NSTableHeaderView.h + + + + NSTableView + NSControl + + + + NSText + NSView + + IBFrameworkSource + AppKit.framework/Headers/NSText.h + + + + NSTextField + NSControl + + IBFrameworkSource + AppKit.framework/Headers/NSTextField.h + + + + NSTextFieldCell + NSActionCell + + IBFrameworkSource + AppKit.framework/Headers/NSTextFieldCell.h + + + + NSToolbar + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSToolbar.h + + + + NSToolbarItem + NSObject + + + + NSUserDefaultsController + NSController + + IBFrameworkSource + AppKit.framework/Headers/NSUserDefaultsController.h + + + + NSView + + IBFrameworkSource + AppKit.framework/Headers/NSClipView.h + + + + NSView + + + + NSView + + IBFrameworkSource + AppKit.framework/Headers/NSRulerView.h + + + + NSView + NSResponder + + + + NSWindow + + IBFrameworkSource + AppKit.framework/Headers/NSDrawer.h + + + + NSWindow + NSResponder + + IBFrameworkSource + AppKit.framework/Headers/NSWindow.h + + + + NSWindow + + IBFrameworkSource + AppKit.framework/Headers/NSWindowScripting.h + + + + NSWindowController + NSResponder + + showWindow: + id + + + IBFrameworkSource + AppKit.framework/Headers/NSWindowController.h + + + + SUUpdater + NSObject + + checkForUpdates: + id + + + delegate + id + + + + + + 0 + + com.apple.InterfaceBuilder.CocoaPlugin.macosx + + + + com.apple.InterfaceBuilder.CocoaPlugin.macosx + + + YES + ../dupeguru.xcodeproj + 3 + + diff --git a/pe/py/app_cocoa.py b/pe/py/app_cocoa.py index 8b9bd23b..a9a56ebb 100644 --- a/pe/py/app_cocoa.py +++ b/pe/py/app_cocoa.py @@ -188,5 +188,7 @@ class DupeGuruPE(app_cocoa.DupeGuru): 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 From c936f9ccc68c7986d9a3bfb11cbdaea1e1fb321c Mon Sep 17 00:00:00 2001 From: hsoft Date: Wed, 28 Oct 2009 15:48:19 +0000 Subject: [PATCH 216/275] [#9 state:port] implemented drag&drop for directories on the cocoa side. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40222 --- base/cocoa/DirectoryPanel.h | 11 +++++- base/cocoa/DirectoryPanel.m | 52 ++++++++++++++++++++++++-- base/cocoa/xib/DirectoryPanel.xib | 62 +++++++++++++++++++------------ 3 files changed, 96 insertions(+), 29 deletions(-) diff --git a/base/cocoa/DirectoryPanel.h b/base/cocoa/DirectoryPanel.h index a606aebc..9beb3928 100644 --- a/base/cocoa/DirectoryPanel.h +++ b/base/cocoa/DirectoryPanel.h @@ -11,10 +11,19 @@ http://www.hardcoded.net/licenses/hs_license #import "Outline.h" #import "PyDupeGuru.h" +@interface DirectoryOutline : OutlineView +{ +} +@end + +@protocol DirectoryOutlineDelegate +- (void)outlineView:(NSOutlineView *)outlineView addDirectory:(NSString *)directory; +@end + @interface DirectoryPanelBase : NSWindowController { IBOutlet NSPopUpButton *addButtonPopUp; - IBOutlet OutlineView *directories; + IBOutlet DirectoryOutline *directories; IBOutlet NSButton *removeButton; PyDupeGuruBase *_py; diff --git a/base/cocoa/DirectoryPanel.m b/base/cocoa/DirectoryPanel.m index 66f99a29..6df9e329 100644 --- a/base/cocoa/DirectoryPanel.m +++ b/base/cocoa/DirectoryPanel.m @@ -11,6 +11,48 @@ http://www.hardcoded.net/licenses/hs_license #import "Utils.h" #import "AppDelegate.h" +@implementation DirectoryOutline +- (void)doInit +{ + [super doInit]; + [self registerForDraggedTypes:[NSArray arrayWithObject:NSFilenamesPboardType]]; +} + +- (NSDragOperation)outlineView:(NSOutlineView *)outlineView validateDrop:(id < NSDraggingInfo >)info proposedItem:(id)item proposedChildIndex:(NSInteger)index +{ + NSPasteboard *pboard; + NSDragOperation sourceDragMask; + sourceDragMask = [info draggingSourceOperationMask]; + pboard = [info draggingPasteboard]; + if ([[pboard types] containsObject:NSFilenamesPboardType]) + { + if (sourceDragMask & NSDragOperationLink) + return NSDragOperationLink; + } + return NSDragOperationNone; +} + +- (BOOL)outlineView:(NSOutlineView *)outlineView acceptDrop:(id < NSDraggingInfo >)info item:(id)item childIndex:(NSInteger)index +{ + NSPasteboard *pboard; + NSDragOperation sourceDragMask; + sourceDragMask = [info draggingSourceOperationMask]; + pboard = [info draggingPasteboard]; + if ( [[pboard types] containsObject:NSFilenamesPboardType] ) + { + NSArray *filenames = [pboard propertyListForType:NSFilenamesPboardType]; + if (!(sourceDragMask & NSDragOperationLink)) + return NO; + if (([self delegate] == nil) || (![[self delegate] respondsToSelector:@selector(outlineView:addDirectory:)])) + return NO; + for (NSString *filename in filenames) + [[self delegate] outlineView:self addDirectory:filename]; + } + return YES; +} + +@end + @implementation DirectoryPanelBase - (id)initWithParentApp:(id)aParentApp { @@ -104,10 +146,7 @@ http://www.hardcoded.net/licenses/hs_license - (IBAction)toggleVisible:(id)sender { - if ([[self window] isVisible]) - [[self window] close]; - else - [[self window] makeKeyAndOrderFront:nil]; + [[self window] makeKeyAndOrderFront:nil]; } /* Public */ @@ -154,6 +193,11 @@ http://www.hardcoded.net/licenses/hs_license /* Delegate */ +- (void)outlineView:(NSOutlineView *)outlineView addDirectory:(NSString *)directory +{ + [self addDirectory:directory]; +} + - (void)outlineView:(NSOutlineView *)outlineView willDisplayCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn item:(id)item { OVNode *node = item; diff --git a/base/cocoa/xib/DirectoryPanel.xib b/base/cocoa/xib/DirectoryPanel.xib index 9f8f8e7a..b14f503d 100644 --- a/base/cocoa/xib/DirectoryPanel.xib +++ b/base/cocoa/xib/DirectoryPanel.xib @@ -12,7 +12,7 @@ YES - + YES @@ -39,19 +39,17 @@ NSApplication - 27 + 11 2 {{387, 290}, {369, 269}} - -260571136 + 1886913536 Directories - - NSPanel - + NSWindow View {1.79769e+308, 1.79769e+308} - {213, 113} + {369, 269} 256 @@ -404,7 +402,7 @@ {{0, 0}, {1440, 878}} - {213, 129} + {369, 291} {1.79769e+308, 1.79769e+308} @@ -731,6 +729,11 @@ 18.ImportedFromIB2 31.IBPluginDependency 31.ImportedFromIB2 + 45.IBPluginDependency + 46.IBPluginDependency + 47.IBPluginDependency + 48.IBPluginDependency + 49.IBPluginDependency 49.IBShouldRemoveOnLegacySave 5.IBEditorWindowLastContentRect 5.IBPluginDependency @@ -738,8 +741,12 @@ 5.ImportedFromIB2 5.windowTemplate.hasMinSize 5.windowTemplate.minSize + 50.IBPluginDependency + 51.IBPluginDependency 51.IBShouldRemoveOnLegacySave + 52.IBPluginDependency 52.IBShouldRemoveOnLegacySave + 53.IBPluginDependency 53.IBShouldRemoveOnLegacySave 6.IBPluginDependency 6.ImportedFromIB2 @@ -756,7 +763,7 @@ com.apple.InterfaceBuilder.CocoaPlugin - OutlineView + DirectoryOutline com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin @@ -771,15 +778,24 @@ com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin {{88, 571}, {369, 269}} com.apple.InterfaceBuilder.CocoaPlugin {{88, 571}, {369, 269}} - {213, 113} + {369, 269} + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin @@ -812,6 +828,14 @@ YES + + DirectoryOutline + OutlineView + + IBProjectSource + dgbase/DirectoryPanel.h + + DirectoryPanel DirectoryPanelBase @@ -895,17 +919,15 @@ YES NSPopUpButton - OutlineView + DirectoryOutline NSButton - - IBProjectSource - dgbase/DirectoryPanel.h - + FirstResponder + NSObject IBUserSource @@ -1328,14 +1350,6 @@ NSTableView - - NSPanel - NSWindow - - IBFrameworkSource - AppKit.framework/Headers/NSPanel.h - - NSPopUpButton NSButton @@ -1485,7 +1499,7 @@ YES - ../dupeguru.xcodeproj + ../../dupeguru.xcodeproj 3 From 607ab86188a4bc513982b47ab02baa45f0e8fefd Mon Sep 17 00:00:00 2001 From: hsoft Date: Wed, 28 Oct 2009 16:13:50 +0000 Subject: [PATCH 217/275] [#9] mapped the delete key to the remove button. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40223 --- base/cocoa/xib/DirectoryPanel.xib | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/base/cocoa/xib/DirectoryPanel.xib b/base/cocoa/xib/DirectoryPanel.xib index b14f503d..90264859 100644 --- a/base/cocoa/xib/DirectoryPanel.xib +++ b/base/cocoa/xib/DirectoryPanel.xib @@ -12,7 +12,7 @@ YES - + YES @@ -369,9 +369,7 @@ 1 - - - +  200 25 From 88127d8b8dcaa507f0775398ff0ae71cdaa15e60 Mon Sep 17 00:00:00 2001 From: hsoft Date: Thu, 29 Oct 2009 12:02:49 +0000 Subject: [PATCH 218/275] [#9] Implemented directories drag&drop on the QT side. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40224 --- base/qt/directories_dialog.ui | 12 ++++++++++ base/qt/directories_model.py | 45 +++++++++++++++++++++++++++-------- 2 files changed, 47 insertions(+), 10 deletions(-) diff --git a/base/qt/directories_dialog.ui b/base/qt/directories_dialog.ui index 68bc8d84..be44694d 100644 --- a/base/qt/directories_dialog.ui +++ b/base/qt/directories_dialog.ui @@ -16,9 +16,18 @@ + + true + QAbstractItemView::DoubleClicked|QAbstractItemView::EditKeyPressed|QAbstractItemView::SelectedClicked + + true + + + QAbstractItemView::DropOnly + true @@ -59,6 +68,9 @@ Remove + + Del + diff --git a/base/qt/directories_model.py b/base/qt/directories_model.py index e04c913f..277ea83a 100644 --- a/base/qt/directories_model.py +++ b/base/qt/directories_model.py @@ -7,7 +7,9 @@ # which should be included with this package. The terms are also available at # http://www.hardcoded.net/licenses/hs_license -from PyQt4.QtCore import QModelIndex, Qt, QRect, QEvent, QPoint +import urllib + +from PyQt4.QtCore import QModelIndex, Qt, QRect, QEvent, QPoint, QUrl from PyQt4.QtGui import QComboBox, QStyledItemDelegate, QMouseEvent, QApplication, QBrush from qtlib.tree_model import TreeNode, TreeModel @@ -47,7 +49,7 @@ class DirectoryNode(TreeNode): return DirectoryNode(self.model, self, ref, row) def _getChildren(self): - return self.model._dirs.get_subfolders(self.ref) + return self.model.dirs.get_subfolders(self.ref) @property def name(self): @@ -59,14 +61,15 @@ class DirectoryNode(TreeNode): class DirectoriesModel(TreeModel): def __init__(self, app): - self._dirs = app.directories + self.app = app + self.dirs = app.directories TreeModel.__init__(self) def _createNode(self, ref, row): return DirectoryNode(self, None, ref, row) def _getChildren(self): - return self._dirs + return self.dirs def columnCount(self, parent): return 2 @@ -79,21 +82,35 @@ class DirectoriesModel(TreeModel): if index.column() == 0: return node.name else: - return STATES[self._dirs.get_state(node.ref)] + return STATES[self.dirs.get_state(node.ref)] elif role == Qt.EditRole and index.column() == 1: - return self._dirs.get_state(node.ref) + return self.dirs.get_state(node.ref) elif role == Qt.ForegroundRole: - state = self._dirs.get_state(node.ref) + state = self.dirs.get_state(node.ref) if state == 1: return QBrush(Qt.blue) elif state == 2: return QBrush(Qt.red) return None + def dropMimeData(self, mimeData, action, row, column, parentIndex): + # the data in mimeData is urlencoded **in utf-8**!!! which means that urllib.unquote has + # to be called on the utf-8 encoded string, and *only then*, decoded to unicode. + if not mimeData.hasFormat('text/uri-list'): + return False + data = str(mimeData.data('text/uri-list')) + unquoted = urllib.unquote(data) + urls = unicode(unquoted, 'utf-8').split('\r\n') + paths = [unicode(QUrl(url).toLocalFile()) for url in urls if url] + for path in paths: + self.app.add_directory(path) + self.reset() + return True + def flags(self, index): if not index.isValid(): - return 0 - result = Qt.ItemIsEnabled | Qt.ItemIsSelectable + return Qt.ItemIsEnabled | Qt.ItemIsDropEnabled + result = Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsDropEnabled if index.column() == 1: result |= Qt.ItemIsEditable return result @@ -104,10 +121,18 @@ class DirectoriesModel(TreeModel): return HEADERS[section] return None + def mimeTypes(self): + return ['text/uri-list'] + def setData(self, index, value, role): if not index.isValid() or role != Qt.EditRole or index.column() != 1: return False node = index.internalPointer() - self._dirs.set_state(node.ref, value) + self.dirs.set_state(node.ref, value) return True + def supportedDropActions(self): + # Normally, the correct action should be ActionLink, but the drop doesn't work. It doesn't + # work with ActionMove either. So screw that, and accept anything. + return Qt.ActionMask + From f070e90347b3be4a4a31eb19ea001ea99c59106a Mon Sep 17 00:00:00 2001 From: hsoft Date: Fri, 30 Oct 2009 11:09:04 +0000 Subject: [PATCH 219/275] [#72 state:fixed] When files are deleted during the scan, don't include them in the grouping phase. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40225 --- base/py/engine.py | 4 +- base/py/scanner.py | 5 +- base/py/tests/engine_test.py | 15 +- base/py/tests/results_test.py | 1 - base/py/tests/scanner_test.py | 829 ++++++++++++++++++---------------- 5 files changed, 448 insertions(+), 406 deletions(-) diff --git a/base/py/engine.py b/base/py/engine.py index b34f2edd..173cbab3 100644 --- a/base/py/engine.py +++ b/base/py/engine.py @@ -208,7 +208,9 @@ def getmatches_by_contents(files, sizeattr='size', partial=False, j=job.nulljob) j = j.start_subjob([2, 8]) size2files = defaultdict(set) for file in j.iter_with_progress(files, 'Read size of %d/%d files'): - size2files[getattr(file, sizeattr)].add(file) + filesize = getattr(file, sizeattr) + if filesize: + size2files[filesize].add(file) possible_matches = [files for files in size2files.values() if len(files) > 1] del size2files result = [] diff --git a/base/py/scanner.py b/base/py/scanner.py index 3f999920..9cd6b21a 100644 --- a/base/py/scanner.py +++ b/base/py/scanner.py @@ -10,7 +10,7 @@ import logging -from hsutil import job +from hsutil import job, io from hsutil.misc import dedupe from hsutil.str import get_file_ext, rem_file_ext @@ -80,9 +80,10 @@ class Scanner(object): logging.info('Getting matches') matches = self._getmatches(files, j) logging.info('Found %d matches' % len(matches)) + j.set_progress(100, 'Removing false matches') if not self.mix_file_kind: - j.set_progress(100, 'Removing false matches') matches = [m for m in matches if get_file_ext(m.first.name) == get_file_ext(m.second.name)] + matches = [m for m in matches if io.exists(m.first.path) and io.exists(m.second.path)] if self.ignore_list: j = j.start_subjob(2) iter_matches = j.iter_with_progress(matches, 'Processed %d/%d matches against the ignore list') diff --git a/base/py/tests/engine_test.py b/base/py/tests/engine_test.py index 1c3366bc..48e4f1d8 100644 --- a/base/py/tests/engine_test.py +++ b/base/py/tests/engine_test.py @@ -15,16 +15,21 @@ from hsutil import job from hsutil.decorators import log_calls from hsutil.testcase import TestCase -from .. import engine +from .. import engine, fs from ..engine import * class NamedObject(object): - def __init__(self, name="foobar", with_words=False): + def __init__(self, name="foobar", with_words=False, size=1): self.name = name + self.size = size + self.md5partial = name + self.md5 = name if with_words: self.words = getwords(name) +no = NamedObject + def get_match_triangle(): o1 = NamedObject(with_words=True) o2 = NamedObject(with_words=True) @@ -486,6 +491,12 @@ class GetMatches(TestCase): self.assertEqual(42, len(r)) +class GetMatchesByContents(TestCase): + def test_dont_compare_empty_files(self): + o1, o2 = no(size=0), no(size=0) + assert not getmatches_by_contents([o1, o2]) + + class TCGroup(TestCase): def test_empy(self): g = Group() diff --git a/base/py/tests/results_test.py b/base/py/tests/results_test.py index f3602b7c..086dc91d 100644 --- a/base/py/tests/results_test.py +++ b/base/py/tests/results_test.py @@ -21,7 +21,6 @@ from .. import engine from ..results import * class NamedObject(engine_test.NamedObject): - size = 1 path = property(lambda x:Path('basepath') + x.name) is_ref = False diff --git a/base/py/tests/scanner_test.py b/base/py/tests/scanner_test.py index 1ce0f8f7..6113be7b 100644 --- a/base/py/tests/scanner_test.py +++ b/base/py/tests/scanner_test.py @@ -9,9 +9,11 @@ from nose.tools import eq_ -from hsutil import job +from hsutil import job, io from hsutil.path import Path +from hsutil.testcase import TestCase +from .. import fs from ..engine import getwords, Match from ..ignore import IgnoreList from ..scanner import * @@ -27,412 +29,439 @@ class NamedObject(object): no = NamedObject #--- Scanner -def test_empty(): - s = Scanner() - r = s.GetDupeGroups([]) - eq_(r, []) - -def test_default_settings(): - s = Scanner() - eq_(s.min_match_percentage, 80) - eq_(s.scan_type, SCAN_TYPE_FILENAME) - eq_(s.mix_file_kind, True) - eq_(s.word_weighting, False) - eq_(s.match_similar_words, False) - assert isinstance(s.ignore_list, IgnoreList) - -def test_simple_with_default_settings(): - s = Scanner() - f = [no('foo bar'), no('foo bar'), no('foo bleh')] - r = s.GetDupeGroups(f) - eq_(len(r), 1) - g = r[0] - #'foo bleh' cannot be in the group because the default min match % is 80 - eq_(len(g), 2) - assert g.ref in f[:2] - assert g.dupes[0] in f[:2] - -def test_simple_with_lower_min_match(): - s = Scanner() - s.min_match_percentage = 50 - f = [no('foo bar'), no('foo bar'), no('foo bleh')] - r = s.GetDupeGroups(f) - eq_(len(r), 1) - g = r[0] - eq_(len(g), 3) - -def test_trim_all_ref_groups(): - # When all files of a group are ref, don't include that group in the results, but also don't - # count the files from that group as discarded. - s = Scanner() - f = [no('foo'), no('foo'), no('bar'), no('bar')] - f[2].is_ref = True - f[3].is_ref = True - r = s.GetDupeGroups(f) - eq_(len(r), 1) - eq_(s.discarded_file_count, 0) - -def test_priorize(): - s = Scanner() - f = [no('foo'), no('foo'), no('bar'), no('bar')] - f[1].size = 2 - f[2].size = 3 - f[3].is_ref = True - r = s.GetDupeGroups(f) - g1, g2 = r - assert f[1] in (g1.ref,g2.ref) - assert f[0] in (g1.dupes[0],g2.dupes[0]) - assert f[3] in (g1.ref,g2.ref) - assert f[2] in (g1.dupes[0],g2.dupes[0]) - -def test_content_scan(): - s = Scanner() - s.scan_type = SCAN_TYPE_CONTENT - f = [no('foo'), no('bar'), no('bleh')] - f[0].md5 = f[0].md5partial = 'foobar' - f[1].md5 = f[1].md5partial = 'foobar' - f[2].md5 = f[2].md5partial = 'bleh' - r = s.GetDupeGroups(f) - eq_(len(r), 1) - eq_(len(r[0]), 2) - eq_(s.discarded_file_count, 0) # don't count the different md5 as discarded! - -def test_content_scan_compare_sizes_first(): - class MyFile(no): - @property - def md5(file): - raise AssertionError() +class ScannerTestFakeFiles(TestCase): + def setUp(self): + # This is a hack to avoid invalidating all previous tests since the scanner started to test + # for file existence before doing the match grouping. + self.mock(io, 'exists', lambda _: True) - s = Scanner() - s.scan_type = SCAN_TYPE_CONTENT - f = [MyFile('foo', 1), MyFile('bar', 2)] - eq_(len(s.GetDupeGroups(f)), 0) - -def test_min_match_perc_doesnt_matter_for_content_scan(): - s = Scanner() - s.scan_type = SCAN_TYPE_CONTENT - f = [no('foo'), no('bar'), no('bleh')] - f[0].md5 = f[0].md5partial = 'foobar' - f[1].md5 = f[1].md5partial = 'foobar' - f[2].md5 = f[2].md5partial = 'bleh' - s.min_match_percentage = 101 - r = s.GetDupeGroups(f) - eq_(len(r), 1) - eq_(len(r[0]), 2) - s.min_match_percentage = 0 - r = s.GetDupeGroups(f) - eq_(len(r), 1) - eq_(len(r[0]), 2) - -def test_content_scan_doesnt_put_md5_in_words_at_the_end(): - s = Scanner() - s.scan_type = SCAN_TYPE_CONTENT - f = [no('foo'),no('bar')] - f[0].md5 = f[0].md5partial = '\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f' - f[1].md5 = f[1].md5partial = '\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f' - r = s.GetDupeGroups(f) - g = r[0] - -def test_extension_is_not_counted_in_filename_scan(): - s = Scanner() - s.min_match_percentage = 100 - f = [no('foo.bar'), no('foo.bleh')] - r = s.GetDupeGroups(f) - eq_(len(r), 1) - eq_(len(r[0]), 2) - -def test_job(): - def do_progress(progress, desc=''): - log.append(progress) - return True + def test_empty(self): + s = Scanner() + r = s.GetDupeGroups([]) + eq_(r, []) - s = Scanner() - log = [] - f = [no('foo bar'), no('foo bar'), no('foo bleh')] - r = s.GetDupeGroups(f, job.Job(1, do_progress)) - eq_(log[0], 0) - eq_(log[-1], 100) - -def test_mix_file_kind(): - s = Scanner() - s.mix_file_kind = False - f = [no('foo.1'), no('foo.2')] - r = s.GetDupeGroups(f) - eq_(len(r), 0) - -def test_word_weighting(): - s = Scanner() - s.min_match_percentage = 75 - s.word_weighting = True - f = [no('foo bar'), no('foo bar bleh')] - r = s.GetDupeGroups(f) - eq_(len(r), 1) - g = r[0] - m = g.get_match_of(g.dupes[0]) - eq_(m.percentage, 75) # 16 letters, 12 matching - -def test_similar_words(): - s = Scanner() - s.match_similar_words = True - f = [no('The White Stripes'), no('The Whites Stripe'), no('Limp Bizkit'), no('Limp Bizkitt')] - r = s.GetDupeGroups(f) - eq_(len(r), 2) - -def test_fields(): - s = Scanner() - s.scan_type = SCAN_TYPE_FIELDS - f = [no('The White Stripes - Little Ghost'), no('The White Stripes - Little Acorn')] - r = s.GetDupeGroups(f) - eq_(len(r), 0) - -def test_fields_no_order(): - s = Scanner() - s.scan_type = SCAN_TYPE_FIELDS_NO_ORDER - f = [no('The White Stripes - Little Ghost'), no('Little Ghost - The White Stripes')] - r = s.GetDupeGroups(f) - eq_(len(r), 1) - -def test_tag_scan(): - s = Scanner() - s.scan_type = SCAN_TYPE_TAG - o1 = no('foo') - o2 = no('bar') - o1.artist = 'The White Stripes' - o1.title = 'The Air Near My Fingers' - o2.artist = 'The White Stripes' - o2.title = 'The Air Near My Fingers' - r = s.GetDupeGroups([o1,o2]) - eq_(len(r), 1) - -def test_tag_with_album_scan(): - s = Scanner() - s.scan_type = SCAN_TYPE_TAG - s.scanned_tags = set(['artist', 'album', 'title']) - o1 = no('foo') - o2 = no('bar') - o3 = no('bleh') - o1.artist = 'The White Stripes' - o1.title = 'The Air Near My Fingers' - o1.album = 'Elephant' - o2.artist = 'The White Stripes' - o2.title = 'The Air Near My Fingers' - o2.album = 'Elephant' - o3.artist = 'The White Stripes' - o3.title = 'The Air Near My Fingers' - o3.album = 'foobar' - r = s.GetDupeGroups([o1,o2,o3]) - eq_(len(r), 1) - -def test_that_dash_in_tags_dont_create_new_fields(): - s = Scanner() - s.scan_type = SCAN_TYPE_TAG - s.scanned_tags = set(['artist', 'album', 'title']) - s.min_match_percentage = 50 - o1 = no('foo') - o2 = no('bar') - o1.artist = 'The White Stripes - a' - o1.title = 'The Air Near My Fingers - a' - o1.album = 'Elephant - a' - o2.artist = 'The White Stripes - b' - o2.title = 'The Air Near My Fingers - b' - o2.album = 'Elephant - b' - r = s.GetDupeGroups([o1,o2]) - eq_(len(r), 1) - -def test_tag_scan_with_different_scanned(): - s = Scanner() - s.scan_type = SCAN_TYPE_TAG - s.scanned_tags = set(['track', 'year']) - o1 = no('foo') - o2 = no('bar') - o1.artist = 'The White Stripes' - o1.title = 'some title' - o1.track = 'foo' - o1.year = 'bar' - o2.artist = 'The White Stripes' - o2.title = 'another title' - o2.track = 'foo' - o2.year = 'bar' - r = s.GetDupeGroups([o1, o2]) - eq_(len(r), 1) - -def test_tag_scan_only_scans_existing_tags(): - s = Scanner() - s.scan_type = SCAN_TYPE_TAG - s.scanned_tags = set(['artist', 'foo']) - o1 = no('foo') - o2 = no('bar') - o1.artist = 'The White Stripes' - o1.foo = 'foo' - o2.artist = 'The White Stripes' - o2.foo = 'bar' - r = s.GetDupeGroups([o1, o2]) - eq_(len(r), 1) # Because 'foo' is not scanned, they match - -def test_tag_scan_converts_to_str(): - s = Scanner() - s.scan_type = SCAN_TYPE_TAG - s.scanned_tags = set(['track']) - o1 = no('foo') - o2 = no('bar') - o1.track = 42 - o2.track = 42 - try: + def test_default_settings(self): + s = Scanner() + eq_(s.min_match_percentage, 80) + eq_(s.scan_type, SCAN_TYPE_FILENAME) + eq_(s.mix_file_kind, True) + eq_(s.word_weighting, False) + eq_(s.match_similar_words, False) + assert isinstance(s.ignore_list, IgnoreList) + + def test_simple_with_default_settings(self): + s = Scanner() + f = [no('foo bar'), no('foo bar'), no('foo bleh')] + r = s.GetDupeGroups(f) + eq_(len(r), 1) + g = r[0] + #'foo bleh' cannot be in the group because the default min match % is 80 + eq_(len(g), 2) + assert g.ref in f[:2] + assert g.dupes[0] in f[:2] + + def test_simple_with_lower_min_match(self): + s = Scanner() + s.min_match_percentage = 50 + f = [no('foo bar'), no('foo bar'), no('foo bleh')] + r = s.GetDupeGroups(f) + eq_(len(r), 1) + g = r[0] + eq_(len(g), 3) + + def test_trim_all_ref_groups(self): + # When all files of a group are ref, don't include that group in the results, but also don't + # count the files from that group as discarded. + s = Scanner() + f = [no('foo'), no('foo'), no('bar'), no('bar')] + f[2].is_ref = True + f[3].is_ref = True + r = s.GetDupeGroups(f) + eq_(len(r), 1) + eq_(s.discarded_file_count, 0) + + def test_priorize(self): + s = Scanner() + f = [no('foo'), no('foo'), no('bar'), no('bar')] + f[1].size = 2 + f[2].size = 3 + f[3].is_ref = True + r = s.GetDupeGroups(f) + g1, g2 = r + assert f[1] in (g1.ref,g2.ref) + assert f[0] in (g1.dupes[0],g2.dupes[0]) + assert f[3] in (g1.ref,g2.ref) + assert f[2] in (g1.dupes[0],g2.dupes[0]) + + def test_content_scan(self): + s = Scanner() + s.scan_type = SCAN_TYPE_CONTENT + f = [no('foo'), no('bar'), no('bleh')] + f[0].md5 = f[0].md5partial = 'foobar' + f[1].md5 = f[1].md5partial = 'foobar' + f[2].md5 = f[2].md5partial = 'bleh' + r = s.GetDupeGroups(f) + eq_(len(r), 1) + eq_(len(r[0]), 2) + eq_(s.discarded_file_count, 0) # don't count the different md5 as discarded! + + def test_content_scan_compare_sizes_first(self): + class MyFile(no): + @property + def md5(file): + raise AssertionError() + + s = Scanner() + s.scan_type = SCAN_TYPE_CONTENT + f = [MyFile('foo', 1), MyFile('bar', 2)] + eq_(len(s.GetDupeGroups(f)), 0) + + def test_min_match_perc_doesnt_matter_for_content_scan(self): + s = Scanner() + s.scan_type = SCAN_TYPE_CONTENT + f = [no('foo'), no('bar'), no('bleh')] + f[0].md5 = f[0].md5partial = 'foobar' + f[1].md5 = f[1].md5partial = 'foobar' + f[2].md5 = f[2].md5partial = 'bleh' + s.min_match_percentage = 101 + r = s.GetDupeGroups(f) + eq_(len(r), 1) + eq_(len(r[0]), 2) + s.min_match_percentage = 0 + r = s.GetDupeGroups(f) + eq_(len(r), 1) + eq_(len(r[0]), 2) + + def test_content_scan_doesnt_put_md5_in_words_at_the_end(self): + s = Scanner() + s.scan_type = SCAN_TYPE_CONTENT + f = [no('foo'),no('bar')] + f[0].md5 = f[0].md5partial = '\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f' + f[1].md5 = f[1].md5partial = '\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f' + r = s.GetDupeGroups(f) + g = r[0] + + def test_extension_is_not_counted_in_filename_scan(self): + s = Scanner() + s.min_match_percentage = 100 + f = [no('foo.bar'), no('foo.bleh')] + r = s.GetDupeGroups(f) + eq_(len(r), 1) + eq_(len(r[0]), 2) + + def test_job(self): + def do_progress(progress, desc=''): + log.append(progress) + return True + + s = Scanner() + log = [] + f = [no('foo bar'), no('foo bar'), no('foo bleh')] + r = s.GetDupeGroups(f, job.Job(1, do_progress)) + eq_(log[0], 0) + eq_(log[-1], 100) + + def test_mix_file_kind(self): + s = Scanner() + s.mix_file_kind = False + f = [no('foo.1'), no('foo.2')] + r = s.GetDupeGroups(f) + eq_(len(r), 0) + + def test_word_weighting(self): + s = Scanner() + s.min_match_percentage = 75 + s.word_weighting = True + f = [no('foo bar'), no('foo bar bleh')] + r = s.GetDupeGroups(f) + eq_(len(r), 1) + g = r[0] + m = g.get_match_of(g.dupes[0]) + eq_(m.percentage, 75) # 16 letters, 12 matching + + def test_similar_words(self): + s = Scanner() + s.match_similar_words = True + f = [no('The White Stripes'), no('The Whites Stripe'), no('Limp Bizkit'), no('Limp Bizkitt')] + r = s.GetDupeGroups(f) + eq_(len(r), 2) + + def test_fields(self): + s = Scanner() + s.scan_type = SCAN_TYPE_FIELDS + f = [no('The White Stripes - Little Ghost'), no('The White Stripes - Little Acorn')] + r = s.GetDupeGroups(f) + eq_(len(r), 0) + + def test_fields_no_order(self): + s = Scanner() + s.scan_type = SCAN_TYPE_FIELDS_NO_ORDER + f = [no('The White Stripes - Little Ghost'), no('Little Ghost - The White Stripes')] + r = s.GetDupeGroups(f) + eq_(len(r), 1) + + def test_tag_scan(self): + s = Scanner() + s.scan_type = SCAN_TYPE_TAG + o1 = no('foo') + o2 = no('bar') + o1.artist = 'The White Stripes' + o1.title = 'The Air Near My Fingers' + o2.artist = 'The White Stripes' + o2.title = 'The Air Near My Fingers' + r = s.GetDupeGroups([o1,o2]) + eq_(len(r), 1) + + def test_tag_with_album_scan(self): + s = Scanner() + s.scan_type = SCAN_TYPE_TAG + s.scanned_tags = set(['artist', 'album', 'title']) + o1 = no('foo') + o2 = no('bar') + o3 = no('bleh') + o1.artist = 'The White Stripes' + o1.title = 'The Air Near My Fingers' + o1.album = 'Elephant' + o2.artist = 'The White Stripes' + o2.title = 'The Air Near My Fingers' + o2.album = 'Elephant' + o3.artist = 'The White Stripes' + o3.title = 'The Air Near My Fingers' + o3.album = 'foobar' + r = s.GetDupeGroups([o1,o2,o3]) + eq_(len(r), 1) + + def test_that_dash_in_tags_dont_create_new_fields(self): + s = Scanner() + s.scan_type = SCAN_TYPE_TAG + s.scanned_tags = set(['artist', 'album', 'title']) + s.min_match_percentage = 50 + o1 = no('foo') + o2 = no('bar') + o1.artist = 'The White Stripes - a' + o1.title = 'The Air Near My Fingers - a' + o1.album = 'Elephant - a' + o2.artist = 'The White Stripes - b' + o2.title = 'The Air Near My Fingers - b' + o2.album = 'Elephant - b' + r = s.GetDupeGroups([o1,o2]) + eq_(len(r), 1) + + def test_tag_scan_with_different_scanned(self): + s = Scanner() + s.scan_type = SCAN_TYPE_TAG + s.scanned_tags = set(['track', 'year']) + o1 = no('foo') + o2 = no('bar') + o1.artist = 'The White Stripes' + o1.title = 'some title' + o1.track = 'foo' + o1.year = 'bar' + o2.artist = 'The White Stripes' + o2.title = 'another title' + o2.track = 'foo' + o2.year = 'bar' r = s.GetDupeGroups([o1, o2]) - except TypeError: - raise AssertionError() - eq_(len(r), 1) - -def test_tag_scan_non_ascii(): - s = Scanner() - s.scan_type = SCAN_TYPE_TAG - s.scanned_tags = set(['title']) - o1 = no('foo') - o2 = no('bar') - o1.title = u'foobar\u00e9' - o2.title = u'foobar\u00e9' - try: + eq_(len(r), 1) + + def test_tag_scan_only_scans_existing_tags(self): + s = Scanner() + s.scan_type = SCAN_TYPE_TAG + s.scanned_tags = set(['artist', 'foo']) + o1 = no('foo') + o2 = no('bar') + o1.artist = 'The White Stripes' + o1.foo = 'foo' + o2.artist = 'The White Stripes' + o2.foo = 'bar' r = s.GetDupeGroups([o1, o2]) - except UnicodeEncodeError: - raise AssertionError() - eq_(len(r), 1) - -def test_audio_content_scan(): - s = Scanner() - s.scan_type = SCAN_TYPE_CONTENT_AUDIO - f = [no('foo'), no('bar'), no('bleh')] - f[0].md5 = 'foo' - f[1].md5 = 'bar' - f[2].md5 = 'bleh' - f[0].md5partial = 'foo' - f[1].md5partial = 'foo' - f[2].md5partial = 'bleh' - f[0].audiosize = 1 - f[1].audiosize = 1 - f[2].audiosize = 1 - r = s.GetDupeGroups(f) - eq_(len(r), 1) - eq_(len(r[0]), 2) + eq_(len(r), 1) # Because 'foo' is not scanned, they match -def test_audio_content_scan_compare_sizes_first(): - class MyFile(no): - @property - def md5partial(file): + def test_tag_scan_converts_to_str(self): + s = Scanner() + s.scan_type = SCAN_TYPE_TAG + s.scanned_tags = set(['track']) + o1 = no('foo') + o2 = no('bar') + o1.track = 42 + o2.track = 42 + try: + r = s.GetDupeGroups([o1, o2]) + except TypeError: raise AssertionError() + eq_(len(r), 1) - s = Scanner() - s.scan_type = SCAN_TYPE_CONTENT_AUDIO - f = [MyFile('foo'), MyFile('bar')] - f[0].audiosize = 1 - f[1].audiosize = 2 - eq_(len(s.GetDupeGroups(f)), 0) - -def test_ignore_list(): - s = Scanner() - f1 = no('foobar') - f2 = no('foobar') - f3 = no('foobar') - f1.path = Path('dir1/foobar') - f2.path = Path('dir2/foobar') - f3.path = Path('dir3/foobar') - s.ignore_list.Ignore(str(f1.path),str(f2.path)) - s.ignore_list.Ignore(str(f1.path),str(f3.path)) - r = s.GetDupeGroups([f1,f2,f3]) - eq_(len(r), 1) - g = r[0] - eq_(len(g.dupes), 1) - assert f1 not in g - assert f2 in g - assert f3 in g - # Ignored matches are not counted as discarded - eq_(s.discarded_file_count, 0) - -def test_ignore_list_checks_for_unicode(): - #scanner was calling path_str for ignore list checks. Since the Path changes, it must - #be unicode(path) - s = Scanner() - f1 = no('foobar') - f2 = no('foobar') - f3 = no('foobar') - f1.path = Path(u'foo1\u00e9') - f2.path = Path(u'foo2\u00e9') - f3.path = Path(u'foo3\u00e9') - s.ignore_list.Ignore(unicode(f1.path),unicode(f2.path)) - s.ignore_list.Ignore(unicode(f1.path),unicode(f3.path)) - r = s.GetDupeGroups([f1,f2,f3]) - eq_(len(r), 1) - g = r[0] - eq_(len(g.dupes), 1) - assert f1 not in g - assert f2 in g - assert f3 in g - -def test_file_evaluates_to_false(): - # A very wrong way to use any() was added at some point, causing resulting group list - # to be empty. - class FalseNamedObject(NamedObject): - def __nonzero__(self): - return False + def test_tag_scan_non_ascii(self): + s = Scanner() + s.scan_type = SCAN_TYPE_TAG + s.scanned_tags = set(['title']) + o1 = no('foo') + o2 = no('bar') + o1.title = u'foobar\u00e9' + o2.title = u'foobar\u00e9' + try: + r = s.GetDupeGroups([o1, o2]) + except UnicodeEncodeError: + raise AssertionError() + eq_(len(r), 1) + + def test_audio_content_scan(self): + s = Scanner() + s.scan_type = SCAN_TYPE_CONTENT_AUDIO + f = [no('foo'), no('bar'), no('bleh')] + f[0].md5 = 'foo' + f[1].md5 = 'bar' + f[2].md5 = 'bleh' + f[0].md5partial = 'foo' + f[1].md5partial = 'foo' + f[2].md5partial = 'bleh' + f[0].audiosize = 1 + f[1].audiosize = 1 + f[2].audiosize = 1 + r = s.GetDupeGroups(f) + eq_(len(r), 1) + eq_(len(r[0]), 2) + + def test_audio_content_scan_compare_sizes_first(self): + class MyFile(no): + @property + def md5partial(file): + raise AssertionError() + + s = Scanner() + s.scan_type = SCAN_TYPE_CONTENT_AUDIO + f = [MyFile('foo'), MyFile('bar')] + f[0].audiosize = 1 + f[1].audiosize = 2 + eq_(len(s.GetDupeGroups(f)), 0) + + def test_ignore_list(self): + s = Scanner() + f1 = no('foobar') + f2 = no('foobar') + f3 = no('foobar') + f1.path = Path('dir1/foobar') + f2.path = Path('dir2/foobar') + f3.path = Path('dir3/foobar') + s.ignore_list.Ignore(str(f1.path),str(f2.path)) + s.ignore_list.Ignore(str(f1.path),str(f3.path)) + r = s.GetDupeGroups([f1,f2,f3]) + eq_(len(r), 1) + g = r[0] + eq_(len(g.dupes), 1) + assert f1 not in g + assert f2 in g + assert f3 in g + # Ignored matches are not counted as discarded + eq_(s.discarded_file_count, 0) + + def test_ignore_list_checks_for_unicode(self): + #scanner was calling path_str for ignore list checks. Since the Path changes, it must + #be unicode(path) + s = Scanner() + f1 = no('foobar') + f2 = no('foobar') + f3 = no('foobar') + f1.path = Path(u'foo1\u00e9') + f2.path = Path(u'foo2\u00e9') + f3.path = Path(u'foo3\u00e9') + s.ignore_list.Ignore(unicode(f1.path),unicode(f2.path)) + s.ignore_list.Ignore(unicode(f1.path),unicode(f3.path)) + r = s.GetDupeGroups([f1,f2,f3]) + eq_(len(r), 1) + g = r[0] + eq_(len(g.dupes), 1) + assert f1 not in g + assert f2 in g + assert f3 in g + + def test_file_evaluates_to_false(self): + # A very wrong way to use any() was added at some point, causing resulting group list + # to be empty. + class FalseNamedObject(NamedObject): + def __nonzero__(self): + return False - s = Scanner() - f1 = FalseNamedObject('foobar') - f2 = FalseNamedObject('foobar') - r = s.GetDupeGroups([f1, f2]) - eq_(len(r), 1) + s = Scanner() + f1 = FalseNamedObject('foobar') + f2 = FalseNamedObject('foobar') + r = s.GetDupeGroups([f1, f2]) + eq_(len(r), 1) + + def test_size_threshold(self): + # Only file equal or higher than the size_threshold in size are scanned + s = Scanner() + f1 = no('foo', 1) + f2 = no('foo', 2) + f3 = no('foo', 3) + s.size_threshold = 2 + groups = s.GetDupeGroups([f1,f2,f3]) + eq_(len(groups), 1) + [group] = groups + eq_(len(group), 2) + assert f1 not in group + assert f2 in group + assert f3 in group + + def test_tie_breaker_path_deepness(self): + # If there is a tie in prioritization, path deepness is used as a tie breaker + s = Scanner() + o1, o2 = no('foo'), no('foo') + o1.path = Path('foo') + o2.path = Path('foo/bar') + [group] = s.GetDupeGroups([o1, o2]) + assert group.ref is o2 + + def test_tie_breaker_copy(self): + # if copy is in the words used (even if it has a deeper path), it becomes a dupe + s = Scanner() + o1, o2 = no('foo bar Copy'), no('foo bar') + o1.path = Path('deeper/path') + o2.path = Path('foo') + [group] = s.GetDupeGroups([o1, o2]) + assert group.ref is o2 + + def test_tie_breaker_same_name_plus_digit(self): + # if ref has the same words as dupe, but has some just one extra word which is a digit, it + # becomes a dupe + s = Scanner() + o1, o2 = no('foo bar 42'), no('foo bar') + o1.path = Path('deeper/path') + o2.path = Path('foo') + [group] = s.GetDupeGroups([o1, o2]) + assert group.ref is o2 + + def test_partial_group_match(self): + # Count the number od discarded matches (when a file doesn't match all other dupes of the + # group) in Scanner.discarded_file_count + s = Scanner() + o1, o2, o3 = no('a b'), no('a'), no('b') + s.min_match_percentage = 50 + [group] = s.GetDupeGroups([o1, o2, o3]) + eq_(len(group), 2) + assert o1 in group + assert o2 in group + assert o3 not in group + eq_(s.discarded_file_count, 1) + -def test_size_threshold(): - # Only file equal or higher than the size_threshold in size are scanned - s = Scanner() - f1 = no('foo', 1) - f2 = no('foo', 2) - f3 = no('foo', 3) - s.size_threshold = 2 - groups = s.GetDupeGroups([f1,f2,f3]) - eq_(len(groups), 1) - [group] = groups - eq_(len(group), 2) - assert f1 not in group - assert f2 in group - assert f3 in group - -def test_tie_breaker_path_deepness(): - # If there is a tie in prioritization, path deepness is used as a tie breaker - s = Scanner() - o1, o2 = no('foo'), no('foo') - o1.path = Path('foo') - o2.path = Path('foo/bar') - [group] = s.GetDupeGroups([o1, o2]) - assert group.ref is o2 - -def test_tie_breaker_copy(): - # if copy is in the words used (even if it has a deeper path), it becomes a dupe - s = Scanner() - o1, o2 = no('foo bar Copy'), no('foo bar') - o1.path = Path('deeper/path') - o2.path = Path('foo') - [group] = s.GetDupeGroups([o1, o2]) - assert group.ref is o2 - -def test_tie_breaker_same_name_plus_digit(): - # if ref has the same words as dupe, but has some just one extra word which is a digit, it - # becomes a dupe - s = Scanner() - o1, o2 = no('foo bar 42'), no('foo bar') - o1.path = Path('deeper/path') - o2.path = Path('foo') - [group] = s.GetDupeGroups([o1, o2]) - assert group.ref is o2 - -def test_partial_group_match(): - # Count the number od discarded matches (when a file doesn't match all other dupes of the - # group) in Scanner.discarded_file_count - s = Scanner() - o1, o2, o3 = no('a b'), no('a'), no('b') - s.min_match_percentage = 50 - [group] = s.GetDupeGroups([o1, o2, o3]) - eq_(len(group), 2) - assert o1 in group - assert o2 in group - assert o3 not in group - eq_(s.discarded_file_count, 1) +class ScannerTest(TestCase): + def test_dont_group_files_that_dont_exist(self): + # when creating groups, check that files exist first. It's possible that these files have + # been moved during the scan by the user. + # In this test, we have to delete one of the files between the get_matches() part and the + # get_groups() part. + s = Scanner() + s.scan_type = SCAN_TYPE_CONTENT + p = self.tmppath() + io.open(p + 'file1', 'w').write('foo') + io.open(p + 'file2', 'w').write('foo') + file1, file2 = fs.get_files(p) + def getmatches(*args, **kw): + io.remove(file2.path) + return [Match(file1, file2, 100)] + s._getmatches = getmatches + + assert not s.GetDupeGroups([file1, file2]) + \ No newline at end of file From 600c7906a410f6578ea211bdf2f5a06cb7095fd8 Mon Sep 17 00:00:00 2001 From: hsoft Date: Fri, 30 Oct 2009 11:19:32 +0000 Subject: [PATCH 220/275] [#13 state:port] On cocoa, the columns were already remembered, so only the window size/pos had to be remembered. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40226 --- se/cocoa/xib/MainMenu.xib | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/se/cocoa/xib/MainMenu.xib b/se/cocoa/xib/MainMenu.xib index 80bf96cd..55abdcc3 100644 --- a/se/cocoa/xib/MainMenu.xib +++ b/se/cocoa/xib/MainMenu.xib @@ -13,7 +13,7 @@ - + com.apple.InterfaceBuilder.CocoaPlugin @@ -60,6 +60,7 @@ 256 {{7, 14}, {67, 24}} + YES 67239424 @@ -153,6 +154,7 @@ 258 {{0, 14}, {81, 22}} + YES 343014976 @@ -288,6 +290,7 @@ 256 {{0, 14}, {58, 26}} + YES -2076049856 @@ -493,6 +496,7 @@ 256 {{4, 14}, {67, 24}} + YES 67239424 @@ -947,6 +951,7 @@ {{0, 0}, {1440, 878}} {340, 418} {1.79769e+308, 1.79769e+308} + MainWindow MainMenu @@ -2772,14 +2777,6 @@ 718 - - - actionMenu - - - - 726 - deleteMarked: @@ -4669,7 +4666,7 @@ com.apple.InterfaceBuilder.CocoaPlugin - {{79, 539}, {617, 227}} + {{79, 766}, {617, 0}} com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin tbbScan @@ -5165,7 +5162,6 @@ id - NSPopUpButton NSMenu NSSearchField NSWindow From 484529add0e3ef02f94586bf16fd608a21f41caa Mon Sep 17 00:00:00 2001 From: hsoft Date: Fri, 30 Oct 2009 11:30:32 +0000 Subject: [PATCH 221/275] [#13] Remember window size/pos. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40227 --- me/cocoa/xib/MainMenu.xib | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/me/cocoa/xib/MainMenu.xib b/me/cocoa/xib/MainMenu.xib index b423cda2..98557add 100644 --- a/me/cocoa/xib/MainMenu.xib +++ b/me/cocoa/xib/MainMenu.xib @@ -11,9 +11,9 @@ 740 - + com.apple.InterfaceBuilder.CocoaPlugin @@ -973,6 +973,7 @@ {{0, 0}, {1440, 878}} {340, 418} {1.79769e+308, 1.79769e+308} + MainWindow MainMenu From 3c90ad81a7913ec02728583e920f585e9339797e Mon Sep 17 00:00:00 2001 From: hsoft Date: Fri, 30 Oct 2009 11:31:40 +0000 Subject: [PATCH 222/275] [#13 state:fixed] Remember window size/pos. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40228 --- pe/cocoa/xib/MainMenu.xib | 37 +++++++++++++++---------------------- 1 file changed, 15 insertions(+), 22 deletions(-) diff --git a/pe/cocoa/xib/MainMenu.xib b/pe/cocoa/xib/MainMenu.xib index 6a458727..91c26344 100644 --- a/pe/cocoa/xib/MainMenu.xib +++ b/pe/cocoa/xib/MainMenu.xib @@ -76,11 +76,9 @@ Action - + 256 {{0, 14}, {58, 26}} - - YES -2076049856 @@ -91,12 +89,8 @@ 1044 - 109068543 + 109199615 1 - - NSImage - NSActionTemplate - @@ -111,7 +105,10 @@ 1048576 2147483647 1 - + + NSImage + NSActionTemplate + NSImage NSMenuCheckmark @@ -267,7 +264,7 @@ 1 - + {58, 26} @@ -286,11 +283,9 @@ Power Marker - + 256 {{7, 14}, {67, 24}} - - YES 67239424 @@ -402,11 +397,9 @@ Filter - + 258 {{0, 14}, {81, 22}} - - YES 343014976 @@ -490,11 +483,9 @@ Delta Values - + 256 {{4, 14}, {67, 24}} - - YES 67239424 @@ -949,6 +940,7 @@ {{0, 0}, {1440, 878}} {340, 418} {1.79769e+308, 1.79769e+308} + MainWindow MainMenu @@ -4295,9 +4287,6 @@ {{58, 789}, {617, 0}} com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin tbbScan com.apple.InterfaceBuilder.CocoaPlugin @@ -4308,9 +4297,13 @@ tbbDetail com.apple.InterfaceBuilder.CocoaPlugin tbbPreferences + com.apple.InterfaceBuilder.CocoaPlugin tbbAction + com.apple.InterfaceBuilder.CocoaPlugin tbbPowerMarker + com.apple.InterfaceBuilder.CocoaPlugin tbbDelta + com.apple.InterfaceBuilder.CocoaPlugin tbbFilter com.apple.InterfaceBuilder.CocoaPlugin From 6adce9bf0386d3dd4eccbaba33be982f1399c4eb Mon Sep 17 00:00:00 2001 From: hsoft Date: Fri, 30 Oct 2009 11:41:14 +0000 Subject: [PATCH 223/275] dgbase qt: Now uses qtlib's about_box and reg. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40229 --- base/qt/about_box.py | 37 --------- base/qt/about_box.ui | 133 ------------------------------- base/qt/app.py | 5 +- base/qt/reg.py | 36 --------- base/qt/reg_demo_dialog.py | 47 ----------- base/qt/reg_demo_dialog.ui | 140 -------------------------------- base/qt/reg_submit_dialog.py | 49 ------------ base/qt/reg_submit_dialog.ui | 149 ----------------------------------- 8 files changed, 3 insertions(+), 593 deletions(-) delete mode 100644 base/qt/about_box.py delete mode 100644 base/qt/about_box.ui delete mode 100644 base/qt/reg.py delete mode 100644 base/qt/reg_demo_dialog.py delete mode 100644 base/qt/reg_demo_dialog.ui delete mode 100644 base/qt/reg_submit_dialog.py delete mode 100644 base/qt/reg_submit_dialog.ui diff --git a/base/qt/about_box.py b/base/qt/about_box.py deleted file mode 100644 index c72a7dfb..00000000 --- a/base/qt/about_box.py +++ /dev/null @@ -1,37 +0,0 @@ -# Created By: Virgil Dupras -# Created On: 2009-05-09 -# $Id$ -# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "HS" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at -# http://www.hardcoded.net/licenses/hs_license - -from PyQt4.QtCore import Qt, QCoreApplication, SIGNAL -from PyQt4.QtGui import QDialog, QDialogButtonBox, QPixmap - -from about_box_ui import Ui_AboutBox - -class AboutBox(QDialog, Ui_AboutBox): - def __init__(self, parent, app): - flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint | Qt.MSWindowsFixedSizeDialogHint - QDialog.__init__(self, parent, flags) - self.app = app - self._setupUi() - - self.connect(self.buttonBox, SIGNAL('clicked(QAbstractButton*)'), self.buttonClicked) - - def _setupUi(self): - self.setupUi(self) - # Stuff that can't be done in the Designer - self.setWindowTitle(u"About %s" % QCoreApplication.instance().applicationName()) - self.nameLabel.setText(QCoreApplication.instance().applicationName()) - self.versionLabel.setText('Version ' + QCoreApplication.instance().applicationVersion()) - self.logoLabel.setPixmap(QPixmap(':/%s_big' % self.app.LOGO_NAME)) - self.registerButton = self.buttonBox.addButton("Register", QDialogButtonBox.ActionRole) - - #--- Events - def buttonClicked(self, button): - if button is self.registerButton: - self.app.ask_for_reg_code() - diff --git a/base/qt/about_box.ui b/base/qt/about_box.ui deleted file mode 100644 index aa9c5ce5..00000000 --- a/base/qt/about_box.ui +++ /dev/null @@ -1,133 +0,0 @@ - - - AboutBox - - - - 0 - 0 - 400 - 190 - - - - - 0 - 0 - - - - About dupeGuru - - - - - - - - - :/logo_me_big - - - - - - - - - - 75 - true - - - - dupeGuru - - - - - - - Version - - - - - - - Copyright Hardcoded Software 2009 - - - - - - - - 75 - true - - - - Registered To: - - - - - - - UNREGISTERED - - - - - - - Qt::Horizontal - - - QDialogButtonBox::Ok - - - - - - - - - - - - - buttonBox - accepted() - AboutBox - accept() - - - 248 - 254 - - - 157 - 274 - - - - - buttonBox - rejected() - AboutBox - reject() - - - 316 - 260 - - - 286 - 274 - - - - - diff --git a/base/qt/app.py b/base/qt/app.py index 93a5e293..2d214cf1 100644 --- a/base/qt/app.py +++ b/base/qt/app.py @@ -23,14 +23,14 @@ from dupeguru import fs from dupeguru.app import (DupeGuru as DupeGuruBase, JOB_SCAN, JOB_LOAD, JOB_MOVE, JOB_COPY, JOB_DELETE) +from qtlib.about_box import AboutBox from qtlib.progress import Progress +from qtlib.reg import Registration from . import platform from .main_window import MainWindow from .directories_dialog import DirectoriesDialog -from .about_box import AboutBox -from .reg import Registration JOBID2TITLE = { JOB_SCAN: "Scanning for duplicates", @@ -54,6 +54,7 @@ class DupeGuru(DupeGuruBase, QObject): LOGO_NAME = '' NAME = '' DELTA_COLUMNS = frozenset() + DEMO_LIMIT_DESC = "In the demo version, only 10 duplicates per session can be sent to the recycle bin, moved or copied." def __init__(self, data_module, appid): appdata = unicode(QDesktopServices.storageLocation(QDesktopServices.DataLocation)) diff --git a/base/qt/reg.py b/base/qt/reg.py deleted file mode 100644 index 79653f08..00000000 --- a/base/qt/reg.py +++ /dev/null @@ -1,36 +0,0 @@ -# Created By: Virgil Dupras -# Created On: 2009-05-09 -# $Id$ -# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "HS" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at -# http://www.hardcoded.net/licenses/hs_license - -from hashlib import md5 - -from PyQt4.QtGui import QDialog - -from reg_submit_dialog import RegSubmitDialog -from reg_demo_dialog import RegDemoDialog - -class Registration(object): - def __init__(self, app): - self.app = app - - def ask_for_code(self): - dialog = RegSubmitDialog(self.app.main_window, self.app.is_code_valid) - result = dialog.exec_() - code = unicode(dialog.codeEdit.text()) - email = unicode(dialog.emailEdit.text()) - dialog.setParent(None) # free it - if result == QDialog.Accepted and self.app.is_code_valid(code, email): - self.app.set_registration(code, email) - return True - return False - - def show_nag(self): - dialog = RegDemoDialog(self.app.main_window, self) - dialog.exec_() - dialog.setParent(None) # free it - diff --git a/base/qt/reg_demo_dialog.py b/base/qt/reg_demo_dialog.py deleted file mode 100644 index a038f7c5..00000000 --- a/base/qt/reg_demo_dialog.py +++ /dev/null @@ -1,47 +0,0 @@ -# Created By: Virgil Dupras -# Created On: 2009-05-10 -# $Id$ -# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "HS" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at -# http://www.hardcoded.net/licenses/hs_license - -from PyQt4.QtCore import SIGNAL, Qt, QUrl, QCoreApplication -from PyQt4.QtGui import QDialog, QMessageBox, QDesktopServices - -from reg_demo_dialog_ui import Ui_RegDemoDialog - -class RegDemoDialog(QDialog, Ui_RegDemoDialog): - def __init__(self, parent, reg): - flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint - QDialog.__init__(self, parent, flags) - self.reg = reg - self._setupUi() - - self.connect(self.enterCodeButton, SIGNAL('clicked()'), self.enterCodeClicked) - self.connect(self.purchaseButton, SIGNAL('clicked()'), self.purchaseClicked) - - def _setupUi(self): - self.setupUi(self) - # Stuff that can't be setup in the Designer - appname = QCoreApplication.instance().applicationName() - title = self.windowTitle() - title = title.replace('$appname', appname) - self.setWindowTitle(title) - title = self.titleLabel.text() - title = title.replace('$appname', appname) - self.titleLabel.setText(title) - desc = self.demoDescLabel.text() - desc = desc.replace('$appname', appname) - self.demoDescLabel.setText(desc) - - #--- Events - def enterCodeClicked(self): - if self.reg.ask_for_code(): - self.accept() - - def purchaseClicked(self): - url = QUrl('http://www.hardcoded.net/purchase.htm') - QDesktopServices.openUrl(url) - diff --git a/base/qt/reg_demo_dialog.ui b/base/qt/reg_demo_dialog.ui deleted file mode 100644 index ef918225..00000000 --- a/base/qt/reg_demo_dialog.ui +++ /dev/null @@ -1,140 +0,0 @@ - - - RegDemoDialog - - - - 0 - 0 - 387 - 161 - - - - $appname Demo Version - - - - - - - 75 - true - - - - $appname Demo Version - - - - - - - You are currently running a demo version of $appname. This version has limited functionalities, and you need to buy it to have access to these functionalities. - - - true - - - - - - - In the demo version, only 10 duplicates per session can be sent to the recycle bin, moved or copied. - - - true - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - 110 - 0 - - - - Try Demo - - - - - - - - 110 - 0 - - - - Enter Code - - - - - - - - 110 - 0 - - - - Purchase - - - - - - - - - - - tryButton - clicked() - RegDemoDialog - accept() - - - 112 - 161 - - - 201 - 94 - - - - - diff --git a/base/qt/reg_submit_dialog.py b/base/qt/reg_submit_dialog.py deleted file mode 100644 index 2befc08f..00000000 --- a/base/qt/reg_submit_dialog.py +++ /dev/null @@ -1,49 +0,0 @@ -# Created By: Virgil Dupras -# Created On: 2009-05-09 -# $Id$ -# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "HS" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at -# http://www.hardcoded.net/licenses/hs_license - -from PyQt4.QtCore import SIGNAL, Qt, QUrl, QCoreApplication -from PyQt4.QtGui import QDialog, QMessageBox, QDesktopServices - -from reg_submit_dialog_ui import Ui_RegSubmitDialog - -class RegSubmitDialog(QDialog, Ui_RegSubmitDialog): - def __init__(self, parent, is_valid_func): - flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint - QDialog.__init__(self, parent, flags) - self._setupUi() - self.is_valid_func = is_valid_func - - self.connect(self.submitButton, SIGNAL('clicked()'), self.submitClicked) - self.connect(self.purchaseButton, SIGNAL('clicked()'), self.purchaseClicked) - - def _setupUi(self): - self.setupUi(self) - # Stuff that can't be setup in the Designer - appname = QCoreApplication.instance().applicationName() - prompt = self.promptLabel.text() - prompt = prompt.replace('$appname', appname) - self.promptLabel.setText(prompt) - - #--- Events - def purchaseClicked(self): - url = QUrl('http://www.hardcoded.net/purchase.htm') - QDesktopServices.openUrl(url) - - def submitClicked(self): - code = unicode(self.codeEdit.text()) - email = unicode(self.emailEdit.text()) - title = "Registration" - if self.is_valid_func(code, email): - msg = "This code is valid. Thanks!" - QMessageBox.information(self, title, msg) - self.accept() - else: - msg = "This code is invalid" - QMessageBox.warning(self, title, msg) - diff --git a/base/qt/reg_submit_dialog.ui b/base/qt/reg_submit_dialog.ui deleted file mode 100644 index 06de4191..00000000 --- a/base/qt/reg_submit_dialog.ui +++ /dev/null @@ -1,149 +0,0 @@ - - - RegSubmitDialog - - - - 0 - 0 - 365 - 134 - - - - Enter your registration code - - - - - - Please enter your $appname registration code and registered e-mail (the e-mail you used for the purchase), then press "Submit". - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop - - - true - - - - - - - QLayout::SetNoConstraint - - - QFormLayout::ExpandingFieldsGrow - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop - - - - - Registration code: - - - - - - - Registered e-mail: - - - - - - - - - - - - - - - - - Purchase - - - false - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - 0 - 0 - - - - Cancel - - - false - - - - - - - - 0 - 0 - - - - Submit - - - false - - - true - - - - - - - - - - - cancelButton - clicked() - RegSubmitDialog - reject() - - - 260 - 159 - - - 198 - 97 - - - - - From 6b30c88fba5288cacd56283d7f4c89c66d4d440f Mon Sep 17 00:00:00 2001 From: hsoft Date: Fri, 30 Oct 2009 12:56:05 +0000 Subject: [PATCH 224/275] dgse cocoa: extracted the pref pane from MainMenu.xib into Preferences.xib, Removed column menu items, which are now generated in the code. other little things. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40230 --- base/cocoa/ResultWindow.h | 9 + base/cocoa/ResultWindow.m | 97 ++ images/folder32.png | Bin 2427 -> 2189 bytes se/cocoa/ResultWindow.h | 10 - se/cocoa/ResultWindow.m | 69 - se/cocoa/dupeguru.xcodeproj/project.pbxproj | 8 +- se/cocoa/py/gen.py | 1 + se/cocoa/xib/MainMenu.xib | 1675 +------------------ se/cocoa/xib/Preferences.xib | 1597 ++++++++++++++++++ 9 files changed, 1736 insertions(+), 1730 deletions(-) create mode 100644 se/cocoa/xib/Preferences.xib diff --git a/base/cocoa/ResultWindow.h b/base/cocoa/ResultWindow.h index 5b062a52..ffd28230 100644 --- a/base/cocoa/ResultWindow.h +++ b/base/cocoa/ResultWindow.h @@ -24,18 +24,25 @@ http://www.hardcoded.net/licenses/hs_license IBOutlet MatchesView *matches; IBOutlet NSSegmentedControl *pmSwitch; IBOutlet NSTextField *stats; + IBOutlet NSMenu *columnsMenu; BOOL _powerMode; BOOL _displayDelta; + NSMutableArray *_resultColumns; + NSWindowController *preferencesPanel; } /* Helpers */ +- (void)fillColumnsMenu; +- (NSTableColumn *)getColumnForIdentifier:(int)aIdentifier title:(NSString *)aTitle width:(int)aWidth refCol:(NSTableColumn *)aColumn; - (NSArray *)getColumnsOrder; - (NSDictionary *)getColumnsWidth; - (NSArray *)getSelected:(BOOL)aDupesOnly; - (NSArray *)getSelectedPaths:(BOOL)aDupesOnly; +- (void)initResultColumns; - (void)updatePySelection; - (void)performPySelection:(NSArray *)aIndexPaths; - (void)refreshStats; +- (void)restoreColumnsPosition:(NSArray *)aColumnsOrder widths:(NSDictionary *)aColumnsWidth; /* Actions */ - (IBAction)changeDelta:(id)sender; @@ -45,7 +52,9 @@ http://www.hardcoded.net/licenses/hs_license - (IBAction)expandAll:(id)sender; - (IBAction)exportToXHTML:(id)sender; - (IBAction)moveMarked:(id)sender; +- (IBAction)showPreferencesPanel:(id)sender; - (IBAction)switchSelected:(id)sender; +- (IBAction)toggleColumn:(id)sender; - (IBAction)togglePowerMarker:(id)sender; /* Notifications */ diff --git a/base/cocoa/ResultWindow.m b/base/cocoa/ResultWindow.m index 42be291a..e55c9976 100644 --- a/base/cocoa/ResultWindow.m +++ b/base/cocoa/ResultWindow.m @@ -51,6 +51,9 @@ http://www.hardcoded.net/licenses/hs_license - (void)awakeFromNib { [self window]; + preferencesPanel = [[NSWindowController alloc] initWithWindowNibName:@"Preferences"]; + [self initResultColumns]; + [self fillColumnsMenu]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(registrationRequired:) name:RegistrationRequired object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(jobCompleted:) name:JobCompletedNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(jobStarted:) name:JobStarted object:nil]; @@ -59,7 +62,42 @@ http://www.hardcoded.net/licenses/hs_license [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(resultsUpdated:) name:ResultsUpdatedNotification object:nil]; } +- (void)dealloc +{ + [preferencesPanel release]; + [super dealloc]; +} + /* Helpers */ +- (void)fillColumnsMenu +{ + // The columns menu is supposed to be empty and initResultColumns must have been called + for (NSTableColumn *col in _resultColumns) + { + NSMenuItem *mi = [columnsMenu addItemWithTitle:[[col headerCell] stringValue] action:@selector(toggleColumn:) keyEquivalent:@""]; + [mi setTag:[[col identifier] integerValue]]; + [mi setTarget:self]; + if ([[matches tableColumns] containsObject:col]) + [mi setState:NSOnState]; + } + [columnsMenu addItem:[NSMenuItem separatorItem]]; + NSMenuItem *mi = [columnsMenu addItemWithTitle:@"Reset to Default" action:@selector(resetColumnsToDefault:) keyEquivalent:@""]; + [mi setTarget:self]; +} + +- (NSTableColumn *)getColumnForIdentifier:(int)aIdentifier title:(NSString *)aTitle width:(int)aWidth refCol:(NSTableColumn *)aColumn +{ + NSNumber *n = [NSNumber numberWithInt:aIdentifier]; + NSTableColumn *col = [[NSTableColumn alloc] initWithIdentifier:[n stringValue]]; + [col setWidth:aWidth]; + [col setEditable:NO]; + [[col dataCell] setFont:[[aColumn dataCell] font]]; + [[col headerCell] setStringValue:aTitle]; + [col setResizingMask:NSTableColumnUserResizingMask]; + [col setSortDescriptorPrototype:[[NSSortDescriptor alloc] initWithKey:[n stringValue] ascending:YES]]; + return col; +} + //Returns an array of identifiers, in order. - (NSArray *)getColumnsOrder { @@ -120,6 +158,40 @@ http://www.hardcoded.net/licenses/hs_license return r; } +- (void)initResultColumns +{ + // Virtual +} + +- (void)restoreColumnsPosition:(NSArray *)aColumnsOrder widths:(NSDictionary *)aColumnsWidth +{ + NSTableColumn *col; + NSString *colId; + NSNumber *width; + NSMenuItem *mi; + //Remove all columns + NSEnumerator *e = [[columnsMenu itemArray] objectEnumerator]; + while (mi = [e nextObject]) + { + if ([mi state] == NSOnState) + [self toggleColumn:mi]; + } + //Add columns and set widths + e = [aColumnsOrder objectEnumerator]; + while (colId = [e nextObject]) + { + if (![colId isEqual:@"mark"]) + { + col = [_resultColumns objectAtIndex:[colId intValue]]; + width = [aColumnsWidth objectForKey:[col identifier]]; + mi = [columnsMenu itemWithTag:[colId intValue]]; + if (width) + [col setWidth:[width floatValue]]; + [self toggleColumn:mi]; + } + } +} + - (void)updatePySelection { NSArray *selection; @@ -227,6 +299,11 @@ http://www.hardcoded.net/licenses/hs_license } } +- (IBAction)showPreferencesPanel:(id)sender +{ + [preferencesPanel showWindow:sender]; +} + - (IBAction)switchSelected:(id)sender { // It might look like a complicated way to get the length of the current dupe list on the py side @@ -244,6 +321,26 @@ http://www.hardcoded.net/licenses/hs_license [[NSNotificationCenter defaultCenter] postNotificationName:ResultsChangedNotification object:self]; } +- (IBAction)toggleColumn:(id)sender +{ + NSMenuItem *mi = sender; + NSString *colId = [NSString stringWithFormat:@"%d",[mi tag]]; + NSTableColumn *col = [matches tableColumnWithIdentifier:colId]; + if (col == nil) + { + //Add Column + col = [_resultColumns objectAtIndex:[mi tag]]; + [matches addTableColumn:col]; + [mi setState:NSOnState]; + } + else + { + //Remove column + [matches removeTableColumn:col]; + [mi setState:NSOffState]; + } +} + - (IBAction)togglePowerMarker:(id)sender { if ([pmSwitch selectedSegment] == 1) diff --git a/images/folder32.png b/images/folder32.png index 85070da7a8a18b27d0c1b9e20daf2c55f4360102..d10341f9010091db419361f2cc48d75f774aa1cd 100755 GIT binary patch delta 2177 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 0) @@ -198,26 +191,6 @@ http://www.hardcoded.net/licenses/hs_license } -- (IBAction)toggleColumn:(id)sender -{ - NSMenuItem *mi = sender; - NSString *colId = [NSString stringWithFormat:@"%d",[mi tag]]; - NSTableColumn *col = [matches tableColumnWithIdentifier:colId]; - if (col == nil) - { - //Add Column - col = [_resultColumns objectAtIndex:[mi tag]]; - [matches addTableColumn:col]; - [mi setState:NSOnState]; - } - else - { - //Remove column - [matches removeTableColumn:col]; - [mi setState:NSOffState]; - } -} - - (IBAction)toggleDelta:(id)sender { if ([deltaSwitch selectedSegment] == 1) @@ -238,19 +211,6 @@ http://www.hardcoded.net/licenses/hs_license } /* Public */ -- (NSTableColumn *)getColumnForIdentifier:(int)aIdentifier title:(NSString *)aTitle width:(int)aWidth refCol:(NSTableColumn *)aColumn -{ - NSNumber *n = [NSNumber numberWithInt:aIdentifier]; - NSTableColumn *col = [[NSTableColumn alloc] initWithIdentifier:[n stringValue]]; - [col setWidth:aWidth]; - [col setEditable:NO]; - [[col dataCell] setFont:[[aColumn dataCell] font]]; - [[col headerCell] setStringValue:aTitle]; - [col setResizingMask:NSTableColumnUserResizingMask]; - [col setSortDescriptorPrototype:[[NSSortDescriptor alloc] initWithKey:[n stringValue] ascending:YES]]; - return col; -} - - (void)initResultColumns { NSTableColumn *refCol = [matches tableColumnWithIdentifier:@"0"]; @@ -266,35 +226,6 @@ http://www.hardcoded.net/licenses/hs_license [_resultColumns addObject:[self getColumnForIdentifier:8 title:@"Dupe Count" width:80 refCol:refCol]]; } -- (void)restoreColumnsPosition:(NSArray *)aColumnsOrder widths:(NSDictionary *)aColumnsWidth -{ - NSTableColumn *col; - NSString *colId; - NSNumber *width; - NSMenuItem *mi; - //Remove all columns - NSEnumerator *e = [[columnsMenu itemArray] objectEnumerator]; - while (mi = [e nextObject]) - { - if ([mi state] == NSOnState) - [self toggleColumn:mi]; - } - //Add columns and set widths - e = [aColumnsOrder objectEnumerator]; - while (colId = [e nextObject]) - { - if (![colId isEqual:@"mark"]) - { - col = [_resultColumns objectAtIndex:[colId intValue]]; - width = [aColumnsWidth objectForKey:[col identifier]]; - mi = [columnsMenu itemWithTag:[colId intValue]]; - if (width) - [col setWidth:[width floatValue]]; - [self toggleColumn:mi]; - } - } -} - /* Delegate */ - (void)outlineView:(NSOutlineView *)outlineView willDisplayCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn item:(id)item { diff --git a/se/cocoa/dupeguru.xcodeproj/project.pbxproj b/se/cocoa/dupeguru.xcodeproj/project.pbxproj index 650e1593..a30c8184 100644 --- a/se/cocoa/dupeguru.xcodeproj/project.pbxproj +++ b/se/cocoa/dupeguru.xcodeproj/project.pbxproj @@ -18,6 +18,7 @@ CE68EE6809ABC48000971085 /* DirectoryPanel.m in Sources */ = {isa = PBXBuildFile; fileRef = CE68EE6609ABC48000971085 /* DirectoryPanel.m */; }; CE6E0DFE1054E9EF008D9390 /* dsa_pub.pem in Resources */ = {isa = PBXBuildFile; fileRef = CE6E0DFD1054E9EF008D9390 /* dsa_pub.pem */; }; CE848A1909DD85810004CB44 /* Consts.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = CE848A1809DD85810004CB44 /* Consts.h */; }; + CEAC6811109B0B7E00B43C85 /* Preferences.xib in Resources */ = {isa = PBXBuildFile; fileRef = CEAC6810109B0B7E00B43C85 /* Preferences.xib */; }; CECA899C09DB132E00A3D774 /* DetailsPanel.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = CECA899A09DB132E00A3D774 /* DetailsPanel.h */; }; CECA899D09DB132E00A3D774 /* DetailsPanel.m in Sources */ = {isa = PBXBuildFile; fileRef = CECA899B09DB132E00A3D774 /* DetailsPanel.m */; }; CEDD92DA0FDD01640031C7B7 /* BRSingleLineFormatter.m in Sources */ = {isa = PBXBuildFile; fileRef = CEDD92D70FDD01640031C7B7 /* BRSingleLineFormatter.m */; }; @@ -30,7 +31,6 @@ CEFC294609C89E3D00D9F998 /* folder32.png in Resources */ = {isa = PBXBuildFile; fileRef = CEFC294509C89E3D00D9F998 /* folder32.png */; }; CEFC295509C89FF200D9F998 /* details32.png in Resources */ = {isa = PBXBuildFile; fileRef = CEFC295309C89FF200D9F998 /* details32.png */; }; CEFC295609C89FF200D9F998 /* preferences32.png in Resources */ = {isa = PBXBuildFile; fileRef = CEFC295409C89FF200D9F998 /* preferences32.png */; }; - CEFC295E09C8A0B000D9F998 /* dgse_logo_32.png in Resources */ = {isa = PBXBuildFile; fileRef = CEFC295D09C8A0B000D9F998 /* dgse_logo_32.png */; }; CEFC7F9E0FC9517500CD5728 /* Dialogs.m in Sources */ = {isa = PBXBuildFile; fileRef = CEFC7F8B0FC9517500CD5728 /* Dialogs.m */; }; CEFC7F9F0FC9517500CD5728 /* HSErrorReportWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = CEFC7F8D0FC9517500CD5728 /* HSErrorReportWindow.m */; }; CEFC7FA00FC9517500CD5728 /* Outline.m in Sources */ = {isa = PBXBuildFile; fileRef = CEFC7F8F0FC9517500CD5728 /* Outline.m */; }; @@ -82,6 +82,7 @@ CE68EE6609ABC48000971085 /* DirectoryPanel.m */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.objc; path = DirectoryPanel.m; sourceTree = SOURCE_ROOT; }; CE6E0DFD1054E9EF008D9390 /* dsa_pub.pem */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = dsa_pub.pem; path = dgbase/dsa_pub.pem; sourceTree = ""; }; CE848A1809DD85810004CB44 /* Consts.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Consts.h; sourceTree = ""; }; + CEAC6810109B0B7E00B43C85 /* Preferences.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Preferences.xib; path = xib/Preferences.xib; sourceTree = ""; }; CECA899A09DB132E00A3D774 /* DetailsPanel.h */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.h; path = DetailsPanel.h; sourceTree = ""; }; CECA899B09DB132E00A3D774 /* DetailsPanel.m */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.objc; path = DetailsPanel.m; sourceTree = ""; }; CEDD92D60FDD01640031C7B7 /* BRSingleLineFormatter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BRSingleLineFormatter.h; path = cocoalib/brsinglelineformatter/BRSingleLineFormatter.h; sourceTree = SOURCE_ROOT; }; @@ -97,7 +98,6 @@ CEFC294509C89E3D00D9F998 /* folder32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = folder32.png; path = images/folder32.png; sourceTree = SOURCE_ROOT; }; CEFC295309C89FF200D9F998 /* details32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = details32.png; path = images/details32.png; sourceTree = SOURCE_ROOT; }; CEFC295409C89FF200D9F998 /* preferences32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = preferences32.png; path = images/preferences32.png; sourceTree = SOURCE_ROOT; }; - CEFC295D09C8A0B000D9F998 /* dgse_logo_32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = dgse_logo_32.png; path = images/dgse_logo_32.png; sourceTree = ""; }; CEFC7F8A0FC9517500CD5728 /* Dialogs.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Dialogs.h; path = cocoalib/Dialogs.h; sourceTree = SOURCE_ROOT; }; CEFC7F8B0FC9517500CD5728 /* Dialogs.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Dialogs.m; path = cocoalib/Dialogs.m; sourceTree = SOURCE_ROOT; }; CEFC7F8C0FC9517500CD5728 /* HSErrorReportWindow.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = HSErrorReportWindow.h; path = cocoalib/HSErrorReportWindow.h; sourceTree = SOURCE_ROOT; }; @@ -241,6 +241,7 @@ CEEFC0CA10943849001F3A39 /* xib */ = { isa = PBXGroup; children = ( + CEAC6810109B0B7E00B43C85 /* Preferences.xib */, CEEFC0EE10944EDD001F3A39 /* MainMenu.xib */, CEEFC0F710945D9F001F3A39 /* DirectoryPanel.xib */, CEEFC0FA10945E37001F3A39 /* DetailsPanel.xib */, @@ -251,7 +252,6 @@ CEFC294309C89E0000D9F998 /* images */ = { isa = PBXGroup; children = ( - CEFC295D09C8A0B000D9F998 /* dgse_logo_32.png */, CEFC295309C89FF200D9F998 /* details32.png */, CEFC295409C89FF200D9F998 /* preferences32.png */, CEFC294509C89E3D00D9F998 /* folder32.png */, @@ -360,7 +360,6 @@ CEFC294609C89E3D00D9F998 /* folder32.png in Resources */, CEFC295509C89FF200D9F998 /* details32.png in Resources */, CEFC295609C89FF200D9F998 /* preferences32.png in Resources */, - CEFC295E09C8A0B000D9F998 /* dgse_logo_32.png in Resources */, CEFC7FAD0FC9518A00CD5728 /* ErrorReportWindow.xib in Resources */, CEFC7FAE0FC9518A00CD5728 /* progress.nib in Resources */, CEFC7FAF0FC9518A00CD5728 /* registration.nib in Resources */, @@ -368,6 +367,7 @@ CEEFC0EF10944EDD001F3A39 /* MainMenu.xib in Resources */, CEEFC0F810945D9F001F3A39 /* DirectoryPanel.xib in Resources */, CEEFC0FB10945E37001F3A39 /* DetailsPanel.xib in Resources */, + CEAC6811109B0B7E00B43C85 /* Preferences.xib in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/se/cocoa/py/gen.py b/se/cocoa/py/gen.py index d4991df3..f8bc07c4 100644 --- a/se/cocoa/py/gen.py +++ b/se/cocoa/py/gen.py @@ -16,4 +16,5 @@ if op.exists('build'): if op.exists('dist'): shutil.rmtree('dist') +os.environ['MACOSX_DEPLOYMENT_TARGET'] = '10.5' print_and_do('python -u setup.py py2app') \ No newline at end of file diff --git a/se/cocoa/xib/MainMenu.xib b/se/cocoa/xib/MainMenu.xib index 55abdcc3..0a6b2538 100644 --- a/se/cocoa/xib/MainMenu.xib +++ b/se/cocoa/xib/MainMenu.xib @@ -11,9 +11,8 @@ 740 - - + com.apple.InterfaceBuilder.CocoaPlugin @@ -56,11 +55,9 @@ Power Marker - + 256 {{7, 14}, {67, 24}} - - YES 67239424 @@ -108,7 +105,7 @@ NSImage - dgse_logo_32 + NSApplicationIcon @@ -150,11 +147,9 @@ Filter - + 258 {{0, 14}, {81, 22}} - - YES 343014976 @@ -169,7 +164,7 @@ YES 1 - + 6 System textBackgroundColor @@ -286,11 +281,9 @@ Action - + 256 {{0, 14}, {58, 26}} - - YES -2076049856 @@ -492,11 +485,9 @@ Delta Values - + 256 {{4, 14}, {67, 24}} - - YES 67239424 @@ -705,7 +696,7 @@ 1211912703 2 - + NSSwitch @@ -935,7 +926,7 @@ Marked: 0 files, 0 B. Total: 0 files, 0 B. - + 6 System controlColor @@ -1342,123 +1333,7 @@ submenuAction: Columns - - - - File Name - - 1048576 - 2147483647 - 1 - - - - - - Directory - - 1048576 - 2147483647 - 1 - - - 1 - - - - Size - - 1048576 - 2147483647 - 1 - - - 2 - - - - Kind - - 1048576 - 2147483647 - - - 3 - - - - Creation - - 1048576 - 2147483647 - - - 4 - - - - Modification - - 1048576 - 2147483647 - - - 5 - - - - Match % - - 1048576 - 2147483647 - 1 - - - 6 - - - - Words Used - - 1048576 - 2147483647 - - - 7 - - - - Dupe Count - - 1048576 - 2147483647 - - - 8 - - - - YES - YES - IA - - 1048576 - 2147483647 - - - -1 - - - - Reset to Default - - 1048576 - 2147483647 - - - -1 - - + @@ -1627,580 +1502,9 @@ ResultWindow - - YES - RecentDirectories - - 3 - 2 - {{92, 276}, {352, 326}} - 1886912512 - dupeGuru Preferences - - NSWindow - - - View - - {1.79769e+308, 1.79769e+308} - {213, 107} - - - 256 - - - - 292 - {{120, 247}, {181, 21}} - - YES - - 67239424 - 0 - - - - - Helvetica - 12 - 16 - - - 100 - 1 - 80 - 0.0 - 0 - 1 - NO - NO - - - - - 292 - {{122, 230}, {80, 13}} - - YES - - 67239424 - 272629760 - More results - - LucidaGrande - 10 - 2843 - - - - - - - - - 289 - {{219, 230}, {80, 13}} - - YES - - 67239424 - 71303168 - Less results - - - - - - - - - 292 - {{17, 252}, {100, 14}} - - YES - - 67239424 - 272629760 - Filter hardness: - - - - - - - - - 292 - {{20, 293}, {85, 13}} - - YES - - 67239424 - 272629760 - Scan type: - - - - - - - - - 292 - {{119, 282}, {216, 26}} - - YES - - -2076049856 - 2048 - - - 109199615 - 1 - - - - - - 400 - 75 - - - Filename - - 1048576 - 2147483647 - 1 - - - _popUpItemAction: - - - YES - - - OtherViews - - - - - - Content - - 1048576 - 2147483647 - - - _popUpItemAction: - - - - - 3 - YES - YES - 1 - - - - - 256 - {{18, 206}, {214, 18}} - - YES - - 67239424 - 0 - Word weighting - - - 1211912703 - 2 - - - - 200 - 25 - - - - - 256 - {{18, 166}, {214, 18}} - - YES - - 67239424 - 0 - Can mix file kind - - - 1211912703 - 2 - - - - 200 - 25 - - - - - 256 - {{304, 252}, {31, 14}} - - YES - - 67239424 - -1874853888 - - - - - - - 0 - - - . - - , - -0 - 0 - - - 0 - -0 - - - - - - - - NaN - - - - 0 - 0 - YES - NO - 1 - AAAAAAAAAAAAAAAAAAAAAA - - - - . - , - NO - YES - YES - - - - - - - - - 256 - {{190, 12}, {148, 32}} - - YES - - 67239424 - 134217728 - Reset to Defaults - - - -2038284033 - 1 - - - - - - 200 - 25 - - - - - 256 - {{18, 186}, {214, 18}} - - YES - - 67239424 - 0 - Match similar words - - - 1211912703 - 2 - - - - 200 - 25 - - - - - 292 - {{20, 67}, {85, 13}} - - YES - - 67239424 - 272629760 - Copy and Move: - - - - - - - - - 292 - {{110, 56}, {216, 26}} - - YES - - -2076049856 - 2048 - - - 109199615 - 1 - - - - - - 400 - 75 - - - Right in destination - - 1048576 - 2147483647 - 1 - - - _popUpItemAction: - - - YES - - - OtherViews - - - - - - Recreate relative path - - 1048576 - 2147483647 - - - _popUpItemAction: - - - - - Recreate absolute path - - 1048576 - 2147483647 - - - _popUpItemAction: - - - - - 3 - YES - YES - 1 - - - - - 256 - {{18, 86}, {283, 18}} - - YES - - 67239424 - 0 - Check for update on startup - - - 1211912703 - 2 - - - - 200 - 25 - - - - - 256 - {{18, 146}, {228, 18}} - - YES - - 67239424 - 0 - Use regular expressions when filtering - - - 1211912703 - 2 - - - - 200 - 25 - - - - - 256 - {{18, 126}, {242, 18}} - - YES - - 67239424 - 0 - Remove empty folders on delete or move - - - 1211912703 - 2 - - - - 200 - 25 - - - - - 256 - {{18, 106}, {152, 18}} - - YES - - 67239424 - 0 - Ignore files smaller than - - - 1211912703 - 2 - - - - 200 - 25 - - - - - 268 - {{176, 104}, {59, 22}} - - YES - - -1804468671 - -1874852864 - - - - - - - - - - - - #0 - - - - #0 - #0 - - - - - - NaN - - - - - - 3 - YES - YES - YES - - . - , - NO - NO - NO - - - YES - - - 6 - System - textColor - - - - - - - 292 - {{243, 106}, {23, 17}} - - YES - - 67239424 - 272629760 - KB - - - - - - - - {352, 326} - - - {{0, 0}, {1440, 878}} - {213, 129} - {1.79769e+308, 1.79769e+308} - PyDupeGuru @@ -2449,66 +1753,6 @@ 503 - - - makeKeyAndOrderFront: - - - - 543 - - - - value: values.minMatchPercentage - - - - - - value: values.minMatchPercentage - value - values.minMatchPercentage - 2 - - - 549 - - - - selectedIndex: values.scanType - - - - - - selectedIndex: values.scanType - selectedIndex - values.scanType - 2 - - - 551 - - - - enabled: values.scanType - - - - - - enabled: values.scanType - enabled - values.scanType - - NSValueTransformerName - vtScanTypeIsNotContent - - 2 - - - 554 - toggleDetailsPanel: @@ -2581,122 +1825,6 @@ 616 - - - toggleColumn: - - - - 627 - - - - toggleColumn: - - - - 628 - - - - toggleColumn: - - - - 629 - - - - toggleColumn: - - - - 630 - - - - toggleColumn: - - - - 631 - - - - toggleColumn: - - - - 632 - - - - toggleColumn: - - - - 633 - - - - enabled: values.scanType - - - - - - enabled: values.scanType - enabled - values.scanType - - NSValueTransformerName - vtScanTypeIsNotContent - - 2 - - - 640 - - - - value: values.wordWeighting - - - - - - value: values.wordWeighting - value - values.wordWeighting - 2 - - - 642 - - - - toggleColumn: - - - - 647 - - - - value: values.mixFileKind - - - - - - value: values.mixFileKind - value - values.mixFileKind - 2 - - - 656 - openSelected: @@ -2721,14 +1849,6 @@ 663 - - - toggleColumn: - - - - 706 - openSelected: @@ -2745,22 +1865,6 @@ 711 - - - value: values.minMatchPercentage - - - - - - value: values.minMatchPercentage - value - values.minMatchPercentage - 2 - - - 713 - switchSelected: @@ -2769,14 +1873,6 @@ 716 - - - preferencesPanel - - - - 718 - deleteMarked: @@ -2881,42 +1977,6 @@ 764 - - - enabled: values.scanType - - - - - - enabled: values.scanType - enabled - values.scanType - - NSValueTransformerName - vtScanTypeIsNotContent - - 2 - - - 774 - - - - value: values.matchSimilarWords - - - - - - value: values.matchSimilarWords - value - values.matchSimilarWords - 2 - - - 775 - removeSelected: @@ -2941,22 +2001,6 @@ 883 - - - selectedIndex: values.recreatePathType - - - - - - selectedIndex: values.recreatePathType - selectedIndex - values.recreatePathType - 2 - - - 914 - ignoreSelected: @@ -2997,14 +2041,6 @@ 930 - - - revertToInitialValues: - - - - 932 - renameSelected: @@ -3037,14 +2073,6 @@ 941 - - - resetColumnsToDefault: - - - - 945 - columnsMenu @@ -3069,22 +2097,6 @@ 951 - - - value: values.SUCheckAtStartup - - - - - - value: values.SUCheckAtStartup - value - values.SUCheckAtStartup - 2 - - - 953 - changePowerMarker: @@ -3181,22 +2193,6 @@ 1024 - - - value: values.useRegexpFilter - - - - - - value: values.useRegexpFilter - value - values.useRegexpFilter - 2 - - - 1026 - filterField @@ -3213,62 +2209,6 @@ 1031 - - - nextKeyView - - - - 1062 - - - - value: values.removeEmptyFolders - - - - - - value: values.removeEmptyFolders - value - values.removeEmptyFolders - 2 - - - 1072 - - - - value: values.ignoreSmallFiles - - - - - - value: values.ignoreSmallFiles - value - values.ignoreSmallFiles - 2 - - - 1111 - - - - value: values.smallFileThreshold - - - - - - value: values.smallFileThreshold - value - values.smallFileThreshold - 2 - - - 1113 - toggleDirectories: @@ -3301,6 +2241,14 @@ 1169 + + + showPreferencesPanel: + + + + 1174 + @@ -3731,76 +2679,9 @@ 619 - - - - - - - - - - - - - + - - 620 - - - - - 621 - - - - - 622 - - - - - 623 - - - - - 624 - - - - - 625 - - - - - 626 - - - - - 646 - - - - - 705 - - - - - 943 - - - - - 944 - - - 959 @@ -3903,205 +2784,12 @@ ResultWindow - - 468 - - - Shared Defaults - 497 RecentDirectoriesController - - 523 - - - - - - preferences - - - 524 - - - - - - - - - - - - - - - - - - - - - - - - - - 531 - - - - - - - - 532 - - - - - - - - 533 - - - - - - - - 534 - - - - - - - - 535 - - - - - - - - 536 - - - - - - - - 635 - - - - - - - - 649 - - - - - - - - 712 - - - - - - - - 750 - - - - - - - - 772 - - - - - - - - 904 - - - - - - - - 905 - - - - - - - - 952 - - - - - - - - 1025 - - - - - - - - 1068 - - - - - - - - 1104 - - - - - - - - 1106 - - - - - - - - 1109 - - - - - - 613 @@ -4169,113 +2857,6 @@ - - 1117 - - - - - 1118 - - - - - 1119 - - - - - 1120 - - - - - 1121 - - - - - 1122 - - - - - - - - 1123 - - - - - 1124 - - - - - 1125 - - - - - - - - 1126 - - - - - 1127 - - - - - 1128 - - - - - 1129 - - - - - - - - 1130 - - - - - 1131 - - - - - 1132 - - - - - 1133 - - - - - 1134 - - - - - - - - 1135 - - - 1140 @@ -4296,60 +2877,6 @@ - - 714 - - - - - 1108 - - - - - 537 - - - - - - - - - 539 - - - - - 538 - - - - - 906 - - - - - - - - - - 913 - - - - - 909 - - - - - 908 - - - 1144 @@ -4607,47 +3134,15 @@ com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin @@ -4739,43 +3234,16 @@ com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - {{88, 519}, {352, 326}} - com.apple.InterfaceBuilder.CocoaPlugin - {{88, 519}, {352, 326}} - - - {213, 107} - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin + {{152, 575}, {221, 193}} com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin @@ -4804,28 +3272,9 @@ com.apple.InterfaceBuilder.CocoaPlugin + {{355, 762}, {64, 6}} com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - {{182, 609}, {331, 133}} com.apple.InterfaceBuilder.CocoaPlugin @@ -4833,18 +3282,12 @@ com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin @@ -4874,28 +3317,12 @@ com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin @@ -4922,17 +3349,11 @@ com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin @@ -4958,7 +3379,7 @@ - 1173 + 1174 @@ -5155,17 +3576,15 @@ id id id - id id id id id - - NSMenu - NSSearchField - NSWindow - + + filterField + NSSearchField + IBProjectSource ResultWindow.h @@ -5214,11 +3633,13 @@ id id id + id id id id + NSMenu NSSegmentedControl MatchesView NSSegmentedControl @@ -5320,14 +3741,6 @@ AppKit.framework/Headers/NSControl.h - - NSController - NSObject - - IBFrameworkSource - AppKit.framework/Headers/NSController.h - - NSFormatter NSObject @@ -5368,14 +3781,6 @@ AppKit.framework/Headers/NSMovieView.h - - NSNumberFormatter - NSFormatter - - IBFrameworkSource - Foundation.framework/Headers/NSNumberFormatter.h - - NSObject @@ -5718,22 +4123,6 @@ AppKit.framework/Headers/NSSegmentedControl.h - - NSSlider - NSControl - - IBFrameworkSource - AppKit.framework/Headers/NSSlider.h - - - - NSSliderCell - NSActionCell - - IBFrameworkSource - AppKit.framework/Headers/NSSliderCell.h - - NSTableColumn NSObject @@ -5792,14 +4181,6 @@ NSObject - - NSUserDefaultsController - NSController - - IBFrameworkSource - AppKit.framework/Headers/NSUserDefaultsController.h - - NSView diff --git a/se/cocoa/xib/Preferences.xib b/se/cocoa/xib/Preferences.xib new file mode 100644 index 00000000..4418101a --- /dev/null +++ b/se/cocoa/xib/Preferences.xib @@ -0,0 +1,1597 @@ + + + + 1060 + 10B504 + 740 + 1038.2 + 437.00 + + com.apple.InterfaceBuilder.CocoaPlugin + 740 + + + YES + + + + YES + com.apple.InterfaceBuilder.CocoaPlugin + + + YES + + YES + + + YES + + + + YES + + NSWindowController + + + FirstResponder + + + NSApplication + + + YES + + + 3 + 2 + {{92, 276}, {352, 326}} + 1886912512 + dupeGuru Preferences + + NSWindow + + + View + + {1.79769e+308, 1.79769e+308} + {213, 107} + + + 256 + + YES + + + 292 + {{120, 247}, {181, 21}} + + YES + + 67239424 + 0 + + + + + Helvetica + 12 + 16 + + + 100 + 1 + 80 + 0.0 + 0 + 1 + NO + NO + + + + + 292 + {{122, 230}, {80, 13}} + + YES + + 67239424 + 272629760 + More results + + LucidaGrande + 10 + 2843 + + + + 6 + System + controlColor + + 3 + MC42NjY2NjY2NjY3AA + + + + 6 + System + controlTextColor + + 3 + MAA + + + + + + + 289 + {{219, 230}, {80, 13}} + + YES + + 67239424 + 71303168 + Less results + + + + + + + + + 292 + {{17, 252}, {100, 14}} + + YES + + 67239424 + 272629760 + Filter hardness: + + LucidaGrande + 11 + 3100 + + + + + + + + + 292 + {{20, 293}, {85, 13}} + + YES + + 67239424 + 272629760 + Scan type: + + + + + + + + + 292 + {{119, 282}, {216, 26}} + + YES + + -2076049856 + 2048 + + LucidaGrande + 13 + 1044 + + + 109199615 + 1 + + + + + + 400 + 75 + + + Filename + + 1048576 + 2147483647 + 1 + + NSImage + NSMenuCheckmark + + + NSImage + NSMenuMixedState + + _popUpItemAction: + + + YES + + + OtherViews + + + YES + + + + Content + + 1048576 + 2147483647 + + + _popUpItemAction: + + + + + 3 + YES + YES + 1 + + + + + 256 + {{18, 206}, {214, 18}} + + YES + + 67239424 + 0 + Word weighting + + + 1211912703 + 2 + + NSSwitch + + + + 200 + 25 + + + + + 256 + {{18, 166}, {214, 18}} + + YES + + 67239424 + 0 + Can mix file kind + + + 1211912703 + 2 + + + + 200 + 25 + + + + + 256 + {{304, 252}, {31, 14}} + + YES + + 67239424 + -1874853888 + + + + + YES + + YES + allowsFloats + attributedStringForZero + decimalSeparator + formatterBehavior + groupingSeparator + negativeFormat + positiveFormat + usesGroupingSeparator + + + YES + + + 0 + + YES + + + YES + + + + . + + , + -0 + 0 + + + + 0 + -0 + + + + + + + + NaN + + + + 0 + 0 + YES + NO + 1 + AAAAAAAAAAAAAAAAAAAAAA + + + + . + , + NO + YES + YES + + + + + + + + + 256 + {{190, 12}, {148, 32}} + + YES + + 67239424 + 134217728 + Reset to Defaults + + + -2038284033 + 1 + + + + + + 200 + 25 + + + + + 256 + {{18, 186}, {214, 18}} + + YES + + 67239424 + 0 + Match similar words + + + 1211912703 + 2 + + + + 200 + 25 + + + + + 292 + {{20, 67}, {85, 13}} + + YES + + 67239424 + 272629760 + Copy and Move: + + + + + + + + + 292 + {{110, 56}, {216, 26}} + + YES + + -2076049856 + 2048 + + + 109199615 + 1 + + + + + + 400 + 75 + + + Right in destination + + 1048576 + 2147483647 + 1 + + + _popUpItemAction: + + + YES + + + OtherViews + + + YES + + + + Recreate relative path + + 1048576 + 2147483647 + + + _popUpItemAction: + + + + + Recreate absolute path + + 1048576 + 2147483647 + + + _popUpItemAction: + + + + + 3 + YES + YES + 1 + + + + + 256 + {{18, 86}, {283, 18}} + + YES + + 67239424 + 0 + Check for update on startup + + + 1211912703 + 2 + + + + 200 + 25 + + + + + 256 + {{18, 146}, {228, 18}} + + YES + + 67239424 + 0 + Use regular expressions when filtering + + + 1211912703 + 2 + + + + 200 + 25 + + + + + 256 + {{18, 126}, {242, 18}} + + YES + + 67239424 + 0 + Remove empty folders on delete or move + + + 1211912703 + 2 + + + + 200 + 25 + + + + + 256 + {{18, 106}, {152, 18}} + + YES + + 67239424 + 0 + Ignore files smaller than + + + 1211912703 + 2 + + + + 200 + 25 + + + + + 268 + {{176, 104}, {59, 22}} + + YES + + -1804468671 + -1874852864 + + + + + YES + + YES + allowsFloats + formatterBehavior + locale + maximumFractionDigits + numberStyle + positiveFormat + roundingIncrement + usesGroupingSeparator + + + YES + + + + + + + + #0 + + + + + #0 + #0 + + + + + + NaN + + + + + + 3 + YES + YES + YES + + . + , + NO + NO + NO + + + YES + + 6 + System + textBackgroundColor + + 3 + MQA + + + + 6 + System + textColor + + + + + + + 292 + {{243, 106}, {23, 17}} + + YES + + 67239424 + 272629760 + KB + + + + + + + + {352, 326} + + + {{0, 0}, {1440, 878}} + {213, 129} + {1.79769e+308, 1.79769e+308} + + + + + YES + + + revertToInitialValues: + + + + 101 + + + + value: values.matchSimilarWords + + + + + + value: values.matchSimilarWords + value + values.matchSimilarWords + 2 + + + 102 + + + + value: values.minMatchPercentage + + + + + + value: values.minMatchPercentage + value + values.minMatchPercentage + 2 + + + 103 + + + + enabled: values.scanType + + + + + + enabled: values.scanType + enabled + values.scanType + + NSValueTransformerName + vtScanTypeIsNotContent + + 2 + + + 104 + + + + enabled: values.scanType + + + + + + enabled: values.scanType + enabled + values.scanType + + NSValueTransformerName + vtScanTypeIsNotContent + + 2 + + + 105 + + + + nextKeyView + + + + 106 + + + + value: values.minMatchPercentage + + + + + + value: values.minMatchPercentage + value + values.minMatchPercentage + 2 + + + 107 + + + + value: values.wordWeighting + + + + + + value: values.wordWeighting + value + values.wordWeighting + 2 + + + 108 + + + + value: values.mixFileKind + + + + + + value: values.mixFileKind + value + values.mixFileKind + 2 + + + 109 + + + + value: values.ignoreSmallFiles + + + + + + value: values.ignoreSmallFiles + value + values.ignoreSmallFiles + 2 + + + 110 + + + + value: values.SUCheckAtStartup + + + + + + value: values.SUCheckAtStartup + value + values.SUCheckAtStartup + 2 + + + 111 + + + + value: values.smallFileThreshold + + + + + + value: values.smallFileThreshold + value + values.smallFileThreshold + 2 + + + 112 + + + + value: values.removeEmptyFolders + + + + + + value: values.removeEmptyFolders + value + values.removeEmptyFolders + 2 + + + 113 + + + + value: values.useRegexpFilter + + + + + + value: values.useRegexpFilter + value + values.useRegexpFilter + 2 + + + 114 + + + + selectedIndex: values.scanType + + + + + + selectedIndex: values.scanType + selectedIndex + values.scanType + 2 + + + 115 + + + + selectedIndex: values.recreatePathType + + + + + + selectedIndex: values.recreatePathType + selectedIndex + values.recreatePathType + 2 + + + 116 + + + + enabled: values.scanType + + + + + + enabled: values.scanType + enabled + values.scanType + + NSValueTransformerName + vtScanTypeIsNotContent + + 2 + + + 117 + + + + window + + + + 118 + + + + + YES + + 0 + + + + + + -2 + + + File's Owner + + + -1 + + + First Responder + + + -3 + + + Application + + + 51 + + + Shared Defaults + + + 52 + + + YES + + + + preferences + + + 53 + + + YES + + + + + + + + + + + + + + + + + + + + + + + + 54 + + + YES + + + + + + 55 + + + YES + + + + + + 56 + + + YES + + + + + + 57 + + + YES + + + + + + 58 + + + YES + + + + + + 59 + + + YES + + + + + + 60 + + + YES + + + + + + 61 + + + YES + + + + + + 62 + + + YES + + + + + + 63 + + + YES + + + + + + 64 + + + YES + + + + + + 65 + + + YES + + + + + + 66 + + + YES + + + + + + 67 + + + YES + + + + + + 68 + + + YES + + + + + + 69 + + + YES + + + + + + 70 + + + YES + + + + + + 71 + + + YES + + + + + + 72 + + + YES + + + + + + 73 + + + + + 74 + + + + + 75 + + + + + 76 + + + + + 77 + + + + + 78 + + + YES + + + + + + 79 + + + YES + + + + + + + 80 + + + + + 81 + + + + + 82 + + + + + 83 + + + + + 84 + + + YES + + + + + + 85 + + + + + 86 + + + + + 87 + + + + + 88 + + + + + 89 + + + YES + + + + + + 90 + + + YES + + + + + + + + 91 + + + + + 92 + + + + + 93 + + + + + 94 + + + + + 95 + + + + + 96 + + + + + 97 + + + + + 98 + + + YES + + + + + + 99 + + + + + 100 + + + + + + + YES + + YES + -1.IBPluginDependency + -2.IBPluginDependency + -3.IBPluginDependency + 100.IBPluginDependency + 51.IBPluginDependency + 51.ImportedFromIB2 + 52.IBEditorWindowLastContentRect + 52.IBPluginDependency + 52.IBWindowTemplateEditedContentRect + 52.ImportedFromIB2 + 52.windowTemplate.hasMinSize + 52.windowTemplate.minSize + 53.IBPluginDependency + 53.ImportedFromIB2 + 54.IBPluginDependency + 54.ImportedFromIB2 + 55.IBPluginDependency + 55.ImportedFromIB2 + 56.IBPluginDependency + 56.ImportedFromIB2 + 57.IBPluginDependency + 57.ImportedFromIB2 + 58.IBPluginDependency + 58.ImportedFromIB2 + 59.IBPluginDependency + 59.ImportedFromIB2 + 60.IBPluginDependency + 60.ImportedFromIB2 + 61.IBPluginDependency + 61.ImportedFromIB2 + 62.IBPluginDependency + 62.ImportedFromIB2 + 63.IBPluginDependency + 63.ImportedFromIB2 + 64.IBPluginDependency + 64.ImportedFromIB2 + 65.IBPluginDependency + 65.ImportedFromIB2 + 66.IBPluginDependency + 66.ImportedFromIB2 + 67.IBPluginDependency + 67.ImportedFromIB2 + 68.IBPluginDependency + 68.ImportedFromIB2 + 69.IBPluginDependency + 69.ImportedFromIB2 + 70.IBPluginDependency + 70.ImportedFromIB2 + 71.IBPluginDependency + 71.ImportedFromIB2 + 72.IBPluginDependency + 72.ImportedFromIB2 + 73.IBPluginDependency + 74.IBPluginDependency + 75.IBPluginDependency + 76.IBPluginDependency + 77.IBPluginDependency + 78.IBPluginDependency + 79.IBPluginDependency + 79.ImportedFromIB2 + 80.IBPluginDependency + 80.ImportedFromIB2 + 81.IBPluginDependency + 81.ImportedFromIB2 + 82.IBPluginDependency + 83.IBPluginDependency + 84.IBPluginDependency + 85.IBPluginDependency + 85.ImportedFromIB2 + 86.IBPluginDependency + 87.IBPluginDependency + 88.IBPluginDependency + 89.IBPluginDependency + 90.IBPluginDependency + 90.ImportedFromIB2 + 91.IBPluginDependency + 91.ImportedFromIB2 + 92.IBPluginDependency + 92.ImportedFromIB2 + 93.IBPluginDependency + 93.ImportedFromIB2 + 94.IBPluginDependency + 95.IBPluginDependency + 96.IBPluginDependency + 97.IBPluginDependency + 98.IBPluginDependency + 99.IBNumberFormatterLocalizesFormatMetadataKey + 99.IBPluginDependency + 99.ImportedFromIB2 + + + YES + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + + {{88, 519}, {352, 326}} + com.apple.InterfaceBuilder.CocoaPlugin + {{88, 519}, {352, 326}} + + + {213, 107} + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + + + + YES + + + YES + + + + + YES + + + YES + + + + 118 + + + 0 + + com.apple.InterfaceBuilder.CocoaPlugin.InterfaceBuilder3 + + + YES + + 3 + + From 80a5290bc8a2bd6c5bcf56124ec2f0031c98a44e Mon Sep 17 00:00:00 2001 From: hsoft Date: Fri, 30 Oct 2009 13:26:39 +0000 Subject: [PATCH 225/275] Moved dgse's MainMenu.xib to dgbase --HG-- rename : se/cocoa/xib/MainMenu.xib => base/cocoa/xib/MainMenu.xib extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40231 --- {se => base}/cocoa/xib/MainMenu.xib | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {se => base}/cocoa/xib/MainMenu.xib (100%) diff --git a/se/cocoa/xib/MainMenu.xib b/base/cocoa/xib/MainMenu.xib similarity index 100% rename from se/cocoa/xib/MainMenu.xib rename to base/cocoa/xib/MainMenu.xib From 6392d08584ec3ce5ccf693f286766ebbfbf5871d Mon Sep 17 00:00:00 2001 From: hsoft Date: Fri, 30 Oct 2009 13:27:55 +0000 Subject: [PATCH 226/275] dgse cocoa: Changed the MainMenu reference to the dgbase one. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40232 --- se/cocoa/dupeguru.xcodeproj/project.pbxproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/se/cocoa/dupeguru.xcodeproj/project.pbxproj b/se/cocoa/dupeguru.xcodeproj/project.pbxproj index a30c8184..9774ee3d 100644 --- a/se/cocoa/dupeguru.xcodeproj/project.pbxproj +++ b/se/cocoa/dupeguru.xcodeproj/project.pbxproj @@ -13,6 +13,7 @@ CE381C9609914ACE003581CE /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = CE381C9409914ACE003581CE /* AppDelegate.m */; }; CE381C9C09914ADF003581CE /* ResultWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = CE381C9A09914ADF003581CE /* ResultWindow.m */; }; CE381D0509915304003581CE /* dg_cocoa.plugin in Resources */ = {isa = PBXBuildFile; fileRef = CE381CF509915304003581CE /* dg_cocoa.plugin */; }; + CE3A46FA109B212E002ABFD5 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = CE3A46F9109B212E002ABFD5 /* MainMenu.xib */; }; CE45579B0AE3BC2B005A9546 /* Sparkle.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE45579A0AE3BC2B005A9546 /* Sparkle.framework */; }; CE4557B40AE3BC50005A9546 /* Sparkle.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = CE45579A0AE3BC2B005A9546 /* Sparkle.framework */; }; CE68EE6809ABC48000971085 /* DirectoryPanel.m in Sources */ = {isa = PBXBuildFile; fileRef = CE68EE6609ABC48000971085 /* DirectoryPanel.m */; }; @@ -25,7 +26,6 @@ CEDD92DB0FDD01640031C7B7 /* NSCharacterSet_Extensions.m in Sources */ = {isa = PBXBuildFile; fileRef = CEDD92D90FDD01640031C7B7 /* NSCharacterSet_Extensions.m */; }; CEE7EA130FE675C80004E467 /* DetailsPanel.m in Sources */ = {isa = PBXBuildFile; fileRef = CEE7EA120FE675C80004E467 /* DetailsPanel.m */; }; CEEB135209C837A2004D2330 /* dupeguru.icns in Resources */ = {isa = PBXBuildFile; fileRef = CEEB135109C837A2004D2330 /* dupeguru.icns */; }; - CEEFC0EF10944EDD001F3A39 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = CEEFC0EE10944EDD001F3A39 /* MainMenu.xib */; }; CEEFC0F810945D9F001F3A39 /* DirectoryPanel.xib in Resources */ = {isa = PBXBuildFile; fileRef = CEEFC0F710945D9F001F3A39 /* DirectoryPanel.xib */; }; CEEFC0FB10945E37001F3A39 /* DetailsPanel.xib in Resources */ = {isa = PBXBuildFile; fileRef = CEEFC0FA10945E37001F3A39 /* DetailsPanel.xib */; }; CEFC294609C89E3D00D9F998 /* folder32.png in Resources */ = {isa = PBXBuildFile; fileRef = CEFC294509C89E3D00D9F998 /* folder32.png */; }; @@ -77,6 +77,7 @@ CE381C9A09914ADF003581CE /* ResultWindow.m */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.objc; path = ResultWindow.m; sourceTree = SOURCE_ROOT; }; CE381C9B09914ADF003581CE /* ResultWindow.h */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.h; path = ResultWindow.h; sourceTree = SOURCE_ROOT; }; CE381CF509915304003581CE /* dg_cocoa.plugin */ = {isa = PBXFileReference; lastKnownFileType = folder; name = dg_cocoa.plugin; path = py/dist/dg_cocoa.plugin; sourceTree = SOURCE_ROOT; }; + CE3A46F9109B212E002ABFD5 /* MainMenu.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = MainMenu.xib; path = dgbase/xib/MainMenu.xib; sourceTree = ""; }; CE45579A0AE3BC2B005A9546 /* Sparkle.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Sparkle.framework; path = /Library/Frameworks/Sparkle.framework; sourceTree = ""; }; CE68EE6509ABC48000971085 /* DirectoryPanel.h */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.h; path = DirectoryPanel.h; sourceTree = SOURCE_ROOT; }; CE68EE6609ABC48000971085 /* DirectoryPanel.m */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.objc; path = DirectoryPanel.m; sourceTree = SOURCE_ROOT; }; @@ -92,7 +93,6 @@ CEE7EA110FE675C80004E467 /* DetailsPanel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = DetailsPanel.h; path = dgbase/DetailsPanel.h; sourceTree = SOURCE_ROOT; }; CEE7EA120FE675C80004E467 /* DetailsPanel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = DetailsPanel.m; path = dgbase/DetailsPanel.m; sourceTree = SOURCE_ROOT; }; CEEB135109C837A2004D2330 /* dupeguru.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; path = dupeguru.icns; sourceTree = ""; }; - CEEFC0EE10944EDD001F3A39 /* MainMenu.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = MainMenu.xib; path = xib/MainMenu.xib; sourceTree = ""; }; CEEFC0F710945D9F001F3A39 /* DirectoryPanel.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = DirectoryPanel.xib; path = dgbase/xib/DirectoryPanel.xib; sourceTree = ""; }; CEEFC0FA10945E37001F3A39 /* DetailsPanel.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = DetailsPanel.xib; path = dgbase/xib/DetailsPanel.xib; sourceTree = ""; }; CEFC294509C89E3D00D9F998 /* folder32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = folder32.png; path = images/folder32.png; sourceTree = SOURCE_ROOT; }; @@ -241,8 +241,8 @@ CEEFC0CA10943849001F3A39 /* xib */ = { isa = PBXGroup; children = ( + CE3A46F9109B212E002ABFD5 /* MainMenu.xib */, CEAC6810109B0B7E00B43C85 /* Preferences.xib */, - CEEFC0EE10944EDD001F3A39 /* MainMenu.xib */, CEEFC0F710945D9F001F3A39 /* DirectoryPanel.xib */, CEEFC0FA10945E37001F3A39 /* DetailsPanel.xib */, ); @@ -364,10 +364,10 @@ CEFC7FAE0FC9518A00CD5728 /* progress.nib in Resources */, CEFC7FAF0FC9518A00CD5728 /* registration.nib in Resources */, CE6E0DFE1054E9EF008D9390 /* dsa_pub.pem in Resources */, - CEEFC0EF10944EDD001F3A39 /* MainMenu.xib in Resources */, CEEFC0F810945D9F001F3A39 /* DirectoryPanel.xib in Resources */, CEEFC0FB10945E37001F3A39 /* DetailsPanel.xib in Resources */, CEAC6811109B0B7E00B43C85 /* Preferences.xib in Resources */, + CE3A46FA109B212E002ABFD5 /* MainMenu.xib in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; From 06859fe9cd05dd366cffdbe09e88c8c95d1e053d Mon Sep 17 00:00:00 2001 From: hsoft Date: Fri, 30 Oct 2009 14:40:17 +0000 Subject: [PATCH 227/275] dgme cocoa: extracted the pref pane from MainMenu.xib into Preferences.xib, Removed code duplicated with dgbase. Pushed down AppDelegate.applicationDidFinishLaunching: to dgbase. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40233 --- base/cocoa/AppDelegate.h | 2 + base/cocoa/AppDelegate.m | 22 +- base/cocoa/ResultWindow.h | 1 + base/cocoa/ResultWindow.m | 5 + base/cocoa/xib/MainMenu.xib | 163 +- me/cocoa/AppDelegate.h | 8 +- me/cocoa/AppDelegate.m | 58 - me/cocoa/ResultWindow.h | 10 - me/cocoa/ResultWindow.m | 83 +- me/cocoa/dupeguru.xcodeproj/project.pbxproj | 20 +- me/cocoa/py/gen.py | 1 + me/cocoa/xib/MainMenu.xib | 6951 ------------------- me/cocoa/xib/Preferences.xib | 2611 +++++++ 13 files changed, 2667 insertions(+), 7268 deletions(-) delete mode 100644 me/cocoa/xib/MainMenu.xib create mode 100644 me/cocoa/xib/Preferences.xib diff --git a/base/cocoa/AppDelegate.h b/base/cocoa/AppDelegate.h index 64fd6325..b24b044d 100644 --- a/base/cocoa/AppDelegate.h +++ b/base/cocoa/AppDelegate.h @@ -9,12 +9,14 @@ http://www.hardcoded.net/licenses/hs_license #import #import "RecentDirectories.h" #import "PyDupeGuru.h" +#import "ResultWindow.h" @interface AppDelegateBase : NSObject { IBOutlet PyDupeGuruBase *py; IBOutlet RecentDirectories *recentDirectories; IBOutlet NSMenuItem *unlockMenuItem; + IBOutlet ResultWindowBase *result; NSString *_appName; } diff --git a/base/cocoa/AppDelegate.m b/base/cocoa/AppDelegate.m index cc9f0647..092e95e4 100644 --- a/base/cocoa/AppDelegate.m +++ b/base/cocoa/AppDelegate.m @@ -27,7 +27,7 @@ http://www.hardcoded.net/licenses/hs_license RegistrationInterface *ri = [[RegistrationInterface alloc] initWithApp:[self py] name:_appName limitDescription:LIMIT_DESC]; if ([ri enterCode] == NSOKButton) { - NSString *menuTitle = [NSString stringWithFormat:@"Thanks for buying %@",_appName]; + NSString *menuTitle = [NSString stringWithFormat:@"Thanks for buying %@!",_appName]; [unlockMenuItem setTitle:menuTitle]; } [ri release]; @@ -35,4 +35,24 @@ http://www.hardcoded.net/licenses/hs_license - (PyDupeGuruBase *)py { return py; } - (RecentDirectories *)recentDirectories { return recentDirectories; } + +/* Delegate */ +- (void)applicationDidFinishLaunching:(NSNotification *)aNotification +{ + [[ProgressController mainProgressController] setWorker:py]; + NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; + //Restore Columns + NSArray *columnsOrder = [ud arrayForKey:@"columnsOrder"]; + NSDictionary *columnsWidth = [ud dictionaryForKey:@"columnsWidth"]; + if ([columnsOrder count]) + [result restoreColumnsPosition:columnsOrder widths:columnsWidth]; + else + [result resetColumnsToDefault:nil]; + //Reg stuff + if ([RegistrationInterface showNagWithApp:[self py] name:_appName limitDescription:LIMIT_DESC]) + [unlockMenuItem setTitle:[NSString stringWithFormat:@"Thanks for buying %@!",_appName]]; + //Restore results + [py loadIgnoreList]; + [py loadResults]; +} @end diff --git a/base/cocoa/ResultWindow.h b/base/cocoa/ResultWindow.h index ffd28230..ae00a2b8 100644 --- a/base/cocoa/ResultWindow.h +++ b/base/cocoa/ResultWindow.h @@ -52,6 +52,7 @@ http://www.hardcoded.net/licenses/hs_license - (IBAction)expandAll:(id)sender; - (IBAction)exportToXHTML:(id)sender; - (IBAction)moveMarked:(id)sender; +- (IBAction)resetColumnsToDefault:(id)sender; - (IBAction)showPreferencesPanel:(id)sender; - (IBAction)switchSelected:(id)sender; - (IBAction)toggleColumn:(id)sender; diff --git a/base/cocoa/ResultWindow.m b/base/cocoa/ResultWindow.m index e55c9976..d9ee413c 100644 --- a/base/cocoa/ResultWindow.m +++ b/base/cocoa/ResultWindow.m @@ -299,6 +299,11 @@ http://www.hardcoded.net/licenses/hs_license } } +- (IBAction)resetColumnsToDefault:(id)sender +{ + // Virtual +} + - (IBAction)showPreferencesPanel:(id)sender { [preferencesPanel showWindow:sender]; diff --git a/base/cocoa/xib/MainMenu.xib b/base/cocoa/xib/MainMenu.xib index 0a6b2538..cd12d2b7 100644 --- a/base/cocoa/xib/MainMenu.xib +++ b/base/cocoa/xib/MainMenu.xib @@ -11,8 +11,8 @@ 740 - + com.apple.InterfaceBuilder.CocoaPlugin @@ -671,7 +671,7 @@ 2048 - + 6 System headerColor @@ -747,96 +747,6 @@ compare: - - 1 - 120 - 10 - 1000 - - 75628096 - 2048 - Directory - - - - - - 337772096 - 2048 - - - - - - 2 - YES - - - 1 - YES - compare: - - - - 2 - 63 - 10 - 1000 - - 75628096 - 2048 - Size (KB) - - - - - - 337772096 - 67110912 - - - - - - 2 - YES - - - 2 - YES - compare: - - - - 6 - 59.9580078125 - 46.9580078125 - 1000 - - 75628096 - 2048 - Match % - - - - - - 337772096 - 2048 - - - - - - 2 - YES - - - 6 - YES - compare: - - 3 2 @@ -881,12 +791,12 @@ -2147483392 - {{1, -30}, {500, 15}} + {{1, 319}, {515, 15}} 1 _doScroller: - 0.99806201457977295 + 0.85406301824212272 @@ -2311,10 +2221,7 @@ - - - @@ -2326,22 +2233,6 @@ - - 223 - - - - - - - - 233 - - - - - - 406 @@ -2355,14 +2246,6 @@ - - 931 - - - - - - 291 @@ -2862,21 +2745,6 @@ - - 1141 - - - - - 1142 - - - - - 1143 - - - 1144 @@ -3149,12 +3017,6 @@ com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin @@ -3213,12 +3075,8 @@ com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin {{140, 768}, {481, 20}} @@ -3337,8 +3195,6 @@ com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin @@ -3525,14 +3381,6 @@ dgbase/PyDupeGuru.h - - PyRegistrable - NSObject - - IBProjectSource - cocoalib/PyRegistrable.h - - RecentDirectories NSObject @@ -3571,13 +3419,13 @@ id id id + id id id id id id id - id id id @@ -3635,6 +3483,7 @@ id id id + id id diff --git a/me/cocoa/AppDelegate.h b/me/cocoa/AppDelegate.h index 3b5388cf..8bdeac69 100644 --- a/me/cocoa/AppDelegate.h +++ b/me/cocoa/AppDelegate.h @@ -13,17 +13,11 @@ http://www.hardcoded.net/licenses/hs_license #import "PyDupeGuru.h" @interface AppDelegate : AppDelegateBase -{ - IBOutlet NSButton *presetsButton; - IBOutlet NSPopUpButton *presetsPopup; - IBOutlet ResultWindow *result; - +{ DirectoryPanel *_directoryPanel; } - (IBAction)openWebsite:(id)sender; -- (IBAction)popupPresets:(id)sender; - (IBAction)toggleDirectories:(id)sender; -- (IBAction)usePreset:(id)sender; - (DirectoryPanel *)directoryPanel; - (PyDupeGuru *)py; diff --git a/me/cocoa/AppDelegate.m b/me/cocoa/AppDelegate.m index db74b01f..894f0110 100644 --- a/me/cocoa/AppDelegate.m +++ b/me/cocoa/AppDelegate.m @@ -60,52 +60,11 @@ http://www.hardcoded.net/licenses/hs_license [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"http://www.hardcoded.net/dupeguru_me"]]; } -- (IBAction)popupPresets:(id)sender -{ - [presetsPopup selectItem: nil]; - [[presetsPopup cell] performClickWithFrame:[sender frame] inView:[sender superview]]; -} - - (IBAction)toggleDirectories:(id)sender { [[self directoryPanel] toggleVisible:sender]; } -- (IBAction)usePreset:(id)sender -{ - NSUserDefaultsController *ud = [NSUserDefaultsController sharedUserDefaultsController]; - [ud revertToInitialValues:nil]; - NSUserDefaults *d = [ud defaults]; - switch ([sender tag]) - { - case 0: - { - [d setInteger:5 forKey:@"scanType"]; - break; - } - //case 1 is defaults - case 2: - { - [d setInteger:2 forKey:@"scanType"]; - break; - } - case 3: - { - [d setInteger:0 forKey:@"scanType"]; - [d setInteger:50 forKey:@"minMatchPercentage"]; - break; - } - case 4: - { - [d setInteger:0 forKey:@"scanType"]; - [d setInteger:50 forKey:@"minMatchPercentage"]; - [d setBool:YES forKey:@"matchSimilarWords"]; - [d setBool:YES forKey:@"wordWeighting"]; - break; - } - } -} - - (DirectoryPanel *)directoryPanel { if (!_directoryPanel) @@ -115,23 +74,6 @@ http://www.hardcoded.net/licenses/hs_license - (PyDupeGuru *)py { return (PyDupeGuru *)py; } //Delegate -- (void)applicationDidFinishLaunching:(NSNotification *)aNotification -{ - [[ProgressController mainProgressController] setWorker:py]; - NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; - //Restore Columns - NSArray *columnsOrder = [ud arrayForKey:@"columnsOrder"]; - NSDictionary *columnsWidth = [ud dictionaryForKey:@"columnsWidth"]; - if ([columnsOrder count]) - [result restoreColumnsPosition:columnsOrder widths:columnsWidth]; - //Reg stuff - if ([RegistrationInterface showNagWithApp:[self py] name:APPNAME limitDescription:LIMIT_DESC]) - [unlockMenuItem setTitle:@"Thanks for buying dupeGuru ME!"]; - //Restore results - [py loadIgnoreList]; - [py loadResults]; -} - - (void)applicationWillBecomeActive:(NSNotification *)aNotification { if (![[result window] isVisible]) diff --git a/me/cocoa/ResultWindow.h b/me/cocoa/ResultWindow.h index cbdaaeae..30284c65 100644 --- a/me/cocoa/ResultWindow.h +++ b/me/cocoa/ResultWindow.h @@ -14,13 +14,10 @@ http://www.hardcoded.net/licenses/hs_license @interface ResultWindow : ResultWindowBase { - IBOutlet NSMenu *columnsMenu; IBOutlet NSSearchField *filterField; - IBOutlet NSWindow *preferencesPanel; NSString *_lastAction; DetailsPanel *_detailsPanel; - NSMutableArray *_resultColumns; NSMutableIndexSet *_deltaColumns; } - (IBAction)clearIgnoreList:(id)sender; @@ -37,15 +34,8 @@ http://www.hardcoded.net/licenses/hs_license - (IBAction)removeMarked:(id)sender; - (IBAction)removeSelected:(id)sender; - (IBAction)renameSelected:(id)sender; -- (IBAction)resetColumnsToDefault:(id)sender; - (IBAction)revealSelected:(id)sender; -- (IBAction)showPreferencesPanel:(id)sender; - (IBAction)startDuplicateScan:(id)sender; -- (IBAction)toggleColumn:(id)sender; - (IBAction)toggleDelta:(id)sender; - (IBAction)toggleDetailsPanel:(id)sender; - -- (NSTableColumn *)getColumnForIdentifier:(int)aIdentifier title:(NSString *)aTitle width:(int)aWidth refCol:(NSTableColumn *)aColumn; -- (void)initResultColumns; -- (void)restoreColumnsPosition:(NSArray *)aColumnsOrder widths:(NSDictionary *)aColumnsWidth; @end diff --git a/me/cocoa/ResultWindow.m b/me/cocoa/ResultWindow.m index 5aa0f34c..bafc7048 100644 --- a/me/cocoa/ResultWindow.m +++ b/me/cocoa/ResultWindow.m @@ -19,6 +19,7 @@ http://www.hardcoded.net/licenses/hs_license - (void)awakeFromNib { [super awakeFromNib]; + [[self window] setTitle:@"dupeGuru Music Edition"]; _detailsPanel = nil; _displayDelta = NO; _powerMode = NO; @@ -29,7 +30,6 @@ http://www.hardcoded.net/licenses/hs_license [py setDisplayDeltaValues:b2n(_displayDelta)]; [matches setTarget:self]; [matches setDoubleAction:@selector(openSelected:)]; - [self initResultColumns]; [self refreshStats]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(resultsMarkingChanged:) name:ResultsMarkingChangedNotification object:nil]; } @@ -168,11 +168,6 @@ http://www.hardcoded.net/licenses/hs_license [py revealSelected]; } -- (IBAction)showPreferencesPanel:(id)sender -{ - [preferencesPanel makeKeyAndOrderFront:sender]; -} - - (IBAction)startDuplicateScan:(id)sender { if ([matches numberOfRows] > 0) @@ -205,26 +200,6 @@ http://www.hardcoded.net/licenses/hs_license } } -- (IBAction)toggleColumn:(id)sender -{ - NSMenuItem *mi = sender; - NSString *colId = [NSString stringWithFormat:@"%d",[mi tag]]; - NSTableColumn *col = [matches tableColumnWithIdentifier:colId]; - if (col == nil) - { - //Add Column - col = [_resultColumns objectAtIndex:[mi tag]]; - [matches addTableColumn:col]; - [mi setState:NSOnState]; - } - else - { - //Remove column - [matches removeTableColumn:col]; - [mi setState:NSOffState]; - } -} - - (IBAction)toggleDelta:(id)sender { if ([deltaSwitch selectedSegment] == 1) @@ -245,28 +220,21 @@ http://www.hardcoded.net/licenses/hs_license } /* Public */ -- (NSTableColumn *)getColumnForIdentifier:(int)aIdentifier title:(NSString *)aTitle width:(int)aWidth refCol:(NSTableColumn *)aColumn -{ - NSNumber *n = [NSNumber numberWithInt:aIdentifier]; - NSTableColumn *col = [[NSTableColumn alloc] initWithIdentifier:[n stringValue]]; - [col setWidth:aWidth]; - [col setEditable:NO]; - [[col dataCell] setFont:[[aColumn dataCell] font]]; - [[col headerCell] setStringValue:aTitle]; - [col setResizingMask:NSTableColumnUserResizingMask]; - [col setSortDescriptorPrototype:[[NSSortDescriptor alloc] initWithKey:[n stringValue] ascending:YES]]; - return col; -} - - (void)initResultColumns { NSTableColumn *refCol = [matches tableColumnWithIdentifier:@"0"]; _resultColumns = [[NSMutableArray alloc] init]; [_resultColumns addObject:[matches tableColumnWithIdentifier:@"0"]]; // File Name [_resultColumns addObject:[self getColumnForIdentifier:1 title:@"Directory" width:120 refCol:refCol]]; - [_resultColumns addObject:[matches tableColumnWithIdentifier:@"2"]]; // Size - [_resultColumns addObject:[matches tableColumnWithIdentifier:@"3"]]; // Time - [_resultColumns addObject:[matches tableColumnWithIdentifier:@"4"]]; // Bitrate + NSTableColumn *sizeCol = [self getColumnForIdentifier:2 title:@"Size (MB)" width:63 refCol:refCol]; + [[sizeCol dataCell] setAlignment:NSRightTextAlignment]; + [_resultColumns addObject:sizeCol]; + NSTableColumn *timeCol = [self getColumnForIdentifier:3 title:@"Time" width:50 refCol:refCol]; + [[timeCol dataCell] setAlignment:NSRightTextAlignment]; + [_resultColumns addObject:timeCol]; + NSTableColumn *brCol = [self getColumnForIdentifier:4 title:@"Bitrate" width:50 refCol:refCol]; + [[brCol dataCell] setAlignment:NSRightTextAlignment]; + [_resultColumns addObject:brCol]; [_resultColumns addObject:[self getColumnForIdentifier:5 title:@"Sample Rate" width:60 refCol:refCol]]; [_resultColumns addObject:[self getColumnForIdentifier:6 title:@"Kind" width:40 refCol:refCol]]; [_resultColumns addObject:[self getColumnForIdentifier:7 title:@"Creation" width:120 refCol:refCol]]; @@ -278,40 +246,11 @@ http://www.hardcoded.net/licenses/hs_license [_resultColumns addObject:[self getColumnForIdentifier:13 title:@"Year" width:40 refCol:refCol]]; [_resultColumns addObject:[self getColumnForIdentifier:14 title:@"Track Number" width:40 refCol:refCol]]; [_resultColumns addObject:[self getColumnForIdentifier:15 title:@"Comment" width:120 refCol:refCol]]; - [_resultColumns addObject:[matches tableColumnWithIdentifier:@"16"]]; // Match % + [_resultColumns addObject:[self getColumnForIdentifier:16 title:@"Match %" width:57 refCol:refCol]]; [_resultColumns addObject:[self getColumnForIdentifier:17 title:@"Words Used" width:120 refCol:refCol]]; [_resultColumns addObject:[self getColumnForIdentifier:18 title:@"Dupe Count" width:80 refCol:refCol]]; } -- (void)restoreColumnsPosition:(NSArray *)aColumnsOrder widths:(NSDictionary *)aColumnsWidth -{ - NSTableColumn *col; - NSString *colId; - NSNumber *width; - NSMenuItem *mi; - //Remove all columns - NSEnumerator *e = [[columnsMenu itemArray] objectEnumerator]; - while (mi = [e nextObject]) - { - if ([mi state] == NSOnState) - [self toggleColumn:mi]; - } - //Add columns and set widths - e = [aColumnsOrder objectEnumerator]; - while (colId = [e nextObject]) - { - if (![colId isEqual:@"mark"]) - { - col = [_resultColumns objectAtIndex:[colId intValue]]; - width = [aColumnsWidth objectForKey:[col identifier]]; - mi = [columnsMenu itemWithTag:[colId intValue]]; - if (width) - [col setWidth:[width floatValue]]; - [self toggleColumn:mi]; - } - } -} - /* Delegate */ - (void)outlineView:(NSOutlineView *)outlineView willDisplayCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn item:(id)item { diff --git a/me/cocoa/dupeguru.xcodeproj/project.pbxproj b/me/cocoa/dupeguru.xcodeproj/project.pbxproj index 940f8094..5b2d5e46 100644 --- a/me/cocoa/dupeguru.xcodeproj/project.pbxproj +++ b/me/cocoa/dupeguru.xcodeproj/project.pbxproj @@ -29,7 +29,6 @@ CE381D0509915304003581CE /* dg_cocoa.plugin in Resources */ = {isa = PBXBuildFile; fileRef = CE381CF509915304003581CE /* dg_cocoa.plugin */; }; CE3FBDD31094637800B72D77 /* DetailsPanel.xib in Resources */ = {isa = PBXBuildFile; fileRef = CE3FBDD11094637800B72D77 /* DetailsPanel.xib */; }; CE3FBDD41094637800B72D77 /* DirectoryPanel.xib in Resources */ = {isa = PBXBuildFile; fileRef = CE3FBDD21094637800B72D77 /* DirectoryPanel.xib */; }; - CE3FBDD7109463AB00B72D77 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = CE3FBDD6109463AB00B72D77 /* MainMenu.xib */; }; CE49DEF60FDFEB810098617B /* BRSingleLineFormatter.m in Sources */ = {isa = PBXBuildFile; fileRef = CE49DEF30FDFEB810098617B /* BRSingleLineFormatter.m */; }; CE49DEF70FDFEB810098617B /* NSCharacterSet_Extensions.m in Sources */ = {isa = PBXBuildFile; fileRef = CE49DEF50FDFEB810098617B /* NSCharacterSet_Extensions.m */; }; CE515DF30FC6C12E00EC695D /* Dialogs.m in Sources */ = {isa = PBXBuildFile; fileRef = CE515DE10FC6C12E00EC695D /* Dialogs.m */; }; @@ -51,10 +50,10 @@ CE68EE6809ABC48000971085 /* DirectoryPanel.m in Sources */ = {isa = PBXBuildFile; fileRef = CE68EE6609ABC48000971085 /* DirectoryPanel.m */; }; CE6E0E9F1054EB97008D9390 /* dsa_pub.pem in Resources */ = {isa = PBXBuildFile; fileRef = CE6E0E9E1054EB97008D9390 /* dsa_pub.pem */; }; CE848A1909DD85810004CB44 /* Consts.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = CE848A1809DD85810004CB44 /* Consts.h */; }; - CEA7D2C50FDFED340037CD8C /* dgme_logo_32.png in Resources */ = {isa = PBXBuildFile; fileRef = CEA7D2C40FDFED340037CD8C /* dgme_logo_32.png */; }; + CE900AD2109B238600754048 /* Preferences.xib in Resources */ = {isa = PBXBuildFile; fileRef = CE900AD1109B238600754048 /* Preferences.xib */; }; + CE900AD7109B2A9B00754048 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = CE900AD6109B2A9B00754048 /* MainMenu.xib */; }; CECA899C09DB132E00A3D774 /* DetailsPanel.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = CECA899A09DB132E00A3D774 /* DetailsPanel.h */; }; CECA899D09DB132E00A3D774 /* DetailsPanel.m in Sources */ = {isa = PBXBuildFile; fileRef = CECA899B09DB132E00A3D774 /* DetailsPanel.m */; }; - CED2A6880A05102700AC4C3F /* power_marker32.png in Resources */ = {isa = PBXBuildFile; fileRef = CED2A6870A05102600AC4C3F /* power_marker32.png */; }; CEEB135209C837A2004D2330 /* dupeguru.icns in Resources */ = {isa = PBXBuildFile; fileRef = CEEB135109C837A2004D2330 /* dupeguru.icns */; }; CEFC294609C89E3D00D9F998 /* folder32.png in Resources */ = {isa = PBXBuildFile; fileRef = CEFC294509C89E3D00D9F998 /* folder32.png */; }; CEFC295509C89FF200D9F998 /* details32.png in Resources */ = {isa = PBXBuildFile; fileRef = CEFC295309C89FF200D9F998 /* details32.png */; }; @@ -93,7 +92,6 @@ CE381CF509915304003581CE /* dg_cocoa.plugin */ = {isa = PBXFileReference; lastKnownFileType = folder; name = dg_cocoa.plugin; path = py/dist/dg_cocoa.plugin; sourceTree = SOURCE_ROOT; }; CE3FBDD11094637800B72D77 /* DetailsPanel.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = DetailsPanel.xib; sourceTree = ""; }; CE3FBDD21094637800B72D77 /* DirectoryPanel.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = DirectoryPanel.xib; sourceTree = ""; }; - CE3FBDD6109463AB00B72D77 /* MainMenu.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = MainMenu.xib; path = ../../xib/MainMenu.xib; sourceTree = ""; }; CE49DEF20FDFEB810098617B /* BRSingleLineFormatter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BRSingleLineFormatter.h; path = cocoalib/brsinglelineformatter/BRSingleLineFormatter.h; sourceTree = SOURCE_ROOT; }; CE49DEF30FDFEB810098617B /* BRSingleLineFormatter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BRSingleLineFormatter.m; path = cocoalib/brsinglelineformatter/BRSingleLineFormatter.m; sourceTree = SOURCE_ROOT; }; CE49DEF40FDFEB810098617B /* NSCharacterSet_Extensions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = NSCharacterSet_Extensions.h; path = cocoalib/brsinglelineformatter/NSCharacterSet_Extensions.h; sourceTree = SOURCE_ROOT; }; @@ -134,10 +132,10 @@ CE68EE6609ABC48000971085 /* DirectoryPanel.m */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.objc; path = DirectoryPanel.m; sourceTree = SOURCE_ROOT; }; CE6E0E9E1054EB97008D9390 /* dsa_pub.pem */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = dsa_pub.pem; path = dgbase/dsa_pub.pem; sourceTree = ""; }; CE848A1809DD85810004CB44 /* Consts.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Consts.h; sourceTree = ""; }; - CEA7D2C40FDFED340037CD8C /* dgme_logo_32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = dgme_logo_32.png; path = images/dgme_logo_32.png; sourceTree = SOURCE_ROOT; }; + CE900AD1109B238600754048 /* Preferences.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Preferences.xib; path = ../../xib/Preferences.xib; sourceTree = ""; }; + CE900AD6109B2A9B00754048 /* MainMenu.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = MainMenu.xib; sourceTree = ""; }; CECA899A09DB132E00A3D774 /* DetailsPanel.h */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.h; path = DetailsPanel.h; sourceTree = ""; }; CECA899B09DB132E00A3D774 /* DetailsPanel.m */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.objc; path = DetailsPanel.m; sourceTree = ""; }; - CED2A6870A05102600AC4C3F /* power_marker32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = power_marker32.png; path = images/power_marker32.png; sourceTree = SOURCE_ROOT; }; CEEB135109C837A2004D2330 /* dupeguru.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; path = dupeguru.icns; sourceTree = ""; }; CEFC294509C89E3D00D9F998 /* folder32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = folder32.png; path = images/folder32.png; sourceTree = SOURCE_ROOT; }; CEFC295309C89FF200D9F998 /* details32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = details32.png; path = images/details32.png; sourceTree = SOURCE_ROOT; }; @@ -250,9 +248,10 @@ CE3FBDD01094637800B72D77 /* xib */ = { isa = PBXGroup; children = ( - CE3FBDD6109463AB00B72D77 /* MainMenu.xib */, + CE900AD6109B2A9B00754048 /* MainMenu.xib */, CE3FBDD11094637800B72D77 /* DetailsPanel.xib */, CE3FBDD21094637800B72D77 /* DirectoryPanel.xib */, + CE900AD1109B238600754048 /* Preferences.xib */, ); name = xib; path = dgbase/xib; @@ -320,8 +319,6 @@ CEFC294309C89E0000D9F998 /* images */ = { isa = PBXGroup; children = ( - CEA7D2C40FDFED340037CD8C /* dgme_logo_32.png */, - CED2A6870A05102600AC4C3F /* power_marker32.png */, CEFC295309C89FF200D9F998 /* details32.png */, CEFC295409C89FF200D9F998 /* preferences32.png */, CEFC294509C89E3D00D9F998 /* folder32.png */, @@ -380,15 +377,14 @@ CEFC294609C89E3D00D9F998 /* folder32.png in Resources */, CEFC295509C89FF200D9F998 /* details32.png in Resources */, CEFC295609C89FF200D9F998 /* preferences32.png in Resources */, - CED2A6880A05102700AC4C3F /* power_marker32.png in Resources */, CE515E020FC6C13E00EC695D /* ErrorReportWindow.xib in Resources */, CE515E030FC6C13E00EC695D /* progress.nib in Resources */, CE515E040FC6C13E00EC695D /* registration.nib in Resources */, - CEA7D2C50FDFED340037CD8C /* dgme_logo_32.png in Resources */, CE6E0E9F1054EB97008D9390 /* dsa_pub.pem in Resources */, CE3FBDD31094637800B72D77 /* DetailsPanel.xib in Resources */, CE3FBDD41094637800B72D77 /* DirectoryPanel.xib in Resources */, - CE3FBDD7109463AB00B72D77 /* MainMenu.xib in Resources */, + CE900AD2109B238600754048 /* Preferences.xib in Resources */, + CE900AD7109B2A9B00754048 /* MainMenu.xib in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/me/cocoa/py/gen.py b/me/cocoa/py/gen.py index d4991df3..f8bc07c4 100644 --- a/me/cocoa/py/gen.py +++ b/me/cocoa/py/gen.py @@ -16,4 +16,5 @@ if op.exists('build'): if op.exists('dist'): shutil.rmtree('dist') +os.environ['MACOSX_DEPLOYMENT_TARGET'] = '10.5' print_and_do('python -u setup.py py2app') \ No newline at end of file diff --git a/me/cocoa/xib/MainMenu.xib b/me/cocoa/xib/MainMenu.xib deleted file mode 100644 index 98557add..00000000 --- a/me/cocoa/xib/MainMenu.xib +++ /dev/null @@ -1,6951 +0,0 @@ - - - - 1050 - 10B504 - 740 - 1038.2 - 437.00 - - com.apple.InterfaceBuilder.CocoaPlugin - 740 - - - - - - - - com.apple.InterfaceBuilder.CocoaPlugin - - - - - NSApplication - - - FirstResponder - - - NSApplication - - - 15 - 2 - {{47, 310}, {557, 400}} - 1886912512 - dupeGuru Music Edition - NSWindow - - - B570361B-ACC8-474D-BCD9-EE4A4EF88C48 - - - YES - YES - YES - YES - 1 - 1 - - - - 00156499-01C0-42C8-8A93-179A727D7695 - - Filter - Filter - - - - 258 - {{0, 14}, {81, 22}} - YES - - 343014976 - 268436480 - - - LucidaGrande - 13 - 1044 - - Filter - - YES - 1 - - 6 - System - textBackgroundColor - - 3 - MQA - - - - 6 - System - controlTextColor - - 3 - MAA - - - - 130560 - 0 - search - - _searchFieldSearch: - - 138690815 - 0 - - 400 - 75 - - - 130560 - 0 - clear - - - cancel - - - - - _searchFieldCancel: - - 138690815 - 0 - - 400 - 75 - - 10 - YES - - - - - - {81, 22} - {9999, 22} - YES - YES - 0 - YES - 0 - - - - 2EE12929-476B-49AF-846C-E3C1498BA2CC - - Start Scanning - Start Scanning - - - - NSImage - dgme_logo_32 - - - - {0, 0} - {0, 0} - YES - YES - -1 - YES - 0 - - - - 3017E624-E1D3-4BAD-9B9E-B7FFD6C99F00 - - Delta Values - Delta Values - - - - 256 - {{4, 14}, {67, 24}} - YES - - 67239424 - 0 - - LucidaGrande - 11 - 3100 - - - - - 30 - Off - 2 - - - 30 - On - 1 - 2 - - - 1 - - - - - - {67, 24} - {67, 24} - YES - YES - 0 - YES - 0 - - - - 3B3D4F13-9057-4C55-9748-ACDC8A39368D - - Action - Action - - - - 256 - {{0, 14}, {58, 26}} - YES - - -2076049856 - 2048 - - - 109199615 - 1 - - - - - - 400 - 75 - - - YES - IA - - 1048576 - 2147483647 - 1 - - NSImage - NSActionTemplate - - - NSImage - NSMenuCheckmark - - - NSImage - NSMenuMixedState - - _popUpItemAction: - - - YES - - - OtherViews - - - - - - Send Marked to Trash - - 2147483647 - - - _popUpItemAction: - - - - - Move Marked to... - - 2147483647 - - - _popUpItemAction: - - - - - Copy Marked to... - - 2147483647 - - - _popUpItemAction: - - - - - Remove Marked from Results - - 2147483647 - - - _popUpItemAction: - - - - - YES - YES - - - 1048576 - 2147483647 - - - _popUpItemAction: - - - - - Remove Selected from Results - - 2147483647 - - - _popUpItemAction: - - - - - Add Selected to Ignore List - - 1048576 - 2147483647 - - - _popUpItemAction: - - - - - Make Selected Reference - - 2147483647 - - - _popUpItemAction: - - - - - YES - YES - - - 1048576 - 2147483647 - - - _popUpItemAction: - - - - - Open Selected with Default Application - - 2147483647 - - - _popUpItemAction: - - - - - Reveal Selected in Finder - - 2147483647 - - - _popUpItemAction: - - - - - Rename Selected - - 1048576 - 2147483647 - - - _popUpItemAction: - - - - - YES - 3 - YES - YES - 1 - - - - - - {58, 26} - {58, 26} - YES - YES - 0 - YES - 0 - - - - 3CF40535-D531-4EBF-97BC-44491B2D99D8 - - Details - Details - - - - NSImage - details32 - - - - {0, 0} - {0, 0} - YES - YES - -1 - YES - 0 - - - - 4D2CC17D-F076-4811-A7EE-A0E07D024C12 - - Directories - Directories - - - - NSImage - folder32 - - - - {0, 0} - {0, 0} - YES - YES - -1 - YES - 0 - - - - 69655571-C90A-48A6-BE96-C70BF9E7BE5B - - Preferences - Preferences - - - - NSImage - preferences32 - - - - {0, 0} - {0, 0} - YES - YES - -1 - YES - 0 - - - - DAD5AB78-1216-48A1-B87C-D451B8C2EBD1 - - Power Marker - Power Marker - - - - 256 - {{7, 14}, {67, 24}} - YES - - 67239424 - 0 - - - - - 30 - Off - 2 - - - 30 - On - 1 - 2 - - - 1 - - - - - - {67, 24} - {67, 24} - YES - YES - 0 - YES - 0 - - - NSToolbarFlexibleSpaceItem - - Flexible Space - - - - - - {1, 5} - {20000, 32} - YES - YES - -1 - YES - 0 - - YES - YES - - - 1048576 - 2147483647 - - - - - - NSToolbarSeparatorItem - - Separator - - - - - - {12, 5} - {12, 1000} - YES - YES - -1 - YES - 0 - - YES - YES - - - 1048576 - 2147483647 - - - - - - NSToolbarSpaceItem - - Space - - - - - - {32, 5} - {32, 32} - YES - YES - -1 - YES - 0 - - YES - YES - - - 1048576 - 2147483647 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {1.79769e+308, 1.79769e+308} - {340, 340} - - - 256 - - - - 274 - - - - 2304 - - - - 274 - {515, 317} - - YES - - - 256 - {515, 17} - - - - - - -2147483392 - {{-26, 0}, {16, 17}} - - - - - mark - 47 - 16 - 1000 - - 75628096 - 2048 - - - - 6 - System - headerColor - - - - 6 - System - headerTextColor - - - - - 67239424 - 131072 - - - LucidaGrande - 12 - 16 - - - 1211912703 - 2 - - NSSwitch - - - - 400 - 75 - - - - - 0 - 230 - 16 - 1000 - - 75628096 - 2048 - Name - - - 3 - MC4zMzMzMzI5OQA - - - - - 337772096 - 2048 - - - - 6 - System - controlBackgroundColor - - 3 - MC42NjY2NjY2NjY3AA - - - - - 2 - YES - - - 0 - YES - compare: - - - - 2 - 63 - 10 - 1000 - - 75628096 - 2048 - Size (MB) - - - - - - 337772096 - 67110912 - - - - - - 2 - YES - - - 2 - YES - compare: - - - - 3 - 50 - 10 - 1000 - - 75628096 - 2048 - Time - - - - - - 337772096 - 67110912 - - - - - - 2 - YES - - - 3 - YES - compare: - - - - 4 - 50 - 10 - 1000 - - 75628096 - 2048 - Bitrate - - - - - - 337772096 - 67110912 - - - - - - 2 - YES - - - 4 - YES - compare: - - - - 16 - 56.9580078125 - 46.9580078125 - 1000 - - 75628096 - 2048 - Match % - - - - - - 337772096 - 2048 - - - - - - 2 - YES - - - 16 - YES - compare: - - - - 3 - 2 - - - 6 - System - gridColor - - 3 - MC41AA - - - 14 - -893386752 - - - 2 - 0 - 15 - 0 - YES - 0 - - - {{1, 17}, {515, 317}} - - - - - 4 - - - - -2147483392 - {{-30, 17}, {15, 302}} - - - _doScroller: - 0.95268136262893677 - - - - -2147483392 - {{1, -30}, {515, 15}} - - 1 - - _doScroller: - 0.96986818313598633 - - - - 2304 - - - - {{1, 0}, {515, 17}} - - - - - 4 - - - - {{20, 45}, {517, 335}} - - - 562 - - - - - - QSAAAEEgAABBgAAAQYAAAA - - - - 290 - {{17, 20}, {523, 17}} - - YES - - 67239424 - 138412032 - Marked: 0 files, 0 B. Total: 0 files, 0 B. - - - - 6 - System - controlColor - - - - - - - {557, 400} - - - {{0, 0}, {1440, 878}} - {340, 418} - {1.79769e+308, 1.79769e+308} - MainWindow - - - MainMenu - - - - dupeGuru ME - - 1048576 - 2147483647 - - - submenuAction: - - dupeGuru ME - - - - About dupeGuru ME - - 2147483647 - - - - - - Unlock dupeGuru ME - - 1048576 - 2147483647 - - - - - - Check for update... - - 1048576 - 2147483647 - - - - - - YES - YES - - - 1048576 - 2147483647 - - - - - - Preferences... - , - 1048576 - 2147483647 - - - - - - YES - YES - - - 1048576 - 2147483647 - - - - - - Hide dupeGuru ME - h - 1048576 - 2147483647 - - - - - - Hide Others - h - 1572864 - 2147483647 - - - - - - Show All - - 1048576 - 2147483647 - - - - - - YES - YES - - - 1048576 - 2147483647 - - - - - - Quit dupeGuru ME - q - 1048576 - 2147483647 - - - - - _NSAppleMenu - - - - - Edit - - 1048576 - 2147483647 - - - submenuAction: - - - Edit - - - - - Mark All - a - 1048576 - 2147483647 - - - - - - Mark None - A - 1048576 - 2147483647 - - - - - - Invert Marking - a - 1572864 - 2147483647 - - - - - - Mark Selected - a - 1310720 - 2147483647 - - - - - - YES - YES - - - 1048576 - 2147483647 - - - - - - Cut - x - 1048576 - 2147483647 - - - - - - Copy - c - 1048576 - 2147483647 - - - - - - Paste - v - 1048576 - 2147483647 - - - - - - - - - Actions - - 1048576 - 2147483647 - - - submenuAction: - - Actions - - - - Start Duplicate Scan - s - 1048576 - 2147483647 - - - - - - Clear Ignore List - I - 1048576 - 2147483647 - - - - - - Export Results to XHTML - E - 1048576 - 2147483647 - - - - - - Remove Dead Tracks in iTunes - - 1048576 - 2147483647 - - - - - - YES - YES - - - 1048576 - 2147483647 - - - - - - Send Marked to Trash - t - 1048576 - 2147483647 - - - - - - Move Marked to... - m - 1048576 - 2147483647 - - - - - - Copy Marked to... - m - 1572864 - 2147483647 - - - - - - Remove Marked from Results - r - 1048576 - 2147483647 - - - - - - YES - YES - - - 1048576 - 2147483647 - - - - - - Remove Selected from Results - R - 1048576 - 2147483647 - - - - - - Add Selected to Ignore List - i - 1048576 - 2147483647 - - - - - - Make Selected Reference -  - 1048576 - 2147483647 - - - - - - YES - YES - - - 1048576 - 2147483647 - - - - - - Open Selected with Default Application - DQ - 1048576 - 2147483647 - - - - - - Reveal Selected in Finder - DQ - 1572864 - 2147483647 - - - - - - Rename Selected - Aw - 2147483647 - - - - - - - - - Columns - - 1048576 - 2147483647 - - - submenuAction: - - Columns - - - - File Name - - 1048576 - 2147483647 - 1 - - - - - - Directory - - 1048576 - 2147483647 - - - 1 - - - - Size - - 1048576 - 2147483647 - 1 - - - 2 - - - - Time - - 1048576 - 2147483647 - 1 - - - 3 - - - - Bitrate - - 1048576 - 2147483647 - 1 - - - 4 - - - - Sample Rate - - 1048576 - 2147483647 - - - 5 - - - - Kind - - 1048576 - 2147483647 - - - 6 - - - - Creation - - 1048576 - 2147483647 - - - 7 - - - - Modification - - 1048576 - 2147483647 - - - 8 - - - - Title - - 1048576 - 2147483647 - - - 9 - - - - Artist - - 1048576 - 2147483647 - - - 10 - - - - Album - - 1048576 - 2147483647 - - - 11 - - - - Genre - - 1048576 - 2147483647 - - - 12 - - - - Year - - 1048576 - 2147483647 - - - 13 - - - - Track Number - - 1048576 - 2147483647 - - - 14 - - - - Comment - - 1048576 - 2147483647 - - - 15 - - - - Match % - - 1048576 - 2147483647 - 1 - - - 16 - - - - Words Used - - 1048576 - 2147483647 - - - 17 - - - - Dupe Count - - 1048576 - 2147483647 - - - 18 - - - - YES - YES - IA - - 1048576 - 2147483647 - - - -1 - - - - Reset to Default - - 1048576 - 2147483647 - - - -1 - - - - - - - Mode - - 1048576 - 2147483647 - - - submenuAction: - - Mode - - - - Power Marker - 1 - 1048576 - 2147483647 - - - - - - Delta Values - 2 - 1048576 - 2147483647 - - - - - - - - - Window - - 1048576 - 2147483647 - - - submenuAction: - - Window - - - - Directory Panel - 3 - 1048576 - 2147483647 - - - - - - Details Panel - 4 - 1048576 - 2147483647 - - - - - - YES - YES - - - 1048576 - 2147483647 - - - - - - Minimize - - 2147483647 - - - - - - Zoom - - 1048576 - 2147483647 - - - - - - Close Window - w - 1048576 - 2147483647 - - - - - - YES - YES - - - 1048576 - 2147483647 - - - - - - Bring All to Front - - 1048576 - 2147483647 - - - - - _NSWindowsMenu - - - - - Help - - 1048576 - 2147483647 - - - submenuAction: - - Help - - - - dupeGuru ME Help - ? - 1048576 - 2147483647 - - - - - - dupeGuru ME Website - - 1048576 - 2147483647 - - - - - - - - _NSMainMenu - - - AppDelegate - - - ResultWindow - - - YES - - - RecentDirectories - - - 3 - 2 - {{92, 259}, {361, 343}} - 1886912512 - dupeGuru ME Preferences - - NSWindow - - - View - - {1.79769e+308, 1.79769e+308} - {213, 107} - - - 256 - - - - 292 - {{120, 264}, {190, 21}} - - YES - - 67239424 - 0 - - - - - Helvetica - 12 - 16 - - - 100 - 1 - 80 - 0.0 - 0 - 1 - NO - NO - - - - - 292 - {{122, 247}, {80, 13}} - - YES - - 67239424 - 272629760 - More results - - LucidaGrande - 10 - 2843 - - - - - - - - - 289 - {{228, 247}, {80, 13}} - - YES - - 67239424 - 71303168 - Less results - - - - - - - - - 292 - {{17, 269}, {100, 14}} - - YES - - 67239424 - 272629760 - Filter hardness: - - - - - - - - - 292 - {{20, 310}, {85, 13}} - - YES - - 67239424 - 272629760 - Scan type: - - - - - - - - - 292 - {{113, 299}, {231, 26}} - - YES - - -2076049856 - 2048 - - - 109199615 - 1 - - - - - - 400 - 75 - - - Tags - - 1048576 - 2147483647 - 1 - - - _popUpItemAction: - - - YES - - - OtherViews - - - - - Filename - - 1048576 - 2147483647 - - - _popUpItemAction: - - - - - Filename - Fields - - 1048576 - 2147483647 - - - _popUpItemAction: - - - - - Filename - Fields (No Order) - - 1048576 - 2147483647 - - - _popUpItemAction: - - - - - - Content - - 1048576 - 2147483647 - - - _popUpItemAction: - - - - - Audio Content - - 1048576 - 2147483647 - - - _popUpItemAction: - - - - - 3 - 3 - YES - YES - 1 - - - - - 256 - {{18, 183}, {214, 18}} - - YES - - 67239424 - 0 - Word weighting - - - 1211912703 - 2 - - - - 200 - 25 - - - - - 256 - {{18, 143}, {214, 18}} - - YES - - 67239424 - 0 - Can mix file kind - - - 1211912703 - 2 - - - - 200 - 25 - - - - - 256 - {{316, 269}, {31, 14}} - - YES - - 67239424 - -1874853888 - - - - - - - 0 - - - . - - , - -0 - 0 - - - 0 - -0 - - - - - - - - NaN - - - - 0 - 0 - YES - NO - 1 - AAAAAAAAAAAAAAAAAAAAAA - - - - . - , - NO - YES - YES - - - - - - - - - 256 - {{234, 12}, {113, 32}} - - YES - - 67239424 - 134217728 - Presets - - - -2038284033 - 1 - - - - - - 200 - 25 - - - - - 256 - {{18, 163}, {214, 18}} - - YES - - 67239424 - 0 - Match similar words - - - 1211912703 - 2 - - - - 200 - 25 - - - - - -2147483392 - {{180, 16}, {46, 26}} - - YES - - -2076049856 - 2048 - - - 109199615 - 1 - - LucidaGrande - 13 - 16 - - - - - - 400 - 75 - - - Tight, 99.9% accurate scan (audio content) - - 1048576 - 2147483647 - 1 - - - _popUpItemAction: - - - YES - - - OtherViews - - - - - - Normal, very accurate scan (tag) - - 1048576 - 2147483647 - - - _popUpItemAction: - 1 - - - - - Scan for tag-less songs (filename - fields) - - 1048576 - 2147483647 - - - _popUpItemAction: - 2 - - - - - Broader scan, might have false positives (filename) - - 1048576 - 2147483647 - - - _popUpItemAction: - 3 - - - - - Deep scan, lots of false positive (filename) - - 1048576 - 2147483647 - - - _popUpItemAction: - 4 - - - - - 3 - YES - YES - 1 - - - - - 292 - {{20, 62}, {85, 13}} - - YES - - 67239424 - 272629760 - Copy and Move: - - - - - - - - - 292 - {{110, 51}, {234, 26}} - - YES - - -2076049856 - 2048 - - - 109199615 - 1 - - - - - - 400 - 75 - - - Right in destination - - 1048576 - 2147483647 - 1 - - - _popUpItemAction: - - - YES - - - OtherViews - - - - - - Recreate relative path - - 1048576 - 2147483647 - - - _popUpItemAction: - - - - - Recreate absolute path - - 1048576 - 2147483647 - - - _popUpItemAction: - - - - - 3 - YES - YES - 1 - - - - - 256 - {{18, 81}, {283, 18}} - - YES - - 67239424 - 0 - Check for update on startup - - - 1211912703 - 2 - - - - 200 - 25 - - - - - 256 - {{18, 123}, {262, 18}} - - YES - - 67239424 - 0 - Use regular expressions when filtering - - - 1211912703 - 2 - - - - 200 - 25 - - - - - 256 - {{18, 103}, {262, 18}} - - YES - - 67239424 - 0 - Remove empty folders after delete and move - - - 1211912703 - 2 - - - - 200 - 25 - - - - - 292 - {{17, 225}, {100, 17}} - - YES - - 67239424 - 272629760 - Tags to scan: - - - - - - - - - 256 - {{27, 205}, {55, 18}} - - YES - - 67239424 - 0 - Track - - - 1211912703 - 2 - - - - 200 - 25 - - - - - 256 - {{80, 205}, {55, 18}} - - YES - - 67239424 - 0 - Artist - - - 1211912703 - 2 - - - - 200 - 25 - - - - - 256 - {{132, 205}, {60, 18}} - - YES - - 67239424 - 0 - Album - - - 1211912703 - 2 - - - - 200 - 25 - - - - - 256 - {{190, 205}, {51, 18}} - - YES - - 67239424 - 0 - Title - - - 1211912703 - 2 - - - - 200 - 25 - - - - - 256 - {{238, 205}, {54, 18}} - - YES - - 67239424 - 0 - Genre - - - 1211912703 - 2 - - - - 200 - 25 - - - - - 256 - {{295, 205}, {54, 18}} - - YES - - 67239424 - 0 - Year - - - 1211912703 - 2 - - - - 200 - 25 - - - - {361, 343} - - - {{0, 0}, {1440, 878}} - {213, 129} - {1.79769e+308, 1.79769e+308} - - - PyDupeGuru - - - Menu - - - - Remove Selected from Results - - 1048576 - 2147483647 - - - - - - Add Selected to Ignore List - - 1048576 - 2147483647 - - - - - - Make Selected Reference - - 1048576 - 2147483647 - - - - - - YES - YES - - - 1048576 - 2147483647 - - - - - - Open Selected with Default Application - - 1048576 - 2147483647 - - - - - - Reveal Selected in Finder - - 1048576 - 2147483647 - - - - - - Rename Selected - - 1048576 - 2147483647 - - - - - - - - SUUpdater - - - - - - - performMiniaturize: - - - - 37 - - - - arrangeInFront: - - - - 39 - - - - showHelp: - - - - 122 - - - - terminate: - - - - 139 - - - - orderFrontStandardAboutPanel: - - - - 142 - - - - hideOtherApplications: - - - - 146 - - - - hide: - - - - 152 - - - - unhideAllApplications: - - - - 153 - - - - performZoom: - - - - 198 - - - - delegate - - - - 207 - - - - delegate - - - - 208 - - - - window - - - - 210 - - - - result - - - - 211 - - - - delegate - - - - 212 - - - - matches - - - - 245 - - - - initialFirstResponder - - - - 279 - - - - delegate - - - - 410 - - - - markToggle: - - - - 414 - - - - stats - - - - 445 - - - - delegate - - - - 502 - - - - recentDirectories - - - - 503 - - - - makeKeyAndOrderFront: - - - - 543 - - - - initialFirstResponder - - - - 544 - - - - value: values.minMatchPercentage - - - - - - value: values.minMatchPercentage - value - values.minMatchPercentage - 2 - - - 549 - - - - selectedIndex: values.scanType - - - - - - selectedIndex: values.scanType - selectedIndex - values.scanType - 2 - - - 551 - - - - enabled: values.scanType - - - - - - enabled: values.scanType - enabled - values.scanType - - NSValueTransformerName - vtScanTypeIsNotContent - - 2 - - - 554 - - - - toggleDetailsPanel: - - - - 596 - - - - deleteMarked: - - - - 606 - - - - moveMarked: - - - - 607 - - - - copyMarked: - - - - 608 - - - - removeMarked: - - - - 609 - - - - switchSelected: - - - - 610 - - - - removeSelected: - - - - 611 - - - - py - - - - 614 - - - - py - - - - 616 - - - - toggleColumn: - - - - 627 - - - - toggleColumn: - - - - 628 - - - - toggleColumn: - - - - 629 - - - - toggleColumn: - - - - 630 - - - - toggleColumn: - - - - 631 - - - - toggleColumn: - - - - 632 - - - - toggleColumn: - - - - 633 - - - - enabled: values.scanType - - - - - - enabled: values.scanType - enabled - values.scanType - - NSValueTransformerName - vtScanTypeIsNotContent - - 2 - - - 640 - - - - value: values.wordWeighting - - - - - - value: values.wordWeighting - value - values.wordWeighting - 2 - - - 642 - - - - toggleColumn: - - - - 647 - - - - value: values.mixFileKind - - - - - - value: values.mixFileKind - value - values.mixFileKind - 2 - - - 656 - - - - openSelected: - - - - 660 - - - - revealSelected: - - - - 661 - - - - menu - - - - 663 - - - - toggleColumn: - - - - 706 - - - - openSelected: - - - - 709 - - - - revealSelected: - - - - 711 - - - - value: values.minMatchPercentage - - - - - - value: values.minMatchPercentage - value - values.minMatchPercentage - 2 - - - 713 - - - - switchSelected: - - - - 716 - - - - preferencesPanel - - - - 718 - - - - deleteMarked: - - - - 741 - - - - moveMarked: - - - - 742 - - - - copyMarked: - - - - 743 - - - - removeMarked: - - - - 744 - - - - removeSelected: - - - - 745 - - - - switchSelected: - - - - 746 - - - - openSelected: - - - - 747 - - - - revealSelected: - - - - 748 - - - - defaultsController - - - - 753 - - - - unlockApp: - - - - 755 - - - - unlockMenuItem - - - - 756 - - - - app - - - - 757 - - - - toggleDirectories: - - - - 758 - - - - py - - - - 764 - - - - enabled: values.scanType - - - - - - enabled: values.scanType - enabled - values.scanType - - NSValueTransformerName - vtScanTypeIsNotContent - - 2 - - - 774 - - - - value: values.matchSimilarWords - - - - - - value: values.matchSimilarWords - value - values.matchSimilarWords - 2 - - - 775 - - - - nextKeyView - - - - 781 - - - - toggleColumn: - - - - 786 - - - - toggleColumn: - - - - 787 - - - - toggleColumn: - - - - 794 - - - - toggleColumn: - - - - 795 - - - - toggleColumn: - - - - 796 - - - - toggleColumn: - - - - 797 - - - - toggleColumn: - - - - 798 - - - - toggleColumn: - - - - 799 - - - - removeSelected: - - - - 873 - - - - toggleColumn: - - - - 875 - - - - changeDelta: - - - - 882 - - - - deltaSwitch - - - - 883 - - - - usePreset: - - - - 891 - - - - usePreset: - - - - 892 - - - - usePreset: - - - - 893 - - - - usePreset: - - - - 894 - - - - usePreset: - - - - 895 - - - - popupPresets: - - - - 896 - - - - presetsButton - - - - 897 - - - - presetsPopup - - - - 902 - - - - selectedIndex: values.recreatePathType - - - - - - selectedIndex: values.recreatePathType - selectedIndex - values.recreatePathType - 2 - - - 914 - - - - ignoreSelected: - - - - 921 - - - - ignoreSelected: - - - - 923 - - - - performClose: - - - - 925 - - - - startDuplicateScan: - - - - 929 - - - - clearIgnoreList: - - - - 930 - - - - renameSelected: - - - - 932 - - - - renameSelected: - - - - 934 - - - - renameSelected: - - - - 938 - - - - ignoreSelected: - - - - 939 - - - - toggleColumn: - - - - 941 - - - - columnsMenu - - - - 942 - - - - resetColumnsToDefault: - - - - 945 - - - - exportToXHTML: - - - - 947 - - - - checkForUpdates: - - - - 950 - - - - value: values.SUCheckAtStartup - - - - - - value: values.SUCheckAtStartup - value - values.SUCheckAtStartup - 2 - - - 952 - - - - openWebsite: - - - - 956 - - - - togglePowerMarker: - - - - 961 - - - - toggleDelta: - - - - 962 - - - - changePowerMarker: - - - - 965 - - - - pmSwitch - - - - 966 - - - - copy: - - - - 1006 - - - - cut: - - - - 1007 - - - - paste: - - - - 1011 - - - - markAll: - - - - 1019 - - - - markNone: - - - - 1020 - - - - markSelected: - - - - 1021 - - - - markInvert: - - - - 1022 - - - - filter: - - - - 1025 - - - - filterField - - - - 1027 - - - - value: values.useRegexpFilter - - - - - - value: values.useRegexpFilter - value - values.useRegexpFilter - 2 - - - 1031 - - - - nextKeyView - - - - 1064 - - - - nextKeyView - - - - 1066 - - - - nextKeyView - - - - 1067 - - - - nextKeyView - - - - 1068 - - - - nextKeyView - - - - 1069 - - - - nextKeyView - - - - 1070 - - - - nextKeyView - - - - 1071 - - - - nextKeyView - - - - 1072 - - - - value: values.removeEmptyFolders - - - - - - value: values.removeEmptyFolders - value - values.removeEmptyFolders - 2 - - - 1074 - - - - nextKeyView - - - - 1121 - - - - nextKeyView - - - - 1122 - - - - nextKeyView - - - - 1123 - - - - nextKeyView - - - - 1124 - - - - nextKeyView - - - - 1125 - - - - nextKeyView - - - - 1126 - - - - nextKeyView - - - - 1127 - - - - value: values.scanTagTrack - - - - - - value: values.scanTagTrack - value - values.scanTagTrack - 2 - - - 1129 - - - - value: values.scanTagArtist - - - - - - value: values.scanTagArtist - value - values.scanTagArtist - 2 - - - 1130 - - - - value: values.scanTagAlbum - - - - - - value: values.scanTagAlbum - value - values.scanTagAlbum - 2 - - - 1131 - - - - value: values.scanTagTitle - - - - - - value: values.scanTagTitle - value - values.scanTagTitle - 2 - - - 1132 - - - - value: values.scanTagGenre - - - - - - value: values.scanTagGenre - value - values.scanTagGenre - 2 - - - 1133 - - - - value: values.scanTagYear - - - - - - value: values.scanTagYear - value - values.scanTagYear - 2 - - - 1134 - - - - enabled: values.scanType - - - - - - enabled: values.scanType - enabled - values.scanType - - NSValueTransformerName - vtScanTypeIsTag - - 2 - - - 1136 - - - - enabled: values.scanType - - - - - - enabled: values.scanType - enabled - values.scanType - - NSValueTransformerName - vtScanTypeIsTag - - 2 - - - 1139 - - - - enabled: values.scanType - - - - - - enabled: values.scanType - enabled - values.scanType - - NSValueTransformerName - vtScanTypeIsTag - - 2 - - - 1141 - - - - enabled: values.scanType - - - - - - enabled: values.scanType - enabled - values.scanType - - NSValueTransformerName - vtScanTypeIsTag - - 2 - - - 1143 - - - - enabled: values.scanType - - - - - - enabled: values.scanType - enabled - values.scanType - - NSValueTransformerName - vtScanTypeIsTag - - 2 - - - 1145 - - - - enabled: values.scanType - - - - - - enabled: values.scanType - enabled - values.scanType - - NSValueTransformerName - vtScanTypeIsTag - - 2 - - - 1147 - - - - removeDeadTracks: - - - - 1187 - - - - startDuplicateScan: - - - - 1242 - - - - toggleDirectories: - - - - 1243 - - - - toggleDetailsPanel: - - - - 1244 - - - - showPreferencesPanel: - - - - 1245 - - - - - - 0 - - - - - - -2 - - - File's Owner - - - -1 - - - First Responder - - - -3 - - - Application - - - 21 - - - - - - - Window - - - 2 - - - - - - - - - 219 - - - - - - - - - - - 220 - - - - - - - - - - - - - 222 - - - - - - - - 223 - - - - - - - - 233 - - - - - - - - 406 - - - - - - - - 407 - - - - - 782 - - - - - - - - 783 - - - - - - - - 291 - - - - - - - - 29 - - - - - - - - - - - - MainMenu - - - 19 - - - - - - - - 24 - - - - - - - - - - - - - - - 5 - - - - - 23 - - - - - 92 - - - - - 197 - - - - - 398 - - - - - 399 - - - - - 579 - - - - - 924 - - - - - 56 - - - - - - - - 57 - - - - - - - - - - - - - - - - - - 58 - - - - - 134 - - - - - 136 - - - - - 144 - - - - - 145 - - - - - 149 - - - - - 150 - - - - - 541 - - - - - 542 - - - - - 754 - - - - - 949 - - - - - 103 - - - - - - - - 106 - - - - - - - - - 111 - - - - - 955 - - - - - 597 - - - - - - - - 598 - - - - - - - - - - - - - - - - - - - - - - - - 599 - - - - - 600 - - - - - 601 - - - - - 602 - - - - - 603 - - - - - 604 - - - - - 605 - - - - - 707 - - - - - 708 - - - - - 710 - - - - - 922 - - - - - 926 - - - - - 927 - - - - - 928 - - - - - 931 - - - - - 946 - - - - - 953 - - - - - 618 - - - - - - - - 619 - - - - - - - - - - - - - - - - - - - - - - - - - - - - 620 - - - - - 621 - - - - - 622 - - - - - 623 - - - - - 624 - - - - - 625 - - - - - 626 - - - - - 646 - - - - - 705 - - - - - 784 - - - - - 785 - - - - - 788 - - - - - 789 - - - - - 790 - - - - - 791 - - - - - 792 - - - - - 793 - - - - - 874 - - - - - 940 - - - - - 943 - - - - - 944 - - - - - 957 - - - - - - - - 958 - - - - - - - - - 959 - - - - - 960 - - - - - 968 - - - - - - - - 969 - - - - - - - - - - - - - - - 974 - - - - - 994 - - - - - 995 - - - - - 1014 - - - - - 1015 - - - - - 1016 - - - - - 1017 - - - - - 1018 - - - - - 206 - - - AppDelegate - - - 209 - - - ResultWindow - - - 468 - - - Shared Defaults - - - 497 - - - RecentDirectoriesController - - - 523 - - - - - - preferences - - - 524 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 531 - - - - - - - - 532 - - - - - - - - 533 - - - - - - - - 534 - - - - - - - - 535 - - - - - - - - 536 - - - - - - - - 635 - - - - - - - - 649 - - - - - - - - 712 - - - - - - - - 750 - - - - - - - - 772 - - - - - - - - 899 - - - - - - - - 904 - - - - - - - - 905 - - - - - - - - 951 - - - - - - - - 1028 - - - - - - - - 1062 - - - - - - - - 1107 - - - - - - - - 1109 - - - - - - - - 1111 - - - - - - - - 1113 - - - - - - - - 1115 - - - - - - - - 1117 - - - - - - - - 1119 - - - - - - - - 613 - - - PyDupeGuru - - - 657 - - - - - - - - - - - - matches_context - - - 658 - - - - - 659 - - - - - 715 - - - - - 872 - - - - - 935 - - - - - 936 - - - - - 937 - - - - - 948 - - - SUUpdater - - - 1189 - - - - - 1190 - - - - - 1191 - - - - - 1192 - - - - - 1193 - - - - - 1194 - - - - - 1195 - - - - - - - - 1196 - - - - - 1197 - - - - - 1198 - - - - - - - - 1199 - - - - - 1200 - - - - - 1201 - - - - - - - - 1202 - - - - - 1203 - - - - - - - - 1204 - - - - - 1205 - - - - - 1206 - - - - - 1207 - - - - - 1208 - - - - - 1209 - - - - - 1210 - - - - - 1211 - - - - - 1212 - - - - - 1213 - - - - - 1218 - - - - - 1219 - - - - - 1220 - - - - - 1221 - - - - - 1222 - - - - - 714 - - - - - 537 - - - - - - - - - - - - - 804 - - - - - 803 - - - - - 801 - - - - - 800 - - - - - 539 - - - - - 538 - - - - - 900 - - - - - - - - - - - - 906 - - - - - - - - - - 913 - - - - - 909 - - - - - 908 - - - - - 1223 - - - - - 1224 - - - - - 1225 - - - - - 1226 - - - - - - - - - - - - - - - - - - 1229 - - - - - 1231 - - - - - 1232 - - - - - 1234 - - - - - 1235 - - - - - 1236 - - - - - 1237 - - - - - 1238 - - - - - - - - 720 - - - - - - - - 1214 - - - - - - - - 721 - - - - - - - - - - - - - - - - - - - - 723 - - - - - 731 - - - - - 732 - - - - - 733 - - - - - 734 - - - - - 735 - - - - - 736 - - - - - 920 - - - - - 738 - - - - - 737 - - - - - 739 - - - - - 740 - - - - - 933 - - - - - 1239 - - - - - - - - 880 - - - - - - - - 1215 - - - - - 1240 - - - - - - - - 964 - - - - - - - - 1216 - - - - - 1241 - - - - - - - - 1024 - - - - - - - - 1217 - - - - - 886 - - - - - 887 - - - - - 889 - - - - - 888 - - - - - 890 - - - - - - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - {{58, 778}, {617, 0}} - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - tbbScan - com.apple.InterfaceBuilder.CocoaPlugin - tbbDirectories - com.apple.InterfaceBuilder.CocoaPlugin - tbbDetail - com.apple.InterfaceBuilder.CocoaPlugin - tbbPreferences - com.apple.InterfaceBuilder.CocoaPlugin - tbbAction - com.apple.InterfaceBuilder.CocoaPlugin - tbbDelta - com.apple.InterfaceBuilder.CocoaPlugin - tbbPowerMarker - com.apple.InterfaceBuilder.CocoaPlugin - tbbFilter - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - - - {{87, 377}, {557, 400}} - com.apple.InterfaceBuilder.CocoaPlugin - {{87, 377}, {557, 400}} - - - - {340, 340} - com.apple.InterfaceBuilder.CocoaPlugin - - MatchesView - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - {{88, 814}, {506, 20}} - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - - com.apple.InterfaceBuilder.CocoaPlugin - - {{88, 502}, {361, 343}} - com.apple.InterfaceBuilder.CocoaPlugin - {{88, 502}, {361, 343}} - - - {213, 107} - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - {{259, 501}, {359, 313}} - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - {{73, 457}, {331, 243}} - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - {{257, 441}, {411, 103}} - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - - - - - - 1245 - - - - - AppDelegate - AppDelegateBase - - id - id - id - id - - - NSButton - NSPopUpButton - ResultWindow - - - IBProjectSource - AppDelegate.h - - - - AppDelegate - AppDelegateBase - - unlockApp: - id - - - NSUserDefaultsController - PyDupeGuru - RecentDirectories - NSMenuItem - - - IBUserSource - - - - - AppDelegateBase - NSObject - - unlockApp: - id - - - PyDupeGuruBase - RecentDirectories - NSMenuItem - - - IBProjectSource - dgbase/AppDelegate.h - - - - FirstResponder - NSObject - - IBUserSource - - - - - MatchesView - OutlineView - - IBProjectSource - dgbase/ResultWindow.h - - - - MatchesView - OutlineView - - IBUserSource - - - - - NSSegmentedControl - NSControl - - IBUserSource - - - - - OutlineView - NSOutlineView - - py - PyApp - - - IBProjectSource - cocoalib/Outline.h - - - - OutlineView - NSOutlineView - - IBUserSource - - - - - PyApp - PyRegistrable - - IBProjectSource - cocoalib/PyApp.h - - - - PyApp - PyRegistrable - - IBUserSource - - - - - PyDupeGuru - PyDupeGuruBase - - IBProjectSource - PyDupeGuru.h - - - - PyDupeGuru - PyDupeGuruBase - - IBUserSource - - - - - PyDupeGuruBase - PyApp - - IBProjectSource - dgbase/PyDupeGuru.h - - - - PyDupeGuruBase - PyApp - - IBUserSource - - - - - RecentDirectories - NSObject - - id - id - - - id - NSMenu - - - IBProjectSource - cocoalib/RecentDirectories.h - - - - RecentDirectories - NSObject - - IBUserSource - - - - - ResultWindow - ResultWindowBase - - id - id - id - id - id - id - id - id - id - id - id - id - id - id - id - id - id - id - id - id - id - - - NSMenu - NSSearchField - NSWindow - - - IBProjectSource - ResultWindow.h - - - - ResultWindow - ResultWindowBase - - id - id - id - id - id - id - id - id - id - id - - - NSView - id - NSSegmentedControl - NSView - NSView - MatchesView - NSSegmentedControl - NSView - PyDupeGuru - NSTextField - - - IBUserSource - - - - - ResultWindowBase - NSWindowController - - id - id - id - id - id - id - id - id - id - - - id - NSSegmentedControl - MatchesView - NSSegmentedControl - PyDupeGuruBase - NSTextField - - - - - ResultWindowBase - NSWindowController - - IBUserSource - - - - - SUUpdater - NSObject - - IBUserSource - - - - - - - NSActionCell - NSCell - - IBFrameworkSource - AppKit.framework/Headers/NSActionCell.h - - - - NSApplication - NSResponder - - IBFrameworkSource - AppKit.framework/Headers/NSApplication.h - - - - NSApplication - - IBFrameworkSource - AppKit.framework/Headers/NSApplicationScripting.h - - - - NSApplication - - IBFrameworkSource - AppKit.framework/Headers/NSColorPanel.h - - - - NSApplication - - IBFrameworkSource - AppKit.framework/Headers/NSHelpManager.h - - - - NSApplication - - IBFrameworkSource - AppKit.framework/Headers/NSPageLayout.h - - - - NSApplication - - IBFrameworkSource - AppKit.framework/Headers/NSUserInterfaceItemSearching.h - - - - NSButton - NSControl - - IBFrameworkSource - AppKit.framework/Headers/NSButton.h - - - - NSButtonCell - NSActionCell - - IBFrameworkSource - AppKit.framework/Headers/NSButtonCell.h - - - - NSCell - NSObject - - IBFrameworkSource - AppKit.framework/Headers/NSCell.h - - - - NSControl - NSView - - IBFrameworkSource - AppKit.framework/Headers/NSControl.h - - - - NSController - NSObject - - IBFrameworkSource - AppKit.framework/Headers/NSController.h - - - - NSFormatter - NSObject - - IBFrameworkSource - Foundation.framework/Headers/NSFormatter.h - - - - NSMenu - NSObject - - IBFrameworkSource - AppKit.framework/Headers/NSMenu.h - - - - NSMenuItem - NSObject - - IBFrameworkSource - AppKit.framework/Headers/NSMenuItem.h - - - - NSMenuItemCell - NSButtonCell - - IBFrameworkSource - AppKit.framework/Headers/NSMenuItemCell.h - - - - NSMovieView - NSView - - IBFrameworkSource - AppKit.framework/Headers/NSMovieView.h - - - - NSNumberFormatter - NSFormatter - - IBFrameworkSource - Foundation.framework/Headers/NSNumberFormatter.h - - - - NSObject - - IBFrameworkSource - AppKit.framework/Headers/NSAccessibility.h - - - - NSObject - - - - NSObject - - - - NSObject - - - - NSObject - - - - NSObject - - IBFrameworkSource - AppKit.framework/Headers/NSDictionaryController.h - - - - NSObject - - IBFrameworkSource - AppKit.framework/Headers/NSDragging.h - - - - NSObject - - IBFrameworkSource - AppKit.framework/Headers/NSFontManager.h - - - - NSObject - - IBFrameworkSource - AppKit.framework/Headers/NSFontPanel.h - - - - NSObject - - IBFrameworkSource - AppKit.framework/Headers/NSKeyValueBinding.h - - - - NSObject - - - - NSObject - - IBFrameworkSource - AppKit.framework/Headers/NSNibLoading.h - - - - NSObject - - IBFrameworkSource - AppKit.framework/Headers/NSOutlineView.h - - - - NSObject - - IBFrameworkSource - AppKit.framework/Headers/NSPasteboard.h - - - - NSObject - - IBFrameworkSource - AppKit.framework/Headers/NSSavePanel.h - - - - NSObject - - IBFrameworkSource - AppKit.framework/Headers/NSTableView.h - - - - NSObject - - IBFrameworkSource - AppKit.framework/Headers/NSToolbarItem.h - - - - NSObject - - IBFrameworkSource - AppKit.framework/Headers/NSView.h - - - - NSObject - - IBFrameworkSource - Foundation.framework/Headers/NSArchiver.h - - - - NSObject - - IBFrameworkSource - Foundation.framework/Headers/NSClassDescription.h - - - - NSObject - - IBFrameworkSource - Foundation.framework/Headers/NSError.h - - - - NSObject - - IBFrameworkSource - Foundation.framework/Headers/NSFileManager.h - - - - NSObject - - IBFrameworkSource - Foundation.framework/Headers/NSKeyValueCoding.h - - - - NSObject - - IBFrameworkSource - Foundation.framework/Headers/NSKeyValueObserving.h - - - - NSObject - - IBFrameworkSource - Foundation.framework/Headers/NSKeyedArchiver.h - - - - NSObject - - IBFrameworkSource - Foundation.framework/Headers/NSObject.h - - - - NSObject - - IBFrameworkSource - Foundation.framework/Headers/NSObjectScripting.h - - - - NSObject - - IBFrameworkSource - Foundation.framework/Headers/NSPortCoder.h - - - - NSObject - - IBFrameworkSource - Foundation.framework/Headers/NSRunLoop.h - - - - NSObject - - IBFrameworkSource - Foundation.framework/Headers/NSScriptClassDescription.h - - - - NSObject - - IBFrameworkSource - Foundation.framework/Headers/NSScriptKeyValueCoding.h - - - - NSObject - - IBFrameworkSource - Foundation.framework/Headers/NSScriptObjectSpecifiers.h - - - - NSObject - - IBFrameworkSource - Foundation.framework/Headers/NSScriptWhoseTests.h - - - - NSObject - - IBFrameworkSource - Foundation.framework/Headers/NSThread.h - - - - NSObject - - IBFrameworkSource - Foundation.framework/Headers/NSURL.h - - - - NSObject - - IBFrameworkSource - Foundation.framework/Headers/NSURLConnection.h - - - - NSObject - - IBFrameworkSource - Foundation.framework/Headers/NSURLDownload.h - - - - NSObject - - IBFrameworkSource - Sparkle.framework/Headers/SUAppcast.h - - - - NSObject - - IBFrameworkSource - Sparkle.framework/Headers/SUUpdater.h - - - - NSOutlineView - NSTableView - - - - NSPopUpButton - NSButton - - IBFrameworkSource - AppKit.framework/Headers/NSPopUpButton.h - - - - NSPopUpButtonCell - NSMenuItemCell - - IBFrameworkSource - AppKit.framework/Headers/NSPopUpButtonCell.h - - - - NSResponder - - IBFrameworkSource - AppKit.framework/Headers/NSInterfaceStyle.h - - - - NSResponder - NSObject - - IBFrameworkSource - AppKit.framework/Headers/NSResponder.h - - - - NSScrollView - NSView - - IBFrameworkSource - AppKit.framework/Headers/NSScrollView.h - - - - NSScroller - NSControl - - IBFrameworkSource - AppKit.framework/Headers/NSScroller.h - - - - NSSearchField - NSTextField - - IBFrameworkSource - AppKit.framework/Headers/NSSearchField.h - - - - NSSearchFieldCell - NSTextFieldCell - - IBFrameworkSource - AppKit.framework/Headers/NSSearchFieldCell.h - - - - NSSegmentedCell - NSActionCell - - IBFrameworkSource - AppKit.framework/Headers/NSSegmentedCell.h - - - - NSSegmentedControl - NSControl - - IBFrameworkSource - AppKit.framework/Headers/NSSegmentedControl.h - - - - NSSlider - NSControl - - IBFrameworkSource - AppKit.framework/Headers/NSSlider.h - - - - NSSliderCell - NSActionCell - - IBFrameworkSource - AppKit.framework/Headers/NSSliderCell.h - - - - NSTableColumn - NSObject - - IBFrameworkSource - AppKit.framework/Headers/NSTableColumn.h - - - - NSTableHeaderView - NSView - - IBFrameworkSource - AppKit.framework/Headers/NSTableHeaderView.h - - - - NSTableView - NSControl - - - - NSText - NSView - - IBFrameworkSource - AppKit.framework/Headers/NSText.h - - - - NSTextField - NSControl - - IBFrameworkSource - AppKit.framework/Headers/NSTextField.h - - - - NSTextFieldCell - NSActionCell - - IBFrameworkSource - AppKit.framework/Headers/NSTextFieldCell.h - - - - NSToolbar - NSObject - - IBFrameworkSource - AppKit.framework/Headers/NSToolbar.h - - - - NSToolbarItem - NSObject - - - - NSUserDefaultsController - NSController - - IBFrameworkSource - AppKit.framework/Headers/NSUserDefaultsController.h - - - - NSView - - IBFrameworkSource - AppKit.framework/Headers/NSClipView.h - - - - NSView - - - - NSView - - IBFrameworkSource - AppKit.framework/Headers/NSRulerView.h - - - - NSView - NSResponder - - - - NSWindow - - IBFrameworkSource - AppKit.framework/Headers/NSDrawer.h - - - - NSWindow - NSResponder - - IBFrameworkSource - AppKit.framework/Headers/NSWindow.h - - - - NSWindow - - IBFrameworkSource - AppKit.framework/Headers/NSWindowScripting.h - - - - NSWindowController - NSResponder - - showWindow: - id - - - IBFrameworkSource - AppKit.framework/Headers/NSWindowController.h - - - - SUUpdater - NSObject - - checkForUpdates: - id - - - delegate - id - - - - - - 0 - - com.apple.InterfaceBuilder.CocoaPlugin.macosx - - - - com.apple.InterfaceBuilder.CocoaPlugin.macosx - - - YES - ../dupeguru.xcodeproj - 3 - - diff --git a/me/cocoa/xib/Preferences.xib b/me/cocoa/xib/Preferences.xib new file mode 100644 index 00000000..04d668af --- /dev/null +++ b/me/cocoa/xib/Preferences.xib @@ -0,0 +1,2611 @@ + + + + 1050 + 10B504 + 740 + 1038.2 + 437.00 + + com.apple.InterfaceBuilder.CocoaPlugin + 740 + + + YES + + + + YES + com.apple.InterfaceBuilder.CocoaPlugin + + + YES + + YES + + + YES + + + + YES + + NSWindowController + + + FirstResponder + + + NSApplication + + + YES + + + 3 + 2 + {{92, 259}, {361, 343}} + 1886912512 + dupeGuru ME Preferences + + NSWindow + + + View + + {1.79769e+308, 1.79769e+308} + {213, 107} + + + 256 + + YES + + + 292 + {{120, 264}, {190, 21}} + + YES + + 67239424 + 0 + + + + + Helvetica + 12 + 16 + + + 100 + 1 + 80 + 0.0 + 0 + 1 + NO + NO + + + + + 292 + {{122, 247}, {80, 13}} + + YES + + 67239424 + 272629760 + More results + + LucidaGrande + 10 + 2843 + + + + 6 + System + controlColor + + 3 + MC42NjY2NjY2NjY3AA + + + + 6 + System + controlTextColor + + 3 + MAA + + + + + + + 289 + {{228, 247}, {80, 13}} + + YES + + 67239424 + 71303168 + Less results + + + + + + + + + 292 + {{17, 269}, {100, 14}} + + YES + + 67239424 + 272629760 + Filter hardness: + + LucidaGrande + 11 + 3100 + + + + + + + + + 292 + {{20, 310}, {85, 13}} + + YES + + 67239424 + 272629760 + Scan type: + + + + + + + + + 292 + {{113, 299}, {231, 26}} + + YES + + -2076049856 + 2048 + + LucidaGrande + 13 + 1044 + + + 109199615 + 1 + + + + + + 400 + 75 + + + Tags + + 1048576 + 2147483647 + 1 + + NSImage + NSMenuCheckmark + + + NSImage + NSMenuMixedState + + _popUpItemAction: + + + YES + + + OtherViews + + + YES + + + Filename + + 1048576 + 2147483647 + + + _popUpItemAction: + + + + + Filename - Fields + + 1048576 + 2147483647 + + + _popUpItemAction: + + + + + Filename - Fields (No Order) + + 1048576 + 2147483647 + + + _popUpItemAction: + + + + + + Content + + 1048576 + 2147483647 + + + _popUpItemAction: + + + + + Audio Content + + 1048576 + 2147483647 + + + _popUpItemAction: + + + + + 3 + 3 + YES + YES + 1 + + + + + 256 + {{18, 183}, {214, 18}} + + YES + + 67239424 + 0 + Word weighting + + + 1211912703 + 2 + + NSSwitch + + + + 200 + 25 + + + + + 256 + {{18, 143}, {214, 18}} + + YES + + 67239424 + 0 + Can mix file kind + + + 1211912703 + 2 + + + + 200 + 25 + + + + + 256 + {{316, 269}, {31, 14}} + + YES + + 67239424 + -1874853888 + + + + + YES + + YES + allowsFloats + attributedStringForZero + decimalSeparator + formatterBehavior + groupingSeparator + negativeFormat + positiveFormat + usesGroupingSeparator + + + YES + + + 0 + + YES + + + YES + + + + . + + , + -0 + 0 + + + + 0 + -0 + + + + + + + + NaN + + + + 0 + 0 + YES + NO + 1 + AAAAAAAAAAAAAAAAAAAAAA + + + + . + , + NO + YES + YES + + + + + + + + + 256 + {{205, 12}, {148, 32}} + + YES + + 67239424 + 134217728 + Reset to Defaults + + + -2038284033 + 1 + + + + + + 200 + 25 + + + + + 256 + {{18, 163}, {214, 18}} + + YES + + 67239424 + 0 + Match similar words + + + 1211912703 + 2 + + + + 200 + 25 + + + + + 292 + {{20, 62}, {85, 13}} + + YES + + 67239424 + 272629760 + Copy and Move: + + + + + + + + + 292 + {{110, 51}, {234, 26}} + + YES + + -2076049856 + 2048 + + + 109199615 + 1 + + + + + + 400 + 75 + + + Right in destination + + 1048576 + 2147483647 + 1 + + + _popUpItemAction: + + + YES + + + OtherViews + + + YES + + + + Recreate relative path + + 1048576 + 2147483647 + + + _popUpItemAction: + + + + + Recreate absolute path + + 1048576 + 2147483647 + + + _popUpItemAction: + + + + + 3 + YES + YES + 1 + + + + + 256 + {{18, 81}, {283, 18}} + + YES + + 67239424 + 0 + Check for update on startup + + + 1211912703 + 2 + + + + 200 + 25 + + + + + 256 + {{18, 123}, {262, 18}} + + YES + + 67239424 + 0 + Use regular expressions when filtering + + + 1211912703 + 2 + + + + 200 + 25 + + + + + 256 + {{18, 103}, {262, 18}} + + YES + + 67239424 + 0 + Remove empty folders after delete and move + + + 1211912703 + 2 + + + + 200 + 25 + + + + + 292 + {{17, 225}, {100, 17}} + + YES + + 67239424 + 272629760 + Tags to scan: + + + + + + + + + 256 + {{27, 205}, {55, 18}} + + YES + + 67239424 + 0 + Track + + + 1211912703 + 2 + + + + 200 + 25 + + + + + 256 + {{80, 205}, {55, 18}} + + YES + + 67239424 + 0 + Artist + + + 1211912703 + 2 + + + + 200 + 25 + + + + + 256 + {{132, 205}, {60, 18}} + + YES + + 67239424 + 0 + Album + + + 1211912703 + 2 + + + + 200 + 25 + + + + + 256 + {{190, 205}, {51, 18}} + + YES + + 67239424 + 0 + Title + + + 1211912703 + 2 + + + + 200 + 25 + + + + + 256 + {{238, 205}, {54, 18}} + + YES + + 67239424 + 0 + Genre + + + 1211912703 + 2 + + + + 200 + 25 + + + + + 256 + {{295, 205}, {54, 18}} + + YES + + 67239424 + 0 + Year + + + 1211912703 + 2 + + + + 200 + 25 + + + + {361, 343} + + + {{0, 0}, {1440, 878}} + {213, 129} + {1.79769e+308, 1.79769e+308} + + + + + YES + + + enabled: values.scanType + + + + + + enabled: values.scanType + enabled + values.scanType + + NSValueTransformerName + vtScanTypeIsNotContent + + 2 + + + 70 + + + + value: values.scanTagTrack + + + + + + value: values.scanTagTrack + value + values.scanTagTrack + 2 + + + 71 + + + + value: values.minMatchPercentage + + + + + + value: values.minMatchPercentage + value + values.minMatchPercentage + 2 + + + 72 + + + + nextKeyView + + + + 73 + + + + value: values.scanTagYear + + + + + + value: values.scanTagYear + value + values.scanTagYear + 2 + + + 74 + + + + enabled: values.scanType + + + + + + enabled: values.scanType + enabled + values.scanType + + NSValueTransformerName + vtScanTypeIsTag + + 2 + + + 75 + + + + nextKeyView + + + + 76 + + + + enabled: values.scanType + + + + + + enabled: values.scanType + enabled + values.scanType + + NSValueTransformerName + vtScanTypeIsNotContent + + 2 + + + 77 + + + + enabled: values.scanType + + + + + + enabled: values.scanType + enabled + values.scanType + + NSValueTransformerName + vtScanTypeIsTag + + 2 + + + 78 + + + + value: values.scanTagTitle + + + + + + value: values.scanTagTitle + value + values.scanTagTitle + 2 + + + 79 + + + + nextKeyView + + + + 80 + + + + enabled: values.scanType + + + + + + enabled: values.scanType + enabled + values.scanType + + NSValueTransformerName + vtScanTypeIsTag + + 2 + + + 81 + + + + nextKeyView + + + + 82 + + + + value: values.matchSimilarWords + + + + + + value: values.matchSimilarWords + value + values.matchSimilarWords + 2 + + + 83 + + + + nextKeyView + + + + 84 + + + + value: values.SUCheckAtStartup + + + + + + value: values.SUCheckAtStartup + value + values.SUCheckAtStartup + 2 + + + 85 + + + + nextKeyView + + + + 86 + + + + value: values.wordWeighting + + + + + + value: values.wordWeighting + value + values.wordWeighting + 2 + + + 87 + + + + nextKeyView + + + + 88 + + + + value: values.scanTagAlbum + + + + + + value: values.scanTagAlbum + value + values.scanTagAlbum + 2 + + + 89 + + + + nextKeyView + + + + 90 + + + + enabled: values.scanType + + + + + + enabled: values.scanType + enabled + values.scanType + + NSValueTransformerName + vtScanTypeIsTag + + 2 + + + 91 + + + + enabled: values.scanType + + + + + + enabled: values.scanType + enabled + values.scanType + + NSValueTransformerName + vtScanTypeIsNotContent + + 2 + + + 92 + + + + value: values.mixFileKind + + + + + + value: values.mixFileKind + value + values.mixFileKind + 2 + + + 93 + + + + value: values.scanTagGenre + + + + + + value: values.scanTagGenre + value + values.scanTagGenre + 2 + + + 94 + + + + enabled: values.scanType + + + + + + enabled: values.scanType + enabled + values.scanType + + NSValueTransformerName + vtScanTypeIsTag + + 2 + + + 95 + + + + selectedIndex: values.scanType + + + + + + selectedIndex: values.scanType + selectedIndex + values.scanType + 2 + + + 96 + + + + value: values.scanTagArtist + + + + + + value: values.scanTagArtist + value + values.scanTagArtist + 2 + + + 97 + + + + selectedIndex: values.recreatePathType + + + + + + selectedIndex: values.recreatePathType + selectedIndex + values.recreatePathType + 2 + + + 98 + + + + nextKeyView + + + + 99 + + + + nextKeyView + + + + 100 + + + + nextKeyView + + + + 101 + + + + value: values.minMatchPercentage + + + + + + value: values.minMatchPercentage + value + values.minMatchPercentage + 2 + + + 102 + + + + value: values.removeEmptyFolders + + + + + + value: values.removeEmptyFolders + value + values.removeEmptyFolders + 2 + + + 103 + + + + nextKeyView + + + + 104 + + + + initialFirstResponder + + + + 105 + + + + value: values.useRegexpFilter + + + + + + value: values.useRegexpFilter + value + values.useRegexpFilter + 2 + + + 106 + + + + nextKeyView + + + + 107 + + + + nextKeyView + + + + 108 + + + + nextKeyView + + + + 109 + + + + enabled: values.scanType + + + + + + enabled: values.scanType + enabled + values.scanType + + NSValueTransformerName + vtScanTypeIsTag + + 2 + + + 110 + + + + nextKeyView + + + + 111 + + + + window + + + + 112 + + + + revertToInitialValues: + + + + 113 + + + + + YES + + 0 + + + + + + -2 + + + File's Owner + + + -1 + + + First Responder + + + -3 + + + Application + + + 1 + + + Shared Defaults + + + 2 + + + YES + + + + preferences + + + 3 + + + YES + + + + + + + + + + + + + + + + + + + + + + + + + + + + 4 + + + YES + + + + + + 5 + + + YES + + + + + + 6 + + + YES + + + + + + 7 + + + YES + + + + + + 8 + + + YES + + + + + + 9 + + + YES + + + + + + 10 + + + YES + + + + + + 11 + + + YES + + + + + + 12 + + + YES + + + + + + 13 + + + YES + + + + + + 14 + + + YES + + + + + + 15 + + + YES + + + + + + 17 + + + YES + + + + + + 18 + + + YES + + + + + + 19 + + + YES + + + + + + 20 + + + YES + + + + + + 21 + + + YES + + + + + + 22 + + + YES + + + + + + 23 + + + YES + + + + + + 24 + + + YES + + + + + + 25 + + + YES + + + + + + 26 + + + YES + + + + + + 27 + + + YES + + + + + + 28 + + + + + 29 + + + + + 30 + + + + + 31 + + + + + 32 + + + + + 33 + + + YES + + + + + + 34 + + + YES + + + + + + + + + + + 35 + + + + + 36 + + + + + 37 + + + + + 38 + + + + + 39 + + + + + 40 + + + + + 41 + + + + + 42 + + + + + 43 + + + YES + + + + + + 44 + + + + + 45 + + + + + 46 + + + + + 54 + + + + + 55 + + + YES + + + + + + 56 + + + YES + + + + + + + + 57 + + + + + 58 + + + + + 59 + + + + + 60 + + + + + 61 + + + + + 62 + + + + + 63 + + + + + 64 + + + + + 65 + + + + + 66 + + + + + 67 + + + + + 68 + + + + + 69 + + + + + + + YES + + YES + -1.IBPluginDependency + -2.IBPluginDependency + -3.IBPluginDependency + 1.IBPluginDependency + 1.ImportedFromIB2 + 10.IBPluginDependency + 10.ImportedFromIB2 + 11.IBPluginDependency + 11.ImportedFromIB2 + 12.IBPluginDependency + 12.ImportedFromIB2 + 13.IBPluginDependency + 13.ImportedFromIB2 + 14.IBPluginDependency + 14.ImportedFromIB2 + 15.IBPluginDependency + 15.ImportedFromIB2 + 17.IBPluginDependency + 17.ImportedFromIB2 + 18.IBPluginDependency + 18.ImportedFromIB2 + 19.IBPluginDependency + 19.ImportedFromIB2 + 2.IBEditorWindowLastContentRect + 2.IBPluginDependency + 2.IBWindowTemplateEditedContentRect + 2.ImportedFromIB2 + 2.windowTemplate.hasMinSize + 2.windowTemplate.minSize + 20.IBPluginDependency + 20.ImportedFromIB2 + 21.IBPluginDependency + 21.ImportedFromIB2 + 22.IBPluginDependency + 22.ImportedFromIB2 + 23.IBPluginDependency + 23.ImportedFromIB2 + 24.IBPluginDependency + 24.ImportedFromIB2 + 25.IBPluginDependency + 25.ImportedFromIB2 + 26.IBPluginDependency + 26.ImportedFromIB2 + 27.IBPluginDependency + 27.ImportedFromIB2 + 28.IBPluginDependency + 29.IBPluginDependency + 3.IBPluginDependency + 3.ImportedFromIB2 + 30.IBPluginDependency + 31.IBPluginDependency + 32.IBPluginDependency + 33.IBPluginDependency + 34.IBPluginDependency + 34.ImportedFromIB2 + 35.IBPluginDependency + 35.ImportedFromIB2 + 36.IBPluginDependency + 36.ImportedFromIB2 + 37.IBPluginDependency + 37.ImportedFromIB2 + 38.IBPluginDependency + 38.ImportedFromIB2 + 39.IBPluginDependency + 39.ImportedFromIB2 + 4.IBPluginDependency + 4.ImportedFromIB2 + 40.IBPluginDependency + 40.ImportedFromIB2 + 41.IBPluginDependency + 42.IBPluginDependency + 43.IBPluginDependency + 44.IBPluginDependency + 44.ImportedFromIB2 + 45.IBPluginDependency + 46.IBPluginDependency + 5.IBPluginDependency + 5.ImportedFromIB2 + 54.IBPluginDependency + 55.IBPluginDependency + 56.IBPluginDependency + 56.ImportedFromIB2 + 57.IBPluginDependency + 57.ImportedFromIB2 + 58.IBPluginDependency + 58.ImportedFromIB2 + 59.IBPluginDependency + 59.ImportedFromIB2 + 6.IBPluginDependency + 6.ImportedFromIB2 + 60.IBPluginDependency + 61.IBPluginDependency + 62.IBPluginDependency + 63.IBPluginDependency + 64.IBPluginDependency + 65.IBPluginDependency + 66.IBPluginDependency + 67.IBPluginDependency + 68.IBPluginDependency + 69.IBPluginDependency + 7.IBPluginDependency + 7.ImportedFromIB2 + 8.IBPluginDependency + 8.ImportedFromIB2 + 9.IBPluginDependency + 9.ImportedFromIB2 + + + YES + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + {{88, 502}, {361, 343}} + com.apple.InterfaceBuilder.CocoaPlugin + {{88, 502}, {361, 343}} + + + {213, 107} + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + + + + YES + + + YES + + + + + YES + + + YES + + + + 113 + + + + YES + + NSActionCell + NSCell + + IBFrameworkSource + AppKit.framework/Headers/NSActionCell.h + + + + NSApplication + NSResponder + + IBFrameworkSource + AppKit.framework/Headers/NSApplication.h + + + + NSApplication + + IBFrameworkSource + AppKit.framework/Headers/NSApplicationScripting.h + + + + NSApplication + + IBFrameworkSource + AppKit.framework/Headers/NSColorPanel.h + + + + NSApplication + + IBFrameworkSource + AppKit.framework/Headers/NSHelpManager.h + + + + NSApplication + + IBFrameworkSource + AppKit.framework/Headers/NSPageLayout.h + + + + NSApplication + + IBFrameworkSource + AppKit.framework/Headers/NSUserInterfaceItemSearching.h + + + + NSButton + NSControl + + IBFrameworkSource + AppKit.framework/Headers/NSButton.h + + + + NSButtonCell + NSActionCell + + IBFrameworkSource + AppKit.framework/Headers/NSButtonCell.h + + + + NSCell + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSCell.h + + + + NSControl + NSView + + IBFrameworkSource + AppKit.framework/Headers/NSControl.h + + + + NSController + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSController.h + + + + NSFormatter + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSFormatter.h + + + + NSMenu + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSMenu.h + + + + NSMenuItem + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSMenuItem.h + + + + NSMenuItemCell + NSButtonCell + + IBFrameworkSource + AppKit.framework/Headers/NSMenuItemCell.h + + + + NSNumberFormatter + NSFormatter + + IBFrameworkSource + Foundation.framework/Headers/NSNumberFormatter.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSAccessibility.h + + + + NSObject + + + + NSObject + + + + NSObject + + + + NSObject + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSDictionaryController.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSDragging.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSFontManager.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSFontPanel.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSKeyValueBinding.h + + + + NSObject + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSNibLoading.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSOutlineView.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSPasteboard.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSSavePanel.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSTableView.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSToolbarItem.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSView.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSArchiver.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSClassDescription.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSError.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSFileManager.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSKeyValueCoding.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSKeyValueObserving.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSKeyedArchiver.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSObject.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSObjectScripting.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSPortCoder.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSRunLoop.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSScriptClassDescription.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSScriptKeyValueCoding.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSScriptObjectSpecifiers.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSScriptWhoseTests.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSThread.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSURL.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSURLConnection.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSURLDownload.h + + + + NSObject + + IBFrameworkSource + Sparkle.framework/Headers/SUAppcast.h + + + + NSObject + + IBFrameworkSource + Sparkle.framework/Headers/SUUpdater.h + + + + NSPopUpButton + NSButton + + IBFrameworkSource + AppKit.framework/Headers/NSPopUpButton.h + + + + NSPopUpButtonCell + NSMenuItemCell + + IBFrameworkSource + AppKit.framework/Headers/NSPopUpButtonCell.h + + + + NSResponder + + IBFrameworkSource + AppKit.framework/Headers/NSInterfaceStyle.h + + + + NSResponder + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSResponder.h + + + + NSSlider + NSControl + + IBFrameworkSource + AppKit.framework/Headers/NSSlider.h + + + + NSSliderCell + NSActionCell + + IBFrameworkSource + AppKit.framework/Headers/NSSliderCell.h + + + + NSTextField + NSControl + + IBFrameworkSource + AppKit.framework/Headers/NSTextField.h + + + + NSTextFieldCell + NSActionCell + + IBFrameworkSource + AppKit.framework/Headers/NSTextFieldCell.h + + + + NSUserDefaultsController + NSController + + IBFrameworkSource + AppKit.framework/Headers/NSUserDefaultsController.h + + + + NSView + + IBFrameworkSource + AppKit.framework/Headers/NSClipView.h + + + + NSView + + + + NSView + + IBFrameworkSource + AppKit.framework/Headers/NSRulerView.h + + + + NSView + NSResponder + + + + NSWindow + + IBFrameworkSource + AppKit.framework/Headers/NSDrawer.h + + + + NSWindow + NSResponder + + IBFrameworkSource + AppKit.framework/Headers/NSWindow.h + + + + NSWindow + + IBFrameworkSource + AppKit.framework/Headers/NSWindowScripting.h + + + + NSWindowController + NSResponder + + showWindow: + id + + + IBFrameworkSource + AppKit.framework/Headers/NSWindowController.h + + + + + 0 + + com.apple.InterfaceBuilder.CocoaPlugin.macosx + + + + com.apple.InterfaceBuilder.CocoaPlugin.InterfaceBuilder3 + + + YES + ../dupeguru.xcodeproj + 3 + + From 024e3c380f45b78c3084e66c20b74ce79c3fdabf Mon Sep 17 00:00:00 2001 From: hsoft Date: Fri, 30 Oct 2009 14:45:38 +0000 Subject: [PATCH 228/275] dgse cocoa: Adjusted to the dgme change (applicationDidFinishLaunching: pushed down and column removal in xib). --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40234 --- se/cocoa/AppDelegate.h | 3 --- se/cocoa/AppDelegate.m | 17 ----------------- se/cocoa/ResultWindow.m | 8 +++++--- 3 files changed, 5 insertions(+), 23 deletions(-) diff --git a/se/cocoa/AppDelegate.h b/se/cocoa/AppDelegate.h index 43264f9e..602b6038 100644 --- a/se/cocoa/AppDelegate.h +++ b/se/cocoa/AppDelegate.h @@ -8,14 +8,11 @@ http://www.hardcoded.net/licenses/hs_license #import #import "dgbase/AppDelegate.h" -#import "ResultWindow.h" #import "DirectoryPanel.h" #import "PyDupeGuru.h" @interface AppDelegate : AppDelegateBase { - IBOutlet ResultWindow *result; - DirectoryPanel *_directoryPanel; } - (IBAction)openWebsite:(id)sender; diff --git a/se/cocoa/AppDelegate.m b/se/cocoa/AppDelegate.m index 746d8abf..dbb7d071 100644 --- a/se/cocoa/AppDelegate.m +++ b/se/cocoa/AppDelegate.m @@ -65,23 +65,6 @@ http://www.hardcoded.net/licenses/hs_license - (PyDupeGuru *)py { return (PyDupeGuru *)py; } //Delegate -- (void)applicationDidFinishLaunching:(NSNotification *)aNotification -{ - [[ProgressController mainProgressController] setWorker:py]; - NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; - //Restore Columns - NSArray *columnsOrder = [ud arrayForKey:@"columnsOrder"]; - NSDictionary *columnsWidth = [ud dictionaryForKey:@"columnsWidth"]; - if ([columnsOrder count]) - [result restoreColumnsPosition:columnsOrder widths:columnsWidth]; - //Reg stuff - if ([RegistrationInterface showNagWithApp:[self py] name:APPNAME limitDescription:LIMIT_DESC]) - [unlockMenuItem setTitle:@"Thanks for buying dupeGuru!"]; - //Restore results - [py loadIgnoreList]; - [py loadResults]; -} - - (void)applicationWillBecomeActive:(NSNotification *)aNotification { if (![[result window] isVisible]) diff --git a/se/cocoa/ResultWindow.m b/se/cocoa/ResultWindow.m index 55a30386..7f70d11a 100644 --- a/se/cocoa/ResultWindow.m +++ b/se/cocoa/ResultWindow.m @@ -216,12 +216,14 @@ http://www.hardcoded.net/licenses/hs_license NSTableColumn *refCol = [matches tableColumnWithIdentifier:@"0"]; _resultColumns = [[NSMutableArray alloc] init]; [_resultColumns addObject:[matches tableColumnWithIdentifier:@"0"]]; // File Name - [_resultColumns addObject:[matches tableColumnWithIdentifier:@"1"]]; // Directory - [_resultColumns addObject:[matches tableColumnWithIdentifier:@"2"]]; // Size + [_resultColumns addObject:[self getColumnForIdentifier:1 title:@"Directory" width:120 refCol:refCol]]; + NSTableColumn *sizeCol = [self getColumnForIdentifier:2 title:@"Size (KB)" width:63 refCol:refCol]; + [[sizeCol dataCell] setAlignment:NSRightTextAlignment]; + [_resultColumns addObject:sizeCol]; [_resultColumns addObject:[self getColumnForIdentifier:3 title:@"Kind" width:40 refCol:refCol]]; [_resultColumns addObject:[self getColumnForIdentifier:4 title:@"Creation" width:120 refCol:refCol]]; [_resultColumns addObject:[self getColumnForIdentifier:5 title:@"Modification" width:120 refCol:refCol]]; - [_resultColumns addObject:[matches tableColumnWithIdentifier:@"6"]]; // Match % + [_resultColumns addObject:[self getColumnForIdentifier:6 title:@"Match %" width:60 refCol:refCol]]; [_resultColumns addObject:[self getColumnForIdentifier:7 title:@"Words Used" width:120 refCol:refCol]]; [_resultColumns addObject:[self getColumnForIdentifier:8 title:@"Dupe Count" width:80 refCol:refCol]]; } From 6d5f6a0c3c0d6414467b2a4362ed1330cf06249a Mon Sep 17 00:00:00 2001 From: hsoft Date: Fri, 30 Oct 2009 15:26:09 +0000 Subject: [PATCH 229/275] dgpe cocoa: extracted the pref pane from MainMenu.xib into Preferences.xib, Removed code duplicated with dgbase. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40235 --- pe/cocoa/AppDelegate.h | 3 - pe/cocoa/AppDelegate.m | 19 +- pe/cocoa/ResultWindow.h | 11 - pe/cocoa/ResultWindow.m | 79 +- pe/cocoa/dupeguru.xcodeproj/project.pbxproj | 12 +- pe/cocoa/xib/MainMenu.xib | 5494 ------------------- pe/cocoa/xib/Preferences.xib | 1650 ++++++ 7 files changed, 1671 insertions(+), 5597 deletions(-) delete mode 100644 pe/cocoa/xib/MainMenu.xib create mode 100644 pe/cocoa/xib/Preferences.xib diff --git a/pe/cocoa/AppDelegate.h b/pe/cocoa/AppDelegate.h index 498281c4..f4093d9f 100644 --- a/pe/cocoa/AppDelegate.h +++ b/pe/cocoa/AppDelegate.h @@ -8,15 +8,12 @@ http://www.hardcoded.net/licenses/hs_license #import #import "dgbase/AppDelegate.h" -#import "ResultWindow.h" #import "DirectoryPanel.h" #import "DetailsPanel.h" #import "PyDupeGuru.h" @interface AppDelegate : AppDelegateBase { - IBOutlet ResultWindow *result; - DetailsPanel *_detailsPanel; DirectoryPanel *_directoryPanel; } diff --git a/pe/cocoa/AppDelegate.m b/pe/cocoa/AppDelegate.m index fdb5cb45..339bdd12 100644 --- a/pe/cocoa/AppDelegate.m +++ b/pe/cocoa/AppDelegate.m @@ -75,19 +75,12 @@ http://www.hardcoded.net/licenses/hs_license //Delegate - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { - [[ProgressController mainProgressController] setWorker:py]; - NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; - //Restore Columns - NSArray *columnsOrder = [ud arrayForKey:@"columnsOrder"]; - NSDictionary *columnsWidth = [ud dictionaryForKey:@"columnsWidth"]; - if ([columnsOrder count]) - [result restoreColumnsPosition:columnsOrder widths:columnsWidth]; - //Reg stuff - if ([RegistrationInterface showNagWithApp:[self py] name:APPNAME limitDescription:LIMIT_DESC]) - [unlockMenuItem setTitle:@"Thanks for buying dupeGuru Picture Edition!"]; - //Restore results - [py loadIgnoreList]; - [py loadResults]; + NSMenu *actionsMenu = [[[NSApp mainMenu] itemWithTitle:@"Actions"] submenu]; + // index 2 is just after "Clear Ingore List" + NSMenuItem *mi = [actionsMenu insertItemWithTitle:@"Clear Picture Cache" action:@selector(clearPictureCache:) keyEquivalent:@"P" atIndex:2]; + [mi setTarget:result]; + [mi setKeyEquivalentModifierMask:NSCommandKeyMask|NSShiftKeyMask]; + [super applicationDidFinishLaunching:aNotification]; } - (void)applicationWillBecomeActive:(NSNotification *)aNotification diff --git a/pe/cocoa/ResultWindow.h b/pe/cocoa/ResultWindow.h index fd3bf08f..7ebc3434 100644 --- a/pe/cocoa/ResultWindow.h +++ b/pe/cocoa/ResultWindow.h @@ -9,15 +9,11 @@ http://www.hardcoded.net/licenses/hs_license #import #import "Outline.h" #import "dgbase/ResultWindow.h" -#import "DirectoryPanel.h" @interface ResultWindow : ResultWindowBase { - IBOutlet NSMenu *columnsMenu; IBOutlet NSSearchField *filterField; - IBOutlet NSWindow *preferencesPanel; - NSMutableArray *_resultColumns; NSMutableIndexSet *_deltaColumns; } - (IBAction)clearIgnoreList:(id)sender; @@ -34,16 +30,9 @@ http://www.hardcoded.net/licenses/hs_license - (IBAction)removeMarked:(id)sender; - (IBAction)removeSelected:(id)sender; - (IBAction)renameSelected:(id)sender; -- (IBAction)resetColumnsToDefault:(id)sender; - (IBAction)revealSelected:(id)sender; -- (IBAction)showPreferencesPanel:(id)sender; - (IBAction)startDuplicateScan:(id)sender; -- (IBAction)toggleColumn:(id)sender; - (IBAction)toggleDelta:(id)sender; - (IBAction)toggleDetailsPanel:(id)sender; - (IBAction)toggleDirectories:(id)sender; - -- (NSTableColumn *)getColumnForIdentifier:(int)aIdentifier title:(NSString *)aTitle width:(int)aWidth refCol:(NSTableColumn *)aColumn; -- (void)initResultColumns; -- (void)restoreColumnsPosition:(NSArray *)aColumnsOrder widths:(NSDictionary *)aColumnsWidth; @end diff --git a/pe/cocoa/ResultWindow.m b/pe/cocoa/ResultWindow.m index 5bfa92e2..4c52a433 100644 --- a/pe/cocoa/ResultWindow.m +++ b/pe/cocoa/ResultWindow.m @@ -19,6 +19,7 @@ http://www.hardcoded.net/licenses/hs_license - (void)awakeFromNib { [super awakeFromNib]; + [[self window] setTitle:@"dupeGuru Picture Edition"]; _displayDelta = NO; _powerMode = NO; _deltaColumns = [[NSMutableIndexSet indexSetWithIndexesInRange:NSMakeRange(2,5)] retain]; @@ -29,7 +30,6 @@ http://www.hardcoded.net/licenses/hs_license [py setDisplayDeltaValues:b2n(_displayDelta)]; [matches setTarget:self]; [matches setDoubleAction:@selector(openSelected:)]; - [self initResultColumns]; [self refreshStats]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(resultsMarkingChanged:) name:ResultsMarkingChangedNotification object:nil]; } @@ -156,7 +156,7 @@ http://www.hardcoded.net/licenses/hs_license [columnsOrder addObject:@"4"]; [columnsOrder addObject:@"7"]; NSMutableDictionary *columnsWidth = [NSMutableDictionary dictionary]; - [columnsWidth setObject:i2n(125) forKey:@"0"]; + [columnsWidth setObject:i2n(121) forKey:@"0"]; [columnsWidth setObject:i2n(120) forKey:@"1"]; [columnsWidth setObject:i2n(63) forKey:@"2"]; [columnsWidth setObject:i2n(73) forKey:@"4"]; @@ -170,11 +170,6 @@ http://www.hardcoded.net/licenses/hs_license [py revealSelected]; } -- (IBAction)showPreferencesPanel:(id)sender -{ - [preferencesPanel makeKeyAndOrderFront:sender]; -} - - (IBAction)startDuplicateScan:(id)sender { if ([matches numberOfRows] > 0) @@ -201,26 +196,6 @@ http://www.hardcoded.net/licenses/hs_license } } -- (IBAction)toggleColumn:(id)sender -{ - NSMenuItem *mi = sender; - NSString *colId = [NSString stringWithFormat:@"%d",[mi tag]]; - NSTableColumn *col = [matches tableColumnWithIdentifier:colId]; - if (col == nil) - { - //Add Column - col = [_resultColumns objectAtIndex:[mi tag]]; - [matches addTableColumn:col]; - [mi setState:NSOnState]; - } - else - { - //Remove column - [matches removeTableColumn:col]; - [mi setState:NSOffState]; - } -} - - (IBAction)toggleDelta:(id)sender { if ([deltaSwitch selectedSegment] == 1) @@ -242,63 +217,23 @@ http://www.hardcoded.net/licenses/hs_license } /* Public */ -- (NSTableColumn *)getColumnForIdentifier:(int)aIdentifier title:(NSString *)aTitle width:(int)aWidth refCol:(NSTableColumn *)aColumn -{ - NSNumber *n = [NSNumber numberWithInt:aIdentifier]; - NSTableColumn *col = [[NSTableColumn alloc] initWithIdentifier:[n stringValue]]; - [col setWidth:aWidth]; - [col setEditable:NO]; - [[col dataCell] setFont:[[aColumn dataCell] font]]; - [[col headerCell] setStringValue:aTitle]; - [col setResizingMask:NSTableColumnUserResizingMask]; - [col setSortDescriptorPrototype:[[NSSortDescriptor alloc] initWithKey:[n stringValue] ascending:YES]]; - return col; -} - - (void)initResultColumns { NSTableColumn *refCol = [matches tableColumnWithIdentifier:@"0"]; _resultColumns = [[NSMutableArray alloc] init]; [_resultColumns addObject:[matches tableColumnWithIdentifier:@"0"]]; // File Name - [_resultColumns addObject:[matches tableColumnWithIdentifier:@"1"]]; // Directory - [_resultColumns addObject:[matches tableColumnWithIdentifier:@"2"]]; // Size + [_resultColumns addObject:[self getColumnForIdentifier:1 title:@"Directory" width:120 refCol:refCol]]; + NSTableColumn *sizeCol = [self getColumnForIdentifier:2 title:@"Size (KB)" width:63 refCol:refCol]; + [[sizeCol dataCell] setAlignment:NSRightTextAlignment]; + [_resultColumns addObject:sizeCol]; [_resultColumns addObject:[self getColumnForIdentifier:3 title:@"Kind" width:40 refCol:refCol]]; [_resultColumns addObject:[self getColumnForIdentifier:4 title:@"Dimensions" width:80 refCol:refCol]]; [_resultColumns addObject:[self getColumnForIdentifier:5 title:@"Creation" width:120 refCol:refCol]]; [_resultColumns addObject:[self getColumnForIdentifier:6 title:@"Modification" width:120 refCol:refCol]]; - [_resultColumns addObject:[matches tableColumnWithIdentifier:@"7"]]; // Match % + [_resultColumns addObject:[self getColumnForIdentifier:7 title:@"Match %" width:58 refCol:refCol]]; [_resultColumns addObject:[self getColumnForIdentifier:8 title:@"Dupe Count" width:80 refCol:refCol]]; } -- (void)restoreColumnsPosition:(NSArray *)aColumnsOrder widths:(NSDictionary *)aColumnsWidth -{ - NSTableColumn *col; - NSString *colId; - NSNumber *width; - NSMenuItem *mi; - //Remove all columns - NSEnumerator *e = [[columnsMenu itemArray] objectEnumerator]; - while (mi = [e nextObject]) - { - if ([mi state] == NSOnState) - [self toggleColumn:mi]; - } - //Add columns and set widths - e = [aColumnsOrder objectEnumerator]; - while (colId = [e nextObject]) - { - if (![colId isEqual:@"mark"]) - { - col = [_resultColumns objectAtIndex:[colId intValue]]; - width = [aColumnsWidth objectForKey:[col identifier]]; - mi = [columnsMenu itemWithTag:[colId intValue]]; - if (width) - [col setWidth:[width floatValue]]; - [self toggleColumn:mi]; - } - } -} - /* Delegate */ - (void)outlineView:(NSOutlineView *)outlineView willDisplayCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn item:(id)item { diff --git a/pe/cocoa/dupeguru.xcodeproj/project.pbxproj b/pe/cocoa/dupeguru.xcodeproj/project.pbxproj index ec81dfc9..eeb21896 100644 --- a/pe/cocoa/dupeguru.xcodeproj/project.pbxproj +++ b/pe/cocoa/dupeguru.xcodeproj/project.pbxproj @@ -9,6 +9,8 @@ /* Begin PBXBuildFile section */ 8D11072D0486CEB800E47090 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 29B97316FDCFA39411CA2CEA /* main.m */; settings = {ATTRIBUTES = (); }; }; 8D11072F0486CEB800E47090 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */; }; + CE031751109B340A00517EE6 /* Preferences.xib in Resources */ = {isa = PBXBuildFile; fileRef = CE031750109B340A00517EE6 /* Preferences.xib */; }; + CE031754109B345200517EE6 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = CE031753109B345200517EE6 /* MainMenu.xib */; }; CE073F6309CAE1A3005C1D2F /* dupeguru_pe_help in Resources */ = {isa = PBXBuildFile; fileRef = CE073F5409CAE1A3005C1D2F /* dupeguru_pe_help */; }; CE0C46AA0FA0647E000BE99B /* PictureBlocks.m in Sources */ = {isa = PBXBuildFile; fileRef = CE0C46A90FA0647E000BE99B /* PictureBlocks.m */; }; CE15C8A80ADEB8B50061D4A5 /* Sparkle.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE15C8A70ADEB8B50061D4A5 /* Sparkle.framework */; }; @@ -20,7 +22,6 @@ CE68EE6809ABC48000971085 /* DirectoryPanel.m in Sources */ = {isa = PBXBuildFile; fileRef = CE68EE6609ABC48000971085 /* DirectoryPanel.m */; }; CE6E0F3D1054EC62008D9390 /* dsa_pub.pem in Resources */ = {isa = PBXBuildFile; fileRef = CE6E0F3C1054EC62008D9390 /* dsa_pub.pem */; }; CE77C89E10946C6D0078B0DB /* DirectoryPanel.xib in Resources */ = {isa = PBXBuildFile; fileRef = CE77C89C10946C6D0078B0DB /* DirectoryPanel.xib */; }; - CE77C8A110946C840078B0DB /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = CE77C8A010946C840078B0DB /* MainMenu.xib */; }; CE77C8A810946CE20078B0DB /* DetailsPanel.xib in Resources */ = {isa = PBXBuildFile; fileRef = CE77C8A710946CE20078B0DB /* DetailsPanel.xib */; }; CE80DB2E0FC192D60086DCA6 /* Dialogs.m in Sources */ = {isa = PBXBuildFile; fileRef = CE80DB1C0FC192D60086DCA6 /* Dialogs.m */; }; CE80DB2F0FC192D60086DCA6 /* HSErrorReportWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = CE80DB1E0FC192D60086DCA6 /* HSErrorReportWindow.m */; }; @@ -74,6 +75,8 @@ 29B97325FDCFA39411CA2CEA /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = /System/Library/Frameworks/Foundation.framework; sourceTree = ""; }; 8D1107310486CEB800E47090 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = SOURCE_ROOT; }; 8D1107320486CEB800E47090 /* dupeGuru PE.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "dupeGuru PE.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + CE031750109B340A00517EE6 /* Preferences.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Preferences.xib; path = ../../xib/Preferences.xib; sourceTree = ""; }; + CE031753109B345200517EE6 /* MainMenu.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = MainMenu.xib; sourceTree = ""; }; CE073F5409CAE1A3005C1D2F /* dupeguru_pe_help */ = {isa = PBXFileReference; lastKnownFileType = folder; name = dupeguru_pe_help; path = help/dupeguru_pe_help; sourceTree = SOURCE_ROOT; }; CE0C46A80FA0647E000BE99B /* PictureBlocks.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PictureBlocks.h; sourceTree = ""; }; CE0C46A90FA0647E000BE99B /* PictureBlocks.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PictureBlocks.m; sourceTree = ""; }; @@ -89,7 +92,6 @@ CE68EE6609ABC48000971085 /* DirectoryPanel.m */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.objc; path = DirectoryPanel.m; sourceTree = SOURCE_ROOT; }; CE6E0F3C1054EC62008D9390 /* dsa_pub.pem */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = dsa_pub.pem; path = dgbase/dsa_pub.pem; sourceTree = ""; }; CE77C89C10946C6D0078B0DB /* DirectoryPanel.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = DirectoryPanel.xib; sourceTree = ""; }; - CE77C8A010946C840078B0DB /* MainMenu.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = MainMenu.xib; path = ../../xib/MainMenu.xib; sourceTree = ""; }; CE77C8A710946CE20078B0DB /* DetailsPanel.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = DetailsPanel.xib; path = ../../xib/DetailsPanel.xib; sourceTree = ""; }; CE80DB1B0FC192D60086DCA6 /* Dialogs.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Dialogs.h; path = cocoalib/Dialogs.h; sourceTree = SOURCE_ROOT; }; CE80DB1C0FC192D60086DCA6 /* Dialogs.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Dialogs.m; path = cocoalib/Dialogs.m; sourceTree = SOURCE_ROOT; }; @@ -247,9 +249,10 @@ CE77C89A10946C6D0078B0DB /* xib */ = { isa = PBXGroup; children = ( - CE77C8A010946C840078B0DB /* MainMenu.xib */, + CE031753109B345200517EE6 /* MainMenu.xib */, CE77C8A710946CE20078B0DB /* DetailsPanel.xib */, CE77C89C10946C6D0078B0DB /* DirectoryPanel.xib */, + CE031750109B340A00517EE6 /* Preferences.xib */, ); name = xib; path = dgbase/xib; @@ -385,8 +388,9 @@ CE80DB780FC194760086DCA6 /* registration.nib in Resources */, CE6E0F3D1054EC62008D9390 /* dsa_pub.pem in Resources */, CE77C89E10946C6D0078B0DB /* DirectoryPanel.xib in Resources */, - CE77C8A110946C840078B0DB /* MainMenu.xib in Resources */, CE77C8A810946CE20078B0DB /* DetailsPanel.xib in Resources */, + CE031751109B340A00517EE6 /* Preferences.xib in Resources */, + CE031754109B345200517EE6 /* MainMenu.xib in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/pe/cocoa/xib/MainMenu.xib b/pe/cocoa/xib/MainMenu.xib deleted file mode 100644 index 91c26344..00000000 --- a/pe/cocoa/xib/MainMenu.xib +++ /dev/null @@ -1,5494 +0,0 @@ - - - - 1050 - 10B504 - 740 - 1038.2 - 437.00 - - com.apple.InterfaceBuilder.CocoaPlugin - 740 - - - - - - com.apple.InterfaceBuilder.CocoaPlugin - - - - - NSApplication - - - FirstResponder - - - NSApplication - - - 15 - 2 - {{47, 310}, {557, 400}} - 1886912512 - dupeGuru Picture Edition - NSWindow - - - 2C450DD5-50E5-4BF2-9ED3-6113712C18E6 - - - YES - YES - YES - YES - 1 - 1 - - - - 092EDA0C-232E-4EC4-9334-68F62220C787 - - Directories - Directories - - - - NSImage - folder32 - - - - {0, 0} - {0, 0} - YES - YES - -1 - YES - 0 - - - - 0A3C1C35-6A27-453D-A627-C7ADA6CC26B3 - - Action - Action - - - - 256 - {{0, 14}, {58, 26}} - YES - - -2076049856 - 2048 - - LucidaGrande - 13 - 1044 - - - 109199615 - 1 - - - - - 400 - 75 - - - YES - IA - - 1048576 - 2147483647 - 1 - - NSImage - NSActionTemplate - - - NSImage - NSMenuCheckmark - - - NSImage - NSMenuMixedState - - _popUpItemAction: - - - YES - - - OtherViews - - - - - - Send Marked to Trash - - 2147483647 - - - _popUpItemAction: - - - - - Move Marked to... - - 2147483647 - - - _popUpItemAction: - - - - - Copy Marked to... - - 2147483647 - - - _popUpItemAction: - - - - - Remove Marked from Results - - 2147483647 - - - _popUpItemAction: - - - - - YES - YES - - - 1048576 - 2147483647 - - - _popUpItemAction: - - - - - Remove Selected from Results - - 2147483647 - - - _popUpItemAction: - - - - - Add Selected to Ignore List - - 1048576 - 2147483647 - - - _popUpItemAction: - - - - - Make Selected Reference - - 2147483647 - - - _popUpItemAction: - - - - - YES - YES - - - 1048576 - 2147483647 - - - _popUpItemAction: - - - - - Open Selected with Default Application - - 2147483647 - - - _popUpItemAction: - - - - - Reveal Selected in Finder - - 2147483647 - - - _popUpItemAction: - - - - - Rename Selected - - 1048576 - 2147483647 - - - _popUpItemAction: - - - - - YES - 3 - YES - YES - 1 - - - - - - {58, 26} - {58, 26} - YES - YES - 0 - YES - 0 - - - - 26033E1E-95A5-4838-8727-1306C619AB37 - - Power Marker - Power Marker - - - - 256 - {{7, 14}, {67, 24}} - YES - - 67239424 - 0 - - LucidaGrande - 11 - 3100 - - - - - 30 - Off - 2 - - - 30 - On - 1 - 2 - - - 1 - - - - - - {67, 24} - {67, 24} - YES - YES - 0 - YES - 0 - - - - 45C339F5-66AC-4590-A395-8BF09951F9DE - - Preferences - Preferences - - - - NSImage - preferences32 - - - - {0, 0} - {0, 0} - YES - YES - -1 - YES - 0 - - - - 67845041-FE64-472F-B902-4364CE189365 - - Start Scanning - Start Scanning - - - - NSImage - dgpe_logo_32 - - - - {0, 0} - {0, 0} - YES - YES - -1 - YES - 0 - - - - 95D92C39-45B7-4C65-8B1C-F33A620227C3 - - Details - Details - - - - NSImage - details32 - - - - {0, 0} - {0, 0} - YES - YES - -1 - YES - 0 - - - - E1E3467D-6276-4BE7-8AEE-942275EEE54D - - Filter - Filter - - - - 258 - {{0, 14}, {81, 22}} - YES - - 343014976 - 268436480 - - - Filter - - YES - 1 - - 6 - System - textBackgroundColor - - 3 - MQA - - - - 6 - System - controlTextColor - - 3 - MAA - - - - 130560 - 0 - search - - _searchFieldSearch: - - 138690815 - 0 - - 400 - 75 - - - 130560 - 0 - clear - - - cancel - - - - - _searchFieldCancel: - - 138690815 - 0 - - 400 - 75 - - 10 - YES - - - - - - {81, 22} - {9999, 22} - YES - YES - 0 - YES - 0 - - - - FF475101-7082-44F5-8CF5-65A1ECE54BD2 - - Delta Values - Delta Values - - - - 256 - {{4, 14}, {67, 24}} - YES - - 67239424 - 0 - - - - - 30 - Off - 2 - - - 30 - On - 1 - 2 - - - 1 - - - - - - {67, 24} - {67, 24} - YES - YES - 0 - YES - 0 - - - NSToolbarFlexibleSpaceItem - - Flexible Space - - - - - - {1, 5} - {20000, 32} - YES - YES - -1 - YES - 0 - - YES - YES - - - 1048576 - 2147483647 - - - - - - NSToolbarSeparatorItem - - Separator - - - - - - {12, 5} - {12, 1000} - YES - YES - -1 - YES - 0 - - YES - YES - - - 1048576 - 2147483647 - - - - - - NSToolbarSpaceItem - - Space - - - - - - {32, 5} - {32, 32} - YES - YES - -1 - YES - 0 - - YES - YES - - - 1048576 - 2147483647 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {1.79769e+308, 1.79769e+308} - {340, 340} - - - 256 - - - - 274 - - - - 2304 - - - - 274 - {515, 317} - - YES - - - 256 - {515, 17} - - - - - - -2147483392 - {{-26, 0}, {16, 17}} - - - - - mark - 47 - 16 - 1000 - - 75628096 - 2048 - - - - 6 - System - headerColor - - - - 6 - System - headerTextColor - - - - - 67239424 - 131072 - - - LucidaGrande - 12 - 16 - - - 1211912703 - 2 - - NSSwitch - - - - 400 - 75 - - - - - 0 - 202 - 16 - 1000 - - 75628096 - 2048 - Name - - - 3 - MC4zMzMzMzI5OQA - - - - - 337772096 - 2048 - - - - 6 - System - controlBackgroundColor - - 3 - MC42NjY2NjY2NjY3AA - - - - - 3 - YES - - - 0 - YES - compare: - - - - 1 - 128 - 10 - 1000 - - 75628096 - 2048 - Directory - - - - - - 337772096 - 2048 - - - - - - 3 - YES - - - 1 - YES - compare: - - - - 2 - 63 - 10 - 1000 - - 75628096 - 2048 - Size (KB) - - - - - - 337772096 - 67110912 - - - - - - 2 - YES - - - 2 - YES - compare: - - - - 7 - 59.9580078125 - 46.9580078125 - 1000 - - 75628096 - 2048 - Match % - - - - - - 337772096 - 2048 - - - - - - 2 - YES - - - 7 - YES - compare: - - - - 3 - 2 - - - 6 - System - gridColor - - 3 - MC41AA - - - 14 - -901742592 - - - 2 - 1 - 15 - 0 - YES - 0 - - - {{1, 17}, {515, 317}} - - - - - 4 - - - - -2147483392 - {{-30, 17}, {15, 302}} - - - _doScroller: - 0.98739492893218994 - - - - -2147483392 - {{1, -30}, {500, 15}} - - 1 - - _doScroller: - 0.99806201457977295 - - - - 2304 - - - - {{1, 0}, {515, 17}} - - - - - 4 - - - - {{20, 45}, {517, 335}} - - - 562 - - - - - - QSAAAEEgAABBgAAAQYAAAA - - - - 290 - {{17, 20}, {523, 17}} - - YES - - 67239424 - 138412032 - Marked: 0 files, 0 B. Total: 0 files, 0 B. - - - - 6 - System - controlColor - - - - - - - {557, 400} - - - {{0, 0}, {1440, 878}} - {340, 418} - {1.79769e+308, 1.79769e+308} - MainWindow - - - MainMenu - - - - dupeGuru PE - - 1048576 - 2147483647 - - - submenuAction: - - dupeGuru PE - - - - About dupeGuru PE - - 2147483647 - - - - - - Unlock dupeGuru PE - - 1048576 - 2147483647 - - - - - - Check for update... - - 1048576 - 2147483647 - - - - - - YES - YES - - - 1048576 - 2147483647 - - - - - - Preferences... - , - 1048576 - 2147483647 - - - - - - YES - YES - - - 1048576 - 2147483647 - - - - - - Hide dupeGuru PE - h - 1048576 - 2147483647 - - - - - - Hide Others - h - 1572864 - 2147483647 - - - - - - Show All - - 1048576 - 2147483647 - - - - - - YES - YES - - - 1048576 - 2147483647 - - - - - - Quit dupeGuru PE - q - 1048576 - 2147483647 - - - - - _NSAppleMenu - - - - - Edit - - 1048576 - 2147483647 - - - submenuAction: - - - Edit - - - - - Mark All - a - 1048576 - 2147483647 - - - - - - Mark None - A - 1048576 - 2147483647 - - - - - - Invert Marking - a - 1572864 - 2147483647 - - - - - - Mark Selected - a - 1310720 - 2147483647 - - - - - - YES - YES - - - 1048576 - 2147483647 - - - - - - Cut - x - 1048576 - 2147483647 - - - - - - Copy - c - 1048576 - 2147483647 - - - - - - Paste - v - 1048576 - 2147483647 - - - - - - - - - Actions - - 1048576 - 2147483647 - - - submenuAction: - - Actions - - - - Start Duplicate Scan - s - 1048576 - 2147483647 - - - - - - Clear Ignore List - I - 1048576 - 2147483647 - - - - - - Clear Picture Cache - P - 1048576 - 2147483647 - - - - - - Export Results to XHTML - E - 1048576 - 2147483647 - - - - - - YES - YES - - - 1048576 - 2147483647 - - - - - - Send Marked to Trash - t - 1048576 - 2147483647 - - - - - - Move Marked to... - m - 1048576 - 2147483647 - - - - - - Copy Marked to... - m - 1572864 - 2147483647 - - - - - - Remove Marked from Results - r - 1048576 - 2147483647 - - - - - - YES - YES - - - 1048576 - 2147483647 - - - - - - Remove Selected from Results - R - 1048576 - 2147483647 - - - - - - Add Selected to Ignore List - i - 1048576 - 2147483647 - - - - - - Make Selected Reference -  - 1048576 - 2147483647 - - - - - - YES - YES - - - 1048576 - 2147483647 - - - - - - Open Selected with Default Application - DQ - 1048576 - 2147483647 - - - - - - Reveal Selected in Finder - DQ - 1572864 - 2147483647 - - - - - - Rename Selected - Aw - 2147483647 - - - - - - - - - Columns - - 1048576 - 2147483647 - - - submenuAction: - - Columns - - - - File Name - - 1048576 - 2147483647 - 1 - - - - - - Directory - - 1048576 - 2147483647 - 1 - - - 1 - - - - Size - - 1048576 - 2147483647 - 1 - - - 2 - - - - Kind - - 1048576 - 2147483647 - - - 3 - - - - Dimensions - - 1048576 - 2147483647 - - - 4 - - - - Creation - - 1048576 - 2147483647 - - - 5 - - - - Modification - - 1048576 - 2147483647 - - - 6 - - - - Match % - - 1048576 - 2147483647 - 1 - - - 7 - - - - Dupe Count - - 1048576 - 2147483647 - - - 8 - - - - YES - YES - IA - - 1048576 - 2147483647 - - - -1 - - - - Reset to Default - - 1048576 - 2147483647 - - - -1 - - - - - - - Modes - - 1048576 - 2147483647 - - - submenuAction: - - Modes - - - - Power Marker - 1 - 1048576 - 2147483647 - - - - - - Delta Values - 2 - 1048576 - 2147483647 - - - - - - - - - Window - - 1048576 - 2147483647 - - - submenuAction: - - Window - - - - Directory Panel - 3 - 1048576 - 2147483647 - - - - - - Details Panel - 4 - 1048576 - 2147483647 - - - - - - YES - YES - - - 1048576 - 2147483647 - - - - - - Minimize - - 2147483647 - - - - - - Zoom - - 1048576 - 2147483647 - - - - - - Close Window - w - 1048576 - 2147483647 - - - - - - YES - YES - - - 1048576 - 2147483647 - - - - - - Bring All to Front - - 1048576 - 2147483647 - - - - - _NSWindowsMenu - - - - - Help - - 1048576 - 2147483647 - - - submenuAction: - - Help - - - - dupeGuru PE Help - ? - 1048576 - 2147483647 - - - - - - dupeGuru PE Website - - 1048576 - 2147483647 - - - - - - - - _NSMainMenu - - - AppDelegate - - - ResultWindow - - - YES - - - RecentDirectories - - - 3 - 2 - {{92, 350}, {352, 252}} - 1886912512 - dupeGuru PE Preferences - - NSWindow - - - View - - {1.79769e+308, 1.79769e+308} - {213, 107} - - - 256 - - - - 292 - {{120, 213}, {181, 21}} - - YES - - 67239424 - 0 - - - - - Helvetica - 12 - 16 - - - 100 - 1 - 80 - 0.0 - 0 - 1 - NO - NO - - - - - 292 - {{122, 196}, {80, 13}} - - YES - - 67239424 - 272629760 - More results - - LucidaGrande - 10 - 2843 - - - - - - - - - 289 - {{219, 196}, {80, 13}} - - YES - - 67239424 - 71303168 - Less results - - - - - - - - - 292 - {{17, 218}, {100, 14}} - - YES - - 67239424 - 272629760 - Filter hardness: - - - - - - - - - 256 - {{18, 152}, {214, 18}} - - YES - - 67239424 - 0 - Can mix file kind - - - 1211912703 - 2 - - - - 200 - 25 - - - - - 256 - {{304, 218}, {31, 14}} - - YES - - 67239424 - -1874853888 - - - - - - - 0 - - - . - - , - -0 - 0 - - - 0 - -0 - - - - - - - - NaN - - - - 0 - 0 - YES - NO - 1 - AAAAAAAAAAAAAAAAAAAAAA - - - - . - , - NO - YES - YES - - - - - - - - - 256 - {{190, 16}, {148, 32}} - - YES - - 67239424 - 134217728 - Reset to Defaults - - - -2038284033 - 1 - - - - - - 200 - 25 - - - - - 292 - {{20, 71}, {85, 13}} - - YES - - 67239424 - 272629760 - Copy and Move: - - - - - - - - - 292 - {{110, 60}, {216, 26}} - - YES - - -2076049856 - 2048 - - - 109199615 - 1 - - - - - - 400 - 75 - - - Right in destination - - 1048576 - 2147483647 - 1 - - - _popUpItemAction: - - - YES - - - OtherViews - - - - - - Recreate relative path - - 1048576 - 2147483647 - - - _popUpItemAction: - - - - - Recreate absolute path - - 1048576 - 2147483647 - - - _popUpItemAction: - - - - - 3 - YES - YES - 1 - - - - - 256 - {{18, 172}, {214, 18}} - - YES - - 67239424 - 0 - Match scaled pictures together - - - 1211912703 - 2 - - - - 200 - 25 - - - - - 256 - {{18, 90}, {283, 18}} - - YES - - 67239424 - 0 - Check for update on startup - - - 1211912703 - 2 - - - - 200 - 25 - - - - - 256 - {{18, 112}, {242, 18}} - - YES - - 67239424 - 0 - Remove empty folders on delete or move - - - 1211912703 - 2 - - - - 200 - 25 - - - - - 256 - {{18, 132}, {228, 18}} - - YES - - 67239424 - 0 - Use regular expressions when filtering - - - 1211912703 - 2 - - - - 200 - 25 - - - - {352, 252} - - {{0, 0}, {1440, 878}} - {213, 129} - {1.79769e+308, 1.79769e+308} - - - PyDupeGuru - - - Menu - - - - Remove Selected from Results - - 1048576 - 2147483647 - - - - - - Add Selected to Ignore List - - 1048576 - 2147483647 - - - - - - Make Selected Reference - - 1048576 - 2147483647 - - - - - - YES - YES - - - 1048576 - 2147483647 - - - - - - Open Selected with Default Application - - 1048576 - 2147483647 - - - - - - Reveal Selected in Finder - - 1048576 - 2147483647 - - - - - - Rename Selected - - 1048576 - 2147483647 - - - - - - - - SUUpdater - - - - - - - performMiniaturize: - - - - 37 - - - - arrangeInFront: - - - - 39 - - - - showHelp: - - - - 122 - - - - terminate: - - - - 139 - - - - orderFrontStandardAboutPanel: - - - - 142 - - - - hideOtherApplications: - - - - 146 - - - - hide: - - - - 152 - - - - unhideAllApplications: - - - - 153 - - - - performZoom: - - - - 198 - - - - delegate - - - - 207 - - - - delegate - - - - 208 - - - - window - - - - 210 - - - - result - - - - 211 - - - - delegate - - - - 212 - - - - matches - - - - 245 - - - - initialFirstResponder - - - - 279 - - - - delegate - - - - 410 - - - - markToggle: - - - - 414 - - - - stats - - - - 445 - - - - delegate - - - - 502 - - - - recentDirectories - - - - 503 - - - - makeKeyAndOrderFront: - - - - 543 - - - - value: values.minMatchPercentage - - - - - - value: values.minMatchPercentage - value - values.minMatchPercentage - 2 - - - 549 - - - - deleteMarked: - - - - 606 - - - - moveMarked: - - - - 607 - - - - copyMarked: - - - - 608 - - - - removeMarked: - - - - 609 - - - - switchSelected: - - - - 610 - - - - removeSelected: - - - - 611 - - - - py - - - - 614 - - - - py - - - - 616 - - - - toggleColumn: - - - - 627 - - - - toggleColumn: - - - - 628 - - - - toggleColumn: - - - - 629 - - - - toggleColumn: - - - - 630 - - - - toggleColumn: - - - - 631 - - - - toggleColumn: - - - - 632 - - - - toggleColumn: - - - - 633 - - - - toggleColumn: - - - - 647 - - - - value: values.mixFileKind - - - - - - value: values.mixFileKind - value - values.mixFileKind - 2 - - - 656 - - - - openSelected: - - - - 660 - - - - revealSelected: - - - - 661 - - - - menu - - - - 663 - - - - toggleColumn: - - - - 706 - - - - openSelected: - - - - 709 - - - - revealSelected: - - - - 711 - - - - value: values.minMatchPercentage - - - - - - value: values.minMatchPercentage - value - values.minMatchPercentage - 2 - - - 713 - - - - switchSelected: - - - - 716 - - - - preferencesPanel - - - - 718 - - - - actionMenu - - - - 726 - - - - deleteMarked: - - - - 741 - - - - moveMarked: - - - - 742 - - - - copyMarked: - - - - 743 - - - - removeMarked: - - - - 744 - - - - removeSelected: - - - - 745 - - - - switchSelected: - - - - 746 - - - - openSelected: - - - - 747 - - - - revealSelected: - - - - 748 - - - - unlockApp: - - - - 755 - - - - unlockMenuItem - - - - 756 - - - - app - - - - 757 - - - - toggleDirectories: - - - - 758 - - - - py - - - - 764 - - - - removeSelected: - - - - 873 - - - - changeDelta: - - - - 882 - - - - deltaSwitch - - - - 883 - - - - selectedIndex: values.recreatePathType - - - - - - selectedIndex: values.recreatePathType - selectedIndex - values.recreatePathType - 2 - - - 914 - - - - ignoreSelected: - - - - 921 - - - - ignoreSelected: - - - - 923 - - - - performClose: - - - - 925 - - - - startDuplicateScan: - - - - 929 - - - - clearIgnoreList: - - - - 930 - - - - revertToInitialValues: - - - - 932 - - - - renameSelected: - - - - 934 - - - - renameSelected: - - - - 936 - - - - ignoreSelected: - - - - 940 - - - - renameSelected: - - - - 941 - - - - resetColumnsToDefault: - - - - 945 - - - - columnsMenu - - - - 946 - - - - toggleDetailsPanel: - - - - 947 - - - - openWebsite: - - - - 949 - - - - value: values.matchScaled - - - - - - value: values.matchScaled - value - values.matchScaled - 2 - - - 951 - - - - clearPictureCache: - - - - 953 - - - - checkForUpdates: - - - - 956 - - - - value: values.SUCheckAtStartup - - - - - - value: values.SUCheckAtStartup - value - values.SUCheckAtStartup - 2 - - - 959 - - - - pmSwitch - - - - 963 - - - - changePowerMarker: - - - - 964 - - - - togglePowerMarker: - - - - 969 - - - - toggleDelta: - - - - 970 - - - - exportToXHTML: - - - - 972 - - - - paste: - - - - 1003 - - - - cut: - - - - 1004 - - - - copy: - - - - 1006 - - - - markAll: - - - - 1024 - - - - markNone: - - - - 1025 - - - - markInvert: - - - - 1026 - - - - markSelected: - - - - 1027 - - - - filter: - - - - 1030 - - - - filterField - - - - 1032 - - - - value: values.useRegexpFilter - - - - - - value: values.useRegexpFilter - value - values.useRegexpFilter - 2 - - - 1065 - - - - value: values.removeEmptyFolders - - - - - - value: values.removeEmptyFolders - value - values.removeEmptyFolders - 2 - - - 1066 - - - - nextKeyView - - - - 1067 - - - - nextKeyView - - - - 1068 - - - - nextKeyView - - - - 1069 - - - - nextKeyView - - - - 1070 - - - - nextKeyView - - - - 1071 - - - - nextKeyView - - - - 1072 - - - - nextKeyView - - - - 1073 - - - - startDuplicateScan: - - - - 1117 - - - - toggleDirectories: - - - - 1118 - - - - toggleDetailsPanel: - - - - 1119 - - - - showPreferencesPanel: - - - - 1120 - - - - - - 0 - - - - - - -2 - - - File's Owner - - - -1 - - - First Responder - - - -3 - - - Application - - - 21 - - - - - - - Window - - - 2 - - - - - - - - - 219 - - - - - - - - - - - 220 - - - - - - - - - - - - 222 - - - - - - - - 223 - - - - - - - - 233 - - - - - - - - 406 - - - - - - - - 407 - - - - - 931 - - - - - - - - 291 - - - - - - - - 29 - - - - - - - - - - - - MainMenu - - - 19 - - - - - - - - 24 - - - - - - - - - - - - - - - 5 - - - - - 23 - - - - - 92 - - - - - 197 - - - - - 398 - - - - - 399 - - - - - 579 - - - - - 924 - - - - - 56 - - - - - - - - 57 - - - - - - - - - - - - - - - - - - 58 - - - - - 134 - - - - - 136 - - - - - 144 - - - - - 145 - - - - - 149 - - - - - 150 - - - - - 541 - - - - - 542 - - - - - 754 - - - - - 955 - - - - - 103 - - - - - - - - 106 - - - - - - - - - 111 - - - - - 948 - - - - - 597 - - - - - - - - 598 - - - - - - - - - - - - - - - - - - - - - - - - 599 - - - - - 600 - - - - - 601 - - - - - 602 - - - - - 603 - - - - - 604 - - - - - 605 - - - - - 707 - - - - - 708 - - - - - 710 - - - - - 922 - - - - - 926 - - - - - 927 - - - - - 928 - - - - - 933 - - - - - 952 - - - - - 971 - - - - - 618 - - - - - - - - 619 - - - - - - - - - - - - - - - - - - 620 - - - - - 621 - - - - - 622 - - - - - 623 - - - - - 624 - - - - - 625 - - - - - 626 - - - - - 646 - - - - - 705 - - - - - 943 - - - - - 944 - - - - - 965 - - - - - - - - 966 - - - - - - - - - 967 - - - - - 968 - - - - - 973 - - - - - - - - 974 - - - - - - - - - - - - - - - 990 - - - - - 991 - - - - - 996 - - - - - 1019 - - - - - 1020 - - - - - 1021 - - - - - 1022 - - - - - 1023 - - - - - 206 - - - AppDelegate - - - 209 - - - ResultWindow - - - 468 - - - Shared Defaults - - - 497 - - - RecentDirectoriesController - - - 523 - - - - - - preferences - - - 524 - - - - - - - - - - - - - - - - - - - - 531 - - - - - - - - 532 - - - - - - - - 533 - - - - - - - - 534 - - - - - - - - 649 - - - - - - - - 712 - - - - - - - - 750 - - - - - - - - 904 - - - - - - - - 905 - - - - - - - - 950 - - - - - - - - 958 - - - - - - - - 1059 - - - - - - - - 1060 - - - - - - - - 613 - - - PyDupeGuru - - - 657 - - - - - - - - - - - - matches_context - - - 658 - - - - - 659 - - - - - 715 - - - - - 872 - - - - - 937 - - - - - 938 - - - - - 939 - - - - - 954 - - - SUUpdater - - - 1076 - - - - - 1077 - - - - - 1078 - - - - - 1079 - - - - - 1080 - - - - - 1081 - - - - - 1082 - - - - - - - - 1083 - - - - - 1084 - - - - - 1085 - - - - - - - - 1086 - - - - - 1087 - - - - - 1088 - - - - - 1089 - - - - - 1094 - - - - - 1095 - - - - - 1096 - - - - - 1097 - - - - - 714 - - - - - 906 - - - - - - - - - - 913 - - - - - 909 - - - - - 908 - - - - - 1098 - - - - - 1099 - - - - - 1100 - - - - - 1101 - - - - - - - - - - - - - - - - - - 1104 - - - - - 1106 - - - - - 1107 - - - - - 1109 - - - - - 1110 - - - - - 1111 - - - - - 1112 - - - - - 1113 - - - - - - - - 720 - - - - - - - - 1090 - - - - - - - - 721 - - - - - - - - - - - - - - - - - - - - 723 - - - - - 731 - - - - - 732 - - - - - 733 - - - - - 734 - - - - - 735 - - - - - 736 - - - - - 920 - - - - - 738 - - - - - 737 - - - - - 739 - - - - - 740 - - - - - 935 - - - - - 1114 - - - - - - - - 961 - - - - - - - - 1092 - - - - - 1115 - - - - - - - - 880 - - - - - - - - 1091 - - - - - 1116 - - - - - - - - 1029 - - - - - - - - 1093 - - - - - - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - {{58, 789}, {617, 0}} - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - tbbScan - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - tbbDirectories - com.apple.InterfaceBuilder.CocoaPlugin - tbbDetail - com.apple.InterfaceBuilder.CocoaPlugin - tbbPreferences - com.apple.InterfaceBuilder.CocoaPlugin - tbbAction - com.apple.InterfaceBuilder.CocoaPlugin - tbbPowerMarker - com.apple.InterfaceBuilder.CocoaPlugin - tbbDelta - com.apple.InterfaceBuilder.CocoaPlugin - tbbFilter - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - - - {{88, 389}, {557, 400}} - com.apple.InterfaceBuilder.CocoaPlugin - {{88, 389}, {557, 400}} - - - - {340, 340} - com.apple.InterfaceBuilder.CocoaPlugin - - MatchesView - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - - com.apple.InterfaceBuilder.CocoaPlugin - - {{88, 593}, {352, 252}} - com.apple.InterfaceBuilder.CocoaPlugin - {{88, 593}, {352, 252}} - - - {213, 107} - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - {{73, 468}, {331, 243}} - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - - - - - - 1120 - - - - - AppDelegate - AppDelegateBase - - id - id - id - - - result - ResultWindow - - - IBProjectSource - AppDelegate.h - - - - AppDelegate - AppDelegateBase - - unlockApp: - id - - - PyDupeGuru - RecentDirectories - NSMenuItem - - - IBUserSource - - - - - AppDelegateBase - NSObject - - unlockApp: - id - - - PyDupeGuruBase - RecentDirectories - NSMenuItem - - - IBProjectSource - dgbase/AppDelegate.h - - - - FirstResponder - NSObject - - IBUserSource - - - - - MatchesView - OutlineView - - IBProjectSource - dgbase/ResultWindow.h - - - - MatchesView - OutlineView - - IBUserSource - - - - - NSSegmentedControl - NSControl - - IBUserSource - - - - - OutlineView - NSOutlineView - - py - PyApp - - - IBProjectSource - cocoalib/Outline.h - - - - OutlineView - NSOutlineView - - IBUserSource - - - - - PyApp - PyRegistrable - - IBProjectSource - cocoalib/PyApp.h - - - - PyApp - PyRegistrable - - IBUserSource - - - - - PyDupeGuru - PyDupeGuruBase - - IBProjectSource - PyDupeGuru.h - - - - PyDupeGuru - PyDupeGuruBase - - IBUserSource - - - - - PyDupeGuruBase - PyApp - - IBProjectSource - dgbase/PyDupeGuru.h - - - - RecentDirectories - NSObject - - id - id - - - id - NSMenu - - - IBProjectSource - cocoalib/RecentDirectories.h - - - - RecentDirectories - NSObject - - IBUserSource - - - - - ResultWindow - ResultWindowBase - - id - id - id - id - id - id - id - id - id - id - id - id - id - id - id - id - id - id - id - id - id - id - - - NSMenu - NSSearchField - NSWindow - - - IBProjectSource - ResultWindow.h - - - - ResultWindow - ResultWindowBase - - id - id - id - id - id - id - id - id - id - id - - - NSPopUpButton - NSView - id - NSSegmentedControl - NSView - NSView - MatchesView - NSSegmentedControl - NSView - PyDupeGuru - NSTextField - - - IBUserSource - - - - - ResultWindowBase - NSWindowController - - id - id - id - id - id - id - id - id - id - - - id - NSSegmentedControl - MatchesView - NSSegmentedControl - PyDupeGuruBase - NSTextField - - - - - SUUpdater - NSObject - - IBUserSource - - - - - - - NSActionCell - NSCell - - IBFrameworkSource - AppKit.framework/Headers/NSActionCell.h - - - - NSApplication - NSResponder - - IBFrameworkSource - AppKit.framework/Headers/NSApplication.h - - - - NSApplication - - IBFrameworkSource - AppKit.framework/Headers/NSApplicationScripting.h - - - - NSApplication - - IBFrameworkSource - AppKit.framework/Headers/NSColorPanel.h - - - - NSApplication - - IBFrameworkSource - AppKit.framework/Headers/NSHelpManager.h - - - - NSApplication - - IBFrameworkSource - AppKit.framework/Headers/NSPageLayout.h - - - - NSApplication - - IBFrameworkSource - AppKit.framework/Headers/NSUserInterfaceItemSearching.h - - - - NSButton - NSControl - - IBFrameworkSource - AppKit.framework/Headers/NSButton.h - - - - NSButtonCell - NSActionCell - - IBFrameworkSource - AppKit.framework/Headers/NSButtonCell.h - - - - NSCell - NSObject - - IBFrameworkSource - AppKit.framework/Headers/NSCell.h - - - - NSControl - NSView - - IBFrameworkSource - AppKit.framework/Headers/NSControl.h - - - - NSController - NSObject - - IBFrameworkSource - AppKit.framework/Headers/NSController.h - - - - NSFormatter - NSObject - - IBFrameworkSource - Foundation.framework/Headers/NSFormatter.h - - - - NSMenu - NSObject - - IBFrameworkSource - AppKit.framework/Headers/NSMenu.h - - - - NSMenuItem - NSObject - - IBFrameworkSource - AppKit.framework/Headers/NSMenuItem.h - - - - NSMenuItemCell - NSButtonCell - - IBFrameworkSource - AppKit.framework/Headers/NSMenuItemCell.h - - - - NSMovieView - NSView - - IBFrameworkSource - AppKit.framework/Headers/NSMovieView.h - - - - NSNumberFormatter - NSFormatter - - IBFrameworkSource - Foundation.framework/Headers/NSNumberFormatter.h - - - - NSObject - - IBFrameworkSource - AppKit.framework/Headers/NSAccessibility.h - - - - NSObject - - - - NSObject - - - - NSObject - - - - NSObject - - - - NSObject - - IBFrameworkSource - AppKit.framework/Headers/NSDictionaryController.h - - - - NSObject - - IBFrameworkSource - AppKit.framework/Headers/NSDragging.h - - - - NSObject - - IBFrameworkSource - AppKit.framework/Headers/NSFontManager.h - - - - NSObject - - IBFrameworkSource - AppKit.framework/Headers/NSFontPanel.h - - - - NSObject - - IBFrameworkSource - AppKit.framework/Headers/NSKeyValueBinding.h - - - - NSObject - - - - NSObject - - IBFrameworkSource - AppKit.framework/Headers/NSNibLoading.h - - - - NSObject - - IBFrameworkSource - AppKit.framework/Headers/NSOutlineView.h - - - - NSObject - - IBFrameworkSource - AppKit.framework/Headers/NSPasteboard.h - - - - NSObject - - IBFrameworkSource - AppKit.framework/Headers/NSSavePanel.h - - - - NSObject - - IBFrameworkSource - AppKit.framework/Headers/NSTableView.h - - - - NSObject - - IBFrameworkSource - AppKit.framework/Headers/NSToolbarItem.h - - - - NSObject - - IBFrameworkSource - AppKit.framework/Headers/NSView.h - - - - NSObject - - IBFrameworkSource - Foundation.framework/Headers/NSArchiver.h - - - - NSObject - - IBFrameworkSource - Foundation.framework/Headers/NSClassDescription.h - - - - NSObject - - IBFrameworkSource - Foundation.framework/Headers/NSError.h - - - - NSObject - - IBFrameworkSource - Foundation.framework/Headers/NSFileManager.h - - - - NSObject - - IBFrameworkSource - Foundation.framework/Headers/NSKeyValueCoding.h - - - - NSObject - - IBFrameworkSource - Foundation.framework/Headers/NSKeyValueObserving.h - - - - NSObject - - IBFrameworkSource - Foundation.framework/Headers/NSKeyedArchiver.h - - - - NSObject - - IBFrameworkSource - Foundation.framework/Headers/NSObject.h - - - - NSObject - - IBFrameworkSource - Foundation.framework/Headers/NSObjectScripting.h - - - - NSObject - - IBFrameworkSource - Foundation.framework/Headers/NSPortCoder.h - - - - NSObject - - IBFrameworkSource - Foundation.framework/Headers/NSRunLoop.h - - - - NSObject - - IBFrameworkSource - Foundation.framework/Headers/NSScriptClassDescription.h - - - - NSObject - - IBFrameworkSource - Foundation.framework/Headers/NSScriptKeyValueCoding.h - - - - NSObject - - IBFrameworkSource - Foundation.framework/Headers/NSScriptObjectSpecifiers.h - - - - NSObject - - IBFrameworkSource - Foundation.framework/Headers/NSScriptWhoseTests.h - - - - NSObject - - IBFrameworkSource - Foundation.framework/Headers/NSThread.h - - - - NSObject - - IBFrameworkSource - Foundation.framework/Headers/NSURL.h - - - - NSObject - - IBFrameworkSource - Foundation.framework/Headers/NSURLConnection.h - - - - NSObject - - IBFrameworkSource - Foundation.framework/Headers/NSURLDownload.h - - - - NSObject - - IBFrameworkSource - Sparkle.framework/Headers/SUAppcast.h - - - - NSObject - - IBFrameworkSource - Sparkle.framework/Headers/SUUpdater.h - - - - NSOutlineView - NSTableView - - - - NSPopUpButton - NSButton - - IBFrameworkSource - AppKit.framework/Headers/NSPopUpButton.h - - - - NSPopUpButtonCell - NSMenuItemCell - - IBFrameworkSource - AppKit.framework/Headers/NSPopUpButtonCell.h - - - - NSResponder - - IBFrameworkSource - AppKit.framework/Headers/NSInterfaceStyle.h - - - - NSResponder - NSObject - - IBFrameworkSource - AppKit.framework/Headers/NSResponder.h - - - - NSScrollView - NSView - - IBFrameworkSource - AppKit.framework/Headers/NSScrollView.h - - - - NSScroller - NSControl - - IBFrameworkSource - AppKit.framework/Headers/NSScroller.h - - - - NSSearchField - NSTextField - - IBFrameworkSource - AppKit.framework/Headers/NSSearchField.h - - - - NSSearchFieldCell - NSTextFieldCell - - IBFrameworkSource - AppKit.framework/Headers/NSSearchFieldCell.h - - - - NSSegmentedCell - NSActionCell - - IBFrameworkSource - AppKit.framework/Headers/NSSegmentedCell.h - - - - NSSegmentedControl - NSControl - - IBFrameworkSource - AppKit.framework/Headers/NSSegmentedControl.h - - - - NSSlider - NSControl - - IBFrameworkSource - AppKit.framework/Headers/NSSlider.h - - - - NSSliderCell - NSActionCell - - IBFrameworkSource - AppKit.framework/Headers/NSSliderCell.h - - - - NSTableColumn - NSObject - - IBFrameworkSource - AppKit.framework/Headers/NSTableColumn.h - - - - NSTableHeaderView - NSView - - IBFrameworkSource - AppKit.framework/Headers/NSTableHeaderView.h - - - - NSTableView - NSControl - - - - NSText - NSView - - IBFrameworkSource - AppKit.framework/Headers/NSText.h - - - - NSTextField - NSControl - - IBFrameworkSource - AppKit.framework/Headers/NSTextField.h - - - - NSTextFieldCell - NSActionCell - - IBFrameworkSource - AppKit.framework/Headers/NSTextFieldCell.h - - - - NSToolbar - NSObject - - IBFrameworkSource - AppKit.framework/Headers/NSToolbar.h - - - - NSToolbarItem - NSObject - - - - NSUserDefaultsController - NSController - - IBFrameworkSource - AppKit.framework/Headers/NSUserDefaultsController.h - - - - NSView - - IBFrameworkSource - AppKit.framework/Headers/NSClipView.h - - - - NSView - - - - NSView - - IBFrameworkSource - AppKit.framework/Headers/NSRulerView.h - - - - NSView - NSResponder - - - - NSWindow - - IBFrameworkSource - AppKit.framework/Headers/NSDrawer.h - - - - NSWindow - NSResponder - - IBFrameworkSource - AppKit.framework/Headers/NSWindow.h - - - - NSWindow - - IBFrameworkSource - AppKit.framework/Headers/NSWindowScripting.h - - - - NSWindowController - NSResponder - - showWindow: - id - - - IBFrameworkSource - AppKit.framework/Headers/NSWindowController.h - - - - SUUpdater - NSObject - - checkForUpdates: - id - - - delegate - id - - - - - - 0 - - com.apple.InterfaceBuilder.CocoaPlugin.macosx - - - - com.apple.InterfaceBuilder.CocoaPlugin.macosx - - - YES - ../dupeguru.xcodeproj - 3 - - diff --git a/pe/cocoa/xib/Preferences.xib b/pe/cocoa/xib/Preferences.xib new file mode 100644 index 00000000..135ed197 --- /dev/null +++ b/pe/cocoa/xib/Preferences.xib @@ -0,0 +1,1650 @@ + + + + 1050 + 10B504 + 740 + 1038.2 + 437.00 + + com.apple.InterfaceBuilder.CocoaPlugin + 740 + + + YES + + + + YES + com.apple.InterfaceBuilder.CocoaPlugin + + + YES + + YES + + + YES + + + + YES + + NSWindowController + + + FirstResponder + + + NSApplication + + + YES + + + 3 + 2 + {{92, 350}, {352, 252}} + 1886912512 + dupeGuru PE Preferences + + NSWindow + + + View + + {1.79769e+308, 1.79769e+308} + {213, 107} + + + 256 + + YES + + + 292 + {{120, 213}, {181, 21}} + + YES + + 67239424 + 0 + + + + + Helvetica + 12 + 16 + + + 100 + 1 + 80 + 0.0 + 0 + 1 + NO + NO + + + + + 292 + {{122, 196}, {80, 13}} + + YES + + 67239424 + 272629760 + More results + + LucidaGrande + 10 + 2843 + + + + 6 + System + controlColor + + 3 + MC42NjY2NjY2NjY3AA + + + + 6 + System + controlTextColor + + 3 + MAA + + + + + + + 289 + {{219, 196}, {80, 13}} + + YES + + 67239424 + 71303168 + Less results + + + + + + + + + 292 + {{17, 218}, {100, 14}} + + YES + + 67239424 + 272629760 + Filter hardness: + + LucidaGrande + 11 + 3100 + + + + + + + + + 256 + {{18, 152}, {214, 18}} + + YES + + 67239424 + 0 + Can mix file kind + + + 1211912703 + 2 + + NSSwitch + + + + 200 + 25 + + + + + 256 + {{304, 218}, {31, 14}} + + YES + + 67239424 + -1874853888 + + + + + YES + + YES + allowsFloats + attributedStringForZero + decimalSeparator + formatterBehavior + groupingSeparator + negativeFormat + positiveFormat + usesGroupingSeparator + + + YES + + + 0 + + YES + + + YES + + + + . + + , + -0 + 0 + + + + 0 + -0 + + + + + + + + NaN + + + + 0 + 0 + YES + NO + 1 + AAAAAAAAAAAAAAAAAAAAAA + + + + . + , + NO + YES + YES + + + + + + + + + 256 + {{190, 16}, {148, 32}} + + YES + + 67239424 + 134217728 + Reset to Defaults + + LucidaGrande + 13 + 1044 + + + -2038284033 + 1 + + + + + + 200 + 25 + + + + + 292 + {{20, 71}, {85, 13}} + + YES + + 67239424 + 272629760 + Copy and Move: + + + + + + + + + 292 + {{110, 60}, {216, 26}} + + YES + + -2076049856 + 2048 + + + 109199615 + 1 + + + + + + 400 + 75 + + + Right in destination + + 1048576 + 2147483647 + 1 + + NSImage + NSMenuCheckmark + + + NSImage + NSMenuMixedState + + _popUpItemAction: + + + YES + + + OtherViews + + + YES + + + + Recreate relative path + + 1048576 + 2147483647 + + + _popUpItemAction: + + + + + Recreate absolute path + + 1048576 + 2147483647 + + + _popUpItemAction: + + + + + 3 + YES + YES + 1 + + + + + 256 + {{18, 172}, {214, 18}} + + YES + + 67239424 + 0 + Match scaled pictures together + + + 1211912703 + 2 + + + + 200 + 25 + + + + + 256 + {{18, 90}, {283, 18}} + + YES + + 67239424 + 0 + Check for update on startup + + + 1211912703 + 2 + + + + 200 + 25 + + + + + 256 + {{18, 112}, {242, 18}} + + YES + + 67239424 + 0 + Remove empty folders on delete or move + + + 1211912703 + 2 + + + + 200 + 25 + + + + + 256 + {{18, 132}, {228, 18}} + + YES + + 67239424 + 0 + Use regular expressions when filtering + + + 1211912703 + 2 + + + + 200 + 25 + + + + {352, 252} + + + {{0, 0}, {1440, 878}} + {213, 129} + {1.79769e+308, 1.79769e+308} + + + + + YES + + + value: values.matchScaled + + + + + + value: values.matchScaled + value + values.matchScaled + 2 + + + 35 + + + + nextKeyView + + + + 36 + + + + value: values.minMatchPercentage + + + + + + value: values.minMatchPercentage + value + values.minMatchPercentage + 2 + + + 37 + + + + value: values.mixFileKind + + + + + + value: values.mixFileKind + value + values.mixFileKind + 2 + + + 38 + + + + nextKeyView + + + + 39 + + + + value: values.SUCheckAtStartup + + + + + + value: values.SUCheckAtStartup + value + values.SUCheckAtStartup + 2 + + + 40 + + + + revertToInitialValues: + + + + 41 + + + + value: values.useRegexpFilter + + + + + + value: values.useRegexpFilter + value + values.useRegexpFilter + 2 + + + 42 + + + + nextKeyView + + + + 43 + + + + nextKeyView + + + + 44 + + + + value: values.removeEmptyFolders + + + + + + value: values.removeEmptyFolders + value + values.removeEmptyFolders + 2 + + + 45 + + + + selectedIndex: values.recreatePathType + + + + + + selectedIndex: values.recreatePathType + selectedIndex + values.recreatePathType + 2 + + + 46 + + + + nextKeyView + + + + 47 + + + + nextKeyView + + + + 48 + + + + value: values.minMatchPercentage + + + + + + value: values.minMatchPercentage + value + values.minMatchPercentage + 2 + + + 49 + + + + nextKeyView + + + + 50 + + + + window + + + + 51 + + + + + YES + + 0 + + + + + + -2 + + + File's Owner + + + -1 + + + First Responder + + + -3 + + + Application + + + 1 + + + Shared Defaults + + + 2 + + + YES + + + + preferences + + + 3 + + + YES + + + + + + + + + + + + + + + + + + 4 + + + YES + + + + + + 5 + + + YES + + + + + + 6 + + + YES + + + + + + 7 + + + YES + + + + + + 8 + + + YES + + + + + + 9 + + + YES + + + + + + 10 + + + YES + + + + + + 11 + + + YES + + + + + + 12 + + + YES + + + + + + 13 + + + YES + + + + + + 14 + + + YES + + + + + + 15 + + + YES + + + + + + 16 + + + YES + + + + + + 17 + + + + + 18 + + + + + 19 + + + + + 20 + + + + + 21 + + + + + 22 + + + YES + + + + + + 23 + + + + + 24 + + + + + 25 + + + + + 26 + + + YES + + + + + + 27 + + + YES + + + + + + + + 28 + + + + + 29 + + + + + 30 + + + + + 31 + + + + + 32 + + + + + 33 + + + + + 34 + + + + + + + YES + + YES + -3.IBPluginDependency + 1.IBPluginDependency + 1.ImportedFromIB2 + 10.IBPluginDependency + 10.ImportedFromIB2 + 11.IBPluginDependency + 11.ImportedFromIB2 + 12.IBPluginDependency + 12.ImportedFromIB2 + 13.IBPluginDependency + 13.ImportedFromIB2 + 14.IBPluginDependency + 14.ImportedFromIB2 + 15.IBPluginDependency + 15.ImportedFromIB2 + 16.IBPluginDependency + 16.ImportedFromIB2 + 17.IBPluginDependency + 18.IBPluginDependency + 19.IBPluginDependency + 2.IBEditorWindowLastContentRect + 2.IBPluginDependency + 2.IBWindowTemplateEditedContentRect + 2.ImportedFromIB2 + 2.windowTemplate.hasMinSize + 2.windowTemplate.minSize + 20.IBPluginDependency + 21.IBPluginDependency + 22.IBPluginDependency + 23.IBPluginDependency + 23.ImportedFromIB2 + 24.IBPluginDependency + 25.IBPluginDependency + 26.IBPluginDependency + 27.IBPluginDependency + 27.ImportedFromIB2 + 28.IBPluginDependency + 28.ImportedFromIB2 + 29.IBPluginDependency + 29.ImportedFromIB2 + 3.IBPluginDependency + 3.ImportedFromIB2 + 30.IBPluginDependency + 30.ImportedFromIB2 + 31.IBPluginDependency + 32.IBPluginDependency + 33.IBPluginDependency + 34.IBPluginDependency + 4.IBPluginDependency + 4.ImportedFromIB2 + 5.IBPluginDependency + 5.ImportedFromIB2 + 6.IBPluginDependency + 6.ImportedFromIB2 + 7.IBPluginDependency + 7.ImportedFromIB2 + 8.IBPluginDependency + 8.ImportedFromIB2 + 9.IBPluginDependency + 9.ImportedFromIB2 + + + YES + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + {{88, 593}, {352, 252}} + com.apple.InterfaceBuilder.CocoaPlugin + {{88, 593}, {352, 252}} + + + {213, 107} + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + + + + YES + + + YES + + + + + YES + + + YES + + + + 51 + + + + YES + + NSActionCell + NSCell + + IBFrameworkSource + AppKit.framework/Headers/NSActionCell.h + + + + NSApplication + NSResponder + + IBFrameworkSource + AppKit.framework/Headers/NSApplication.h + + + + NSApplication + + IBFrameworkSource + AppKit.framework/Headers/NSApplicationScripting.h + + + + NSApplication + + IBFrameworkSource + AppKit.framework/Headers/NSColorPanel.h + + + + NSApplication + + IBFrameworkSource + AppKit.framework/Headers/NSHelpManager.h + + + + NSApplication + + IBFrameworkSource + AppKit.framework/Headers/NSPageLayout.h + + + + NSApplication + + IBFrameworkSource + AppKit.framework/Headers/NSUserInterfaceItemSearching.h + + + + NSButton + NSControl + + IBFrameworkSource + AppKit.framework/Headers/NSButton.h + + + + NSButtonCell + NSActionCell + + IBFrameworkSource + AppKit.framework/Headers/NSButtonCell.h + + + + NSCell + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSCell.h + + + + NSControl + NSView + + IBFrameworkSource + AppKit.framework/Headers/NSControl.h + + + + NSController + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSController.h + + + + NSFormatter + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSFormatter.h + + + + NSMenu + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSMenu.h + + + + NSMenuItem + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSMenuItem.h + + + + NSMenuItemCell + NSButtonCell + + IBFrameworkSource + AppKit.framework/Headers/NSMenuItemCell.h + + + + NSNumberFormatter + NSFormatter + + IBFrameworkSource + Foundation.framework/Headers/NSNumberFormatter.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSAccessibility.h + + + + NSObject + + + + NSObject + + + + NSObject + + + + NSObject + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSDictionaryController.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSDragging.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSFontManager.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSFontPanel.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSKeyValueBinding.h + + + + NSObject + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSNibLoading.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSOutlineView.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSPasteboard.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSSavePanel.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSTableView.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSToolbarItem.h + + + + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSView.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSArchiver.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSClassDescription.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSError.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSFileManager.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSKeyValueCoding.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSKeyValueObserving.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSKeyedArchiver.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSObject.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSObjectScripting.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSPortCoder.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSRunLoop.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSScriptClassDescription.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSScriptKeyValueCoding.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSScriptObjectSpecifiers.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSScriptWhoseTests.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSThread.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSURL.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSURLConnection.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSURLDownload.h + + + + NSObject + + IBFrameworkSource + Sparkle.framework/Headers/SUAppcast.h + + + + NSObject + + IBFrameworkSource + Sparkle.framework/Headers/SUUpdater.h + + + + NSPopUpButton + NSButton + + IBFrameworkSource + AppKit.framework/Headers/NSPopUpButton.h + + + + NSPopUpButtonCell + NSMenuItemCell + + IBFrameworkSource + AppKit.framework/Headers/NSPopUpButtonCell.h + + + + NSResponder + + IBFrameworkSource + AppKit.framework/Headers/NSInterfaceStyle.h + + + + NSResponder + NSObject + + IBFrameworkSource + AppKit.framework/Headers/NSResponder.h + + + + NSSlider + NSControl + + IBFrameworkSource + AppKit.framework/Headers/NSSlider.h + + + + NSSliderCell + NSActionCell + + IBFrameworkSource + AppKit.framework/Headers/NSSliderCell.h + + + + NSTextField + NSControl + + IBFrameworkSource + AppKit.framework/Headers/NSTextField.h + + + + NSTextFieldCell + NSActionCell + + IBFrameworkSource + AppKit.framework/Headers/NSTextFieldCell.h + + + + NSUserDefaultsController + NSController + + IBFrameworkSource + AppKit.framework/Headers/NSUserDefaultsController.h + + + + NSView + + IBFrameworkSource + AppKit.framework/Headers/NSClipView.h + + + + NSView + + + + NSView + + IBFrameworkSource + AppKit.framework/Headers/NSRulerView.h + + + + NSView + NSResponder + + + + NSWindow + + IBFrameworkSource + AppKit.framework/Headers/NSDrawer.h + + + + NSWindow + NSResponder + + IBFrameworkSource + AppKit.framework/Headers/NSWindow.h + + + + NSWindow + + IBFrameworkSource + AppKit.framework/Headers/NSWindowScripting.h + + + + NSWindowController + NSResponder + + showWindow: + id + + + IBFrameworkSource + AppKit.framework/Headers/NSWindowController.h + + + + + 0 + + com.apple.InterfaceBuilder.CocoaPlugin.macosx + + + + com.apple.InterfaceBuilder.CocoaPlugin.InterfaceBuilder3 + + + YES + ../dupeguru.xcodeproj + 3 + + From e004c0c2d4fe14b6eaed7695f3768597b8b34db1 Mon Sep 17 00:00:00 2001 From: hsoft Date: Fri, 30 Oct 2009 15:31:18 +0000 Subject: [PATCH 230/275] [#10 state:port] Changed the Remove Selected from Results's shortcut to Cmd-Del. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40236 --- base/cocoa/xib/MainMenu.xib | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/base/cocoa/xib/MainMenu.xib b/base/cocoa/xib/MainMenu.xib index cd12d2b7..5d208e33 100644 --- a/base/cocoa/xib/MainMenu.xib +++ b/base/cocoa/xib/MainMenu.xib @@ -11,7 +11,7 @@ 740 - + @@ -1168,7 +1168,7 @@ Remove Selected from Results - R + CA 1048576 2147483647 @@ -2458,7 +2458,6 @@ - @@ -2468,6 +2467,7 @@ + @@ -2501,11 +2501,6 @@ - - 605 - - - 707 @@ -2985,6 +2980,11 @@ + + 605 + + + @@ -3110,7 +3110,7 @@ com.apple.InterfaceBuilder.CocoaPlugin - {{286, 475}, {359, 293}} + {{286, 475}, {361, 293}} com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin @@ -3246,10 +3246,6 @@ id id - - result - ResultWindow - IBProjectSource AppDelegate.h @@ -3282,6 +3278,7 @@ PyDupeGuruBase RecentDirectories + ResultWindowBase NSMenuItem @@ -3381,6 +3378,14 @@ dgbase/PyDupeGuru.h + + PyRegistrable + NSObject + + IBProjectSource + cocoalib/PyRegistrable.h + + RecentDirectories NSObject @@ -3419,7 +3424,6 @@ id id id - id id id id @@ -3481,6 +3485,7 @@ id id id + id id id id From 0a06e52d65923257574e11ae9e5372cfd5a6ba15 Mon Sep 17 00:00:00 2001 From: hsoft Date: Fri, 30 Oct 2009 16:24:34 +0000 Subject: [PATCH 231/275] dg cocoa: adjusted to pyobjc upgrade (functions with multiple return values now require None placeholders to be placed at the places of the output arguments) and removed a couple of forgotten "dupe.parent". --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40237 --- base/py/app.py | 2 -- base/py/app_cocoa.py | 12 ++---------- base/py/tests/directories_test.py | 10 +++++----- se/py/app_cocoa.py | 18 ++++++++---------- 4 files changed, 15 insertions(+), 27 deletions(-) diff --git a/base/py/app.py b/base/py/app.py index f21cb4e4..f4301814 100644 --- a/base/py/app.py +++ b/base/py/app.py @@ -70,12 +70,10 @@ class DupeGuru(RegistrableApplication): def _do_delete_dupe(self, dupe): if not io.exists(dupe.path): - dupe.parent = None return True self._recycle_dupe(dupe) self.clean_empty_dirs(dupe.path[:-1]) if not io.exists(dupe.path): - dupe.parent = None return True logging.warning("Could not send {0} to trash.".format(unicode(dupe.path))) return False diff --git a/base/py/app_cocoa.py b/base/py/app_cocoa.py index 780388bb..6169cd5b 100644 --- a/base/py/app_cocoa.py +++ b/base/py/app_cocoa.py @@ -53,18 +53,10 @@ class DupeGuru(app.DupeGuru): #--- Override @staticmethod def _recycle_dupe(dupe): - if not io.exists(dupe.path): - dupe.parent = None - return True - directory = unicode(dupe.parent.path) + directory = unicode(dupe.path[:-1]) filename = dupe.name result, tag = NSWorkspace.sharedWorkspace().performFileOperation_source_destination_files_tag_( - NSWorkspaceRecycleOperation, directory, '', [filename]) - if not io.exists(dupe.path): - dupe.parent = None - return True - logging.warning('Could not send %s to trash. tag: %d' % (unicode(dupe.path), tag)) - return False + NSWorkspaceRecycleOperation, directory, '', [filename], None) def _start_job(self, jobid, func): try: diff --git a/base/py/tests/directories_test.py b/base/py/tests/directories_test.py index 4a550f7a..fa74c6bf 100644 --- a/base/py/tests/directories_test.py +++ b/base/py/tests/directories_test.py @@ -154,13 +154,13 @@ class TCDirectories(TestCase): d.add_path(p) d.set_state(p + 'dir1',STATE_REFERENCE) d.set_state(p + 'dir2',STATE_EXCLUDED) - files = d.get_files() - self.assertEqual(5, len(list(files))) + files = list(d.get_files()) + self.assertEqual(5, len(files)) for f in files: - if f.parent.path == p + 'dir1': - self.assert_(f.is_ref) + if f.path[:-1] == p + 'dir1': + assert f.is_ref else: - self.assert_(not f.is_ref) + assert not f.is_ref def test_get_files_with_inherited_exclusion(self): d = Directories() diff --git a/se/py/app_cocoa.py b/se/py/app_cocoa.py index 4eb58820..fcc9b0ae 100644 --- a/se/py/app_cocoa.py +++ b/se/py/app_cocoa.py @@ -7,6 +7,8 @@ # which should be included with this package. The terms are also available at # http://www.hardcoded.net/licenses/hs_license +from __future__ import unicode_literals + import logging from AppKit import * @@ -21,16 +23,12 @@ from dupeguru.directories import Directories as DirectoriesBase, STATE_EXCLUDED from . import data from .fs import Bundle as BundleBase -if NSWorkspace.sharedWorkspace().respondsToSelector_('typeOfFile:error:'): # Only from 10.5 - def is_bundle(str_path): - sw = NSWorkspace.sharedWorkspace() - uti, error = sw.typeOfFile_error_(str_path) - if error is not None: - logging.warning(u'There was an error trying to detect the UTI of %s', str_path) - return sw.type_conformsToType_(uti, 'com.apple.bundle') or sw.type_conformsToType_(uti, 'com.apple.package') -else: # Tiger - def is_bundle(str_path): # just return a list of a few known bundle extensions. - return get_file_ext(str_path) in ('app', 'pages', 'numbers') +def is_bundle(str_path): + sw = NSWorkspace.sharedWorkspace() + uti, error = sw.typeOfFile_error_(str_path, None) + if error is not None: + logging.warning(u'There was an error trying to detect the UTI of %s', str_path) + return sw.type_conformsToType_(uti, 'com.apple.bundle') or sw.type_conformsToType_(uti, 'com.apple.package') class Bundle(BundleBase): @classmethod From eb3645d4932f57919d906bbe56f21e18ca148ba9 Mon Sep 17 00:00:00 2001 From: hsoft Date: Mon, 2 Nov 2009 14:40:29 +0000 Subject: [PATCH 232/275] Fixed a bug where the mark state of a switched dupe would not be refreshed immediately on Make Selected Reference. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40238 --- base/cocoa/ResultWindow.m | 1 + 1 file changed, 1 insertion(+) diff --git a/base/cocoa/ResultWindow.m b/base/cocoa/ResultWindow.m index d9ee413c..1a010610 100644 --- a/base/cocoa/ResultWindow.m +++ b/base/cocoa/ResultWindow.m @@ -440,6 +440,7 @@ http://www.hardcoded.net/licenses/hs_license - (void)resultsUpdated:(NSNotification *)aNotification { [matches invalidateBuffers]; + [matches invalidateMarkings]; } - (BOOL)validateToolbarItem:(NSToolbarItem *)theItem From 13dc9ff76d1eef882b7fcc21b35ef082ec8587d5 Mon Sep 17 00:00:00 2001 From: hsoft Date: Mon, 2 Nov 2009 15:16:34 +0000 Subject: [PATCH 233/275] Fixed a bug where the details panel would sometimes not be updated. Also, pushed the details toggling mechanism in dgbase. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40239 --- base/cocoa/AppDelegate.h | 3 +++ base/cocoa/AppDelegate.m | 1 + base/cocoa/DetailsPanel.h | 1 + base/cocoa/DetailsPanel.m | 11 +++++++++++ base/cocoa/ResultWindow.h | 2 +- base/cocoa/ResultWindow.m | 5 +++++ se/cocoa/AppDelegate.m | 9 +++++++++ se/cocoa/ResultWindow.h | 3 --- se/cocoa/ResultWindow.m | 11 ----------- 9 files changed, 31 insertions(+), 15 deletions(-) diff --git a/base/cocoa/AppDelegate.h b/base/cocoa/AppDelegate.h index b24b044d..50d4eea8 100644 --- a/base/cocoa/AppDelegate.h +++ b/base/cocoa/AppDelegate.h @@ -10,6 +10,7 @@ http://www.hardcoded.net/licenses/hs_license #import "RecentDirectories.h" #import "PyDupeGuru.h" #import "ResultWindow.h" +#import "DetailsPanel.h" @interface AppDelegateBase : NSObject { @@ -19,9 +20,11 @@ http://www.hardcoded.net/licenses/hs_license IBOutlet ResultWindowBase *result; NSString *_appName; + DetailsPanelBase *_detailsPanel; } - (IBAction)unlockApp:(id)sender; - (PyDupeGuruBase *)py; - (RecentDirectories *)recentDirectories; +- (DetailsPanelBase *)detailsPanel; // Virtual @end diff --git a/base/cocoa/AppDelegate.m b/base/cocoa/AppDelegate.m index 092e95e4..fd915a26 100644 --- a/base/cocoa/AppDelegate.m +++ b/base/cocoa/AppDelegate.m @@ -35,6 +35,7 @@ http://www.hardcoded.net/licenses/hs_license - (PyDupeGuruBase *)py { return py; } - (RecentDirectories *)recentDirectories { return recentDirectories; } +- (DetailsPanelBase *)detailsPanel { return nil; } // Virtual /* Delegate */ - (void)applicationDidFinishLaunching:(NSNotification *)aNotification diff --git a/base/cocoa/DetailsPanel.h b/base/cocoa/DetailsPanel.h index a9965268..60c2eb4b 100644 --- a/base/cocoa/DetailsPanel.h +++ b/base/cocoa/DetailsPanel.h @@ -18,6 +18,7 @@ http://www.hardcoded.net/licenses/hs_license - (id)initWithPy:(PyApp *)aPy; - (void)refresh; +- (void)toggleVisibility; /* Notifications */ - (void)duplicateSelectionChanged:(NSNotification *)aNotification; diff --git a/base/cocoa/DetailsPanel.m b/base/cocoa/DetailsPanel.m index 859a6d42..e31ad177 100644 --- a/base/cocoa/DetailsPanel.m +++ b/base/cocoa/DetailsPanel.m @@ -24,6 +24,17 @@ http://www.hardcoded.net/licenses/hs_license [detailsTable reloadData]; } +- (void)toggleVisibility +{ + if ([[self window] isVisible]) + [[self window] close]; + else + { + [self refresh]; // selection might have changed since last time + [[self window] orderFront:nil]; + } +} + /* Notifications */ - (void)duplicateSelectionChanged:(NSNotification *)aNotification { diff --git a/base/cocoa/ResultWindow.h b/base/cocoa/ResultWindow.h index ae00a2b8..05115497 100644 --- a/base/cocoa/ResultWindow.h +++ b/base/cocoa/ResultWindow.h @@ -8,7 +8,6 @@ http://www.hardcoded.net/licenses/hs_license #import #import "Outline.h" -#import "DirectoryPanel.h" #import "PyDupeGuru.h" @interface MatchesView : OutlineView @@ -56,6 +55,7 @@ http://www.hardcoded.net/licenses/hs_license - (IBAction)showPreferencesPanel:(id)sender; - (IBAction)switchSelected:(id)sender; - (IBAction)toggleColumn:(id)sender; +- (IBAction)toggleDetailsPanel:(id)sender; - (IBAction)togglePowerMarker:(id)sender; /* Notifications */ diff --git a/base/cocoa/ResultWindow.m b/base/cocoa/ResultWindow.m index 1a010610..1c5e9a96 100644 --- a/base/cocoa/ResultWindow.m +++ b/base/cocoa/ResultWindow.m @@ -346,6 +346,11 @@ http://www.hardcoded.net/licenses/hs_license } } +- (IBAction)toggleDetailsPanel:(id)sender +{ + [[(AppDelegateBase *)app detailsPanel] toggleVisibility]; +} + - (IBAction)togglePowerMarker:(id)sender { if ([pmSwitch selectedSegment] == 1) diff --git a/se/cocoa/AppDelegate.m b/se/cocoa/AppDelegate.m index dbb7d071..d3ecb58e 100644 --- a/se/cocoa/AppDelegate.m +++ b/se/cocoa/AppDelegate.m @@ -11,6 +11,7 @@ http://www.hardcoded.net/licenses/hs_license #import "cocoalib/RegistrationInterface.h" #import "cocoalib/Utils.h" #import "cocoalib/ValueTransformers.h" +#import "DetailsPanel.h" #import "Consts.h" @implementation AppDelegate @@ -62,6 +63,14 @@ http://www.hardcoded.net/licenses/hs_license _directoryPanel = [[DirectoryPanel alloc] initWithParentApp:self]; return _directoryPanel; } + +- (DetailsPanelBase *)detailsPanel +{ + if (!_detailsPanel) + _detailsPanel = [[DetailsPanel alloc] initWithPy:py]; + return _detailsPanel; +} + - (PyDupeGuru *)py { return (PyDupeGuru *)py; } //Delegate diff --git a/se/cocoa/ResultWindow.h b/se/cocoa/ResultWindow.h index 2794a995..86ec8305 100644 --- a/se/cocoa/ResultWindow.h +++ b/se/cocoa/ResultWindow.h @@ -9,7 +9,6 @@ http://www.hardcoded.net/licenses/hs_license #import #import "cocoalib/Outline.h" #import "dgbase/ResultWindow.h" -#import "DetailsPanel.h" #import "DirectoryPanel.h" @interface ResultWindow : ResultWindowBase @@ -17,7 +16,6 @@ http://www.hardcoded.net/licenses/hs_license IBOutlet NSSearchField *filterField; NSString *_lastAction; - DetailsPanel *_detailsPanel; NSMutableIndexSet *_deltaColumns; } - (IBAction)clearIgnoreList:(id)sender; @@ -37,5 +35,4 @@ http://www.hardcoded.net/licenses/hs_license - (IBAction)revealSelected:(id)sender; - (IBAction)startDuplicateScan:(id)sender; - (IBAction)toggleDelta:(id)sender; -- (IBAction)toggleDetailsPanel:(id)sender; @end diff --git a/se/cocoa/ResultWindow.m b/se/cocoa/ResultWindow.m index 7f70d11a..942f4291 100644 --- a/se/cocoa/ResultWindow.m +++ b/se/cocoa/ResultWindow.m @@ -18,7 +18,6 @@ http://www.hardcoded.net/licenses/hs_license - (void)awakeFromNib { [super awakeFromNib]; - _detailsPanel = nil; _displayDelta = NO; _powerMode = NO; _deltaColumns = [[NSMutableIndexSet indexSetWithIndexesInRange:NSMakeRange(2,4)] retain]; @@ -200,16 +199,6 @@ http://www.hardcoded.net/licenses/hs_license [self changeDelta:sender]; } -- (IBAction)toggleDetailsPanel:(id)sender -{ - if (!_detailsPanel) - _detailsPanel = [[DetailsPanel alloc] initWithPy:py]; - if ([[_detailsPanel window] isVisible]) - [[_detailsPanel window] close]; - else - [[_detailsPanel window] orderFront:nil]; -} - /* Public */ - (void)initResultColumns { From 5c8d90a57c175385d1ea5477f85aedf05008545f Mon Sep 17 00:00:00 2001 From: hsoft Date: Mon, 2 Nov 2009 15:22:30 +0000 Subject: [PATCH 234/275] dgpe cocoa: adjusted to the detail panel toggling change. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40240 --- pe/cocoa/AppDelegate.h | 3 --- pe/cocoa/AppDelegate.m | 22 ++++++++-------------- pe/cocoa/ResultWindow.h | 1 - pe/cocoa/ResultWindow.m | 6 ------ 4 files changed, 8 insertions(+), 24 deletions(-) diff --git a/pe/cocoa/AppDelegate.h b/pe/cocoa/AppDelegate.h index f4093d9f..602b6038 100644 --- a/pe/cocoa/AppDelegate.h +++ b/pe/cocoa/AppDelegate.h @@ -9,16 +9,13 @@ http://www.hardcoded.net/licenses/hs_license #import #import "dgbase/AppDelegate.h" #import "DirectoryPanel.h" -#import "DetailsPanel.h" #import "PyDupeGuru.h" @interface AppDelegate : AppDelegateBase { - DetailsPanel *_detailsPanel; DirectoryPanel *_directoryPanel; } - (IBAction)openWebsite:(id)sender; -- (IBAction)toggleDetailsPanel:(id)sender; - (IBAction)toggleDirectories:(id)sender; - (DirectoryPanel *)directoryPanel; diff --git a/pe/cocoa/AppDelegate.m b/pe/cocoa/AppDelegate.m index 339bdd12..8d022325 100644 --- a/pe/cocoa/AppDelegate.m +++ b/pe/cocoa/AppDelegate.m @@ -12,6 +12,7 @@ http://www.hardcoded.net/licenses/hs_license #import "Utils.h" #import "ValueTransformers.h" #import "Consts.h" +#import "DetailsPanel.h" @implementation AppDelegate + (void)initialize @@ -36,29 +37,22 @@ http://www.hardcoded.net/licenses/hs_license { self = [super init]; _directoryPanel = nil; - _detailsPanel = nil; _appName = APPNAME; return self; } +- (DetailsPanelBase *)detailsPanel +{ + if (!_detailsPanel) + _detailsPanel = [[DetailsPanel alloc] initWithPy:py]; + return _detailsPanel; +} + - (IBAction)openWebsite:(id)sender { [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"http://www.hardcoded.net/dupeguru_pe"]]; } -- (IBAction)toggleDetailsPanel:(id)sender -{ - if (!_detailsPanel) - _detailsPanel = [[DetailsPanel alloc] initWithPy:py]; - if ([[_detailsPanel window] isVisible]) - [[_detailsPanel window] close]; - else - { - [[_detailsPanel window] orderFront:nil]; - [_detailsPanel refresh]; - } -} - - (IBAction)toggleDirectories:(id)sender { [[self directoryPanel] toggleVisible:sender]; diff --git a/pe/cocoa/ResultWindow.h b/pe/cocoa/ResultWindow.h index 7ebc3434..06769641 100644 --- a/pe/cocoa/ResultWindow.h +++ b/pe/cocoa/ResultWindow.h @@ -33,6 +33,5 @@ http://www.hardcoded.net/licenses/hs_license - (IBAction)revealSelected:(id)sender; - (IBAction)startDuplicateScan:(id)sender; - (IBAction)toggleDelta:(id)sender; -- (IBAction)toggleDetailsPanel:(id)sender; - (IBAction)toggleDirectories:(id)sender; @end diff --git a/pe/cocoa/ResultWindow.m b/pe/cocoa/ResultWindow.m index 4c52a433..ea3c0e86 100644 --- a/pe/cocoa/ResultWindow.m +++ b/pe/cocoa/ResultWindow.m @@ -205,12 +205,6 @@ http://www.hardcoded.net/licenses/hs_license [self changeDelta:sender]; } - -- (IBAction)toggleDetailsPanel:(id)sender -{ - [(AppDelegate *)app toggleDetailsPanel:sender]; -} - - (IBAction)toggleDirectories:(id)sender { [(AppDelegate *)app toggleDirectories:sender]; From 78bef5c3c64704a2d974429f9ffaf27c26a53008 Mon Sep 17 00:00:00 2001 From: hsoft Date: Mon, 2 Nov 2009 15:27:54 +0000 Subject: [PATCH 235/275] dgme cocoa: adjusted to the detail panel toggling change. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40241 --- me/cocoa/AppDelegate.m | 9 +++++++++ me/cocoa/ResultWindow.h | 3 --- me/cocoa/ResultWindow.m | 11 ----------- 3 files changed, 9 insertions(+), 14 deletions(-) diff --git a/me/cocoa/AppDelegate.m b/me/cocoa/AppDelegate.m index 894f0110..a91cb290 100644 --- a/me/cocoa/AppDelegate.m +++ b/me/cocoa/AppDelegate.m @@ -12,6 +12,7 @@ http://www.hardcoded.net/licenses/hs_license #import "cocoalib/Utils.h" #import "cocoalib/ValueTransformers.h" #import "cocoalib/Dialogs.h" +#import "DetailsPanel.h" #import "Consts.h" @implementation AppDelegate @@ -65,6 +66,14 @@ http://www.hardcoded.net/licenses/hs_license [[self directoryPanel] toggleVisible:sender]; } + +- (DetailsPanelBase *)detailsPanel +{ + if (!_detailsPanel) + _detailsPanel = [[DetailsPanel alloc] initWithPy:py]; + return _detailsPanel; +} + - (DirectoryPanel *)directoryPanel { if (!_directoryPanel) diff --git a/me/cocoa/ResultWindow.h b/me/cocoa/ResultWindow.h index 30284c65..e175a4c9 100644 --- a/me/cocoa/ResultWindow.h +++ b/me/cocoa/ResultWindow.h @@ -9,7 +9,6 @@ http://www.hardcoded.net/licenses/hs_license #import #import "cocoalib/Outline.h" #import "dgbase/ResultWindow.h" -#import "DetailsPanel.h" #import "DirectoryPanel.h" @interface ResultWindow : ResultWindowBase @@ -17,7 +16,6 @@ http://www.hardcoded.net/licenses/hs_license IBOutlet NSSearchField *filterField; NSString *_lastAction; - DetailsPanel *_detailsPanel; NSMutableIndexSet *_deltaColumns; } - (IBAction)clearIgnoreList:(id)sender; @@ -37,5 +35,4 @@ http://www.hardcoded.net/licenses/hs_license - (IBAction)revealSelected:(id)sender; - (IBAction)startDuplicateScan:(id)sender; - (IBAction)toggleDelta:(id)sender; -- (IBAction)toggleDetailsPanel:(id)sender; @end diff --git a/me/cocoa/ResultWindow.m b/me/cocoa/ResultWindow.m index bafc7048..0ad98ca4 100644 --- a/me/cocoa/ResultWindow.m +++ b/me/cocoa/ResultWindow.m @@ -20,7 +20,6 @@ http://www.hardcoded.net/licenses/hs_license { [super awakeFromNib]; [[self window] setTitle:@"dupeGuru Music Edition"]; - _detailsPanel = nil; _displayDelta = NO; _powerMode = NO; _deltaColumns = [[NSMutableIndexSet indexSetWithIndexesInRange:NSMakeRange(2,7)] retain]; @@ -209,16 +208,6 @@ http://www.hardcoded.net/licenses/hs_license [self changeDelta:sender]; } -- (IBAction)toggleDetailsPanel:(id)sender -{ - if (!_detailsPanel) - _detailsPanel = [[DetailsPanel alloc] initWithPy:py]; - if ([[_detailsPanel window] isVisible]) - [[_detailsPanel window] close]; - else - [[_detailsPanel window] orderFront:nil]; -} - /* Public */ - (void)initResultColumns { From 7aea384f869c02a2d76faf10d14bfe299eccde8e Mon Sep 17 00:00:00 2001 From: hsoft Date: Mon, 2 Nov 2009 16:10:40 +0000 Subject: [PATCH 236/275] dgse qt: fixed the gen script (didn't gen rc file). --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40242 --- base/qt/dg.qrc | 1 - se/qt/gen.py | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/base/qt/dg.qrc b/base/qt/dg.qrc index f2f5e936..bd8d0a26 100644 --- a/base/qt/dg.qrc +++ b/base/qt/dg.qrc @@ -8,7 +8,6 @@ images/dgse_logo_32.png images/dgse_logo_128.png images/folderwin32.png - images/gear.png images/preferences32.png images/actions32.png images/delta32.png diff --git a/se/qt/gen.py b/se/qt/gen.py index 6ae54431..88b8e4a9 100644 --- a/se/qt/gen.py +++ b/se/qt/gen.py @@ -16,6 +16,7 @@ from hsutil.build import print_and_do, build_all_qt_ui build_all_qt_ui(op.join('qtlib', 'ui')) build_all_qt_ui('base') build_all_qt_ui('.') +print_and_do("pyrcc4 {0} > {1}".format(op.join('base', 'dg.qrc'), op.join('base', 'dg_rc.py'))) os.chdir('help') print_and_do('python gen.py') From fbfb16e77a1c756e0d692afaf58c6a1a4ff3eb26 Mon Sep 17 00:00:00 2001 From: hsoft Date: Mon, 2 Nov 2009 16:31:16 +0000 Subject: [PATCH 237/275] se cocoa: v2.9.0 --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40243 --- se/cocoa/Info.plist | 2 +- se/help/changelog.yaml | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/se/cocoa/Info.plist b/se/cocoa/Info.plist index 648f0f39..e18f71ca 100644 --- a/se/cocoa/Info.plist +++ b/se/cocoa/Info.plist @@ -23,7 +23,7 @@ CFBundleSignature hsft CFBundleVersion - 2.8.2 + 2.9.0 NSMainNibFile MainMenu NSPrincipalClass diff --git a/se/help/changelog.yaml b/se/help/changelog.yaml index 3f4b1845..970d995d 100644 --- a/se/help/changelog.yaml +++ b/se/help/changelog.yaml @@ -1,3 +1,9 @@ +- date: 2009-11-02 + version: 2.9.0 + description: | + * Significantly improved speed and memory usage of big scans. + * Added drag & drop support in the Directories panel. (#9) + * Fixed a bug causing dupeGuru to be confused if a scanned file was moved during the scan. (#72) - date: 2009-10-14 version: 2.8.2 description: | From 30d29c6b3481b7ce3c51627f5669d953180a5498 Mon Sep 17 00:00:00 2001 From: hsoft Date: Mon, 2 Nov 2009 16:43:05 +0000 Subject: [PATCH 238/275] dgbase qt: fixed rename. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40244 --- base/qt/app.py | 2 +- base/qt/results_model.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/base/qt/app.py b/base/qt/app.py index 2d214cf1..568c9d05 100644 --- a/base/qt/app.py +++ b/base/qt/app.py @@ -199,7 +199,7 @@ class DupeGuru(DupeGuruBase, QObject): def rename_dupe(self, dupe, newname): try: - dupe.move(dupe.parent, newname) + dupe.rename(newname) return True except (IndexError, fs.FSError) as e: logging.warning("dupeGuru Warning: %s" % unicode(e)) diff --git a/base/qt/results_model.py b/base/qt/results_model.py index d29f73bc..7b4df8d3 100644 --- a/base/qt/results_model.py +++ b/base/qt/results_model.py @@ -153,7 +153,7 @@ class ResultsModel(TreeModel): if index.column() == 0: value = unicode(value.toString()) if self._app.rename_dupe(node.dupe, value): - node.reset() + node.invalidate() return True return False From 5fe11f3b3a25d240e1b3f38e3ea0e7dadd6dbe43 Mon Sep 17 00:00:00 2001 From: hsoft Date: Mon, 2 Nov 2009 16:55:36 +0000 Subject: [PATCH 239/275] qt: fixed bug in registration mechanism. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40245 --- base/qt/app.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/base/qt/app.py b/base/qt/app.py index 568c9d05..12a8c77c 100644 --- a/base/qt/app.py +++ b/base/qt/app.py @@ -145,9 +145,7 @@ class DupeGuru(DupeGuruBase, QObject): self.emit(SIGNAL('resultsChanged()')) def ask_for_reg_code(self): - if self.reg.ask_for_code(): - #XXX bug??? - self._setup_ui_as_registered() + self.reg.ask_for_code() @demo_method def copy_or_move_marked(self, copy): From 686c60b83b585e0bdc08002dd7d257d72d16c65f Mon Sep 17 00:00:00 2001 From: hsoft Date: Mon, 2 Nov 2009 16:59:48 +0000 Subject: [PATCH 240/275] se qt v2.9.0 --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40246 --- se/qt/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/se/qt/app.py b/se/qt/app.py index de3240f5..2d7f5a17 100644 --- a/se/qt/app.py +++ b/se/qt/app.py @@ -27,7 +27,7 @@ class Directories(DirectoriesBase): class DupeGuru(DupeGuruBase): LOGO_NAME = 'logo_se' NAME = 'dupeGuru' - VERSION = '2.8.2' + VERSION = '2.9.0' DELTA_COLUMNS = frozenset([2, 4, 5]) def __init__(self): From 9729e05fe84591a9b87ae9d8c1ce6a5323501870 Mon Sep 17 00:00:00 2001 From: hsoft Date: Tue, 3 Nov 2009 08:57:43 +0000 Subject: [PATCH 241/275] se: Fixed changelog for v2.9.0 --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40247 --- se/help/changelog.yaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/se/help/changelog.yaml b/se/help/changelog.yaml index 970d995d..105740a1 100644 --- a/se/help/changelog.yaml +++ b/se/help/changelog.yaml @@ -1,9 +1,10 @@ -- date: 2009-11-02 +- date: 2009-11-03 version: 2.9.0 description: | - * Significantly improved speed and memory usage of big scans. + * Significantly improved speed and memory usage of big contents-based scans. * Added drag & drop support in the Directories panel. (#9) * Fixed a bug causing dupeGuru to be confused if a scanned file was moved during the scan. (#72) + * Dropped support for Mac OS X 10.4 (Tiger) - date: 2009-10-14 version: 2.8.2 description: | From 6724e710d8eec7fe914a93af994ab0e550cbe9bd Mon Sep 17 00:00:00 2001 From: hsoft Date: Tue, 3 Nov 2009 13:19:46 +0000 Subject: [PATCH 242/275] cocoa: Removed fixed toolbar identifiers so that IB 3.0 can compile MainMenu.xib. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40248 --- base/cocoa/xib/MainMenu.xib | 2487 +++++++++++++++++++++-------------- 1 file changed, 1491 insertions(+), 996 deletions(-) diff --git a/base/cocoa/xib/MainMenu.xib b/base/cocoa/xib/MainMenu.xib index 5d208e33..bb7c8c7c 100644 --- a/base/cocoa/xib/MainMenu.xib +++ b/base/cocoa/xib/MainMenu.xib @@ -1,5 +1,5 @@ - + 1050 10B504 @@ -10,15 +10,26 @@ com.apple.InterfaceBuilder.CocoaPlugin 740 - - - - - + + YES + + + + + YES com.apple.InterfaceBuilder.CocoaPlugin - - - + + + YES + + YES + + + YES + + + + YES NSApplication @@ -46,564 +57,604 @@ YES 1 1 - - - - 05CA01D3-49FE-42B7-B043-791FFDF37644 - - Power Marker - Power Marker - - - - 256 - {{7, 14}, {67, 24}} - YES - - 67239424 - 0 - - LucidaGrande - 11 - 3100 - - - - - 30 - Off - 2 - - - 30 - On - 1 - 2 - - - 1 + + YES + + YES + 05CA01D3-49FE-42B7-B043-791FFDF37644 + 3E17CA47-6688-44FC-963C-CF22D780305F + 8021CD05-A38E-4F80-99DD-7771914CEE06 + 8E5ADD0F-24AD-452A-BE68-464FE9E5E240 + BA65FFF2-9E56-4E88-AB2E-8FBE2B3D030F + DAF37663-5062-4BEC-809F-0F335CC144ED + F37510C7-955F-4141-9D09-AC2881ADCCFA + F813A7D3-0C98-4465-A6F8-799EF380A59F + NSToolbarFlexibleSpaceItem + NSToolbarSeparatorItem + NSToolbarSpaceItem + + + YES + + + 05CA01D3-49FE-42B7-B043-791FFDF37644 - - - - - {67, 24} - {67, 24} - YES - YES - 0 - YES - 0 - - - - 3E17CA47-6688-44FC-963C-CF22D780305F - - Start Scanning - Start Scanning - - - - NSImage - NSApplicationIcon - - - - {0, 0} - {0, 0} - YES - YES - -1 - YES - 0 - - - - 8021CD05-A38E-4F80-99DD-7771914CEE06 - - Preferences - Preferences - - - - NSImage - preferences32 - - - - {0, 0} - {0, 0} - YES - YES - -1 - YES - 0 - - - - 8E5ADD0F-24AD-452A-BE68-464FE9E5E240 - - Filter - Filter - - - - 258 - {{0, 14}, {81, 22}} - YES - - 343014976 - 268436480 - - - LucidaGrande - 13 - 1044 - - Filter - - YES - 1 - - 6 - System - textBackgroundColor - - 3 - MQA - - - - 6 - System - controlTextColor - - 3 - MAA - - - - 130560 + Power Marker + Power Marker + + + + 256 + {{7, 14}, {67, 24}} + + + YES + + 67239424 0 - search + + LucidaGrande + 11 + 3100 + + + + YES + + 30 + Off + 2 + + + 30 + On + 1 + 2 + + + 1 + + + + + + {67, 24} + {67, 24} + YES + YES + 0 + YES + 0 + + + + 3E17CA47-6688-44FC-963C-CF22D780305F + + Start Scanning + Start Scanning + + + + NSImage + NSApplicationIcon + + + + {0, 0} + {0, 0} + YES + YES + -1 + YES + 0 + + + + 8021CD05-A38E-4F80-99DD-7771914CEE06 + + Preferences + Preferences + + + + NSImage + preferences32 + + + + {0, 0} + {0, 0} + YES + YES + -1 + YES + 0 + + + + 8E5ADD0F-24AD-452A-BE68-464FE9E5E240 + + Filter + Filter + + + + 258 + {{0, 14}, {81, 22}} + + + YES + + 343014976 + 268436480 + + + LucidaGrande + 13 + 1044 + + Filter - _searchFieldSearch: - - 138690815 - 0 - + YES + 1 + + 6 + System + textBackgroundColor + + 3 + MQA + + + + 6 + System + controlTextColor + + 3 + MAA + + + + 130560 + 0 + search + + _searchFieldSearch: + + 138690815 + 0 + + 400 + 75 + + + 130560 + 0 + clear + + YES + + YES + + YES + AXDescription + NSAccessibilityEncodedAttributesValueType + + + YES + cancel + + + + + + _searchFieldCancel: + + 138690815 + 0 + + 400 + 75 + + 10 + YES + + + + + + {81, 22} + {9999, 22} + YES + YES + 0 + YES + 0 + + + + BA65FFF2-9E56-4E88-AB2E-8FBE2B3D030F + + Directories + Directories + + + + NSImage + folder32 + + + + {0, 0} + {0, 0} + YES + YES + -1 + YES + 0 + + + + DAF37663-5062-4BEC-809F-0F335CC144ED + + Details + Details + + + + NSImage + details32 + + + + {0, 0} + {0, 0} + YES + YES + -1 + YES + 0 + + + + F37510C7-955F-4141-9D09-AC2881ADCCFA + + Action + Action + + + + 256 + {{0, 14}, {58, 26}} + + + YES + + -2076049856 + 2048 + + + 109199615 + 1 + + + + + 400 75 + + + YES + IA + + 1048576 + 2147483647 + 1 + + NSImage + NSActionTemplate + + + NSImage + NSMenuCheckmark + + + NSImage + NSMenuMixedState + + _popUpItemAction: + + + YES + + + OtherViews + + + YES + + + + Send Marked to Trash + + 2147483647 + + + _popUpItemAction: + + + + + Move Marked to... + + 2147483647 + + + _popUpItemAction: + + + + + Copy Marked to... + + 2147483647 + + + _popUpItemAction: + + + + + Remove Marked from Results + + 2147483647 + + + _popUpItemAction: + + + + + YES + YES + + + 1048576 + 2147483647 + + + _popUpItemAction: + + + + + Remove Selected from Results + + 2147483647 + + + _popUpItemAction: + + + + + Add Selected to Ignore List + + 1048576 + 2147483647 + + + _popUpItemAction: + + + + + Make Selected Reference + + 2147483647 + + + _popUpItemAction: + + + + + YES + YES + + + 1048576 + 2147483647 + + + _popUpItemAction: + + + + + Open Selected with Default Application + + 2147483647 + + + _popUpItemAction: + + + + + Reveal Selected in Finder + + 2147483647 + + + _popUpItemAction: + + + + + Rename Selected + + 1048576 + 2147483647 + + + _popUpItemAction: + + + + + YES + 3 + YES + YES + 1 - - 130560 + + + + + {58, 26} + {58, 26} + YES + YES + 0 + YES + 0 + + + + F813A7D3-0C98-4465-A6F8-799EF380A59F + + Delta Values + Delta Values + + + + 256 + {{4, 14}, {67, 24}} + + + YES + + 67239424 0 - clear - - - cancel - - - - - _searchFieldCancel: - - 138690815 - 0 - - 400 - 75 + + + + YES + + 30 + Off + 2 + + + 30 + On + 1 + 2 + + + 1 - 10 - YES + + + + + {67, 24} + {67, 24} + YES + YES + 0 + YES + 0 + + + NSToolbarFlexibleSpaceItem + + Flexible Space + + + + + + {1, 5} + {20000, 32} + YES + YES + -1 + YES + 0 + + YES + YES + + + 1048576 + 2147483647 + + - - - - {81, 22} - {9999, 22} - YES - YES - 0 - YES - 0 - - - - BA65FFF2-9E56-4E88-AB2E-8FBE2B3D030F - - Directories - Directories - - - - NSImage - folder32 - - - - {0, 0} - {0, 0} - YES - YES - -1 - YES - 0 - - - - DAF37663-5062-4BEC-809F-0F335CC144ED - - Details - Details - - - - NSImage - details32 - - - - {0, 0} - {0, 0} - YES - YES - -1 - YES - 0 - - - - F37510C7-955F-4141-9D09-AC2881ADCCFA - - Action - Action - - - - 256 - {{0, 14}, {58, 26}} - YES - - -2076049856 - 2048 - - - 109199615 - 1 - - - - - - 400 - 75 - - - YES - IA - - 1048576 - 2147483647 - 1 - - NSImage - NSActionTemplate - - - NSImage - NSMenuCheckmark - - - NSImage - NSMenuMixedState - - _popUpItemAction: - - - YES - - - OtherViews - - - - - - Send Marked to Trash - - 2147483647 - - - _popUpItemAction: - - - - - Move Marked to... - - 2147483647 - - - _popUpItemAction: - - - - - Copy Marked to... - - 2147483647 - - - _popUpItemAction: - - - - - Remove Marked from Results - - 2147483647 - - - _popUpItemAction: - - - - - YES - YES - - - 1048576 - 2147483647 - - - _popUpItemAction: - - - - - Remove Selected from Results - - 2147483647 - - - _popUpItemAction: - - - - - Add Selected to Ignore List - - 1048576 - 2147483647 - - - _popUpItemAction: - - - - - Make Selected Reference - - 2147483647 - - - _popUpItemAction: - - - - - YES - YES - - - 1048576 - 2147483647 - - - _popUpItemAction: - - - - - Open Selected with Default Application - - 2147483647 - - - _popUpItemAction: - - - - - Reveal Selected in Finder - - 2147483647 - - - _popUpItemAction: - - - - - Rename Selected - - 1048576 - 2147483647 - - - _popUpItemAction: - - - - - YES - 3 - YES - YES - 1 + + NSToolbarSeparatorItem + + Separator + + + + + + {12, 5} + {12, 1000} + YES + YES + -1 + YES + 0 + + YES + YES + + + 1048576 + 2147483647 + + - - - - {58, 26} - {58, 26} - YES - YES - 0 - YES - 0 - - - - F813A7D3-0C98-4465-A6F8-799EF380A59F - - Delta Values - Delta Values - - - - 256 - {{4, 14}, {67, 24}} - YES - - 67239424 - 0 - - - - - 30 - Off - 2 - - - 30 - On - 1 - 2 - - - 1 + + NSToolbarSpaceItem + + Space + + + + + + {32, 5} + {32, 32} + YES + YES + -1 + YES + 0 + + YES + YES + + + 1048576 + 2147483647 + + - - - - {67, 24} - {67, 24} - YES - YES - 0 - YES - 0 - - NSToolbarFlexibleSpaceItem - - Flexible Space - - - - - - {1, 5} - {20000, 32} - YES - YES - -1 - YES - 0 - - YES - YES - - - 1048576 - 2147483647 - - - - - - NSToolbarSeparatorItem - - Separator - - - - - - {12, 5} - {12, 1000} - YES - YES - -1 - YES - 0 - - YES - YES - - - 1048576 - 2147483647 - - - - - - NSToolbarSpaceItem - - Space - - - - - - {32, 5} - {32, 32} - YES - YES - -1 - YES - 0 - - YES - YES - - - 1048576 - 2147483647 - - - - - - + + + YES @@ -615,8 +666,9 @@ - - + + + YES @@ -624,23 +676,26 @@ - - + + {1.79769e+308, 1.79769e+308} {340, 340} 256 - + + YES 274 - + + YES 2304 - + + YES 274 @@ -660,7 +715,8 @@ {{-26, 0}, {16, 17}} - + + YES mark 47 @@ -747,7 +803,7 @@ compare: - + 3 2 @@ -771,7 +827,7 @@ YES 0 - + {{1, 17}, {515, 317}} @@ -801,9 +857,10 @@ 2304 - + + YES - + {{1, 0}, {515, 17}} @@ -812,7 +869,7 @@ 4 - + {{20, 45}, {517, 335}} @@ -845,7 +902,7 @@ - + {557, 400} @@ -856,7 +913,8 @@ MainMenu - + + YES dupeGuru @@ -868,7 +926,8 @@ submenuAction: dupeGuru - + + YES About dupeGuru @@ -973,7 +1032,7 @@ - + _NSAppleMenu @@ -990,7 +1049,8 @@ Edit - + + YES Mark All @@ -1065,7 +1125,7 @@ - + @@ -1079,7 +1139,8 @@ submenuAction: Actions - + + YES Start Duplicate Scan @@ -1229,7 +1290,7 @@ - + @@ -1243,7 +1304,9 @@ submenuAction: Columns - + + YES + @@ -1257,7 +1320,8 @@ submenuAction: Mode - + + YES Power Marker @@ -1276,7 +1340,7 @@ - + @@ -1290,7 +1354,8 @@ submenuAction: Window - + + YES Directory Panel @@ -1366,7 +1431,7 @@ - + _NSWindowsMenu @@ -1381,7 +1446,8 @@ submenuAction: Help - + + YES dupeGuru Help @@ -1400,10 +1466,10 @@ - + - + _NSMainMenu @@ -1420,7 +1486,8 @@ Menu - + + YES Remove Selected from Results @@ -1486,15 +1553,16 @@ - + SUUpdater - + - + + YES performMiniaturize: @@ -2159,9 +2227,10 @@ 1174 - + - + + YES 0 @@ -2189,56 +2258,62 @@ 21 - + + YES - + Window 2 - + + YES - + 219 - + + YES - + 220 - + + YES - + 222 - + + YES - + 406 - + + YES - + @@ -2249,15 +2324,17 @@ 291 - + + YES - + 29 - + + YES @@ -2265,22 +2342,24 @@ - + MainMenu 19 - + + YES - + 24 - + + YES @@ -2289,7 +2368,7 @@ - + @@ -2335,15 +2414,17 @@ 56 - + + YES - + 57 - + + YES @@ -2355,7 +2436,7 @@ - + @@ -2416,18 +2497,20 @@ 103 - + + YES - + 106 - + + YES - + @@ -2443,15 +2526,17 @@ 597 - + + YES - + 598 - + + YES @@ -2468,7 +2553,7 @@ - + @@ -2549,32 +2634,37 @@ 618 - + + YES - + 619 - + + YES + 959 - + + YES - + 960 - + + YES - + @@ -2590,15 +2680,17 @@ 965 - + + YES - + 966 - + + YES @@ -2607,7 +2699,7 @@ - + @@ -2677,7 +2769,8 @@ 657 - + + YES @@ -2685,7 +2778,7 @@ - + matches_context @@ -2758,7 +2851,8 @@ 1147 - + + YES @@ -2770,7 +2864,7 @@ - + @@ -2811,31 +2905,35 @@ 1170 - + + YES - + 720 - + + YES - + 1136 - + + YES - + 721 - + + YES @@ -2849,7 +2947,7 @@ - + @@ -2920,17 +3018,19 @@ 1171 - + + YES - + 955 - + + YES - + @@ -2941,17 +3041,19 @@ 1172 - + + YES - + 880 - + + YES - + @@ -2962,17 +3064,19 @@ 1173 - + + YES - + 1028 - + + YES - + @@ -2985,267 +3089,522 @@ - + + + + YES + + YES + -3.IBPluginDependency + -3.ImportedFromIB2 + 1011.IBPluginDependency + 1011.ImportedFromIB2 + 1012.IBPluginDependency + 1012.ImportedFromIB2 + 1013.IBPluginDependency + 1013.ImportedFromIB2 + 1014.IBPluginDependency + 1014.ImportedFromIB2 + 1018.IBPluginDependency + 1018.ImportedFromIB2 + 1023.IBPluginDependency + 1023.ImportedFromIB2 + 1028.IBPluginDependency + 1028.ImportedFromIB2 + 103.IBPluginDependency + 103.ImportedFromIB2 + 106.IBPluginDependency + 106.ImportedFromIB2 + 111.IBPluginDependency + 111.ImportedFromIB2 + 1116.IBPluginDependency + 1136.IBPluginDependency + 1137.IBPluginDependency + 1138.IBPluginDependency + 1139.IBPluginDependency + 1140.IBPluginDependency + 1140.IBShouldRemoveOnLegacySave + 1144.IBPluginDependency + 1144.IBShouldRemoveOnLegacySave + 1145.IBPluginDependency + 1145.IBShouldRemoveOnLegacySave + 1146.IBPluginDependency + 1146.IBShouldRemoveOnLegacySave + 1147.IBEditorWindowLastContentRect + 1147.IBPluginDependency + 1156.IBPluginDependency + 1158.IBPluginDependency + 1159.IBPluginDependency + 1160.IBPluginDependency + 1170.IBPluginDependency + 1171.IBPluginDependency + 1172.IBPluginDependency + 1173.IBPluginDependency + 134.IBPluginDependency + 134.ImportedFromIB2 + 136.IBPluginDependency + 136.ImportedFromIB2 + 144.IBPluginDependency + 144.ImportedFromIB2 + 145.IBPluginDependency + 145.ImportedFromIB2 + 149.IBPluginDependency + 149.ImportedFromIB2 + 150.IBPluginDependency + 150.ImportedFromIB2 + 19.IBPluginDependency + 19.ImportedFromIB2 + 197.IBPluginDependency + 197.ImportedFromIB2 + 2.IBPluginDependency + 2.ImportedFromIB2 + 206.ImportedFromIB2 + 209.ImportedFromIB2 + 21.IBEditorWindowLastContentRect + 21.IBPluginDependency + 21.IBWindowTemplateEditedContentRect + 21.ImportedFromIB2 + 21.NSWindowTemplate.visibleAtLaunch + 21.windowTemplate.hasMinSize + 21.windowTemplate.minSize + 219.IBPluginDependency + 219.ImportedFromIB2 + 220.CustomClassName + 220.IBPluginDependency + 220.ImportedFromIB2 + 222.IBPluginDependency + 222.ImportedFromIB2 + 23.IBPluginDependency + 23.ImportedFromIB2 + 24.IBPluginDependency + 24.ImportedFromIB2 + 29.IBEditorWindowLastContentRect + 29.IBPluginDependency + 29.ImportedFromIB2 + 291.IBPluginDependency + 291.ImportedFromIB2 + 398.IBPluginDependency + 398.ImportedFromIB2 + 399.IBPluginDependency + 399.ImportedFromIB2 + 406.IBPluginDependency + 406.ImportedFromIB2 + 407.IBPluginDependency + 407.ImportedFromIB2 + 497.ImportedFromIB2 + 5.IBPluginDependency + 5.ImportedFromIB2 + 541.IBPluginDependency + 541.ImportedFromIB2 + 542.IBPluginDependency + 542.ImportedFromIB2 + 56.IBPluginDependency + 56.ImportedFromIB2 + 57.IBEditorWindowLastContentRect + 57.IBPluginDependency + 57.ImportedFromIB2 + 579.IBPluginDependency + 579.ImportedFromIB2 + 58.IBPluginDependency + 58.ImportedFromIB2 + 597.IBPluginDependency + 597.ImportedFromIB2 + 598.IBEditorWindowLastContentRect + 598.IBPluginDependency + 598.ImportedFromIB2 + 599.IBPluginDependency + 599.ImportedFromIB2 + 600.IBPluginDependency + 600.ImportedFromIB2 + 601.IBPluginDependency + 601.ImportedFromIB2 + 602.IBPluginDependency + 602.ImportedFromIB2 + 603.IBPluginDependency + 603.ImportedFromIB2 + 604.IBPluginDependency + 604.ImportedFromIB2 + 605.IBPluginDependency + 605.ImportedFromIB2 + 613.ImportedFromIB2 + 618.IBPluginDependency + 618.ImportedFromIB2 + 619.IBEditorWindowLastContentRect + 619.IBPluginDependency + 619.ImportedFromIB2 + 657.IBEditorWindowLastContentRect + 657.IBPluginDependency + 657.ImportedFromIB2 + 658.IBPluginDependency + 658.ImportedFromIB2 + 659.IBPluginDependency + 659.ImportedFromIB2 + 707.IBPluginDependency + 707.ImportedFromIB2 + 708.IBPluginDependency + 708.ImportedFromIB2 + 710.IBPluginDependency + 710.ImportedFromIB2 + 715.IBPluginDependency + 715.ImportedFromIB2 + 720.IBPluginDependency + 720.ImportedFromIB2 + 721.IBEditorWindowLastContentRect + 721.IBPluginDependency + 721.ImportedFromIB2 + 723.IBPluginDependency + 723.ImportedFromIB2 + 731.IBPluginDependency + 731.ImportedFromIB2 + 732.IBPluginDependency + 732.ImportedFromIB2 + 733.IBPluginDependency + 733.ImportedFromIB2 + 734.IBPluginDependency + 734.ImportedFromIB2 + 735.IBPluginDependency + 735.ImportedFromIB2 + 736.IBPluginDependency + 736.ImportedFromIB2 + 737.IBPluginDependency + 737.ImportedFromIB2 + 738.IBPluginDependency + 738.ImportedFromIB2 + 739.IBPluginDependency + 739.ImportedFromIB2 + 740.IBPluginDependency + 740.ImportedFromIB2 + 754.IBPluginDependency + 754.ImportedFromIB2 + 872.IBPluginDependency + 872.ImportedFromIB2 + 880.IBPluginDependency + 880.ImportedFromIB2 + 92.IBPluginDependency + 92.ImportedFromIB2 + 920.IBPluginDependency + 920.ImportedFromIB2 + 922.IBPluginDependency + 922.ImportedFromIB2 + 924.IBPluginDependency + 924.ImportedFromIB2 + 926.IBPluginDependency + 926.ImportedFromIB2 + 927.IBPluginDependency + 927.ImportedFromIB2 + 928.IBPluginDependency + 928.ImportedFromIB2 + 933.IBPluginDependency + 933.ImportedFromIB2 + 935.IBPluginDependency + 935.ImportedFromIB2 + 937.IBPluginDependency + 937.ImportedFromIB2 + 938.IBPluginDependency + 938.ImportedFromIB2 + 939.IBPluginDependency + 939.ImportedFromIB2 + 947.IBPluginDependency + 947.ImportedFromIB2 + 949.ImportedFromIB2 + 950.IBPluginDependency + 950.ImportedFromIB2 + 955.IBPluginDependency + 955.ImportedFromIB2 + 959.IBPluginDependency + 959.ImportedFromIB2 + 960.IBPluginDependency + 960.ImportedFromIB2 + 961.IBPluginDependency + 961.ImportedFromIB2 + 962.IBPluginDependency + 962.ImportedFromIB2 + 965.IBPluginDependency + 965.ImportedFromIB2 + 966.IBPluginDependency + 966.ImportedFromIB2 + 985.IBPluginDependency + 985.ImportedFromIB2 + 986.IBPluginDependency + 986.ImportedFromIB2 + 991.IBPluginDependency + 991.ImportedFromIB2 + + + YES + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + {{79, 766}, {617, 0}} + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + + + {{109, 366}, {557, 400}} + com.apple.InterfaceBuilder.CocoaPlugin + {{109, 366}, {557, 400}} + + + + {340, 340} + com.apple.InterfaceBuilder.CocoaPlugin + + MatchesView + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + {{140, 768}, {481, 20}} + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + {{152, 575}, {221, 193}} + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + {{286, 475}, {361, 293}} + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + + com.apple.InterfaceBuilder.CocoaPlugin + + {{355, 762}, {64, 6}} + com.apple.InterfaceBuilder.CocoaPlugin + + {{182, 609}, {331, 133}} + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + {{94, 408}, {331, 243}} + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin + + + + + YES + + + YES + - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - {{79, 766}, {617, 0}} - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - tbbScan - com.apple.InterfaceBuilder.CocoaPlugin - tbbDirectories - com.apple.InterfaceBuilder.CocoaPlugin - tbbDetail - com.apple.InterfaceBuilder.CocoaPlugin - tbbPreferences - com.apple.InterfaceBuilder.CocoaPlugin - tbbAction - com.apple.InterfaceBuilder.CocoaPlugin - tbbPowerMarker - com.apple.InterfaceBuilder.CocoaPlugin - tbbDelta - com.apple.InterfaceBuilder.CocoaPlugin - tbbFilter - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - - - {{109, 366}, {557, 400}} - com.apple.InterfaceBuilder.CocoaPlugin - {{109, 366}, {557, 400}} - - - - {340, 340} - com.apple.InterfaceBuilder.CocoaPlugin - - MatchesView - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - {{140, 768}, {481, 20}} - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - {{152, 575}, {221, 193}} - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - {{286, 475}, {361, 293}} - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - - com.apple.InterfaceBuilder.CocoaPlugin - - {{355, 762}, {64, 6}} - com.apple.InterfaceBuilder.CocoaPlugin - - {{182, 609}, {331, 133}} - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - {{94, 408}, {331, 243}} - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - - - + + YES + + + YES + + 1174 - + + YES AppDelegate AppDelegateBase - - id - id - + + YES + + YES + openWebsite: + toggleDirectories: + + + YES + id + id + + IBProjectSource AppDelegate.h @@ -3258,11 +3617,21 @@ unlockApp: id - - PyDupeGuru - RecentDirectories - NSMenuItem - + + YES + + YES + py + recentDirectories + unlockMenuItem + + + YES + PyDupeGuru + RecentDirectories + NSMenuItem + + IBUserSource @@ -3275,12 +3644,23 @@ unlockApp: id - - PyDupeGuruBase - RecentDirectories - ResultWindowBase - NSMenuItem - + + YES + + YES + py + recentDirectories + result + unlockMenuItem + + + YES + PyDupeGuruBase + RecentDirectories + ResultWindowBase + NSMenuItem + + IBProjectSource dgbase/AppDelegate.h @@ -3389,14 +3769,32 @@ RecentDirectories NSObject - - id - id - - - id - NSMenu - + + YES + + YES + clearMenu: + menuClick: + + + YES + id + id + + + + YES + + YES + delegate + menu + + + YES + id + NSMenu + + IBProjectSource cocoalib/RecentDirectories.h @@ -3413,26 +3811,49 @@ ResultWindow ResultWindowBase - - id - id - id - id - id - id - id - id - id - id - id - id - id - id - id - id - id - id - + + YES + + YES + clearIgnoreList: + filter: + ignoreSelected: + markAll: + markInvert: + markNone: + markSelected: + markToggle: + openSelected: + refresh: + removeMarked: + removeSelected: + renameSelected: + resetColumnsToDefault: + revealSelected: + startDuplicateScan: + toggleDelta: + + + YES + id + id + id + id + id + id + id + id + id + id + id + id + id + id + id + id + id + + filterField NSSearchField @@ -3445,30 +3866,64 @@ ResultWindow ResultWindowBase - - id - id - id - id - id - id - id - id - id - id - - - NSView - id - NSSegmentedControl - NSView - NSView - MatchesView - NSSegmentedControl - NSView - PyDupeGuru - NSTextField - + + YES + + YES + changeDelta: + changePowerMarker: + collapseAll: + copyMarked: + deleteMarked: + expandAll: + exportToXHTML: + moveMarked: + switchSelected: + togglePowerMarker: + + + YES + id + id + id + id + id + id + id + id + id + id + + + + YES + + YES + actionMenuView + app + deltaSwitch + deltaSwitchView + filterFieldView + matches + pmSwitch + pmSwitchView + py + stats + + + YES + NSView + id + NSSegmentedControl + NSView + NSView + MatchesView + NSSegmentedControl + NSView + PyDupeGuru + NSTextField + + IBUserSource @@ -3477,29 +3932,64 @@ ResultWindowBase NSWindowController - - id - id - id - id - id - id - id - id - id - id - id - id - - - id - NSMenu - NSSegmentedControl - MatchesView - NSSegmentedControl - PyDupeGuruBase - NSTextField - + + YES + + YES + changeDelta: + changePowerMarker: + copyMarked: + deleteMarked: + expandAll: + exportToXHTML: + moveMarked: + resetColumnsToDefault: + showPreferencesPanel: + switchSelected: + toggleColumn: + toggleDetailsPanel: + togglePowerMarker: + + + YES + id + id + id + id + id + id + id + id + id + id + id + id + id + + + + YES + + YES + app + columnsMenu + deltaSwitch + matches + pmSwitch + py + stats + + + YES + id + NSMenu + NSSegmentedControl + MatchesView + NSSegmentedControl + PyDupeGuruBase + NSTextField + + @@ -3510,8 +4000,9 @@ - - + + + YES NSActionCell NSCell @@ -4105,7 +4596,7 @@ - + 0 @@ -4116,8 +4607,12 @@ com.apple.InterfaceBuilder.CocoaPlugin.macosx + + com.apple.InterfaceBuilder.CocoaPlugin.InterfaceBuilder3 + + YES - ../dupeguru.xcodeproj + ../../dupeguru.xcodeproj 3 From 2f153003b3ca9c3a54f86792b8773b883d8a1643 Mon Sep 17 00:00:00 2001 From: hsoft Date: Tue, 15 Dec 2009 11:34:21 +0000 Subject: [PATCH 243/275] dgpe help: packagified the help folder. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40252 --- pe/help/__init__.py | 0 pe/help/gen.py | 8 +++----- 2 files changed, 3 insertions(+), 5 deletions(-) create mode 100644 pe/help/__init__.py diff --git a/pe/help/__init__.py b/pe/help/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pe/help/gen.py b/pe/help/gen.py index bb523f62..5e9c34c2 100644 --- a/pe/help/gen.py +++ b/pe/help/gen.py @@ -5,10 +5,8 @@ # which should be included with this package. The terms are also available at # http://www.hardcoded.net/licenses/hs_license -import os - from hsdocgen import generate_help, filters -tix = filters.tixgen("https://hardcoded.lighthouseapp.com/projects/31699-dupeguru/tickets/{0}") - -generate_help.main('.', 'dupeguru_pe_help', force_render=True, tix=tix) +def generate(windows=False): + tix = filters.tixgen("https://hardcoded.lighthouseapp.com/projects/31699-dupeguru/tickets/{0}") + generate_help.main('.', 'dupeguru_pe_help', force_render=True, tix=tix, windows=windows) From 3262ee99385c084f80150ad066cef2e632f76a27 Mon Sep 17 00:00:00 2001 From: hsoft Date: Tue, 15 Dec 2009 11:35:08 +0000 Subject: [PATCH 244/275] dgbase: Changed ask_for_reg_code() to askForRegCode() to adapt to change in qtlib. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40253 --- base/qt/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base/qt/app.py b/base/qt/app.py index 12a8c77c..d0839d0d 100644 --- a/base/qt/app.py +++ b/base/qt/app.py @@ -144,7 +144,7 @@ class DupeGuru(DupeGuruBase, QObject): DupeGuruBase.apply_filter(self, filter) self.emit(SIGNAL('resultsChanged()')) - def ask_for_reg_code(self): + def askForRegCode(self): self.reg.ask_for_code() @demo_method From e2665610e97bbf299d927a5b6e336c65a199d733 Mon Sep 17 00:00:00 2001 From: hsoft Date: Tue, 15 Dec 2009 11:35:30 +0000 Subject: [PATCH 245/275] dgpe qt: Packagified help. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40254 --- pe/qt/gen.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pe/qt/gen.py b/pe/qt/gen.py index caa7557e..5daf4e65 100644 --- a/pe/qt/gen.py +++ b/pe/qt/gen.py @@ -13,10 +13,14 @@ import os.path as op from hsutil.build import print_and_do, build_all_qt_ui +from help import gen + build_all_qt_ui(op.join('qtlib', 'ui')) build_all_qt_ui('base') build_all_qt_ui('.') -print_and_do("pyrcc4 base\\dg.qrc > base\\dg_rc.py") +os.chdir('base') +print_and_do("pyrcc4 dg.qrc > dg_rc.py") +os.chdir('..') def move(src, dst): if not op.exists(src): @@ -39,5 +43,5 @@ move(op.join('modules', 'block', '_block.so'), op.join('.', '_block.so')) move(op.join('modules', 'block', '_block.pyd'), op.join('.', '_block.pyd')) os.chdir('help') -print_and_do('python gen.py') +gen.generate(windows=True) os.chdir('..') \ No newline at end of file From d5a60b15808e52f4f25465e80d09d866246feb66 Mon Sep 17 00:00:00 2001 From: hsoft Date: Tue, 15 Dec 2009 12:52:21 +0000 Subject: [PATCH 246/275] dgpe cocoa: Made the help file generation process independent from the current work directory. Reverted XCode version of the project to 3.0 so that it can be compiled on Leopard. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40255 --- pe/cocoa/dupeguru.xcodeproj/project.pbxproj | 9 +++++---- pe/cocoa/gen.py | 10 ++++++---- pe/cocoa/py/gen.py | 2 +- pe/help/gen.py | 4 +++- 4 files changed, 15 insertions(+), 10 deletions(-) diff --git a/pe/cocoa/dupeguru.xcodeproj/project.pbxproj b/pe/cocoa/dupeguru.xcodeproj/project.pbxproj index eeb21896..10b5dd4c 100644 --- a/pe/cocoa/dupeguru.xcodeproj/project.pbxproj +++ b/pe/cocoa/dupeguru.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 46; + objectVersion = 44; objects = { /* Begin PBXBuildFile section */ @@ -360,7 +360,7 @@ 29B97313FDCFA39411CA2CEA /* Project object */ = { isa = PBXProject; buildConfigurationList = C01FCF4E08A954540054247B /* Build configuration list for PBXProject "dupeguru" */; - compatibilityVersion = "Xcode 3.2"; + compatibilityVersion = "Xcode 3.0"; hasScannedForEncodings = 1; mainGroup = 29B97314FDCFA39411CA2CEA /* dupeguru */; projectDirPath = ""; @@ -480,12 +480,13 @@ C01FCF5008A954540054247B /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - ARCHS = "$(ARCHS_STANDARD_32_BIT)"; + ARCHS = "$(ARCHS_STANDARD_32_BIT_PRE_XCODE_3_1)"; + ARCHS_STANDARD_32_BIT_PRE_XCODE_3_1 = "ppc i386"; GCC_C_LANGUAGE_STANDARD = c99; GCC_WARN_ABOUT_RETURN_TYPE = YES; GCC_WARN_UNUSED_VARIABLE = YES; MACOSX_DEPLOYMENT_TARGET = 10.5; - SDKROOT = macosx10.5; + SDKROOT = "$(DEVELOPER_SDK_DIR)/MacOSX10.5.sdk"; }; name = Release; }; diff --git a/pe/cocoa/gen.py b/pe/cocoa/gen.py index 06c73e35..60cabb4e 100644 --- a/pe/cocoa/gen.py +++ b/pe/cocoa/gen.py @@ -5,13 +5,15 @@ # which should be included with this package. The terms are also available at # http://www.hardcoded.net/licenses/hs_license +import sys +sys.path.insert(0, 'py') # for hsutil and hsdocgen import os +from help import gen + print "Generating help" -os.chdir('help') -os.system('python -u gen.py') -os.system('/Developer/Applications/Utilities/Help\\ Indexer.app/Contents/MacOS/Help\\ Indexer dupeguru_pe_help') -os.chdir('..') +gen.generate() +os.system('/Developer/Applications/Utilities/Help\\ Indexer.app/Contents/MacOS/Help\\ Indexer help/dupeguru_pe_help') print "Generating py plugin" os.chdir('py') diff --git a/pe/cocoa/py/gen.py b/pe/cocoa/py/gen.py index bd44475f..98225454 100644 --- a/pe/cocoa/py/gen.py +++ b/pe/cocoa/py/gen.py @@ -20,4 +20,4 @@ if op.exists('build'): if op.exists('dist'): shutil.rmtree('dist') -print_and_do('python -u setup.py py2app') \ No newline at end of file +print_and_do('python -u setup.py py2app -A') \ No newline at end of file diff --git a/pe/help/gen.py b/pe/help/gen.py index 5e9c34c2..f5d78df2 100644 --- a/pe/help/gen.py +++ b/pe/help/gen.py @@ -5,8 +5,10 @@ # which should be included with this package. The terms are also available at # http://www.hardcoded.net/licenses/hs_license +import os.path as op from hsdocgen import generate_help, filters def generate(windows=False): tix = filters.tixgen("https://hardcoded.lighthouseapp.com/projects/31699-dupeguru/tickets/{0}") - generate_help.main('.', 'dupeguru_pe_help', force_render=True, tix=tix, windows=windows) + basepath = op.dirname(__file__) + generate_help.main(basepath, op.join(basepath, 'dupeguru_pe_help'), force_render=True, tix=tix, windows=windows) From b487189742c8f8ca81e6b64502bca2ab47a2d699 Mon Sep 17 00:00:00 2001 From: hsoft Date: Tue, 15 Dec 2009 14:09:13 +0000 Subject: [PATCH 247/275] [#76 state:fixed] Added combobox painting for the selected item in Directories' State column. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40256 --- base/qt/directories_model.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/base/qt/directories_model.py b/base/qt/directories_model.py index 277ea83a..492a783c 100644 --- a/base/qt/directories_model.py +++ b/base/qt/directories_model.py @@ -10,7 +10,8 @@ import urllib from PyQt4.QtCore import QModelIndex, Qt, QRect, QEvent, QPoint, QUrl -from PyQt4.QtGui import QComboBox, QStyledItemDelegate, QMouseEvent, QApplication, QBrush +from PyQt4.QtGui import (QComboBox, QStyledItemDelegate, QMouseEvent, QApplication, QBrush, QStyle, + QStyleOptionComboBox, QStyleOptionViewItemV4) from qtlib.tree_model import TreeNode, TreeModel @@ -23,6 +24,24 @@ class DirectoriesDelegate(QStyledItemDelegate): editor.addItems(STATES) return editor + def paint(self, painter, option, index): + self.initStyleOption(option, index) + # No idea why, but this cast is required if we want to have access to the V4 valuess + option = QStyleOptionViewItemV4(option) + if (index.column() == 1) and (option.state & QStyle.State_Selected): + cboption = QStyleOptionComboBox() + cboption.rect = option.rect + # On OS X (with Qt4.6.0), adding State_Enabled to the flags causes the whole drawing to + # fail (draw nothing), but it's an OS X only glitch. On Windows, it works alright. + cboption.state |= QStyle.State_Enabled + QApplication.style().drawComplexControl(QStyle.CC_ComboBox, cboption, painter) + painter.setBrush(option.palette.text()) + rect = QRect(option.rect) + rect.setLeft(rect.left()+4) + painter.drawText(rect, Qt.AlignLeft, option.text) + else: + QStyledItemDelegate.paint(self, painter, option, index) + def setEditorData(self, editor, index): value = index.model().data(index, Qt.EditRole) editor.setCurrentIndex(value); From c9b0a278cad3b8ee4600d1d274ed79517504eb5c Mon Sep 17 00:00:00 2001 From: hsoft Date: Tue, 15 Dec 2009 16:23:02 +0000 Subject: [PATCH 248/275] [#78 state:fixed] Wrapped appscript errors, updated error message and the F.A.Q. to give users a clue of what to do. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40257 --- base/cocoa/ResultWindow.m | 6 +++++- pe/help/templates/faq.mako | 10 ++++++++++ pe/py/app_cocoa.py | 32 +++++++++++++++++++++----------- 3 files changed, 36 insertions(+), 12 deletions(-) diff --git a/base/cocoa/ResultWindow.m b/base/cocoa/ResultWindow.m index 1c5e9a96..46e4298a 100644 --- a/base/cocoa/ResultWindow.m +++ b/base/cocoa/ResultWindow.m @@ -403,7 +403,11 @@ http://www.hardcoded.net/licenses/hs_license if ([lastAction isEqualTo:jobDelete]) { if (r > 0) - [Dialogs showMessage:[NSString stringWithFormat:@"%d file(s) couldn't be sent to Trash. They were kept in the results, and still are marked.",r]]; + { + NSString *msg = @"%d file(s) couldn't be sent to Trash. They were kept in the results, "\ + "and still are marked. See the F.A.Q. section in the help file for details."; + [Dialogs showMessage:[NSString stringWithFormat:msg,r]]; + } else [Dialogs showMessage:@"All marked files were sucessfully sent to Trash."]; } diff --git a/pe/help/templates/faq.mako b/pe/help/templates/faq.mako index 1c4e998f..29dc383f 100644 --- a/pe/help/templates/faq.mako +++ b/pe/help/templates/faq.mako @@ -61,4 +61,14 @@ Enable the [Power Marker](power_marker.htm) mode and click on the Directory colu * **Windows**: Click on **Actions --> Apply Filter**, then type "copy", then click OK. * **Mac OS X**: Type "copy" in the "Filter" field in the toolbar. * Click on **Mark --> Mark All**. + +### I tried to send my duplicates to Trash, but dupeGuru is telling me it can't do it. Why? What can I do? + +Most of the time, the reason why dupeGuru can't send files to Trash is because of file permissions. You need *write* permissions on files you want to send to Trash. If you're not familiar with the command line, you can use utilities such as [BatChmod](http://macchampion.com/arbysoft/BatchMod) to fix your permissions. + +If dupeGuru still gives you troubles after fixing your permissions, there have been some cases where using "Move Marked to..." as a workaround did the trick. So instead of sending your files to Trash, you send them to a temporary folder with the "Move Marked to..." action, and then you delete that temporary folder manually. + +If you're trying to delete *iPhoto* pictures, then the reason for the failure is different. The deletion fails because dupeGuru can't communicate with iPhoto. Be aware that for the deletion to work correctly, you're not supposed to play around iPhoto while dupeGuru is working. Also, sometimes, the Applescript system doesn't seem to know where to find iPhoto to launch it. It might help in these cases to launch iPhoto *before* you send your duplicates to Trash. + +If all of this fail, [contact HS support](http://www.hardcoded.net/support), we'll figure it out. \ No newline at end of file diff --git a/pe/py/app_cocoa.py b/pe/py/app_cocoa.py index a9a56ebb..4b3303e8 100644 --- a/pe/py/app_cocoa.py +++ b/pe/py/app_cocoa.py @@ -14,7 +14,7 @@ import re from Foundation import * from AppKit import * -from appscript import app, k +from appscript import app, k, CommandError from hsutil import io from hsutil.str import get_file_ext @@ -142,12 +142,18 @@ class DupeGuruPE(app_cocoa.DupeGuru): self.path2iphoto = {} if any(isinstance(dupe, IPhoto) for dupe in marked): j = j.start_subjob([6, 4], "Probing iPhoto. Don\'t touch it during the operation!") - a = app('iPhoto') - a.activate(timeout=0) - a.select(a.photo_library_album(timeout=0), timeout=0) - photos = as_fetch(a.photo_library_album().photos, k.item) - for photo in j.iter_with_progress(photos): - self.path2iphoto[unicode(photo.image_path(timeout=0))] = photo + try: + a = app('iPhoto') + a.activate(timeout=0) + a.select(a.photo_library_album(timeout=0), timeout=0) + photos = as_fetch(a.photo_library_album().photos, k.item) + for photo in j.iter_with_progress(photos): + try: + self.path2iphoto[unicode(photo.image_path(timeout=0))] = photo + except CommandError: + pass + except (CommandError, RuntimeError): + pass j.start_job(self.results.mark_count, "Sending dupes to the Trash") self.last_op_error_count = self.results.perform_on_marked(op, True) del self.path2iphoto @@ -156,11 +162,15 @@ class DupeGuruPE(app_cocoa.DupeGuru): if isinstance(dupe, IPhoto): if unicode(dupe.path) in self.path2iphoto: photo = self.path2iphoto[unicode(dupe.path)] - a = app('iPhoto') - a.remove(photo, timeout=0) - return True + try: + a = app('iPhoto') + a.remove(photo, timeout=0) + return True + except (CommandError, RuntimeError): + return False else: - logging.warning("Could not find photo {0} in iPhoto Library", unicode(dupe.path)) + logging.warning(u"Could not find photo %s in iPhoto Library", unicode(dupe.path)) + return False else: return app_cocoa.DupeGuru._do_delete_dupe(self, dupe) From 59de033523bab024f5e32fac02fc50bc88ef6087 Mon Sep 17 00:00:00 2001 From: hsoft Date: Tue, 15 Dec 2009 16:54:47 +0000 Subject: [PATCH 249/275] [#79 state:fixed] Wrapped PIL's IOError into a warning logging. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40258 --- pe/qt/app.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/pe/qt/app.py b/pe/qt/app.py index 88eb35e4..7187529a 100644 --- a/pe/qt/app.py +++ b/pe/qt/app.py @@ -8,6 +8,7 @@ # http://www.hardcoded.net/licenses/hs_license import os.path as op +import logging from PyQt4.QtGui import QImage import PIL.Image @@ -40,8 +41,12 @@ class File(fs.File): def _read_info(self, field): fs.File._read_info(self, field) if field == 'dimensions': - im = PIL.Image.open(unicode(self.path)) - self.dimensions = im.size + try: + im = PIL.Image.open(unicode(self.path)) + self.dimensions = im.size + except IOError: + self.dimensions = (0, 0) + logging.warning(u"Could not read image '%s'", unicode(self.path)) def get_blocks(self, block_count_per_side): image = QImage(unicode(self.path)) From 51b14435e05ce00b71e51c704565b5ea37b8af13 Mon Sep 17 00:00:00 2001 From: hsoft Date: Wed, 16 Dec 2009 09:54:00 +0000 Subject: [PATCH 250/275] dgpe qt v1.8.0 --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40259 --- pe/help/changelog.yaml | 9 +++++++++ pe/qt/app.py | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/pe/help/changelog.yaml b/pe/help/changelog.yaml index eda6590d..4ca20407 100644 --- a/pe/help/changelog.yaml +++ b/pe/help/changelog.yaml @@ -1,3 +1,12 @@ +- date: 2009-11-02 + version: 1.8.0 + description: | + * Added drag & drop support in the Directories panel. (#9) + * Fixed a bug causing dupeGuru to be confused if a scanned file was moved during the scan. (#72) + * Clarified how directories' state are set by painting a combo box in the state cells. [Windows] + (#76) + * Fixed some crashes. (#78 and #79) + * Dropped Mac OS X Tiger support. - date: 2009-10-24 version: 1.7.8 description: | diff --git a/pe/qt/app.py b/pe/qt/app.py index 7187529a..4bdee272 100644 --- a/pe/qt/app.py +++ b/pe/qt/app.py @@ -57,7 +57,7 @@ class File(fs.File): class DupeGuru(DupeGuruBase): LOGO_NAME = 'logo_pe' NAME = 'dupeGuru Picture Edition' - VERSION = '1.7.7' + VERSION = '1.8.0' DELTA_COLUMNS = frozenset([2, 5, 6]) def __init__(self): From eb82a35e5b14e2d4cba99bae95ccf46702febd01 Mon Sep 17 00:00:00 2001 From: hsoft Date: Wed, 16 Dec 2009 09:54:51 +0000 Subject: [PATCH 251/275] dgpe cocoa v1.8.0 --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40260 --- pe/cocoa/Info.plist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pe/cocoa/Info.plist b/pe/cocoa/Info.plist index b77bdff5..4726a231 100644 --- a/pe/cocoa/Info.plist +++ b/pe/cocoa/Info.plist @@ -23,7 +23,7 @@ CFBundleSignature hsft CFBundleVersion - 1.8.0b + 1.8.0 NSMainNibFile MainMenu NSPrincipalClass From 2b53a6e7d64c8b6ff82795888d3cd598ea5cd7fb Mon Sep 17 00:00:00 2001 From: hsoft Date: Wed, 16 Dec 2009 10:10:18 +0000 Subject: [PATCH 252/275] dgpe cocoa: removed the forgotten "-A" flag in bundle generation script. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40261 --- pe/cocoa/py/gen.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pe/cocoa/py/gen.py b/pe/cocoa/py/gen.py index 98225454..bd44475f 100644 --- a/pe/cocoa/py/gen.py +++ b/pe/cocoa/py/gen.py @@ -20,4 +20,4 @@ if op.exists('build'): if op.exists('dist'): shutil.rmtree('dist') -print_and_do('python -u setup.py py2app -A') \ No newline at end of file +print_and_do('python -u setup.py py2app') \ No newline at end of file From 5dc78809b61b0fa3460c19bdba81d1f3ba73e7c1 Mon Sep 17 00:00:00 2001 From: hsoft Date: Wed, 16 Dec 2009 10:29:02 +0000 Subject: [PATCH 253/275] dgpe: oops, wrong release date for 1.8.0. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40262 --- pe/help/changelog.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pe/help/changelog.yaml b/pe/help/changelog.yaml index 4ca20407..860bb3b4 100644 --- a/pe/help/changelog.yaml +++ b/pe/help/changelog.yaml @@ -1,4 +1,4 @@ -- date: 2009-11-02 +- date: 2009-12-16 version: 1.8.0 description: | * Added drag & drop support in the Directories panel. (#9) From 9a7bb30df44cb226320901018318ffae4f679088 Mon Sep 17 00:00:00 2001 From: hsoft Date: Wed, 16 Dec 2009 14:48:18 +0000 Subject: [PATCH 254/275] dgse cocoa: Since there are problems with the latest pyobjc + py2app + Snow Leopard, I've got to stick with pyobjc1.4 for a while after all... --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40263 --- se/py/app_cocoa.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/se/py/app_cocoa.py b/se/py/app_cocoa.py index fcc9b0ae..25c33b01 100644 --- a/se/py/app_cocoa.py +++ b/se/py/app_cocoa.py @@ -11,6 +11,7 @@ from __future__ import unicode_literals import logging +import objc from AppKit import * from hsutil import io @@ -25,7 +26,10 @@ from .fs import Bundle as BundleBase def is_bundle(str_path): sw = NSWorkspace.sharedWorkspace() - uti, error = sw.typeOfFile_error_(str_path, None) + if objc.__version__ == '1.4': # For a while, we have to support this. + uti, error = sw.typeOfFile_error_(str_path) + else: + uti, error = sw.typeOfFile_error_(str_path, None) if error is not None: logging.warning(u'There was an error trying to detect the UTI of %s', str_path) return sw.type_conformsToType_(uti, 'com.apple.bundle') or sw.type_conformsToType_(uti, 'com.apple.package') From cf34164191ed9add7164ffd521c757d01aa6f7be Mon Sep 17 00:00:00 2001 From: hsoft Date: Wed, 16 Dec 2009 14:48:37 +0000 Subject: [PATCH 255/275] dg cocoa: Since there are problems with the latest pyobjc + py2app + Snow Leopard, I've got to stick with pyobjc1.4 for a while after all... --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40264 --- base/py/app_cocoa.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/base/py/app_cocoa.py b/base/py/app_cocoa.py index 6169cd5b..fb030e05 100644 --- a/base/py/app_cocoa.py +++ b/base/py/app_cocoa.py @@ -7,6 +7,7 @@ # which should be included with this package. The terms are also available at # http://www.hardcoded.net/licenses/hs_license +import objc from Foundation import * from AppKit import * import logging @@ -55,8 +56,12 @@ class DupeGuru(app.DupeGuru): def _recycle_dupe(dupe): directory = unicode(dupe.path[:-1]) filename = dupe.name - result, tag = NSWorkspace.sharedWorkspace().performFileOperation_source_destination_files_tag_( - NSWorkspaceRecycleOperation, directory, '', [filename], None) + if objc.__version__ == '1.4': # For a while, we have to support this. + result, tag = NSWorkspace.sharedWorkspace().performFileOperation_source_destination_files_tag_( + NSWorkspaceRecycleOperation, directory, '', [filename]) + else: + result, tag = NSWorkspace.sharedWorkspace().performFileOperation_source_destination_files_tag_( + NSWorkspaceRecycleOperation, directory, '', [filename], None) def _start_job(self, jobid, func): try: From a6d2a9b7b31b4a2a14a27c7ca3935b06d45b330e Mon Sep 17 00:00:00 2001 From: hsoft Date: Wed, 16 Dec 2009 15:51:26 +0000 Subject: [PATCH 256/275] dgpe cocoa: Fixed a crash happening when iPhoto was never launched. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40265 --- pe/py/app_cocoa.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/pe/py/app_cocoa.py b/pe/py/app_cocoa.py index 4b3303e8..f7cbe73a 100644 --- a/pe/py/app_cocoa.py +++ b/pe/py/app_cocoa.py @@ -66,6 +66,8 @@ class IPhoto(Photo): def get_iphoto_database_path(): ud = NSUserDefaults.standardUserDefaults() prefs = ud.persistentDomainForName_('com.apple.iApps') + if prefs is None: + raise directories.InvalidPathError() if 'iPhotoRecentDatabases' not in prefs: raise directories.InvalidPathError() plisturl = NSURL.URLWithString_(prefs['iPhotoRecentDatabases'][0]) @@ -96,11 +98,16 @@ def get_iphoto_pictures(plistpath): class Directories(directories.Directories): def __init__(self): directories.Directories.__init__(self, fileclasses=[Photo]) - self.iphoto_libpath = get_iphoto_database_path() - self.set_state(self.iphoto_libpath[:-1], directories.STATE_EXCLUDED) + try: + self.iphoto_libpath = get_iphoto_database_path() + self.set_state(self.iphoto_libpath[:-1], directories.STATE_EXCLUDED) + except directories.InvalidPathError: + self.iphoto_libpath = None def _get_files(self, from_path): if from_path == Path('iPhoto Library'): + if self.iphoto_libpath is None: + return [] is_ref = self.get_state(from_path) == directories.STATE_REFERENCE photos = get_iphoto_pictures(self.iphoto_libpath) for photo in photos: @@ -180,7 +187,7 @@ class DupeGuruPE(app_cocoa.DupeGuru): def _get_file(self, str_path): p = Path(str_path) - if p in self.directories.iphoto_libpath[:-1]: + if (self.iphoto_libpath is not None) and (p in self.directories.iphoto_libpath[:-1]): return IPhoto(p) return app_cocoa.DupeGuru._get_file(self, str_path) From 4f6af6e4dcd60cf61316355faf3e0f8feb24fc7b Mon Sep 17 00:00:00 2001 From: hsoft Date: Wed, 16 Dec 2009 16:16:22 +0000 Subject: [PATCH 257/275] dgpe cocoa: ugh... fixed typo --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40266 --- pe/py/app_cocoa.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pe/py/app_cocoa.py b/pe/py/app_cocoa.py index f7cbe73a..660faa3a 100644 --- a/pe/py/app_cocoa.py +++ b/pe/py/app_cocoa.py @@ -187,7 +187,7 @@ class DupeGuruPE(app_cocoa.DupeGuru): def _get_file(self, str_path): p = Path(str_path) - if (self.iphoto_libpath is not None) and (p in self.directories.iphoto_libpath[:-1]): + if (self.directories.iphoto_libpath is not None) and (p in self.directories.iphoto_libpath[:-1]): return IPhoto(p) return app_cocoa.DupeGuru._get_file(self, str_path) From cf819dc0a834413509400567713660f2279e2385 Mon Sep 17 00:00:00 2001 From: hsoft Date: Fri, 18 Dec 2009 12:21:33 +0000 Subject: [PATCH 258/275] dgme qt: fixed gen script and updated FAQ. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40268 --- me/help/templates/faq.mako | 8 ++++++++ me/qt/gen.py | 1 + 2 files changed, 9 insertions(+) diff --git a/me/help/templates/faq.mako b/me/help/templates/faq.mako index 13e2e601..deb13f56 100644 --- a/me/help/templates/faq.mako +++ b/me/help/templates/faq.mako @@ -64,4 +64,12 @@ If your comparison threshold is low enough, you will probably end up with live a * **Mac OS X**: Type "[*]" in the "Filter" field in the toolbar. * Click on **Mark --> Mark All**. * Click on **Actions --> Remove Selected from Results**. + +### I tried to send my duplicates to Trash, but dupeGuru is telling me it can't do it. Why? What can I do? + +Most of the time, the reason why dupeGuru can't send files to Trash is because of file permissions. You need *write* permissions on files you want to send to Trash. If you're not familiar with the command line, you can use utilities such as [BatChmod](http://macchampion.com/arbysoft/BatchMod) to fix your permissions. + +If dupeGuru still gives you troubles after fixing your permissions, there have been some cases where using "Move Marked to..." as a workaround did the trick. So instead of sending your files to Trash, you send them to a temporary folder with the "Move Marked to..." action, and then you delete that temporary folder manually. + +If all of this fail, [contact HS support](http://www.hardcoded.net/support), we'll figure it out. \ No newline at end of file diff --git a/me/qt/gen.py b/me/qt/gen.py index 9510c5dc..c29e0a83 100644 --- a/me/qt/gen.py +++ b/me/qt/gen.py @@ -16,6 +16,7 @@ from hsutil.build import print_and_do, build_all_qt_ui build_all_qt_ui(op.join('qtlib', 'ui')) build_all_qt_ui('base') build_all_qt_ui('.') +print_and_do("pyrcc4 base\\dg.qrc > base\\dg_rc.py") os.chdir('help') print_and_do('python gen.py') From 6e226f32fdacf7cfa455442afe7ab19afae6e75f Mon Sep 17 00:00:00 2001 From: hsoft Date: Fri, 18 Dec 2009 12:57:47 +0000 Subject: [PATCH 259/275] dgme help: "packagified" help and updated to 5.7.0. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40269 --- me/help/__init__.py | 0 me/help/changelog.yaml | 9 +++++++++ me/help/gen.py | 10 +++++----- 3 files changed, 14 insertions(+), 5 deletions(-) create mode 100644 me/help/__init__.py diff --git a/me/help/__init__.py b/me/help/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/me/help/changelog.yaml b/me/help/changelog.yaml index 20ec0064..0b8ecaf1 100644 --- a/me/help/changelog.yaml +++ b/me/help/changelog.yaml @@ -1,3 +1,12 @@ +- date: 2009-12-18 + version: 5.7.0 + description: | + * Added drag & drop support in the Directories panel. (#9) + * Fixed a bug causing dupeGuru to be confused if a scanned file was moved during the scan. (#72) + * Clarified how directories' state are set by painting a combo box in the state cells. [Windows] + (#76) + * Fixed some crashes. (#78 and #79) + * Dropped Mac OS X Tiger support. - date: 2009-10-14 version: 5.6.6 description: | diff --git a/me/help/gen.py b/me/help/gen.py index 7fc7a854..d3b4da24 100644 --- a/me/help/gen.py +++ b/me/help/gen.py @@ -5,10 +5,10 @@ # which should be included with this package. The terms are also available at # http://www.hardcoded.net/licenses/hs_license -import os - +import os.path as op from hsdocgen import generate_help, filters -tix = filters.tixgen("https://hardcoded.lighthouseapp.com/projects/31699-dupeguru/tickets/{0}") - -generate_help.main('.', 'dupeguru_me_help', force_render=True, tix=tix) \ No newline at end of file +def generate(windows=False): + tix = filters.tixgen("https://hardcoded.lighthouseapp.com/projects/31699-dupeguru/tickets/{0}") + basepath = op.dirname(__file__) + generate_help.main(basepath, op.join(basepath, 'dupeguru_me_help'), force_render=True, tix=tix, windows=windows) From 67dff7fbf2a99e1e2766242518c630dc6679274b Mon Sep 17 00:00:00 2001 From: hsoft Date: Fri, 18 Dec 2009 12:58:14 +0000 Subject: [PATCH 260/275] dgme qt: v5.7.0 --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40270 --- me/qt/app.py | 2 +- me/qt/gen.py | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/me/qt/app.py b/me/qt/app.py index 5234c8fa..d4c32cdc 100644 --- a/me/qt/app.py +++ b/me/qt/app.py @@ -17,7 +17,7 @@ from preferences_dialog import PreferencesDialog class DupeGuru(DupeGuruBase): LOGO_NAME = 'logo_me' NAME = 'dupeGuru Music Edition' - VERSION = '5.6.6' + VERSION = '5.7.0' DELTA_COLUMNS = frozenset([2, 3, 4, 5, 7, 8]) def __init__(self): diff --git a/me/qt/gen.py b/me/qt/gen.py index c29e0a83..f71e4f81 100644 --- a/me/qt/gen.py +++ b/me/qt/gen.py @@ -13,11 +13,13 @@ import os.path as op from hsutil.build import print_and_do, build_all_qt_ui +from help import gen + build_all_qt_ui(op.join('qtlib', 'ui')) build_all_qt_ui('base') build_all_qt_ui('.') -print_and_do("pyrcc4 base\\dg.qrc > base\\dg_rc.py") - -os.chdir('help') -print_and_do('python gen.py') +os.chdir('base') +print_and_do("pyrcc4 dg.qrc > dg_rc.py") os.chdir('..') + +gen.generate(windows=True) From b8bb40de62ed4e94dee4442d1927b530f425d11c Mon Sep 17 00:00:00 2001 From: hsoft Date: Fri, 18 Dec 2009 13:04:30 +0000 Subject: [PATCH 261/275] dgme cocoa: v5.7.0 --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40271 --- me/cocoa/Info.plist | 2 +- me/cocoa/gen.py | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/me/cocoa/Info.plist b/me/cocoa/Info.plist index f3e12846..e538360f 100644 --- a/me/cocoa/Info.plist +++ b/me/cocoa/Info.plist @@ -23,7 +23,7 @@ CFBundleSignature hsft CFBundleVersion - 5.6.6 + 5.7.0 NSMainNibFile MainMenu NSPrincipalClass diff --git a/me/cocoa/gen.py b/me/cocoa/gen.py index ad9be98e..ea248ae7 100644 --- a/me/cocoa/gen.py +++ b/me/cocoa/gen.py @@ -5,13 +5,15 @@ # which should be included with this package. The terms are also available at # http://www.hardcoded.net/licenses/hs_license +import sys +sys.path.insert(0, 'py') # for hsutil and hsdocgen import os +from help import gen + print "Generating help" -os.chdir('help') -os.system('python -u gen.py') -os.system('/Developer/Applications/Utilities/Help\\ Indexer.app/Contents/MacOS/Help\\ Indexer dupeguru_me_help') -os.chdir('..') +gen.generate() +os.system('/Developer/Applications/Utilities/Help\\ Indexer.app/Contents/MacOS/Help\\ Indexer help/dupeguru_me_help') print "Generating py plugin" os.chdir('py') From 63aad8ca84af40981a90b55a8afc2a6c42760bd0 Mon Sep 17 00:00:00 2001 From: hsoft Date: Wed, 30 Dec 2009 10:22:33 +0000 Subject: [PATCH 262/275] Renamed se/py to se/core --HG-- rename : se/py/LICENSE => se/core/LICENSE rename : se/py/__init__.py => se/core/__init__.py rename : se/py/app_cocoa.py => se/core/app_cocoa.py rename : se/py/data.py => se/core/data.py rename : se/py/fs.py => se/core/fs.py rename : se/py/tests/__init__.py => se/core/tests/__init__.py rename : se/py/tests/fs_test.py => se/core/tests/fs_test.py extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40273 --- se/{py => core}/LICENSE | 0 se/{py => core}/__init__.py | 0 se/{py => core}/app_cocoa.py | 0 se/{py => core}/data.py | 0 se/{py => core}/fs.py | 0 se/{py => core}/tests/__init__.py | 0 se/{py => core}/tests/fs_test.py | 0 7 files changed, 0 insertions(+), 0 deletions(-) rename se/{py => core}/LICENSE (100%) rename se/{py => core}/__init__.py (100%) rename se/{py => core}/app_cocoa.py (100%) rename se/{py => core}/data.py (100%) rename se/{py => core}/fs.py (100%) rename se/{py => core}/tests/__init__.py (100%) rename se/{py => core}/tests/fs_test.py (100%) diff --git a/se/py/LICENSE b/se/core/LICENSE similarity index 100% rename from se/py/LICENSE rename to se/core/LICENSE diff --git a/se/py/__init__.py b/se/core/__init__.py similarity index 100% rename from se/py/__init__.py rename to se/core/__init__.py diff --git a/se/py/app_cocoa.py b/se/core/app_cocoa.py similarity index 100% rename from se/py/app_cocoa.py rename to se/core/app_cocoa.py diff --git a/se/py/data.py b/se/core/data.py similarity index 100% rename from se/py/data.py rename to se/core/data.py diff --git a/se/py/fs.py b/se/core/fs.py similarity index 100% rename from se/py/fs.py rename to se/core/fs.py diff --git a/se/py/tests/__init__.py b/se/core/tests/__init__.py similarity index 100% rename from se/py/tests/__init__.py rename to se/core/tests/__init__.py diff --git a/se/py/tests/fs_test.py b/se/core/tests/fs_test.py similarity index 100% rename from se/py/tests/fs_test.py rename to se/core/tests/fs_test.py From 74dba7cb6ceda9fe8851a566a6331f213cf3541e Mon Sep 17 00:00:00 2001 From: hsoft Date: Wed, 30 Dec 2009 10:23:02 +0000 Subject: [PATCH 263/275] Renamed pe/py to pe/core --HG-- rename : pe/py/LICENSE => pe/core/LICENSE rename : pe/py/__init__.py => pe/core/__init__.py rename : pe/py/app_cocoa.py => pe/core/app_cocoa.py rename : pe/py/block.py => pe/core/block.py rename : pe/py/cache.py => pe/core/cache.py rename : pe/py/data.py => pe/core/data.py rename : pe/py/gen.py => pe/core/gen.py rename : pe/py/matchbase.py => pe/core/matchbase.py rename : pe/py/modules/block/block.pyx => pe/core/modules/block/block.pyx rename : pe/py/modules/block/setup.py => pe/core/modules/block/setup.py rename : pe/py/modules/cache/cache.pyx => pe/core/modules/cache/cache.pyx rename : pe/py/modules/cache/setup.py => pe/core/modules/cache/setup.py rename : pe/py/scanner.py => pe/core/scanner.py rename : pe/py/tests/__init__.py => pe/core/tests/__init__.py rename : pe/py/tests/block_test.py => pe/core/tests/block_test.py rename : pe/py/tests/cache_test.py => pe/core/tests/cache_test.py extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40274 --- pe/{py => core}/LICENSE | 0 pe/{py => core}/__init__.py | 0 pe/{py => core}/app_cocoa.py | 0 pe/{py => core}/block.py | 0 pe/{py => core}/cache.py | 0 pe/{py => core}/data.py | 0 pe/{py => core}/gen.py | 0 pe/{py => core}/matchbase.py | 0 pe/{py => core}/modules/block/block.pyx | 0 pe/{py => core}/modules/block/setup.py | 0 pe/{py => core}/modules/cache/cache.pyx | 0 pe/{py => core}/modules/cache/setup.py | 0 pe/{py => core}/scanner.py | 0 pe/{py => core}/tests/__init__.py | 0 pe/{py => core}/tests/block_test.py | 0 pe/{py => core}/tests/cache_test.py | 0 16 files changed, 0 insertions(+), 0 deletions(-) rename pe/{py => core}/LICENSE (100%) rename pe/{py => core}/__init__.py (100%) rename pe/{py => core}/app_cocoa.py (100%) rename pe/{py => core}/block.py (100%) rename pe/{py => core}/cache.py (100%) rename pe/{py => core}/data.py (100%) rename pe/{py => core}/gen.py (100%) rename pe/{py => core}/matchbase.py (100%) rename pe/{py => core}/modules/block/block.pyx (100%) rename pe/{py => core}/modules/block/setup.py (100%) rename pe/{py => core}/modules/cache/cache.pyx (100%) rename pe/{py => core}/modules/cache/setup.py (100%) rename pe/{py => core}/scanner.py (100%) rename pe/{py => core}/tests/__init__.py (100%) rename pe/{py => core}/tests/block_test.py (100%) rename pe/{py => core}/tests/cache_test.py (100%) diff --git a/pe/py/LICENSE b/pe/core/LICENSE similarity index 100% rename from pe/py/LICENSE rename to pe/core/LICENSE diff --git a/pe/py/__init__.py b/pe/core/__init__.py similarity index 100% rename from pe/py/__init__.py rename to pe/core/__init__.py diff --git a/pe/py/app_cocoa.py b/pe/core/app_cocoa.py similarity index 100% rename from pe/py/app_cocoa.py rename to pe/core/app_cocoa.py diff --git a/pe/py/block.py b/pe/core/block.py similarity index 100% rename from pe/py/block.py rename to pe/core/block.py diff --git a/pe/py/cache.py b/pe/core/cache.py similarity index 100% rename from pe/py/cache.py rename to pe/core/cache.py diff --git a/pe/py/data.py b/pe/core/data.py similarity index 100% rename from pe/py/data.py rename to pe/core/data.py diff --git a/pe/py/gen.py b/pe/core/gen.py similarity index 100% rename from pe/py/gen.py rename to pe/core/gen.py diff --git a/pe/py/matchbase.py b/pe/core/matchbase.py similarity index 100% rename from pe/py/matchbase.py rename to pe/core/matchbase.py diff --git a/pe/py/modules/block/block.pyx b/pe/core/modules/block/block.pyx similarity index 100% rename from pe/py/modules/block/block.pyx rename to pe/core/modules/block/block.pyx diff --git a/pe/py/modules/block/setup.py b/pe/core/modules/block/setup.py similarity index 100% rename from pe/py/modules/block/setup.py rename to pe/core/modules/block/setup.py diff --git a/pe/py/modules/cache/cache.pyx b/pe/core/modules/cache/cache.pyx similarity index 100% rename from pe/py/modules/cache/cache.pyx rename to pe/core/modules/cache/cache.pyx diff --git a/pe/py/modules/cache/setup.py b/pe/core/modules/cache/setup.py similarity index 100% rename from pe/py/modules/cache/setup.py rename to pe/core/modules/cache/setup.py diff --git a/pe/py/scanner.py b/pe/core/scanner.py similarity index 100% rename from pe/py/scanner.py rename to pe/core/scanner.py diff --git a/pe/py/tests/__init__.py b/pe/core/tests/__init__.py similarity index 100% rename from pe/py/tests/__init__.py rename to pe/core/tests/__init__.py diff --git a/pe/py/tests/block_test.py b/pe/core/tests/block_test.py similarity index 100% rename from pe/py/tests/block_test.py rename to pe/core/tests/block_test.py diff --git a/pe/py/tests/cache_test.py b/pe/core/tests/cache_test.py similarity index 100% rename from pe/py/tests/cache_test.py rename to pe/core/tests/cache_test.py From 252638018433823d2bbf4ffb158ff49c367a9758 Mon Sep 17 00:00:00 2001 From: hsoft Date: Wed, 30 Dec 2009 10:23:31 +0000 Subject: [PATCH 264/275] Renamed me/py to me/core --HG-- rename : me/py/__init__.py => me/core/__init__.py rename : me/py/app_cocoa.py => me/core/app_cocoa.py rename : me/py/data.py => me/core/data.py rename : me/py/fs.py => me/core/fs.py rename : me/py/scanner.py => me/core/scanner.py rename : me/py/tests/__init__.py => me/core/tests/__init__.py rename : me/py/tests/scanner_test.py => me/core/tests/scanner_test.py extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40275 --- me/{py => core}/__init__.py | 0 me/{py => core}/app_cocoa.py | 0 me/{py => core}/data.py | 0 me/{py => core}/fs.py | 0 me/{py => core}/scanner.py | 0 me/{py => core}/tests/__init__.py | 0 me/{py => core}/tests/scanner_test.py | 0 7 files changed, 0 insertions(+), 0 deletions(-) rename me/{py => core}/__init__.py (100%) rename me/{py => core}/app_cocoa.py (100%) rename me/{py => core}/data.py (100%) rename me/{py => core}/fs.py (100%) rename me/{py => core}/scanner.py (100%) rename me/{py => core}/tests/__init__.py (100%) rename me/{py => core}/tests/scanner_test.py (100%) diff --git a/me/py/__init__.py b/me/core/__init__.py similarity index 100% rename from me/py/__init__.py rename to me/core/__init__.py diff --git a/me/py/app_cocoa.py b/me/core/app_cocoa.py similarity index 100% rename from me/py/app_cocoa.py rename to me/core/app_cocoa.py diff --git a/me/py/data.py b/me/core/data.py similarity index 100% rename from me/py/data.py rename to me/core/data.py diff --git a/me/py/fs.py b/me/core/fs.py similarity index 100% rename from me/py/fs.py rename to me/core/fs.py diff --git a/me/py/scanner.py b/me/core/scanner.py similarity index 100% rename from me/py/scanner.py rename to me/core/scanner.py diff --git a/me/py/tests/__init__.py b/me/core/tests/__init__.py similarity index 100% rename from me/py/tests/__init__.py rename to me/core/tests/__init__.py diff --git a/me/py/tests/scanner_test.py b/me/core/tests/scanner_test.py similarity index 100% rename from me/py/tests/scanner_test.py rename to me/core/tests/scanner_test.py From 1e18a08998f0b1978332733f1116e1e2745b72ff Mon Sep 17 00:00:00 2001 From: hsoft Date: Wed, 30 Dec 2009 10:23:59 +0000 Subject: [PATCH 265/275] Renamed base/py to base/core --HG-- rename : base/py/LICENSE => base/core/LICENSE rename : base/py/__init__.py => base/core/__init__.py rename : base/py/app.py => base/core/app.py rename : base/py/app_cocoa.py => base/core/app_cocoa.py rename : base/py/data.py => base/core/data.py rename : base/py/directories.py => base/core/directories.py rename : base/py/engine.py => base/core/engine.py rename : base/py/export.py => base/core/export.py rename : base/py/fs.py => base/core/fs.py rename : base/py/ignore.py => base/core/ignore.py rename : base/py/results.py => base/core/results.py rename : base/py/scanner.py => base/core/scanner.py rename : base/py/tests/__init__.py => base/core/tests/__init__.py rename : base/py/tests/app_cocoa_test.py => base/core/tests/app_cocoa_test.py rename : base/py/tests/app_test.py => base/core/tests/app_test.py rename : base/py/tests/data.py => base/core/tests/data.py rename : base/py/tests/directories_test.py => base/core/tests/directories_test.py rename : base/py/tests/engine_test.py => base/core/tests/engine_test.py rename : base/py/tests/ignore_test.py => base/core/tests/ignore_test.py rename : base/py/tests/results_test.py => base/core/tests/results_test.py rename : base/py/tests/scanner_test.py => base/core/tests/scanner_test.py extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40276 --- base/{py => core}/LICENSE | 0 base/{py => core}/__init__.py | 0 base/{py => core}/app.py | 0 base/{py => core}/app_cocoa.py | 0 base/{py => core}/data.py | 0 base/{py => core}/directories.py | 0 base/{py => core}/engine.py | 0 base/{py => core}/export.py | 0 base/{py => core}/fs.py | 0 base/{py => core}/ignore.py | 0 base/{py => core}/results.py | 0 base/{py => core}/scanner.py | 0 base/{py => core}/tests/__init__.py | 0 base/{py => core}/tests/app_cocoa_test.py | 0 base/{py => core}/tests/app_test.py | 0 base/{py => core}/tests/data.py | 0 base/{py => core}/tests/directories_test.py | 0 base/{py => core}/tests/engine_test.py | 0 base/{py => core}/tests/ignore_test.py | 0 base/{py => core}/tests/results_test.py | 0 base/{py => core}/tests/scanner_test.py | 0 21 files changed, 0 insertions(+), 0 deletions(-) rename base/{py => core}/LICENSE (100%) rename base/{py => core}/__init__.py (100%) rename base/{py => core}/app.py (100%) rename base/{py => core}/app_cocoa.py (100%) rename base/{py => core}/data.py (100%) rename base/{py => core}/directories.py (100%) rename base/{py => core}/engine.py (100%) rename base/{py => core}/export.py (100%) rename base/{py => core}/fs.py (100%) rename base/{py => core}/ignore.py (100%) rename base/{py => core}/results.py (100%) rename base/{py => core}/scanner.py (100%) rename base/{py => core}/tests/__init__.py (100%) rename base/{py => core}/tests/app_cocoa_test.py (100%) rename base/{py => core}/tests/app_test.py (100%) rename base/{py => core}/tests/data.py (100%) rename base/{py => core}/tests/directories_test.py (100%) rename base/{py => core}/tests/engine_test.py (100%) rename base/{py => core}/tests/ignore_test.py (100%) rename base/{py => core}/tests/results_test.py (100%) rename base/{py => core}/tests/scanner_test.py (100%) diff --git a/base/py/LICENSE b/base/core/LICENSE similarity index 100% rename from base/py/LICENSE rename to base/core/LICENSE diff --git a/base/py/__init__.py b/base/core/__init__.py similarity index 100% rename from base/py/__init__.py rename to base/core/__init__.py diff --git a/base/py/app.py b/base/core/app.py similarity index 100% rename from base/py/app.py rename to base/core/app.py diff --git a/base/py/app_cocoa.py b/base/core/app_cocoa.py similarity index 100% rename from base/py/app_cocoa.py rename to base/core/app_cocoa.py diff --git a/base/py/data.py b/base/core/data.py similarity index 100% rename from base/py/data.py rename to base/core/data.py diff --git a/base/py/directories.py b/base/core/directories.py similarity index 100% rename from base/py/directories.py rename to base/core/directories.py diff --git a/base/py/engine.py b/base/core/engine.py similarity index 100% rename from base/py/engine.py rename to base/core/engine.py diff --git a/base/py/export.py b/base/core/export.py similarity index 100% rename from base/py/export.py rename to base/core/export.py diff --git a/base/py/fs.py b/base/core/fs.py similarity index 100% rename from base/py/fs.py rename to base/core/fs.py diff --git a/base/py/ignore.py b/base/core/ignore.py similarity index 100% rename from base/py/ignore.py rename to base/core/ignore.py diff --git a/base/py/results.py b/base/core/results.py similarity index 100% rename from base/py/results.py rename to base/core/results.py diff --git a/base/py/scanner.py b/base/core/scanner.py similarity index 100% rename from base/py/scanner.py rename to base/core/scanner.py diff --git a/base/py/tests/__init__.py b/base/core/tests/__init__.py similarity index 100% rename from base/py/tests/__init__.py rename to base/core/tests/__init__.py diff --git a/base/py/tests/app_cocoa_test.py b/base/core/tests/app_cocoa_test.py similarity index 100% rename from base/py/tests/app_cocoa_test.py rename to base/core/tests/app_cocoa_test.py diff --git a/base/py/tests/app_test.py b/base/core/tests/app_test.py similarity index 100% rename from base/py/tests/app_test.py rename to base/core/tests/app_test.py diff --git a/base/py/tests/data.py b/base/core/tests/data.py similarity index 100% rename from base/py/tests/data.py rename to base/core/tests/data.py diff --git a/base/py/tests/directories_test.py b/base/core/tests/directories_test.py similarity index 100% rename from base/py/tests/directories_test.py rename to base/core/tests/directories_test.py diff --git a/base/py/tests/engine_test.py b/base/core/tests/engine_test.py similarity index 100% rename from base/py/tests/engine_test.py rename to base/core/tests/engine_test.py diff --git a/base/py/tests/ignore_test.py b/base/core/tests/ignore_test.py similarity index 100% rename from base/py/tests/ignore_test.py rename to base/core/tests/ignore_test.py diff --git a/base/py/tests/results_test.py b/base/core/tests/results_test.py similarity index 100% rename from base/py/tests/results_test.py rename to base/core/tests/results_test.py diff --git a/base/py/tests/scanner_test.py b/base/core/tests/scanner_test.py similarity index 100% rename from base/py/tests/scanner_test.py rename to base/core/tests/scanner_test.py From 6eba99eba16576ea3ddd4c84110861948cf28440 Mon Sep 17 00:00:00 2001 From: hsoft Date: Wed, 30 Dec 2009 10:26:50 +0000 Subject: [PATCH 266/275] Adjusted externals to the py --> core renames. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40277 From f5accbfaed0feeca926919d3de02af354a29d602 Mon Sep 17 00:00:00 2001 From: hsoft Date: Wed, 30 Dec 2009 10:37:57 +0000 Subject: [PATCH 267/275] Changed dupeguru and dupeguru_* external references to core and core_* references. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40278 --- base/core/tests/data.py | 2 +- base/qt/app.py | 5 ++--- base/qt/main_window.py | 2 +- me/cocoa/py/dg_cocoa.py | 8 ++++---- me/core/app_cocoa.py | 2 +- me/core/data.py | 2 +- me/core/fs.py | 2 +- me/core/scanner.py | 2 +- me/core/tests/scanner_test.py | 2 +- me/qt/app.py | 2 +- me/qt/preferences.py | 2 +- me/qt/preferences_dialog.py | 2 +- pe/cocoa/py/dg_cocoa.py | 6 +++--- pe/core/app_cocoa.py | 4 ++-- pe/core/data.py | 2 +- pe/core/matchbase.py | 2 +- pe/core/scanner.py | 2 +- pe/qt/app.py | 8 ++++---- se/cocoa/py/dg_cocoa.py | 8 ++++---- se/core/app_cocoa.py | 6 +++--- se/core/data.py | 2 +- se/core/fs.py | 2 +- se/core/tests/fs_test.py | 4 ++-- se/qt/app.py | 4 ++-- se/qt/preferences.py | 2 +- se/qt/preferences_dialog.py | 2 +- 26 files changed, 43 insertions(+), 44 deletions(-) diff --git a/base/core/tests/data.py b/base/core/tests/data.py index d71582c5..09318b51 100644 --- a/base/core/tests/data.py +++ b/base/core/tests/data.py @@ -11,7 +11,7 @@ # data module for tests from hsutil.str import format_size -from dupeguru.data import format_path, cmp_value +from ..data import format_path, cmp_value COLUMNS = [ {'attr':'name','display':'Filename'}, diff --git a/base/qt/app.py b/base/qt/app.py index d0839d0d..fe42878c 100644 --- a/base/qt/app.py +++ b/base/qt/app.py @@ -19,9 +19,8 @@ from PyQt4.QtGui import QProgressDialog, QDesktopServices, QFileDialog, QDialog, from hsutil import job from hsutil.reg import RegistrationRequired -from dupeguru import fs -from dupeguru.app import (DupeGuru as DupeGuruBase, JOB_SCAN, JOB_LOAD, JOB_MOVE, JOB_COPY, - JOB_DELETE) +from core import fs +from core.app import DupeGuru as DupeGuruBase, JOB_SCAN, JOB_LOAD, JOB_MOVE, JOB_COPY, JOB_DELETE from qtlib.about_box import AboutBox from qtlib.progress import Progress diff --git a/base/qt/main_window.py b/base/qt/main_window.py index c19416e7..4d6c0c0c 100644 --- a/base/qt/main_window.py +++ b/base/qt/main_window.py @@ -13,7 +13,7 @@ from PyQt4.QtGui import (QMainWindow, QMenu, QPixmap, QIcon, QToolButton, QLabel from hsutil.misc import nonone -from dupeguru.app import NoScannableFileError, AllFilesAreRefError +from core.app import NoScannableFileError, AllFilesAreRefError import dg_rc from main_window_ui import Ui_MainWindow diff --git a/me/cocoa/py/dg_cocoa.py b/me/cocoa/py/dg_cocoa.py index 0bb65ba4..a8a87f50 100644 --- a/me/cocoa/py/dg_cocoa.py +++ b/me/cocoa/py/dg_cocoa.py @@ -8,13 +8,13 @@ import objc from AppKit import * -from dupeguru_me.app_cocoa import DupeGuruME -from dupeguru.scanner import (SCAN_TYPE_FILENAME, SCAN_TYPE_FIELDS, SCAN_TYPE_FIELDS_NO_ORDER, +from core_me.app_cocoa import DupeGuruME +from core.scanner import (SCAN_TYPE_FILENAME, SCAN_TYPE_FIELDS, SCAN_TYPE_FIELDS_NO_ORDER, SCAN_TYPE_TAG, SCAN_TYPE_CONTENT, SCAN_TYPE_CONTENT_AUDIO) # Fix py2app imports which chokes on relative imports -from dupeguru_me import app_cocoa, data, fs, scanner -from dupeguru import app, app_cocoa, data, directories, engine, export, ignore, results, scanner, fs +from core_me import app_cocoa, data, fs, scanner +from core import app, app_cocoa, data, directories, engine, export, ignore, results, scanner, fs from hsmedia import aiff, flac, genres, id3v1, id3v2, mp4, mpeg, ogg, wma from hsutil import conflict diff --git a/me/core/app_cocoa.py b/me/core/app_cocoa.py index 692f847f..d810d1bf 100644 --- a/me/core/app_cocoa.py +++ b/me/core/app_cocoa.py @@ -13,7 +13,7 @@ import time from hsutil.cocoa import as_fetch -from dupeguru.app_cocoa import JOBID2TITLE, DupeGuru as DupeGuruBase +from core.app_cocoa import JOBID2TITLE, DupeGuru as DupeGuruBase from . import data, scanner, fs diff --git a/me/core/data.py b/me/core/data.py index ad9edda5..14ed0c99 100644 --- a/me/core/data.py +++ b/me/core/data.py @@ -8,7 +8,7 @@ # http://www.hardcoded.net/licenses/hs_license from hsutil.str import format_time, FT_MINUTES, format_size -from dupeguru.data import (format_path, format_timestamp, format_words, format_perc, +from core.data import (format_path, format_timestamp, format_words, format_perc, format_dupe_count, cmp_value) COLUMNS = [ diff --git a/me/core/fs.py b/me/core/fs.py index 0a47e709..9969d4c4 100644 --- a/me/core/fs.py +++ b/me/core/fs.py @@ -10,7 +10,7 @@ from hsmedia import mpeg, wma, mp4, ogg, flac, aiff from hsutil.str import get_file_ext -from dupeguru import fs +from core import fs TAG_FIELDS = ['audiosize', 'duration', 'bitrate', 'samplerate', 'title', 'artist', 'album', 'genre', 'year', 'track', 'comment'] diff --git a/me/core/scanner.py b/me/core/scanner.py index 7fce8427..45c066ba 100644 --- a/me/core/scanner.py +++ b/me/core/scanner.py @@ -7,7 +7,7 @@ # which should be included with this package. The terms are also available at # http://www.hardcoded.net/licenses/hs_license -from dupeguru.scanner import Scanner as ScannerBase +from core.scanner import Scanner as ScannerBase class ScannerME(ScannerBase): @staticmethod diff --git a/me/core/tests/scanner_test.py b/me/core/tests/scanner_test.py index 6ab32a6d..f14e7856 100644 --- a/me/core/tests/scanner_test.py +++ b/me/core/tests/scanner_test.py @@ -10,7 +10,7 @@ from hsutil.path import Path -from dupeguru.engine import getwords +from core.engine import getwords from ..scanner import * class NamedObject(object): diff --git a/me/qt/app.py b/me/qt/app.py index d4c32cdc..62c21f60 100644 --- a/me/qt/app.py +++ b/me/qt/app.py @@ -7,7 +7,7 @@ # which should be included with this package. The terms are also available at # http://www.hardcoded.net/licenses/hs_license -from dupeguru_me import data, scanner, fs +from core_me import data, scanner, fs from base.app import DupeGuru as DupeGuruBase from details_dialog import DetailsDialog diff --git a/me/qt/preferences.py b/me/qt/preferences.py index 43bf1306..5b4fd535 100644 --- a/me/qt/preferences.py +++ b/me/qt/preferences.py @@ -7,7 +7,7 @@ # which should be included with this package. The terms are also available at # http://www.hardcoded.net/licenses/hs_license -from dupeguru.scanner import (SCAN_TYPE_FILENAME, SCAN_TYPE_FIELDS, SCAN_TYPE_FIELDS_NO_ORDER, +from core.scanner import (SCAN_TYPE_FILENAME, SCAN_TYPE_FIELDS, SCAN_TYPE_FIELDS_NO_ORDER, SCAN_TYPE_TAG, SCAN_TYPE_CONTENT, SCAN_TYPE_CONTENT_AUDIO) from base.preferences import Preferences as PreferencesBase diff --git a/me/qt/preferences_dialog.py b/me/qt/preferences_dialog.py index 11905db1..e47f8aaa 100644 --- a/me/qt/preferences_dialog.py +++ b/me/qt/preferences_dialog.py @@ -10,7 +10,7 @@ from PyQt4.QtCore import SIGNAL, Qt from PyQt4.QtGui import QDialog, QDialogButtonBox -from dupeguru.scanner import (SCAN_TYPE_FILENAME, SCAN_TYPE_FIELDS, SCAN_TYPE_FIELDS_NO_ORDER, +from core.scanner import (SCAN_TYPE_FILENAME, SCAN_TYPE_FIELDS, SCAN_TYPE_FIELDS_NO_ORDER, SCAN_TYPE_TAG, SCAN_TYPE_CONTENT, SCAN_TYPE_CONTENT_AUDIO) from preferences_dialog_ui import Ui_PreferencesDialog diff --git a/pe/cocoa/py/dg_cocoa.py b/pe/cocoa/py/dg_cocoa.py index 32c5575f..57a6e92f 100644 --- a/pe/cocoa/py/dg_cocoa.py +++ b/pe/cocoa/py/dg_cocoa.py @@ -7,11 +7,11 @@ import objc from AppKit import * -from dupeguru_pe import app_cocoa as app_pe_cocoa +from core_pe import app_cocoa as app_pe_cocoa # Fix py2app imports which chokes on relative imports -from dupeguru import app, app_cocoa, data, directories, engine, export, ignore, results, scanner -from dupeguru_pe import block, cache, matchbase, data +from core import app, app_cocoa, data, directories, engine, export, ignore, results, scanner +from core_pe import block, cache, matchbase, data from hsutil import conflict class PyApp(NSObject): diff --git a/pe/core/app_cocoa.py b/pe/core/app_cocoa.py index 660faa3a..cc3c291d 100644 --- a/pe/core/app_cocoa.py +++ b/pe/core/app_cocoa.py @@ -21,8 +21,8 @@ from hsutil.str import get_file_ext from hsutil.path import Path from hsutil.cocoa import as_fetch -from dupeguru import fs -from dupeguru import app_cocoa, directories +from core import fs +from core import app_cocoa, directories from . import data from .cache import string_to_colors, Cache from .scanner import ScannerPE diff --git a/pe/core/data.py b/pe/core/data.py index f13cf14b..356cdf70 100644 --- a/pe/core/data.py +++ b/pe/core/data.py @@ -8,7 +8,7 @@ # http://www.hardcoded.net/licenses/hs_license from hsutil.str import format_size -from dupeguru.data import format_path, format_timestamp, format_perc, format_dupe_count, cmp_value +from core.data import format_path, format_timestamp, format_perc, format_dupe_count, cmp_value def format_dimensions(dimensions): return '%d x %d' % (dimensions[0], dimensions[1]) diff --git a/pe/core/matchbase.py b/pe/core/matchbase.py index 8cfae038..4eea13f9 100644 --- a/pe/core/matchbase.py +++ b/pe/core/matchbase.py @@ -15,7 +15,7 @@ from collections import defaultdict from hsutil import job from hsutil.misc import dedupe -from dupeguru.engine import Match +from core.engine import Match from .block import avgdiff, DifferentBlockCountError, NoBlocksError from .cache import Cache diff --git a/pe/core/scanner.py b/pe/core/scanner.py index b25f0011..9e1c8cce 100644 --- a/pe/core/scanner.py +++ b/pe/core/scanner.py @@ -8,7 +8,7 @@ # which should be included with this package. The terms are also available at # http://www.hardcoded.net/licenses/hs_license -from dupeguru.scanner import Scanner +from core.scanner import Scanner from . import matchbase diff --git a/pe/qt/app.py b/pe/qt/app.py index 4bdee272..39642220 100644 --- a/pe/qt/app.py +++ b/pe/qt/app.py @@ -15,10 +15,10 @@ import PIL.Image from hsutil.str import get_file_ext -from dupeguru import fs -from dupeguru_pe import data as data_pe -from dupeguru_pe.cache import Cache -from dupeguru_pe.scanner import ScannerPE +from core import fs +from core_pe import data as data_pe +from core_pe.cache import Cache +from core_pe.scanner import ScannerPE from block import getblocks from base.app import DupeGuru as DupeGuruBase diff --git a/se/cocoa/py/dg_cocoa.py b/se/cocoa/py/dg_cocoa.py index 11ce95b6..d9444c17 100644 --- a/se/cocoa/py/dg_cocoa.py +++ b/se/cocoa/py/dg_cocoa.py @@ -8,12 +8,12 @@ import objc from AppKit import * -from dupeguru_se.app_cocoa import DupeGuru -from dupeguru import scanner +from core_se.app_cocoa import DupeGuru +from core import scanner # Fix py2app imports with chokes on relative imports -from dupeguru_se import fs, data -from dupeguru import app, app_cocoa, data, directories, engine, export, ignore, results, fs +from core_se import fs, data +from core import app, app_cocoa, data, directories, engine, export, ignore, results, fs from hsutil import conflict class PyApp(NSObject): diff --git a/se/core/app_cocoa.py b/se/core/app_cocoa.py index 25c33b01..9e0f4e49 100644 --- a/se/core/app_cocoa.py +++ b/se/core/app_cocoa.py @@ -18,9 +18,9 @@ from hsutil import io from hsutil.path import Path from hsutil.str import get_file_ext -from dupeguru import fs -from dupeguru.app_cocoa import DupeGuru as DupeGuruBase -from dupeguru.directories import Directories as DirectoriesBase, STATE_EXCLUDED +from core import fs +from core.app_cocoa import DupeGuru as DupeGuruBase +from core.directories import Directories as DirectoriesBase, STATE_EXCLUDED from . import data from .fs import Bundle as BundleBase diff --git a/se/core/data.py b/se/core/data.py index dc353319..ff6a4d33 100644 --- a/se/core/data.py +++ b/se/core/data.py @@ -8,7 +8,7 @@ # http://www.hardcoded.net/licenses/hs_license from hsutil.str import format_size -from dupeguru.data import (format_path, format_timestamp, format_words, format_perc, +from core.data import (format_path, format_timestamp, format_words, format_perc, format_dupe_count, cmp_value) COLUMNS = [ diff --git a/se/core/fs.py b/se/core/fs.py index dc7d0025..692d0bb6 100644 --- a/se/core/fs.py +++ b/se/core/fs.py @@ -13,7 +13,7 @@ import hashlib from hsutil import io from hsutil.misc import nonone -from dupeguru import fs +from core import fs class Bundle(fs.File): """This class is for Mac OSX bundles (.app). Bundles are seen by the OS as diff --git a/se/core/tests/fs_test.py b/se/core/tests/fs_test.py index c948ede7..045d40fa 100644 --- a/se/core/tests/fs_test.py +++ b/se/core/tests/fs_test.py @@ -13,8 +13,8 @@ import hashlib from nose.tools import eq_ from hsutil.testcase import TestCase -from dupeguru.fs import File -from dupeguru.tests.directories_test import create_fake_fs +from core.fs import File +from core.tests.directories_test import create_fake_fs from .. import fs diff --git a/se/qt/app.py b/se/qt/app.py index 2d7f5a17..913b8dba 100644 --- a/se/qt/app.py +++ b/se/qt/app.py @@ -7,8 +7,8 @@ # which should be included with this package. The terms are also available at # http://www.hardcoded.net/licenses/hs_license -from dupeguru_se import data -from dupeguru.directories import Directories as DirectoriesBase, STATE_EXCLUDED +from core_se import data +from core.directories import Directories as DirectoriesBase, STATE_EXCLUDED from base.app import DupeGuru as DupeGuruBase from details_dialog import DetailsDialog diff --git a/se/qt/preferences.py b/se/qt/preferences.py index 0ed1c954..2ebd0e85 100644 --- a/se/qt/preferences.py +++ b/se/qt/preferences.py @@ -7,7 +7,7 @@ # which should be included with this package. The terms are also available at # http://www.hardcoded.net/licenses/hs_license -from dupeguru.scanner import SCAN_TYPE_FILENAME, SCAN_TYPE_CONTENT +from core.scanner import SCAN_TYPE_FILENAME, SCAN_TYPE_CONTENT from base.preferences import Preferences as PreferencesBase diff --git a/se/qt/preferences_dialog.py b/se/qt/preferences_dialog.py index 3a1bf1b0..15fb31fd 100644 --- a/se/qt/preferences_dialog.py +++ b/se/qt/preferences_dialog.py @@ -12,7 +12,7 @@ from PyQt4.QtGui import QDialog, QDialogButtonBox from hsutil.misc import tryint -from dupeguru.scanner import SCAN_TYPE_FILENAME, SCAN_TYPE_CONTENT +from core.scanner import SCAN_TYPE_FILENAME, SCAN_TYPE_CONTENT from preferences_dialog_ui import Ui_PreferencesDialog import preferences From 4d44753f6e0b031d6564a6b20f45ce56262423bd Mon Sep 17 00:00:00 2001 From: hsoft Date: Wed, 30 Dec 2009 10:40:48 +0000 Subject: [PATCH 268/275] cocoa se: updated the project for 10.5-updated cocoalib. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40279 --- se/cocoa/dupeguru.xcodeproj/project.pbxproj | 6 ------ 1 file changed, 6 deletions(-) diff --git a/se/cocoa/dupeguru.xcodeproj/project.pbxproj b/se/cocoa/dupeguru.xcodeproj/project.pbxproj index 9774ee3d..de0d842d 100644 --- a/se/cocoa/dupeguru.xcodeproj/project.pbxproj +++ b/se/cocoa/dupeguru.xcodeproj/project.pbxproj @@ -23,7 +23,6 @@ CECA899C09DB132E00A3D774 /* DetailsPanel.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = CECA899A09DB132E00A3D774 /* DetailsPanel.h */; }; CECA899D09DB132E00A3D774 /* DetailsPanel.m in Sources */ = {isa = PBXBuildFile; fileRef = CECA899B09DB132E00A3D774 /* DetailsPanel.m */; }; CEDD92DA0FDD01640031C7B7 /* BRSingleLineFormatter.m in Sources */ = {isa = PBXBuildFile; fileRef = CEDD92D70FDD01640031C7B7 /* BRSingleLineFormatter.m */; }; - CEDD92DB0FDD01640031C7B7 /* NSCharacterSet_Extensions.m in Sources */ = {isa = PBXBuildFile; fileRef = CEDD92D90FDD01640031C7B7 /* NSCharacterSet_Extensions.m */; }; CEE7EA130FE675C80004E467 /* DetailsPanel.m in Sources */ = {isa = PBXBuildFile; fileRef = CEE7EA120FE675C80004E467 /* DetailsPanel.m */; }; CEEB135209C837A2004D2330 /* dupeguru.icns in Resources */ = {isa = PBXBuildFile; fileRef = CEEB135109C837A2004D2330 /* dupeguru.icns */; }; CEEFC0F810945D9F001F3A39 /* DirectoryPanel.xib in Resources */ = {isa = PBXBuildFile; fileRef = CEEFC0F710945D9F001F3A39 /* DirectoryPanel.xib */; }; @@ -88,8 +87,6 @@ CECA899B09DB132E00A3D774 /* DetailsPanel.m */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.objc; path = DetailsPanel.m; sourceTree = ""; }; CEDD92D60FDD01640031C7B7 /* BRSingleLineFormatter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BRSingleLineFormatter.h; path = cocoalib/brsinglelineformatter/BRSingleLineFormatter.h; sourceTree = SOURCE_ROOT; }; CEDD92D70FDD01640031C7B7 /* BRSingleLineFormatter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BRSingleLineFormatter.m; path = cocoalib/brsinglelineformatter/BRSingleLineFormatter.m; sourceTree = SOURCE_ROOT; }; - CEDD92D80FDD01640031C7B7 /* NSCharacterSet_Extensions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = NSCharacterSet_Extensions.h; path = cocoalib/brsinglelineformatter/NSCharacterSet_Extensions.h; sourceTree = SOURCE_ROOT; }; - CEDD92D90FDD01640031C7B7 /* NSCharacterSet_Extensions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = NSCharacterSet_Extensions.m; path = cocoalib/brsinglelineformatter/NSCharacterSet_Extensions.m; sourceTree = SOURCE_ROOT; }; CEE7EA110FE675C80004E467 /* DetailsPanel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = DetailsPanel.h; path = dgbase/DetailsPanel.h; sourceTree = SOURCE_ROOT; }; CEE7EA120FE675C80004E467 /* DetailsPanel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = DetailsPanel.m; path = dgbase/DetailsPanel.m; sourceTree = SOURCE_ROOT; }; CEEB135109C837A2004D2330 /* dupeguru.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; path = dupeguru.icns; sourceTree = ""; }; @@ -231,8 +228,6 @@ children = ( CEDD92D60FDD01640031C7B7 /* BRSingleLineFormatter.h */, CEDD92D70FDD01640031C7B7 /* BRSingleLineFormatter.m */, - CEDD92D80FDD01640031C7B7 /* NSCharacterSet_Extensions.h */, - CEDD92D90FDD01640031C7B7 /* NSCharacterSet_Extensions.m */, ); name = brsinglelineformatter; path = cocoalib/brsinglelineformatter; @@ -396,7 +391,6 @@ CEFC7FBA0FC951A700CD5728 /* DirectoryPanel.m in Sources */, CEFC7FBB0FC951A700CD5728 /* ResultWindow.m in Sources */, CEDD92DA0FDD01640031C7B7 /* BRSingleLineFormatter.m in Sources */, - CEDD92DB0FDD01640031C7B7 /* NSCharacterSet_Extensions.m in Sources */, CEE7EA130FE675C80004E467 /* DetailsPanel.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; From e18f8ba6d449c1ee2fd0a1dbb7d45a67d6117fcb Mon Sep 17 00:00:00 2001 From: hsoft Date: Wed, 30 Dec 2009 10:43:00 +0000 Subject: [PATCH 269/275] se help: updated FAQ. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40280 --- se/help/templates/faq.mako | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/se/help/templates/faq.mako b/se/help/templates/faq.mako index 659d8b14..02200ffa 100644 --- a/se/help/templates/faq.mako +++ b/se/help/templates/faq.mako @@ -61,4 +61,12 @@ Enable the [Power Marker](power_marker.htm) mode and click on the Directory colu * **Windows**: Click on **Actions --> Apply Filter**, then type "copy", then click OK. * **Mac OS X**: Type "copy" in the "Filter" field in the toolbar. * Click on **Mark --> Mark All**. + +### I tried to send my duplicates to Trash, but dupeGuru is telling me it can't do it. Why? What can I do? + +Most of the time, the reason why dupeGuru can't send files to Trash is because of file permissions. You need *write* permissions on files you want to send to Trash. If you're not familiar with the command line, you can use utilities such as [BatChmod](http://macchampion.com/arbysoft/BatchMod) to fix your permissions. + +If dupeGuru still gives you troubles after fixing your permissions, there have been some cases where using "Move Marked to..." as a workaround did the trick. So instead of sending your files to Trash, you send them to a temporary folder with the "Move Marked to..." action, and then you delete that temporary folder manually. + +If all of this fail, [contact HS support](http://www.hardcoded.net/support), we'll figure it out. \ No newline at end of file From 7dee2c67c6d70cf12debe3f83481bd9da534e640 Mon Sep 17 00:00:00 2001 From: hsoft Date: Wed, 30 Dec 2009 10:43:33 +0000 Subject: [PATCH 270/275] se qt: updates externals to reference to "core_se" instead of "dupeguru_se". --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40281 From a9f9534ce63f60b6209d45af431e0f16145c37ba Mon Sep 17 00:00:00 2001 From: hsoft Date: Wed, 30 Dec 2009 10:46:08 +0000 Subject: [PATCH 271/275] pe qt: Changed a reference to "dupeguru_pe" to "core_pe" in gen script. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40282 --- pe/qt/gen.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pe/qt/gen.py b/pe/qt/gen.py index 5daf4e65..83c4875a 100644 --- a/pe/qt/gen.py +++ b/pe/qt/gen.py @@ -30,7 +30,7 @@ def move(src, dst): print 'Moving %s --> %s' % (src, dst) os.rename(src, dst) -os.chdir('dupeguru_pe') +os.chdir('core_pe') print_and_do('python gen.py') os.chdir('..') From d114ffb2c4596d454ed09ea53d323549d41756fa Mon Sep 17 00:00:00 2001 From: hsoft Date: Wed, 30 Dec 2009 10:48:01 +0000 Subject: [PATCH 272/275] pe cocoa: Fixed gen script and project dead references. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40283 --- pe/cocoa/dupeguru.xcodeproj/project.pbxproj | 6 ------ pe/cocoa/py/gen.py | 2 +- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/pe/cocoa/dupeguru.xcodeproj/project.pbxproj b/pe/cocoa/dupeguru.xcodeproj/project.pbxproj index 10b5dd4c..b3f8cf4a 100644 --- a/pe/cocoa/dupeguru.xcodeproj/project.pbxproj +++ b/pe/cocoa/dupeguru.xcodeproj/project.pbxproj @@ -42,7 +42,6 @@ CE80DB8C0FC1951C0086DCA6 /* ResultWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = CE80DB890FC1951C0086DCA6 /* ResultWindow.m */; }; CE848A1909DD85810004CB44 /* Consts.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = CE848A1809DD85810004CB44 /* Consts.h */; }; CEBAE4270FDA97E000B7887D /* BRSingleLineFormatter.m in Sources */ = {isa = PBXBuildFile; fileRef = CEBAE4240FDA97E000B7887D /* BRSingleLineFormatter.m */; }; - CEBAE4280FDA97E000B7887D /* NSCharacterSet_Extensions.m in Sources */ = {isa = PBXBuildFile; fileRef = CEBAE4260FDA97E000B7887D /* NSCharacterSet_Extensions.m */; }; CECA899C09DB132E00A3D774 /* DetailsPanel.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = CECA899A09DB132E00A3D774 /* DetailsPanel.h */; }; CECA899D09DB132E00A3D774 /* DetailsPanel.m in Sources */ = {isa = PBXBuildFile; fileRef = CECA899B09DB132E00A3D774 /* DetailsPanel.m */; }; CEEB135209C837A2004D2330 /* dupeguru.icns in Resources */ = {isa = PBXBuildFile; fileRef = CEEB135109C837A2004D2330 /* dupeguru.icns */; }; @@ -130,8 +129,6 @@ CE848A1809DD85810004CB44 /* Consts.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Consts.h; sourceTree = ""; }; CEBAE4230FDA97E000B7887D /* BRSingleLineFormatter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BRSingleLineFormatter.h; path = cocoalib/brsinglelineformatter/BRSingleLineFormatter.h; sourceTree = SOURCE_ROOT; }; CEBAE4240FDA97E000B7887D /* BRSingleLineFormatter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BRSingleLineFormatter.m; path = cocoalib/brsinglelineformatter/BRSingleLineFormatter.m; sourceTree = SOURCE_ROOT; }; - CEBAE4250FDA97E000B7887D /* NSCharacterSet_Extensions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = NSCharacterSet_Extensions.h; path = cocoalib/brsinglelineformatter/NSCharacterSet_Extensions.h; sourceTree = SOURCE_ROOT; }; - CEBAE4260FDA97E000B7887D /* NSCharacterSet_Extensions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = NSCharacterSet_Extensions.m; path = cocoalib/brsinglelineformatter/NSCharacterSet_Extensions.m; sourceTree = SOURCE_ROOT; }; CECA899A09DB132E00A3D774 /* DetailsPanel.h */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.h; path = DetailsPanel.h; sourceTree = ""; }; CECA899B09DB132E00A3D774 /* DetailsPanel.m */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.objc; path = DetailsPanel.m; sourceTree = ""; }; CEEB135109C837A2004D2330 /* dupeguru.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; path = dupeguru.icns; sourceTree = ""; }; @@ -314,8 +311,6 @@ children = ( CEBAE4230FDA97E000B7887D /* BRSingleLineFormatter.h */, CEBAE4240FDA97E000B7887D /* BRSingleLineFormatter.m */, - CEBAE4250FDA97E000B7887D /* NSCharacterSet_Extensions.h */, - CEBAE4260FDA97E000B7887D /* NSCharacterSet_Extensions.m */, ); name = brsinglelineformatter; path = cocoalib/brsinglelineformatter; @@ -422,7 +417,6 @@ CE80DB8B0FC1951C0086DCA6 /* DirectoryPanel.m in Sources */, CE80DB8C0FC1951C0086DCA6 /* ResultWindow.m in Sources */, CEBAE4270FDA97E000B7887D /* BRSingleLineFormatter.m in Sources */, - CEBAE4280FDA97E000B7887D /* NSCharacterSet_Extensions.m in Sources */, CE6044EC0FE6796200B71262 /* DetailsPanel.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/pe/cocoa/py/gen.py b/pe/cocoa/py/gen.py index bd44475f..4b9f73d6 100644 --- a/pe/cocoa/py/gen.py +++ b/pe/cocoa/py/gen.py @@ -11,7 +11,7 @@ import shutil from hsutil.build import print_and_do -os.chdir('dupeguru_pe') +os.chdir('core_pe') print_and_do('python gen.py') os.chdir('..') From 5645515d90701627ea4e1ae3ef20224613f6cb5a Mon Sep 17 00:00:00 2001 From: hsoft Date: Wed, 30 Dec 2009 10:52:23 +0000 Subject: [PATCH 273/275] me cocoa: fixed dead reference in project and broken external ref. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40284 --- me/cocoa/dupeguru.xcodeproj/project.pbxproj | 6 ------ 1 file changed, 6 deletions(-) diff --git a/me/cocoa/dupeguru.xcodeproj/project.pbxproj b/me/cocoa/dupeguru.xcodeproj/project.pbxproj index 5b2d5e46..09fa0eb0 100644 --- a/me/cocoa/dupeguru.xcodeproj/project.pbxproj +++ b/me/cocoa/dupeguru.xcodeproj/project.pbxproj @@ -30,7 +30,6 @@ CE3FBDD31094637800B72D77 /* DetailsPanel.xib in Resources */ = {isa = PBXBuildFile; fileRef = CE3FBDD11094637800B72D77 /* DetailsPanel.xib */; }; CE3FBDD41094637800B72D77 /* DirectoryPanel.xib in Resources */ = {isa = PBXBuildFile; fileRef = CE3FBDD21094637800B72D77 /* DirectoryPanel.xib */; }; CE49DEF60FDFEB810098617B /* BRSingleLineFormatter.m in Sources */ = {isa = PBXBuildFile; fileRef = CE49DEF30FDFEB810098617B /* BRSingleLineFormatter.m */; }; - CE49DEF70FDFEB810098617B /* NSCharacterSet_Extensions.m in Sources */ = {isa = PBXBuildFile; fileRef = CE49DEF50FDFEB810098617B /* NSCharacterSet_Extensions.m */; }; CE515DF30FC6C12E00EC695D /* Dialogs.m in Sources */ = {isa = PBXBuildFile; fileRef = CE515DE10FC6C12E00EC695D /* Dialogs.m */; }; CE515DF40FC6C12E00EC695D /* HSErrorReportWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = CE515DE30FC6C12E00EC695D /* HSErrorReportWindow.m */; }; CE515DF50FC6C12E00EC695D /* Outline.m in Sources */ = {isa = PBXBuildFile; fileRef = CE515DE50FC6C12E00EC695D /* Outline.m */; }; @@ -94,8 +93,6 @@ CE3FBDD21094637800B72D77 /* DirectoryPanel.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = DirectoryPanel.xib; sourceTree = ""; }; CE49DEF20FDFEB810098617B /* BRSingleLineFormatter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BRSingleLineFormatter.h; path = cocoalib/brsinglelineformatter/BRSingleLineFormatter.h; sourceTree = SOURCE_ROOT; }; CE49DEF30FDFEB810098617B /* BRSingleLineFormatter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BRSingleLineFormatter.m; path = cocoalib/brsinglelineformatter/BRSingleLineFormatter.m; sourceTree = SOURCE_ROOT; }; - CE49DEF40FDFEB810098617B /* NSCharacterSet_Extensions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = NSCharacterSet_Extensions.h; path = cocoalib/brsinglelineformatter/NSCharacterSet_Extensions.h; sourceTree = SOURCE_ROOT; }; - CE49DEF50FDFEB810098617B /* NSCharacterSet_Extensions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = NSCharacterSet_Extensions.m; path = cocoalib/brsinglelineformatter/NSCharacterSet_Extensions.m; sourceTree = SOURCE_ROOT; }; CE515DE00FC6C12E00EC695D /* Dialogs.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Dialogs.h; path = cocoalib/Dialogs.h; sourceTree = SOURCE_ROOT; }; CE515DE10FC6C12E00EC695D /* Dialogs.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Dialogs.m; path = cocoalib/Dialogs.m; sourceTree = SOURCE_ROOT; }; CE515DE20FC6C12E00EC695D /* HSErrorReportWindow.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = HSErrorReportWindow.h; path = cocoalib/HSErrorReportWindow.h; sourceTree = SOURCE_ROOT; }; @@ -262,8 +259,6 @@ children = ( CE49DEF20FDFEB810098617B /* BRSingleLineFormatter.h */, CE49DEF30FDFEB810098617B /* BRSingleLineFormatter.m */, - CE49DEF40FDFEB810098617B /* NSCharacterSet_Extensions.h */, - CE49DEF50FDFEB810098617B /* NSCharacterSet_Extensions.m */, ); name = brsinglelineformatter; path = cocoalib/brsinglelineformatter; @@ -413,7 +408,6 @@ CE515E1E0FC6C19300EC695D /* DirectoryPanel.m in Sources */, CE515E1F0FC6C19300EC695D /* ResultWindow.m in Sources */, CE49DEF60FDFEB810098617B /* BRSingleLineFormatter.m in Sources */, - CE49DEF70FDFEB810098617B /* NSCharacterSet_Extensions.m in Sources */, CE6032C00FE6784C007E33FF /* DetailsPanel.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; From 838f8ae352a90e4d92809fa800418dea7595c60e Mon Sep 17 00:00:00 2001 From: hsoft Date: Wed, 30 Dec 2009 16:34:41 +0000 Subject: [PATCH 274/275] Changed the build system (that commit is *huge*) --HG-- rename : base/cocoa/AppDelegate.h => cocoa/base/AppDelegate.h rename : base/cocoa/AppDelegate.m => cocoa/base/AppDelegate.m rename : base/cocoa/Consts.h => cocoa/base/Consts.h rename : base/cocoa/DetailsPanel.h => cocoa/base/DetailsPanel.h rename : base/cocoa/DetailsPanel.m => cocoa/base/DetailsPanel.m rename : base/cocoa/DirectoryPanel.h => cocoa/base/DirectoryPanel.h rename : base/cocoa/DirectoryPanel.m => cocoa/base/DirectoryPanel.m rename : base/cocoa/PyDupeGuru.h => cocoa/base/PyDupeGuru.h rename : base/cocoa/ResultWindow.h => cocoa/base/ResultWindow.h rename : base/cocoa/ResultWindow.m => cocoa/base/ResultWindow.m rename : base/cocoa/dsa_pub.pem => cocoa/base/dsa_pub.pem rename : base/cocoa/xib/DetailsPanel.xib => cocoa/base/xib/DetailsPanel.xib rename : base/cocoa/xib/DirectoryPanel.xib => cocoa/base/xib/DirectoryPanel.xib rename : base/cocoa/xib/MainMenu.xib => cocoa/base/xib/MainMenu.xib rename : me/cocoa/AppDelegate.h => cocoa/me/AppDelegate.h rename : me/cocoa/AppDelegate.m => cocoa/me/AppDelegate.m rename : me/cocoa/Consts.h => cocoa/me/Consts.h rename : me/cocoa/DetailsPanel.h => cocoa/me/DetailsPanel.h rename : me/cocoa/DetailsPanel.m => cocoa/me/DetailsPanel.m rename : me/cocoa/DirectoryPanel.h => cocoa/me/DirectoryPanel.h rename : me/cocoa/DirectoryPanel.m => cocoa/me/DirectoryPanel.m rename : me/cocoa/Info.plist => cocoa/me/Info.plist rename : me/cocoa/PyDupeGuru.h => cocoa/me/PyDupeGuru.h rename : me/cocoa/ResultWindow.h => cocoa/me/ResultWindow.h rename : me/cocoa/ResultWindow.m => cocoa/me/ResultWindow.m rename : me/cocoa/dupeguru.icns => cocoa/me/dupeguru.icns rename : me/cocoa/dupeguru.xcodeproj/project.pbxproj => cocoa/me/dupeguru.xcodeproj/project.pbxproj rename : me/cocoa/gen.py => cocoa/me/gen.py rename : me/cocoa/main.m => cocoa/me/main.m rename : me/cocoa/py/dg_cocoa.py => cocoa/me/py/dg_cocoa.py rename : me/cocoa/py/setup.py => cocoa/me/py/setup.py rename : me/cocoa/xib/Preferences.xib => cocoa/me/xib/Preferences.xib rename : pe/cocoa/AppDelegate.h => cocoa/pe/AppDelegate.h rename : pe/cocoa/AppDelegate.m => cocoa/pe/AppDelegate.m rename : pe/cocoa/Consts.h => cocoa/pe/Consts.h rename : pe/cocoa/DetailsPanel.h => cocoa/pe/DetailsPanel.h rename : pe/cocoa/DetailsPanel.m => cocoa/pe/DetailsPanel.m rename : pe/cocoa/DirectoryPanel.h => cocoa/pe/DirectoryPanel.h rename : pe/cocoa/DirectoryPanel.m => cocoa/pe/DirectoryPanel.m rename : pe/cocoa/Info.plist => cocoa/pe/Info.plist rename : pe/cocoa/PictureBlocks.h => cocoa/pe/PictureBlocks.h rename : pe/cocoa/PictureBlocks.m => cocoa/pe/PictureBlocks.m rename : pe/cocoa/PyDupeGuru.h => cocoa/pe/PyDupeGuru.h rename : pe/cocoa/ResultWindow.h => cocoa/pe/ResultWindow.h rename : pe/cocoa/ResultWindow.m => cocoa/pe/ResultWindow.m rename : pe/cocoa/dupeguru.icns => cocoa/pe/dupeguru.icns rename : pe/cocoa/dupeguru.xcodeproj/project.pbxproj => cocoa/pe/dupeguru.xcodeproj/project.pbxproj rename : pe/cocoa/gen.py => cocoa/pe/gen.py rename : pe/cocoa/main.m => cocoa/pe/main.m rename : pe/cocoa/py/dg_cocoa.py => cocoa/pe/py/dg_cocoa.py rename : pe/cocoa/py/setup.py => cocoa/pe/py/setup.py rename : pe/cocoa/xib/DetailsPanel.xib => cocoa/pe/xib/DetailsPanel.xib rename : pe/cocoa/xib/Preferences.xib => cocoa/pe/xib/Preferences.xib rename : se/cocoa/AppDelegate.h => cocoa/se/AppDelegate.h rename : se/cocoa/AppDelegate.m => cocoa/se/AppDelegate.m rename : se/cocoa/Consts.h => cocoa/se/Consts.h rename : se/cocoa/DetailsPanel.h => cocoa/se/DetailsPanel.h rename : se/cocoa/DetailsPanel.m => cocoa/se/DetailsPanel.m rename : se/cocoa/DirectoryPanel.h => cocoa/se/DirectoryPanel.h rename : se/cocoa/DirectoryPanel.m => cocoa/se/DirectoryPanel.m rename : se/cocoa/Info.plist => cocoa/se/Info.plist rename : se/cocoa/PyDupeGuru.h => cocoa/se/PyDupeGuru.h rename : se/cocoa/ResultWindow.h => cocoa/se/ResultWindow.h rename : se/cocoa/ResultWindow.m => cocoa/se/ResultWindow.m rename : se/cocoa/dupeguru.icns => cocoa/se/dupeguru.icns rename : se/cocoa/dupeguru.xcodeproj/project.pbxproj => cocoa/se/dupeguru.xcodeproj/project.pbxproj rename : se/cocoa/gen.py => cocoa/se/gen.py rename : se/cocoa/main.m => cocoa/se/main.m rename : se/cocoa/py/dg_cocoa.py => cocoa/se/py/dg_cocoa.py rename : se/cocoa/py/setup.py => cocoa/se/py/setup.py rename : se/cocoa/xib/Preferences.xib => cocoa/se/xib/Preferences.xib rename : base/core/LICENSE => core/LICENSE rename : base/core/__init__.py => core/__init__.py rename : base/core/app.py => core/app.py rename : base/core/app_cocoa.py => core/app_cocoa.py rename : base/core/data.py => core/data.py rename : base/core/directories.py => core/directories.py rename : base/core/engine.py => core/engine.py rename : base/core/export.py => core/export.py rename : base/core/fs.py => core/fs.py rename : base/core/ignore.py => core/ignore.py rename : base/core/results.py => core/results.py rename : base/core/scanner.py => core/scanner.py rename : base/core/tests/__init__.py => core/tests/__init__.py rename : base/core/tests/app_cocoa_test.py => core/tests/app_cocoa_test.py rename : base/core/tests/app_test.py => core/tests/app_test.py rename : base/core/tests/data.py => core/tests/data.py rename : base/core/tests/directories_test.py => core/tests/directories_test.py rename : base/core/tests/engine_test.py => core/tests/engine_test.py rename : base/core/tests/ignore_test.py => core/tests/ignore_test.py rename : base/core/tests/results_test.py => core/tests/results_test.py rename : base/core/tests/scanner_test.py => core/tests/scanner_test.py rename : me/core/__init__.py => core_me/__init__.py rename : me/core/app_cocoa.py => core_me/app_cocoa.py rename : me/core/data.py => core_me/data.py rename : me/core/fs.py => core_me/fs.py rename : me/core/scanner.py => core_me/scanner.py rename : me/core/tests/__init__.py => core_me/tests/__init__.py rename : me/core/tests/scanner_test.py => core_me/tests/scanner_test.py rename : pe/core/LICENSE => core_pe/LICENSE rename : pe/core/__init__.py => core_pe/__init__.py rename : pe/core/app_cocoa.py => core_pe/app_cocoa.py rename : pe/core/block.py => core_pe/block.py rename : pe/core/cache.py => core_pe/cache.py rename : pe/core/data.py => core_pe/data.py rename : pe/core/gen.py => core_pe/gen.py rename : pe/core/matchbase.py => core_pe/matchbase.py rename : pe/core/modules/block/block.pyx => core_pe/modules/block/block.pyx rename : pe/core/modules/block/setup.py => core_pe/modules/block/setup.py rename : pe/core/modules/cache/cache.pyx => core_pe/modules/cache/cache.pyx rename : pe/core/modules/cache/setup.py => core_pe/modules/cache/setup.py rename : pe/core/scanner.py => core_pe/scanner.py rename : pe/core/tests/__init__.py => core_pe/tests/__init__.py rename : pe/core/tests/block_test.py => core_pe/tests/block_test.py rename : pe/core/tests/cache_test.py => core_pe/tests/cache_test.py rename : se/core/LICENSE => core_se/LICENSE rename : se/core/__init__.py => core_se/__init__.py rename : se/core/app_cocoa.py => core_se/app_cocoa.py rename : se/core/data.py => core_se/data.py rename : se/core/fs.py => core_se/fs.py rename : se/core/tests/__init__.py => core_se/tests/__init__.py rename : se/core/tests/fs_test.py => core_se/tests/fs_test.py rename : me/help/LICENSE => help_me/LICENSE rename : me/help/__init__.py => help_me/__init__.py rename : me/help/changelog.yaml => help_me/changelog.yaml rename : me/help/gen.py => help_me/gen.py rename : me/help/skeleton/hardcoded.css => help_me/skeleton/hardcoded.css rename : me/help/skeleton/images/hs_title.png => help_me/skeleton/images/hs_title.png rename : me/help/templates/base_dg.mako => help_me/templates/base_dg.mako rename : me/help/templates/credits.mako => help_me/templates/credits.mako rename : me/help/templates/directories.mako => help_me/templates/directories.mako rename : me/help/templates/faq.mako => help_me/templates/faq.mako rename : me/help/templates/intro.mako => help_me/templates/intro.mako rename : me/help/templates/power_marker.mako => help_me/templates/power_marker.mako rename : me/help/templates/preferences.mako => help_me/templates/preferences.mako rename : me/help/templates/quick_start.mako => help_me/templates/quick_start.mako rename : me/help/templates/results.mako => help_me/templates/results.mako rename : me/help/templates/versions.mako => help_me/templates/versions.mako rename : pe/help/LICENSE => help_pe/LICENSE rename : pe/help/__init__.py => help_pe/__init__.py rename : pe/help/changelog.yaml => help_pe/changelog.yaml rename : pe/help/gen.py => help_pe/gen.py rename : pe/help/skeleton/hardcoded.css => help_pe/skeleton/hardcoded.css rename : pe/help/skeleton/images/hs_title.png => help_pe/skeleton/images/hs_title.png rename : pe/help/templates/base_dg.mako => help_pe/templates/base_dg.mako rename : pe/help/templates/credits.mako => help_pe/templates/credits.mako rename : pe/help/templates/directories.mako => help_pe/templates/directories.mako rename : pe/help/templates/faq.mako => help_pe/templates/faq.mako rename : pe/help/templates/intro.mako => help_pe/templates/intro.mako rename : pe/help/templates/power_marker.mako => help_pe/templates/power_marker.mako rename : pe/help/templates/preferences.mako => help_pe/templates/preferences.mako rename : pe/help/templates/quick_start.mako => help_pe/templates/quick_start.mako rename : pe/help/templates/results.mako => help_pe/templates/results.mako rename : pe/help/templates/versions.mako => help_pe/templates/versions.mako rename : se/help/LICENSE => help_se/LICENSE rename : se/help/changelog.yaml => help_se/changelog.yaml rename : se/help/gen.py => help_se/gen.py rename : se/help/skeleton/hardcoded.css => help_se/skeleton/hardcoded.css rename : se/help/skeleton/images/hs_title.png => help_se/skeleton/images/hs_title.png rename : se/help/templates/base_dg.mako => help_se/templates/base_dg.mako rename : se/help/templates/credits.mako => help_se/templates/credits.mako rename : se/help/templates/directories.mako => help_se/templates/directories.mako rename : se/help/templates/faq.mako => help_se/templates/faq.mako rename : se/help/templates/intro.mako => help_se/templates/intro.mako rename : se/help/templates/power_marker.mako => help_se/templates/power_marker.mako rename : se/help/templates/preferences.mako => help_se/templates/preferences.mako rename : se/help/templates/quick_start.mako => help_se/templates/quick_start.mako rename : se/help/templates/results.mako => help_se/templates/results.mako rename : se/help/templates/versions.mako => help_se/templates/versions.mako rename : base/qt/WARNING => qt/WARNING rename : base/qt/__init__.py => qt/base/__init__.py rename : base/qt/app.py => qt/base/app.py rename : base/qt/details_table.py => qt/base/details_table.py rename : base/qt/dg.qrc => qt/base/dg.qrc rename : base/qt/directories_dialog.py => qt/base/directories_dialog.py rename : base/qt/directories_dialog.ui => qt/base/directories_dialog.ui rename : base/qt/directories_model.py => qt/base/directories_model.py rename : base/qt/main_window.py => qt/base/main_window.py rename : base/qt/main_window.ui => qt/base/main_window.ui rename : base/qt/platform.py => qt/base/platform.py rename : base/qt/platform_osx.py => qt/base/platform_osx.py rename : base/qt/platform_win.py => qt/base/platform_win.py rename : base/qt/preferences.py => qt/base/preferences.py rename : base/qt/results_model.py => qt/base/results_model.py rename : me/qt/app.py => qt/me/app.py rename : me/qt/build.py => qt/me/build.py rename : me/qt/details_dialog.py => qt/me/details_dialog.py rename : me/qt/details_dialog.ui => qt/me/details_dialog.ui rename : me/qt/dgme.spec => qt/me/dgme.spec rename : me/qt/gen.py => qt/me/gen.py rename : me/qt/installer.aip => qt/me/installer.aip rename : me/qt/preferences.py => qt/me/preferences.py rename : me/qt/preferences_dialog.py => qt/me/preferences_dialog.py rename : me/qt/preferences_dialog.ui => qt/me/preferences_dialog.ui rename : me/qt/profile.py => qt/me/profile.py rename : me/qt/start.py => qt/me/start.py rename : me/qt/verinfo => qt/me/verinfo rename : pe/qt/app.py => qt/pe/app.py rename : pe/qt/block.py => qt/pe/block.py rename : pe/qt/build.py => qt/pe/build.py rename : pe/qt/details_dialog.py => qt/pe/details_dialog.py rename : pe/qt/details_dialog.ui => qt/pe/details_dialog.ui rename : pe/qt/dgpe.spec => qt/pe/dgpe.spec rename : pe/qt/gen.py => qt/pe/gen.py rename : pe/qt/installer.aip => qt/pe/installer.aip rename : pe/qt/main_window.py => qt/pe/main_window.py rename : pe/qt/modules/block/block.pyx => qt/pe/modules/block/block.pyx rename : pe/qt/modules/block/setup.py => qt/pe/modules/block/setup.py rename : pe/qt/preferences.py => qt/pe/preferences.py rename : pe/qt/preferences_dialog.py => qt/pe/preferences_dialog.py rename : pe/qt/preferences_dialog.ui => qt/pe/preferences_dialog.ui rename : pe/qt/profile.py => qt/pe/profile.py rename : pe/qt/start.py => qt/pe/start.py rename : pe/qt/verinfo => qt/pe/verinfo rename : se/qt/app.py => qt/se/app.py rename : se/qt/build.py => qt/se/build.py rename : se/qt/details_dialog.py => qt/se/details_dialog.py rename : se/qt/details_dialog.ui => qt/se/details_dialog.ui rename : se/qt/dgse.spec => qt/se/dgse.spec rename : se/qt/gen.py => qt/se/gen.py rename : se/qt/installer.aip => qt/se/installer.aip rename : se/qt/preferences.py => qt/se/preferences.py rename : se/qt/preferences_dialog.py => qt/se/preferences_dialog.py rename : se/qt/preferences_dialog.ui => qt/se/preferences_dialog.ui rename : se/qt/profile.py => qt/se/profile.py rename : se/qt/start.py => qt/se/start.py rename : se/qt/verinfo => qt/se/verinfo extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40285 --- base/cocoa/LICENSE => LICENSE | 0 README | 68 ++++++++++++ base/qt/dg.qrc | 16 --- build.py | 62 +++++++++++ {base/cocoa => cocoa/base}/AppDelegate.h | 0 {base/cocoa => cocoa/base}/AppDelegate.m | 0 {base/cocoa => cocoa/base}/Consts.h | 0 {base/cocoa => cocoa/base}/DetailsPanel.h | 0 {base/cocoa => cocoa/base}/DetailsPanel.m | 0 {base/cocoa => cocoa/base}/DirectoryPanel.h | 0 {base/cocoa => cocoa/base}/DirectoryPanel.m | 0 {base/cocoa => cocoa/base}/PyDupeGuru.h | 0 {base/cocoa => cocoa/base}/ResultWindow.h | 0 {base/cocoa => cocoa/base}/ResultWindow.m | 0 {base/cocoa => cocoa/base}/dsa_pub.pem | 0 .../cocoa => cocoa/base}/xib/DetailsPanel.xib | 0 .../base}/xib/DirectoryPanel.xib | 0 {base/cocoa => cocoa/base}/xib/MainMenu.xib | 0 {me/cocoa => cocoa/me}/AppDelegate.h | 2 +- {me/cocoa => cocoa/me}/AppDelegate.m | 10 +- {me/cocoa => cocoa/me}/Consts.h | 2 +- {me/cocoa => cocoa/me}/DetailsPanel.h | 2 +- {me/cocoa => cocoa/me}/DetailsPanel.m | 0 {me/cocoa => cocoa/me}/DirectoryPanel.h | 2 +- {me/cocoa => cocoa/me}/DirectoryPanel.m | 0 {me/cocoa => cocoa/me}/Info.plist | 0 {me/cocoa => cocoa/me}/PyDupeGuru.h | 2 +- {me/cocoa => cocoa/me}/ResultWindow.h | 4 +- {me/cocoa => cocoa/me}/ResultWindow.m | 8 +- {me/cocoa => cocoa/me}/dupeguru.icns | Bin .../me}/dupeguru.xcodeproj/project.pbxproj | 96 ++++++++--------- cocoa/me/gen.py | 41 +++++++ {me/cocoa => cocoa/me}/main.m | 0 {me/cocoa => cocoa/me}/py/dg_cocoa.py | 0 {me/cocoa => cocoa/me}/py/setup.py | 16 ++- {me/cocoa => cocoa/me}/xib/Preferences.xib | 0 {se/cocoa => cocoa/pe}/AppDelegate.h | 2 +- {pe/cocoa => cocoa/pe}/AppDelegate.m | 0 {pe/cocoa => cocoa/pe}/Consts.h | 2 +- {pe/cocoa => cocoa/pe}/DetailsPanel.h | 2 +- {pe/cocoa => cocoa/pe}/DetailsPanel.m | 0 {pe/cocoa => cocoa/pe}/DirectoryPanel.h | 2 +- {pe/cocoa => cocoa/pe}/DirectoryPanel.m | 0 {pe/cocoa => cocoa/pe}/Info.plist | 0 {pe/cocoa => cocoa/pe}/PictureBlocks.h | 0 {pe/cocoa => cocoa/pe}/PictureBlocks.m | 0 {pe/cocoa => cocoa/pe}/PyDupeGuru.h | 2 +- {pe/cocoa => cocoa/pe}/ResultWindow.h | 2 +- {pe/cocoa => cocoa/pe}/ResultWindow.m | 0 {pe/cocoa => cocoa/pe}/dupeguru.icns | Bin .../pe}/dupeguru.xcodeproj/project.pbxproj | 101 +++++++++--------- cocoa/pe/gen.py | 41 +++++++ {pe/cocoa => cocoa/pe}/main.m | 0 {pe/cocoa => cocoa/pe}/py/dg_cocoa.py | 0 {pe/cocoa => cocoa/pe}/py/setup.py | 12 +-- {pe/cocoa => cocoa/pe}/xib/DetailsPanel.xib | 0 {pe/cocoa => cocoa/pe}/xib/Preferences.xib | 0 {pe/cocoa => cocoa/se}/AppDelegate.h | 2 +- {se/cocoa => cocoa/se}/AppDelegate.m | 8 +- {se/cocoa => cocoa/se}/Consts.h | 2 +- {se/cocoa => cocoa/se}/DetailsPanel.h | 2 +- {se/cocoa => cocoa/se}/DetailsPanel.m | 0 {se/cocoa => cocoa/se}/DirectoryPanel.h | 2 +- {se/cocoa => cocoa/se}/DirectoryPanel.m | 0 {se/cocoa => cocoa/se}/Info.plist | 0 {se/cocoa => cocoa/se}/PyDupeGuru.h | 2 +- {se/cocoa => cocoa/se}/ResultWindow.h | 4 +- {se/cocoa => cocoa/se}/ResultWindow.m | 6 +- {se/cocoa => cocoa/se}/dupeguru.icns | Bin .../se}/dupeguru.xcodeproj/project.pbxproj | 93 ++++++++-------- cocoa/se/gen.py | 41 +++++++ {se/cocoa => cocoa/se}/main.m | 0 {se/cocoa => cocoa/se}/py/dg_cocoa.py | 0 {se/cocoa => cocoa/se}/py/setup.py | 0 {se/cocoa => cocoa/se}/xib/Preferences.xib | 0 configure.py | 40 +++++++ {base/core => core}/LICENSE | 0 {base/core => core}/__init__.py | 0 {base/core => core}/app.py | 0 {base/core => core}/app_cocoa.py | 0 {base/core => core}/data.py | 0 {base/core => core}/directories.py | 0 {base/core => core}/engine.py | 0 {base/core => core}/export.py | 0 {base/core => core}/fs.py | 0 {base/core => core}/ignore.py | 0 {base/core => core}/results.py | 0 {base/core => core}/scanner.py | 0 {base/core => core}/tests/__init__.py | 0 {base/core => core}/tests/app_cocoa_test.py | 0 {base/core => core}/tests/app_test.py | 0 {base/core => core}/tests/data.py | 0 {base/core => core}/tests/directories_test.py | 0 {base/core => core}/tests/engine_test.py | 0 {base/core => core}/tests/ignore_test.py | 0 {base/core => core}/tests/results_test.py | 0 {base/core => core}/tests/scanner_test.py | 0 {base/qt => core_me}/__init__.py | 0 {me/core => core_me}/app_cocoa.py | 0 {me/core => core_me}/data.py | 0 {me/core => core_me}/fs.py | 0 {me/core => core_me}/scanner.py | 0 {me/core => core_me/tests}/__init__.py | 0 {me/core => core_me}/tests/scanner_test.py | 0 {base/qt => core_pe}/LICENSE | 0 {me/core/tests => core_pe}/__init__.py | 0 {pe/core => core_pe}/app_cocoa.py | 0 {pe/core => core_pe}/block.py | 0 {pe/core => core_pe}/cache.py | 0 {pe/core => core_pe}/data.py | 0 {pe/core => core_pe}/gen.py | 0 {pe/core => core_pe}/matchbase.py | 0 {pe/core => core_pe}/modules/block/block.pyx | 0 {pe/core => core_pe}/modules/block/setup.py | 0 {pe/core => core_pe}/modules/cache/cache.pyx | 0 {pe/core => core_pe}/modules/cache/setup.py | 0 {pe/core => core_pe}/scanner.py | 0 {me/help => core_pe/tests}/__init__.py | 0 {pe/core => core_pe}/tests/block_test.py | 0 {pe/core => core_pe}/tests/cache_test.py | 0 {me/cocoa => core_se}/LICENSE | 0 {se/core => core_se}/__init__.py | 0 {se/core => core_se}/app_cocoa.py | 0 {se/core => core_se}/data.py | 0 {se/core => core_se}/fs.py | 0 {pe/core => core_se/tests}/__init__.py | 0 {se/core => core_se}/tests/fs_test.py | 0 {me/help => help_me}/LICENSE | 0 {pe/core/tests => help_me}/__init__.py | 0 {me/help => help_me}/changelog.yaml | 0 {me/help => help_me}/gen.py | 4 +- {me/help => help_me}/skeleton/hardcoded.css | 0 .../skeleton/images/hs_title.png | Bin {me/help => help_me}/templates/base_dg.mako | 0 {me/help => help_me}/templates/credits.mako | 0 .../templates/directories.mako | 0 {me/help => help_me}/templates/faq.mako | 0 {me/help => help_me}/templates/intro.mako | 0 .../templates/power_marker.mako | 0 .../templates/preferences.mako | 0 .../templates/quick_start.mako | 0 {me/help => help_me}/templates/results.mako | 0 {me/help => help_me}/templates/versions.mako | 0 {me/qt => help_pe}/LICENSE | 0 {pe/help => help_pe}/__init__.py | 0 {pe/help => help_pe}/changelog.yaml | 0 {pe/help => help_pe}/gen.py | 4 +- {pe/help => help_pe}/skeleton/hardcoded.css | 0 .../skeleton/images/hs_title.png | Bin {pe/help => help_pe}/templates/base_dg.mako | 0 {pe/help => help_pe}/templates/credits.mako | 0 .../templates/directories.mako | 0 {pe/help => help_pe}/templates/faq.mako | 0 {pe/help => help_pe}/templates/intro.mako | 0 .../templates/power_marker.mako | 0 .../templates/preferences.mako | 0 .../templates/quick_start.mako | 0 {pe/help => help_pe}/templates/results.mako | 0 {pe/help => help_pe}/templates/versions.mako | 0 {pe/cocoa => help_se}/LICENSE | 0 {se/core/tests => help_se}/__init__.py | 0 {se/help => help_se}/changelog.yaml | 0 {se/help => help_se}/gen.py | 9 +- {se/help => help_se}/skeleton/hardcoded.css | 0 .../skeleton/images/hs_title.png | Bin {se/help => help_se}/templates/base_dg.mako | 0 {se/help => help_se}/templates/credits.mako | 0 .../templates/directories.mako | 0 {se/help => help_se}/templates/faq.mako | 0 {se/help => help_se}/templates/intro.mako | 0 .../templates/power_marker.mako | 0 .../templates/preferences.mako | 0 .../templates/quick_start.mako | 0 {se/help => help_se}/templates/results.mako | 0 {se/help => help_se}/templates/versions.mako | 0 me/cocoa/gen.py | 21 ---- me/cocoa/py/gen.py | 20 ---- me/qt/WARNING | 11 -- package.py | 45 ++++++++ pe/cocoa/gen.py | 21 ---- pe/cocoa/py/gen.py | 23 ---- pe/core/LICENSE | 11 -- pe/help/LICENSE | 11 -- pe/qt/LICENSE | 11 -- pe/qt/WARNING | 11 -- {base/qt => qt}/WARNING | 0 qt/base/__init__.py | 0 {base/qt => qt/base}/app.py | 0 {base/qt => qt/base}/details_table.py | 0 qt/base/dg.qrc | 16 +++ {base/qt => qt/base}/directories_dialog.py | 0 {base/qt => qt/base}/directories_dialog.ui | 0 {base/qt => qt/base}/directories_model.py | 0 {base/qt => qt/base}/main_window.py | 0 {base/qt => qt/base}/main_window.ui | 0 {base/qt => qt/base}/platform.py | 0 {base/qt => qt/base}/platform_osx.py | 0 {base/qt => qt/base}/platform_win.py | 0 {base/qt => qt/base}/preferences.py | 0 {base/qt => qt/base}/results_model.py | 0 {me/qt => qt/me}/app.py | 0 {me/qt => qt/me}/build.py | 0 {me/qt => qt/me}/details_dialog.py | 0 {me/qt => qt/me}/details_dialog.ui | 0 {me/qt => qt/me}/dgme.spec | 4 +- {me/qt => qt/me}/gen.py | 12 +-- {me/qt => qt/me}/installer.aip | 0 {me/qt => qt/me}/preferences.py | 0 {me/qt => qt/me}/preferences_dialog.py | 0 {me/qt => qt/me}/preferences_dialog.ui | 0 {me/qt => qt/me}/profile.py | 0 {me/qt => qt/me}/start.py | 0 {me/qt => qt/me}/verinfo | 0 {pe/qt => qt/pe}/app.py | 0 {pe/qt => qt/pe}/block.py | 0 {pe/qt => qt/pe}/build.py | 0 {pe/qt => qt/pe}/details_dialog.py | 0 {pe/qt => qt/pe}/details_dialog.ui | 0 {pe/qt => qt/pe}/dgpe.spec | 2 +- {pe/qt => qt/pe}/gen.py | 20 +--- {pe/qt => qt/pe}/installer.aip | 0 {pe/qt => qt/pe}/main_window.py | 0 {pe/qt => qt/pe}/modules/block/block.pyx | 0 {pe/qt => qt/pe}/modules/block/setup.py | 0 {pe/qt => qt/pe}/preferences.py | 0 {pe/qt => qt/pe}/preferences_dialog.py | 0 {pe/qt => qt/pe}/preferences_dialog.ui | 0 {pe/qt => qt/pe}/profile.py | 0 {pe/qt => qt/pe}/start.py | 0 {pe/qt => qt/pe}/verinfo | 0 {se/qt => qt/se}/app.py | 0 {se/qt => qt/se}/build.py | 0 {se/qt => qt/se}/details_dialog.py | 0 {se/qt => qt/se}/details_dialog.ui | 0 {se/qt => qt/se}/dgse.spec | 4 +- {se/qt => qt/se}/gen.py | 10 +- {se/qt => qt/se}/installer.aip | 0 {se/qt => qt/se}/preferences.py | 0 {se/qt => qt/se}/preferences_dialog.py | 0 {se/qt => qt/se}/preferences_dialog.ui | 0 {se/qt => qt/se}/profile.py | 0 {se/qt => qt/se}/start.py | 0 {se/qt => qt/se}/verinfo | 0 run.py | 39 +++++++ se/cocoa/LICENSE | 11 -- se/cocoa/gen.py | 19 ---- se/cocoa/py/gen.py | 20 ---- se/core/LICENSE | 11 -- se/help/LICENSE | 11 -- se/qt/LICENSE | 11 -- se/qt/WARNING | 11 -- 251 files changed, 602 insertions(+), 500 deletions(-) rename base/cocoa/LICENSE => LICENSE (100%) create mode 100644 README delete mode 100644 base/qt/dg.qrc create mode 100644 build.py rename {base/cocoa => cocoa/base}/AppDelegate.h (100%) rename {base/cocoa => cocoa/base}/AppDelegate.m (100%) rename {base/cocoa => cocoa/base}/Consts.h (100%) rename {base/cocoa => cocoa/base}/DetailsPanel.h (100%) rename {base/cocoa => cocoa/base}/DetailsPanel.m (100%) rename {base/cocoa => cocoa/base}/DirectoryPanel.h (100%) rename {base/cocoa => cocoa/base}/DirectoryPanel.m (100%) rename {base/cocoa => cocoa/base}/PyDupeGuru.h (100%) rename {base/cocoa => cocoa/base}/ResultWindow.h (100%) rename {base/cocoa => cocoa/base}/ResultWindow.m (100%) rename {base/cocoa => cocoa/base}/dsa_pub.pem (100%) rename {base/cocoa => cocoa/base}/xib/DetailsPanel.xib (100%) rename {base/cocoa => cocoa/base}/xib/DirectoryPanel.xib (100%) rename {base/cocoa => cocoa/base}/xib/MainMenu.xib (100%) rename {me/cocoa => cocoa/me}/AppDelegate.h (94%) rename {me/cocoa => cocoa/me}/AppDelegate.m (94%) rename {me/cocoa => cocoa/me}/Consts.h (92%) rename {me/cocoa => cocoa/me}/DetailsPanel.h (91%) rename {me/cocoa => cocoa/me}/DetailsPanel.m (100%) rename {me/cocoa => cocoa/me}/DirectoryPanel.h (91%) rename {me/cocoa => cocoa/me}/DirectoryPanel.m (100%) rename {me/cocoa => cocoa/me}/Info.plist (100%) rename {me/cocoa => cocoa/me}/PyDupeGuru.h (96%) rename {me/cocoa => cocoa/me}/ResultWindow.h (94%) rename {me/cocoa => cocoa/me}/ResultWindow.m (98%) rename {me/cocoa => cocoa/me}/dupeguru.icns (100%) rename {me/cocoa => cocoa/me}/dupeguru.xcodeproj/project.pbxproj (86%) create mode 100644 cocoa/me/gen.py rename {me/cocoa => cocoa/me}/main.m (100%) rename {me/cocoa => cocoa/me}/py/dg_cocoa.py (100%) rename {me/cocoa => cocoa/me}/py/setup.py (63%) rename {me/cocoa => cocoa/me}/xib/Preferences.xib (100%) rename {se/cocoa => cocoa/pe}/AppDelegate.h (94%) rename {pe/cocoa => cocoa/pe}/AppDelegate.m (100%) rename {pe/cocoa => cocoa/pe}/Consts.h (93%) rename {pe/cocoa => cocoa/pe}/DetailsPanel.h (95%) rename {pe/cocoa => cocoa/pe}/DetailsPanel.m (100%) rename {pe/cocoa => cocoa/pe}/DirectoryPanel.h (91%) rename {pe/cocoa => cocoa/pe}/DirectoryPanel.m (100%) rename {pe/cocoa => cocoa/pe}/Info.plist (100%) rename {pe/cocoa => cocoa/pe}/PictureBlocks.h (100%) rename {pe/cocoa => cocoa/pe}/PictureBlocks.m (100%) rename {pe/cocoa => cocoa/pe}/PyDupeGuru.h (94%) rename {pe/cocoa => cocoa/pe}/ResultWindow.h (97%) rename {pe/cocoa => cocoa/pe}/ResultWindow.m (100%) rename {pe/cocoa => cocoa/pe}/dupeguru.icns (100%) rename {pe/cocoa => cocoa/pe}/dupeguru.xcodeproj/project.pbxproj (86%) create mode 100644 cocoa/pe/gen.py rename {pe/cocoa => cocoa/pe}/main.m (100%) rename {pe/cocoa => cocoa/pe}/py/dg_cocoa.py (100%) rename {pe/cocoa => cocoa/pe}/py/setup.py (63%) rename {pe/cocoa => cocoa/pe}/xib/DetailsPanel.xib (100%) rename {pe/cocoa => cocoa/pe}/xib/Preferences.xib (100%) rename {pe/cocoa => cocoa/se}/AppDelegate.h (94%) rename {se/cocoa => cocoa/se}/AppDelegate.m (95%) rename {se/cocoa => cocoa/se}/Consts.h (91%) rename {se/cocoa => cocoa/se}/DetailsPanel.h (91%) rename {se/cocoa => cocoa/se}/DetailsPanel.m (100%) rename {se/cocoa => cocoa/se}/DirectoryPanel.h (91%) rename {se/cocoa => cocoa/se}/DirectoryPanel.m (100%) rename {se/cocoa => cocoa/se}/Info.plist (100%) rename {se/cocoa => cocoa/se}/PyDupeGuru.h (94%) rename {se/cocoa => cocoa/se}/ResultWindow.h (94%) rename {se/cocoa => cocoa/se}/ResultWindow.m (98%) rename {se/cocoa => cocoa/se}/dupeguru.icns (100%) rename {se/cocoa => cocoa/se}/dupeguru.xcodeproj/project.pbxproj (87%) create mode 100644 cocoa/se/gen.py rename {se/cocoa => cocoa/se}/main.m (100%) rename {se/cocoa => cocoa/se}/py/dg_cocoa.py (100%) rename {se/cocoa => cocoa/se}/py/setup.py (100%) rename {se/cocoa => cocoa/se}/xib/Preferences.xib (100%) create mode 100644 configure.py rename {base/core => core}/LICENSE (100%) rename {base/core => core}/__init__.py (100%) rename {base/core => core}/app.py (100%) rename {base/core => core}/app_cocoa.py (100%) rename {base/core => core}/data.py (100%) rename {base/core => core}/directories.py (100%) rename {base/core => core}/engine.py (100%) rename {base/core => core}/export.py (100%) rename {base/core => core}/fs.py (100%) rename {base/core => core}/ignore.py (100%) rename {base/core => core}/results.py (100%) rename {base/core => core}/scanner.py (100%) rename {base/core => core}/tests/__init__.py (100%) rename {base/core => core}/tests/app_cocoa_test.py (100%) rename {base/core => core}/tests/app_test.py (100%) rename {base/core => core}/tests/data.py (100%) rename {base/core => core}/tests/directories_test.py (100%) rename {base/core => core}/tests/engine_test.py (100%) rename {base/core => core}/tests/ignore_test.py (100%) rename {base/core => core}/tests/results_test.py (100%) rename {base/core => core}/tests/scanner_test.py (100%) rename {base/qt => core_me}/__init__.py (100%) rename {me/core => core_me}/app_cocoa.py (100%) rename {me/core => core_me}/data.py (100%) rename {me/core => core_me}/fs.py (100%) rename {me/core => core_me}/scanner.py (100%) rename {me/core => core_me/tests}/__init__.py (100%) rename {me/core => core_me}/tests/scanner_test.py (100%) rename {base/qt => core_pe}/LICENSE (100%) rename {me/core/tests => core_pe}/__init__.py (100%) rename {pe/core => core_pe}/app_cocoa.py (100%) rename {pe/core => core_pe}/block.py (100%) rename {pe/core => core_pe}/cache.py (100%) rename {pe/core => core_pe}/data.py (100%) rename {pe/core => core_pe}/gen.py (100%) rename {pe/core => core_pe}/matchbase.py (100%) rename {pe/core => core_pe}/modules/block/block.pyx (100%) rename {pe/core => core_pe}/modules/block/setup.py (100%) rename {pe/core => core_pe}/modules/cache/cache.pyx (100%) rename {pe/core => core_pe}/modules/cache/setup.py (100%) rename {pe/core => core_pe}/scanner.py (100%) rename {me/help => core_pe/tests}/__init__.py (100%) rename {pe/core => core_pe}/tests/block_test.py (100%) rename {pe/core => core_pe}/tests/cache_test.py (100%) rename {me/cocoa => core_se}/LICENSE (100%) rename {se/core => core_se}/__init__.py (100%) rename {se/core => core_se}/app_cocoa.py (100%) rename {se/core => core_se}/data.py (100%) rename {se/core => core_se}/fs.py (100%) rename {pe/core => core_se/tests}/__init__.py (100%) rename {se/core => core_se}/tests/fs_test.py (100%) rename {me/help => help_me}/LICENSE (100%) rename {pe/core/tests => help_me}/__init__.py (100%) rename {me/help => help_me}/changelog.yaml (100%) rename {me/help => help_me}/gen.py (83%) rename {me/help => help_me}/skeleton/hardcoded.css (100%) rename {me/help => help_me}/skeleton/images/hs_title.png (100%) rename {me/help => help_me}/templates/base_dg.mako (100%) rename {me/help => help_me}/templates/credits.mako (100%) rename {me/help => help_me}/templates/directories.mako (100%) rename {me/help => help_me}/templates/faq.mako (100%) rename {me/help => help_me}/templates/intro.mako (100%) rename {me/help => help_me}/templates/power_marker.mako (100%) rename {me/help => help_me}/templates/preferences.mako (100%) rename {me/help => help_me}/templates/quick_start.mako (100%) rename {me/help => help_me}/templates/results.mako (100%) rename {me/help => help_me}/templates/versions.mako (100%) rename {me/qt => help_pe}/LICENSE (100%) rename {pe/help => help_pe}/__init__.py (100%) rename {pe/help => help_pe}/changelog.yaml (100%) rename {pe/help => help_pe}/gen.py (83%) rename {pe/help => help_pe}/skeleton/hardcoded.css (100%) rename {pe/help => help_pe}/skeleton/images/hs_title.png (100%) rename {pe/help => help_pe}/templates/base_dg.mako (100%) rename {pe/help => help_pe}/templates/credits.mako (100%) rename {pe/help => help_pe}/templates/directories.mako (100%) rename {pe/help => help_pe}/templates/faq.mako (100%) rename {pe/help => help_pe}/templates/intro.mako (100%) rename {pe/help => help_pe}/templates/power_marker.mako (100%) rename {pe/help => help_pe}/templates/preferences.mako (100%) rename {pe/help => help_pe}/templates/quick_start.mako (100%) rename {pe/help => help_pe}/templates/results.mako (100%) rename {pe/help => help_pe}/templates/versions.mako (100%) rename {pe/cocoa => help_se}/LICENSE (100%) rename {se/core/tests => help_se}/__init__.py (100%) rename {se/help => help_se}/changelog.yaml (100%) rename {se/help => help_se}/gen.py (51%) rename {se/help => help_se}/skeleton/hardcoded.css (100%) rename {se/help => help_se}/skeleton/images/hs_title.png (100%) rename {se/help => help_se}/templates/base_dg.mako (100%) rename {se/help => help_se}/templates/credits.mako (100%) rename {se/help => help_se}/templates/directories.mako (100%) rename {se/help => help_se}/templates/faq.mako (100%) rename {se/help => help_se}/templates/intro.mako (100%) rename {se/help => help_se}/templates/power_marker.mako (100%) rename {se/help => help_se}/templates/preferences.mako (100%) rename {se/help => help_se}/templates/quick_start.mako (100%) rename {se/help => help_se}/templates/results.mako (100%) rename {se/help => help_se}/templates/versions.mako (100%) delete mode 100644 me/cocoa/gen.py delete mode 100644 me/cocoa/py/gen.py delete mode 100644 me/qt/WARNING create mode 100644 package.py delete mode 100644 pe/cocoa/gen.py delete mode 100644 pe/cocoa/py/gen.py delete mode 100644 pe/core/LICENSE delete mode 100644 pe/help/LICENSE delete mode 100644 pe/qt/LICENSE delete mode 100644 pe/qt/WARNING rename {base/qt => qt}/WARNING (100%) create mode 100644 qt/base/__init__.py rename {base/qt => qt/base}/app.py (100%) rename {base/qt => qt/base}/details_table.py (100%) create mode 100644 qt/base/dg.qrc rename {base/qt => qt/base}/directories_dialog.py (100%) rename {base/qt => qt/base}/directories_dialog.ui (100%) rename {base/qt => qt/base}/directories_model.py (100%) rename {base/qt => qt/base}/main_window.py (100%) rename {base/qt => qt/base}/main_window.ui (100%) rename {base/qt => qt/base}/platform.py (100%) rename {base/qt => qt/base}/platform_osx.py (100%) rename {base/qt => qt/base}/platform_win.py (100%) rename {base/qt => qt/base}/preferences.py (100%) rename {base/qt => qt/base}/results_model.py (100%) rename {me/qt => qt/me}/app.py (100%) rename {me/qt => qt/me}/build.py (100%) rename {me/qt => qt/me}/details_dialog.py (100%) rename {me/qt => qt/me}/details_dialog.ui (100%) rename {me/qt => qt/me}/dgme.spec (78%) rename {me/qt => qt/me}/gen.py (70%) rename {me/qt => qt/me}/installer.aip (100%) rename {me/qt => qt/me}/preferences.py (100%) rename {me/qt => qt/me}/preferences_dialog.py (100%) rename {me/qt => qt/me}/preferences_dialog.ui (100%) rename {me/qt => qt/me}/profile.py (100%) rename {me/qt => qt/me}/start.py (100%) rename {me/qt => qt/me}/verinfo (100%) rename {pe/qt => qt/pe}/app.py (100%) rename {pe/qt => qt/pe}/block.py (100%) rename {pe/qt => qt/pe}/build.py (100%) rename {pe/qt => qt/pe}/details_dialog.py (100%) rename {pe/qt => qt/pe}/details_dialog.ui (100%) rename {pe/qt => qt/pe}/dgpe.spec (84%) rename {pe/qt => qt/pe}/gen.py (77%) rename {pe/qt => qt/pe}/installer.aip (100%) rename {pe/qt => qt/pe}/main_window.py (100%) rename {pe/qt => qt/pe}/modules/block/block.pyx (100%) rename {pe/qt => qt/pe}/modules/block/setup.py (100%) rename {pe/qt => qt/pe}/preferences.py (100%) rename {pe/qt => qt/pe}/preferences_dialog.py (100%) rename {pe/qt => qt/pe}/preferences_dialog.ui (100%) rename {pe/qt => qt/pe}/profile.py (100%) rename {pe/qt => qt/pe}/start.py (100%) rename {pe/qt => qt/pe}/verinfo (100%) rename {se/qt => qt/se}/app.py (100%) rename {se/qt => qt/se}/build.py (100%) rename {se/qt => qt/se}/details_dialog.py (100%) rename {se/qt => qt/se}/details_dialog.ui (100%) rename {se/qt => qt/se}/dgse.spec (84%) rename {se/qt => qt/se}/gen.py (67%) rename {se/qt => qt/se}/installer.aip (100%) rename {se/qt => qt/se}/preferences.py (100%) rename {se/qt => qt/se}/preferences_dialog.py (100%) rename {se/qt => qt/se}/preferences_dialog.ui (100%) rename {se/qt => qt/se}/profile.py (100%) rename {se/qt => qt/se}/start.py (100%) rename {se/qt => qt/se}/verinfo (100%) create mode 100644 run.py delete mode 100644 se/cocoa/LICENSE delete mode 100644 se/cocoa/gen.py delete mode 100644 se/cocoa/py/gen.py delete mode 100644 se/core/LICENSE delete mode 100644 se/help/LICENSE delete mode 100644 se/qt/LICENSE delete mode 100644 se/qt/WARNING diff --git a/base/cocoa/LICENSE b/LICENSE similarity index 100% rename from base/cocoa/LICENSE rename to LICENSE diff --git a/README b/README new file mode 100644 index 00000000..4f9c540d --- /dev/null +++ b/README @@ -0,0 +1,68 @@ +Contents +===== + +This package contains the source for dupeGuru. To learns how to build it, refer to the "Build dupeGuru" section. Below is the description of the various subfolders: + +- core: Contains the core logic code for dupeGuru. It's Python code written in TDD style. +- 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. Before redistributing changes in this part of the code, read the "qt/WARNING" file. +- images: Images used by the different UI codebases. + +There are also other sub-folder that comes from external repositories (automatically checked out +with svn:externals): + +- hsutil: A collection of helpers used across HS applications. +- hsdocgen: An ad-hoc document generation used across HS project (used for help files) +- hsmedia: A library to read audio file metadata, used in dupeGuru ME. +- 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. + +dupeGuru Dependencies +===== + +Before being able to build dupeGuru, a few dependencies have to be installed: + +General dependencies +----- + +- Python 2.6 (http://www.python.org) +- Mako, to generate help files. (http://www.makotemplates.org/) +- PyYaml, for help files and the build system. (http://pyyaml.org/) +- Nose, to run unit tests. (http://somethingaboutorange.com/mrl/projects/nose/) +- Cython to compile a few optimized bottlenecks. (http://www.cython.org/) +- Python Imaging Library for dupeGuru PE. (http://www.pythonware.com/products/pil/) + +OS X prerequisites +----- + +- XCode 3.1 (http://developer.apple.com/TOOLS/xcode/) +- Sparkle (http://sparkle.andymatuschak.org/) +- PyObjC. Although Tiger support has been dropped with dupeGuru 1.7, I still use PyObjC 1.4 because funky stuff happens with newer releases. However, it's mostly related to packaging with py2app. (http://pyobjc.sourceforge.net/) +- py2app (http://svn.pythonmac.org/py2app/py2app/trunk/doc/index.html) + +Windows prerequisites +--- + +- Visual Studio 2008 (Express is enough) is needed to build the Cython extensions. (http://www.microsoft.com/Express/) +- PyQt 4.6 (http://www.riverbankcomputing.co.uk/news) +- PyInstaller, if you want to build a exe. You don't need it if you just want to run dupeGuru. (http://www.pyinstaller.org/) +- Advanced Installer, if you want to build the installer file. (http://www.advancedinstaller.com/) + +Building dupeGuru +===== + +First, make sure you meet the dependencies listed in the section above. Then you need to configure your build with: + + python configure.py + +If you want, you can specify a UI to use with the `--ui` option. So, if you want to build dupeGuru with Qt on OS X, then you have to type `python configure.py --ui=qt`. You can also use the `--dev` flag to indicate a dev build (it will build `mg_cocoa.plugin` in alias mode). + +Then, just build the thing and then run it with: + + python build.py + python run.py + +If you want to create ready-to-upload package, run: + + python package.py diff --git a/base/qt/dg.qrc b/base/qt/dg.qrc deleted file mode 100644 index bd8d0a26..00000000 --- a/base/qt/dg.qrc +++ /dev/null @@ -1,16 +0,0 @@ - - - images/details32.png - images/dgpe_logo_32.png - images/dgpe_logo_128.png - images/dgme_logo_32.png - images/dgme_logo_128.png - images/dgse_logo_32.png - images/dgse_logo_128.png - images/folderwin32.png - images/preferences32.png - images/actions32.png - images/delta32.png - images/power_marker32.png - - \ No newline at end of file diff --git a/build.py b/build.py new file mode 100644 index 00000000..4ab1e047 --- /dev/null +++ b/build.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- +# Created By: Virgil Dupras +# Created On: 2009-12-30 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license + +import sys +import os +import os.path as op + +import yaml + +from hsutil.build import move_testdata_out, put_testdata_back, add_to_pythonpath + +def main(): + conf = yaml.load(open('conf.yaml')) + edition = conf['edition'] + ui = conf['ui'] + dev = conf['dev'] + print "Building dupeGuru {0} with UI {1}".format(edition.upper(), ui) + add_to_pythonpath('.') + if dev: + print "Building in Dev mode" + print "Generating Help" + windows = sys.platform=='win32' + if edition == 'se': + import help_se.gen + help_se.gen.generate(windows=windows, force_render=not dev) + elif edition == 'me': + import help_me.gen + help_me.gen.generate(windows=windows, force_render=not dev) + elif edition == 'pe': + import help_pe.gen + help_pe.gen.generate(windows=windows, force_render=not dev) + + print "Building dupeGuru" + if edition == 'pe': + os.chdir('core_pe') + os.system('python gen.py') + os.chdir('..') + if ui == 'cocoa': + move_log = move_testdata_out() + try: + os.chdir(op.join('cocoa', edition)) + if dev: + os.system('python gen.py --dev') + else: + os.system('python gen.py') + os.chdir(op.join('..', '..')) + finally: + put_testdata_back(move_log) + elif ui == 'qt': + os.chdir(op.join('qt', edition)) + os.system('python gen.py') + os.chdir(op.join('..', '..')) + +if __name__ == '__main__': + main() diff --git a/base/cocoa/AppDelegate.h b/cocoa/base/AppDelegate.h similarity index 100% rename from base/cocoa/AppDelegate.h rename to cocoa/base/AppDelegate.h diff --git a/base/cocoa/AppDelegate.m b/cocoa/base/AppDelegate.m similarity index 100% rename from base/cocoa/AppDelegate.m rename to cocoa/base/AppDelegate.m diff --git a/base/cocoa/Consts.h b/cocoa/base/Consts.h similarity index 100% rename from base/cocoa/Consts.h rename to cocoa/base/Consts.h diff --git a/base/cocoa/DetailsPanel.h b/cocoa/base/DetailsPanel.h similarity index 100% rename from base/cocoa/DetailsPanel.h rename to cocoa/base/DetailsPanel.h diff --git a/base/cocoa/DetailsPanel.m b/cocoa/base/DetailsPanel.m similarity index 100% rename from base/cocoa/DetailsPanel.m rename to cocoa/base/DetailsPanel.m diff --git a/base/cocoa/DirectoryPanel.h b/cocoa/base/DirectoryPanel.h similarity index 100% rename from base/cocoa/DirectoryPanel.h rename to cocoa/base/DirectoryPanel.h diff --git a/base/cocoa/DirectoryPanel.m b/cocoa/base/DirectoryPanel.m similarity index 100% rename from base/cocoa/DirectoryPanel.m rename to cocoa/base/DirectoryPanel.m diff --git a/base/cocoa/PyDupeGuru.h b/cocoa/base/PyDupeGuru.h similarity index 100% rename from base/cocoa/PyDupeGuru.h rename to cocoa/base/PyDupeGuru.h diff --git a/base/cocoa/ResultWindow.h b/cocoa/base/ResultWindow.h similarity index 100% rename from base/cocoa/ResultWindow.h rename to cocoa/base/ResultWindow.h diff --git a/base/cocoa/ResultWindow.m b/cocoa/base/ResultWindow.m similarity index 100% rename from base/cocoa/ResultWindow.m rename to cocoa/base/ResultWindow.m diff --git a/base/cocoa/dsa_pub.pem b/cocoa/base/dsa_pub.pem similarity index 100% rename from base/cocoa/dsa_pub.pem rename to cocoa/base/dsa_pub.pem diff --git a/base/cocoa/xib/DetailsPanel.xib b/cocoa/base/xib/DetailsPanel.xib similarity index 100% rename from base/cocoa/xib/DetailsPanel.xib rename to cocoa/base/xib/DetailsPanel.xib diff --git a/base/cocoa/xib/DirectoryPanel.xib b/cocoa/base/xib/DirectoryPanel.xib similarity index 100% rename from base/cocoa/xib/DirectoryPanel.xib rename to cocoa/base/xib/DirectoryPanel.xib diff --git a/base/cocoa/xib/MainMenu.xib b/cocoa/base/xib/MainMenu.xib similarity index 100% rename from base/cocoa/xib/MainMenu.xib rename to cocoa/base/xib/MainMenu.xib diff --git a/me/cocoa/AppDelegate.h b/cocoa/me/AppDelegate.h similarity index 94% rename from me/cocoa/AppDelegate.h rename to cocoa/me/AppDelegate.h index 8bdeac69..fee028ed 100644 --- a/me/cocoa/AppDelegate.h +++ b/cocoa/me/AppDelegate.h @@ -7,7 +7,7 @@ http://www.hardcoded.net/licenses/hs_license */ #import -#import "dgbase/AppDelegate.h" +#import "../base/AppDelegate.h" #import "ResultWindow.h" #import "DirectoryPanel.h" #import "PyDupeGuru.h" diff --git a/me/cocoa/AppDelegate.m b/cocoa/me/AppDelegate.m similarity index 94% rename from me/cocoa/AppDelegate.m rename to cocoa/me/AppDelegate.m index a91cb290..d618300d 100644 --- a/me/cocoa/AppDelegate.m +++ b/cocoa/me/AppDelegate.m @@ -7,11 +7,11 @@ http://www.hardcoded.net/licenses/hs_license */ #import "AppDelegate.h" -#import "cocoalib/ProgressController.h" -#import "cocoalib/RegistrationInterface.h" -#import "cocoalib/Utils.h" -#import "cocoalib/ValueTransformers.h" -#import "cocoalib/Dialogs.h" +#import "../../cocoalib/ProgressController.h" +#import "../../cocoalib/RegistrationInterface.h" +#import "../../cocoalib/Utils.h" +#import "../../cocoalib/ValueTransformers.h" +#import "../../cocoalib/Dialogs.h" #import "DetailsPanel.h" #import "Consts.h" diff --git a/me/cocoa/Consts.h b/cocoa/me/Consts.h similarity index 92% rename from me/cocoa/Consts.h rename to cocoa/me/Consts.h index ebc7c1f3..489f17a9 100644 --- a/me/cocoa/Consts.h +++ b/cocoa/me/Consts.h @@ -6,7 +6,7 @@ which should be included with this package. The terms are also available at http://www.hardcoded.net/licenses/hs_license */ -#import "dgbase/Consts.h" +#import "../base/Consts.h" #define APPNAME @"dupeGuru ME" diff --git a/me/cocoa/DetailsPanel.h b/cocoa/me/DetailsPanel.h similarity index 91% rename from me/cocoa/DetailsPanel.h rename to cocoa/me/DetailsPanel.h index eca22c12..4c1b5cca 100644 --- a/me/cocoa/DetailsPanel.h +++ b/cocoa/me/DetailsPanel.h @@ -7,7 +7,7 @@ http://www.hardcoded.net/licenses/hs_license */ #import -#import "dgbase/DetailsPanel.h" +#import "../base/DetailsPanel.h" @interface DetailsPanel : DetailsPanelBase diff --git a/me/cocoa/DetailsPanel.m b/cocoa/me/DetailsPanel.m similarity index 100% rename from me/cocoa/DetailsPanel.m rename to cocoa/me/DetailsPanel.m diff --git a/me/cocoa/DirectoryPanel.h b/cocoa/me/DirectoryPanel.h similarity index 91% rename from me/cocoa/DirectoryPanel.h rename to cocoa/me/DirectoryPanel.h index 536258af..23d05199 100644 --- a/me/cocoa/DirectoryPanel.h +++ b/cocoa/me/DirectoryPanel.h @@ -7,7 +7,7 @@ http://www.hardcoded.net/licenses/hs_license */ #import -#import "dgbase/DirectoryPanel.h" +#import "../base/DirectoryPanel.h" @interface DirectoryPanel : DirectoryPanelBase { diff --git a/me/cocoa/DirectoryPanel.m b/cocoa/me/DirectoryPanel.m similarity index 100% rename from me/cocoa/DirectoryPanel.m rename to cocoa/me/DirectoryPanel.m diff --git a/me/cocoa/Info.plist b/cocoa/me/Info.plist similarity index 100% rename from me/cocoa/Info.plist rename to cocoa/me/Info.plist diff --git a/me/cocoa/PyDupeGuru.h b/cocoa/me/PyDupeGuru.h similarity index 96% rename from me/cocoa/PyDupeGuru.h rename to cocoa/me/PyDupeGuru.h index 1d45ad3a..b7631a27 100644 --- a/me/cocoa/PyDupeGuru.h +++ b/cocoa/me/PyDupeGuru.h @@ -7,7 +7,7 @@ http://www.hardcoded.net/licenses/hs_license */ #import -#import "dgbase/PyDupeGuru.h" +#import "../base/PyDupeGuru.h" @interface PyDupeGuru : PyDupeGuruBase //Scanning options diff --git a/me/cocoa/ResultWindow.h b/cocoa/me/ResultWindow.h similarity index 94% rename from me/cocoa/ResultWindow.h rename to cocoa/me/ResultWindow.h index e175a4c9..c918ca32 100644 --- a/me/cocoa/ResultWindow.h +++ b/cocoa/me/ResultWindow.h @@ -7,8 +7,8 @@ http://www.hardcoded.net/licenses/hs_license */ #import -#import "cocoalib/Outline.h" -#import "dgbase/ResultWindow.h" +#import "../../cocoalib/Outline.h" +#import "../base/ResultWindow.h" #import "DirectoryPanel.h" @interface ResultWindow : ResultWindowBase diff --git a/me/cocoa/ResultWindow.m b/cocoa/me/ResultWindow.m similarity index 98% rename from me/cocoa/ResultWindow.m rename to cocoa/me/ResultWindow.m index 0ad98ca4..67fd2917 100644 --- a/me/cocoa/ResultWindow.m +++ b/cocoa/me/ResultWindow.m @@ -7,10 +7,10 @@ http://www.hardcoded.net/licenses/hs_license */ #import "ResultWindow.h" -#import "cocoalib/Dialogs.h" -#import "cocoalib/ProgressController.h" -#import "cocoalib/RegistrationInterface.h" -#import "cocoalib/Utils.h" +#import "../../cocoalib/Dialogs.h" +#import "../../cocoalib/ProgressController.h" +#import "../../cocoalib/RegistrationInterface.h" +#import "../../cocoalib/Utils.h" #import "AppDelegate.h" #import "Consts.h" diff --git a/me/cocoa/dupeguru.icns b/cocoa/me/dupeguru.icns similarity index 100% rename from me/cocoa/dupeguru.icns rename to cocoa/me/dupeguru.icns diff --git a/me/cocoa/dupeguru.xcodeproj/project.pbxproj b/cocoa/me/dupeguru.xcodeproj/project.pbxproj similarity index 86% rename from me/cocoa/dupeguru.xcodeproj/project.pbxproj rename to cocoa/me/dupeguru.xcodeproj/project.pbxproj index 09fa0eb0..556e626e 100644 --- a/me/cocoa/dupeguru.xcodeproj/project.pbxproj +++ b/cocoa/me/dupeguru.xcodeproj/project.pbxproj @@ -82,61 +82,61 @@ 29B97325FDCFA39411CA2CEA /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = /System/Library/Frameworks/Foundation.framework; sourceTree = ""; }; 8D1107310486CEB800E47090 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = SOURCE_ROOT; }; 8D1107320486CEB800E47090 /* dupeGuru ME.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "dupeGuru ME.app"; sourceTree = BUILT_PRODUCTS_DIR; }; - CE073F5409CAE1A3005C1D2F /* dupeguru_me_help */ = {isa = PBXFileReference; lastKnownFileType = folder; name = dupeguru_me_help; path = help/dupeguru_me_help; sourceTree = ""; }; + CE073F5409CAE1A3005C1D2F /* dupeguru_me_help */ = {isa = PBXFileReference; lastKnownFileType = folder; name = dupeguru_me_help; path = ../../help_me/dupeguru_me_help; sourceTree = ""; }; CE1425880AFB718500BD5167 /* Sparkle.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Sparkle.framework; path = /Library/Frameworks/Sparkle.framework; sourceTree = ""; }; CE381C9409914ACE003581CE /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = SOURCE_ROOT; }; CE381C9509914ACE003581CE /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = SOURCE_ROOT; }; CE381C9A09914ADF003581CE /* ResultWindow.m */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.objc; path = ResultWindow.m; sourceTree = SOURCE_ROOT; }; CE381C9B09914ADF003581CE /* ResultWindow.h */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.h; path = ResultWindow.h; sourceTree = SOURCE_ROOT; }; CE381CF509915304003581CE /* dg_cocoa.plugin */ = {isa = PBXFileReference; lastKnownFileType = folder; name = dg_cocoa.plugin; path = py/dist/dg_cocoa.plugin; sourceTree = SOURCE_ROOT; }; - CE3FBDD11094637800B72D77 /* DetailsPanel.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = DetailsPanel.xib; sourceTree = ""; }; - CE3FBDD21094637800B72D77 /* DirectoryPanel.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = DirectoryPanel.xib; sourceTree = ""; }; - CE49DEF20FDFEB810098617B /* BRSingleLineFormatter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BRSingleLineFormatter.h; path = cocoalib/brsinglelineformatter/BRSingleLineFormatter.h; sourceTree = SOURCE_ROOT; }; - CE49DEF30FDFEB810098617B /* BRSingleLineFormatter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BRSingleLineFormatter.m; path = cocoalib/brsinglelineformatter/BRSingleLineFormatter.m; sourceTree = SOURCE_ROOT; }; - CE515DE00FC6C12E00EC695D /* Dialogs.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Dialogs.h; path = cocoalib/Dialogs.h; sourceTree = SOURCE_ROOT; }; - CE515DE10FC6C12E00EC695D /* Dialogs.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Dialogs.m; path = cocoalib/Dialogs.m; sourceTree = SOURCE_ROOT; }; - CE515DE20FC6C12E00EC695D /* HSErrorReportWindow.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = HSErrorReportWindow.h; path = cocoalib/HSErrorReportWindow.h; sourceTree = SOURCE_ROOT; }; - CE515DE30FC6C12E00EC695D /* HSErrorReportWindow.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = HSErrorReportWindow.m; path = cocoalib/HSErrorReportWindow.m; sourceTree = SOURCE_ROOT; }; - CE515DE40FC6C12E00EC695D /* Outline.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Outline.h; path = cocoalib/Outline.h; sourceTree = SOURCE_ROOT; }; - CE515DE50FC6C12E00EC695D /* Outline.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Outline.m; path = cocoalib/Outline.m; sourceTree = SOURCE_ROOT; }; - CE515DE60FC6C12E00EC695D /* ProgressController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ProgressController.h; path = cocoalib/ProgressController.h; sourceTree = SOURCE_ROOT; }; - CE515DE70FC6C12E00EC695D /* ProgressController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ProgressController.m; path = cocoalib/ProgressController.m; sourceTree = SOURCE_ROOT; }; - CE515DE80FC6C12E00EC695D /* PyApp.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PyApp.h; path = cocoalib/PyApp.h; sourceTree = SOURCE_ROOT; }; - CE515DE90FC6C12E00EC695D /* RecentDirectories.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RecentDirectories.h; path = cocoalib/RecentDirectories.h; sourceTree = SOURCE_ROOT; }; - CE515DEA0FC6C12E00EC695D /* RecentDirectories.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RecentDirectories.m; path = cocoalib/RecentDirectories.m; sourceTree = SOURCE_ROOT; }; - CE515DEB0FC6C12E00EC695D /* RegistrationInterface.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RegistrationInterface.h; path = cocoalib/RegistrationInterface.h; sourceTree = SOURCE_ROOT; }; - CE515DEC0FC6C12E00EC695D /* RegistrationInterface.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RegistrationInterface.m; path = cocoalib/RegistrationInterface.m; sourceTree = SOURCE_ROOT; }; - CE515DED0FC6C12E00EC695D /* Table.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Table.h; path = cocoalib/Table.h; sourceTree = SOURCE_ROOT; }; - CE515DEE0FC6C12E00EC695D /* Table.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Table.m; path = cocoalib/Table.m; sourceTree = SOURCE_ROOT; }; - CE515DEF0FC6C12E00EC695D /* Utils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Utils.h; path = cocoalib/Utils.h; sourceTree = SOURCE_ROOT; }; - CE515DF00FC6C12E00EC695D /* Utils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Utils.m; path = cocoalib/Utils.m; sourceTree = SOURCE_ROOT; }; - CE515DF10FC6C12E00EC695D /* ValueTransformers.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ValueTransformers.h; path = cocoalib/ValueTransformers.h; sourceTree = SOURCE_ROOT; }; - CE515DF20FC6C12E00EC695D /* ValueTransformers.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ValueTransformers.m; path = cocoalib/ValueTransformers.m; sourceTree = SOURCE_ROOT; }; - CE515DFD0FC6C13E00EC695D /* English */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = English; path = cocoalib/English.lproj/ErrorReportWindow.xib; sourceTree = ""; }; - CE515DFF0FC6C13E00EC695D /* English */ = {isa = PBXFileReference; lastKnownFileType = wrapper.nib; name = English; path = cocoalib/English.lproj/progress.nib; sourceTree = ""; }; - CE515E010FC6C13E00EC695D /* English */ = {isa = PBXFileReference; lastKnownFileType = wrapper.nib; name = English; path = cocoalib/English.lproj/registration.nib; sourceTree = ""; }; - CE515E150FC6C19300EC695D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = dgbase/AppDelegate.h; sourceTree = SOURCE_ROOT; }; - CE515E160FC6C19300EC695D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AppDelegate.m; path = dgbase/AppDelegate.m; sourceTree = SOURCE_ROOT; }; - CE515E170FC6C19300EC695D /* Consts.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Consts.h; path = dgbase/Consts.h; sourceTree = SOURCE_ROOT; }; - CE515E180FC6C19300EC695D /* DirectoryPanel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = DirectoryPanel.h; path = dgbase/DirectoryPanel.h; sourceTree = SOURCE_ROOT; }; - CE515E190FC6C19300EC695D /* DirectoryPanel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = DirectoryPanel.m; path = dgbase/DirectoryPanel.m; sourceTree = SOURCE_ROOT; }; - CE515E1A0FC6C19300EC695D /* PyDupeGuru.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PyDupeGuru.h; path = dgbase/PyDupeGuru.h; sourceTree = SOURCE_ROOT; }; - CE515E1B0FC6C19300EC695D /* ResultWindow.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ResultWindow.h; path = dgbase/ResultWindow.h; sourceTree = SOURCE_ROOT; }; - CE515E1C0FC6C19300EC695D /* ResultWindow.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ResultWindow.m; path = dgbase/ResultWindow.m; sourceTree = SOURCE_ROOT; }; - CE6032BE0FE6784C007E33FF /* DetailsPanel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = DetailsPanel.h; path = dgbase/DetailsPanel.h; sourceTree = SOURCE_ROOT; }; - CE6032BF0FE6784C007E33FF /* DetailsPanel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = DetailsPanel.m; path = dgbase/DetailsPanel.m; sourceTree = SOURCE_ROOT; }; + CE3FBDD11094637800B72D77 /* DetailsPanel.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = DetailsPanel.xib; path = ../../base/xib/DetailsPanel.xib; sourceTree = ""; }; + CE3FBDD21094637800B72D77 /* DirectoryPanel.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = DirectoryPanel.xib; path = ../../base/xib/DirectoryPanel.xib; sourceTree = ""; }; + CE49DEF20FDFEB810098617B /* BRSingleLineFormatter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BRSingleLineFormatter.h; path = ../../cocoalib/brsinglelineformatter/BRSingleLineFormatter.h; sourceTree = SOURCE_ROOT; }; + CE49DEF30FDFEB810098617B /* BRSingleLineFormatter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BRSingleLineFormatter.m; path = ../../cocoalib/brsinglelineformatter/BRSingleLineFormatter.m; sourceTree = SOURCE_ROOT; }; + CE515DE00FC6C12E00EC695D /* Dialogs.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Dialogs.h; path = ../../cocoalib/Dialogs.h; sourceTree = SOURCE_ROOT; }; + CE515DE10FC6C12E00EC695D /* Dialogs.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Dialogs.m; path = ../../cocoalib/Dialogs.m; sourceTree = SOURCE_ROOT; }; + CE515DE20FC6C12E00EC695D /* HSErrorReportWindow.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = HSErrorReportWindow.h; path = ../../cocoalib/HSErrorReportWindow.h; sourceTree = SOURCE_ROOT; }; + CE515DE30FC6C12E00EC695D /* HSErrorReportWindow.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = HSErrorReportWindow.m; path = ../../cocoalib/HSErrorReportWindow.m; sourceTree = SOURCE_ROOT; }; + CE515DE40FC6C12E00EC695D /* Outline.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Outline.h; path = ../../cocoalib/Outline.h; sourceTree = SOURCE_ROOT; }; + CE515DE50FC6C12E00EC695D /* Outline.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Outline.m; path = ../../cocoalib/Outline.m; sourceTree = SOURCE_ROOT; }; + CE515DE60FC6C12E00EC695D /* ProgressController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ProgressController.h; path = ../../cocoalib/ProgressController.h; sourceTree = SOURCE_ROOT; }; + CE515DE70FC6C12E00EC695D /* ProgressController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ProgressController.m; path = ../../cocoalib/ProgressController.m; sourceTree = SOURCE_ROOT; }; + CE515DE80FC6C12E00EC695D /* PyApp.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PyApp.h; path = ../../cocoalib/PyApp.h; sourceTree = SOURCE_ROOT; }; + CE515DE90FC6C12E00EC695D /* RecentDirectories.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RecentDirectories.h; path = ../../cocoalib/RecentDirectories.h; sourceTree = SOURCE_ROOT; }; + CE515DEA0FC6C12E00EC695D /* RecentDirectories.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RecentDirectories.m; path = ../../cocoalib/RecentDirectories.m; sourceTree = SOURCE_ROOT; }; + CE515DEB0FC6C12E00EC695D /* RegistrationInterface.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RegistrationInterface.h; path = ../../cocoalib/RegistrationInterface.h; sourceTree = SOURCE_ROOT; }; + CE515DEC0FC6C12E00EC695D /* RegistrationInterface.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RegistrationInterface.m; path = ../../cocoalib/RegistrationInterface.m; sourceTree = SOURCE_ROOT; }; + CE515DED0FC6C12E00EC695D /* Table.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Table.h; path = ../../cocoalib/Table.h; sourceTree = SOURCE_ROOT; }; + CE515DEE0FC6C12E00EC695D /* Table.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Table.m; path = ../../cocoalib/Table.m; sourceTree = SOURCE_ROOT; }; + CE515DEF0FC6C12E00EC695D /* Utils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Utils.h; path = ../../cocoalib/Utils.h; sourceTree = SOURCE_ROOT; }; + CE515DF00FC6C12E00EC695D /* Utils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Utils.m; path = ../../cocoalib/Utils.m; sourceTree = SOURCE_ROOT; }; + CE515DF10FC6C12E00EC695D /* ValueTransformers.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ValueTransformers.h; path = ../../cocoalib/ValueTransformers.h; sourceTree = SOURCE_ROOT; }; + CE515DF20FC6C12E00EC695D /* ValueTransformers.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ValueTransformers.m; path = ../../cocoalib/ValueTransformers.m; sourceTree = SOURCE_ROOT; }; + CE515DFD0FC6C13E00EC695D /* English */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = English; path = ../../cocoalib/English.lproj/ErrorReportWindow.xib; sourceTree = ""; }; + CE515DFF0FC6C13E00EC695D /* English */ = {isa = PBXFileReference; lastKnownFileType = wrapper.nib; name = English; path = ../../cocoalib/English.lproj/progress.nib; sourceTree = ""; }; + CE515E010FC6C13E00EC695D /* English */ = {isa = PBXFileReference; lastKnownFileType = wrapper.nib; name = English; path = ../../cocoalib/English.lproj/registration.nib; sourceTree = ""; }; + CE515E150FC6C19300EC695D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = ../base/AppDelegate.h; sourceTree = SOURCE_ROOT; }; + CE515E160FC6C19300EC695D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AppDelegate.m; path = ../base/AppDelegate.m; sourceTree = SOURCE_ROOT; }; + CE515E170FC6C19300EC695D /* Consts.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Consts.h; path = ../base/Consts.h; sourceTree = SOURCE_ROOT; }; + CE515E180FC6C19300EC695D /* DirectoryPanel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = DirectoryPanel.h; path = ../base/DirectoryPanel.h; sourceTree = SOURCE_ROOT; }; + CE515E190FC6C19300EC695D /* DirectoryPanel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = DirectoryPanel.m; path = ../base/DirectoryPanel.m; sourceTree = SOURCE_ROOT; }; + CE515E1A0FC6C19300EC695D /* PyDupeGuru.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PyDupeGuru.h; path = ../base/PyDupeGuru.h; sourceTree = SOURCE_ROOT; }; + CE515E1B0FC6C19300EC695D /* ResultWindow.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ResultWindow.h; path = ../base/ResultWindow.h; sourceTree = SOURCE_ROOT; }; + CE515E1C0FC6C19300EC695D /* ResultWindow.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ResultWindow.m; path = ../base/ResultWindow.m; sourceTree = SOURCE_ROOT; }; + CE6032BE0FE6784C007E33FF /* DetailsPanel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = DetailsPanel.h; path = ../base/DetailsPanel.h; sourceTree = SOURCE_ROOT; }; + CE6032BF0FE6784C007E33FF /* DetailsPanel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = DetailsPanel.m; path = ../base/DetailsPanel.m; sourceTree = SOURCE_ROOT; }; CE68EE6509ABC48000971085 /* DirectoryPanel.h */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.h; path = DirectoryPanel.h; sourceTree = SOURCE_ROOT; }; CE68EE6609ABC48000971085 /* DirectoryPanel.m */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.objc; path = DirectoryPanel.m; sourceTree = SOURCE_ROOT; }; - CE6E0E9E1054EB97008D9390 /* dsa_pub.pem */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = dsa_pub.pem; path = dgbase/dsa_pub.pem; sourceTree = ""; }; + CE6E0E9E1054EB97008D9390 /* dsa_pub.pem */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = dsa_pub.pem; path = ../base/dsa_pub.pem; sourceTree = ""; }; CE848A1809DD85810004CB44 /* Consts.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Consts.h; sourceTree = ""; }; - CE900AD1109B238600754048 /* Preferences.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Preferences.xib; path = ../../xib/Preferences.xib; sourceTree = ""; }; - CE900AD6109B2A9B00754048 /* MainMenu.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = MainMenu.xib; sourceTree = ""; }; + CE900AD1109B238600754048 /* Preferences.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = Preferences.xib; sourceTree = ""; }; + CE900AD6109B2A9B00754048 /* MainMenu.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = MainMenu.xib; path = ../../base/xib/MainMenu.xib; sourceTree = ""; }; CECA899A09DB132E00A3D774 /* DetailsPanel.h */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.h; path = DetailsPanel.h; sourceTree = ""; }; CECA899B09DB132E00A3D774 /* DetailsPanel.m */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.objc; path = DetailsPanel.m; sourceTree = ""; }; CEEB135109C837A2004D2330 /* dupeguru.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; path = dupeguru.icns; sourceTree = ""; }; - CEFC294509C89E3D00D9F998 /* folder32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = folder32.png; path = images/folder32.png; sourceTree = SOURCE_ROOT; }; - CEFC295309C89FF200D9F998 /* details32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = details32.png; path = images/details32.png; sourceTree = SOURCE_ROOT; }; - CEFC295409C89FF200D9F998 /* preferences32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = preferences32.png; path = images/preferences32.png; sourceTree = SOURCE_ROOT; }; + CEFC294509C89E3D00D9F998 /* folder32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = folder32.png; path = ../../images/folder32.png; sourceTree = SOURCE_ROOT; }; + CEFC295309C89FF200D9F998 /* details32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = details32.png; path = ../../images/details32.png; sourceTree = SOURCE_ROOT; }; + CEFC295409C89FF200D9F998 /* preferences32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = preferences32.png; path = ../../images/preferences32.png; sourceTree = SOURCE_ROOT; }; CEFF18A009A4D387005E6321 /* PyDupeGuru.h */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.h; path = PyDupeGuru.h; sourceTree = SOURCE_ROOT; }; /* End PBXFileReference section */ @@ -250,8 +250,7 @@ CE3FBDD21094637800B72D77 /* DirectoryPanel.xib */, CE900AD1109B238600754048 /* Preferences.xib */, ); - name = xib; - path = dgbase/xib; + path = xib; sourceTree = ""; }; CE49DEF10FDFEB810098617B /* brsinglelineformatter */ = { @@ -261,7 +260,7 @@ CE49DEF30FDFEB810098617B /* BRSingleLineFormatter.m */, ); name = brsinglelineformatter; - path = cocoalib/brsinglelineformatter; + path = ../../cocoalib/brsinglelineformatter; sourceTree = SOURCE_ROOT; }; CE515DDD0FC6C09400EC695D /* cocoalib */ = { @@ -445,11 +444,6 @@ C01FCF4C08A954540054247B /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - FRAMEWORK_SEARCH_PATHS = ( - "$(FRAMEWORK_SEARCH_PATHS)", - "$(SRCROOT)/../../../cocoalib/build/Release", - "\"$(SRCROOT)/../../base/cocoa/build/Release\"", - ); GCC_GENERATE_DEBUGGING_SYMBOLS = NO; GCC_MODEL_TUNING = G5; INFOPLIST_FILE = Info.plist; diff --git a/cocoa/me/gen.py b/cocoa/me/gen.py new file mode 100644 index 00000000..bd69f84e --- /dev/null +++ b/cocoa/me/gen.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license + +import os.path as op +import os +import shutil +from optparse import OptionParser + +def main(dev): + if not dev: + print "Building help index" + help_path = op.abspath('../../help_me/dupeguru_me_help') + os.system('open /Developer/Applications/Utilities/Help\\ Indexer.app --args {0}'.format(help_path)) + + print "Building dg_cocoa.plugin" + if op.exists('py/build'): + shutil.rmtree('py/build') + if op.exists('py/dist'): + shutil.rmtree('py/dist') + + os.chdir('py') + if dev: + os.system('python -u setup.py py2app -A') + else: + os.system('python -u setup.py py2app') + os.chdir('..') + + print "Building the XCode project" + os.system('xcodebuild') + +if __name__ == '__main__': + usage = "usage: %prog [options]" + parser = OptionParser(usage=usage) + parser.add_option('--dev', action='store_true', dest='dev', default=False, + help="If this flag is set, will configure for dev builds.") + (options, args) = parser.parse_args() + main(options.dev) \ No newline at end of file diff --git a/me/cocoa/main.m b/cocoa/me/main.m similarity index 100% rename from me/cocoa/main.m rename to cocoa/me/main.m diff --git a/me/cocoa/py/dg_cocoa.py b/cocoa/me/py/dg_cocoa.py similarity index 100% rename from me/cocoa/py/dg_cocoa.py rename to cocoa/me/py/dg_cocoa.py diff --git a/me/cocoa/py/setup.py b/cocoa/me/py/setup.py similarity index 63% rename from me/cocoa/py/setup.py rename to cocoa/me/py/setup.py index c589447a..d6bd60b9 100644 --- a/me/cocoa/py/setup.py +++ b/cocoa/me/py/setup.py @@ -4,15 +4,13 @@ # which should be included with this package. The terms are also available at # http://www.hardcoded.net/licenses/hs_license +import os.path as op +import sys +sys.path.insert(0, op.abspath('../../..')) # for all cross-toolkit modules + from distutils.core import setup import py2app -from hsutil.build import move_testdata_out, put_testdata_back - -move_log = move_testdata_out() -try: - setup( - plugin = ['dg_cocoa.py'], - ) -finally: - put_testdata_back(move_log) +setup( + plugin = ['dg_cocoa.py'], +) diff --git a/me/cocoa/xib/Preferences.xib b/cocoa/me/xib/Preferences.xib similarity index 100% rename from me/cocoa/xib/Preferences.xib rename to cocoa/me/xib/Preferences.xib diff --git a/se/cocoa/AppDelegate.h b/cocoa/pe/AppDelegate.h similarity index 94% rename from se/cocoa/AppDelegate.h rename to cocoa/pe/AppDelegate.h index 602b6038..cf95800b 100644 --- a/se/cocoa/AppDelegate.h +++ b/cocoa/pe/AppDelegate.h @@ -7,7 +7,7 @@ http://www.hardcoded.net/licenses/hs_license */ #import -#import "dgbase/AppDelegate.h" +#import "../base/AppDelegate.h" #import "DirectoryPanel.h" #import "PyDupeGuru.h" diff --git a/pe/cocoa/AppDelegate.m b/cocoa/pe/AppDelegate.m similarity index 100% rename from pe/cocoa/AppDelegate.m rename to cocoa/pe/AppDelegate.m diff --git a/pe/cocoa/Consts.h b/cocoa/pe/Consts.h similarity index 93% rename from pe/cocoa/Consts.h rename to cocoa/pe/Consts.h index badc3600..fdeba4d1 100644 --- a/pe/cocoa/Consts.h +++ b/cocoa/pe/Consts.h @@ -6,7 +6,7 @@ which should be included with this package. The terms are also available at http://www.hardcoded.net/licenses/hs_license */ -#import "dgbase/Consts.h" +#import "../base/Consts.h" #define APPNAME @"dupeGuru PE" #define ImageLoadedNotification @"ImageLoadedNotification" diff --git a/pe/cocoa/DetailsPanel.h b/cocoa/pe/DetailsPanel.h similarity index 95% rename from pe/cocoa/DetailsPanel.h rename to cocoa/pe/DetailsPanel.h index 7f43535f..81109c75 100644 --- a/pe/cocoa/DetailsPanel.h +++ b/cocoa/pe/DetailsPanel.h @@ -7,7 +7,7 @@ http://www.hardcoded.net/licenses/hs_license */ #import -#import "dgbase/DetailsPanel.h" +#import "../base/DetailsPanel.h" @interface DetailsPanel : DetailsPanelBase { diff --git a/pe/cocoa/DetailsPanel.m b/cocoa/pe/DetailsPanel.m similarity index 100% rename from pe/cocoa/DetailsPanel.m rename to cocoa/pe/DetailsPanel.m diff --git a/pe/cocoa/DirectoryPanel.h b/cocoa/pe/DirectoryPanel.h similarity index 91% rename from pe/cocoa/DirectoryPanel.h rename to cocoa/pe/DirectoryPanel.h index c57b8140..90a7b7df 100644 --- a/pe/cocoa/DirectoryPanel.h +++ b/cocoa/pe/DirectoryPanel.h @@ -7,7 +7,7 @@ http://www.hardcoded.net/licenses/hs_license */ #import -#import "dgbase/DirectoryPanel.h" +#import "../base/DirectoryPanel.h" @interface DirectoryPanel : DirectoryPanelBase { diff --git a/pe/cocoa/DirectoryPanel.m b/cocoa/pe/DirectoryPanel.m similarity index 100% rename from pe/cocoa/DirectoryPanel.m rename to cocoa/pe/DirectoryPanel.m diff --git a/pe/cocoa/Info.plist b/cocoa/pe/Info.plist similarity index 100% rename from pe/cocoa/Info.plist rename to cocoa/pe/Info.plist diff --git a/pe/cocoa/PictureBlocks.h b/cocoa/pe/PictureBlocks.h similarity index 100% rename from pe/cocoa/PictureBlocks.h rename to cocoa/pe/PictureBlocks.h diff --git a/pe/cocoa/PictureBlocks.m b/cocoa/pe/PictureBlocks.m similarity index 100% rename from pe/cocoa/PictureBlocks.m rename to cocoa/pe/PictureBlocks.m diff --git a/pe/cocoa/PyDupeGuru.h b/cocoa/pe/PyDupeGuru.h similarity index 94% rename from pe/cocoa/PyDupeGuru.h rename to cocoa/pe/PyDupeGuru.h index 89191538..180875d4 100644 --- a/pe/cocoa/PyDupeGuru.h +++ b/cocoa/pe/PyDupeGuru.h @@ -7,7 +7,7 @@ http://www.hardcoded.net/licenses/hs_license */ #import -#import "dgbase/PyDupeGuru.h" +#import "../base/PyDupeGuru.h" @interface PyDupeGuru : PyDupeGuruBase - (void)clearPictureCache; diff --git a/pe/cocoa/ResultWindow.h b/cocoa/pe/ResultWindow.h similarity index 97% rename from pe/cocoa/ResultWindow.h rename to cocoa/pe/ResultWindow.h index 06769641..f93b2cc9 100644 --- a/pe/cocoa/ResultWindow.h +++ b/cocoa/pe/ResultWindow.h @@ -8,7 +8,7 @@ http://www.hardcoded.net/licenses/hs_license #import #import "Outline.h" -#import "dgbase/ResultWindow.h" +#import "../base/ResultWindow.h" @interface ResultWindow : ResultWindowBase { diff --git a/pe/cocoa/ResultWindow.m b/cocoa/pe/ResultWindow.m similarity index 100% rename from pe/cocoa/ResultWindow.m rename to cocoa/pe/ResultWindow.m diff --git a/pe/cocoa/dupeguru.icns b/cocoa/pe/dupeguru.icns similarity index 100% rename from pe/cocoa/dupeguru.icns rename to cocoa/pe/dupeguru.icns diff --git a/pe/cocoa/dupeguru.xcodeproj/project.pbxproj b/cocoa/pe/dupeguru.xcodeproj/project.pbxproj similarity index 86% rename from pe/cocoa/dupeguru.xcodeproj/project.pbxproj rename to cocoa/pe/dupeguru.xcodeproj/project.pbxproj index b3f8cf4a..2c7ea589 100644 --- a/pe/cocoa/dupeguru.xcodeproj/project.pbxproj +++ b/cocoa/pe/dupeguru.xcodeproj/project.pbxproj @@ -74,9 +74,9 @@ 29B97325FDCFA39411CA2CEA /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = /System/Library/Frameworks/Foundation.framework; sourceTree = ""; }; 8D1107310486CEB800E47090 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = SOURCE_ROOT; }; 8D1107320486CEB800E47090 /* dupeGuru PE.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "dupeGuru PE.app"; sourceTree = BUILT_PRODUCTS_DIR; }; - CE031750109B340A00517EE6 /* Preferences.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Preferences.xib; path = ../../xib/Preferences.xib; sourceTree = ""; }; - CE031753109B345200517EE6 /* MainMenu.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = MainMenu.xib; sourceTree = ""; }; - CE073F5409CAE1A3005C1D2F /* dupeguru_pe_help */ = {isa = PBXFileReference; lastKnownFileType = folder; name = dupeguru_pe_help; path = help/dupeguru_pe_help; sourceTree = SOURCE_ROOT; }; + CE031750109B340A00517EE6 /* Preferences.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = Preferences.xib; sourceTree = ""; }; + CE031753109B345200517EE6 /* MainMenu.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = MainMenu.xib; path = ../../base/xib/MainMenu.xib; sourceTree = ""; }; + CE073F5409CAE1A3005C1D2F /* dupeguru_pe_help */ = {isa = PBXFileReference; lastKnownFileType = folder; name = dupeguru_pe_help; path = ../../help_pe/dupeguru_pe_help; sourceTree = SOURCE_ROOT; }; CE0C46A80FA0647E000BE99B /* PictureBlocks.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PictureBlocks.h; sourceTree = ""; }; CE0C46A90FA0647E000BE99B /* PictureBlocks.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PictureBlocks.m; sourceTree = ""; }; CE15C8A70ADEB8B50061D4A5 /* Sparkle.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Sparkle.framework; path = /Library/Frameworks/Sparkle.framework; sourceTree = ""; }; @@ -85,57 +85,57 @@ CE381C9A09914ADF003581CE /* ResultWindow.m */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.objc; path = ResultWindow.m; sourceTree = SOURCE_ROOT; }; CE381C9B09914ADF003581CE /* ResultWindow.h */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.h; path = ResultWindow.h; sourceTree = SOURCE_ROOT; }; CE381CF509915304003581CE /* dg_cocoa.plugin */ = {isa = PBXFileReference; lastKnownFileType = folder; name = dg_cocoa.plugin; path = py/dist/dg_cocoa.plugin; sourceTree = SOURCE_ROOT; }; - CE6044EA0FE6796200B71262 /* DetailsPanel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = DetailsPanel.h; path = dgbase/DetailsPanel.h; sourceTree = SOURCE_ROOT; }; - CE6044EB0FE6796200B71262 /* DetailsPanel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = DetailsPanel.m; path = dgbase/DetailsPanel.m; sourceTree = SOURCE_ROOT; }; + CE6044EA0FE6796200B71262 /* DetailsPanel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = DetailsPanel.h; path = ../base/DetailsPanel.h; sourceTree = SOURCE_ROOT; }; + CE6044EB0FE6796200B71262 /* DetailsPanel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = DetailsPanel.m; path = ../base/DetailsPanel.m; sourceTree = SOURCE_ROOT; }; CE68EE6509ABC48000971085 /* DirectoryPanel.h */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.h; path = DirectoryPanel.h; sourceTree = SOURCE_ROOT; }; CE68EE6609ABC48000971085 /* DirectoryPanel.m */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.objc; path = DirectoryPanel.m; sourceTree = SOURCE_ROOT; }; - CE6E0F3C1054EC62008D9390 /* dsa_pub.pem */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = dsa_pub.pem; path = dgbase/dsa_pub.pem; sourceTree = ""; }; - CE77C89C10946C6D0078B0DB /* DirectoryPanel.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = DirectoryPanel.xib; sourceTree = ""; }; - CE77C8A710946CE20078B0DB /* DetailsPanel.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = DetailsPanel.xib; path = ../../xib/DetailsPanel.xib; sourceTree = ""; }; - CE80DB1B0FC192D60086DCA6 /* Dialogs.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Dialogs.h; path = cocoalib/Dialogs.h; sourceTree = SOURCE_ROOT; }; - CE80DB1C0FC192D60086DCA6 /* Dialogs.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Dialogs.m; path = cocoalib/Dialogs.m; sourceTree = SOURCE_ROOT; }; - CE80DB1D0FC192D60086DCA6 /* HSErrorReportWindow.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = HSErrorReportWindow.h; path = cocoalib/HSErrorReportWindow.h; sourceTree = SOURCE_ROOT; }; - CE80DB1E0FC192D60086DCA6 /* HSErrorReportWindow.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = HSErrorReportWindow.m; path = cocoalib/HSErrorReportWindow.m; sourceTree = SOURCE_ROOT; }; - CE80DB1F0FC192D60086DCA6 /* Outline.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Outline.h; path = cocoalib/Outline.h; sourceTree = SOURCE_ROOT; }; - CE80DB200FC192D60086DCA6 /* Outline.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Outline.m; path = cocoalib/Outline.m; sourceTree = SOURCE_ROOT; }; - CE80DB210FC192D60086DCA6 /* ProgressController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ProgressController.h; path = cocoalib/ProgressController.h; sourceTree = SOURCE_ROOT; }; - CE80DB220FC192D60086DCA6 /* ProgressController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ProgressController.m; path = cocoalib/ProgressController.m; sourceTree = SOURCE_ROOT; }; - CE80DB230FC192D60086DCA6 /* PyApp.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PyApp.h; path = cocoalib/PyApp.h; sourceTree = SOURCE_ROOT; }; - CE80DB240FC192D60086DCA6 /* RecentDirectories.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RecentDirectories.h; path = cocoalib/RecentDirectories.h; sourceTree = SOURCE_ROOT; }; - CE80DB250FC192D60086DCA6 /* RecentDirectories.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RecentDirectories.m; path = cocoalib/RecentDirectories.m; sourceTree = SOURCE_ROOT; }; - CE80DB260FC192D60086DCA6 /* RegistrationInterface.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RegistrationInterface.h; path = cocoalib/RegistrationInterface.h; sourceTree = SOURCE_ROOT; }; - CE80DB270FC192D60086DCA6 /* RegistrationInterface.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RegistrationInterface.m; path = cocoalib/RegistrationInterface.m; sourceTree = SOURCE_ROOT; }; - CE80DB280FC192D60086DCA6 /* Table.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Table.h; path = cocoalib/Table.h; sourceTree = SOURCE_ROOT; }; - CE80DB290FC192D60086DCA6 /* Table.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Table.m; path = cocoalib/Table.m; sourceTree = SOURCE_ROOT; }; - CE80DB2A0FC192D60086DCA6 /* Utils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Utils.h; path = cocoalib/Utils.h; sourceTree = SOURCE_ROOT; }; - CE80DB2B0FC192D60086DCA6 /* Utils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Utils.m; path = cocoalib/Utils.m; sourceTree = SOURCE_ROOT; }; - CE80DB2C0FC192D60086DCA6 /* ValueTransformers.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ValueTransformers.h; path = cocoalib/ValueTransformers.h; sourceTree = SOURCE_ROOT; }; - CE80DB2D0FC192D60086DCA6 /* ValueTransformers.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ValueTransformers.m; path = cocoalib/ValueTransformers.m; sourceTree = SOURCE_ROOT; }; - CE80DB450FC193650086DCA6 /* NSNotificationAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = NSNotificationAdditions.h; path = cocoalib/NSNotificationAdditions.h; sourceTree = SOURCE_ROOT; }; - CE80DB460FC193650086DCA6 /* NSNotificationAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = NSNotificationAdditions.m; path = cocoalib/NSNotificationAdditions.m; sourceTree = SOURCE_ROOT; }; - CE80DB480FC193770086DCA6 /* NSImageAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = NSImageAdditions.h; path = cocoalib/NSImageAdditions.h; sourceTree = SOURCE_ROOT; }; - CE80DB490FC193770086DCA6 /* NSImageAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = NSImageAdditions.m; path = cocoalib/NSImageAdditions.m; sourceTree = SOURCE_ROOT; }; - CE80DB710FC194760086DCA6 /* English */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = English; path = cocoalib/English.lproj/ErrorReportWindow.xib; sourceTree = SOURCE_ROOT; }; - CE80DB730FC194760086DCA6 /* English */ = {isa = PBXFileReference; lastKnownFileType = wrapper.nib; name = English; path = cocoalib/English.lproj/progress.nib; sourceTree = SOURCE_ROOT; }; - CE80DB750FC194760086DCA6 /* English */ = {isa = PBXFileReference; lastKnownFileType = wrapper.nib; name = English; path = cocoalib/English.lproj/registration.nib; sourceTree = SOURCE_ROOT; }; - CE80DB820FC1951C0086DCA6 /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = dgbase/AppDelegate.h; sourceTree = SOURCE_ROOT; }; - CE80DB830FC1951C0086DCA6 /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AppDelegate.m; path = dgbase/AppDelegate.m; sourceTree = SOURCE_ROOT; }; - CE80DB840FC1951C0086DCA6 /* Consts.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Consts.h; path = dgbase/Consts.h; sourceTree = SOURCE_ROOT; }; - CE80DB850FC1951C0086DCA6 /* DirectoryPanel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = DirectoryPanel.h; path = dgbase/DirectoryPanel.h; sourceTree = SOURCE_ROOT; }; - CE80DB860FC1951C0086DCA6 /* DirectoryPanel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = DirectoryPanel.m; path = dgbase/DirectoryPanel.m; sourceTree = SOURCE_ROOT; }; - CE80DB870FC1951C0086DCA6 /* PyDupeGuru.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PyDupeGuru.h; path = dgbase/PyDupeGuru.h; sourceTree = SOURCE_ROOT; }; - CE80DB880FC1951C0086DCA6 /* ResultWindow.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ResultWindow.h; path = dgbase/ResultWindow.h; sourceTree = SOURCE_ROOT; }; - CE80DB890FC1951C0086DCA6 /* ResultWindow.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ResultWindow.m; path = dgbase/ResultWindow.m; sourceTree = SOURCE_ROOT; }; + CE6E0F3C1054EC62008D9390 /* dsa_pub.pem */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = dsa_pub.pem; path = ../base/dsa_pub.pem; sourceTree = ""; }; + CE77C89C10946C6D0078B0DB /* DirectoryPanel.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = DirectoryPanel.xib; path = ../../base/xib/DirectoryPanel.xib; sourceTree = ""; }; + CE77C8A710946CE20078B0DB /* DetailsPanel.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = DetailsPanel.xib; sourceTree = ""; }; + CE80DB1B0FC192D60086DCA6 /* Dialogs.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Dialogs.h; path = ../../cocoalib/Dialogs.h; sourceTree = SOURCE_ROOT; }; + CE80DB1C0FC192D60086DCA6 /* Dialogs.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Dialogs.m; path = ../../cocoalib/Dialogs.m; sourceTree = SOURCE_ROOT; }; + CE80DB1D0FC192D60086DCA6 /* HSErrorReportWindow.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = HSErrorReportWindow.h; path = ../../cocoalib/HSErrorReportWindow.h; sourceTree = SOURCE_ROOT; }; + CE80DB1E0FC192D60086DCA6 /* HSErrorReportWindow.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = HSErrorReportWindow.m; path = ../../cocoalib/HSErrorReportWindow.m; sourceTree = SOURCE_ROOT; }; + CE80DB1F0FC192D60086DCA6 /* Outline.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Outline.h; path = ../../cocoalib/Outline.h; sourceTree = SOURCE_ROOT; }; + CE80DB200FC192D60086DCA6 /* Outline.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Outline.m; path = ../../cocoalib/Outline.m; sourceTree = SOURCE_ROOT; }; + CE80DB210FC192D60086DCA6 /* ProgressController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ProgressController.h; path = ../../cocoalib/ProgressController.h; sourceTree = SOURCE_ROOT; }; + CE80DB220FC192D60086DCA6 /* ProgressController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ProgressController.m; path = ../../cocoalib/ProgressController.m; sourceTree = SOURCE_ROOT; }; + CE80DB230FC192D60086DCA6 /* PyApp.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PyApp.h; path = ../../cocoalib/PyApp.h; sourceTree = SOURCE_ROOT; }; + CE80DB240FC192D60086DCA6 /* RecentDirectories.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RecentDirectories.h; path = ../../cocoalib/RecentDirectories.h; sourceTree = SOURCE_ROOT; }; + CE80DB250FC192D60086DCA6 /* RecentDirectories.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RecentDirectories.m; path = ../../cocoalib/RecentDirectories.m; sourceTree = SOURCE_ROOT; }; + CE80DB260FC192D60086DCA6 /* RegistrationInterface.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RegistrationInterface.h; path = ../../cocoalib/RegistrationInterface.h; sourceTree = SOURCE_ROOT; }; + CE80DB270FC192D60086DCA6 /* RegistrationInterface.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RegistrationInterface.m; path = ../../cocoalib/RegistrationInterface.m; sourceTree = SOURCE_ROOT; }; + CE80DB280FC192D60086DCA6 /* Table.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Table.h; path = ../../cocoalib/Table.h; sourceTree = SOURCE_ROOT; }; + CE80DB290FC192D60086DCA6 /* Table.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Table.m; path = ../../cocoalib/Table.m; sourceTree = SOURCE_ROOT; }; + CE80DB2A0FC192D60086DCA6 /* Utils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Utils.h; path = ../../cocoalib/Utils.h; sourceTree = SOURCE_ROOT; }; + CE80DB2B0FC192D60086DCA6 /* Utils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Utils.m; path = ../../cocoalib/Utils.m; sourceTree = SOURCE_ROOT; }; + CE80DB2C0FC192D60086DCA6 /* ValueTransformers.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ValueTransformers.h; path = ../../cocoalib/ValueTransformers.h; sourceTree = SOURCE_ROOT; }; + CE80DB2D0FC192D60086DCA6 /* ValueTransformers.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ValueTransformers.m; path = ../../cocoalib/ValueTransformers.m; sourceTree = SOURCE_ROOT; }; + CE80DB450FC193650086DCA6 /* NSNotificationAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = NSNotificationAdditions.h; path = ../../cocoalib/NSNotificationAdditions.h; sourceTree = SOURCE_ROOT; }; + CE80DB460FC193650086DCA6 /* NSNotificationAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = NSNotificationAdditions.m; path = ../../cocoalib/NSNotificationAdditions.m; sourceTree = SOURCE_ROOT; }; + CE80DB480FC193770086DCA6 /* NSImageAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = NSImageAdditions.h; path = ../../cocoalib/NSImageAdditions.h; sourceTree = SOURCE_ROOT; }; + CE80DB490FC193770086DCA6 /* NSImageAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = NSImageAdditions.m; path = ../../cocoalib/NSImageAdditions.m; sourceTree = SOURCE_ROOT; }; + CE80DB710FC194760086DCA6 /* English */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = English; path = ../../cocoalib/English.lproj/ErrorReportWindow.xib; sourceTree = SOURCE_ROOT; }; + CE80DB730FC194760086DCA6 /* English */ = {isa = PBXFileReference; lastKnownFileType = wrapper.nib; name = English; path = ../../cocoalib/English.lproj/progress.nib; sourceTree = SOURCE_ROOT; }; + CE80DB750FC194760086DCA6 /* English */ = {isa = PBXFileReference; lastKnownFileType = wrapper.nib; name = English; path = ../../cocoalib/English.lproj/registration.nib; sourceTree = SOURCE_ROOT; }; + CE80DB820FC1951C0086DCA6 /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = ../base/AppDelegate.h; sourceTree = SOURCE_ROOT; }; + CE80DB830FC1951C0086DCA6 /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AppDelegate.m; path = ../base/AppDelegate.m; sourceTree = SOURCE_ROOT; }; + CE80DB840FC1951C0086DCA6 /* Consts.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Consts.h; path = ../base/Consts.h; sourceTree = SOURCE_ROOT; }; + CE80DB850FC1951C0086DCA6 /* DirectoryPanel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = DirectoryPanel.h; path = ../base/DirectoryPanel.h; sourceTree = SOURCE_ROOT; }; + CE80DB860FC1951C0086DCA6 /* DirectoryPanel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = DirectoryPanel.m; path = ../base/DirectoryPanel.m; sourceTree = SOURCE_ROOT; }; + CE80DB870FC1951C0086DCA6 /* PyDupeGuru.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PyDupeGuru.h; path = ../base/PyDupeGuru.h; sourceTree = SOURCE_ROOT; }; + CE80DB880FC1951C0086DCA6 /* ResultWindow.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ResultWindow.h; path = ../base/ResultWindow.h; sourceTree = SOURCE_ROOT; }; + CE80DB890FC1951C0086DCA6 /* ResultWindow.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ResultWindow.m; path = ../base/ResultWindow.m; sourceTree = SOURCE_ROOT; }; CE848A1809DD85810004CB44 /* Consts.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Consts.h; sourceTree = ""; }; - CEBAE4230FDA97E000B7887D /* BRSingleLineFormatter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BRSingleLineFormatter.h; path = cocoalib/brsinglelineformatter/BRSingleLineFormatter.h; sourceTree = SOURCE_ROOT; }; - CEBAE4240FDA97E000B7887D /* BRSingleLineFormatter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BRSingleLineFormatter.m; path = cocoalib/brsinglelineformatter/BRSingleLineFormatter.m; sourceTree = SOURCE_ROOT; }; + CEBAE4230FDA97E000B7887D /* BRSingleLineFormatter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BRSingleLineFormatter.h; path = ../../cocoalib/brsinglelineformatter/BRSingleLineFormatter.h; sourceTree = SOURCE_ROOT; }; + CEBAE4240FDA97E000B7887D /* BRSingleLineFormatter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BRSingleLineFormatter.m; path = ../../cocoalib/brsinglelineformatter/BRSingleLineFormatter.m; sourceTree = SOURCE_ROOT; }; CECA899A09DB132E00A3D774 /* DetailsPanel.h */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.h; path = DetailsPanel.h; sourceTree = ""; }; CECA899B09DB132E00A3D774 /* DetailsPanel.m */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.objc; path = DetailsPanel.m; sourceTree = ""; }; CEEB135109C837A2004D2330 /* dupeguru.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; path = dupeguru.icns; sourceTree = ""; }; - CEFC294509C89E3D00D9F998 /* folder32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = folder32.png; path = images/folder32.png; sourceTree = SOURCE_ROOT; }; - CEFC295309C89FF200D9F998 /* details32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = details32.png; path = images/details32.png; sourceTree = SOURCE_ROOT; }; - CEFC295409C89FF200D9F998 /* preferences32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = preferences32.png; path = images/preferences32.png; sourceTree = SOURCE_ROOT; }; - CEFCDE2C0AB0418600C33A93 /* dgpe_logo_32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = dgpe_logo_32.png; path = images/dgpe_logo_32.png; sourceTree = SOURCE_ROOT; }; + CEFC294509C89E3D00D9F998 /* folder32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = folder32.png; path = ../../images/folder32.png; sourceTree = SOURCE_ROOT; }; + CEFC295309C89FF200D9F998 /* details32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = details32.png; path = ../../images/details32.png; sourceTree = SOURCE_ROOT; }; + CEFC295409C89FF200D9F998 /* preferences32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = preferences32.png; path = ../../images/preferences32.png; sourceTree = SOURCE_ROOT; }; + CEFCDE2C0AB0418600C33A93 /* dgpe_logo_32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = dgpe_logo_32.png; path = ../../images/dgpe_logo_32.png; sourceTree = SOURCE_ROOT; }; CEFF18A009A4D387005E6321 /* PyDupeGuru.h */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.h; path = PyDupeGuru.h; sourceTree = SOURCE_ROOT; }; /* End PBXFileReference section */ @@ -251,8 +251,7 @@ CE77C89C10946C6D0078B0DB /* DirectoryPanel.xib */, CE031750109B340A00517EE6 /* Preferences.xib */, ); - name = xib; - path = dgbase/xib; + path = xib; sourceTree = ""; }; CE80DB1A0FC192AB0086DCA6 /* cocoalib */ = { @@ -313,7 +312,7 @@ CEBAE4240FDA97E000B7887D /* BRSingleLineFormatter.m */, ); name = brsinglelineformatter; - path = cocoalib/brsinglelineformatter; + path = ../../cocoalib/brsinglelineformatter; sourceTree = SOURCE_ROOT; }; CEFC294309C89E0000D9F998 /* images */ = { diff --git a/cocoa/pe/gen.py b/cocoa/pe/gen.py new file mode 100644 index 00000000..4176197d --- /dev/null +++ b/cocoa/pe/gen.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license + +import os.path as op +import os +import shutil +from optparse import OptionParser + +def main(dev): + if not dev: + print "Building help index" + help_path = op.abspath('../../help_pe/dupeguru_pe_help') + os.system('open /Developer/Applications/Utilities/Help\\ Indexer.app --args {0}'.format(help_path)) + + print "Building dg_cocoa.plugin" + if op.exists('py/build'): + shutil.rmtree('py/build') + if op.exists('py/dist'): + shutil.rmtree('py/dist') + + os.chdir('py') + if dev: + os.system('python -u setup.py py2app -A') + else: + os.system('python -u setup.py py2app') + os.chdir('..') + + print "Building the XCode project" + os.system('xcodebuild') + +if __name__ == '__main__': + usage = "usage: %prog [options]" + parser = OptionParser(usage=usage) + parser.add_option('--dev', action='store_true', dest='dev', default=False, + help="If this flag is set, will configure for dev builds.") + (options, args) = parser.parse_args() + main(options.dev) \ No newline at end of file diff --git a/pe/cocoa/main.m b/cocoa/pe/main.m similarity index 100% rename from pe/cocoa/main.m rename to cocoa/pe/main.m diff --git a/pe/cocoa/py/dg_cocoa.py b/cocoa/pe/py/dg_cocoa.py similarity index 100% rename from pe/cocoa/py/dg_cocoa.py rename to cocoa/pe/py/dg_cocoa.py diff --git a/pe/cocoa/py/setup.py b/cocoa/pe/py/setup.py similarity index 63% rename from pe/cocoa/py/setup.py rename to cocoa/pe/py/setup.py index c589447a..06434ffb 100644 --- a/pe/cocoa/py/setup.py +++ b/cocoa/pe/py/setup.py @@ -7,12 +7,6 @@ from distutils.core import setup import py2app -from hsutil.build import move_testdata_out, put_testdata_back - -move_log = move_testdata_out() -try: - setup( - plugin = ['dg_cocoa.py'], - ) -finally: - put_testdata_back(move_log) +setup( + plugin = ['dg_cocoa.py'], +) \ No newline at end of file diff --git a/pe/cocoa/xib/DetailsPanel.xib b/cocoa/pe/xib/DetailsPanel.xib similarity index 100% rename from pe/cocoa/xib/DetailsPanel.xib rename to cocoa/pe/xib/DetailsPanel.xib diff --git a/pe/cocoa/xib/Preferences.xib b/cocoa/pe/xib/Preferences.xib similarity index 100% rename from pe/cocoa/xib/Preferences.xib rename to cocoa/pe/xib/Preferences.xib diff --git a/pe/cocoa/AppDelegate.h b/cocoa/se/AppDelegate.h similarity index 94% rename from pe/cocoa/AppDelegate.h rename to cocoa/se/AppDelegate.h index 602b6038..cf95800b 100644 --- a/pe/cocoa/AppDelegate.h +++ b/cocoa/se/AppDelegate.h @@ -7,7 +7,7 @@ http://www.hardcoded.net/licenses/hs_license */ #import -#import "dgbase/AppDelegate.h" +#import "../base/AppDelegate.h" #import "DirectoryPanel.h" #import "PyDupeGuru.h" diff --git a/se/cocoa/AppDelegate.m b/cocoa/se/AppDelegate.m similarity index 95% rename from se/cocoa/AppDelegate.m rename to cocoa/se/AppDelegate.m index d3ecb58e..7ddcdd89 100644 --- a/se/cocoa/AppDelegate.m +++ b/cocoa/se/AppDelegate.m @@ -7,10 +7,10 @@ http://www.hardcoded.net/licenses/hs_license */ #import "AppDelegate.h" -#import "cocoalib/ProgressController.h" -#import "cocoalib/RegistrationInterface.h" -#import "cocoalib/Utils.h" -#import "cocoalib/ValueTransformers.h" +#import "../../cocoalib/ProgressController.h" +#import "../../cocoalib/RegistrationInterface.h" +#import "../../cocoalib/Utils.h" +#import "../../cocoalib/ValueTransformers.h" #import "DetailsPanel.h" #import "Consts.h" diff --git a/se/cocoa/Consts.h b/cocoa/se/Consts.h similarity index 91% rename from se/cocoa/Consts.h rename to cocoa/se/Consts.h index aaff95ae..b5048810 100644 --- a/se/cocoa/Consts.h +++ b/cocoa/se/Consts.h @@ -6,6 +6,6 @@ which should be included with this package. The terms are also available at http://www.hardcoded.net/licenses/hs_license */ -#import "dgbase/Consts.h" +#import "../base/Consts.h" #define APPNAME @"dupeGuru" \ No newline at end of file diff --git a/se/cocoa/DetailsPanel.h b/cocoa/se/DetailsPanel.h similarity index 91% rename from se/cocoa/DetailsPanel.h rename to cocoa/se/DetailsPanel.h index 9fad6e39..f7c26c86 100644 --- a/se/cocoa/DetailsPanel.h +++ b/cocoa/se/DetailsPanel.h @@ -7,7 +7,7 @@ http://www.hardcoded.net/licenses/hs_license */ #import -#import "dgbase/DetailsPanel.h" +#import "../base/DetailsPanel.h" @interface DetailsPanel : DetailsPanelBase @end \ No newline at end of file diff --git a/se/cocoa/DetailsPanel.m b/cocoa/se/DetailsPanel.m similarity index 100% rename from se/cocoa/DetailsPanel.m rename to cocoa/se/DetailsPanel.m diff --git a/se/cocoa/DirectoryPanel.h b/cocoa/se/DirectoryPanel.h similarity index 91% rename from se/cocoa/DirectoryPanel.h rename to cocoa/se/DirectoryPanel.h index dafdec92..f9f44d38 100644 --- a/se/cocoa/DirectoryPanel.h +++ b/cocoa/se/DirectoryPanel.h @@ -7,7 +7,7 @@ http://www.hardcoded.net/licenses/hs_license */ #import -#import "dgbase/DirectoryPanel.h" +#import "../base/DirectoryPanel.h" @interface DirectoryPanel : DirectoryPanelBase { diff --git a/se/cocoa/DirectoryPanel.m b/cocoa/se/DirectoryPanel.m similarity index 100% rename from se/cocoa/DirectoryPanel.m rename to cocoa/se/DirectoryPanel.m diff --git a/se/cocoa/Info.plist b/cocoa/se/Info.plist similarity index 100% rename from se/cocoa/Info.plist rename to cocoa/se/Info.plist diff --git a/se/cocoa/PyDupeGuru.h b/cocoa/se/PyDupeGuru.h similarity index 94% rename from se/cocoa/PyDupeGuru.h rename to cocoa/se/PyDupeGuru.h index d5cc454c..ac911aae 100644 --- a/se/cocoa/PyDupeGuru.h +++ b/cocoa/se/PyDupeGuru.h @@ -7,7 +7,7 @@ http://www.hardcoded.net/licenses/hs_license */ #import -#import "dgbase/PyDupeGuru.h" +#import "../base/PyDupeGuru.h" @interface PyDupeGuru : PyDupeGuruBase //Scanning options diff --git a/se/cocoa/ResultWindow.h b/cocoa/se/ResultWindow.h similarity index 94% rename from se/cocoa/ResultWindow.h rename to cocoa/se/ResultWindow.h index 86ec8305..7e6e7dfb 100644 --- a/se/cocoa/ResultWindow.h +++ b/cocoa/se/ResultWindow.h @@ -7,8 +7,8 @@ http://www.hardcoded.net/licenses/hs_license */ #import -#import "cocoalib/Outline.h" -#import "dgbase/ResultWindow.h" +#import "../../cocoalib/Outline.h" +#import "../base/ResultWindow.h" #import "DirectoryPanel.h" @interface ResultWindow : ResultWindowBase diff --git a/se/cocoa/ResultWindow.m b/cocoa/se/ResultWindow.m similarity index 98% rename from se/cocoa/ResultWindow.m rename to cocoa/se/ResultWindow.m index 942f4291..41e2779d 100644 --- a/se/cocoa/ResultWindow.m +++ b/cocoa/se/ResultWindow.m @@ -7,9 +7,9 @@ http://www.hardcoded.net/licenses/hs_license */ #import "ResultWindow.h" -#import "cocoalib/Dialogs.h" -#import "cocoalib/ProgressController.h" -#import "cocoalib/Utils.h" +#import "../../cocoalib/Dialogs.h" +#import "../../cocoalib/ProgressController.h" +#import "../../cocoalib/Utils.h" #import "AppDelegate.h" #import "Consts.h" diff --git a/se/cocoa/dupeguru.icns b/cocoa/se/dupeguru.icns similarity index 100% rename from se/cocoa/dupeguru.icns rename to cocoa/se/dupeguru.icns diff --git a/se/cocoa/dupeguru.xcodeproj/project.pbxproj b/cocoa/se/dupeguru.xcodeproj/project.pbxproj similarity index 87% rename from se/cocoa/dupeguru.xcodeproj/project.pbxproj rename to cocoa/se/dupeguru.xcodeproj/project.pbxproj index de0d842d..63b20181 100644 --- a/se/cocoa/dupeguru.xcodeproj/project.pbxproj +++ b/cocoa/se/dupeguru.xcodeproj/project.pbxproj @@ -70,62 +70,62 @@ 29B97325FDCFA39411CA2CEA /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = /System/Library/Frameworks/Foundation.framework; sourceTree = ""; }; 8D1107310486CEB800E47090 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = SOURCE_ROOT; }; 8D1107320486CEB800E47090 /* dupeGuru.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = dupeGuru.app; sourceTree = BUILT_PRODUCTS_DIR; }; - CE073F5409CAE1A3005C1D2F /* dupeguru_help */ = {isa = PBXFileReference; lastKnownFileType = folder; name = dupeguru_help; path = help/dupeguru_help; sourceTree = ""; }; + CE073F5409CAE1A3005C1D2F /* dupeguru_help */ = {isa = PBXFileReference; lastKnownFileType = folder; name = dupeguru_help; path = ../../help_se/dupeguru_help; sourceTree = ""; }; CE381C9409914ACE003581CE /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = SOURCE_ROOT; }; CE381C9509914ACE003581CE /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = SOURCE_ROOT; }; CE381C9A09914ADF003581CE /* ResultWindow.m */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.objc; path = ResultWindow.m; sourceTree = SOURCE_ROOT; }; CE381C9B09914ADF003581CE /* ResultWindow.h */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.h; path = ResultWindow.h; sourceTree = SOURCE_ROOT; }; CE381CF509915304003581CE /* dg_cocoa.plugin */ = {isa = PBXFileReference; lastKnownFileType = folder; name = dg_cocoa.plugin; path = py/dist/dg_cocoa.plugin; sourceTree = SOURCE_ROOT; }; - CE3A46F9109B212E002ABFD5 /* MainMenu.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = MainMenu.xib; path = dgbase/xib/MainMenu.xib; sourceTree = ""; }; + CE3A46F9109B212E002ABFD5 /* MainMenu.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = MainMenu.xib; path = ../base/xib/MainMenu.xib; sourceTree = ""; }; CE45579A0AE3BC2B005A9546 /* Sparkle.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Sparkle.framework; path = /Library/Frameworks/Sparkle.framework; sourceTree = ""; }; CE68EE6509ABC48000971085 /* DirectoryPanel.h */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.h; path = DirectoryPanel.h; sourceTree = SOURCE_ROOT; }; CE68EE6609ABC48000971085 /* DirectoryPanel.m */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.objc; path = DirectoryPanel.m; sourceTree = SOURCE_ROOT; }; - CE6E0DFD1054E9EF008D9390 /* dsa_pub.pem */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = dsa_pub.pem; path = dgbase/dsa_pub.pem; sourceTree = ""; }; + CE6E0DFD1054E9EF008D9390 /* dsa_pub.pem */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = dsa_pub.pem; path = ../base/dsa_pub.pem; sourceTree = ""; }; CE848A1809DD85810004CB44 /* Consts.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Consts.h; sourceTree = ""; }; CEAC6810109B0B7E00B43C85 /* Preferences.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Preferences.xib; path = xib/Preferences.xib; sourceTree = ""; }; CECA899A09DB132E00A3D774 /* DetailsPanel.h */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.h; path = DetailsPanel.h; sourceTree = ""; }; CECA899B09DB132E00A3D774 /* DetailsPanel.m */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.objc; path = DetailsPanel.m; sourceTree = ""; }; - CEDD92D60FDD01640031C7B7 /* BRSingleLineFormatter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BRSingleLineFormatter.h; path = cocoalib/brsinglelineformatter/BRSingleLineFormatter.h; sourceTree = SOURCE_ROOT; }; - CEDD92D70FDD01640031C7B7 /* BRSingleLineFormatter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BRSingleLineFormatter.m; path = cocoalib/brsinglelineformatter/BRSingleLineFormatter.m; sourceTree = SOURCE_ROOT; }; - CEE7EA110FE675C80004E467 /* DetailsPanel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = DetailsPanel.h; path = dgbase/DetailsPanel.h; sourceTree = SOURCE_ROOT; }; - CEE7EA120FE675C80004E467 /* DetailsPanel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = DetailsPanel.m; path = dgbase/DetailsPanel.m; sourceTree = SOURCE_ROOT; }; + CEDD92D60FDD01640031C7B7 /* BRSingleLineFormatter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BRSingleLineFormatter.h; path = ../../cocoalib/brsinglelineformatter/BRSingleLineFormatter.h; sourceTree = SOURCE_ROOT; }; + CEDD92D70FDD01640031C7B7 /* BRSingleLineFormatter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BRSingleLineFormatter.m; path = ../../cocoalib/brsinglelineformatter/BRSingleLineFormatter.m; sourceTree = SOURCE_ROOT; }; + CEE7EA110FE675C80004E467 /* DetailsPanel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = DetailsPanel.h; path = ../base/DetailsPanel.h; sourceTree = SOURCE_ROOT; }; + CEE7EA120FE675C80004E467 /* DetailsPanel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = DetailsPanel.m; path = ../base/DetailsPanel.m; sourceTree = SOURCE_ROOT; }; CEEB135109C837A2004D2330 /* dupeguru.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; path = dupeguru.icns; sourceTree = ""; }; - CEEFC0F710945D9F001F3A39 /* DirectoryPanel.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = DirectoryPanel.xib; path = dgbase/xib/DirectoryPanel.xib; sourceTree = ""; }; - CEEFC0FA10945E37001F3A39 /* DetailsPanel.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = DetailsPanel.xib; path = dgbase/xib/DetailsPanel.xib; sourceTree = ""; }; - CEFC294509C89E3D00D9F998 /* folder32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = folder32.png; path = images/folder32.png; sourceTree = SOURCE_ROOT; }; - CEFC295309C89FF200D9F998 /* details32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = details32.png; path = images/details32.png; sourceTree = SOURCE_ROOT; }; - CEFC295409C89FF200D9F998 /* preferences32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = preferences32.png; path = images/preferences32.png; sourceTree = SOURCE_ROOT; }; - CEFC7F8A0FC9517500CD5728 /* Dialogs.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Dialogs.h; path = cocoalib/Dialogs.h; sourceTree = SOURCE_ROOT; }; - CEFC7F8B0FC9517500CD5728 /* Dialogs.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Dialogs.m; path = cocoalib/Dialogs.m; sourceTree = SOURCE_ROOT; }; - CEFC7F8C0FC9517500CD5728 /* HSErrorReportWindow.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = HSErrorReportWindow.h; path = cocoalib/HSErrorReportWindow.h; sourceTree = SOURCE_ROOT; }; - CEFC7F8D0FC9517500CD5728 /* HSErrorReportWindow.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = HSErrorReportWindow.m; path = cocoalib/HSErrorReportWindow.m; sourceTree = SOURCE_ROOT; }; - CEFC7F8E0FC9517500CD5728 /* Outline.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Outline.h; path = cocoalib/Outline.h; sourceTree = SOURCE_ROOT; }; - CEFC7F8F0FC9517500CD5728 /* Outline.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Outline.m; path = cocoalib/Outline.m; sourceTree = SOURCE_ROOT; }; - CEFC7F900FC9517500CD5728 /* ProgressController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ProgressController.h; path = cocoalib/ProgressController.h; sourceTree = SOURCE_ROOT; }; - CEFC7F910FC9517500CD5728 /* ProgressController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ProgressController.m; path = cocoalib/ProgressController.m; sourceTree = SOURCE_ROOT; }; - CEFC7F920FC9517500CD5728 /* PyApp.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PyApp.h; path = cocoalib/PyApp.h; sourceTree = SOURCE_ROOT; }; - CEFC7F930FC9517500CD5728 /* PyRegistrable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PyRegistrable.h; path = cocoalib/PyRegistrable.h; sourceTree = SOURCE_ROOT; }; - CEFC7F940FC9517500CD5728 /* RecentDirectories.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RecentDirectories.h; path = cocoalib/RecentDirectories.h; sourceTree = SOURCE_ROOT; }; - CEFC7F950FC9517500CD5728 /* RecentDirectories.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RecentDirectories.m; path = cocoalib/RecentDirectories.m; sourceTree = SOURCE_ROOT; }; - CEFC7F960FC9517500CD5728 /* RegistrationInterface.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RegistrationInterface.h; path = cocoalib/RegistrationInterface.h; sourceTree = SOURCE_ROOT; }; - CEFC7F970FC9517500CD5728 /* RegistrationInterface.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RegistrationInterface.m; path = cocoalib/RegistrationInterface.m; sourceTree = SOURCE_ROOT; }; - CEFC7F980FC9517500CD5728 /* Table.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Table.h; path = cocoalib/Table.h; sourceTree = SOURCE_ROOT; }; - CEFC7F990FC9517500CD5728 /* Table.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Table.m; path = cocoalib/Table.m; sourceTree = SOURCE_ROOT; }; - CEFC7F9A0FC9517500CD5728 /* Utils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Utils.h; path = cocoalib/Utils.h; sourceTree = SOURCE_ROOT; }; - CEFC7F9B0FC9517500CD5728 /* Utils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Utils.m; path = cocoalib/Utils.m; sourceTree = SOURCE_ROOT; }; - CEFC7F9C0FC9517500CD5728 /* ValueTransformers.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ValueTransformers.h; path = cocoalib/ValueTransformers.h; sourceTree = SOURCE_ROOT; }; - CEFC7F9D0FC9517500CD5728 /* ValueTransformers.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ValueTransformers.m; path = cocoalib/ValueTransformers.m; sourceTree = SOURCE_ROOT; }; - CEFC7FA80FC9518A00CD5728 /* English */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = English; path = cocoalib/English.lproj/ErrorReportWindow.xib; sourceTree = ""; }; - CEFC7FAA0FC9518A00CD5728 /* English */ = {isa = PBXFileReference; lastKnownFileType = wrapper.nib; name = English; path = cocoalib/English.lproj/progress.nib; sourceTree = ""; }; - CEFC7FAC0FC9518A00CD5728 /* English */ = {isa = PBXFileReference; lastKnownFileType = wrapper.nib; name = English; path = cocoalib/English.lproj/registration.nib; sourceTree = ""; }; - CEFC7FB10FC951A700CD5728 /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = dgbase/AppDelegate.h; sourceTree = SOURCE_ROOT; }; - CEFC7FB20FC951A700CD5728 /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AppDelegate.m; path = dgbase/AppDelegate.m; sourceTree = SOURCE_ROOT; }; - CEFC7FB30FC951A700CD5728 /* Consts.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Consts.h; path = dgbase/Consts.h; sourceTree = SOURCE_ROOT; }; - CEFC7FB40FC951A700CD5728 /* DirectoryPanel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = DirectoryPanel.h; path = dgbase/DirectoryPanel.h; sourceTree = SOURCE_ROOT; }; - CEFC7FB50FC951A700CD5728 /* DirectoryPanel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = DirectoryPanel.m; path = dgbase/DirectoryPanel.m; sourceTree = SOURCE_ROOT; }; - CEFC7FB60FC951A700CD5728 /* PyDupeGuru.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PyDupeGuru.h; path = dgbase/PyDupeGuru.h; sourceTree = SOURCE_ROOT; }; - CEFC7FB70FC951A700CD5728 /* ResultWindow.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ResultWindow.h; path = dgbase/ResultWindow.h; sourceTree = SOURCE_ROOT; }; - CEFC7FB80FC951A700CD5728 /* ResultWindow.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ResultWindow.m; path = dgbase/ResultWindow.m; sourceTree = SOURCE_ROOT; }; + CEEFC0F710945D9F001F3A39 /* DirectoryPanel.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = DirectoryPanel.xib; path = ../base/xib/DirectoryPanel.xib; sourceTree = ""; }; + CEEFC0FA10945E37001F3A39 /* DetailsPanel.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = DetailsPanel.xib; path = ../base/xib/DetailsPanel.xib; sourceTree = ""; }; + CEFC294509C89E3D00D9F998 /* folder32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = folder32.png; path = ../../images/folder32.png; sourceTree = SOURCE_ROOT; }; + CEFC295309C89FF200D9F998 /* details32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = details32.png; path = ../../images/details32.png; sourceTree = SOURCE_ROOT; }; + CEFC295409C89FF200D9F998 /* preferences32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = preferences32.png; path = ../../images/preferences32.png; sourceTree = SOURCE_ROOT; }; + CEFC7F8A0FC9517500CD5728 /* Dialogs.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Dialogs.h; path = ../../cocoalib/Dialogs.h; sourceTree = SOURCE_ROOT; }; + CEFC7F8B0FC9517500CD5728 /* Dialogs.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Dialogs.m; path = ../../cocoalib/Dialogs.m; sourceTree = SOURCE_ROOT; }; + CEFC7F8C0FC9517500CD5728 /* HSErrorReportWindow.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = HSErrorReportWindow.h; path = ../../cocoalib/HSErrorReportWindow.h; sourceTree = SOURCE_ROOT; }; + CEFC7F8D0FC9517500CD5728 /* HSErrorReportWindow.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = HSErrorReportWindow.m; path = ../../cocoalib/HSErrorReportWindow.m; sourceTree = SOURCE_ROOT; }; + CEFC7F8E0FC9517500CD5728 /* Outline.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Outline.h; path = ../../cocoalib/Outline.h; sourceTree = SOURCE_ROOT; }; + CEFC7F8F0FC9517500CD5728 /* Outline.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Outline.m; path = ../../cocoalib/Outline.m; sourceTree = SOURCE_ROOT; }; + CEFC7F900FC9517500CD5728 /* ProgressController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ProgressController.h; path = ../../cocoalib/ProgressController.h; sourceTree = SOURCE_ROOT; }; + CEFC7F910FC9517500CD5728 /* ProgressController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ProgressController.m; path = ../../cocoalib/ProgressController.m; sourceTree = SOURCE_ROOT; }; + CEFC7F920FC9517500CD5728 /* PyApp.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PyApp.h; path = ../../cocoalib/PyApp.h; sourceTree = SOURCE_ROOT; }; + CEFC7F930FC9517500CD5728 /* PyRegistrable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PyRegistrable.h; path = ../../cocoalib/PyRegistrable.h; sourceTree = SOURCE_ROOT; }; + CEFC7F940FC9517500CD5728 /* RecentDirectories.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RecentDirectories.h; path = ../../cocoalib/RecentDirectories.h; sourceTree = SOURCE_ROOT; }; + CEFC7F950FC9517500CD5728 /* RecentDirectories.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RecentDirectories.m; path = ../../cocoalib/RecentDirectories.m; sourceTree = SOURCE_ROOT; }; + CEFC7F960FC9517500CD5728 /* RegistrationInterface.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RegistrationInterface.h; path = ../../cocoalib/RegistrationInterface.h; sourceTree = SOURCE_ROOT; }; + CEFC7F970FC9517500CD5728 /* RegistrationInterface.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RegistrationInterface.m; path = ../../cocoalib/RegistrationInterface.m; sourceTree = SOURCE_ROOT; }; + CEFC7F980FC9517500CD5728 /* Table.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Table.h; path = ../../cocoalib/Table.h; sourceTree = SOURCE_ROOT; }; + CEFC7F990FC9517500CD5728 /* Table.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Table.m; path = ../../cocoalib/Table.m; sourceTree = SOURCE_ROOT; }; + CEFC7F9A0FC9517500CD5728 /* Utils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Utils.h; path = ../../cocoalib/Utils.h; sourceTree = SOURCE_ROOT; }; + CEFC7F9B0FC9517500CD5728 /* Utils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Utils.m; path = ../../cocoalib/Utils.m; sourceTree = SOURCE_ROOT; }; + CEFC7F9C0FC9517500CD5728 /* ValueTransformers.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ValueTransformers.h; path = ../../cocoalib/ValueTransformers.h; sourceTree = SOURCE_ROOT; }; + CEFC7F9D0FC9517500CD5728 /* ValueTransformers.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ValueTransformers.m; path = ../../cocoalib/ValueTransformers.m; sourceTree = SOURCE_ROOT; }; + CEFC7FA80FC9518A00CD5728 /* English */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = English; path = ../../cocoalib/English.lproj/ErrorReportWindow.xib; sourceTree = ""; }; + CEFC7FAA0FC9518A00CD5728 /* English */ = {isa = PBXFileReference; lastKnownFileType = wrapper.nib; name = English; path = ../../cocoalib/English.lproj/progress.nib; sourceTree = ""; }; + CEFC7FAC0FC9518A00CD5728 /* English */ = {isa = PBXFileReference; lastKnownFileType = wrapper.nib; name = English; path = ../../cocoalib/English.lproj/registration.nib; sourceTree = ""; }; + CEFC7FB10FC951A700CD5728 /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = ../base/AppDelegate.h; sourceTree = SOURCE_ROOT; }; + CEFC7FB20FC951A700CD5728 /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AppDelegate.m; path = ../base/AppDelegate.m; sourceTree = SOURCE_ROOT; }; + CEFC7FB30FC951A700CD5728 /* Consts.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Consts.h; path = ../base/Consts.h; sourceTree = SOURCE_ROOT; }; + CEFC7FB40FC951A700CD5728 /* DirectoryPanel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = DirectoryPanel.h; path = ../base/DirectoryPanel.h; sourceTree = SOURCE_ROOT; }; + CEFC7FB50FC951A700CD5728 /* DirectoryPanel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = DirectoryPanel.m; path = ../base/DirectoryPanel.m; sourceTree = SOURCE_ROOT; }; + CEFC7FB60FC951A700CD5728 /* PyDupeGuru.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PyDupeGuru.h; path = ../base/PyDupeGuru.h; sourceTree = SOURCE_ROOT; }; + CEFC7FB70FC951A700CD5728 /* ResultWindow.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ResultWindow.h; path = ../base/ResultWindow.h; sourceTree = SOURCE_ROOT; }; + CEFC7FB80FC951A700CD5728 /* ResultWindow.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ResultWindow.m; path = ../base/ResultWindow.m; sourceTree = SOURCE_ROOT; }; CEFF18A009A4D387005E6321 /* PyDupeGuru.h */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.h; path = PyDupeGuru.h; sourceTree = SOURCE_ROOT; }; /* End PBXFileReference section */ @@ -230,7 +230,7 @@ CEDD92D70FDD01640031C7B7 /* BRSingleLineFormatter.m */, ); name = brsinglelineformatter; - path = cocoalib/brsinglelineformatter; + path = ../../cocoalib/brsinglelineformatter; sourceTree = SOURCE_ROOT; }; CEEFC0CA10943849001F3A39 /* xib */ = { @@ -432,11 +432,6 @@ ppc, i386, ); - FRAMEWORK_SEARCH_PATHS = ( - "$(FRAMEWORK_SEARCH_PATHS)", - "$(SRCROOT)/../../../cocoalib/build/Release", - "$(FRAMEWORK_SEARCH_PATHS_QUOTED_FOR_TARGET_1)", - ); FRAMEWORK_SEARCH_PATHS_QUOTED_FOR_TARGET_1 = "\"$(SRCROOT)/../../base/cocoa/build/Release\""; GCC_GENERATE_DEBUGGING_SYMBOLS = NO; GCC_MODEL_TUNING = G5; diff --git a/cocoa/se/gen.py b/cocoa/se/gen.py new file mode 100644 index 00000000..d147b7ec --- /dev/null +++ b/cocoa/se/gen.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license + +import os.path as op +import os +import shutil +from optparse import OptionParser + +def main(dev): + if not dev: + print "Building help index" + help_path = op.abspath('../../help_se/dupeguru_help') + os.system('open /Developer/Applications/Utilities/Help\\ Indexer.app --args {0}'.format(help_path)) + + print "Building dg_cocoa.plugin" + if op.exists('py/build'): + shutil.rmtree('py/build') + if op.exists('py/dist'): + shutil.rmtree('py/dist') + + os.chdir('py') + if dev: + os.system('python -u setup.py py2app -A') + else: + os.system('python -u setup.py py2app') + os.chdir('..') + + print "Building the XCode project" + os.system('xcodebuild') + +if __name__ == '__main__': + usage = "usage: %prog [options]" + parser = OptionParser(usage=usage) + parser.add_option('--dev', action='store_true', dest='dev', default=False, + help="If this flag is set, will configure for dev builds.") + (options, args) = parser.parse_args() + main(options.dev) \ No newline at end of file diff --git a/se/cocoa/main.m b/cocoa/se/main.m similarity index 100% rename from se/cocoa/main.m rename to cocoa/se/main.m diff --git a/se/cocoa/py/dg_cocoa.py b/cocoa/se/py/dg_cocoa.py similarity index 100% rename from se/cocoa/py/dg_cocoa.py rename to cocoa/se/py/dg_cocoa.py diff --git a/se/cocoa/py/setup.py b/cocoa/se/py/setup.py similarity index 100% rename from se/cocoa/py/setup.py rename to cocoa/se/py/setup.py diff --git a/se/cocoa/xib/Preferences.xib b/cocoa/se/xib/Preferences.xib similarity index 100% rename from se/cocoa/xib/Preferences.xib rename to cocoa/se/xib/Preferences.xib diff --git a/configure.py b/configure.py new file mode 100644 index 00000000..a9b0d381 --- /dev/null +++ b/configure.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +# Created By: Virgil Dupras +# Created On: 2009-12-30 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license + +import sys +from optparse import OptionParser + +import yaml + +def main(edition, ui, dev): + if edition not in ('se', 'me', 'pe'): + edition = 'se' + if ui not in ('cocoa', 'qt'): + ui = 'cocoa' if sys.platform == 'darwin' else 'qt' + build_type = 'Dev' if dev else 'Release' + print "Configuring dupeGuru {0} for UI {1} ({2})".format(edition.upper(), ui, build_type) + conf = { + 'edition': edition, + 'ui': ui, + 'dev': dev, + } + yaml.dump(conf, open('conf.yaml', 'w')) + +if __name__ == '__main__': + usage = "usage: %prog [options]" + parser = OptionParser(usage=usage) + parser.add_option('--edition', dest='edition', + help="dupeGuru edition to build (se, me or pe). Default is se.") + 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.") + (options, args) = parser.parse_args() + main(options.edition, options.ui, options.dev) diff --git a/base/core/LICENSE b/core/LICENSE similarity index 100% rename from base/core/LICENSE rename to core/LICENSE diff --git a/base/core/__init__.py b/core/__init__.py similarity index 100% rename from base/core/__init__.py rename to core/__init__.py diff --git a/base/core/app.py b/core/app.py similarity index 100% rename from base/core/app.py rename to core/app.py diff --git a/base/core/app_cocoa.py b/core/app_cocoa.py similarity index 100% rename from base/core/app_cocoa.py rename to core/app_cocoa.py diff --git a/base/core/data.py b/core/data.py similarity index 100% rename from base/core/data.py rename to core/data.py diff --git a/base/core/directories.py b/core/directories.py similarity index 100% rename from base/core/directories.py rename to core/directories.py diff --git a/base/core/engine.py b/core/engine.py similarity index 100% rename from base/core/engine.py rename to core/engine.py diff --git a/base/core/export.py b/core/export.py similarity index 100% rename from base/core/export.py rename to core/export.py diff --git a/base/core/fs.py b/core/fs.py similarity index 100% rename from base/core/fs.py rename to core/fs.py diff --git a/base/core/ignore.py b/core/ignore.py similarity index 100% rename from base/core/ignore.py rename to core/ignore.py diff --git a/base/core/results.py b/core/results.py similarity index 100% rename from base/core/results.py rename to core/results.py diff --git a/base/core/scanner.py b/core/scanner.py similarity index 100% rename from base/core/scanner.py rename to core/scanner.py diff --git a/base/core/tests/__init__.py b/core/tests/__init__.py similarity index 100% rename from base/core/tests/__init__.py rename to core/tests/__init__.py diff --git a/base/core/tests/app_cocoa_test.py b/core/tests/app_cocoa_test.py similarity index 100% rename from base/core/tests/app_cocoa_test.py rename to core/tests/app_cocoa_test.py diff --git a/base/core/tests/app_test.py b/core/tests/app_test.py similarity index 100% rename from base/core/tests/app_test.py rename to core/tests/app_test.py diff --git a/base/core/tests/data.py b/core/tests/data.py similarity index 100% rename from base/core/tests/data.py rename to core/tests/data.py diff --git a/base/core/tests/directories_test.py b/core/tests/directories_test.py similarity index 100% rename from base/core/tests/directories_test.py rename to core/tests/directories_test.py diff --git a/base/core/tests/engine_test.py b/core/tests/engine_test.py similarity index 100% rename from base/core/tests/engine_test.py rename to core/tests/engine_test.py diff --git a/base/core/tests/ignore_test.py b/core/tests/ignore_test.py similarity index 100% rename from base/core/tests/ignore_test.py rename to core/tests/ignore_test.py diff --git a/base/core/tests/results_test.py b/core/tests/results_test.py similarity index 100% rename from base/core/tests/results_test.py rename to core/tests/results_test.py diff --git a/base/core/tests/scanner_test.py b/core/tests/scanner_test.py similarity index 100% rename from base/core/tests/scanner_test.py rename to core/tests/scanner_test.py diff --git a/base/qt/__init__.py b/core_me/__init__.py similarity index 100% rename from base/qt/__init__.py rename to core_me/__init__.py diff --git a/me/core/app_cocoa.py b/core_me/app_cocoa.py similarity index 100% rename from me/core/app_cocoa.py rename to core_me/app_cocoa.py diff --git a/me/core/data.py b/core_me/data.py similarity index 100% rename from me/core/data.py rename to core_me/data.py diff --git a/me/core/fs.py b/core_me/fs.py similarity index 100% rename from me/core/fs.py rename to core_me/fs.py diff --git a/me/core/scanner.py b/core_me/scanner.py similarity index 100% rename from me/core/scanner.py rename to core_me/scanner.py diff --git a/me/core/__init__.py b/core_me/tests/__init__.py similarity index 100% rename from me/core/__init__.py rename to core_me/tests/__init__.py diff --git a/me/core/tests/scanner_test.py b/core_me/tests/scanner_test.py similarity index 100% rename from me/core/tests/scanner_test.py rename to core_me/tests/scanner_test.py diff --git a/base/qt/LICENSE b/core_pe/LICENSE similarity index 100% rename from base/qt/LICENSE rename to core_pe/LICENSE diff --git a/me/core/tests/__init__.py b/core_pe/__init__.py similarity index 100% rename from me/core/tests/__init__.py rename to core_pe/__init__.py diff --git a/pe/core/app_cocoa.py b/core_pe/app_cocoa.py similarity index 100% rename from pe/core/app_cocoa.py rename to core_pe/app_cocoa.py diff --git a/pe/core/block.py b/core_pe/block.py similarity index 100% rename from pe/core/block.py rename to core_pe/block.py diff --git a/pe/core/cache.py b/core_pe/cache.py similarity index 100% rename from pe/core/cache.py rename to core_pe/cache.py diff --git a/pe/core/data.py b/core_pe/data.py similarity index 100% rename from pe/core/data.py rename to core_pe/data.py diff --git a/pe/core/gen.py b/core_pe/gen.py similarity index 100% rename from pe/core/gen.py rename to core_pe/gen.py diff --git a/pe/core/matchbase.py b/core_pe/matchbase.py similarity index 100% rename from pe/core/matchbase.py rename to core_pe/matchbase.py diff --git a/pe/core/modules/block/block.pyx b/core_pe/modules/block/block.pyx similarity index 100% rename from pe/core/modules/block/block.pyx rename to core_pe/modules/block/block.pyx diff --git a/pe/core/modules/block/setup.py b/core_pe/modules/block/setup.py similarity index 100% rename from pe/core/modules/block/setup.py rename to core_pe/modules/block/setup.py diff --git a/pe/core/modules/cache/cache.pyx b/core_pe/modules/cache/cache.pyx similarity index 100% rename from pe/core/modules/cache/cache.pyx rename to core_pe/modules/cache/cache.pyx diff --git a/pe/core/modules/cache/setup.py b/core_pe/modules/cache/setup.py similarity index 100% rename from pe/core/modules/cache/setup.py rename to core_pe/modules/cache/setup.py diff --git a/pe/core/scanner.py b/core_pe/scanner.py similarity index 100% rename from pe/core/scanner.py rename to core_pe/scanner.py diff --git a/me/help/__init__.py b/core_pe/tests/__init__.py similarity index 100% rename from me/help/__init__.py rename to core_pe/tests/__init__.py diff --git a/pe/core/tests/block_test.py b/core_pe/tests/block_test.py similarity index 100% rename from pe/core/tests/block_test.py rename to core_pe/tests/block_test.py diff --git a/pe/core/tests/cache_test.py b/core_pe/tests/cache_test.py similarity index 100% rename from pe/core/tests/cache_test.py rename to core_pe/tests/cache_test.py diff --git a/me/cocoa/LICENSE b/core_se/LICENSE similarity index 100% rename from me/cocoa/LICENSE rename to core_se/LICENSE diff --git a/se/core/__init__.py b/core_se/__init__.py similarity index 100% rename from se/core/__init__.py rename to core_se/__init__.py diff --git a/se/core/app_cocoa.py b/core_se/app_cocoa.py similarity index 100% rename from se/core/app_cocoa.py rename to core_se/app_cocoa.py diff --git a/se/core/data.py b/core_se/data.py similarity index 100% rename from se/core/data.py rename to core_se/data.py diff --git a/se/core/fs.py b/core_se/fs.py similarity index 100% rename from se/core/fs.py rename to core_se/fs.py diff --git a/pe/core/__init__.py b/core_se/tests/__init__.py similarity index 100% rename from pe/core/__init__.py rename to core_se/tests/__init__.py diff --git a/se/core/tests/fs_test.py b/core_se/tests/fs_test.py similarity index 100% rename from se/core/tests/fs_test.py rename to core_se/tests/fs_test.py diff --git a/me/help/LICENSE b/help_me/LICENSE similarity index 100% rename from me/help/LICENSE rename to help_me/LICENSE diff --git a/pe/core/tests/__init__.py b/help_me/__init__.py similarity index 100% rename from pe/core/tests/__init__.py rename to help_me/__init__.py diff --git a/me/help/changelog.yaml b/help_me/changelog.yaml similarity index 100% rename from me/help/changelog.yaml rename to help_me/changelog.yaml diff --git a/me/help/gen.py b/help_me/gen.py similarity index 83% rename from me/help/gen.py rename to help_me/gen.py index d3b4da24..2c83a6e4 100644 --- a/me/help/gen.py +++ b/help_me/gen.py @@ -8,7 +8,7 @@ import os.path as op from hsdocgen import generate_help, filters -def generate(windows=False): +def generate(windows=False, force_render=True): tix = filters.tixgen("https://hardcoded.lighthouseapp.com/projects/31699-dupeguru/tickets/{0}") basepath = op.dirname(__file__) - generate_help.main(basepath, op.join(basepath, 'dupeguru_me_help'), force_render=True, tix=tix, windows=windows) + generate_help.main(basepath, op.join(basepath, 'dupeguru_me_help'), force_render=force_render, tix=tix, windows=windows) diff --git a/me/help/skeleton/hardcoded.css b/help_me/skeleton/hardcoded.css similarity index 100% rename from me/help/skeleton/hardcoded.css rename to help_me/skeleton/hardcoded.css diff --git a/me/help/skeleton/images/hs_title.png b/help_me/skeleton/images/hs_title.png similarity index 100% rename from me/help/skeleton/images/hs_title.png rename to help_me/skeleton/images/hs_title.png diff --git a/me/help/templates/base_dg.mako b/help_me/templates/base_dg.mako similarity index 100% rename from me/help/templates/base_dg.mako rename to help_me/templates/base_dg.mako diff --git a/me/help/templates/credits.mako b/help_me/templates/credits.mako similarity index 100% rename from me/help/templates/credits.mako rename to help_me/templates/credits.mako diff --git a/me/help/templates/directories.mako b/help_me/templates/directories.mako similarity index 100% rename from me/help/templates/directories.mako rename to help_me/templates/directories.mako diff --git a/me/help/templates/faq.mako b/help_me/templates/faq.mako similarity index 100% rename from me/help/templates/faq.mako rename to help_me/templates/faq.mako diff --git a/me/help/templates/intro.mako b/help_me/templates/intro.mako similarity index 100% rename from me/help/templates/intro.mako rename to help_me/templates/intro.mako diff --git a/me/help/templates/power_marker.mako b/help_me/templates/power_marker.mako similarity index 100% rename from me/help/templates/power_marker.mako rename to help_me/templates/power_marker.mako diff --git a/me/help/templates/preferences.mako b/help_me/templates/preferences.mako similarity index 100% rename from me/help/templates/preferences.mako rename to help_me/templates/preferences.mako diff --git a/me/help/templates/quick_start.mako b/help_me/templates/quick_start.mako similarity index 100% rename from me/help/templates/quick_start.mako rename to help_me/templates/quick_start.mako diff --git a/me/help/templates/results.mako b/help_me/templates/results.mako similarity index 100% rename from me/help/templates/results.mako rename to help_me/templates/results.mako diff --git a/me/help/templates/versions.mako b/help_me/templates/versions.mako similarity index 100% rename from me/help/templates/versions.mako rename to help_me/templates/versions.mako diff --git a/me/qt/LICENSE b/help_pe/LICENSE similarity index 100% rename from me/qt/LICENSE rename to help_pe/LICENSE diff --git a/pe/help/__init__.py b/help_pe/__init__.py similarity index 100% rename from pe/help/__init__.py rename to help_pe/__init__.py diff --git a/pe/help/changelog.yaml b/help_pe/changelog.yaml similarity index 100% rename from pe/help/changelog.yaml rename to help_pe/changelog.yaml diff --git a/pe/help/gen.py b/help_pe/gen.py similarity index 83% rename from pe/help/gen.py rename to help_pe/gen.py index f5d78df2..7f01fe19 100644 --- a/pe/help/gen.py +++ b/help_pe/gen.py @@ -8,7 +8,7 @@ import os.path as op from hsdocgen import generate_help, filters -def generate(windows=False): +def generate(windows=False, force_render=True): tix = filters.tixgen("https://hardcoded.lighthouseapp.com/projects/31699-dupeguru/tickets/{0}") basepath = op.dirname(__file__) - generate_help.main(basepath, op.join(basepath, 'dupeguru_pe_help'), force_render=True, tix=tix, windows=windows) + generate_help.main(basepath, op.join(basepath, 'dupeguru_pe_help'), force_render=force_render, tix=tix, windows=windows) diff --git a/pe/help/skeleton/hardcoded.css b/help_pe/skeleton/hardcoded.css similarity index 100% rename from pe/help/skeleton/hardcoded.css rename to help_pe/skeleton/hardcoded.css diff --git a/pe/help/skeleton/images/hs_title.png b/help_pe/skeleton/images/hs_title.png similarity index 100% rename from pe/help/skeleton/images/hs_title.png rename to help_pe/skeleton/images/hs_title.png diff --git a/pe/help/templates/base_dg.mako b/help_pe/templates/base_dg.mako similarity index 100% rename from pe/help/templates/base_dg.mako rename to help_pe/templates/base_dg.mako diff --git a/pe/help/templates/credits.mako b/help_pe/templates/credits.mako similarity index 100% rename from pe/help/templates/credits.mako rename to help_pe/templates/credits.mako diff --git a/pe/help/templates/directories.mako b/help_pe/templates/directories.mako similarity index 100% rename from pe/help/templates/directories.mako rename to help_pe/templates/directories.mako diff --git a/pe/help/templates/faq.mako b/help_pe/templates/faq.mako similarity index 100% rename from pe/help/templates/faq.mako rename to help_pe/templates/faq.mako diff --git a/pe/help/templates/intro.mako b/help_pe/templates/intro.mako similarity index 100% rename from pe/help/templates/intro.mako rename to help_pe/templates/intro.mako diff --git a/pe/help/templates/power_marker.mako b/help_pe/templates/power_marker.mako similarity index 100% rename from pe/help/templates/power_marker.mako rename to help_pe/templates/power_marker.mako diff --git a/pe/help/templates/preferences.mako b/help_pe/templates/preferences.mako similarity index 100% rename from pe/help/templates/preferences.mako rename to help_pe/templates/preferences.mako diff --git a/pe/help/templates/quick_start.mako b/help_pe/templates/quick_start.mako similarity index 100% rename from pe/help/templates/quick_start.mako rename to help_pe/templates/quick_start.mako diff --git a/pe/help/templates/results.mako b/help_pe/templates/results.mako similarity index 100% rename from pe/help/templates/results.mako rename to help_pe/templates/results.mako diff --git a/pe/help/templates/versions.mako b/help_pe/templates/versions.mako similarity index 100% rename from pe/help/templates/versions.mako rename to help_pe/templates/versions.mako diff --git a/pe/cocoa/LICENSE b/help_se/LICENSE similarity index 100% rename from pe/cocoa/LICENSE rename to help_se/LICENSE diff --git a/se/core/tests/__init__.py b/help_se/__init__.py similarity index 100% rename from se/core/tests/__init__.py rename to help_se/__init__.py diff --git a/se/help/changelog.yaml b/help_se/changelog.yaml similarity index 100% rename from se/help/changelog.yaml rename to help_se/changelog.yaml diff --git a/se/help/gen.py b/help_se/gen.py similarity index 51% rename from se/help/gen.py rename to help_se/gen.py index 375aaee8..eb02c162 100644 --- a/se/help/gen.py +++ b/help_se/gen.py @@ -5,10 +5,11 @@ # which should be included with this package. The terms are also available at # http://www.hardcoded.net/licenses/hs_license -import os - +import os.path as op from hsdocgen import generate_help, filters -tix = filters.tixgen("https://hardcoded.lighthouseapp.com/projects/31699-dupeguru/tickets/{0}") +def generate(windows=False, force_render=True): + tix = filters.tixgen("https://hardcoded.lighthouseapp.com/projects/31699-dupeguru/tickets/{0}") + basepath = op.dirname(__file__) + generate_help.main(basepath, op.join(basepath, 'dupeguru_help'), force_render=force_render, tix=tix, windows=windows) -generate_help.main('.', 'dupeguru_help', force_render=True, tix=tix) diff --git a/se/help/skeleton/hardcoded.css b/help_se/skeleton/hardcoded.css similarity index 100% rename from se/help/skeleton/hardcoded.css rename to help_se/skeleton/hardcoded.css diff --git a/se/help/skeleton/images/hs_title.png b/help_se/skeleton/images/hs_title.png similarity index 100% rename from se/help/skeleton/images/hs_title.png rename to help_se/skeleton/images/hs_title.png diff --git a/se/help/templates/base_dg.mako b/help_se/templates/base_dg.mako similarity index 100% rename from se/help/templates/base_dg.mako rename to help_se/templates/base_dg.mako diff --git a/se/help/templates/credits.mako b/help_se/templates/credits.mako similarity index 100% rename from se/help/templates/credits.mako rename to help_se/templates/credits.mako diff --git a/se/help/templates/directories.mako b/help_se/templates/directories.mako similarity index 100% rename from se/help/templates/directories.mako rename to help_se/templates/directories.mako diff --git a/se/help/templates/faq.mako b/help_se/templates/faq.mako similarity index 100% rename from se/help/templates/faq.mako rename to help_se/templates/faq.mako diff --git a/se/help/templates/intro.mako b/help_se/templates/intro.mako similarity index 100% rename from se/help/templates/intro.mako rename to help_se/templates/intro.mako diff --git a/se/help/templates/power_marker.mako b/help_se/templates/power_marker.mako similarity index 100% rename from se/help/templates/power_marker.mako rename to help_se/templates/power_marker.mako diff --git a/se/help/templates/preferences.mako b/help_se/templates/preferences.mako similarity index 100% rename from se/help/templates/preferences.mako rename to help_se/templates/preferences.mako diff --git a/se/help/templates/quick_start.mako b/help_se/templates/quick_start.mako similarity index 100% rename from se/help/templates/quick_start.mako rename to help_se/templates/quick_start.mako diff --git a/se/help/templates/results.mako b/help_se/templates/results.mako similarity index 100% rename from se/help/templates/results.mako rename to help_se/templates/results.mako diff --git a/se/help/templates/versions.mako b/help_se/templates/versions.mako similarity index 100% rename from se/help/templates/versions.mako rename to help_se/templates/versions.mako diff --git a/me/cocoa/gen.py b/me/cocoa/gen.py deleted file mode 100644 index ea248ae7..00000000 --- a/me/cocoa/gen.py +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env python -# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "HS" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at -# http://www.hardcoded.net/licenses/hs_license - -import sys -sys.path.insert(0, 'py') # for hsutil and hsdocgen -import os - -from help import gen - -print "Generating help" -gen.generate() -os.system('/Developer/Applications/Utilities/Help\\ Indexer.app/Contents/MacOS/Help\\ Indexer help/dupeguru_me_help') - -print "Generating py plugin" -os.chdir('py') -os.system('python -u gen.py') -os.chdir('..') \ No newline at end of file diff --git a/me/cocoa/py/gen.py b/me/cocoa/py/gen.py deleted file mode 100644 index f8bc07c4..00000000 --- a/me/cocoa/py/gen.py +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env python -# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "HS" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at -# http://www.hardcoded.net/licenses/hs_license - -import os -import os.path as op -import shutil - -from hsutil.build import print_and_do - -if op.exists('build'): - shutil.rmtree('build') -if op.exists('dist'): - shutil.rmtree('dist') - -os.environ['MACOSX_DEPLOYMENT_TARGET'] = '10.5' -print_and_do('python -u setup.py py2app') \ No newline at end of file diff --git a/me/qt/WARNING b/me/qt/WARNING deleted file mode 100644 index 729666cc..00000000 --- a/me/qt/WARNING +++ /dev/null @@ -1,11 +0,0 @@ -WARNING ABOUT THE HS LICENSE AND PyQt - -Although Qt is now LGPL licensed, PyQt still is dual licensed. Until Nokia buys Riverbank and -releases PyQt as LGPL, users of this part of the code (The PyQt-based GUI code) have to use the -GPL version of PyQt, unless they possess a commercial license to it. - -There is no problem to this AS LONG AS YOU DON'T REDISTRIBUTE HS LICENSED CODE. The GPL license, from the point of view of the user, is very permissive. You can do WHATEVER you want with the GPLed version of PyQt, as long as you don't redistribute any of the code, or code dependent on it. When you do, the code you distribute has to be GPL compliant. The HS license is NOT, I repeat, NOT compliant with the GPL. - -So, what does it all mean? You have no restriction on the usage of the PyQt-dependent-HS-licensed code, but unless you possess a commercial PyQt license, Hardcoded Software (or anyone) cannot accept any contribution from you for this part of the code. - -Note that this only affects the PyQt dependent code, and not any other part of HS licensed code (if it has "import PyQt4" in it, it's PyQt dependent code). For the rest of the code, the only restrictions that apply are the ones from the HS license. \ No newline at end of file diff --git a/package.py b/package.py new file mode 100644 index 00000000..5e4042ad --- /dev/null +++ b/package.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +# Created By: Virgil Dupras +# Created On: 2009-12-30 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license + +import sys +import os +import os.path as op + +import yaml + +from hsutil.build import build_dmg, add_to_pythonpath + +def main(): + conf = yaml.load(open('conf.yaml')) + edition = conf['edition'] + ui = conf['ui'] + dev = conf['dev'] + if dev: + print "You can't package in dev mode" + return + print "Packaging dupeGuru {0} with UI {1}".format(edition.upper(), ui) + if ui == 'cocoa': + app_path = { + 'se': 'cocoa/se/build/Release/dupeGuru.app', + 'me': 'cocoa/me/build/Release/dupeGuru ME.app', + 'pe': 'cocoa/pe/build/Release/dupeGuru PE.app', + }[edition] + build_dmg(app_path, '.') + elif ui == 'qt': + if sys.platform != "win32": + print "Qt packaging only works under Windows." + return + add_to_pythonpath('.') + os.chdir('qt') + os.system('python build.py') + os.chdir('..') + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/pe/cocoa/gen.py b/pe/cocoa/gen.py deleted file mode 100644 index 60cabb4e..00000000 --- a/pe/cocoa/gen.py +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env python -# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "HS" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at -# http://www.hardcoded.net/licenses/hs_license - -import sys -sys.path.insert(0, 'py') # for hsutil and hsdocgen -import os - -from help import gen - -print "Generating help" -gen.generate() -os.system('/Developer/Applications/Utilities/Help\\ Indexer.app/Contents/MacOS/Help\\ Indexer help/dupeguru_pe_help') - -print "Generating py plugin" -os.chdir('py') -os.system('python -u gen.py') -os.chdir('..') \ No newline at end of file diff --git a/pe/cocoa/py/gen.py b/pe/cocoa/py/gen.py deleted file mode 100644 index 4b9f73d6..00000000 --- a/pe/cocoa/py/gen.py +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env python -# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "HS" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at -# http://www.hardcoded.net/licenses/hs_license - -import os -import os.path as op -import shutil - -from hsutil.build import print_and_do - -os.chdir('core_pe') -print_and_do('python gen.py') -os.chdir('..') - -if op.exists('build'): - shutil.rmtree('build') -if op.exists('dist'): - shutil.rmtree('dist') - -print_and_do('python -u setup.py py2app') \ No newline at end of file diff --git a/pe/core/LICENSE b/pe/core/LICENSE deleted file mode 100644 index f8818048..00000000 --- a/pe/core/LICENSE +++ /dev/null @@ -1,11 +0,0 @@ -Copyright 2009 Hardcoded Software Inc. (http://www.hardcoded.net) -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - * Neither the name of Hardcoded Software Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. - * If the source code has been published less than two years ago, any redistribution, in whole or in part, must retain full licensing functionality, without any attempt to change, obscure or in other ways circumvent its intent. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/pe/help/LICENSE b/pe/help/LICENSE deleted file mode 100644 index f8818048..00000000 --- a/pe/help/LICENSE +++ /dev/null @@ -1,11 +0,0 @@ -Copyright 2009 Hardcoded Software Inc. (http://www.hardcoded.net) -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - * Neither the name of Hardcoded Software Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. - * If the source code has been published less than two years ago, any redistribution, in whole or in part, must retain full licensing functionality, without any attempt to change, obscure or in other ways circumvent its intent. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/pe/qt/LICENSE b/pe/qt/LICENSE deleted file mode 100644 index f8818048..00000000 --- a/pe/qt/LICENSE +++ /dev/null @@ -1,11 +0,0 @@ -Copyright 2009 Hardcoded Software Inc. (http://www.hardcoded.net) -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - * Neither the name of Hardcoded Software Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. - * If the source code has been published less than two years ago, any redistribution, in whole or in part, must retain full licensing functionality, without any attempt to change, obscure or in other ways circumvent its intent. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/pe/qt/WARNING b/pe/qt/WARNING deleted file mode 100644 index 729666cc..00000000 --- a/pe/qt/WARNING +++ /dev/null @@ -1,11 +0,0 @@ -WARNING ABOUT THE HS LICENSE AND PyQt - -Although Qt is now LGPL licensed, PyQt still is dual licensed. Until Nokia buys Riverbank and -releases PyQt as LGPL, users of this part of the code (The PyQt-based GUI code) have to use the -GPL version of PyQt, unless they possess a commercial license to it. - -There is no problem to this AS LONG AS YOU DON'T REDISTRIBUTE HS LICENSED CODE. The GPL license, from the point of view of the user, is very permissive. You can do WHATEVER you want with the GPLed version of PyQt, as long as you don't redistribute any of the code, or code dependent on it. When you do, the code you distribute has to be GPL compliant. The HS license is NOT, I repeat, NOT compliant with the GPL. - -So, what does it all mean? You have no restriction on the usage of the PyQt-dependent-HS-licensed code, but unless you possess a commercial PyQt license, Hardcoded Software (or anyone) cannot accept any contribution from you for this part of the code. - -Note that this only affects the PyQt dependent code, and not any other part of HS licensed code (if it has "import PyQt4" in it, it's PyQt dependent code). For the rest of the code, the only restrictions that apply are the ones from the HS license. \ No newline at end of file diff --git a/base/qt/WARNING b/qt/WARNING similarity index 100% rename from base/qt/WARNING rename to qt/WARNING diff --git a/qt/base/__init__.py b/qt/base/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/base/qt/app.py b/qt/base/app.py similarity index 100% rename from base/qt/app.py rename to qt/base/app.py diff --git a/base/qt/details_table.py b/qt/base/details_table.py similarity index 100% rename from base/qt/details_table.py rename to qt/base/details_table.py diff --git a/qt/base/dg.qrc b/qt/base/dg.qrc new file mode 100644 index 00000000..5c689656 --- /dev/null +++ b/qt/base/dg.qrc @@ -0,0 +1,16 @@ + + + ../../images/details32.png + ../../images/dgpe_logo_32.png + ../../images/dgpe_logo_128.png + ../../images/dgme_logo_32.png + ../../images/dgme_logo_128.png + ../../images/dgse_logo_32.png + ../../images/dgse_logo_128.png + ../../images/folderwin32.png + ../../images/preferences32.png + ../../images/actions32.png + ../../images/delta32.png + ../../images/power_marker32.png + + \ No newline at end of file diff --git a/base/qt/directories_dialog.py b/qt/base/directories_dialog.py similarity index 100% rename from base/qt/directories_dialog.py rename to qt/base/directories_dialog.py diff --git a/base/qt/directories_dialog.ui b/qt/base/directories_dialog.ui similarity index 100% rename from base/qt/directories_dialog.ui rename to qt/base/directories_dialog.ui diff --git a/base/qt/directories_model.py b/qt/base/directories_model.py similarity index 100% rename from base/qt/directories_model.py rename to qt/base/directories_model.py diff --git a/base/qt/main_window.py b/qt/base/main_window.py similarity index 100% rename from base/qt/main_window.py rename to qt/base/main_window.py diff --git a/base/qt/main_window.ui b/qt/base/main_window.ui similarity index 100% rename from base/qt/main_window.ui rename to qt/base/main_window.ui diff --git a/base/qt/platform.py b/qt/base/platform.py similarity index 100% rename from base/qt/platform.py rename to qt/base/platform.py diff --git a/base/qt/platform_osx.py b/qt/base/platform_osx.py similarity index 100% rename from base/qt/platform_osx.py rename to qt/base/platform_osx.py diff --git a/base/qt/platform_win.py b/qt/base/platform_win.py similarity index 100% rename from base/qt/platform_win.py rename to qt/base/platform_win.py diff --git a/base/qt/preferences.py b/qt/base/preferences.py similarity index 100% rename from base/qt/preferences.py rename to qt/base/preferences.py diff --git a/base/qt/results_model.py b/qt/base/results_model.py similarity index 100% rename from base/qt/results_model.py rename to qt/base/results_model.py diff --git a/me/qt/app.py b/qt/me/app.py similarity index 100% rename from me/qt/app.py rename to qt/me/app.py diff --git a/me/qt/build.py b/qt/me/build.py similarity index 100% rename from me/qt/build.py rename to qt/me/build.py diff --git a/me/qt/details_dialog.py b/qt/me/details_dialog.py similarity index 100% rename from me/qt/details_dialog.py rename to qt/me/details_dialog.py diff --git a/me/qt/details_dialog.ui b/qt/me/details_dialog.ui similarity index 100% rename from me/qt/details_dialog.ui rename to qt/me/details_dialog.ui diff --git a/me/qt/dgme.spec b/qt/me/dgme.spec similarity index 78% rename from me/qt/dgme.spec rename to qt/me/dgme.spec index 4d47e350..ab510aaa 100644 --- a/me/qt/dgme.spec +++ b/qt/me/dgme.spec @@ -1,6 +1,6 @@ # -*- mode: python -*- a = Analysis([os.path.join(HOMEPATH,'support\\_mountzlib.py'), os.path.join(HOMEPATH,'support\\useUnicode.py'), 'start.py'], - pathex=['C:\\src\\dupeguru\\me\\qt']) + pathex=[]) pyz = PYZ(a.pure) exe = EXE(pyz, a.scripts, @@ -9,7 +9,7 @@ exe = EXE(pyz, debug=False, strip=False, upx=True, - console=False , icon='base\\images\\dgme_logo.ico', version='verinfo_tmp') + console=False , icon='..\\base\\images\\dgme_logo.ico', version='verinfo_tmp') coll = COLLECT( exe, a.binaries, a.zipfiles, diff --git a/me/qt/gen.py b/qt/me/gen.py similarity index 70% rename from me/qt/gen.py rename to qt/me/gen.py index f71e4f81..f77c4af8 100644 --- a/me/qt/gen.py +++ b/qt/me/gen.py @@ -13,13 +13,7 @@ import os.path as op from hsutil.build import print_and_do, build_all_qt_ui -from help import gen - -build_all_qt_ui(op.join('qtlib', 'ui')) -build_all_qt_ui('base') +build_all_qt_ui(op.join('..', '..', 'qtlib', 'ui')) +build_all_qt_ui(op.join('..', 'base')) build_all_qt_ui('.') -os.chdir('base') -print_and_do("pyrcc4 dg.qrc > dg_rc.py") -os.chdir('..') - -gen.generate(windows=True) +print_and_do("pyrcc4 {0} > {1}".format(op.join('..', 'base', 'dg.qrc'), op.join('..', 'base', 'dg_rc.py'))) \ No newline at end of file diff --git a/me/qt/installer.aip b/qt/me/installer.aip similarity index 100% rename from me/qt/installer.aip rename to qt/me/installer.aip diff --git a/me/qt/preferences.py b/qt/me/preferences.py similarity index 100% rename from me/qt/preferences.py rename to qt/me/preferences.py diff --git a/me/qt/preferences_dialog.py b/qt/me/preferences_dialog.py similarity index 100% rename from me/qt/preferences_dialog.py rename to qt/me/preferences_dialog.py diff --git a/me/qt/preferences_dialog.ui b/qt/me/preferences_dialog.ui similarity index 100% rename from me/qt/preferences_dialog.ui rename to qt/me/preferences_dialog.ui diff --git a/me/qt/profile.py b/qt/me/profile.py similarity index 100% rename from me/qt/profile.py rename to qt/me/profile.py diff --git a/me/qt/start.py b/qt/me/start.py similarity index 100% rename from me/qt/start.py rename to qt/me/start.py diff --git a/me/qt/verinfo b/qt/me/verinfo similarity index 100% rename from me/qt/verinfo rename to qt/me/verinfo diff --git a/pe/qt/app.py b/qt/pe/app.py similarity index 100% rename from pe/qt/app.py rename to qt/pe/app.py diff --git a/pe/qt/block.py b/qt/pe/block.py similarity index 100% rename from pe/qt/block.py rename to qt/pe/block.py diff --git a/pe/qt/build.py b/qt/pe/build.py similarity index 100% rename from pe/qt/build.py rename to qt/pe/build.py diff --git a/pe/qt/details_dialog.py b/qt/pe/details_dialog.py similarity index 100% rename from pe/qt/details_dialog.py rename to qt/pe/details_dialog.py diff --git a/pe/qt/details_dialog.ui b/qt/pe/details_dialog.ui similarity index 100% rename from pe/qt/details_dialog.ui rename to qt/pe/details_dialog.ui diff --git a/pe/qt/dgpe.spec b/qt/pe/dgpe.spec similarity index 84% rename from pe/qt/dgpe.spec rename to qt/pe/dgpe.spec index 673d9039..55405804 100644 --- a/pe/qt/dgpe.spec +++ b/qt/pe/dgpe.spec @@ -9,7 +9,7 @@ exe = EXE(pyz, debug=False, strip=False, upx=True, - console=False , icon='base\\images\\dgpe_logo.ico', version='verinfo_tmp') + console=False , icon='..\\base\\images\\dgpe_logo.ico', version='verinfo_tmp') coll = COLLECT( exe, a.binaries, a.zipfiles, diff --git a/pe/qt/gen.py b/qt/pe/gen.py similarity index 77% rename from pe/qt/gen.py rename to qt/pe/gen.py index 83c4875a..12ec8463 100644 --- a/pe/qt/gen.py +++ b/qt/pe/gen.py @@ -13,14 +13,10 @@ import os.path as op from hsutil.build import print_and_do, build_all_qt_ui -from help import gen - -build_all_qt_ui(op.join('qtlib', 'ui')) -build_all_qt_ui('base') +build_all_qt_ui(op.join('..', '..', 'qtlib', 'ui')) +build_all_qt_ui(op.join('..', 'base')) build_all_qt_ui('.') -os.chdir('base') -print_and_do("pyrcc4 dg.qrc > dg_rc.py") -os.chdir('..') +print_and_do("pyrcc4 {0} > {1}".format(op.join('..', 'base', 'dg.qrc'), op.join('..', 'base', 'dg_rc.py'))) def move(src, dst): if not op.exists(src): @@ -30,18 +26,10 @@ def move(src, dst): print 'Moving %s --> %s' % (src, dst) os.rename(src, dst) -os.chdir('core_pe') -print_and_do('python gen.py') -os.chdir('..') - # The CC=gcc-4.0 thing is because, in Snow Leopard, gcc-4.2 can't compile these units. os.environ['CC'] = 'gcc-4.0' os.chdir(op.join('modules', 'block')) os.system('python setup.py build_ext --inplace') os.chdir(op.join('..', '..')) move(op.join('modules', 'block', '_block.so'), op.join('.', '_block.so')) -move(op.join('modules', 'block', '_block.pyd'), op.join('.', '_block.pyd')) - -os.chdir('help') -gen.generate(windows=True) -os.chdir('..') \ No newline at end of file +move(op.join('modules', 'block', '_block.pyd'), op.join('.', '_block.pyd')) \ No newline at end of file diff --git a/pe/qt/installer.aip b/qt/pe/installer.aip similarity index 100% rename from pe/qt/installer.aip rename to qt/pe/installer.aip diff --git a/pe/qt/main_window.py b/qt/pe/main_window.py similarity index 100% rename from pe/qt/main_window.py rename to qt/pe/main_window.py diff --git a/pe/qt/modules/block/block.pyx b/qt/pe/modules/block/block.pyx similarity index 100% rename from pe/qt/modules/block/block.pyx rename to qt/pe/modules/block/block.pyx diff --git a/pe/qt/modules/block/setup.py b/qt/pe/modules/block/setup.py similarity index 100% rename from pe/qt/modules/block/setup.py rename to qt/pe/modules/block/setup.py diff --git a/pe/qt/preferences.py b/qt/pe/preferences.py similarity index 100% rename from pe/qt/preferences.py rename to qt/pe/preferences.py diff --git a/pe/qt/preferences_dialog.py b/qt/pe/preferences_dialog.py similarity index 100% rename from pe/qt/preferences_dialog.py rename to qt/pe/preferences_dialog.py diff --git a/pe/qt/preferences_dialog.ui b/qt/pe/preferences_dialog.ui similarity index 100% rename from pe/qt/preferences_dialog.ui rename to qt/pe/preferences_dialog.ui diff --git a/pe/qt/profile.py b/qt/pe/profile.py similarity index 100% rename from pe/qt/profile.py rename to qt/pe/profile.py diff --git a/pe/qt/start.py b/qt/pe/start.py similarity index 100% rename from pe/qt/start.py rename to qt/pe/start.py diff --git a/pe/qt/verinfo b/qt/pe/verinfo similarity index 100% rename from pe/qt/verinfo rename to qt/pe/verinfo diff --git a/se/qt/app.py b/qt/se/app.py similarity index 100% rename from se/qt/app.py rename to qt/se/app.py diff --git a/se/qt/build.py b/qt/se/build.py similarity index 100% rename from se/qt/build.py rename to qt/se/build.py diff --git a/se/qt/details_dialog.py b/qt/se/details_dialog.py similarity index 100% rename from se/qt/details_dialog.py rename to qt/se/details_dialog.py diff --git a/se/qt/details_dialog.ui b/qt/se/details_dialog.ui similarity index 100% rename from se/qt/details_dialog.ui rename to qt/se/details_dialog.ui diff --git a/se/qt/dgse.spec b/qt/se/dgse.spec similarity index 84% rename from se/qt/dgse.spec rename to qt/se/dgse.spec index 124cf273..5b2d3470 100644 --- a/se/qt/dgse.spec +++ b/qt/se/dgse.spec @@ -1,6 +1,6 @@ # -*- mode: python -*- a = Analysis([os.path.join(HOMEPATH,'support\\_mountzlib.py'), os.path.join(HOMEPATH,'support\\useUnicode.py'), 'start.py'], - pathex=['C:\\src\\dupeguru\\se']) + pathex=[]) pyz = PYZ(a.pure) exe = EXE(pyz, a.scripts, @@ -10,7 +10,7 @@ exe = EXE(pyz, strip=False, upx=True, console=False, - icon='base\\images\\dgse_logo.ico', + icon='..\\base\\images\\dgse_logo.ico', version='verinfo_tmp') coll = COLLECT( exe, a.binaries, diff --git a/se/qt/gen.py b/qt/se/gen.py similarity index 67% rename from se/qt/gen.py rename to qt/se/gen.py index 88b8e4a9..d36494a4 100644 --- a/se/qt/gen.py +++ b/qt/se/gen.py @@ -13,11 +13,7 @@ import os.path as op from hsutil.build import print_and_do, build_all_qt_ui -build_all_qt_ui(op.join('qtlib', 'ui')) -build_all_qt_ui('base') +build_all_qt_ui(op.join('..', '..', 'qtlib', 'ui')) +build_all_qt_ui(op.join('..', 'base')) build_all_qt_ui('.') -print_and_do("pyrcc4 {0} > {1}".format(op.join('base', 'dg.qrc'), op.join('base', 'dg_rc.py'))) - -os.chdir('help') -print_and_do('python gen.py') -os.chdir('..') +print_and_do("pyrcc4 {0} > {1}".format(op.join('..', 'base', 'dg.qrc'), op.join('..', 'base', 'dg_rc.py'))) diff --git a/se/qt/installer.aip b/qt/se/installer.aip similarity index 100% rename from se/qt/installer.aip rename to qt/se/installer.aip diff --git a/se/qt/preferences.py b/qt/se/preferences.py similarity index 100% rename from se/qt/preferences.py rename to qt/se/preferences.py diff --git a/se/qt/preferences_dialog.py b/qt/se/preferences_dialog.py similarity index 100% rename from se/qt/preferences_dialog.py rename to qt/se/preferences_dialog.py diff --git a/se/qt/preferences_dialog.ui b/qt/se/preferences_dialog.ui similarity index 100% rename from se/qt/preferences_dialog.ui rename to qt/se/preferences_dialog.ui diff --git a/se/qt/profile.py b/qt/se/profile.py similarity index 100% rename from se/qt/profile.py rename to qt/se/profile.py diff --git a/se/qt/start.py b/qt/se/start.py similarity index 100% rename from se/qt/start.py rename to qt/se/start.py diff --git a/se/qt/verinfo b/qt/se/verinfo similarity index 100% rename from se/qt/verinfo rename to qt/se/verinfo diff --git a/run.py b/run.py new file mode 100644 index 00000000..232cddff --- /dev/null +++ b/run.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# Created By: Virgil Dupras +# Created On: 2009-12-30 +# $Id$ +# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "HS" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/hs_license + +import sys +import os +import os.path as op + +import yaml + +from hsutil.build import add_to_pythonpath + +def main(): + conf = yaml.load(open('conf.yaml')) + edition = conf['edition'] + ui = conf['ui'] + print "Running dupeGuru {0} with UI {1}".format(edition.upper(), ui) + if ui == 'cocoa': + app_path = { + 'se': 'cocoa/se/build/Release/dupeGuru.app', + 'me': 'cocoa/me/build/Release/dupeGuru\\ ME.app', + 'pe': 'cocoa/pe/build/Release/dupeGuru\\ PE.app', + }[edition] + os.system('open {0}'.format(app_path)) + elif ui == 'qt': + add_to_pythonpath('.') + add_to_pythonpath('qt') + os.chdir(op.join('qt', edition)) + os.system('python start.py') + os.chdir('..') + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/se/cocoa/LICENSE b/se/cocoa/LICENSE deleted file mode 100644 index f8818048..00000000 --- a/se/cocoa/LICENSE +++ /dev/null @@ -1,11 +0,0 @@ -Copyright 2009 Hardcoded Software Inc. (http://www.hardcoded.net) -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - * Neither the name of Hardcoded Software Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. - * If the source code has been published less than two years ago, any redistribution, in whole or in part, must retain full licensing functionality, without any attempt to change, obscure or in other ways circumvent its intent. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/se/cocoa/gen.py b/se/cocoa/gen.py deleted file mode 100644 index 7f6bc18e..00000000 --- a/se/cocoa/gen.py +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env python -# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "HS" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at -# http://www.hardcoded.net/licenses/hs_license - -import os - -print "Generating help" -os.chdir('help') -os.system('python -u gen.py') -os.system('/Developer/Applications/Utilities/Help\\ Indexer.app/Contents/MacOS/Help\\ Indexer dupeguru_help') -os.chdir('..') - -print "Generating py plugin" -os.chdir('py') -os.system('python -u gen.py') -os.chdir('..') \ No newline at end of file diff --git a/se/cocoa/py/gen.py b/se/cocoa/py/gen.py deleted file mode 100644 index f8bc07c4..00000000 --- a/se/cocoa/py/gen.py +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env python -# Copyright 2009 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "HS" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at -# http://www.hardcoded.net/licenses/hs_license - -import os -import os.path as op -import shutil - -from hsutil.build import print_and_do - -if op.exists('build'): - shutil.rmtree('build') -if op.exists('dist'): - shutil.rmtree('dist') - -os.environ['MACOSX_DEPLOYMENT_TARGET'] = '10.5' -print_and_do('python -u setup.py py2app') \ No newline at end of file diff --git a/se/core/LICENSE b/se/core/LICENSE deleted file mode 100644 index f8818048..00000000 --- a/se/core/LICENSE +++ /dev/null @@ -1,11 +0,0 @@ -Copyright 2009 Hardcoded Software Inc. (http://www.hardcoded.net) -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - * Neither the name of Hardcoded Software Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. - * If the source code has been published less than two years ago, any redistribution, in whole or in part, must retain full licensing functionality, without any attempt to change, obscure or in other ways circumvent its intent. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/se/help/LICENSE b/se/help/LICENSE deleted file mode 100644 index f8818048..00000000 --- a/se/help/LICENSE +++ /dev/null @@ -1,11 +0,0 @@ -Copyright 2009 Hardcoded Software Inc. (http://www.hardcoded.net) -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - * Neither the name of Hardcoded Software Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. - * If the source code has been published less than two years ago, any redistribution, in whole or in part, must retain full licensing functionality, without any attempt to change, obscure or in other ways circumvent its intent. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/se/qt/LICENSE b/se/qt/LICENSE deleted file mode 100644 index f8818048..00000000 --- a/se/qt/LICENSE +++ /dev/null @@ -1,11 +0,0 @@ -Copyright 2009 Hardcoded Software Inc. (http://www.hardcoded.net) -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - * Neither the name of Hardcoded Software Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. - * If the source code has been published less than two years ago, any redistribution, in whole or in part, must retain full licensing functionality, without any attempt to change, obscure or in other ways circumvent its intent. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/se/qt/WARNING b/se/qt/WARNING deleted file mode 100644 index 729666cc..00000000 --- a/se/qt/WARNING +++ /dev/null @@ -1,11 +0,0 @@ -WARNING ABOUT THE HS LICENSE AND PyQt - -Although Qt is now LGPL licensed, PyQt still is dual licensed. Until Nokia buys Riverbank and -releases PyQt as LGPL, users of this part of the code (The PyQt-based GUI code) have to use the -GPL version of PyQt, unless they possess a commercial license to it. - -There is no problem to this AS LONG AS YOU DON'T REDISTRIBUTE HS LICENSED CODE. The GPL license, from the point of view of the user, is very permissive. You can do WHATEVER you want with the GPLed version of PyQt, as long as you don't redistribute any of the code, or code dependent on it. When you do, the code you distribute has to be GPL compliant. The HS license is NOT, I repeat, NOT compliant with the GPL. - -So, what does it all mean? You have no restriction on the usage of the PyQt-dependent-HS-licensed code, but unless you possess a commercial PyQt license, Hardcoded Software (or anyone) cannot accept any contribution from you for this part of the code. - -Note that this only affects the PyQt dependent code, and not any other part of HS licensed code (if it has "import PyQt4" in it, it's PyQt dependent code). For the rest of the code, the only restrictions that apply are the ones from the HS license. \ No newline at end of file From 70af8541da0e0bc9c47575735c9e4bacff06289f Mon Sep 17 00:00:00 2001 From: hsoft Date: Wed, 30 Dec 2009 16:52:46 +0000 Subject: [PATCH 275/275] Fixed packaging, which didn't work on windows. --HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40286 --- package.py | 5 +++-- qt/me/dgme.spec | 2 +- qt/pe/dgpe.spec | 2 +- qt/se/dgse.spec | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/package.py b/package.py index 5e4042ad..cd01c0e2 100644 --- a/package.py +++ b/package.py @@ -37,9 +37,10 @@ def main(): print "Qt packaging only works under Windows." return add_to_pythonpath('.') - os.chdir('qt') + add_to_pythonpath('qt') + os.chdir(op.join('qt', edition)) os.system('python build.py') - os.chdir('..') + os.chdir(op.join('..', '..')) if __name__ == '__main__': main() \ No newline at end of file diff --git a/qt/me/dgme.spec b/qt/me/dgme.spec index ab510aaa..0e21463b 100644 --- a/qt/me/dgme.spec +++ b/qt/me/dgme.spec @@ -9,7 +9,7 @@ exe = EXE(pyz, debug=False, strip=False, upx=True, - console=False , icon='..\\base\\images\\dgme_logo.ico', version='verinfo_tmp') + console=False , icon='..\\..\\images\\dgme_logo.ico', version='verinfo_tmp') coll = COLLECT( exe, a.binaries, a.zipfiles, diff --git a/qt/pe/dgpe.spec b/qt/pe/dgpe.spec index 55405804..ab00fde5 100644 --- a/qt/pe/dgpe.spec +++ b/qt/pe/dgpe.spec @@ -9,7 +9,7 @@ exe = EXE(pyz, debug=False, strip=False, upx=True, - console=False , icon='..\\base\\images\\dgpe_logo.ico', version='verinfo_tmp') + console=False , icon='..\\..\\images\\dgpe_logo.ico', version='verinfo_tmp') coll = COLLECT( exe, a.binaries, a.zipfiles, diff --git a/qt/se/dgse.spec b/qt/se/dgse.spec index 5b2d3470..7810d2e3 100644 --- a/qt/se/dgse.spec +++ b/qt/se/dgse.spec @@ -10,7 +10,7 @@ exe = EXE(pyz, strip=False, upx=True, console=False, - icon='..\\base\\images\\dgse_logo.ico', + icon='..\\..\\images\\dgse_logo.ico', version='verinfo_tmp') coll = COLLECT( exe, a.binaries,