From bb1f0f5be60b7461eb8788fdd573a99731e9fb9e Mon Sep 17 00:00:00 2001 From: Virgil Dupras Date: Wed, 25 May 2016 21:07:30 -0400 Subject: [PATCH] Convert hscommon, qtlib and cocoalib to submodules ... rather than subtrees. That also represents a small qtlib updates which needed a code adjustment. --- .gitmodules | 9 + README.md | 2 +- cocoalib | 1 + cocoalib/.gitignore | 15 - cocoalib/.tx/config | 8 - cocoalib/Dialogs.h | 14 - cocoalib/Dialogs.m | 31 - cocoalib/HSAboutBox.h | 27 - cocoalib/HSAboutBox.m | 42 -- cocoalib/HSErrorReportWindow.h | 26 - cocoalib/HSErrorReportWindow.m | 44 -- cocoalib/HSGeometry.h | 15 - cocoalib/HSGeometry.m | 71 --- cocoalib/HSPyUtil.h | 13 - cocoalib/HSPyUtil.m | 34 -- cocoalib/HSQuicklook.h | 18 - cocoalib/HSQuicklook.m | 36 -- cocoalib/HSRecentFiles.h | 35 -- cocoalib/HSRecentFiles.m | 89 --- cocoalib/LICENSE | 10 - cocoalib/NSEventAdditions.h | 24 - cocoalib/NSEventAdditions.m | 85 --- cocoalib/NSImageAdditions.h | 21 - cocoalib/NSImageAdditions.m | 114 ---- cocoalib/NSNotificationAdditions.h | 10 - cocoalib/NSNotificationAdditions.m | 48 -- cocoalib/ProgressController.h | 50 -- cocoalib/ProgressController.m | 160 ------ cocoalib/Sparkle.framework/Headers | 1 - cocoalib/Sparkle.framework/Resources | 1 - cocoalib/Sparkle.framework/Sparkle | 1 - .../Versions/A/Headers/SUAppcast.h | 33 -- .../Versions/A/Headers/SUAppcastItem.h | 47 -- .../Versions/A/Headers/SUUpdater.h | 118 ---- .../A/Headers/SUVersionComparisonProtocol.h | 27 - .../Versions/A/Headers/Sparkle.h | 21 - .../Versions/A/Resources/Info.plist | 24 - .../Versions/A/Resources/License.txt | 7 - .../A/Resources/SUModelTranslation.plist | 174 ------ .../A/Resources/SUStatus.nib/classes.nib | 56 -- .../A/Resources/SUStatus.nib/info.nib | 20 - .../A/Resources/SUStatus.nib/keyedobjects.nib | Bin 7344 -> 0 bytes .../SUAutomaticUpdateAlert.nib/classes.nib | 50 -- .../SUAutomaticUpdateAlert.nib/info.nib | 20 - .../keyedobjects.nib | Bin 7148 -> 0 bytes .../en.lproj/SUUpdateAlert.nib/classes.nib | 67 --- .../en.lproj/SUUpdateAlert.nib/info.nib | 20 - .../SUUpdateAlert.nib/keyedobjects.nib | Bin 10623 -> 0 bytes .../SUUpdatePermissionPrompt.nib/classes.nib | 59 -- .../SUUpdatePermissionPrompt.nib/info.nib | 21 - .../keyedobjects.nib | Bin 13263 -> 0 bytes .../A/Resources/en.lproj/Sparkle.strings | Bin 8216 -> 0 bytes .../Versions/A/Resources/relaunch | Bin 58924 -> 0 bytes cocoalib/Sparkle.framework/Versions/A/Sparkle | Bin 463540 -> 0 bytes cocoalib/Sparkle.framework/Versions/Current | 1 - cocoalib/Utils.h | 36 -- cocoalib/Utils.m | 92 --- cocoalib/ValueTransformers.h | 26 - cocoalib/ValueTransformers.m | 79 --- cocoalib/Worker.h | 14 - cocoalib/cocoa/CocoaProxy.h | 34 -- cocoalib/cocoa/CocoaProxy.m | 171 ------ cocoalib/cocoa/__init__.py | 118 ---- cocoalib/cocoa/inter.py | 300 ---------- cocoalib/controllers/HSColumns.h | 38 -- cocoalib/controllers/HSColumns.m | 198 ------- cocoalib/controllers/HSComboBox.h | 25 - cocoalib/controllers/HSComboBox.m | 119 ---- cocoalib/controllers/HSGUIController.h | 23 - cocoalib/controllers/HSGUIController.m | 62 -- cocoalib/controllers/HSOutline.h | 44 -- cocoalib/controllers/HSOutline.m | 286 --------- cocoalib/controllers/HSPopUpList.h | 23 - cocoalib/controllers/HSPopUpList.m | 60 -- cocoalib/controllers/HSProgressWindow.h | 30 - cocoalib/controllers/HSProgressWindow.m | 79 --- cocoalib/controllers/HSSelectableList.h | 24 - cocoalib/controllers/HSSelectableList.m | 107 ---- cocoalib/controllers/HSTable.h | 32 -- cocoalib/controllers/HSTable.m | 136 ----- cocoalib/controllers/HSTextField.h | 21 - cocoalib/controllers/HSTextField.m | 53 -- cocoalib/en.lproj/cocoalib.strings | 15 - cocoalib/locale/cocoalib.pot | 62 -- cocoalib/locale/cs/LC_MESSAGES/cocoalib.po | 70 --- cocoalib/locale/de/LC_MESSAGES/cocoalib.po | 70 --- cocoalib/locale/es/LC_MESSAGES/cocoalib.po | 70 --- cocoalib/locale/fr/LC_MESSAGES/cocoalib.po | 70 --- cocoalib/locale/hy/LC_MESSAGES/cocoalib.po | 70 --- cocoalib/locale/it/LC_MESSAGES/cocoalib.po | 70 --- cocoalib/locale/ko/LC_MESSAGES/cocoalib.po | 70 --- cocoalib/locale/nl/LC_MESSAGES/cocoalib.po | 70 --- cocoalib/locale/pl_PL/LC_MESSAGES/cocoalib.po | 71 --- cocoalib/locale/pt_BR/LC_MESSAGES/cocoalib.po | 74 --- cocoalib/locale/ru/LC_MESSAGES/cocoalib.po | 74 --- cocoalib/locale/uk/LC_MESSAGES/cocoalib.po | 70 --- cocoalib/locale/vi/LC_MESSAGES/cocoalib.po | 70 --- cocoalib/locale/zh_CN/LC_MESSAGES/cocoalib.po | 70 --- cocoalib/ui/about.py | 36 -- cocoalib/ui/error_report.py | 41 -- cocoalib/ui/progress.py | 31 - cocoalib/views/HSOutlineView.h | 41 -- cocoalib/views/HSOutlineView.m | 190 ------ cocoalib/views/HSTableView.h | 31 - cocoalib/views/HSTableView.m | 102 ---- cocoalib/views/NSIndexPathAdditions.h | 20 - cocoalib/views/NSIndexPathAdditions.m | 16 - cocoalib/views/NSTableViewAdditions.h | 24 - cocoalib/views/NSTableViewAdditions.m | 118 ---- hscommon | 1 + hscommon/.gitignore | 5 - hscommon/LICENSE | 10 - hscommon/README | 3 - hscommon/__init__.py | 0 hscommon/build.py | 486 ---------------- hscommon/conflict.py | 79 --- hscommon/currency.py | 533 ----------------- hscommon/debug.py | 22 - hscommon/desktop.py | 93 --- hscommon/geometry.py | 218 ------- hscommon/gui/__init__.py | 0 hscommon/gui/base.py | 74 --- hscommon/gui/column.py | 289 ---------- hscommon/gui/progress_window.py | 122 ---- hscommon/gui/selectable_list.py | 208 ------- hscommon/gui/table.py | 543 ------------------ hscommon/gui/text_field.py | 108 ---- hscommon/gui/tree.py | 251 -------- hscommon/jobprogress/__init__.py | 0 hscommon/jobprogress/job.py | 166 ------ hscommon/jobprogress/performer.py | 72 --- hscommon/jobprogress/qt.py | 52 -- hscommon/loc.py | 197 ------- hscommon/notify.py | 89 --- hscommon/path.py | 243 -------- hscommon/plat.py | 16 - hscommon/pygettext.py | 417 -------------- hscommon/sphinxgen.py | 73 --- hscommon/sqlite.py | 141 ----- hscommon/tests/__init__.py | 0 hscommon/tests/conflict_test.py | 104 ---- hscommon/tests/currency_test.py | 210 ------- hscommon/tests/notify_test.py | 140 ----- hscommon/tests/path_test.py | 256 --------- hscommon/tests/selectable_list_test.py | 65 --- hscommon/tests/sqlite_test.py | 126 ---- hscommon/tests/table_test.py | 313 ---------- hscommon/tests/tree_test.py | 109 ---- hscommon/tests/util_test.py | 325 ----------- hscommon/testutil.py | 212 ------- hscommon/trans.py | 162 ------ hscommon/util.py | 407 ------------- qt/base/preferences_dialog.py | 7 +- qtlib | 1 + qtlib/.gitignore | 4 - qtlib/.tx/config | 8 - qtlib/LICENSE | 10 - qtlib/__init__.py | 0 qtlib/about_box.py | 76 --- qtlib/app.py | 19 - qtlib/column.py | 86 --- qtlib/error_report_dialog.py | 84 --- qtlib/images/search_clear_13.png | Bin 398 -> 0 bytes qtlib/locale/cs/LC_MESSAGES/qtlib.po | 106 ---- qtlib/locale/de/LC_MESSAGES/qtlib.po | 106 ---- qtlib/locale/es/LC_MESSAGES/qtlib.po | 106 ---- qtlib/locale/fr/LC_MESSAGES/qtlib.po | 106 ---- qtlib/locale/hy/LC_MESSAGES/qtlib.po | 106 ---- qtlib/locale/it/LC_MESSAGES/qtlib.po | 106 ---- qtlib/locale/ko/LC_MESSAGES/qtlib.po | 106 ---- qtlib/locale/nl/LC_MESSAGES/qtlib.po | 106 ---- qtlib/locale/pl_PL/LC_MESSAGES/qtlib.po | 107 ---- qtlib/locale/pt_BR/LC_MESSAGES/qtlib.po | 110 ---- qtlib/locale/qtlib.pot | 101 ---- qtlib/locale/ru/LC_MESSAGES/qtlib.po | 110 ---- qtlib/locale/uk/LC_MESSAGES/qtlib.po | 106 ---- qtlib/locale/vi/LC_MESSAGES/qtlib.po | 106 ---- qtlib/locale/zh_CN/LC_MESSAGES/qtlib.po | 106 ---- qtlib/preferences.py | 129 ----- qtlib/progress_window.py | 48 -- qtlib/radio_box.py | 88 --- qtlib/recent.py | 95 --- qtlib/search_edit.py | 98 ---- qtlib/selectable_list.py | 98 ---- qtlib/table.py | 152 ----- qtlib/text_field.py | 23 - qtlib/tree_model.py | 173 ------ qtlib/util.py | 104 ---- 188 files changed, 17 insertions(+), 15586 deletions(-) create mode 100644 .gitmodules create mode 160000 cocoalib delete mode 100644 cocoalib/.gitignore delete mode 100644 cocoalib/.tx/config delete mode 100644 cocoalib/Dialogs.h delete mode 100644 cocoalib/Dialogs.m delete mode 100644 cocoalib/HSAboutBox.h delete mode 100644 cocoalib/HSAboutBox.m delete mode 100644 cocoalib/HSErrorReportWindow.h delete mode 100644 cocoalib/HSErrorReportWindow.m delete mode 100644 cocoalib/HSGeometry.h delete mode 100644 cocoalib/HSGeometry.m delete mode 100644 cocoalib/HSPyUtil.h delete mode 100644 cocoalib/HSPyUtil.m delete mode 100644 cocoalib/HSQuicklook.h delete mode 100644 cocoalib/HSQuicklook.m delete mode 100644 cocoalib/HSRecentFiles.h delete mode 100644 cocoalib/HSRecentFiles.m delete mode 100644 cocoalib/LICENSE delete mode 100644 cocoalib/NSEventAdditions.h delete mode 100644 cocoalib/NSEventAdditions.m delete mode 100644 cocoalib/NSImageAdditions.h delete mode 100644 cocoalib/NSImageAdditions.m delete mode 100644 cocoalib/NSNotificationAdditions.h delete mode 100644 cocoalib/NSNotificationAdditions.m delete mode 100644 cocoalib/ProgressController.h delete mode 100644 cocoalib/ProgressController.m delete mode 120000 cocoalib/Sparkle.framework/Headers delete mode 120000 cocoalib/Sparkle.framework/Resources delete mode 120000 cocoalib/Sparkle.framework/Sparkle delete mode 100644 cocoalib/Sparkle.framework/Versions/A/Headers/SUAppcast.h delete mode 100644 cocoalib/Sparkle.framework/Versions/A/Headers/SUAppcastItem.h delete mode 100644 cocoalib/Sparkle.framework/Versions/A/Headers/SUUpdater.h delete mode 100644 cocoalib/Sparkle.framework/Versions/A/Headers/SUVersionComparisonProtocol.h delete mode 100644 cocoalib/Sparkle.framework/Versions/A/Headers/Sparkle.h delete mode 100644 cocoalib/Sparkle.framework/Versions/A/Resources/Info.plist delete mode 100644 cocoalib/Sparkle.framework/Versions/A/Resources/License.txt delete mode 100644 cocoalib/Sparkle.framework/Versions/A/Resources/SUModelTranslation.plist delete mode 100644 cocoalib/Sparkle.framework/Versions/A/Resources/SUStatus.nib/classes.nib delete mode 100644 cocoalib/Sparkle.framework/Versions/A/Resources/SUStatus.nib/info.nib delete mode 100644 cocoalib/Sparkle.framework/Versions/A/Resources/SUStatus.nib/keyedobjects.nib delete mode 100644 cocoalib/Sparkle.framework/Versions/A/Resources/en.lproj/SUAutomaticUpdateAlert.nib/classes.nib delete mode 100644 cocoalib/Sparkle.framework/Versions/A/Resources/en.lproj/SUAutomaticUpdateAlert.nib/info.nib delete mode 100644 cocoalib/Sparkle.framework/Versions/A/Resources/en.lproj/SUAutomaticUpdateAlert.nib/keyedobjects.nib delete mode 100644 cocoalib/Sparkle.framework/Versions/A/Resources/en.lproj/SUUpdateAlert.nib/classes.nib delete mode 100644 cocoalib/Sparkle.framework/Versions/A/Resources/en.lproj/SUUpdateAlert.nib/info.nib delete mode 100644 cocoalib/Sparkle.framework/Versions/A/Resources/en.lproj/SUUpdateAlert.nib/keyedobjects.nib delete mode 100644 cocoalib/Sparkle.framework/Versions/A/Resources/en.lproj/SUUpdatePermissionPrompt.nib/classes.nib delete mode 100644 cocoalib/Sparkle.framework/Versions/A/Resources/en.lproj/SUUpdatePermissionPrompt.nib/info.nib delete mode 100644 cocoalib/Sparkle.framework/Versions/A/Resources/en.lproj/SUUpdatePermissionPrompt.nib/keyedobjects.nib delete mode 100644 cocoalib/Sparkle.framework/Versions/A/Resources/en.lproj/Sparkle.strings delete mode 100755 cocoalib/Sparkle.framework/Versions/A/Resources/relaunch delete mode 100755 cocoalib/Sparkle.framework/Versions/A/Sparkle delete mode 120000 cocoalib/Sparkle.framework/Versions/Current delete mode 100644 cocoalib/Utils.h delete mode 100644 cocoalib/Utils.m delete mode 100644 cocoalib/ValueTransformers.h delete mode 100644 cocoalib/ValueTransformers.m delete mode 100644 cocoalib/Worker.h delete mode 100644 cocoalib/cocoa/CocoaProxy.h delete mode 100644 cocoalib/cocoa/CocoaProxy.m delete mode 100644 cocoalib/cocoa/__init__.py delete mode 100644 cocoalib/cocoa/inter.py delete mode 100644 cocoalib/controllers/HSColumns.h delete mode 100644 cocoalib/controllers/HSColumns.m delete mode 100644 cocoalib/controllers/HSComboBox.h delete mode 100644 cocoalib/controllers/HSComboBox.m delete mode 100644 cocoalib/controllers/HSGUIController.h delete mode 100644 cocoalib/controllers/HSGUIController.m delete mode 100644 cocoalib/controllers/HSOutline.h delete mode 100644 cocoalib/controllers/HSOutline.m delete mode 100644 cocoalib/controllers/HSPopUpList.h delete mode 100644 cocoalib/controllers/HSPopUpList.m delete mode 100644 cocoalib/controllers/HSProgressWindow.h delete mode 100644 cocoalib/controllers/HSProgressWindow.m delete mode 100644 cocoalib/controllers/HSSelectableList.h delete mode 100644 cocoalib/controllers/HSSelectableList.m delete mode 100644 cocoalib/controllers/HSTable.h delete mode 100644 cocoalib/controllers/HSTable.m delete mode 100644 cocoalib/controllers/HSTextField.h delete mode 100644 cocoalib/controllers/HSTextField.m delete mode 100644 cocoalib/en.lproj/cocoalib.strings delete mode 100644 cocoalib/locale/cocoalib.pot delete mode 100644 cocoalib/locale/cs/LC_MESSAGES/cocoalib.po delete mode 100644 cocoalib/locale/de/LC_MESSAGES/cocoalib.po delete mode 100755 cocoalib/locale/es/LC_MESSAGES/cocoalib.po delete mode 100644 cocoalib/locale/fr/LC_MESSAGES/cocoalib.po delete mode 100755 cocoalib/locale/hy/LC_MESSAGES/cocoalib.po delete mode 100644 cocoalib/locale/it/LC_MESSAGES/cocoalib.po delete mode 100644 cocoalib/locale/ko/LC_MESSAGES/cocoalib.po delete mode 100644 cocoalib/locale/nl/LC_MESSAGES/cocoalib.po delete mode 100644 cocoalib/locale/pl_PL/LC_MESSAGES/cocoalib.po delete mode 100644 cocoalib/locale/pt_BR/LC_MESSAGES/cocoalib.po delete mode 100755 cocoalib/locale/ru/LC_MESSAGES/cocoalib.po delete mode 100755 cocoalib/locale/uk/LC_MESSAGES/cocoalib.po delete mode 100644 cocoalib/locale/vi/LC_MESSAGES/cocoalib.po delete mode 100644 cocoalib/locale/zh_CN/LC_MESSAGES/cocoalib.po delete mode 100644 cocoalib/ui/about.py delete mode 100644 cocoalib/ui/error_report.py delete mode 100644 cocoalib/ui/progress.py delete mode 100644 cocoalib/views/HSOutlineView.h delete mode 100644 cocoalib/views/HSOutlineView.m delete mode 100644 cocoalib/views/HSTableView.h delete mode 100644 cocoalib/views/HSTableView.m delete mode 100644 cocoalib/views/NSIndexPathAdditions.h delete mode 100644 cocoalib/views/NSIndexPathAdditions.m delete mode 100644 cocoalib/views/NSTableViewAdditions.h delete mode 100644 cocoalib/views/NSTableViewAdditions.m create mode 160000 hscommon delete mode 100644 hscommon/.gitignore delete mode 100644 hscommon/LICENSE delete mode 100644 hscommon/README delete mode 100755 hscommon/__init__.py delete mode 100644 hscommon/build.py delete mode 100644 hscommon/conflict.py delete mode 100644 hscommon/currency.py delete mode 100644 hscommon/debug.py delete mode 100644 hscommon/desktop.py delete mode 100644 hscommon/geometry.py delete mode 100644 hscommon/gui/__init__.py delete mode 100644 hscommon/gui/base.py delete mode 100644 hscommon/gui/column.py delete mode 100644 hscommon/gui/progress_window.py delete mode 100644 hscommon/gui/selectable_list.py delete mode 100644 hscommon/gui/table.py delete mode 100644 hscommon/gui/text_field.py delete mode 100644 hscommon/gui/tree.py delete mode 100644 hscommon/jobprogress/__init__.py delete mode 100644 hscommon/jobprogress/job.py delete mode 100644 hscommon/jobprogress/performer.py delete mode 100644 hscommon/jobprogress/qt.py delete mode 100644 hscommon/loc.py delete mode 100644 hscommon/notify.py delete mode 100644 hscommon/path.py delete mode 100644 hscommon/plat.py delete mode 100755 hscommon/pygettext.py delete mode 100644 hscommon/sphinxgen.py delete mode 100644 hscommon/sqlite.py delete mode 100644 hscommon/tests/__init__.py delete mode 100644 hscommon/tests/conflict_test.py delete mode 100644 hscommon/tests/currency_test.py delete mode 100644 hscommon/tests/notify_test.py delete mode 100644 hscommon/tests/path_test.py delete mode 100644 hscommon/tests/selectable_list_test.py delete mode 100644 hscommon/tests/sqlite_test.py delete mode 100644 hscommon/tests/table_test.py delete mode 100644 hscommon/tests/tree_test.py delete mode 100644 hscommon/tests/util_test.py delete mode 100644 hscommon/testutil.py delete mode 100644 hscommon/trans.py delete mode 100644 hscommon/util.py create mode 160000 qtlib delete mode 100644 qtlib/.gitignore delete mode 100644 qtlib/.tx/config delete mode 100644 qtlib/LICENSE delete mode 100644 qtlib/__init__.py delete mode 100644 qtlib/about_box.py delete mode 100644 qtlib/app.py delete mode 100644 qtlib/column.py delete mode 100644 qtlib/error_report_dialog.py delete mode 100644 qtlib/images/search_clear_13.png delete mode 100644 qtlib/locale/cs/LC_MESSAGES/qtlib.po delete mode 100644 qtlib/locale/de/LC_MESSAGES/qtlib.po delete mode 100644 qtlib/locale/es/LC_MESSAGES/qtlib.po delete mode 100644 qtlib/locale/fr/LC_MESSAGES/qtlib.po delete mode 100755 qtlib/locale/hy/LC_MESSAGES/qtlib.po delete mode 100644 qtlib/locale/it/LC_MESSAGES/qtlib.po delete mode 100644 qtlib/locale/ko/LC_MESSAGES/qtlib.po delete mode 100644 qtlib/locale/nl/LC_MESSAGES/qtlib.po delete mode 100644 qtlib/locale/pl_PL/LC_MESSAGES/qtlib.po delete mode 100644 qtlib/locale/pt_BR/LC_MESSAGES/qtlib.po delete mode 100644 qtlib/locale/qtlib.pot delete mode 100644 qtlib/locale/ru/LC_MESSAGES/qtlib.po delete mode 100755 qtlib/locale/uk/LC_MESSAGES/qtlib.po delete mode 100644 qtlib/locale/vi/LC_MESSAGES/qtlib.po delete mode 100644 qtlib/locale/zh_CN/LC_MESSAGES/qtlib.po delete mode 100644 qtlib/preferences.py delete mode 100644 qtlib/progress_window.py delete mode 100644 qtlib/radio_box.py delete mode 100644 qtlib/recent.py delete mode 100644 qtlib/search_edit.py delete mode 100644 qtlib/selectable_list.py delete mode 100644 qtlib/table.py delete mode 100644 qtlib/text_field.py delete mode 100644 qtlib/tree_model.py delete mode 100644 qtlib/util.py diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..5a80a95c --- /dev/null +++ b/.gitmodules @@ -0,0 +1,9 @@ +[submodule "qtlib"] + path = qtlib + url = https://github.com/hsoft/qtlib.git +[submodule "hscommon"] + path = hscommon + url = https://github.com/hsoft/hscommon.git +[submodule "cocoalib"] + path = cocoalib + url = https://github.com/hsoft/cocoalib.git diff --git a/README.md b/README.md index 9fde2e95..6008ca02 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ This folder contains the source for dupeGuru. Its documentation is in `help`, bu * locale: .po files for localisation. There are also other sub-folder that comes from external repositories and are part of this repo as -git subtrees: +git submodules: * hscommon: A collection of helpers used across HS applications. * cocoalib: A collection of helpers used across Cocoa UI codebases of HS applications. diff --git a/cocoalib b/cocoalib new file mode 160000 index 00000000..65ab3b5f --- /dev/null +++ b/cocoalib @@ -0,0 +1 @@ +Subproject commit 65ab3b5fb61b9a477786850d3a8083e3892d9020 diff --git a/cocoalib/.gitignore b/cocoalib/.gitignore deleted file mode 100644 index aef674d2..00000000 --- a/cocoalib/.gitignore +++ /dev/null @@ -1,15 +0,0 @@ -.DS_Store -__pycache__ -autogen -*.pyc -*.so -nl.lproj -cs.lproj -de.lproj -fr.lproj -it.lproj -hy.lproj -ru.lproj -uk.lproj -zh_CN.lproj -pt_BR.lproj diff --git a/cocoalib/.tx/config b/cocoalib/.tx/config deleted file mode 100644 index 9ffd2ec4..00000000 --- a/cocoalib/.tx/config +++ /dev/null @@ -1,8 +0,0 @@ -[main] -host = https://www.transifex.com - -[hscommon.cocoalib] -file_filter = locale//LC_MESSAGES/cocoalib.po -source_file = locale/cocoalib.pot -source_lang = en -type = PO diff --git a/cocoalib/Dialogs.h b/cocoalib/Dialogs.h deleted file mode 100644 index 5a55972d..00000000 --- a/cocoalib/Dialogs.h +++ /dev/null @@ -1,14 +0,0 @@ -/* -Copyright 2015 Hardcoded Software (http://www.hardcoded.net) - -This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -which should be included with this package. The terms are also available at -http://www.gnu.org/licenses/gpl-3.0.html -*/ - -#import - -@interface Dialogs : NSObject -+ (void)showMessage:(NSString *)message; -+ (NSInteger)askYesNo:(NSString *)message; -@end diff --git a/cocoalib/Dialogs.m b/cocoalib/Dialogs.m deleted file mode 100644 index 254d87c3..00000000 --- a/cocoalib/Dialogs.m +++ /dev/null @@ -1,31 +0,0 @@ -/* -Copyright 2015 Hardcoded Software (http://www.hardcoded.net) - -This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -which should be included with this package. The terms are also available at -http://www.gnu.org/licenses/gpl-3.0.html -*/ - -#import "Dialogs.h" - -@implementation Dialogs -+ (void)showMessage:(NSString *)message -{ - NSAlert *a = [[NSAlert alloc] init]; - [a addButtonWithTitle:NSLocalizedStringFromTable(@"OK", @"cocoalib", @"")]; - [a setMessageText:message]; - [a runModal]; - [a release]; -} - -+ (NSInteger)askYesNo:(NSString *)message -{ - NSAlert *a = [[NSAlert alloc] init]; - [a addButtonWithTitle:NSLocalizedStringFromTable(@"Yes", @"cocoalib", @"")]; - [[a addButtonWithTitle:NSLocalizedStringFromTable(@"No", @"cocoalib", @"")] setKeyEquivalent:@"\E"]; - [a setMessageText:message]; - NSInteger r = [a runModal]; - [a release]; - return r; -} -@end diff --git a/cocoalib/HSAboutBox.h b/cocoalib/HSAboutBox.h deleted file mode 100644 index 450e0bdc..00000000 --- a/cocoalib/HSAboutBox.h +++ /dev/null @@ -1,27 +0,0 @@ -/* -Copyright 2015 Hardcoded Software (http://www.hardcoded.net) - -This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -which should be included with this package. The terms are also available at -http://www.gnu.org/licenses/gpl-3.0.html -*/ - -#import -#import "PyBaseApp.h" - -@interface HSAboutBox : NSWindowController -{ - NSTextField *titleTextField; - NSTextField *versionTextField; - NSTextField *copyrightTextField; - - PyBaseApp *app; -} - -@property (readwrite, retain) NSTextField *titleTextField; -@property (readwrite, retain) NSTextField *versionTextField; -@property (readwrite, retain) NSTextField *copyrightTextField; - -- (id)initWithApp:(PyBaseApp *)app; -- (void)updateFields; -@end \ No newline at end of file diff --git a/cocoalib/HSAboutBox.m b/cocoalib/HSAboutBox.m deleted file mode 100644 index 65e7fd2d..00000000 --- a/cocoalib/HSAboutBox.m +++ /dev/null @@ -1,42 +0,0 @@ -/* -Copyright 2015 Hardcoded Software (http://www.hardcoded.net) - -This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -which should be included with this package. The terms are also available at -http://www.gnu.org/licenses/gpl-3.0.html -*/ - -#import "HSAboutBox.h" -#import "HSAboutBox_UI.h" - -@implementation HSAboutBox - -@synthesize titleTextField; -@synthesize versionTextField; -@synthesize copyrightTextField; - -- (id)initWithApp:(PyBaseApp *)aApp -{ - self = [super initWithWindow:nil]; - [self setWindow:createHSAboutBox_UI(self)]; - app = [aApp retain]; - [self updateFields]; - return self; -} - -- (void)dealloc -{ - [app release]; - [super dealloc]; -} - -- (void)updateFields -{ - [titleTextField setStringValue:[app appLongName]]; - NSString *version = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"]; - [versionTextField setStringValue:[NSString stringWithFormat:@"Version: %@",version]]; - NSString *copyright = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"NSHumanReadableCopyright"]; - [copyrightTextField setStringValue:copyright]; -} - -@end diff --git a/cocoalib/HSErrorReportWindow.h b/cocoalib/HSErrorReportWindow.h deleted file mode 100644 index 51a07219..00000000 --- a/cocoalib/HSErrorReportWindow.h +++ /dev/null @@ -1,26 +0,0 @@ -/* -Copyright 2015 Hardcoded Software (http://www.hardcoded.net) - -This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -which should be included with this package. The terms are also available at -http://www.gnu.org/licenses/gpl-3.0.html -*/ - -#import - -@interface HSErrorReportWindow : NSWindowController -{ - NSTextView *contentTextView; - NSString *githubUrl; -} - -@property (readwrite, retain) NSTextView *contentTextView; -@property (readwrite, retain) NSString *githubUrl; - -// True if the user wants to send the report -+ (void)showErrorReportWithContent:(NSString *)content githubUrl:(NSString *)githubUrl; -- (id)initWithContent:(NSString *)content githubUrl:(NSString *)githubUrl; - -- (void)goToGithub; -- (void)close; -@end \ No newline at end of file diff --git a/cocoalib/HSErrorReportWindow.m b/cocoalib/HSErrorReportWindow.m deleted file mode 100644 index 9fb0aff4..00000000 --- a/cocoalib/HSErrorReportWindow.m +++ /dev/null @@ -1,44 +0,0 @@ -/* -Copyright 2015 Hardcoded Software (http://www.hardcoded.net) - -This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -which should be included with this package. The terms are also available at -http://www.gnu.org/licenses/gpl-3.0.html -*/ - -#import "HSErrorReportWindow.h" -#import "HSErrorReportWindow_UI.h" - -@implementation HSErrorReportWindow - -@synthesize contentTextView; -@synthesize githubUrl; - -+ (void)showErrorReportWithContent:(NSString *)content githubUrl:(NSString *)githubUrl -{ - HSErrorReportWindow *report = [[HSErrorReportWindow alloc] initWithContent:content githubUrl:githubUrl]; - [NSApp runModalForWindow:[report window]]; - [report release]; -} - -- (id)initWithContent:(NSString *)content githubUrl:(NSString *)aGithubUrl -{ - self = [super initWithWindow:nil]; - [self setWindow:createHSErrorReportWindow_UI(self)]; - [contentTextView alignLeft:nil]; - [[[contentTextView textStorage] mutableString] setString:content]; - self.githubUrl = aGithubUrl; - return self; -} - -- (void)goToGithub -{ - [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:self.githubUrl]]; -} - -- (void)close -{ - [[self window] orderOut:self]; - [NSApp stopModalWithCode:NSOKButton]; -} -@end \ No newline at end of file diff --git a/cocoalib/HSGeometry.h b/cocoalib/HSGeometry.h deleted file mode 100644 index fd9a224b..00000000 --- a/cocoalib/HSGeometry.h +++ /dev/null @@ -1,15 +0,0 @@ -/* -Copyright 2015 Hardcoded Software (http://www.hardcoded.net) - -This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -which should be included with this package. The terms are also available at -http://www.gnu.org/licenses/gpl-3.0.html -*/ - -#import -#import - -CGFloat deg2rad(CGFloat deg); -CGFloat distance(NSPoint p1, NSPoint p2); -NSPoint pointInCircle(NSPoint center, CGFloat radius, CGFloat angle); -CGFloat angleFromPoints(NSPoint pt1, NSPoint pt2); \ No newline at end of file diff --git a/cocoalib/HSGeometry.m b/cocoalib/HSGeometry.m deleted file mode 100644 index 2aa9fdaf..00000000 --- a/cocoalib/HSGeometry.m +++ /dev/null @@ -1,71 +0,0 @@ -/* -Copyright 2015 Hardcoded Software (http://www.hardcoded.net) - -This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -which should be included with this package. The terms are also available at -http://www.gnu.org/licenses/gpl-3.0.html -*/ - -#import "HSGeometry.h" - -CGFloat deg2rad(CGFloat deg) -{ - return deg * M_PI / 180; -} - -CGFloat distance(NSPoint p1, NSPoint p2) -{ - CGFloat dX = p1.x - p2.x; - CGFloat dY = p1.y - p2.y; - return sqrt(dX * dX + dY * dY); -} - -NSPoint pointInCircle(NSPoint center, CGFloat radius, CGFloat angle) -{ - // a/sin(A) = b/sin(B) = c/sin(C) = 2R - // the start point it (center.x + radius, center.y) and goes counterclockwise - angle = fmod(angle, M_PI*2); - CGFloat C = M_PI/2; - CGFloat A = fmod(angle, M_PI/2); - CGFloat B = C - A; - CGFloat c = radius; - CGFloat ratio = c / sin(C); - CGFloat b = ratio * sin(B); - CGFloat a = ratio * sin(A); - if (angle >= M_PI * 1.5) - return NSMakePoint(center.x + a, center.y - b); - else if (angle >= M_PI) - return NSMakePoint(center.x - b, center.y - a); - else if (angle >= M_PI/2) - return NSMakePoint(center.x - a, center.y + b); - else - return NSMakePoint(center.x + b, center.y + a); -} - -CGFloat angleFromPoints(NSPoint pt1, NSPoint pt2) -{ - // Returns the angle (radian) formed by the line pt1-pt2. The angle follows the same logic - // as in pointInCircle. - // What we do here is that we take the line and reduce it to fit a "unit circle" (circle with - // a radius of 1). Then, either asin(adjusted_dy) or acos(adjusted_dx) will give us our angle. - // We'll use asin(adjusted_dy). - CGFloat length = distance(pt1, pt2); - CGFloat dx = pt2.x - pt1.x; - CGFloat dy = pt2.y - pt1.y; - CGFloat ajdusted_dy = ABS(dy) / length; - CGFloat angle = asin(ajdusted_dy); - - if ((dx < 0) && (dy >= 0)) { - // top-left quadrant - angle = M_PI - angle; - } - else if ((dx < 0) && (dy < 0)) { - // bottom-left quadrant - angle = M_PI + angle; - } - else if ((dx >= 0) && (dy < 0)) { - // bottom-right quadrant - angle = (2 * M_PI) - angle; - } - return angle; -} \ No newline at end of file diff --git a/cocoalib/HSPyUtil.h b/cocoalib/HSPyUtil.h deleted file mode 100644 index f4dc4225..00000000 --- a/cocoalib/HSPyUtil.h +++ /dev/null @@ -1,13 +0,0 @@ -/* -Copyright 2015 Hardcoded Software (http://www.hardcoded.net) - -This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -which should be included with this package. The terms are also available at -http://www.gnu.org/licenses/gpl-3.0.html -*/ - -#import -#import - -void setCocoaViewsModuleName(NSString *moduleName); -PyObject* createCallback(NSString *aViewClassName, id aViewRef); diff --git a/cocoalib/HSPyUtil.m b/cocoalib/HSPyUtil.m deleted file mode 100644 index ff3e4e31..00000000 --- a/cocoalib/HSPyUtil.m +++ /dev/null @@ -1,34 +0,0 @@ -/* -Copyright 2015 Hardcoded Software (http://www.hardcoded.net) - -This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -which should be included with this package. The terms are also available at -http://www.gnu.org/licenses/gpl-3.0.html -*/ - -#import "HSPyUtil.h" -#import "ObjP.h" - -static NSString *gCocoaViewsModuleName; -void setCocoaViewsModuleName(NSString *moduleName) -{ - if (gCocoaViewsModuleName != nil) { - [gCocoaViewsModuleName release]; - } - gCocoaViewsModuleName = [moduleName retain]; -} - -PyObject* createCallback(NSString *aViewClassName, id aViewRef) -{ - NSString *moduleName; - if (gCocoaViewsModuleName != nil) { - moduleName = gCocoaViewsModuleName; - } - else { - moduleName = @"inter.CocoaViews"; - } - PyGILState_STATE gilState = PyGILState_Ensure(); - PyObject *pCallback = ObjP_classInstanceWithRef(aViewClassName, moduleName, aViewRef); - PyGILState_Release(gilState); - return pCallback; -} diff --git a/cocoalib/HSQuicklook.h b/cocoalib/HSQuicklook.h deleted file mode 100644 index 60f4a910..00000000 --- a/cocoalib/HSQuicklook.h +++ /dev/null @@ -1,18 +0,0 @@ -/* -Copyright 2015 Hardcoded Software (http://www.hardcoded.net) - -This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -which should be included with this package. The terms are also available at -http://www.gnu.org/licenses/gpl-3.0.html -*/ - -#import -#import - -@interface HSQLPreviewItem : NSObject -{ - NSURL *url; - NSString *title; -} -- (id)initWithUrl:(NSURL *)aUrl title:(NSString *)aTitle; -@end \ No newline at end of file diff --git a/cocoalib/HSQuicklook.m b/cocoalib/HSQuicklook.m deleted file mode 100644 index d0975100..00000000 --- a/cocoalib/HSQuicklook.m +++ /dev/null @@ -1,36 +0,0 @@ -/* -Copyright 2015 Hardcoded Software (http://www.hardcoded.net) - -This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -which should be included with this package. The terms are also available at -http://www.gnu.org/licenses/gpl-3.0.html -*/ - -#import "HSQuicklook.h" - -@implementation HSQLPreviewItem -- (id)initWithUrl:(NSURL *)aUrl title:(NSString *)aTitle -{ - self = [super init]; - url = [aUrl retain]; - title = [aTitle retain]; - return self; -} - -- (void)dealloc -{ - [url release]; - [title release]; - [super dealloc]; -} - -- (NSURL *)previewItemURL -{ - return url; -} - -- (NSString *)previewItemTitle -{ - return title; -} -@end \ No newline at end of file diff --git a/cocoalib/HSRecentFiles.h b/cocoalib/HSRecentFiles.h deleted file mode 100644 index 0c214f25..00000000 --- a/cocoalib/HSRecentFiles.h +++ /dev/null @@ -1,35 +0,0 @@ -/* -Copyright 2015 Hardcoded Software (http://www.hardcoded.net) - -This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -which should be included with this package. The terms are also available at -http://www.gnu.org/licenses/gpl-3.0.html -*/ - -#import - -@interface HSRecentFiles : NSObject -{ - id delegate; - NSMenu *menu; - NSString *name; - NSMutableArray *filepaths; - NSInteger numberOfMenuItemsToPreserve; -} -- (id)initWithName:(NSString *)aName menu:(NSMenu *)aMenu; - -- (void)addFile:(NSString *)path; -- (void)rebuildMenu; -- (void)fillMenu:(NSMenu *)menu; -- (void)clearMenu:(id)sender; -- (void)menuClick:(id)sender; - -- (NSMenu *)menu; -- (id)delegate; -- (void)setDelegate:(id)aDelegate; -- (NSArray *)filepaths; -@end - -@protocol HSRecentFilesDelegate -- (void)recentFileClicked:(NSString *)path; -@end diff --git a/cocoalib/HSRecentFiles.m b/cocoalib/HSRecentFiles.m deleted file mode 100644 index 231ebe6d..00000000 --- a/cocoalib/HSRecentFiles.m +++ /dev/null @@ -1,89 +0,0 @@ -/* -Copyright 2015 Hardcoded Software (http://www.hardcoded.net) - -This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -which should be included with this package. The terms are also available at -http://www.gnu.org/licenses/gpl-3.0.html -*/ - -#import "HSRecentFiles.h" - -@implementation HSRecentFiles -- (id)initWithName:(NSString *)aName menu:(NSMenu *)aMenu -{ - self = [super init]; - name = aName; - menu = [aMenu retain]; - numberOfMenuItemsToPreserve = [menu numberOfItems]; - NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; - filepaths = [[NSMutableArray alloc] initWithArray:[ud arrayForKey:name]]; - NSFileManager *fm = [NSFileManager defaultManager]; - for (NSInteger i=[filepaths count]-1;i>=0;i--) { - NSString *path = [filepaths objectAtIndex:i]; - // We check for path class because we might be fed with garbage from the prefs. - if ((![path isKindOfClass:[NSString class]]) || (![fm fileExistsAtPath:path])) { - [filepaths removeObjectAtIndex:i]; - } - } - [self rebuildMenu]; - return self; -} - -- (void)dealloc -{ - NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; - [ud setObject:filepaths forKey:name]; - [ud synchronize]; - [filepaths release]; - [menu release]; - [super dealloc]; -} - -- (void)addFile:(NSString *)path -{ - [filepaths removeObject:path]; - [filepaths insertObject:path atIndex:0]; - [self rebuildMenu]; -} - -- (void)rebuildMenu -{ - while ([menu numberOfItems] > numberOfMenuItemsToPreserve) - [menu removeItemAtIndex:[menu numberOfItems]-1]; - [self fillMenu:menu]; - if ([filepaths count] > 0) { - [menu addItem:[NSMenuItem separatorItem]]; - NSMenuItem *mi = [menu addItemWithTitle:NSLocalizedStringFromTable(@"Clear List", @"cocoalib", @"") action:@selector(clearMenu:) keyEquivalent:@""]; - [mi setTarget:self]; - } -} - -- (void)fillMenu:(NSMenu *)menuToFill -{ - for (int i=0;i<[filepaths count];i++) { - NSMenuItem *mi = [menuToFill addItemWithTitle:[filepaths objectAtIndex:i] action:@selector(menuClick:) keyEquivalent:@""]; - [mi setTag:i]; - [mi setTarget:self]; - } -} - -- (void)clearMenu:(id)sender -{ - [filepaths removeAllObjects]; - [self rebuildMenu]; -} - -- (void)menuClick:(id)sender -{ - if (delegate == nil) - return; - if ([delegate respondsToSelector:@selector(recentFileClicked:)]) - [delegate recentFileClicked:[filepaths objectAtIndex:[sender tag]]]; -} - -/* Properties */ -- (NSMenu *)menu {return menu;} -- (id)delegate { return delegate; } -- (void)setDelegate:(id)aDelegate { delegate = aDelegate; } -- (NSArray *)filepaths {return filepaths;} -@end diff --git a/cocoalib/LICENSE b/cocoalib/LICENSE deleted file mode 100644 index 5a8d3ceb..00000000 --- a/cocoalib/LICENSE +++ /dev/null @@ -1,10 +0,0 @@ -Copyright 2014, 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. - -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/cocoalib/NSEventAdditions.h b/cocoalib/NSEventAdditions.h deleted file mode 100644 index e5661736..00000000 --- a/cocoalib/NSEventAdditions.h +++ /dev/null @@ -1,24 +0,0 @@ -/* -Copyright 2015 Hardcoded Software (http://www.hardcoded.net) - -This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -which should be included with this package. The terms are also available at -http://www.gnu.org/licenses/gpl-3.0.html -*/ - -#import - -@interface NSEvent(NSEventAdditions) -- (unichar)firstCharacter; -- (NSUInteger)flags; -- (NSUInteger)modifierKeysFlags; -- (BOOL)isDeleteOrBackspace; -- (BOOL)isReturnOrEnter; -- (BOOL)isTab; -- (BOOL)isBackTab; -- (BOOL)isSpace; -- (BOOL)isUp; -- (BOOL)isDown; -- (BOOL)isLeft; -- (BOOL)isRight; -@end diff --git a/cocoalib/NSEventAdditions.m b/cocoalib/NSEventAdditions.m deleted file mode 100644 index 03f8b007..00000000 --- a/cocoalib/NSEventAdditions.m +++ /dev/null @@ -1,85 +0,0 @@ -/* -Copyright 2015 Hardcoded Software (http://www.hardcoded.net) - -This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -which should be included with this package. The terms are also available at -http://www.gnu.org/licenses/gpl-3.0.html -*/ - -#import "NSEventAdditions.h" - -@implementation NSEvent(NSEventAdditions) - -- (unichar)firstCharacter -{ - NSString *characters = [self characters]; - if ([characters length] == 0) - { - return '\0'; - } - return [characters characterAtIndex:0]; -} - -- (NSUInteger)flags -{ - // get flags and strip the lower 16 (device dependant) bits - // See modifierFlags's doc for details - return [self modifierFlags] & NSDeviceIndependentModifierFlagsMask; -} - -- (NSUInteger)modifierKeysFlags -{ - // This is modifierFlags with only Command, Opt, Ctrl and Shift, without the rest of the flags - // to pollute. - return [self flags] & (NSShiftKeyMask | NSControlKeyMask | NSAlternateKeyMask | NSCommandKeyMask); -} - -- (BOOL)isDeleteOrBackspace -{ - unichar firstChar = [self firstCharacter]; - return firstChar == NSDeleteFunctionKey || firstChar == NSDeleteCharFunctionKey || - firstChar == NSDeleteCharacter || firstChar == NSBackspaceCharacter; -} - -- (BOOL)isReturnOrEnter -{ - unichar firstChar = [self firstCharacter]; - return firstChar == NSCarriageReturnCharacter || firstChar == NSEnterCharacter; -} - -- (BOOL)isTab -{ - return [self firstCharacter] == NSTabCharacter; -} - -- (BOOL)isBackTab -{ - return [self firstCharacter] == NSBackTabCharacter; -} - -- (BOOL)isSpace -{ - return ([self firstCharacter] == 0x20) && (![self flags]); -} - -- (BOOL)isUp -{ - return [self firstCharacter] == NSUpArrowFunctionKey; -} - -- (BOOL)isDown -{ - return [self firstCharacter] == NSDownArrowFunctionKey; -} - -- (BOOL)isLeft -{ - return [self firstCharacter] == NSLeftArrowFunctionKey; -} - -- (BOOL)isRight -{ - return [self firstCharacter] == NSRightArrowFunctionKey; -} - -@end diff --git a/cocoalib/NSImageAdditions.h b/cocoalib/NSImageAdditions.h deleted file mode 100644 index 410d62d4..00000000 --- a/cocoalib/NSImageAdditions.h +++ /dev/null @@ -1,21 +0,0 @@ -// Created by Scott Stevenson on 9/28/07. -// -// Personal site: http://theocacao.com/ -// Post for this sample: http://theocacao.com/document.page/497 -// -// The code in this project is intended to be used as a learning -// tool for Cocoa programmers. You may freely use the code in -// your own programs, but please do not use the code as-is in -// other tutorials. - -#import - - -@interface NSImage (Extras) - -// creates a copy of the current image while maintaining -// proportions. also centers image, if necessary - -- (NSImage*)imageByScalingProportionallyToSize:(NSSize)aSize; - -@end \ No newline at end of file diff --git a/cocoalib/NSImageAdditions.m b/cocoalib/NSImageAdditions.m deleted file mode 100644 index ad5fedd8..00000000 --- a/cocoalib/NSImageAdditions.m +++ /dev/null @@ -1,114 +0,0 @@ -// Created by Scott Stevenson on 9/28/07. -// -// Personal site: http://theocacao.com/ -// Post for this sample: http://theocacao.com/document.page/497 -// -// The code in this project is intended to be used as a learning -// tool for Cocoa programmers. You may freely use the code in -// your own programs, but please do not use the code as-is in -// other tutorials. - -#import "NSImageAdditions.h" - - -@implementation NSImage (Extras) - -- (NSImage*)imageByScalingProportionallyToSize:(NSSize)targetSize -{ - NSImage* sourceImage = self; - NSImage* newImage = nil; - - if ([sourceImage isValid]) - { - NSSize imageSize = [sourceImage size]; - CGFloat width = imageSize.width; - CGFloat height = imageSize.height; - - CGFloat targetWidth = targetSize.width; - CGFloat targetHeight = targetSize.height; - - // scaleFactor will be the fraction that we'll - // use to adjust the size. For example, if we shrink - // an image by half, scaleFactor will be 0.5. the - // scaledWidth and scaledHeight will be the original, - // multiplied by the scaleFactor. - // - // IMPORTANT: the "targetHeight" is the size of the space - // we're drawing into. The "scaledHeight" is the height that - // the image actually is drawn at, once we take into - // account the ideal of maintaining proportions - - CGFloat scaleFactor = 0.0; - CGFloat scaledWidth = targetWidth; - CGFloat scaledHeight = targetHeight; - - NSPoint thumbnailPoint = NSMakePoint(0,0); - - // since not all images are square, we want to scale - // proportionately. To do this, we find the longest - // edge and use that as a guide. - - if ( NSEqualSizes( imageSize, targetSize ) == NO ) - { - // use the longeset edge as a guide. if the - // image is wider than tall, we'll figure out - // the scale factor by dividing it by the - // intended width. Otherwise, we'll use the - // height. - - CGFloat widthFactor = targetWidth / width; - CGFloat heightFactor = targetHeight / height; - - if ( widthFactor < heightFactor ) - scaleFactor = widthFactor; - else - scaleFactor = heightFactor; - - // ex: 500 * 0.5 = 250 (newWidth) - - scaledWidth = width * scaleFactor; - scaledHeight = height * scaleFactor; - - // center the thumbnail in the frame. if - // wider than tall, we need to adjust the - // vertical drawing point (y axis) - - if ( widthFactor < heightFactor ) - thumbnailPoint.y = (targetHeight - scaledHeight) * 0.5; - - else if ( widthFactor > heightFactor ) - thumbnailPoint.x = (targetWidth - scaledWidth) * 0.5; - } - - - // create a new image to draw into - newImage = [[NSImage alloc] initWithSize:targetSize]; - - // once focus is locked, all drawing goes into this NSImage instance - // directly, not to the screen. It also receives its own graphics - // context. - // - // Also, keep in mind that we're doing this in a background thread. - // You only want to draw to the screen in the main thread, but - // drawing to an offscreen image is (apparently) okay. - - [newImage lockFocus]; - - NSRect thumbnailRect; - thumbnailRect.origin = thumbnailPoint; - thumbnailRect.size.width = scaledWidth; - thumbnailRect.size.height = scaledHeight; - - [sourceImage drawInRect: thumbnailRect - fromRect: NSZeroRect - operation: NSCompositeSourceOver - fraction: 1.0]; - - [newImage unlockFocus]; - - } - - return [newImage autorelease]; -} - -@end \ No newline at end of file diff --git a/cocoalib/NSNotificationAdditions.h b/cocoalib/NSNotificationAdditions.h deleted file mode 100644 index 0b8c2a0a..00000000 --- a/cocoalib/NSNotificationAdditions.h +++ /dev/null @@ -1,10 +0,0 @@ -// from http://www.cocoadev.com/index.pl?NotificationsAcrossThreads -#import -@interface NSNotificationCenter (NSNotificationCenterAdditions) -- (void) postNotificationOnMainThread:(NSNotification *) notification; -- (void) postNotificationOnMainThread:(NSNotification *) notification waitUntilDone:(BOOL) wait; - -- (void) postNotificationOnMainThreadWithName:(NSString *) name object:(id) object; -- (void) postNotificationOnMainThreadWithName:(NSString *) name object:(id) object userInfo:(NSDictionary *) userInfo; -- (void) postNotificationOnMainThreadWithName:(NSString *) name object:(id) object userInfo:(NSDictionary *) userInfo waitUntilDone:(BOOL) wait; -@end diff --git a/cocoalib/NSNotificationAdditions.m b/cocoalib/NSNotificationAdditions.m deleted file mode 100644 index c6081d06..00000000 --- a/cocoalib/NSNotificationAdditions.m +++ /dev/null @@ -1,48 +0,0 @@ -#import "NSNotificationAdditions.h" -#import - -@implementation NSNotificationCenter (NSNotificationCenterAdditions) -- (void) postNotificationOnMainThread:(NSNotification *) notification { - if( pthread_main_np() ) return [self postNotification:notification]; - [self postNotificationOnMainThread:notification waitUntilDone:NO]; -} - -- (void) postNotificationOnMainThread:(NSNotification *) notification waitUntilDone:(BOOL) wait { - if( pthread_main_np() ) return [self postNotification:notification]; - [[self class] performSelectorOnMainThread:@selector( _postNotification: ) withObject:notification waitUntilDone:wait]; -} - -+ (void) _postNotification:(NSNotification *) notification { - [[self defaultCenter] postNotification:notification]; -} - -- (void) postNotificationOnMainThreadWithName:(NSString *) name object:(id) object { - if( pthread_main_np() ) return [self postNotificationName:name object:object userInfo:nil]; - [self postNotificationOnMainThreadWithName:name object:object userInfo:nil waitUntilDone:NO]; -} - -- (void) postNotificationOnMainThreadWithName:(NSString *) name object:(id) object userInfo:(NSDictionary *) userInfo { - if( pthread_main_np() ) return [self postNotificationName:name object:object userInfo:userInfo]; - [self postNotificationOnMainThreadWithName:name object:object userInfo:nil waitUntilDone:NO]; -} - -- (void) postNotificationOnMainThreadWithName:(NSString *) name object:(id) object userInfo:(NSDictionary *) userInfo waitUntilDone:(BOOL) wait { - if( pthread_main_np() ) return [self postNotificationName:name object:object userInfo:userInfo]; - - NSMutableDictionary *info = [[NSMutableDictionary allocWithZone:nil] init]; - [info setObject:name forKey:@"name"]; - if( object ) [info setObject:object forKey:@"object"]; - if( userInfo ) [info setObject:userInfo forKey:@"userInfo"]; - - [[self class] performSelectorOnMainThread:@selector( _postNotificationName: ) withObject:info waitUntilDone:wait]; - [info release]; -} - -+ (void) _postNotificationName:(NSDictionary *) info { - NSString *name = [info objectForKey:@"name"]; - id object = [info objectForKey:@"object"]; - NSDictionary *userInfo = [info objectForKey:@"userInfo"]; - - [[self defaultCenter] postNotificationName:name object:object userInfo:userInfo]; -} -@end \ No newline at end of file diff --git a/cocoalib/ProgressController.h b/cocoalib/ProgressController.h deleted file mode 100644 index 46e70345..00000000 --- a/cocoalib/ProgressController.h +++ /dev/null @@ -1,50 +0,0 @@ -/* -Copyright 2015 Hardcoded Software (http://www.hardcoded.net) - -This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -which should be included with this package. The terms are also available at -http://www.gnu.org/licenses/gpl-3.0.html -*/ - -#import -#import "Worker.h" - -extern NSString *JobCompletedNotification; -extern NSString *JobCancelledNotification; - -@interface ProgressController : NSWindowController -{ - NSButton *cancelButton; - NSProgressIndicator *progressBar; - NSTextField *statusText; - NSTextField *descText; - - id _jobId; - BOOL _running; - NSObject *_worker; -} - -@property (readwrite, retain) NSButton *cancelButton; -@property (readwrite, retain) NSProgressIndicator *progressBar; -@property (readwrite, retain) NSTextField *statusText; -@property (readwrite, retain) NSTextField *descText; - -+ (ProgressController *)mainProgressController; - -- (id)init; - -- (void)cancel; - -- (void)hide; -- (void)show; -- (void)showWithCancelButton:(BOOL)cancelEnabled; -- (void)showSheetForParent:(NSWindow *) parentWindow; -- (void)showSheetForParent:(NSWindow *) parentWindow withCancelButton:(BOOL)cancelEnabled; - -/* Properties */ -- (BOOL)isShown; -- (id)jobId; -- (void)setJobId:(id)jobId; -- (void)setJobDesc:(NSString *)desc; -- (void)setWorker:(NSObject *)worker; -@end diff --git a/cocoalib/ProgressController.m b/cocoalib/ProgressController.m deleted file mode 100644 index 891ca7ae..00000000 --- a/cocoalib/ProgressController.m +++ /dev/null @@ -1,160 +0,0 @@ -/* -Copyright 2015 Hardcoded Software (http://www.hardcoded.net) - -This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -which should be included with this package. The terms are also available at -http://www.gnu.org/licenses/gpl-3.0.html -*/ - -#import "ProgressController.h" -#import "Utils.h" -#import "ProgressController_UI.h" - -NSString *JobCompletedNotification = @"JobCompletedNotification"; -NSString *JobCancelledNotification = @"JobCancelledNotification"; -static ProgressController *_mainPC = nil; - -@implementation ProgressController - -@synthesize cancelButton; -@synthesize progressBar; -@synthesize statusText; -@synthesize descText; - -+ (ProgressController *)mainProgressController -{ - if (_mainPC == nil) - _mainPC = [[ProgressController alloc] init]; - return _mainPC; -} - -- (id)init -{ - self = [super initWithWindow:nil]; - [self setWindow:createProgressController_UI(self)]; - [progressBar setUsesThreadedAnimation:YES]; - _worker = nil; - _running = NO; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidBecomeActive:) name:NSApplicationDidBecomeActiveNotification object:nil]; - return self; -} - -- (void)cancel -{ - [self hide]; -} - -- (void)hide -{ - if (_worker != nil) - [_worker cancelJob]; - [[NSNotificationCenter defaultCenter] postNotificationName:JobCancelledNotification object:self]; - _running = NO; - [NSApp endSheet:[self window] returnCode:NSRunAbortedResponse]; - /* There's this really strange thing where when the app is inactive at the point we want to hide - the progress dialog, it becomes impossible to close it. I guess it's due to some strange - thread-related crap. Anyway, *DO NOT HIDE THE SHEET WHILE THE APP IS INACTIVE*. Do it later, - when the app becomes active again. - */ - if ([NSApp isActive]) { - [[self window] orderOut:nil]; - } -} - -- (void)show -{ - [self showWithCancelButton:YES]; -} - -- (void)showWithCancelButton:(BOOL)cancelEnabled -{ - [progressBar setIndeterminate:YES]; - [[self window] makeKeyAndOrderFront:nil]; - [progressBar setUsesThreadedAnimation:YES]; - [progressBar startAnimation:nil]; - [cancelButton setEnabled:cancelEnabled]; - _running = YES; - [NSThread detachNewThreadSelector:@selector(threadedWorkerProbe) toTarget:self withObject:nil]; -} - -- (void)showSheetForParent:(NSWindow *) parentWindow -{ - [self showSheetForParent:parentWindow withCancelButton:YES]; -} - -- (void)showSheetForParent:(NSWindow *) parentWindow withCancelButton:(BOOL)cancelEnabled -{ - [progressBar setIndeterminate:YES]; - [progressBar startAnimation:nil]; - [cancelButton setEnabled:cancelEnabled]; - _running = YES; - [NSThread detachNewThreadSelector:@selector(threadedWorkerProbe) toTarget:self withObject:nil]; - [NSApp beginSheet:[self window] modalForWindow:parentWindow modalDelegate:nil didEndSelector:nil contextInfo:nil]; -} - -- (void)updateProgress -{ - if (!_running) - return; - NSNumber *progress = [_worker getJobProgress]; - NSString *status = [_worker getJobDesc]; - if ((status != nil) && ([status length] > 0)) - { - [statusText setStringValue:status]; - } - if (progress != nil) - { - [progressBar setDoubleValue:n2i(progress)]; - [progressBar setIndeterminate: n2i(progress) < 0]; - } - else - { - [self hide]; - [_worker jobCompleted:_jobId]; - [[NSNotificationCenter defaultCenter] postNotificationName:JobCompletedNotification object:self]; - } -} - -- (void)threadedWorkerProbe -{ - while (_running && (_worker != nil)) - { - NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; - [NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]]; - [self performSelectorOnMainThread:@selector(updateProgress) withObject:nil waitUntilDone:YES]; - [pool release]; - } -} - -/* Properties */ -- (BOOL)isShown -{ - return _running; -} - -- (id)jobId {return _jobId;} -- (void)setJobId:(id)jobId -{ - [_jobId autorelease]; - _jobId = [jobId retain]; -} - -- (void)setJobDesc:(NSString *)desc -{ - [descText setStringValue:desc]; - [statusText setStringValue:NSLocalizedStringFromTable(@"Please wait...", @"cocoalib", @"")]; -} - -- (void)setWorker:(NSObject *)worker -{ - _worker = worker; -} - -/* Delegate and Notifs */ -- (void)applicationDidBecomeActive:(NSNotification *)notification -{ - if (!_running) { - [[self window] orderOut:nil]; - } -} -@end diff --git a/cocoalib/Sparkle.framework/Headers b/cocoalib/Sparkle.framework/Headers deleted file mode 120000 index a177d2a6..00000000 --- a/cocoalib/Sparkle.framework/Headers +++ /dev/null @@ -1 +0,0 @@ -Versions/Current/Headers \ No newline at end of file diff --git a/cocoalib/Sparkle.framework/Resources b/cocoalib/Sparkle.framework/Resources deleted file mode 120000 index 953ee36f..00000000 --- a/cocoalib/Sparkle.framework/Resources +++ /dev/null @@ -1 +0,0 @@ -Versions/Current/Resources \ No newline at end of file diff --git a/cocoalib/Sparkle.framework/Sparkle b/cocoalib/Sparkle.framework/Sparkle deleted file mode 120000 index b2c52731..00000000 --- a/cocoalib/Sparkle.framework/Sparkle +++ /dev/null @@ -1 +0,0 @@ -Versions/Current/Sparkle \ No newline at end of file diff --git a/cocoalib/Sparkle.framework/Versions/A/Headers/SUAppcast.h b/cocoalib/Sparkle.framework/Versions/A/Headers/SUAppcast.h deleted file mode 100644 index 171148a4..00000000 --- a/cocoalib/Sparkle.framework/Versions/A/Headers/SUAppcast.h +++ /dev/null @@ -1,33 +0,0 @@ -// -// SUAppcast.h -// Sparkle -// -// Created by Andy Matuschak on 3/12/06. -// Copyright 2006 Andy Matuschak. All rights reserved. -// - -#ifndef SUAPPCAST_H -#define SUAPPCAST_H - -@class SUAppcastItem; -@interface SUAppcast : NSObject { - NSArray *items; - NSString *userAgentString; - id delegate; - NSMutableData *incrementalData; -} - -- (void)fetchAppcastFromURL:(NSURL *)url; -- (void)setDelegate:delegate; -- (void)setUserAgentString:(NSString *)userAgentString; - -- (NSArray *)items; - -@end - -@interface NSObject (SUAppcastDelegate) -- (void)appcastDidFinishLoading:(SUAppcast *)appcast; -- (void)appcast:(SUAppcast *)appcast failedToLoadWithError:(NSError *)error; -@end - -#endif diff --git a/cocoalib/Sparkle.framework/Versions/A/Headers/SUAppcastItem.h b/cocoalib/Sparkle.framework/Versions/A/Headers/SUAppcastItem.h deleted file mode 100644 index 7f1ca65c..00000000 --- a/cocoalib/Sparkle.framework/Versions/A/Headers/SUAppcastItem.h +++ /dev/null @@ -1,47 +0,0 @@ -// -// SUAppcastItem.h -// Sparkle -// -// Created by Andy Matuschak on 3/12/06. -// Copyright 2006 Andy Matuschak. All rights reserved. -// - -#ifndef SUAPPCASTITEM_H -#define SUAPPCASTITEM_H - -@interface SUAppcastItem : NSObject { - NSString *title; - NSDate *date; - NSString *itemDescription; - - NSURL *releaseNotesURL; - - NSString *DSASignature; - NSString *minimumSystemVersion; - - NSURL *fileURL; - NSString *versionString; - NSString *displayVersionString; - - NSDictionary *propertiesDictionary; -} - -// Initializes with data from a dictionary provided by the RSS class. -- initWithDictionary:(NSDictionary *)dict; - -- (NSString *)title; -- (NSString *)versionString; -- (NSString *)displayVersionString; -- (NSDate *)date; -- (NSString *)itemDescription; -- (NSURL *)releaseNotesURL; -- (NSURL *)fileURL; -- (NSString *)DSASignature; -- (NSString *)minimumSystemVersion; - -// Returns the dictionary provided in initWithDictionary; this might be useful later for extensions. -- (NSDictionary *)propertiesDictionary; - -@end - -#endif diff --git a/cocoalib/Sparkle.framework/Versions/A/Headers/SUUpdater.h b/cocoalib/Sparkle.framework/Versions/A/Headers/SUUpdater.h deleted file mode 100644 index e78c4d35..00000000 --- a/cocoalib/Sparkle.framework/Versions/A/Headers/SUUpdater.h +++ /dev/null @@ -1,118 +0,0 @@ -// -// SUUpdater.h -// Sparkle -// -// Created by Andy Matuschak on 1/4/06. -// Copyright 2006 Andy Matuschak. All rights reserved. -// - -#ifndef SUUPDATER_H -#define SUUPDATER_H - -#import - -@class SUUpdateDriver, SUAppcastItem, SUHost, SUAppcast; -@interface SUUpdater : NSObject { - NSTimer *checkTimer; - SUUpdateDriver *driver; - - SUHost *host; - IBOutlet id delegate; -} - -+ (SUUpdater *)sharedUpdater; -+ (SUUpdater *)updaterForBundle:(NSBundle *)bundle; -- (NSBundle *)hostBundle; - -- (void)setDelegate:(id)delegate; -- delegate; - -- (void)setAutomaticallyChecksForUpdates:(BOOL)automaticallyChecks; -- (BOOL)automaticallyChecksForUpdates; - -- (void)setUpdateCheckInterval:(NSTimeInterval)interval; -- (NSTimeInterval)updateCheckInterval; - -- (void)setFeedURL:(NSURL *)feedURL; -- (NSURL *)feedURL; - -- (void)setSendsSystemProfile:(BOOL)sendsSystemProfile; -- (BOOL)sendsSystemProfile; - -- (void)setAutomaticallyDownloadsUpdates:(BOOL)automaticallyDownloadsUpdates; -- (BOOL)automaticallyDownloadsUpdates; - -// This IBAction is meant for a main menu item. Hook up any menu item to this action, -// and Sparkle will check for updates and report back its findings verbosely. -- (IBAction)checkForUpdates:sender; - -// This kicks off an update meant to be programmatically initiated. That is, it will display no UI unless it actually finds an update, -// in which case it proceeds as usual. If the fully automated updating is turned on, however, this will invoke that behavior, and if an -// update is found, it will be downloaded and prepped for installation. -- (void)checkForUpdatesInBackground; - -// Date of last update check. Returns null if no check has been performed. -- (NSDate*)lastUpdateCheckDate; - -// This begins a "probing" check for updates which will not actually offer to update to that version. The delegate methods, though, -// (up to updater:didFindValidUpdate: and updaterDidNotFindUpdate:), are called, so you can use that information in your UI. -- (void)checkForUpdateInformation; - -// Call this to appropriately schedule or cancel the update checking timer according to the preferences for time interval and automatic checks. This call does not change the date of the next check, but only the internal NSTimer. -- (void)resetUpdateCycle; - -- (BOOL)updateInProgress; -@end - -@interface NSObject (SUUpdaterDelegateInformalProtocol) -// This method allows you to add extra parameters to the appcast URL, potentially based on whether or not Sparkle will also be sending along the system profile. This method should return an array of dictionaries with keys: "key", "value", "displayKey", "displayValue", the latter two being specifically for display to the user. -- (NSArray *)feedParametersForUpdater:(SUUpdater *)updater sendingSystemProfile:(BOOL)sendingProfile; - -// Use this to override the default behavior for Sparkle prompting the user about automatic update checks. -- (BOOL)updaterShouldPromptForPermissionToCheckForUpdates:(SUUpdater *)bundle; - -// Implement this if you want to do some special handling with the appcast once it finishes loading. -- (void)updater:(SUUpdater *)updater didFinishLoadingAppcast:(SUAppcast *)appcast; - -// If you're using special logic or extensions in your appcast, implement this to use your own logic for finding -// a valid update, if any, in the given appcast. -- (SUAppcastItem *)bestValidUpdateInAppcast:(SUAppcast *)appcast forUpdater:(SUUpdater *)bundle; - -// Sent when a valid update is found by the update driver. -- (void)updater:(SUUpdater *)updater didFindValidUpdate:(SUAppcastItem *)update; - -// Sent when a valid update is not found. -- (void)updaterDidNotFindUpdate:(SUUpdater *)update; - -// Sent immediately before installing the specified update. -- (void)updater:(SUUpdater *)updater willInstallUpdate:(SUAppcastItem *)update; - -// Return YES to delay the relaunch until you do some processing; invoke the given NSInvocation to continue. -- (BOOL)updater:(SUUpdater *)updater shouldPostponeRelaunchForUpdate:(SUAppcastItem *)update untilInvoking:(NSInvocation *)invocation; - -// Called immediately before relaunching. -- (void)updaterWillRelaunchApplication:(SUUpdater *)updater; - -// This method allows you to provide a custom version comparator. -// If you don't implement this method or return nil, the standard version comparator will be used. -- (id )versionComparatorForUpdater:(SUUpdater *)updater; - -// Returns the path which is used to relaunch the client after the update is installed. By default, the path of the host bundle. -- (NSString *)pathToRelaunchForUpdater:(SUUpdater *)updater; - -@end - -// Define some minimum intervals to avoid DOS-like checking attacks. These are in seconds. -#ifdef DEBUG -#define SU_MIN_CHECK_INTERVAL 60 -#else -#define SU_MIN_CHECK_INTERVAL 60*60 -#endif - -#ifdef DEBUG -#define SU_DEFAULT_CHECK_INTERVAL 60 -#else -#define SU_DEFAULT_CHECK_INTERVAL 60*60*24 -#endif - -#endif diff --git a/cocoalib/Sparkle.framework/Versions/A/Headers/SUVersionComparisonProtocol.h b/cocoalib/Sparkle.framework/Versions/A/Headers/SUVersionComparisonProtocol.h deleted file mode 100644 index 3d11ae87..00000000 --- a/cocoalib/Sparkle.framework/Versions/A/Headers/SUVersionComparisonProtocol.h +++ /dev/null @@ -1,27 +0,0 @@ -// -// SUVersionComparisonProtocol.h -// Sparkle -// -// Created by Andy Matuschak on 12/21/07. -// Copyright 2007 Andy Matuschak. All rights reserved. -// - -#ifndef SUVERSIONCOMPARISONPROTOCOL_H -#define SUVERSIONCOMPARISONPROTOCOL_H - -/*! - @protocol - @abstract Implement this protocol to provide version comparison facilities for Sparkle. -*/ -@protocol SUVersionComparison - -/*! - @method - @abstract An abstract method to compare two version strings. - @discussion Should return NSOrderedAscending if b > a, NSOrderedDescending if b < a, and NSOrderedSame if they are equivalent. -*/ -- (NSComparisonResult)compareVersion:(NSString *)versionA toVersion:(NSString *)versionB; - -@end - -#endif diff --git a/cocoalib/Sparkle.framework/Versions/A/Headers/Sparkle.h b/cocoalib/Sparkle.framework/Versions/A/Headers/Sparkle.h deleted file mode 100644 index 08dd5777..00000000 --- a/cocoalib/Sparkle.framework/Versions/A/Headers/Sparkle.h +++ /dev/null @@ -1,21 +0,0 @@ -// -// Sparkle.h -// Sparkle -// -// Created by Andy Matuschak on 3/16/06. (Modified by CDHW on 23/12/07) -// Copyright 2006 Andy Matuschak. All rights reserved. -// - -#ifndef SPARKLE_H -#define SPARKLE_H - -// This list should include the shared headers. It doesn't matter if some of them aren't shared (unless -// there are name-space collisions) so we can list all of them to start with: - -#import - -#import -#import -#import - -#endif diff --git a/cocoalib/Sparkle.framework/Versions/A/Resources/Info.plist b/cocoalib/Sparkle.framework/Versions/A/Resources/Info.plist deleted file mode 100644 index c7f277d0..00000000 --- a/cocoalib/Sparkle.framework/Versions/A/Resources/Info.plist +++ /dev/null @@ -1,24 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - Sparkle - CFBundleIdentifier - org.andymatuschak.Sparkle - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - Sparkle - CFBundlePackageType - FMWK - CFBundleShortVersionString - 1.5 Beta 6 - CFBundleSignature - ???? - CFBundleVersion - 313 - - diff --git a/cocoalib/Sparkle.framework/Versions/A/Resources/License.txt b/cocoalib/Sparkle.framework/Versions/A/Resources/License.txt deleted file mode 100644 index 20466c41..00000000 --- a/cocoalib/Sparkle.framework/Versions/A/Resources/License.txt +++ /dev/null @@ -1,7 +0,0 @@ -Copyright (c) 2006 Andy Matuschak - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/cocoalib/Sparkle.framework/Versions/A/Resources/SUModelTranslation.plist b/cocoalib/Sparkle.framework/Versions/A/Resources/SUModelTranslation.plist deleted file mode 100644 index 92ef9471..00000000 --- a/cocoalib/Sparkle.framework/Versions/A/Resources/SUModelTranslation.plist +++ /dev/null @@ -1,174 +0,0 @@ - - - - - ADP2,1 - Developer Transition Kit - MacBook1,1 - MacBook (Core Duo) - MacBook2,1 - MacBook (Core 2 Duo) - MacBook4,1 - MacBook (Core 2 Duo Feb 2008) - MacBookAir1,1 - MacBook Air (January 2008) - MacBookPro1,1 - MacBook Pro Core Duo (15-inch) - MacBookPro1,2 - MacBook Pro Core Duo (17-inch) - MacBookPro2,1 - MacBook Pro Core 2 Duo (17-inch) - MacBookPro2,2 - MacBook Pro Core 2 Duo (15-inch) - MacBookPro3,1 - MacBook Pro Core 2 Duo (15-inch LED, Core 2 Duo) - MacBookPro3,2 - MacBook Pro Core 2 Duo (17-inch HD, Core 2 Duo) - MacBookPro4,1 - MacBook Pro (Core 2 Duo Feb 2008) - MacPro1,1 - Mac Pro (four-core) - MacPro2,1 - Mac Pro (eight-core) - MacPro3,1 - Mac Pro (January 2008 4- or 8- core "Harpertown") - Macmini1,1 - Mac Mini (Core Solo/Duo) - PowerBook1,1 - PowerBook G3 - PowerBook2,1 - iBook G3 - PowerBook2,2 - iBook G3 (FireWire) - PowerBook2,3 - iBook G3 - PowerBook2,4 - iBook G3 - PowerBook3,1 - PowerBook G3 (FireWire) - PowerBook3,2 - PowerBook G4 - PowerBook3,3 - PowerBook G4 (Gigabit Ethernet) - PowerBook3,4 - PowerBook G4 (DVI) - PowerBook3,5 - PowerBook G4 (1GHz / 867MHz) - PowerBook4,1 - iBook G3 (Dual USB, Late 2001) - PowerBook4,2 - iBook G3 (16MB VRAM) - PowerBook4,3 - iBook G3 Opaque 16MB VRAM, 32MB VRAM, Early 2003) - PowerBook5,1 - PowerBook G4 (17 inch) - PowerBook5,2 - PowerBook G4 (15 inch FW 800) - PowerBook5,3 - PowerBook G4 (17-inch 1.33GHz) - PowerBook5,4 - PowerBook G4 (15 inch 1.5/1.33GHz) - PowerBook5,5 - PowerBook G4 (17-inch 1.5GHz) - PowerBook5,6 - PowerBook G4 (15 inch 1.67GHz/1.5GHz) - PowerBook5,7 - PowerBook G4 (17-inch 1.67GHz) - PowerBook5,8 - PowerBook G4 (Double layer SD, 15 inch) - PowerBook5,9 - PowerBook G4 (Double layer SD, 17 inch) - PowerBook6,1 - PowerBook G4 (12 inch) - PowerBook6,2 - PowerBook G4 (12 inch, DVI) - PowerBook6,3 - iBook G4 - PowerBook6,4 - PowerBook G4 (12 inch 1.33GHz) - PowerBook6,5 - iBook G4 (Early-Late 2004) - PowerBook6,7 - iBook G4 (Mid 2005) - PowerBook6,8 - PowerBook G4 (12 inch 1.5GHz) - PowerMac1,1 - Power Macintosh G3 (Blue & White) - PowerMac1,2 - Power Macintosh G4 (PCI Graphics) - PowerMac10,1 - Mac Mini G4 - PowerMac10,2 - Mac Mini (Late 2005) - PowerMac11,2 - Power Macintosh G5 (Late 2005) - PowerMac12,1 - iMac G5 (iSight) - PowerMac2,1 - iMac G3 (Slot-loading CD-ROM) - PowerMac2,2 - iMac G3 (Summer 2000) - PowerMac3,1 - Power Macintosh G4 (AGP Graphics) - PowerMac3,2 - Power Macintosh G4 (AGP Graphics) - PowerMac3,3 - Power Macintosh G4 (Gigabit Ethernet) - PowerMac3,4 - Power Macintosh G4 (Digital Audio) - PowerMac3,5 - Power Macintosh G4 (Quick Silver) - PowerMac3,6 - Power Macintosh G4 (Mirrored Drive Door) - PowerMac4,1 - iMac G3 (Early/Summer 2001) - PowerMac4,2 - iMac G4 (Flat Panel) - PowerMac4,4 - eMac - PowerMac4,5 - iMac G4 (17-inch Flat Panel) - PowerMac5,1 - Power Macintosh G4 Cube - PowerMac6,1 - iMac G4 (USB 2.0) - PowerMac6,3 - iMac G4 (20-inch Flat Panel) - PowerMac6,4 - eMac (USB 2.0, 2005) - PowerMac7,2 - Power Macintosh G5 - PowerMac7,3 - Power Macintosh G5 - PowerMac8,1 - iMac G5 - PowerMac8,2 - iMac G5 (Ambient Light Sensor) - PowerMac9,1 - Power Macintosh G5 (Late 2005) - RackMac1,1 - Xserve G4 - RackMac1,2 - Xserve G4 (slot-loading, cluster node) - RackMac3,1 - Xserve G5 - Xserve1,1 - Xserve (Intel Xeon) - Xserve2,1 - Xserve (January 2008 quad-core) - iMac1,1 - iMac G3 (Rev A-D) - iMac4,1 - iMac (Core Duo) - iMac4,2 - iMac for Education (17-inch, Core Duo) - iMac5,1 - iMac (Core 2 Duo, 17 or 20 inch, SuperDrive) - iMac5,2 - iMac (Core 2 Duo, 17 inch, Combo Drive) - iMac6,1 - iMac (Core 2 Duo, 24 inch, SuperDrive) - iMac8,1 - iMac (April 2008) - - diff --git a/cocoalib/Sparkle.framework/Versions/A/Resources/SUStatus.nib/classes.nib b/cocoalib/Sparkle.framework/Versions/A/Resources/SUStatus.nib/classes.nib deleted file mode 100644 index 22f13f8b..00000000 --- a/cocoalib/Sparkle.framework/Versions/A/Resources/SUStatus.nib/classes.nib +++ /dev/null @@ -1,56 +0,0 @@ - - - - - IBClasses - - - CLASS - SUWindowController - LANGUAGE - ObjC - SUPERCLASS - NSWindowController - - - CLASS - NSApplication - LANGUAGE - ObjC - SUPERCLASS - NSResponder - - - CLASS - FirstResponder - LANGUAGE - ObjC - SUPERCLASS - NSObject - - - CLASS - NSObject - LANGUAGE - ObjC - - - CLASS - SUStatusController - LANGUAGE - ObjC - OUTLETS - - actionButton - NSButton - progressBar - NSProgressIndicator - - SUPERCLASS - SUWindowController - - - IBVersion - 1 - - diff --git a/cocoalib/Sparkle.framework/Versions/A/Resources/SUStatus.nib/info.nib b/cocoalib/Sparkle.framework/Versions/A/Resources/SUStatus.nib/info.nib deleted file mode 100644 index a9ac8673..00000000 --- a/cocoalib/Sparkle.framework/Versions/A/Resources/SUStatus.nib/info.nib +++ /dev/null @@ -1,20 +0,0 @@ - - - - - IBFramework Version - 670 - IBLastKnownRelativeProjectPath - Sparkle.xcodeproj - IBOldestOS - 5 - IBOpenObjects - - 6 - - IBSystem Version - 10A96 - targetFramework - IBCocoaFramework - - diff --git a/cocoalib/Sparkle.framework/Versions/A/Resources/SUStatus.nib/keyedobjects.nib b/cocoalib/Sparkle.framework/Versions/A/Resources/SUStatus.nib/keyedobjects.nib deleted file mode 100644 index 4f1d598179ccecb32c7a12eead96bb5fe3663eb0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7344 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 I{hK-Azn((800000 diff --git a/cocoalib/Sparkle.framework/Versions/A/Resources/en.lproj/SUAutomaticUpdateAlert.nib/classes.nib b/cocoalib/Sparkle.framework/Versions/A/Resources/en.lproj/SUAutomaticUpdateAlert.nib/classes.nib deleted file mode 100644 index 4b1ab30e..00000000 --- a/cocoalib/Sparkle.framework/Versions/A/Resources/en.lproj/SUAutomaticUpdateAlert.nib/classes.nib +++ /dev/null @@ -1,50 +0,0 @@ - - - - - IBClasses - - - CLASS - SUWindowController - LANGUAGE - ObjC - SUPERCLASS - NSWindowController - - - ACTIONS - - doNotInstall - id - installLater - id - installNow - id - - CLASS - SUAutomaticUpdateAlert - LANGUAGE - ObjC - SUPERCLASS - SUWindowController - - - CLASS - FirstResponder - LANGUAGE - ObjC - SUPERCLASS - NSObject - - - CLASS - NSObject - LANGUAGE - ObjC - - - IBVersion - 1 - - diff --git a/cocoalib/Sparkle.framework/Versions/A/Resources/en.lproj/SUAutomaticUpdateAlert.nib/info.nib b/cocoalib/Sparkle.framework/Versions/A/Resources/en.lproj/SUAutomaticUpdateAlert.nib/info.nib deleted file mode 100644 index ab36d310..00000000 --- a/cocoalib/Sparkle.framework/Versions/A/Resources/en.lproj/SUAutomaticUpdateAlert.nib/info.nib +++ /dev/null @@ -1,20 +0,0 @@ - - - - - IBFramework Version - 658 - IBLastKnownRelativeProjectPath - ../Sparkle.xcodeproj - IBOldestOS - 5 - IBOpenObjects - - 6 - - IBSystem Version - 9C7010 - targetFramework - IBCocoaFramework - - diff --git a/cocoalib/Sparkle.framework/Versions/A/Resources/en.lproj/SUAutomaticUpdateAlert.nib/keyedobjects.nib b/cocoalib/Sparkle.framework/Versions/A/Resources/en.lproj/SUAutomaticUpdateAlert.nib/keyedobjects.nib deleted file mode 100644 index 7630390c89e63ae4aa0af3491cb03518c1321d8a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7148 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 diff --git a/cocoalib/Sparkle.framework/Versions/A/Resources/en.lproj/SUUpdateAlert.nib/classes.nib b/cocoalib/Sparkle.framework/Versions/A/Resources/en.lproj/SUUpdateAlert.nib/classes.nib deleted file mode 100644 index 994d4c36..00000000 --- a/cocoalib/Sparkle.framework/Versions/A/Resources/en.lproj/SUUpdateAlert.nib/classes.nib +++ /dev/null @@ -1,67 +0,0 @@ - - - - - IBClasses - - - CLASS - SUWindowController - LANGUAGE - ObjC - SUPERCLASS - NSWindowController - - - CLASS - NSApplication - LANGUAGE - ObjC - SUPERCLASS - NSResponder - - - ACTIONS - - installUpdate - id - remindMeLater - id - skipThisVersion - id - - CLASS - SUUpdateAlert - LANGUAGE - ObjC - OUTLETS - - delegate - id - description - NSTextField - releaseNotesView - WebView - - SUPERCLASS - SUWindowController - - - CLASS - FirstResponder - LANGUAGE - ObjC - SUPERCLASS - NSObject - - - CLASS - NSObject - LANGUAGE - ObjC - - - IBVersion - 1 - - diff --git a/cocoalib/Sparkle.framework/Versions/A/Resources/en.lproj/SUUpdateAlert.nib/info.nib b/cocoalib/Sparkle.framework/Versions/A/Resources/en.lproj/SUUpdateAlert.nib/info.nib deleted file mode 100644 index 2fb8a837..00000000 --- a/cocoalib/Sparkle.framework/Versions/A/Resources/en.lproj/SUUpdateAlert.nib/info.nib +++ /dev/null @@ -1,20 +0,0 @@ - - - - - IBFramework Version - 670 - IBLastKnownRelativeProjectPath - ../Sparkle.xcodeproj - IBOldestOS - 5 - IBOpenObjects - - 18 - - IBSystem Version - 10A96 - targetFramework - IBCocoaFramework - - diff --git a/cocoalib/Sparkle.framework/Versions/A/Resources/en.lproj/SUUpdateAlert.nib/keyedobjects.nib b/cocoalib/Sparkle.framework/Versions/A/Resources/en.lproj/SUUpdateAlert.nib/keyedobjects.nib deleted file mode 100644 index e7e7497db4bbc8e89d15986ad7531f1b0a07c217..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10623 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 diff --git a/cocoalib/Sparkle.framework/Versions/A/Resources/en.lproj/SUUpdatePermissionPrompt.nib/classes.nib b/cocoalib/Sparkle.framework/Versions/A/Resources/en.lproj/SUUpdatePermissionPrompt.nib/classes.nib deleted file mode 100644 index 5220a221..00000000 --- a/cocoalib/Sparkle.framework/Versions/A/Resources/en.lproj/SUUpdatePermissionPrompt.nib/classes.nib +++ /dev/null @@ -1,59 +0,0 @@ - - - - - IBClasses - - - CLASS - SUWindowController - LANGUAGE - ObjC - SUPERCLASS - NSWindowController - - - ACTIONS - - finishPrompt - id - toggleMoreInfo - id - - CLASS - SUUpdatePermissionPrompt - LANGUAGE - ObjC - OUTLETS - - delegate - id - descriptionTextField - NSTextField - moreInfoButton - NSButton - moreInfoView - NSView - - SUPERCLASS - SUWindowController - - - CLASS - FirstResponder - LANGUAGE - ObjC - SUPERCLASS - NSObject - - - CLASS - NSObject - LANGUAGE - ObjC - - - IBVersion - 1 - - diff --git a/cocoalib/Sparkle.framework/Versions/A/Resources/en.lproj/SUUpdatePermissionPrompt.nib/info.nib b/cocoalib/Sparkle.framework/Versions/A/Resources/en.lproj/SUUpdatePermissionPrompt.nib/info.nib deleted file mode 100644 index b1cd28ed..00000000 --- a/cocoalib/Sparkle.framework/Versions/A/Resources/en.lproj/SUUpdatePermissionPrompt.nib/info.nib +++ /dev/null @@ -1,21 +0,0 @@ - - - - - IBFramework Version - 670 - IBLastKnownRelativeProjectPath - ../Sparkle.xcodeproj - IBOldestOS - 5 - IBOpenObjects - - 6 - 41 - - IBSystem Version - 10A96 - targetFramework - IBCocoaFramework - - diff --git a/cocoalib/Sparkle.framework/Versions/A/Resources/en.lproj/SUUpdatePermissionPrompt.nib/keyedobjects.nib b/cocoalib/Sparkle.framework/Versions/A/Resources/en.lproj/SUUpdatePermissionPrompt.nib/keyedobjects.nib deleted file mode 100644 index e8dc5b88028a8bf491e46d29e25091c39735c921..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13263 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 C5g()g diff --git a/cocoalib/Sparkle.framework/Versions/A/Resources/en.lproj/Sparkle.strings b/cocoalib/Sparkle.framework/Versions/A/Resources/en.lproj/Sparkle.strings deleted file mode 100644 index 16e0787b46dd69d9a9d59133dfdfc683787f5293..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8216 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

B-YH*!3RdDu#}q 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 diff --git a/cocoalib/Sparkle.framework/Versions/Current b/cocoalib/Sparkle.framework/Versions/Current deleted file mode 120000 index 8c7e5a66..00000000 --- a/cocoalib/Sparkle.framework/Versions/Current +++ /dev/null @@ -1 +0,0 @@ -A \ No newline at end of file diff --git a/cocoalib/Utils.h b/cocoalib/Utils.h deleted file mode 100644 index 233e0347..00000000 --- a/cocoalib/Utils.h +++ /dev/null @@ -1,36 +0,0 @@ -/* -Copyright 2015 Hardcoded Software (http://www.hardcoded.net) - -This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -which should be included with this package. The terms are also available at -http://www.gnu.org/licenses/gpl-3.0.html -*/ - -#import - -//Useful shortcuts -#define i2n(i) [NSNumber numberWithInteger:i] -#define n2i(n) [n integerValue] -#define b2n(b) [NSNumber numberWithBool:b] -#define n2b(n) [n boolValue] -#if __LP64__ - #define f2n(d) [NSNumber numberWithDouble:d] - #define n2f(n) [n doubleValue] -#else - #define f2n(f) [NSNumber numberWithFloat:f] - #define n2f(n) [n floatValue] -#endif -#define p2a(p) [Utils indexPath2Array:p] -#define a2p(a) [Utils array2IndexPath:a] -#define fmt(x,...) [NSString stringWithFormat:x,__VA_ARGS__] - -@interface Utils : NSObject -+ (NSArray *)indexSet2Array:(NSIndexSet *)aIndexSet; -+ (NSIndexSet *)array2IndexSet:(NSArray *)numberArray; -+ (NSArray *)indexPath2Array:(NSIndexPath *)aIndexPath; -+ (NSIndexPath *)array2IndexPath:(NSArray *)indexArray; -+ (NSString *)indexPath2String:(NSIndexPath *)aIndexPath; -+ (NSIndexPath *)string2IndexPath:(NSString *)aString; -@end - -void replacePlaceholderInView(NSView *placeholder, NSView *replaceWith); \ No newline at end of file diff --git a/cocoalib/Utils.m b/cocoalib/Utils.m deleted file mode 100644 index 024a0956..00000000 --- a/cocoalib/Utils.m +++ /dev/null @@ -1,92 +0,0 @@ -/* -Copyright 2015 Hardcoded Software (http://www.hardcoded.net) - -This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -which should be included with this package. The terms are also available at -http://www.gnu.org/licenses/gpl-3.0.html -*/ - -#import "Utils.h" -#import - -@implementation Utils -//This is to pass index sets to python as arrays (so it can be converted to native lists) -+ (NSArray *)indexSet2Array:(NSIndexSet *)aIndexSet -{ - NSMutableArray *r = [NSMutableArray array]; - NSInteger i = [aIndexSet firstIndex]; - while (i != NSNotFound) - { - [r addObject:[NSNumber numberWithInteger:i]]; - i = [aIndexSet indexGreaterThanIndex:i]; - } - return r; -} - -// numberArray is an array of NSNumber -+ (NSIndexSet *)array2IndexSet:(NSArray *)numberArray -{ - NSMutableIndexSet *set = [NSMutableIndexSet indexSet]; - NSEnumerator *e = [numberArray objectEnumerator]; - NSNumber *n; - while (n = [e nextObject]) - [set addIndex:n2i(n)]; - return set; -} - -//Changes an NSIndexPath into an NSArray -+ (NSArray *)indexPath2Array:(NSIndexPath *)aIndexPath -{ - NSMutableArray *r = [NSMutableArray array]; - if (!aIndexPath) - return r; - for (int i=0;i<[aIndexPath length];i++) - [r addObject:i2n([aIndexPath indexAtPosition:i])]; - return r; -} - -// Changes a NSArray of numbers into a NSIndexPath -// indexArray must have at least one item -+ (NSIndexPath *)array2IndexPath:(NSArray *)indexArray -{ - if (![indexArray count]) - { - return nil; - } - NSEnumerator *e = [indexArray objectEnumerator]; - NSNumber *n = [e nextObject]; - NSIndexPath *ip = [NSIndexPath indexPathWithIndex:n2i(n)]; - while (n = [e nextObject]) - ip = [ip indexPathByAddingIndex:n2i(n)]; - return ip; -} - -+ (NSString *)indexPath2String:(NSIndexPath *)aIndexPath -{ - NSMutableArray *components = [NSMutableArray array]; - for (int i=0; i<[aIndexPath length]; i++) - [components addObject:i2n([aIndexPath indexAtPosition:i])]; - return [components componentsJoinedByString:@"_"]; -} - -+ (NSIndexPath *)string2IndexPath:(NSString *)aString -{ - if (aString == nil) - { - return nil; - } - NSArray *components = [aString componentsSeparatedByString:@"_"]; - NSMutableArray *indexes = [NSMutableArray array]; - for (int i=0; i<[components count]; i++) - [indexes addObject:i2n([[components objectAtIndex:i] intValue])]; - return [Utils array2IndexPath:indexes]; -} -@end - -void replacePlaceholderInView(NSView *placeholder, NSView *replaceWith) -{ - NSView *parent = [placeholder superview]; - [replaceWith setFrame:[placeholder frame]]; - [replaceWith setAutoresizingMask:[placeholder autoresizingMask]]; - [parent replaceSubview:placeholder with:replaceWith]; -} diff --git a/cocoalib/ValueTransformers.h b/cocoalib/ValueTransformers.h deleted file mode 100644 index e1ef288c..00000000 --- a/cocoalib/ValueTransformers.h +++ /dev/null @@ -1,26 +0,0 @@ -/* -Copyright 2015 Hardcoded Software (http://www.hardcoded.net) - -This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -which should be included with this package. The terms are also available at -http://www.gnu.org/licenses/gpl-3.0.html -*/ - -#import - - -@interface VTIsIntIn : NSValueTransformer -{ - NSIndexSet *ints; - BOOL reverse; -} -- (id)initWithValues:(NSIndexSet *)values; -- (id)initWithValues:(NSIndexSet *)values reverse:(BOOL)doReverse; -@end - -@interface HSVTAdd : NSValueTransformer -{ - int toAdd; -} -- (id)initWithValue:(int)value; -@end diff --git a/cocoalib/ValueTransformers.m b/cocoalib/ValueTransformers.m deleted file mode 100644 index 24428f50..00000000 --- a/cocoalib/ValueTransformers.m +++ /dev/null @@ -1,79 +0,0 @@ -/* -Copyright 2015 Hardcoded Software (http://www.hardcoded.net) - -This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -which should be included with this package. The terms are also available at -http://www.gnu.org/licenses/gpl-3.0.html -*/ - -#import "ValueTransformers.h" -#import "Utils.h" - -@implementation VTIsIntIn -- (id)initWithValues:(NSIndexSet *)values -{ - return [self initWithValues:values reverse:NO]; -} - -- (id)initWithValues:(NSIndexSet *)values reverse:(BOOL)doReverse -{ - self = [super init]; - ints = values; - [ints retain]; - reverse = doReverse; - return self; -} - -- (void)dealloc -{ - [ints release]; - [super dealloc]; -} - -+ (Class)transformedValueClass -{ - return [NSNumber class]; //Boolean -} - -+ (BOOL)allowsReverseTransformation -{ - return NO; -} - -- (id)transformedValue:(id)value -{ - if (value == nil) - return nil; - NSNumber *i = value; - BOOL r = [ints containsIndex:[i intValue]]; - if (reverse) - r = !r; - return [NSNumber numberWithBool:r]; -} -@end - -@implementation HSVTAdd -- (id)initWithValue:(int)value -{ - self = [super init]; - toAdd = value; - return self; -} - -+ (Class)transformedValueClass -{ - return [NSNumber class]; -} - -+ (BOOL)allowsReverseTransformation -{ - return NO; -} - -- (id)transformedValue:(id)value -{ - if (value == nil) - return nil; - return i2n(n2i(value) + toAdd); -} -@end diff --git a/cocoalib/Worker.h b/cocoalib/Worker.h deleted file mode 100644 index 65130ff4..00000000 --- a/cocoalib/Worker.h +++ /dev/null @@ -1,14 +0,0 @@ -#import - -//The worker should work in a separate thread or have it's own mechanism to keep the GUI updated as ProgressController -//provides none. -@protocol Worker -// -1: Indeterminate. nil: Not working. 0-100: Progressing -- (NSNumber *)getJobProgress; -- (NSString *)getJobDesc; -- (void)cancelJob; -/* This might seem a little stupid, but it's the simplest way to get a **sync** call to the python -side after a job. Because the python-side app is not an NSObject subclass, it can't listen to -notifications. */ -- (void)jobCompleted:(NSString *)jobid; -@end \ No newline at end of file diff --git a/cocoalib/cocoa/CocoaProxy.h b/cocoalib/cocoa/CocoaProxy.h deleted file mode 100644 index 0646e9be..00000000 --- a/cocoalib/cocoa/CocoaProxy.h +++ /dev/null @@ -1,34 +0,0 @@ -#import - -@interface CocoaProxy : NSObject -{ - NSAutoreleasePool *currentPool; -} -- (void)openPath:(NSString *)path; -- (void)openURL:(NSString *)url; -- (void)revealPath:(NSString *)path; -- (NSString *)getUTI:(NSString *)path; -- (BOOL)type:(NSString *)type conformsToType:(NSString *)refType; -- (NSString *)getAppdataPath; -- (NSString *)getCachePath; -- (NSString *)getResourcePath; -- (NSString *)systemLang; -- (NSString *)systemShortDateFormat; -- (NSString *)systemNumberDecimalSeparator; -- (NSString *)systemNumberGroupingSeparator; -- (NSString *)systemCurrency; -- (NSString *)bundleIdentifier; -- (NSString *)appVersion; -- (NSString *)osxVersion; -- (NSString *)bundleInfo:(NSString *)key; -- (void)postNotification:(NSString *)name userInfo:(NSDictionary *)userInfo; -- (id)prefValue:(NSString *)prefname; -- (void)setPrefValue:(NSString *)prefname value:(id)value; -- (id)prefValue:(NSString *)prefname inDomain:(NSString *)domain; -- (NSString *)url2path:(NSString *)url; -- (void)createPool; -- (void)destroyPool; -- (void)reportCrash:(NSString *)crashReport withGithubUrl:(NSString *)githubUrl; -- (void)log:(NSString *)s; -- (NSDictionary *)readExifData:(NSString *)imagePath; -@end \ No newline at end of file diff --git a/cocoalib/cocoa/CocoaProxy.m b/cocoalib/cocoa/CocoaProxy.m deleted file mode 100644 index 7051a278..00000000 --- a/cocoalib/cocoa/CocoaProxy.m +++ /dev/null @@ -1,171 +0,0 @@ -#import "CocoaProxy.h" -#import "HSErrorReportWindow.h" - -@implementation CocoaProxy -- (void)openPath:(NSString *)path -{ - [[NSWorkspace sharedWorkspace] openURL:[NSURL fileURLWithPath:path isDirectory:NO]]; -} - -- (void)openURL:(NSString *)url -{ - [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:url]]; -} - -- (void)revealPath:(NSString *)path -{ - [[NSWorkspace sharedWorkspace] selectFile:path inFileViewerRootedAtPath:@""]; -} - -- (NSString *)getUTI:(NSString *)path -{ - NSError *error; - return [[NSWorkspace sharedWorkspace] typeOfFile:path error:&error]; -} - -- (BOOL)type:(NSString *)type conformsToType:(NSString *)refType -{ - return [[NSWorkspace sharedWorkspace] type:type conformsToType:refType]; -} - -- (NSString *)getAppdataPath -{ - return [NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES) objectAtIndex:0]; -} -- (NSString *)getCachePath -{ - return [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex:0]; -} - -- (NSString *)getResourcePath -{ - return [[[NSBundle mainBundle] resourceURL] path]; -} - -- (NSString *)systemLang -{ - return [[NSBundle preferredLocalizationsFromArray:[[NSBundle mainBundle] localizations]] objectAtIndex:0]; -} - -- (NSString *)systemShortDateFormat -{ - [NSDateFormatter setDefaultFormatterBehavior:NSDateFormatterBehavior10_4]; - NSDateFormatter *f = [[NSDateFormatter alloc] init]; - [f setDateStyle:NSDateFormatterShortStyle]; - [f setTimeStyle:NSDateFormatterNoStyle]; - NSString *result = [[f dateFormat] retain]; - [f release]; - return [result autorelease]; -} - -- (NSString *)systemNumberDecimalSeparator -{ - [NSNumberFormatter setDefaultFormatterBehavior:NSNumberFormatterBehavior10_4]; - NSNumberFormatter *f = [[NSNumberFormatter alloc] init]; - NSString *result = [[f decimalSeparator] retain]; - [f release]; - return [result autorelease]; -} - -- (NSString *)systemNumberGroupingSeparator -{ - [NSNumberFormatter setDefaultFormatterBehavior:NSNumberFormatterBehavior10_4]; - NSNumberFormatter *f = [[NSNumberFormatter alloc] init]; - NSString *result = [[f groupingSeparator] retain]; - [f release]; - return [result autorelease]; -} - -- (NSString *)systemCurrency -{ - return [[NSLocale currentLocale] objectForKey:NSLocaleCurrencyCode]; -} - -- (NSString *)bundleIdentifier -{ - return [[NSBundle mainBundle] bundleIdentifier]; -} - -- (NSString *)appVersion -{ - return [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"]; -} - -- (NSString *)bundleInfo:(NSString *)key -{ - return [[NSBundle mainBundle] objectForInfoDictionaryKey:key]; -} - -- (NSString *)osxVersion -{ - return [[NSProcessInfo processInfo] operatingSystemVersionString]; -} - -- (void)postNotification:(NSString *)name userInfo:(NSDictionary *)userInfo -{ - [[NSNotificationCenter defaultCenter] postNotificationName:name object:nil userInfo:userInfo]; -} - -- (id)prefValue:(NSString *)prefname -{ - return [[NSUserDefaults standardUserDefaults] objectForKey:prefname]; -} - -- (void)setPrefValue:(NSString *)prefname value:(id)value -{ - [[NSUserDefaults standardUserDefaults] setObject:value forKey:prefname]; -} - -- (id)prefValue:(NSString *)prefname inDomain:(NSString *)domain -{ - NSDictionary *dict = [[NSUserDefaults standardUserDefaults] persistentDomainForName:domain]; - return [dict objectForKey:prefname]; -} - -// Changes a file:/// path into a normal path -- (NSString *)url2path:(NSString *)url -{ - NSURL *u = [NSURL URLWithString:url]; - return [u path]; -} - -// Create a pool for use into a separate thread. -- (void)createPool -{ - [self destroyPool]; - currentPool = [[NSAutoreleasePool alloc] init]; -} -- (void)destroyPool -{ - if (currentPool != nil) { - [currentPool release]; - currentPool = nil; - } -} - -- (void)reportCrash:(NSString *)crashReport withGithubUrl:(NSString *)githubUrl -{ - return [HSErrorReportWindow showErrorReportWithContent:crashReport githubUrl:githubUrl]; -} - -- (void)log:(NSString *)s -{ - NSLog(@"%@", s); -} - -- (NSDictionary *)readExifData:(NSString *)imagePath -{ - NSDictionary *result = nil; - NSURL* url = [NSURL fileURLWithPath:imagePath]; - CGImageSourceRef source = CGImageSourceCreateWithURL((CFURLRef)url, nil); - if (source != nil) { - CFDictionaryRef metadataRef = CGImageSourceCopyPropertiesAtIndex (source, 0, nil); - if (metadataRef != nil) { - result = [NSDictionary dictionaryWithDictionary:(NSDictionary *)metadataRef]; - CFRelease(metadataRef); - } - CFRelease(source); - } - return result; -} -@end \ No newline at end of file diff --git a/cocoalib/cocoa/__init__.py b/cocoalib/cocoa/__init__.py deleted file mode 100644 index 5a23bf2f..00000000 --- a/cocoalib/cocoa/__init__.py +++ /dev/null @@ -1,118 +0,0 @@ -# Created By: Virgil Dupras -# Created On: 2007-10-06 -# Copyright 2015 Hardcoded Software (http://www.hardcoded.net) - -# This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at -# http://www.gnu.org/licenses/gpl-3.0.html - -import logging -import time -import traceback -import sys - -from .CocoaProxy import CocoaProxy - -proxy = CocoaProxy() - -def autoreleasepool(func): - def wrapper(*args, **kwargs): - proxy.createPool() - try: - func(*args, **kwargs) - finally: - proxy.destroyPool() - return wrapper - -def as_fetch(as_list, as_type, step_size=1000): - """When fetching items from a very big list through applescript, the connection with the app - will timeout. This function is to circumvent that. 'as_type' is the type of the items in the - list (found in appscript.k). If we don't pass it to the 'each' arg of 'count()', it doesn't work. - applescript is rather stupid...""" - result = [] - # no timeout. default timeout is 60 secs, and it is reached for libs > 30k songs - item_count = as_list.count(each=as_type, timeout=0) - steps = item_count // step_size - if item_count % step_size: - steps += 1 - logging.info('Fetching %d items in %d steps' % (item_count, steps)) - # Don't forget that the indexes are 1-based and that the upper limit is included - for step in range(steps): - begin = step * step_size + 1 - end = min(item_count, begin + step_size - 1) - if end > begin: - result += as_list[begin:end](timeout=0) - else: # When there is only one item, the stupid fuck gives it directly instead of putting it in a list. - result.append(as_list[begin:end](timeout=0)) - time.sleep(.1) - logging.info('%d items fetched' % len(result)) - return result - -def extract_tb_noline(tb): - # Same as traceback.extract_tb(), but without line fetching - limit = 100 - list = [] - n = 0 - while tb is not None and (limit is None or n < limit): - f = tb.tb_frame - lineno = tb.tb_lineno - co = f.f_code - filename = co.co_filename - name = co.co_name - list.append((filename, lineno, name, None)) - tb = tb.tb_next - n = n+1 - return list - -def safe_format_exception(type, value, tb): - """Format exception from type, value and tb and fallback if there's a problem. - - In some cases in threaded exceptions under Cocoa, I get tracebacks targeting pyc files instead - of py files, which results in traceback.format_exception() trying to print lines from pyc files - and then crashing when trying to interpret that binary data as utf-8. We want a fallback in - these cases. - """ - try: - return traceback.format_exception(type, value, tb) - except Exception: - result = ['Traceback (most recent call last):\n'] - result.extend(traceback.format_list(extract_tb_noline(tb))) - result.extend(traceback.format_exception_only(type, value)) - return result - -def install_exception_hook(github_url): - def report_crash(type, value, tb): - app_identifier = proxy.bundleIdentifier() - app_version = proxy.appVersion() - osx_version = proxy.osxVersion() - s = "Application Identifier: {}\n".format(app_identifier) - s += "Application Version: {}\n".format(app_version) - s += "Mac OS X Version: {}\n\n".format(osx_version) - s += ''.join(safe_format_exception(type, value, tb)) - if LOG_BUFFER: - s += '\nRelevant Console logs:\n\n' - s += '\n'.join(LOG_BUFFER) - proxy.reportCrash_withGithubUrl_(s, github_url) - - sys.excepthook = report_crash - -# A global log buffer to use for error reports -LOG_BUFFER = [] - -class CocoaHandler(logging.Handler): - def emit(self, record): - msg = record.getMessage() - proxy.log_(msg) - LOG_BUFFER.append(msg) - del LOG_BUFFER[:-20] - -def install_cocoa_logger(): - logging.getLogger().addHandler(CocoaHandler()) - -def patch_threaded_job_performer(): - # _async_run, under cocoa, has to be run within an autorelease pool to prevent leaks. - # You only need this patch is you use one of CocoaProxy's function (which allocate objc - # structures) inside a threaded job. - from hscommon.jobprogress.performer import ThreadedJobPerformer - ThreadedJobPerformer._async_run = autoreleasepool(ThreadedJobPerformer._async_run) - diff --git a/cocoalib/cocoa/inter.py b/cocoalib/cocoa/inter.py deleted file mode 100644 index 01d7e77e..00000000 --- a/cocoalib/cocoa/inter.py +++ /dev/null @@ -1,300 +0,0 @@ -import logging -from objp.util import pyref, dontwrap -from . import proxy - -class GUIObjectView: - def refresh(self): pass - -class PyGUIObject: - def __init__(self, model: pyref): - self.model = model - self.callback = None - - # This *has* to be called right after initialization. - def bindCallback_(self, callback: pyref): - self.callback = callback - self.model.view = self - - # Call this before the ObjC callback is deallocated to avoid calls to that deallocated instance. - def free(self): - self.model.view = None - self.callback = None - - def modelRef(self) -> pyref: - return self.model - - #--- Python -> Cocoa - @dontwrap - def refresh(self): - self.callback.refresh() - -class PyTextField(PyGUIObject): - def text(self) -> str: - return self.model.text - - def setText_(self, newtext: str): - self.model.text = newtext - - -class SelectableListView(GUIObjectView): - def updateSelection(self): pass - -class PySelectableList(PyGUIObject): - def items(self) -> list: - # Should normally always return strings - return self.model[:] - - def selectIndex_(self, index: int): - self.model.select(index) - - def selectedIndex(self) -> int: - result = self.model.selected_index - if result is None: - result = -1 - return result - - def selectedIndexes(self) -> list: - return self.model.selected_indexes - - def selectIndexes_(self, indexes: list): - self.model.select(indexes) - - def searchByPrefix_(self, prefix: str) -> int: - return self.model.search_by_prefix(prefix) - - #--- model --> view - @dontwrap - def update_selection(self): - self.callback.updateSelection() - -class ColumnsView: - def restoreColumns(self): pass - def setColumn_visible_(self, colname: str, visible: bool): pass - -class PyColumns(PyGUIObject): - def columnNamesInOrder(self) -> list: - return self.model.colnames - - def columnDisplay_(self, colname: str) -> str: - return self.model.column_display(colname) - - def columnIsVisible_(self, colname: str) -> bool: - return self.model.column_is_visible(colname) - - def columnWidth_(self, colname: str) -> int: - return self.model.column_width(colname) - - def moveColumn_toIndex_(self, colname: str, index: int): - self.model.move_column(colname, index) - - def resizeColumn_toWidth_(self, colname: str, newwidth: int): - self.model.resize_column(colname, newwidth) - - def setColumn_defaultWidth_(self, colname: str, width: int): - self.model.set_default_width(colname, width) - - def menuItems(self) -> list: - return self.model.menu_items() - - def toggleMenuItem_(self, index: int) -> bool: - return self.model.toggle_menu_item(index) - - def resetToDefaults(self): - self.model.reset_to_defaults() - - #--- Python --> Cocoa - @dontwrap - def restore_columns(self): - self.callback.restoreColumns() - - @dontwrap - def set_column_visible(self, colname: str, visible): - self.callback.setColumn_visible_(colname, visible) - -class OutlineView(GUIObjectView): - def startEditing(self): pass - def stopEditing(self): pass - def updateSelection(self): pass - -class PyOutline(PyGUIObject): - def cancelEdits(self): - self.model.cancel_edits() - - def canEditProperty_atPath_(self, property: str, path: list) -> bool: - node = self.model.get_node(path) - assert node is self.model.selected_node - return getattr(node, 'can_edit_' + property, False) - - def saveEdits(self): - self.model.save_edits() - - def selectedPath(self) -> list: - return self.model.selected_path - - def setSelectedPath_(self, path: list): - self.model.selected_path = path - - def selectedPaths(self) -> list: - return self.model.selected_paths - - def setSelectedPaths_(self, paths: list): - self.model.selected_paths = paths - - def property_valueAtPath_(self, property: str, path: list) -> object: - try: - return getattr(self.model.get_node(path), property) - except IndexError: - logging.warning("%r doesn't have a node at path %r", self.model, path) - return '' - - def setProperty_value_atPath_(self, property: str, value: object, path: list): - setattr(self.model.get_node(path), property, value) - - #--- Python -> Cocoa - @dontwrap - def start_editing(self): - self.callback.startEditing() - - @dontwrap - def stop_editing(self): - self.callback.stopEditing() - - @dontwrap - def update_selection(self): - self.callback.updateSelection() - -class TableView(GUIObjectView): - def showSelectedRow(self): pass - def startEditing(self): pass - def stopEditing(self): pass - def updateSelection(self): pass - -class PyTable(PyGUIObject): - #--- Helpers - @dontwrap - def _getrow(self, row): - try: - return self.model[row] - except IndexError: - msg = "Trying to get an out of bounds row ({} / {}) on table {}" - logging.warning(msg.format(row, len(self.model), self.model.__class__.__name__)) - - #--- Cocoa --> Python - def columns(self) -> pyref: - return self.model.columns - - def add(self): - self.model.add() - - def cancelEdits(self): - self.model.cancel_edits() - - def canEditColumn_atRow_(self, column: str, row: int) -> object: - return self.model.can_edit_cell(column, row) - - def deleteSelectedRows(self): - self.model.delete() - - def numberOfRows(self) -> int: - return len(self.model) - - def saveEdits(self): - self.model.save_edits() - - def selectRows_(self, rows: list): - self.model.select(list(rows)) - - def selectedRows(self) -> list: - return self.model.selected_indexes - - def selectionAsCSV(self) -> str: - return self.model.selection_as_csv() - - def setValue_forColumn_row_(self, value: object, column: str, row: int): - # this try except is important for the case while a row is in edition mode and the delete - # button is clicked. - try: - self._getrow(row).set_cell_value(column, value) - except AttributeError: - msg = "Trying to set an attribute that can't: {} with value {} at row {} on table {}" - logging.warning(msg.format(column, value, row, self.model.__class__.__name__)) - raise - - def sortByColumn_desc_(self, column: str, desc: bool): - self.model.sort_by(column, desc=desc) - - def valueForColumn_row_(self, column: str, row: int) -> object: - return self._getrow(row).get_cell_value(column) - - #--- Python -> Cocoa - @dontwrap - def show_selected_row(self): - self.callback.showSelectedRow() - - @dontwrap - def start_editing(self): - self.callback.startEditing() - - @dontwrap - def stop_editing(self): - self.callback.stopEditing() - - @dontwrap - def update_selection(self): - self.callback.updateSelection() - -class ProgressWindowView(GUIObjectView): - def setProgress_(self, progress: int): pass - def showWindow(self): pass - def closeWindow(self): pass - -class PyProgressWindow(PyGUIObject): - def jobdescTextField(self) -> pyref: - return self.model.jobdesc_textfield - - def progressdescTextField(self) -> pyref: - return self.model.progressdesc_textfield - - def pulse(self): - self.model.pulse() - - def cancel(self): - self.model.cancel() - - #--- Python -> Cocoa - @dontwrap - def set_progress(self, last_progress): - self.callback.setProgress_(last_progress) - - @dontwrap - def show(self): - self.callback.showWindow() - - @dontwrap - def close(self): - self.callback.closeWindow() - - -class BaseAppView: - def showMessage_(self, msg: str): pass - -class PyBaseApp(PyGUIObject): - def appName(self) -> str: - return self.model.PROMPT_NAME - - def appLongName(self) -> str: - return self.model.NAME - - #--- Python --> Cocoa - @dontwrap - def get_default(self, key_name): - return proxy.prefValue_(key_name) - - @dontwrap - def set_default(self, key_name, value): - proxy.setPrefValue_value_(key_name, value) - - @dontwrap - def show_message(self, msg): - self.callback.showMessage_(msg) - diff --git a/cocoalib/controllers/HSColumns.h b/cocoalib/controllers/HSColumns.h deleted file mode 100644 index 10f5e39e..00000000 --- a/cocoalib/controllers/HSColumns.h +++ /dev/null @@ -1,38 +0,0 @@ -/* -Copyright 2015 Hardcoded Software (http://www.hardcoded.net) - -This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -which should be included with this package. The terms are also available at -http://www.gnu.org/licenses/gpl-3.0.html -*/ - -#import -#import -#import "HSGUIController.h" -#import "PyColumns.h" - -/* - This structure is to define constants describing table columns (it's easier to maintain in code - than in XIB files). -*/ -typedef struct { - NSString *attrname; - NSUInteger defaultWidth; - NSUInteger minWidth; - NSUInteger maxWidth; - BOOL sortable; - Class cellClass; -} HSColumnDef; - -@interface HSColumns : HSGUIController {} -- (id)initWithPyRef:(PyObject *)aPyRef tableView:(NSTableView *)aTableView; -- (PyColumns *)model; -- (NSTableView *)view; -- (void)connectNotifications; -- (void)disconnectNotifications; -- (void)initializeColumns:(HSColumnDef *)columns; -- (void)initializeColumnsFromModel:(HSColumnDef)columnModel; -- (void)setColumnsAsReadOnly; -- (void)restoreColumns; -- (void)setColumn:(NSString *)colname visible:(BOOL)visible; -@end \ No newline at end of file diff --git a/cocoalib/controllers/HSColumns.m b/cocoalib/controllers/HSColumns.m deleted file mode 100644 index 58d8b325..00000000 --- a/cocoalib/controllers/HSColumns.m +++ /dev/null @@ -1,198 +0,0 @@ -/* -Copyright 2015 Hardcoded Software (http://www.hardcoded.net) - -This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -which should be included with this package. The terms are also available at -http://www.gnu.org/licenses/gpl-3.0.html -*/ - -#import "HSColumns.h" -#import "Utils.h" -#import "HSTableView.h" // To prevent warning on stopEditing - -@implementation HSColumns -- (id)initWithPyRef:(PyObject *)aPyRef tableView:(NSTableView *)aTableView -{ - self = [super initWithPyRef:aPyRef wrapperClass:[PyColumns class] - callbackClassName:@"ColumnsView" view:aTableView]; - [self connectNotifications]; - return self; -} - -- (void)dealloc -{ - [self disconnectNotifications]; - [super dealloc]; -} - -- (PyColumns *)model -{ - return (PyColumns *)model; -} - -- (NSTableView *)view -{ - return (NSTableView *)view; -} - -- (void)connectNotifications -{ - if ([self view] == nil) { - /* This can happen if there something broken somewhere, and even though when that happens, - it means that something serious is going on, the fact that we connect to all columnMoved: - events messes thigs up even MORE. Don't connect when tableView is nil! - */ - return; - } - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(columnMoved:) - name:NSTableViewColumnDidMoveNotification object:[self view]]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(columnMoved:) - name:NSOutlineViewColumnDidMoveNotification object:[self view]]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(columnResized:) - name:NSTableViewColumnDidResizeNotification object:[self view]]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(columnResized:) - name:NSOutlineViewColumnDidResizeNotification object:[self view]]; -} - -- (void)disconnectNotifications -{ - [[NSNotificationCenter defaultCenter] removeObserver:self]; -} - -/* - It is assumed, when this method is used, that the table/outline is empty *OR* that it is not - defined in the column list. - - Special note about NSOutlineView. You can use HSColumns on outline views, but you be aware that - the "main" column (the one having the tree disclosure buttons) cannot be removed. Therefore, - it has to be defined in the XIB and it must *not* be in column defs. -*/ -- (void)initializeColumns:(HSColumnDef *)columns -{ - /* We don't want default widths to overwrite stored with in the core code */ - [self disconnectNotifications]; - /* Translate the title of columns (needed for outlines) present already */ - for (NSTableColumn *c in [[self view] tableColumns]) { - NSString *title = NSLocalizedStringFromTable([[c headerCell] stringValue], @"columns", @""); - [[c headerCell] setStringValue:title]; - } - NSUserDefaultsController *udc = [NSUserDefaultsController sharedUserDefaultsController]; - HSColumnDef *cdef = columns; - while (cdef->attrname != nil) { - if ([[self view] tableColumnWithIdentifier:cdef->attrname] != nil) { - cdef++; - continue; - } - NSTableColumn *c = [[[NSTableColumn alloc] initWithIdentifier:cdef->attrname] autorelease]; - [c setResizingMask:NSTableColumnUserResizingMask]; - /* If the column is not added right away, it causes glitches under 10.5 (minwidths instead of default widths) */ - [[self view] addTableColumn:c]; - NSString *title = [[self model] columnDisplay:cdef->attrname]; - [[c headerCell] setStringValue:title]; - if (cdef->sortable) { - NSSortDescriptor *d = [[[NSSortDescriptor alloc] initWithKey:cdef->attrname ascending:YES] autorelease]; - [c setSortDescriptorPrototype:d]; - } - [c setWidth:cdef->defaultWidth]; - [[self model] setColumn:cdef->attrname defaultWidth:cdef->defaultWidth]; - [c setMinWidth:cdef->minWidth]; - NSUInteger maxWidth = cdef->maxWidth; - if (maxWidth == 0) { - maxWidth = 0xffffff; - } - [c setMaxWidth:maxWidth]; - if (cdef->cellClass != nil) { - id cell = [[[cdef->cellClass alloc] initTextCell:@""] autorelease]; - [cell setEditable:YES]; - [c setDataCell:cell]; - } - [c bind:@"fontSize" toObject:udc withKeyPath:@"values.TableFontSize" options:nil]; - cdef++; - } - [self connectNotifications]; -} - -/* - Here, instead of having all our column defs, we have one column model, which we use to create - our column defs using column names in [[self model] columnNamesInOrder]. -*/ -- (void)initializeColumnsFromModel:(HSColumnDef)columnModel -{ - NSArray *colnames = [[self model] columnNamesInOrder]; - HSColumnDef *defs = (HSColumnDef *)malloc(([colnames count]+1)*sizeof(HSColumnDef)); - HSColumnDef *def = defs; - for (NSString *colname in colnames) { - def->attrname = colname; - def->defaultWidth = columnModel.defaultWidth; - def->minWidth = columnModel.minWidth; - def->maxWidth = columnModel.maxWidth; - def->sortable = columnModel.sortable; - def->cellClass = columnModel.cellClass; - def++; - } - def->attrname = nil; // Sentinel - [self initializeColumns:defs]; - free(defs); -} - -- (void)setColumnsAsReadOnly -{ - for (NSTableColumn *col in [[self view] tableColumns]) { - [col setEditable:NO]; - } -} - -/* Notifications */ -- (void)columnMoved:(NSNotification *)notification -{ - /* We only get this call after the move. Although there's "NSOldColumn" and "NSNewColumn", - the old index is irrelevant since we have to find the moved column's name. - */ - NSInteger index = n2i([[notification userInfo] objectForKey:@"NSNewColumn"]); - NSTableColumn *c = [[[self view] tableColumns] objectAtIndex:index]; - NSString *colName = [c identifier]; - [[self model] moveColumn:colName toIndex:index]; -} - -- (void)columnResized:(NSNotification *)notification -{ - NSTableColumn *c = [[notification userInfo] objectForKey:@"NSTableColumn"]; - [[self model] resizeColumn:[c identifier] toWidth:[c width]]; -} - -/* Python --> Cocoa */ -- (void)restoreColumns -{ - [self disconnectNotifications]; - NSArray *columnOrder = [[self model] columnNamesInOrder]; - for (NSInteger i=0; i<[columnOrder count]; i++) { - NSString *colName = [columnOrder objectAtIndex:i]; - NSInteger index = [[self view] columnWithIdentifier:colName]; - if ((index != -1) && (index != i)) { - [[self view] moveColumn:index toColumn:i]; - } - } - for (NSTableColumn *c in [[self view] tableColumns]) { - NSInteger width = [[self model] columnWidth:[c identifier]]; - if (width > 0) { - [c setWidth:width]; - } - BOOL isVisible = [[self model] columnIsVisible:[c identifier]]; - [c setHidden:!isVisible]; - } - [self connectNotifications]; -} - -- (void)setColumn:(NSString *)colname visible:(BOOL)visible -{ - NSTableColumn *col = [[self view] tableColumnWithIdentifier:colname]; - if (col == nil) - return; - if ([col isHidden] == !visible) - return; - if ([[self view] respondsToSelector:@selector(stopEditing)]) { - [(id)[self view] stopEditing]; - } - [col setHidden:!visible]; -} -@end diff --git a/cocoalib/controllers/HSComboBox.h b/cocoalib/controllers/HSComboBox.h deleted file mode 100644 index 57c31141..00000000 --- a/cocoalib/controllers/HSComboBox.h +++ /dev/null @@ -1,25 +0,0 @@ -/* -Copyright 2015 Hardcoded Software (http://www.hardcoded.net) - -This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -which should be included with this package. The terms are also available at -http://www.gnu.org/licenses/gpl-3.0.html -*/ - -#import -#import "HSGUIController.h" -#import "PySelectableList.h" - -@interface HSComboBox : HSGUIController -{ - NSArray *items; -} -- (id)initWithPyRef:(PyObject *)aPyRef view:(NSComboBox *)aView; -- (NSComboBox *)view; -- (void)setView:(NSComboBox *)aComboboxView; -- (PySelectableList *)model; - -- (void)comboboxViewSelectionChanged; -- (void)refresh; -- (void)updateSelection; -@end \ No newline at end of file diff --git a/cocoalib/controllers/HSComboBox.m b/cocoalib/controllers/HSComboBox.m deleted file mode 100644 index 5e8dea9c..00000000 --- a/cocoalib/controllers/HSComboBox.m +++ /dev/null @@ -1,119 +0,0 @@ -/* -Copyright 2015 Hardcoded Software (http://www.hardcoded.net) - -This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -which should be included with this package. The terms are also available at -http://www.gnu.org/licenses/gpl-3.0.html -*/ - -#import "HSComboBox.h" -#import "HSPyUtil.h" - -@implementation HSComboBox -- (id)initWithPyRef:(PyObject *)aPyRef view:(NSComboBox *)aView -{ - PySelectableList *m = [[PySelectableList alloc] initWithModel:aPyRef]; - self = [super initWithModel:m]; - [m bindCallback:createCallback(@"SelectableListView", self)]; - [m release]; - [self setView:aView]; - return self; -} - -- (void)dealloc -{ - [[self view] setTarget:nil]; - [[self view] setDataSource:nil]; - [items release]; - [super dealloc]; -} - -- (NSComboBox *)view -{ - return (NSComboBox *)view; -} - -- (void)setView:(NSComboBox *)aComboboxView -{ - if ([self view] != nil) { - [[self view] setDataSource:nil]; - [[self view] setTarget:nil]; - } - [super setView:aComboboxView]; - if (aComboboxView != nil) { - [aComboboxView setUsesDataSource:YES]; - [aComboboxView setDataSource:self]; - [aComboboxView setAction:@selector(comboboxViewSelectionChanged)]; - [aComboboxView setTarget:self]; - /* This is required for the combobox to send its action whenever it's changed. Normally, it's - already set, but then the combobox is created programmatically (xibless), it's not. We - make sure it is here. - */ - [[aComboboxView cell] setSendsActionOnEndEditing:YES]; - [self refresh]; - } -} - -- (PySelectableList *)model -{ - return (PySelectableList *)model; -} - -- (void)comboboxViewSelectionChanged -{ - NSInteger index = [[self view] indexOfSelectedItem]; - if (index >= 0) { - [[self model] selectIndex:index]; - } -} - -/* data source */ -- (id)comboBox:(NSComboBox *)aComboBox objectValueForItemAtIndex:(NSInteger)index -{ - if (index < 0) { - return nil; - } - return [items objectAtIndex:index]; -} - -- (NSInteger)numberOfItemsInComboBox:(NSComboBox *)aComboBox -{ - return [items count]; -} - -- (NSUInteger)comboBox:(NSComboBox *)aComboBox indexOfItemWithStringValue:(NSString *)aString -{ - NSInteger index = [[self model] searchByPrefix:aString]; - if (index >= 0) { - return index; - } - else { - return NSNotFound; - } -} - -- (NSString *)comboBox:(NSComboBox *)aComboBox completedString:(NSString *)uncompletedString -{ - NSInteger index = [[self model] searchByPrefix:uncompletedString]; - if (index >= 0) { - return [items objectAtIndex:index]; - } - else { - return nil; - } -} - -/* model --> view */ -- (void)refresh -{ - [items release]; - items = [[[self model] items] retain]; - [[self view] reloadData]; - [self updateSelection]; -} - -- (void)updateSelection -{ - [[self view] selectItemAtIndex:[[self model] selectedIndex]]; -} -@end \ No newline at end of file diff --git a/cocoalib/controllers/HSGUIController.h b/cocoalib/controllers/HSGUIController.h deleted file mode 100644 index 5042ce15..00000000 --- a/cocoalib/controllers/HSGUIController.h +++ /dev/null @@ -1,23 +0,0 @@ -/* -Copyright 2015 Hardcoded Software (http://www.hardcoded.net) - -This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -which should be included with this package. The terms are also available at -http://www.gnu.org/licenses/gpl-3.0.html -*/ - -#import -#import "PyGUIObject.h" - -@interface HSGUIController : NSObject -{ - PyGUIObject *model; - NSView *view; -} -- (id)initWithModel:(PyGUIObject *)aPy; -- (id)initWithModel:(PyGUIObject *)aPy view:(NSView *)aView; -- (id)initWithPyRef:(PyObject *)aPyRef wrapperClass:(Class)aWrapperClass callbackClassName:(NSString *)aCallbackClassName view:(NSView *)aView; -- (PyGUIObject *)model; -- (NSView *)view; -- (void)setView:(NSView *)aView; -@end diff --git a/cocoalib/controllers/HSGUIController.m b/cocoalib/controllers/HSGUIController.m deleted file mode 100644 index b7658e2a..00000000 --- a/cocoalib/controllers/HSGUIController.m +++ /dev/null @@ -1,62 +0,0 @@ -/* -Copyright 2015 Hardcoded Software (http://www.hardcoded.net) - -This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -which should be included with this package. The terms are also available at -http://www.gnu.org/licenses/gpl-3.0.html -*/ - -#import "HSGUIController.h" -#import "HSPyUtil.h" - -@implementation HSGUIController -- (id)initWithModel:(PyGUIObject *)aModel -{ - self = [super init]; - model = [aModel retain]; - view = nil; - return self; -} - -- (id)initWithModel:(PyGUIObject *)aModel view:(NSView *)aView -{ - self = [super init]; - model = [aModel retain]; - [self setView:aView]; - return self; -} - -- (id)initWithPyRef:(PyObject *)aPyRef wrapperClass:(Class)aWrapperClass callbackClassName:(NSString *)aCallbackClassName view:(NSView *)aView -{ - PyGUIObject *m = [[aWrapperClass alloc] initWithModel:aPyRef]; - self = [self initWithModel:m view:aView]; - [m bindCallback:createCallback(aCallbackClassName, self)]; - [m release]; - return self; -} - -- (void)dealloc -{ - // NSLog([NSString stringWithFormat:@"%@ dealloc",[[self class] description]]); - [self setView:nil]; - [model free]; - [model release]; - [super dealloc]; -} - -- (PyGUIObject *)model -{ - return model; -} - -- (NSView *)view -{ - return view; -} - -- (void)setView:(NSView *)aView -{ - [view release]; - view = [aView retain]; -} -@end diff --git a/cocoalib/controllers/HSOutline.h b/cocoalib/controllers/HSOutline.h deleted file mode 100644 index b7f54f83..00000000 --- a/cocoalib/controllers/HSOutline.h +++ /dev/null @@ -1,44 +0,0 @@ -/* -Copyright 2015 Hardcoded Software (http://www.hardcoded.net) - -This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -which should be included with this package. The terms are also available at -http://www.gnu.org/licenses/gpl-3.0.html -*/ - -#import -#import "HSGUIController.h" -#import "HSOutlineView.h" -#import "PyOutline.h" -#import "NSIndexPathAdditions.h" - -@interface HSOutline : HSGUIController { - NSMutableDictionary *itemData; - NSMutableSet *itemRetainer; -} -- (id)initWithPyRef:(PyObject *)aPyRef wrapperClass:(Class)aWrapperClass callbackClassName:(NSString *)aCallbackClassName view:(HSOutlineView *)aView; -- (PyOutline *)model; -- (HSOutlineView *)view; - -/* Public */ -- (void)refresh; -- (NSIndexPath *)selectedIndexPath; -- (NSArray *)selectedIndexPaths; -- (NSString *)dataForCopyToPasteboard; -- (void)startEditing; -- (void)stopEditing; -- (void)updateSelection; -- (void)expandItem:(NSIndexPath *)item; -- (NSIndexPath *)internalizedPath:(NSIndexPath *)path; - -/* Caching */ -- (id)property:(NSString *)property valueAtPath:(NSIndexPath *)path; -- (void)setProperty:(NSString *)property value:(id)value atPath:(NSIndexPath *)path; -- (NSString *)stringProperty:(NSString *)property valueAtPath:(NSIndexPath *)path; -- (void)setStringProperty:(NSString *)property value:(NSString *)value atPath:(NSIndexPath *)path; -- (BOOL)boolProperty:(NSString *)property valueAtPath:(NSIndexPath *)path; -- (void)setBoolProperty:(NSString *)property value:(BOOL)value atPath:(NSIndexPath *)path; -- (NSInteger)intProperty:(NSString *)property valueAtPath:(NSIndexPath *)path; -- (void)setIntProperty:(NSString *)property value:(int)value atPath:(NSIndexPath *)path; -- (void)refreshItemAtPath:(NSIndexPath *)path; -@end \ No newline at end of file diff --git a/cocoalib/controllers/HSOutline.m b/cocoalib/controllers/HSOutline.m deleted file mode 100644 index 5bb32c15..00000000 --- a/cocoalib/controllers/HSOutline.m +++ /dev/null @@ -1,286 +0,0 @@ -/* -Copyright 2015 Hardcoded Software (http://www.hardcoded.net) - -This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -which should be included with this package. The terms are also available at -http://www.gnu.org/licenses/gpl-3.0.html -*/ - -#import "HSOutline.h" -#import "Utils.h" - -#define CHILDREN_COUNT_PROPERTY @"children_count" - -@implementation HSOutline -- (id)initWithPyRef:(PyObject *)aPyRef wrapperClass:(Class)aWrapperClass callbackClassName:(NSString *)aCallbackClassName view:(HSOutlineView *)aView -{ - self = [super initWithPyRef:aPyRef wrapperClass:aWrapperClass callbackClassName:aCallbackClassName view:aView]; - itemData = [[NSMutableDictionary dictionary] retain]; - /* Dictionaries don't retain its keys because it copies them. Our items are NSIndexPath and when - an index path has the same value, it's the same instance. Before OS X 10.7, all these instances - stayed in memory, so we didn't need to worry about retaining them. Hoever, it seems now that - index path instances are sometimes released. Oops. So, we now need to retain our index path - instances and that's why we use itemRetainer. - - In fact, it seems that unlike what the doc says, it's not true that two index paths with the - same value will always be the same instance. - */ - itemRetainer = [[NSMutableSet set] retain]; - if (([[self view] outlineTableColumn] == nil) && ([[[self view] tableColumns] count] > 0)) { - [[self view] setOutlineTableColumn:[[[self view] tableColumns] objectAtIndex:0]]; - } - return self; -} - -- (void)dealloc -{ - [itemData release]; - [itemRetainer release]; - [super dealloc]; -} - -- (HSOutlineView *)view -{ - return (HSOutlineView *)view; -} - -- (void)setView:(HSOutlineView *)aOutlineView -{ - if ([self view] != nil) { - [[self view] setDataSource:nil]; - [[self view] setDelegate:nil]; - } - [super setView:aOutlineView]; - if (aOutlineView != nil) { - [aOutlineView setDataSource:self]; - [aOutlineView setDelegate:self]; - } -} - -- (PyOutline *)model -{ - return (PyOutline *)model; -} - -/* Private */ -- (void)setPySelection -{ - NSMutableArray *paths = [NSMutableArray array]; - NSIndexSet *indexes = [[self view] selectedRowIndexes]; - NSInteger i = [indexes firstIndex]; - while (i != NSNotFound) { - NSIndexPath *path = [[self view] itemAtRow:i]; - [paths addObject:p2a(path)]; - i = [indexes indexGreaterThanIndex:i]; - } - [[self model] setSelectedPaths:paths]; -} - -- (NSIndexPath *)internalizedPath:(NSIndexPath *)path -{ - /* Because NSIndexPath stopped guaranteeing that the same paths always were represented by the - same instances, we have to make sure, when we manipulate paths, that we manipulate the same - instances as those that were given by outlineView:child:ofItem: - */ - NSIndexPath *result = [itemRetainer member:path]; - if (result == nil) { - result = path; - [itemData setObject:[NSMutableDictionary dictionary] forKey:result]; - [itemRetainer addObject:result]; - } - return result; -} - -/* Public */ -- (void)refresh -{ - [itemData removeAllObjects]; - // We can't get rid of our instances just yet, we have to wait until after reloadData - NSSet *oldRetainer = itemRetainer; - itemRetainer = [[NSMutableSet set] retain]; - [[self view] setDelegate:nil]; - [[self view] reloadData]; - [[self view] setDelegate:self]; - /* Item retainer and releasing - - In theory, [oldRetainer release] should work, but in practice, doing so causes occasional - crashes during drag & drop, which I guess keep the reference of an item a bit longer than it - should. This is why we autorelease here. See #354. - */ - [oldRetainer autorelease]; - [self updateSelection]; -} - -- (NSArray *)selectedIndexPaths -{ - NSArray *arrayPaths = [[self model] selectedPaths]; - NSMutableArray *result = [NSMutableArray array]; - for (NSArray *arrayPath in arrayPaths) { - [result addObject:[self internalizedPath:a2p(arrayPath)]]; - } - return result; -} - -- (void)startEditing -{ - [[self view] startEditing]; -} - -- (void)stopEditing -{ - [[self view] stopEditing]; -} - -- (void)updateSelection -{ - [[self view] updateSelection]; -} - -- (void)expandItem:(NSIndexPath *)item -{ - [[self view] ensureExpanded:item]; -} - -/* Caching */ -- (id)property:(NSString *)property valueAtPath:(NSIndexPath *)path -{ - NSMutableDictionary *props = [itemData objectForKey:path]; - id value = [props objectForKey:property]; - if (value == nil) { - value = [[self model] property:property valueAtPath:p2a(path)]; - if (value == nil) { - value = [NSNull null]; - } - [props setObject:value forKey:property]; - } - if (value == [NSNull null]) { - value = nil; - } - return value; -} - -- (void)setProperty:(NSString *)property value:(id)value atPath:(NSIndexPath *)path -{ - NSMutableDictionary *props = [itemData objectForKey:path]; - [props removeObjectForKey:property]; - [[self model] setProperty:property value:value atPath:p2a(path)]; -} - -- (NSString *)stringProperty:(NSString *)property valueAtPath:(NSIndexPath *)path -{ - return [self property:property valueAtPath:path]; -} - -- (void)setStringProperty:(NSString *)property value:(NSString *)value atPath:(NSIndexPath *)path -{ - [self setProperty:property value:value atPath:path]; -} - -- (BOOL)boolProperty:(NSString *)property valueAtPath:(NSIndexPath *)path -{ - NSNumber *value = [self property:property valueAtPath:path]; - return [value boolValue]; -} - -- (void)setBoolProperty:(NSString *)property value:(BOOL)value atPath:(NSIndexPath *)path -{ - [self setProperty:property value:[NSNumber numberWithBool:value] atPath:path]; -} - -- (NSInteger)intProperty:(NSString *)property valueAtPath:(NSIndexPath *)path -{ - NSNumber *value = [self property:property valueAtPath:path]; - return [value intValue]; -} - -- (void)setIntProperty:(NSString *)property value:(int)value atPath:(NSIndexPath *)path -{ - [self setProperty:property value:[NSNumber numberWithInt:value] atPath:path]; -} - -- (void)refreshItemAtPath:(NSIndexPath *)path -{ - NSMutableDictionary *props = [itemData objectForKey:path]; - [props removeAllObjects]; -} - -/* NSOutlineView data source */ - -- (NSInteger)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item -{ - return [self intProperty:CHILDREN_COUNT_PROPERTY valueAtPath:(NSIndexPath *)item]; -} - -- (id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)index ofItem:(id)item -{ - NSIndexPath *parent = item; - NSIndexPath *path = parent == nil ? [NSIndexPath indexPathWithIndex:index] : [parent indexPathByAddingIndex:index]; - return [self internalizedPath:path]; -} - -- (BOOL)outlineView:(NSOutlineView *)theOutlineView isItemExpandable:(id)item -{ - return [self outlineView:[self view] numberOfChildrenOfItem:item] > 0; -} - -- (BOOL)outlineView:(NSOutlineView *)outlineView shouldEditTableColumn:(NSTableColumn *)column item:(id)item -{ - return [[self model] canEditProperty:[column identifier] atPath:p2a((NSIndexPath *)item)]; -} - -- (id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)column byItem:(id)item -{ - return [self property:[column identifier] valueAtPath:(NSIndexPath *)item]; -} - -- (void)outlineView:(NSOutlineView *)outlineView setObjectValue:(id)value forTableColumn:(NSTableColumn *)column byItem:(id)item -{ - [self setProperty:[column identifier] value:value atPath:(NSIndexPath *)item]; -} - -/* We need to change the model selection at both IsChanging and DidChange. We need to set the -model selection at IsChanging before of the arrow clicking. The action launched by this little arrow -is performed before DidChange. However, when using the arrow to change the selection, IsChanging is -never called -*/ -- (void)outlineViewSelectionIsChanging:(NSNotification *)notification -{ - [self setPySelection]; -} - -- (void)outlineViewSelectionDidChange:(NSNotification *)notification -{ - [self setPySelection]; -} - -/* HSOutlineView delegate */ -- (NSIndexPath *)selectedIndexPath -{ - NSArray *paths = [self selectedIndexPaths]; - if ([paths count] > 0) { - return [paths objectAtIndex:0]; - } - else { - return nil; - } -} - -- (NSString *)dataForCopyToPasteboard -{ - return nil; -} - -- (void)outlineViewDidEndEditing:(HSOutlineView *)outlineView -{ - [[self model] saveEdits]; -} - -- (void)outlineViewWasDoubleClicked:(HSOutlineView *)outlineView -{ -} - -- (void)outlineViewCancelsEdition:(HSOutlineView *)outlineView -{ - [[self model] cancelEdits]; -} -@end \ No newline at end of file diff --git a/cocoalib/controllers/HSPopUpList.h b/cocoalib/controllers/HSPopUpList.h deleted file mode 100644 index 88e82349..00000000 --- a/cocoalib/controllers/HSPopUpList.h +++ /dev/null @@ -1,23 +0,0 @@ -/* -Copyright 2015 Hardcoded Software (http://www.hardcoded.net) - -This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -which should be included with this package. The terms are also available at -http://www.gnu.org/licenses/gpl-3.0.html -*/ - -#import -#import -#import "HSGUIController.h" -#import "PySelectableList.h" - -@interface HSPopUpList : HSGUIController {} -- (id)initWithPyRef:(PyObject *)aPyRef popupView:(NSPopUpButton *)aPopupView; -- (NSPopUpButton *)view; -- (void)setView:(NSPopUpButton *)aPopupView; -- (PySelectableList *)model; - -- (void)popupViewSelectionChanged; -- (void)refresh; -- (void)updateSelection; -@end \ No newline at end of file diff --git a/cocoalib/controllers/HSPopUpList.m b/cocoalib/controllers/HSPopUpList.m deleted file mode 100644 index 2e6fbbc0..00000000 --- a/cocoalib/controllers/HSPopUpList.m +++ /dev/null @@ -1,60 +0,0 @@ -/* -Copyright 2015 Hardcoded Software (http://www.hardcoded.net) - -This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -which should be included with this package. The terms are also available at -http://www.gnu.org/licenses/gpl-3.0.html -*/ - -#import "HSPopUpList.h" -#import "Utils.h" - -@implementation HSPopUpList -- (id)initWithPyRef:(PyObject *)aPyRef popupView:(NSPopUpButton *)aPopupView -{ - self = [super initWithPyRef:aPyRef wrapperClass:[PySelectableList class] - callbackClassName:@"SelectableListView" view:aPopupView]; - return self; -} - -- (NSPopUpButton *)view -{ - return (NSPopUpButton *)view; -} - -- (void)setView:(NSPopUpButton *)aPopupView -{ - if ([self view] != nil) { - [[self view] setTarget:nil]; - } - [super setView:aPopupView]; - if (aPopupView != nil) { - [aPopupView setAction:@selector(popupViewSelectionChanged)]; - [aPopupView setTarget:self]; - [self refresh]; - } -} - -- (PySelectableList *)model -{ - return (PySelectableList *)model; -} - -- (void)popupViewSelectionChanged -{ - [[self model] selectIndex:[[self view] indexOfSelectedItem]]; -} - -/* model --> view */ -- (void)refresh -{ - [[self view] removeAllItems]; - [[self view] addItemsWithTitles:[[self model] items]]; - [self updateSelection]; -} - -- (void)updateSelection -{ - [[self view] selectItemAtIndex:[[self model] selectedIndex]]; -} -@end \ No newline at end of file diff --git a/cocoalib/controllers/HSProgressWindow.h b/cocoalib/controllers/HSProgressWindow.h deleted file mode 100644 index 7d9458c3..00000000 --- a/cocoalib/controllers/HSProgressWindow.h +++ /dev/null @@ -1,30 +0,0 @@ -/* -Copyright 2015 Hardcoded Software (http://www.hardcoded.net) - -This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -which should be included with this package. The terms are also available at -http://www.gnu.org/licenses/gpl-3.0.html -*/ - -#import -#import "HSGUIController.h" -#import "HSTextField.h" -#import "Worker.h" -#import "PyProgressWindow.h" - -@interface HSProgressWindow : HSGUIController -{ - NSInteger progress; - HSTextField *jobdescTextField; - HSTextField *progressdescTextField; - NSWindow *parentWindow; -} - -- (id)initWithPyRef:(PyObject *)aPyRef view:(NSView *)aView; -- (PyProgressWindow *)model; -- (void)setParentWindow:(NSWindow *)aParentWindow; - -- (void)setProgress:(NSInteger)aProgress; -- (void)showWindow; -- (void)closeWindow; -@end \ No newline at end of file diff --git a/cocoalib/controllers/HSProgressWindow.m b/cocoalib/controllers/HSProgressWindow.m deleted file mode 100644 index 1ddc211f..00000000 --- a/cocoalib/controllers/HSProgressWindow.m +++ /dev/null @@ -1,79 +0,0 @@ -/* -Copyright 2015 Hardcoded Software (http://www.hardcoded.net) - -This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -which should be included with this package. The terms are also available at -http://www.gnu.org/licenses/gpl-3.0.html -*/ - -#import "HSProgressWindow.h" -#import "ProgressController.h" -#import "Utils.h" - -@implementation HSProgressWindow -- (id)initWithPyRef:(PyObject *)aPyRef view:(NSView *)aView -{ - self = [self initWithPyRef:aPyRef wrapperClass:[PyProgressWindow class] callbackClassName:@"ProgressWindowView" view:aView]; - [[ProgressController mainProgressController] setWorker:self]; - jobdescTextField = [[HSTextField alloc] initWithPyRef:[[self model] jobdescTextField] view:[[ProgressController mainProgressController] descText]]; - progressdescTextField = [[HSTextField alloc] initWithPyRef:[[self model] progressdescTextField] view:[[ProgressController mainProgressController] statusText]]; - parentWindow = nil; - return self; -} - -- (PyProgressWindow *)model -{ - return (PyProgressWindow *)model; -} - -/* Public */ -- (void)setParentWindow:(NSWindow *)aParentWindow -{ - parentWindow = aParentWindow; -} - -- (void)setProgress:(NSInteger)aProgress -{ - progress = aProgress; -} - -- (void)showWindow -{ - if (parentWindow != nil) { - [[ProgressController mainProgressController] showSheetForParent:parentWindow]; - } - else { - [[ProgressController mainProgressController] show]; - } -} - -- (void)closeWindow -{ - [[ProgressController mainProgressController] hide]; -} - -/* Worker */ - -- (NSNumber *)getJobProgress -{ - [[self model] pulse]; - return [NSNumber numberWithInt:progress]; -} - -- (NSString *)getJobDesc -{ - // Our desc label is updated independently. - return nil; -} - -- (void)cancelJob -{ - [[self model] cancel]; -} - -- (void)jobCompleted:(NSString *)jobid -{ - // With the new hscommon.gui.progress_window, this call is done from within the core. Do nothing. -} - -@end diff --git a/cocoalib/controllers/HSSelectableList.h b/cocoalib/controllers/HSSelectableList.h deleted file mode 100644 index c2a6645d..00000000 --- a/cocoalib/controllers/HSSelectableList.h +++ /dev/null @@ -1,24 +0,0 @@ -/* -Copyright 2015 Hardcoded Software (http://www.hardcoded.net) - -This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -which should be included with this package. The terms are also available at -http://www.gnu.org/licenses/gpl-3.0.html -*/ - -#import -#import "HSGUIController.h" -#import "PySelectableList.h" - -@interface HSSelectableList : HSGUIController -{ - NSArray *items; -} -- (id)initWithPyRef:(PyObject *)aPyRef wrapperClass:(Class)aWrapperClass callbackClassName:(NSString *)aCallbackClassName view:(NSTableView *)aTableView; -- (id)initWithPyRef:(PyObject *)aPyRef tableView:(NSTableView *)aTableView; -- (NSTableView *)view; -- (void)setView:(NSTableView *)aTableView; -- (PySelectableList *)model; - -- (void)refresh; -@end diff --git a/cocoalib/controllers/HSSelectableList.m b/cocoalib/controllers/HSSelectableList.m deleted file mode 100644 index 94e47e3c..00000000 --- a/cocoalib/controllers/HSSelectableList.m +++ /dev/null @@ -1,107 +0,0 @@ -/* -Copyright 2015 Hardcoded Software (http://www.hardcoded.net) - -This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -which should be included with this package. The terms are also available at -http://www.gnu.org/licenses/gpl-3.0.html -*/ - -#import "HSSelectableList.h" -#import "Utils.h" - -@implementation HSSelectableList -- (id)initWithPyRef:(PyObject *)aPyRef wrapperClass:(Class)aWrapperClass callbackClassName:(NSString *)aCallbackClassName view:(NSTableView *)aTableView; -{ - self = [super initWithPyRef:aPyRef wrapperClass:aWrapperClass callbackClassName:aCallbackClassName view:aTableView]; - return self; -} - -- (id)initWithPyRef:(PyObject *)aPyRef tableView:(NSTableView *)aTableView -{ - self = [self initWithPyRef:aPyRef wrapperClass:[PySelectableList class] callbackClassName:@"SelectableListView" view:aTableView]; - return self; -} - -- (void)dealloc -{ - [items release]; - [super dealloc]; -} - -- (PySelectableList *)model -{ - return (PySelectableList *)model; -} - -- (NSTableView *)view -{ - return (NSTableView *)view; -} - -- (void)setView:(NSTableView *)aTableView -{ - if ([self view] != nil) { - [[self view] setDataSource:nil]; - [[self view] setDelegate:nil]; - } - [super setView:aTableView]; - if (aTableView != nil) { - [aTableView setDataSource:self]; - [aTableView setDelegate:self]; - [self refresh]; - } -} - -/* Private */ -- (void)setPySelection -{ - NSArray *selection = [Utils indexSet2Array:[[self view] selectedRowIndexes]]; - NSArray *pyselection = [[self model] selectedIndexes]; - if (![selection isEqualTo:pyselection]) { - [[self model] selectIndexes:selection]; - } -} - -- (void)setViewSelection -{ - NSIndexSet *selection = [Utils array2IndexSet:[[self model] selectedIndexes]]; - [[self view] selectRowIndexes:selection byExtendingSelection:NO]; -} - -/* Data source */ -- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView -{ - return [items count]; -} - -- (id)tableView:(NSTableView *)tableView objectValueForTableColumn:(NSTableColumn *)column row:(NSInteger)row -{ - // Cocoa's typeselect mechanism can call us with an out-of-range row - if (row >= [items count]) { - return @""; - } - return [items objectAtIndex:row]; -} - -- (void)tableViewSelectionDidChange:(NSNotification *)notification -{ - [self setPySelection]; -} - -/* Public */ - -- (void)refresh -{ - // If we just deleted the last item, we want to update the selection before we reload - [items release]; - items = [[[self model] items] retain]; - [[self view] reloadData]; - [self setViewSelection]; -} - -- (void)updateSelection -{ - NSIndexSet *selection = [NSIndexSet indexSetWithIndex:[[self model] selectedIndex]]; - [[self view] selectRowIndexes:selection byExtendingSelection:NO]; -} -@end diff --git a/cocoalib/controllers/HSTable.h b/cocoalib/controllers/HSTable.h deleted file mode 100644 index 41a29123..00000000 --- a/cocoalib/controllers/HSTable.h +++ /dev/null @@ -1,32 +0,0 @@ -/* -Copyright 2015 Hardcoded Software (http://www.hardcoded.net) - -This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -which should be included with this package. The terms are also available at -http://www.gnu.org/licenses/gpl-3.0.html -*/ - -#import -#import "HSGUIController.h" -#import "HSColumns.h" -#import "PyTable.h" - -@interface HSTable : HSGUIController -{ - HSColumns *columns; -} -- (id)initWithModel:(PyTable *)aModel tableView:(NSTableView *)aTableView; -- (id)initWithPyRef:(PyObject *)aPyRef wrapperClass:(Class)aWrapperClass callbackClassName:(NSString *)aCallbackClassName view:(NSTableView *)aTableView; -- (id)initWithPyRef:(PyObject *)aPyRef tableView:(NSTableView *)aTableView; - -/* Virtual */ -- (PyTable *)model; -- (NSTableView *)view; -- (void)setView:(NSTableView *)aTableView; - -/* Public */ -- (HSColumns *)columns; -- (void)refresh; -- (void)showSelectedRow; -- (void)updateSelection; -@end diff --git a/cocoalib/controllers/HSTable.m b/cocoalib/controllers/HSTable.m deleted file mode 100644 index 7325c816..00000000 --- a/cocoalib/controllers/HSTable.m +++ /dev/null @@ -1,136 +0,0 @@ -/* -Copyright 2015 Hardcoded Software (http://www.hardcoded.net) - -This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -which should be included with this package. The terms are also available at -http://www.gnu.org/licenses/gpl-3.0.html -*/ - -#import "HSTable.h" -#import "Utils.h" - -@implementation HSTable -- (id)initWithModel:(PyTable *)aModel tableView:(NSTableView *)aTableView -{ - self = [super initWithModel:aModel view:aTableView]; - columns = [[HSColumns alloc] initWithPyRef:[[self model] columns] tableView:aTableView]; - return self; -} - -- (id)initWithPyRef:(PyObject *)aPyRef wrapperClass:(Class)aWrapperClass callbackClassName:(NSString *)aCallbackClassName view:(NSTableView *)aTableView -{ - self = [super initWithPyRef:aPyRef wrapperClass:aWrapperClass callbackClassName:aCallbackClassName view:aTableView]; - columns = [[HSColumns alloc] initWithPyRef:[[self model] columns] tableView:aTableView]; - return self; -} - -- (id)initWithPyRef:(PyObject *)aPyRef tableView:(NSTableView *)aTableView -{ - return [self initWithPyRef:aPyRef wrapperClass:[PyTable class] callbackClassName:@"TableView" view:aTableView]; -} - -- (void)dealloc -{ - [columns release]; - [super dealloc]; -} - -/* Private */ -- (void)setPySelection -{ - NSArray *selection = [Utils indexSet2Array:[[self view] selectedRowIndexes]]; - NSArray *pyselection = [[self model] selectedRows]; - if (![selection isEqualTo:pyselection]) - [[self model] selectRows:selection]; -} - -- (void)setViewSelection -{ - NSIndexSet *selection = [Utils array2IndexSet:[[self model] selectedRows]]; - [[self view] selectRowIndexes:selection byExtendingSelection:NO]; -} - -/* HSGUIController */ -- (PyTable *)model -{ - return (PyTable *)model; -} - -- (NSTableView *)view -{ - return (NSTableView *)view; -} - -- (void)setView:(NSTableView *)aTableView -{ - if ([self view] != nil) { - [[self view] setDataSource:nil]; - [[self view] setDelegate:nil]; - } - [super setView:aTableView]; - if (aTableView != nil) { - [aTableView setDataSource:self]; - [aTableView setDelegate:self]; - } -} - -/* Data source */ -- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView -{ - return [[self model] numberOfRows]; -} - -- (id)tableView:(NSTableView *)tableView objectValueForTableColumn:(NSTableColumn *)column row:(NSInteger)row -{ - // Cocoa's typeselect mechanism can call us with an out-of-range row - if (row >= [[self model] numberOfRows]) { - return @""; - } - return [[self model] valueForColumn:[column identifier] row:row]; -} - -/* NSTableView Delegate */ -- (void)tableView:(NSTableView *)aTableView didClickTableColumn:(NSTableColumn *)column -{ - if ([[aTableView sortDescriptors] count] == 0) { - return; - } - NSSortDescriptor *sd = [[aTableView sortDescriptors] objectAtIndex:0]; - [[self model] sortByColumn:[sd key] desc:![sd ascending]]; -} - -// See HSOutline.outlineViewSelectionIsChanging: to know why we update selection in both notifs -- (void)tableViewSelectionIsChanging:(NSNotification *)notification -{ - [self setPySelection]; -} - -- (void)tableViewSelectionDidChange:(NSNotification *)notification -{ - [self setPySelection]; -} - -/* Public */ -- (HSColumns *)columns -{ - return columns; -} - -- (void)refresh -{ - // If we just deleted the last item, we want to update the selection before we reload - [self setViewSelection]; - [[self view] reloadData]; - [self setViewSelection]; -} - -- (void)showSelectedRow -{ - [[self view] scrollRowToVisible:[[self view] selectedRow]]; -} - -- (void)updateSelection -{ - [self setViewSelection]; -} -@end diff --git a/cocoalib/controllers/HSTextField.h b/cocoalib/controllers/HSTextField.h deleted file mode 100644 index 26a3cd2e..00000000 --- a/cocoalib/controllers/HSTextField.h +++ /dev/null @@ -1,21 +0,0 @@ -/* -Copyright 2015 Hardcoded Software (http://www.hardcoded.net) - -This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -which should be included with this package. The terms are also available at -http://www.gnu.org/licenses/gpl-3.0.html -*/ - -#import -#import -#import "HSGUIController.h" -#import "PyTextField.h" - -@interface HSTextField : HSGUIController {} -- (id)initWithPyRef:(PyObject *)aPyRef view:(NSTextField *)aView; -- (NSTextField *)view; -- (void)setView:(NSTextField *)aView; -- (PyTextField *)model; - -- (void)refresh; -@end \ No newline at end of file diff --git a/cocoalib/controllers/HSTextField.m b/cocoalib/controllers/HSTextField.m deleted file mode 100644 index 145f1261..00000000 --- a/cocoalib/controllers/HSTextField.m +++ /dev/null @@ -1,53 +0,0 @@ -/* -Copyright 2015 Hardcoded Software (http://www.hardcoded.net) - -This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -which should be included with this package. The terms are also available at -http://www.gnu.org/licenses/gpl-3.0.html -*/ - -#import "HSTextField.h" -#import "Utils.h" - -@implementation HSTextField -- (id)initWithPyRef:(PyObject *)aPyRef view:(NSTextField *)aView -{ - self = [super initWithPyRef:aPyRef wrapperClass:[PyTextField class] - callbackClassName:@"GUIObjectView" view:aView]; - return self; -} - -- (NSTextField *)view -{ - return (NSTextField *)view; -} - -- (void)setView:(NSTextField *)aView -{ - if ([self view] != nil) { - [[self view] setDelegate:nil]; - } - [super setView:aView]; - if (aView != nil) { - [aView setDelegate:self]; - [self refresh]; - } -} - -- (PyTextField *)model -{ - return (PyTextField *)model; -} - -/* Delegate */ -- (void)controlTextDidEndEditing:(NSNotification *)aNotification -{ - [[self model] setText:[[self view] stringValue]]; -} - -/* model --> view */ -- (void)refresh -{ - [[self view] setStringValue:[[self model] text]]; -} -@end \ No newline at end of file diff --git a/cocoalib/en.lproj/cocoalib.strings b/cocoalib/en.lproj/cocoalib.strings deleted file mode 100644 index 40411c64..00000000 --- a/cocoalib/en.lproj/cocoalib.strings +++ /dev/null @@ -1,15 +0,0 @@ - -"Cancel" = "Cancel"; -"Clear List" = "Clear List"; -"Close" = "Close"; -"Error Report" = "Error Report"; -"Error reports should be reported as Github issues. You can copy the error traceback above and paste it in a new issue (bonus point if you run a search to make sure the issue doesn't already exist). What usually really helps is if you add a description of how you got the error. Thanks!\n\nAlthough the application should continue to run after this error, it may be in an unstable state, so it is recommended that you restart the application." = "Error reports should be reported as Github issues. You can copy the error traceback above and paste it in a new issue (bonus point if you run a search to make sure the issue doesn't already exist). What usually really helps is if you add a description of how you got the error. Thanks!\n\nAlthough the application should continue to run after this error, it may be in an unstable state, so it is recommended that you restart the application."; -"Go to Github" = "Go to Github"; -"No" = "No"; -"OK" = "OK"; -"Please wait..." = "Please wait..."; -"Something went wrong. How about reporting the error?" = "Something went wrong. How about reporting the error?"; -"Status: Working..." = "Status: Working..."; -"Work in progress, please wait." = "Work in progress, please wait."; -"Work in progress..." = "Work in progress..."; -"Yes" = "Yes"; diff --git a/cocoalib/locale/cocoalib.pot b/cocoalib/locale/cocoalib.pot deleted file mode 100644 index 4589403f..00000000 --- a/cocoalib/locale/cocoalib.pot +++ /dev/null @@ -1,62 +0,0 @@ -# -msgid "" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Cancel" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Clear List" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Close" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Error Report" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "" -"Error reports should be reported as Github issues. You can copy the error traceback above and paste it in a new issue (bonus point if you run a search to make sure the issue doesn't already exist). What usually really helps is if you add a description of how you got the error. Thanks!\n" -"\n" -"Although the application should continue to run after this error, it may be in an unstable state, so it is recommended that you restart the application." -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Go to Github" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "No" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "OK" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Please wait..." -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Something went wrong. How about reporting the error?" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Status: Working..." -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Work in progress, please wait." -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Work in progress..." -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Yes" -msgstr "" diff --git a/cocoalib/locale/cs/LC_MESSAGES/cocoalib.po b/cocoalib/locale/cs/LC_MESSAGES/cocoalib.po deleted file mode 100644 index 44c0a847..00000000 --- a/cocoalib/locale/cs/LC_MESSAGES/cocoalib.po +++ /dev/null @@ -1,70 +0,0 @@ -# Translators: -msgid "" -msgstr "" -"Project-Id-Version: hscommon\n" -"PO-Revision-Date: 2014-03-30 14:54+0000\n" -"Last-Translator: Virgil Dupras \n" -"Language-Team: Czech (http://www.transifex.com/p/hscommon/language/cs/)\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: utf-8\n" -"Language: cs\n" -"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Cancel" -msgstr "Cancel" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Clear List" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Close" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Error Report" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "" -"Error reports should be reported as Github issues. You can copy the error traceback above and paste it in a new issue (bonus point if you run a search to make sure the issue doesn't already exist). What usually really helps is if you add a description of how you got the error. Thanks!\n" -"\n" -"Although the application should continue to run after this error, it may be in an unstable state, so it is recommended that you restart the application." -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Go to Github" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "No" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "OK" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Please wait..." -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Something went wrong. How about reporting the error?" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Status: Working..." -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Work in progress, please wait." -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Work in progress..." -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Yes" -msgstr "" diff --git a/cocoalib/locale/de/LC_MESSAGES/cocoalib.po b/cocoalib/locale/de/LC_MESSAGES/cocoalib.po deleted file mode 100644 index fb8bdbc3..00000000 --- a/cocoalib/locale/de/LC_MESSAGES/cocoalib.po +++ /dev/null @@ -1,70 +0,0 @@ -# Translators: -msgid "" -msgstr "" -"Project-Id-Version: hscommon\n" -"PO-Revision-Date: 2014-03-30 14:54+0000\n" -"Last-Translator: Virgil Dupras \n" -"Language-Team: German (http://www.transifex.com/p/hscommon/language/de/)\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: utf-8\n" -"Language: de\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Cancel" -msgstr "Abbrechen" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Clear List" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Close" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Error Report" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "" -"Error reports should be reported as Github issues. You can copy the error traceback above and paste it in a new issue (bonus point if you run a search to make sure the issue doesn't already exist). What usually really helps is if you add a description of how you got the error. Thanks!\n" -"\n" -"Although the application should continue to run after this error, it may be in an unstable state, so it is recommended that you restart the application." -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Go to Github" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "No" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "OK" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Please wait..." -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Something went wrong. How about reporting the error?" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Status: Working..." -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Work in progress, please wait." -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Work in progress..." -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Yes" -msgstr "" diff --git a/cocoalib/locale/es/LC_MESSAGES/cocoalib.po b/cocoalib/locale/es/LC_MESSAGES/cocoalib.po deleted file mode 100755 index 79ec036e..00000000 --- a/cocoalib/locale/es/LC_MESSAGES/cocoalib.po +++ /dev/null @@ -1,70 +0,0 @@ -# Translators: -msgid "" -msgstr "" -"Project-Id-Version: hscommon\n" -"PO-Revision-Date: 2014-03-30 14:54+0000\n" -"Last-Translator: Virgil Dupras \n" -"Language-Team: Spanish (http://www.transifex.com/p/hscommon/language/es/)\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: utf-8\n" -"Language: es\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Cancel" -msgstr "Cancelar" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Clear List" -msgstr "Limpiar lista" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Close" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Error Report" -msgstr "Informe de error" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "" -"Error reports should be reported as Github issues. You can copy the error traceback above and paste it in a new issue (bonus point if you run a search to make sure the issue doesn't already exist). What usually really helps is if you add a description of how you got the error. Thanks!\n" -"\n" -"Although the application should continue to run after this error, it may be in an unstable state, so it is recommended that you restart the application." -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Go to Github" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "No" -msgstr "No" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "OK" -msgstr "Aceptar" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Please wait..." -msgstr "Por favor, espere..." - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Something went wrong. How about reporting the error?" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Status: Working..." -msgstr "Estado: procesando..." - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Work in progress, please wait." -msgstr "En proceso, por favor, espere." - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Work in progress..." -msgstr "En proceso..." - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Yes" -msgstr "Sí" diff --git a/cocoalib/locale/fr/LC_MESSAGES/cocoalib.po b/cocoalib/locale/fr/LC_MESSAGES/cocoalib.po deleted file mode 100644 index 68609710..00000000 --- a/cocoalib/locale/fr/LC_MESSAGES/cocoalib.po +++ /dev/null @@ -1,70 +0,0 @@ -# Translators: -msgid "" -msgstr "" -"Project-Id-Version: hscommon\n" -"PO-Revision-Date: 2014-03-30 14:54+0000\n" -"Last-Translator: Virgil Dupras \n" -"Language-Team: French (http://www.transifex.com/p/hscommon/language/fr/)\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: utf-8\n" -"Language: fr\n" -"Plural-Forms: nplurals=2; plural=(n > 1);\n" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Cancel" -msgstr "Annuler" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Clear List" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Close" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Error Report" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "" -"Error reports should be reported as Github issues. You can copy the error traceback above and paste it in a new issue (bonus point if you run a search to make sure the issue doesn't already exist). What usually really helps is if you add a description of how you got the error. Thanks!\n" -"\n" -"Although the application should continue to run after this error, it may be in an unstable state, so it is recommended that you restart the application." -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Go to Github" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "No" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "OK" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Please wait..." -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Something went wrong. How about reporting the error?" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Status: Working..." -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Work in progress, please wait." -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Work in progress..." -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Yes" -msgstr "" diff --git a/cocoalib/locale/hy/LC_MESSAGES/cocoalib.po b/cocoalib/locale/hy/LC_MESSAGES/cocoalib.po deleted file mode 100755 index 7b0ad2fd..00000000 --- a/cocoalib/locale/hy/LC_MESSAGES/cocoalib.po +++ /dev/null @@ -1,70 +0,0 @@ -# Translators: -msgid "" -msgstr "" -"Project-Id-Version: hscommon\n" -"PO-Revision-Date: 2014-03-30 14:54+0000\n" -"Last-Translator: Virgil Dupras \n" -"Language-Team: Armenian (http://www.transifex.com/p/hscommon/language/hy/)\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: utf-8\n" -"Language: hy\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Cancel" -msgstr "Չեղարկել" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Clear List" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Close" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Error Report" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "" -"Error reports should be reported as Github issues. You can copy the error traceback above and paste it in a new issue (bonus point if you run a search to make sure the issue doesn't already exist). What usually really helps is if you add a description of how you got the error. Thanks!\n" -"\n" -"Although the application should continue to run after this error, it may be in an unstable state, so it is recommended that you restart the application." -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Go to Github" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "No" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "OK" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Please wait..." -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Something went wrong. How about reporting the error?" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Status: Working..." -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Work in progress, please wait." -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Work in progress..." -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Yes" -msgstr "" diff --git a/cocoalib/locale/it/LC_MESSAGES/cocoalib.po b/cocoalib/locale/it/LC_MESSAGES/cocoalib.po deleted file mode 100644 index ae2504ed..00000000 --- a/cocoalib/locale/it/LC_MESSAGES/cocoalib.po +++ /dev/null @@ -1,70 +0,0 @@ -# Translators: -msgid "" -msgstr "" -"Project-Id-Version: hscommon\n" -"PO-Revision-Date: 2014-03-30 14:54+0000\n" -"Last-Translator: Virgil Dupras \n" -"Language-Team: Italian (http://www.transifex.com/p/hscommon/language/it/)\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: utf-8\n" -"Language: it\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Cancel" -msgstr "Annulla" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Clear List" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Close" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Error Report" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "" -"Error reports should be reported as Github issues. You can copy the error traceback above and paste it in a new issue (bonus point if you run a search to make sure the issue doesn't already exist). What usually really helps is if you add a description of how you got the error. Thanks!\n" -"\n" -"Although the application should continue to run after this error, it may be in an unstable state, so it is recommended that you restart the application." -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Go to Github" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "No" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "OK" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Please wait..." -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Something went wrong. How about reporting the error?" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Status: Working..." -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Work in progress, please wait." -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Work in progress..." -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Yes" -msgstr "" diff --git a/cocoalib/locale/ko/LC_MESSAGES/cocoalib.po b/cocoalib/locale/ko/LC_MESSAGES/cocoalib.po deleted file mode 100644 index 06823e5a..00000000 --- a/cocoalib/locale/ko/LC_MESSAGES/cocoalib.po +++ /dev/null @@ -1,70 +0,0 @@ -# Translators: -msgid "" -msgstr "" -"Project-Id-Version: hscommon\n" -"PO-Revision-Date: 2015-07-20 16:42+0000\n" -"Last-Translator: Virgil Dupras \n" -"Language-Team: Korean (http://www.transifex.com/p/hscommon/language/ko/)\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: utf-8\n" -"Language: ko\n" -"Plural-Forms: nplurals=1; plural=0;\n" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Cancel" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Clear List" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Close" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Error Report" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "" -"Error reports should be reported as Github issues. You can copy the error traceback above and paste it in a new issue (bonus point if you run a search to make sure the issue doesn't already exist). What usually really helps is if you add a description of how you got the error. Thanks!\n" -"\n" -"Although the application should continue to run after this error, it may be in an unstable state, so it is recommended that you restart the application." -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Go to Github" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "No" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "OK" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Please wait..." -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Something went wrong. How about reporting the error?" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Status: Working..." -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Work in progress, please wait." -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Work in progress..." -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Yes" -msgstr "" diff --git a/cocoalib/locale/nl/LC_MESSAGES/cocoalib.po b/cocoalib/locale/nl/LC_MESSAGES/cocoalib.po deleted file mode 100644 index cfb1e08a..00000000 --- a/cocoalib/locale/nl/LC_MESSAGES/cocoalib.po +++ /dev/null @@ -1,70 +0,0 @@ -# Translators: -msgid "" -msgstr "" -"Project-Id-Version: hscommon\n" -"PO-Revision-Date: 2014-03-30 14:54+0000\n" -"Last-Translator: Virgil Dupras \n" -"Language-Team: Dutch (http://www.transifex.com/p/hscommon/language/nl/)\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: utf-8\n" -"Language: nl\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Cancel" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Clear List" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Close" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Error Report" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "" -"Error reports should be reported as Github issues. You can copy the error traceback above and paste it in a new issue (bonus point if you run a search to make sure the issue doesn't already exist). What usually really helps is if you add a description of how you got the error. Thanks!\n" -"\n" -"Although the application should continue to run after this error, it may be in an unstable state, so it is recommended that you restart the application." -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Go to Github" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "No" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "OK" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Please wait..." -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Something went wrong. How about reporting the error?" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Status: Working..." -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Work in progress, please wait." -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Work in progress..." -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Yes" -msgstr "" diff --git a/cocoalib/locale/pl_PL/LC_MESSAGES/cocoalib.po b/cocoalib/locale/pl_PL/LC_MESSAGES/cocoalib.po deleted file mode 100644 index 0b0d21a9..00000000 --- a/cocoalib/locale/pl_PL/LC_MESSAGES/cocoalib.po +++ /dev/null @@ -1,71 +0,0 @@ -# -# Translators: -msgid "" -msgstr "" -"Project-Id-Version: hscommon\n" -"PO-Revision-Date: 2015-07-20 16:53+0000\n" -"Last-Translator: Virgil Dupras \n" -"Language-Team: Polish (Poland) (http://www.transifex.com/p/hscommon/language/pl_PL/)\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: utf-8\n" -"Language: pl_PL\n" -"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Cancel" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Clear List" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Close" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Error Report" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "" -"Error reports should be reported as Github issues. You can copy the error traceback above and paste it in a new issue (bonus point if you run a search to make sure the issue doesn't already exist). What usually really helps is if you add a description of how you got the error. Thanks!\n" -"\n" -"Although the application should continue to run after this error, it may be in an unstable state, so it is recommended that you restart the application." -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Go to Github" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "No" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "OK" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Please wait..." -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Something went wrong. How about reporting the error?" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Status: Working..." -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Work in progress, please wait." -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Work in progress..." -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Yes" -msgstr "" diff --git a/cocoalib/locale/pt_BR/LC_MESSAGES/cocoalib.po b/cocoalib/locale/pt_BR/LC_MESSAGES/cocoalib.po deleted file mode 100644 index fd7e5cbf..00000000 --- a/cocoalib/locale/pt_BR/LC_MESSAGES/cocoalib.po +++ /dev/null @@ -1,74 +0,0 @@ -# Translators: -# Vitu , 2013-2014 -msgid "" -msgstr "" -"Project-Id-Version: hscommon\n" -"PO-Revision-Date: 2014-04-30 19:00+0000\n" -"Last-Translator: Vitu \n" -"Language-Team: Portuguese (Brazil) (http://www.transifex.com/p/hscommon/language/pt_BR/)\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: utf-8\n" -"Language: pt_BR\n" -"Plural-Forms: nplurals=2; plural=(n > 1);\n" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Cancel" -msgstr "Cancelar" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Clear List" -msgstr "Limpar Lista" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Close" -msgstr "Fechar" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Error Report" -msgstr "Relatório de Erro" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "" -"Error reports should be reported as Github issues. You can copy the error traceback above and paste it in a new issue (bonus point if you run a search to make sure the issue doesn't already exist). What usually really helps is if you add a description of how you got the error. Thanks!\n" -"\n" -"Although the application should continue to run after this error, it may be in an unstable state, so it is recommended that you restart the application." -msgstr "" -"Os erros devem ser relatados como problemas no Github. Copie o código de rastreamento acima e cole-o em um problema novo (pontos bônus caso você busque o erro, certificando-se de que ele ainda não exista). O que mais ajuda é adicionar uma descrição de como o erro ocorreu. Obrigado!\n" -"\n" -"Embora o aplicativo continue a funcionar após esse erro, ele pode estar “instável”. É recomendável reiniciá-lo." - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Go to Github" -msgstr "Ir para o Github" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "No" -msgstr "Não" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "OK" -msgstr "OK" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Please wait..." -msgstr "Aguarde..." - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Something went wrong. How about reporting the error?" -msgstr "Algo deu errado. Deseja relatar o erro?" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Status: Working..." -msgstr "Estado: em andamento…" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Work in progress, please wait." -msgstr "Tarefa em andamento. Aguarde." - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Work in progress..." -msgstr "Tarefa em andamento…" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Yes" -msgstr "Sim" diff --git a/cocoalib/locale/ru/LC_MESSAGES/cocoalib.po b/cocoalib/locale/ru/LC_MESSAGES/cocoalib.po deleted file mode 100755 index 4955cf55..00000000 --- a/cocoalib/locale/ru/LC_MESSAGES/cocoalib.po +++ /dev/null @@ -1,74 +0,0 @@ -# Translators: -# Igor Fokusov , 2015 -msgid "" -msgstr "" -"Project-Id-Version: hscommon\n" -"PO-Revision-Date: 2015-04-14 21:20+0000\n" -"Last-Translator: Igor Fokusov \n" -"Language-Team: Russian (http://www.transifex.com/p/hscommon/language/ru/)\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: utf-8\n" -"Language: ru\n" -"Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n%100>=11 && n%100<=14)? 2 : 3);\n" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Cancel" -msgstr "Отменить" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Clear List" -msgstr "Очистить список" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Close" -msgstr "Закрыть" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Error Report" -msgstr "Сообщение об ошибке" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "" -"Error reports should be reported as Github issues. You can copy the error traceback above and paste it in a new issue (bonus point if you run a search to make sure the issue doesn't already exist). What usually really helps is if you add a description of how you got the error. Thanks!\n" -"\n" -"Although the application should continue to run after this error, it may be in an unstable state, so it is recommended that you restart the application." -msgstr "" -"Отчеты об ошибках нужно отправлять в Github issues проекта. Скопируйте текст ошибки выше и вставьте в созданную заметку о проблеме (перед этим желательно проверить - не создано ли уже такой проблемы до вас). Также нам очень поможет краткое описание как вы получили такую ошибку. Спасибо!\n" -"\n" -"В принципе, программа может продолжать работу, но стабильная работа не гарантируется. Поэтому желательно перезапустить программу." - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Go to Github" -msgstr "Перейти на Github" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "No" -msgstr "Нет" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "OK" -msgstr "ОК" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Please wait..." -msgstr "Пожалуйста, ждите..." - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Something went wrong. How about reporting the error?" -msgstr "Что-то пошло не так. Хотите отправить отчёт об ошибке?" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Status: Working..." -msgstr "Состояние: Работает..." - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Work in progress, please wait." -msgstr "Идет работа. Пожалуйста, ждите." - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Work in progress..." -msgstr "Идет работа..." - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Yes" -msgstr "Да" diff --git a/cocoalib/locale/uk/LC_MESSAGES/cocoalib.po b/cocoalib/locale/uk/LC_MESSAGES/cocoalib.po deleted file mode 100755 index 3d397308..00000000 --- a/cocoalib/locale/uk/LC_MESSAGES/cocoalib.po +++ /dev/null @@ -1,70 +0,0 @@ -# Translators: -msgid "" -msgstr "" -"Project-Id-Version: hscommon\n" -"PO-Revision-Date: 2014-03-30 14:54+0000\n" -"Last-Translator: Virgil Dupras \n" -"Language-Team: Ukrainian (http://www.transifex.com/p/hscommon/language/uk/)\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: utf-8\n" -"Language: uk\n" -"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Cancel" -msgstr "Відмінити" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Clear List" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Close" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Error Report" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "" -"Error reports should be reported as Github issues. You can copy the error traceback above and paste it in a new issue (bonus point if you run a search to make sure the issue doesn't already exist). What usually really helps is if you add a description of how you got the error. Thanks!\n" -"\n" -"Although the application should continue to run after this error, it may be in an unstable state, so it is recommended that you restart the application." -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Go to Github" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "No" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "OK" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Please wait..." -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Something went wrong. How about reporting the error?" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Status: Working..." -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Work in progress, please wait." -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Work in progress..." -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Yes" -msgstr "" diff --git a/cocoalib/locale/vi/LC_MESSAGES/cocoalib.po b/cocoalib/locale/vi/LC_MESSAGES/cocoalib.po deleted file mode 100644 index cb410a78..00000000 --- a/cocoalib/locale/vi/LC_MESSAGES/cocoalib.po +++ /dev/null @@ -1,70 +0,0 @@ -# Translators: -msgid "" -msgstr "" -"Project-Id-Version: hscommon\n" -"PO-Revision-Date: 2014-03-30 14:54+0000\n" -"Last-Translator: Virgil Dupras \n" -"Language-Team: Vietnamese (http://www.transifex.com/p/hscommon/language/vi/)\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: utf-8\n" -"Language: vi\n" -"Plural-Forms: nplurals=1; plural=0;\n" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Cancel" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Clear List" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Close" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Error Report" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "" -"Error reports should be reported as Github issues. You can copy the error traceback above and paste it in a new issue (bonus point if you run a search to make sure the issue doesn't already exist). What usually really helps is if you add a description of how you got the error. Thanks!\n" -"\n" -"Although the application should continue to run after this error, it may be in an unstable state, so it is recommended that you restart the application." -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Go to Github" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "No" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "OK" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Please wait..." -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Something went wrong. How about reporting the error?" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Status: Working..." -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Work in progress, please wait." -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Work in progress..." -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Yes" -msgstr "" diff --git a/cocoalib/locale/zh_CN/LC_MESSAGES/cocoalib.po b/cocoalib/locale/zh_CN/LC_MESSAGES/cocoalib.po deleted file mode 100644 index 3b129bbd..00000000 --- a/cocoalib/locale/zh_CN/LC_MESSAGES/cocoalib.po +++ /dev/null @@ -1,70 +0,0 @@ -# Translators: -msgid "" -msgstr "" -"Project-Id-Version: hscommon\n" -"PO-Revision-Date: 2014-03-30 14:54+0000\n" -"Last-Translator: Virgil Dupras \n" -"Language-Team: Chinese (China) (http://www.transifex.com/p/hscommon/language/zh_CN/)\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: utf-8\n" -"Language: zh_CN\n" -"Plural-Forms: nplurals=1; plural=0;\n" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Cancel" -msgstr "取消" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Clear List" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Close" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Error Report" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "" -"Error reports should be reported as Github issues. You can copy the error traceback above and paste it in a new issue (bonus point if you run a search to make sure the issue doesn't already exist). What usually really helps is if you add a description of how you got the error. Thanks!\n" -"\n" -"Although the application should continue to run after this error, it may be in an unstable state, so it is recommended that you restart the application." -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Go to Github" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "No" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "OK" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Please wait..." -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Something went wrong. How about reporting the error?" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Status: Working..." -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Work in progress, please wait." -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Work in progress..." -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Yes" -msgstr "" diff --git a/cocoalib/ui/about.py b/cocoalib/ui/about.py deleted file mode 100644 index 37fbe63b..00000000 --- a/cocoalib/ui/about.py +++ /dev/null @@ -1,36 +0,0 @@ -ownerclass = 'HSAboutBox' -ownerimport = 'HSAboutBox.h' - -result = Window(259, 217, "") -result.canResize = False -result.canMinimize = False -image = ImageView(result, "NSApplicationIcon") -titleLabel = Label(result, NLSTR("AppTitle")) -versionLabel = Label(result, NLSTR("AppVersion")) -copyrightLabel = Label(result, NLSTR("AppCopyright")) - -owner.window = result -owner.titleTextField = titleLabel -owner.versionTextField = versionLabel -owner.copyrightTextField = copyrightLabel -for label in (titleLabel, versionLabel, copyrightLabel): - label.alignment = const.NSCenterTextAlignment -titleLabel.font = Font(FontFamily.Label, FontSize.RegularControl, traits=[FontTrait.Bold]) -for label in (versionLabel, copyrightLabel): - label.font = Font(FontFamily.Label, FontSize.SmallControl) - label.height = 14 - -image.height = 96 -image.packToCorner(Pack.UpperLeft) -image.y = result.height - 10 - image.height -image.fill(Pack.Right) -image.setAnchor(Pack.UpperLeft, growX=True) -titleLabel.packRelativeTo(image, Pack.Below, Pack.Left) -titleLabel.fill(Pack.Right) -titleLabel.setAnchor(Pack.UpperLeft, growX=True) -versionLabel.packRelativeTo(titleLabel, Pack.Below, Pack.Left) -versionLabel.fill(Pack.Right) -versionLabel.setAnchor(Pack.UpperLeft, growX=True) -copyrightLabel.packRelativeTo(versionLabel, Pack.Below, Pack.Left) -copyrightLabel.fill(Pack.Right) -copyrightLabel.setAnchor(Pack.UpperLeft, growX=True) diff --git a/cocoalib/ui/error_report.py b/cocoalib/ui/error_report.py deleted file mode 100644 index 5494623d..00000000 --- a/cocoalib/ui/error_report.py +++ /dev/null @@ -1,41 +0,0 @@ -ownerclass = 'HSErrorReportWindow' -ownerimport = 'HSErrorReportWindow.h' - -result = Window(524, 470, "Error Report") -result.canClose = False -result.canResize = False -result.canMinimize = False -label1 = Label(result, "Something went wrong. How about reporting the error?") -errorTextView = TextView(result) -label2 = Label(result, - "Error reports should be reported as Github issues. You can copy the error traceback " - "above and paste it in a new issue (bonus point if you run a search to make sure the " - "issue doesn't already exist). What usually really helps is if you add a description " - "of how you got the error. Thanks!" - "\n\n" - "Although the application should continue to run after this error, it may be in an " - "unstable state, so it is recommended that you restart the application." -) -sendButton = Button(result, "Go to Github") -dontSendButton = Button(result, "Close") - -owner.contentTextView = errorTextView -sendButton.action = Action(owner, 'goToGithub') -sendButton.keyEquivalent = "\\r" -dontSendButton.action = Action(owner, 'close') -dontSendButton.keyEquivalent = "\\E" - -label1.height = 34 -errorTextView.height = 221 -label2.height = 130 -sendButton.width = 100 -dontSendButton.width = 100 - -label1.moveTo(Pack.UpperLeft) -label1.fill(Pack.Right) -errorTextView.moveNextTo(label1, Pack.Below, Pack.Left) -errorTextView.fill(Pack.Right) -label2.moveNextTo(errorTextView, Pack.Below, Pack.Left) -label2.fill(Pack.Right) -sendButton.moveNextTo(label2, Pack.Below, Pack.Right) -dontSendButton.moveNextTo(sendButton, Pack.Left, Pack.Middle) diff --git a/cocoalib/ui/progress.py b/cocoalib/ui/progress.py deleted file mode 100644 index 99dcbe7b..00000000 --- a/cocoalib/ui/progress.py +++ /dev/null @@ -1,31 +0,0 @@ -ownerclass = 'ProgressController' -ownerimport = 'ProgressController.h' - -result = Window(323, 143, "Work in progress...") -result.canClose = result.canResize = result.canMinimize = False -descLabel = Label(result, "Work in progress, please wait.") -progress = ProgressIndicator(result) -statusLabel = Label(result, "Status: Working...") -cancelButton = Button(result, "Cancel") - -owner.window = result -owner.cancelButton = cancelButton -owner.descText = descLabel -owner.statusText = statusLabel -owner.progressBar = progress -result.properties['delegate'] = owner -statusLabel.font = Font(FontFamily.Label, FontSize.SmallControl) -cancelButton.keyEquivalent = '\\E' -cancelButton.action = Action(owner, 'cancel') - -descLabel.packToCorner(Pack.UpperLeft) -descLabel.fill(Pack.Right) -descLabel.setAnchor(Pack.UpperLeft, growX=True) -progress.packRelativeTo(descLabel, Pack.Below, Pack.Left) -progress.fill(Pack.Right) -progress.setAnchor(Pack.UpperLeft, growX=True) -statusLabel.packRelativeTo(progress, Pack.Below, Pack.Left) -statusLabel.fill(Pack.Right) -statusLabel.setAnchor(Pack.UpperLeft, growX=True) -cancelButton.packToCorner(Pack.LowerRight) -cancelButton.setAnchor(Pack.LowerRight) diff --git a/cocoalib/views/HSOutlineView.h b/cocoalib/views/HSOutlineView.h deleted file mode 100644 index ec524896..00000000 --- a/cocoalib/views/HSOutlineView.h +++ /dev/null @@ -1,41 +0,0 @@ -/* -Copyright 2015 Hardcoded Software (http://www.hardcoded.net) - -This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -which should be included with this package. The terms are also available at -http://www.gnu.org/licenses/gpl-3.0.html -*/ - -#import -#import "NSTableViewAdditions.h" -#import "NSIndexPathAdditions.h" - -@class HSOutlineView; - -@protocol HSOutlineViewDelegate -- (NSArray *)selectedIndexPaths; /* array of NSIndexPath* */ -- (NSString *)dataForCopyToPasteboard; -- (NSIndexPath *)internalizedPath:(NSIndexPath *)path; -- (void)outlineViewDidEndEditing:(HSOutlineView *)outlineView; -- (void)outlineViewCancelsEdition:(HSOutlineView *)outlineView; -- (void)outlineViewWasDoubleClicked:(HSOutlineView *)outlineView; -@end - -@interface HSOutlineView : NSOutlineView -{ - BOOL manualEditionStop; - NSEvent *eventToIgnore; -} -- (id )delegate; -- (void)setDelegate:(id )aDelegate; -- (NSIndexPath *)selectedPath; -- (void)selectPath:(NSIndexPath *)aPath; -- (NSArray *)selectedNodePaths; -- (void)selectNodePaths:(NSArray *)nodePaths; -- (void)stopEditing; -- (void)updateSelection; -- (void)ignoreEventForEdition:(NSEvent *)aEvent; -- (void)ensureExpanded:(NSIndexPath *)aPath; - -- (IBAction)copy:(id)sender; -@end diff --git a/cocoalib/views/HSOutlineView.m b/cocoalib/views/HSOutlineView.m deleted file mode 100644 index 14ab995c..00000000 --- a/cocoalib/views/HSOutlineView.m +++ /dev/null @@ -1,190 +0,0 @@ -/* -Copyright 2015 Hardcoded Software (http://www.hardcoded.net) - -This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -which should be included with this package. The terms are also available at -http://www.gnu.org/licenses/gpl-3.0.html -*/ - -#import "HSOutlineView.h" - -@implementation HSOutlineView -- (id )delegate -{ - return (id )[super delegate]; -} - -- (void)setDelegate:(id )aDelegate -{ - [super setDelegate:aDelegate]; - id delegate = [self delegate]; - if ([delegate respondsToSelector:@selector(outlineViewWasDoubleClicked:)]) { - [self setTarget:[self delegate]]; - [self setDoubleAction:@selector(outlineViewWasDoubleClicked:)]; - } -} -/* NSOutlineView overrides */ -- (void)keyDown:(NSEvent *)event -{ - if (![self dispatchSpecialKeys:event]) - { - [super keyDown:event]; - } -} - -- (BOOL)shouldEditTableColumn:(NSTableColumn *)column row:(NSInteger)row -{ - BOOL result = [super shouldEditTableColumn:column row:row]; - if (!result) - return result; - id delegate = [self delegate]; - if ([delegate respondsToSelector:@selector(outlineView:shouldEditTableColumn:item:)]) - return [delegate outlineView:self shouldEditTableColumn:column item:[self itemAtRow:row]]; - return YES; -} - -/* Notifications & Delegate */ -- (void)textDidEndEditing:(NSNotification *)notification -{ - notification = [self processTextDidEndEditing:notification]; - NSView *nextKeyView = [self nextKeyView]; - [self setNextKeyView:nil]; - [super textDidEndEditing:notification]; - [self setNextKeyView:nextKeyView]; - - if ([self editedColumn] == -1) - { - if (!manualEditionStop) - { - id delegate = [self delegate]; - if ([delegate respondsToSelector:@selector(outlineViewDidEndEditing:)]) - { - [delegate outlineViewDidEndEditing:self]; - } - } - // We may have lost the focus - [[self window] makeFirstResponder:self]; - } -} - -- (BOOL)textView:(NSTextView *)textView doCommandBySelector:(SEL)command -{ - if (command == @selector(cancelOperation:)) - { - [self stopEditing]; // The stop editing has to happen before the cancelEdits - id delegate = [self delegate]; - if ([delegate respondsToSelector:@selector(outlineViewCancelsEdition:)]) - { - [delegate outlineViewCancelsEdition:self]; - } - return YES; - } - return NO; -} - -/* Public */ -- (NSIndexPath *)selectedPath -{ - NSInteger row = [self selectedRow]; - return [self itemAtRow:row]; -} - -- (void)selectPath:(NSIndexPath *)aPath -{ - [self selectNodePaths:[NSArray arrayWithObject:aPath]]; -} - -- (NSArray *)selectedNodePaths -{ - NSMutableArray *r = [NSMutableArray array]; - NSIndexSet *indexes = [self selectedRowIndexes]; - NSInteger i = [indexes firstIndex]; - while (i != NSNotFound) { - NSIndexPath *path = [self itemAtRow:i]; - [r addObject:path]; - i = [indexes indexGreaterThanIndex:i]; - } - return r; -} - -- (void)selectNodePaths:(NSArray *)aPaths -{ - NSMutableIndexSet *toSelect = [NSMutableIndexSet indexSet]; - /* To ensure that we have correct row indexes, we must first expand all paths, and *then* select - * row indexes. - **/ - for (NSIndexPath *path in aPaths) { - [self ensureExpanded:path]; - } - for (NSIndexPath *path in aPaths) { - [toSelect addIndex:[self rowForItem:path]]; - } - [self selectRowIndexes:toSelect byExtendingSelection:NO]; - if ([toSelect count] > 0) { - [self scrollRowToVisible:[toSelect firstIndex]]; - } -} - -- (void)ensureExpanded:(NSIndexPath *)aPath -{ - /* Expands aPath and make sure that its parent items are expanded as well. - */ - id delegate = [self delegate]; - NSIndexPath *tmppath = [NSIndexPath indexPathWithIndex:[aPath indexAtPosition:0]]; - [self expandItem:[delegate internalizedPath:tmppath]]; - for (NSInteger i=1; i<[aPath length]; i++) { - tmppath = [tmppath indexPathByAddingIndex:[aPath indexAtPosition:i]]; - [self expandItem:[delegate internalizedPath:tmppath]]; - } -} - -- (void)stopEditing -{ - // If we're not editing, don't do anything because we don't want to steal focus from another view - if ([self editedColumn] >= 0) - { - manualEditionStop = YES; - [[self window] makeFirstResponder:self]; // This will abort edition - manualEditionStop = NO; - } -} - -- (void)updateSelection -{ - id delegate = [self delegate]; - if ([delegate respondsToSelector:@selector(selectedIndexPaths)]) { - [self selectNodePaths:[delegate selectedIndexPaths]]; - } -} - -/* Actions */ - -- (IBAction)copy:(id)sender -{ - NSString *data = [[self delegate] dataForCopyToPasteboard]; - if (data != nil) { - NSPasteboard *p = [NSPasteboard generalPasteboard]; - [p declareTypes:[NSArray arrayWithObjects:NSStringPboardType, nil] owner:nil]; - [p setString:data forType:NSStringPboardType]; - } -} - -/* BIG HACK ZONE -When tracking clicks in the NSTextField, the NSTableView goes in edition mode even if we click on the -arrow or the button. The only way I found to avoid this is this scheme: let the HSOutlineView know -of the event that caused the click, and don't go in edit mode if it happens. -*/ - -- (void)ignoreEventForEdition:(NSEvent *)aEvent -{ - eventToIgnore = aEvent; -} - -- (void)editColumn:(NSInteger)columnIndex row:(NSInteger)rowIndex withEvent:(NSEvent *)theEvent select:(BOOL)flag -{ - if ((theEvent != nil) && (theEvent == eventToIgnore)) - return; - [super editColumn:columnIndex row:rowIndex withEvent:theEvent select:flag]; -} - -@end diff --git a/cocoalib/views/HSTableView.h b/cocoalib/views/HSTableView.h deleted file mode 100644 index ad229dc0..00000000 --- a/cocoalib/views/HSTableView.h +++ /dev/null @@ -1,31 +0,0 @@ -/* -Copyright 2015 Hardcoded Software (http://www.hardcoded.net) - -This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -which should be included with this package. The terms are also available at -http://www.gnu.org/licenses/gpl-3.0.html -*/ - -#import -#import "NSTableViewAdditions.h" - -@class HSTableView; - -@protocol HSTableViewDelegate -- (NSIndexSet *)selectedIndexes; -- (void)tableViewDidEndEditing:(HSTableView *)tableView; -- (void)tableViewCancelsEdition:(HSTableView *)tableView; -- (void)tableViewWasDoubleClicked:(HSTableView *)tableView; -@end - -@interface HSTableView : NSTableView -{ - BOOL manualEditionStop; -} -- (void)updateSelection; -- (void)stopEditing; -- (id )delegate; -- (void)setDelegate:(id )aDelegate; -- (NSScrollView *)wrapInScrollView; -@end - diff --git a/cocoalib/views/HSTableView.m b/cocoalib/views/HSTableView.m deleted file mode 100644 index ea045f87..00000000 --- a/cocoalib/views/HSTableView.m +++ /dev/null @@ -1,102 +0,0 @@ -/* -Copyright 2015 Hardcoded Software (http://www.hardcoded.net) - -This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -which should be included with this package. The terms are also available at -http://www.gnu.org/licenses/gpl-3.0.html -*/ - -#import "HSTableView.h" -#import "NSEventAdditions.h" - -@implementation HSTableView -/* NSTableView */ -- (void)keyDown:(NSEvent *)event -{ - if (![self dispatchSpecialKeys:event]) { - [super keyDown:event]; - } -} - -- (id )delegate -{ - return (id )[super delegate]; -} - -- (void)setDelegate:(id )aDelegate -{ - [super setDelegate:aDelegate]; - id delegate = [self delegate]; - if ([delegate respondsToSelector:@selector(tableViewWasDoubleClicked:)]) { - [self setTarget:[self delegate]]; - [self setDoubleAction:@selector(tableViewWasDoubleClicked:)]; - } -} - -- (void)textDidEndEditing:(NSNotification *)notification -{ - notification = [self processTextDidEndEditing:notification]; - NSView *nextKeyView = [self nextKeyView]; - [self setNextKeyView:nil]; - [super textDidEndEditing:notification]; - [self setNextKeyView:nextKeyView]; - - if ([self editedColumn] == -1) { - if (!manualEditionStop) { - id delegate = [self delegate]; - if ([delegate respondsToSelector:@selector(tableViewDidEndEditing:)]) { - [delegate tableViewDidEndEditing:self]; - } - } - // We may have lost the focus - [[self window] makeFirstResponder:self]; - } -} - -/* NSTextView delegate */ -- (BOOL)textView:(NSTextView *)textView doCommandBySelector:(SEL)command -{ - if (command == @selector(cancelOperation:)) { - [self stopEditing]; // The stop editing has to happen before the cancelEdits - id delegate = [self delegate]; - if ([delegate respondsToSelector:@selector(tableViewCancelsEdition:)]) { - [delegate tableViewCancelsEdition:self]; - } - return YES; - } - return NO; -} - -/* Public methods */ - -- (void)updateSelection -{ - NSIndexSet *selection = [[self delegate] selectedIndexes]; - [self selectRowIndexes:selection byExtendingSelection:NO]; -} - -// Calling this does not result in a tableViewDidEndEditing: call -- (void)stopEditing -{ - // If we're not editing, don't do anything because we don't want to steal focus from another view - if ([self editedColumn] >= 0) { - manualEditionStop = YES; - [[self window] makeFirstResponder:self]; // This will abort edition - manualEditionStop = NO; - } -} - -- (NSScrollView *)wrapInScrollView; -{ - /* When programmatically creating an NSTableView, we have to wrap it in a scroll view for it to - behave properly. - */ - NSScrollView *container = [[NSScrollView alloc] initWithFrame:NSMakeRect(0, 0, 100, 100)]; - [container setDocumentView:self]; - [container setHasVerticalScroller:YES]; - [container setHasHorizontalScroller:YES]; - [container setAutohidesScrollers:YES]; - return [container autorelease]; -} - -@end diff --git a/cocoalib/views/NSIndexPathAdditions.h b/cocoalib/views/NSIndexPathAdditions.h deleted file mode 100644 index 376adf2b..00000000 --- a/cocoalib/views/NSIndexPathAdditions.h +++ /dev/null @@ -1,20 +0,0 @@ -/* -Copyright 2015 Hardcoded Software (http://www.hardcoded.net) - -This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -which should be included with this package. The terms are also available at -http://www.gnu.org/licenses/gpl-3.0.html -*/ - -#import - -/* This is a hack to easily get around a cocoa limitation - -In some weird circumstances, NSOutlineView calls [item indexPath] to the item instances given to -it. I guess is expects eveyone to give it NSTreeNode instances. Anyway, because in MG, simple -NSIndexPath are used, it causes problems. Not anymore. -*/ - -@interface NSIndexPath(NSIndexPathAdditions) -- (NSIndexPath *)indexPath; -@end \ No newline at end of file diff --git a/cocoalib/views/NSIndexPathAdditions.m b/cocoalib/views/NSIndexPathAdditions.m deleted file mode 100644 index b8590a17..00000000 --- a/cocoalib/views/NSIndexPathAdditions.m +++ /dev/null @@ -1,16 +0,0 @@ -/* -Copyright 2015 Hardcoded Software (http://www.hardcoded.net) - -This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -which should be included with this package. The terms are also available at -http://www.gnu.org/licenses/gpl-3.0.html -*/ - -#import "NSIndexPathAdditions.h" - -@implementation NSIndexPath(NSIndexPathAdditions) -- (NSIndexPath *)indexPath -{ - return self; -} -@end \ No newline at end of file diff --git a/cocoalib/views/NSTableViewAdditions.h b/cocoalib/views/NSTableViewAdditions.h deleted file mode 100644 index 308a0415..00000000 --- a/cocoalib/views/NSTableViewAdditions.h +++ /dev/null @@ -1,24 +0,0 @@ -/* -Copyright 2015 Hardcoded Software (http://www.hardcoded.net) - -This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -which should be included with this package. The terms are also available at -http://www.gnu.org/licenses/gpl-3.0.html -*/ - -#import - - -@interface NSTableView(NSTableViewAdditions) -- (BOOL)dispatchSpecialKeys:(NSEvent *)event; -- (NSNotification *)processTextDidEndEditing:(NSNotification *)notification; -- (void)startEditing; -@end - -@interface NSObject(NSTableViewAdditionsDelegate) -- (BOOL)tableViewHadDeletePressed:(NSTableView *)tableView; -- (BOOL)tableViewHadReturnPressed:(NSTableView *)tableView; -- (BOOL)tableViewHadSpacePressed:(NSTableView *)tableView; -- (BOOL)tableView:(NSTableView *)tableView receivedKeyEvent:(NSEvent *)aEvent; -- (BOOL)shouldEditTableColumn:(NSTableColumn *)column row:(NSInteger)row; -@end \ No newline at end of file diff --git a/cocoalib/views/NSTableViewAdditions.m b/cocoalib/views/NSTableViewAdditions.m deleted file mode 100644 index 0306d494..00000000 --- a/cocoalib/views/NSTableViewAdditions.m +++ /dev/null @@ -1,118 +0,0 @@ -/* -Copyright 2015 Hardcoded Software (http://www.hardcoded.net) - -This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -which should be included with this package. The terms are also available at -http://www.gnu.org/licenses/gpl-3.0.html -*/ - -#import "NSTableViewAdditions.h" -#import "NSEventAdditions.h" -#import "Utils.h" - -@implementation NSTableView(NSTableViewAdditions) - -/* Private methods */ - -// Alright, this is a hack. It has been added to put in common some table and outline code, but the -// thing is an outline view delegate doesn't use tableView:shouldEditTableColumn:row:. Anyway, for -// the outline, just using [column isEditable] works in moneyGuru for now, so we can keep it that way. -- (BOOL)shouldEditTableColumn:(NSTableColumn *)column row:(NSInteger)row -{ - if (![column isEditable]) - return NO; - id delegate = [self delegate]; - if ([delegate respondsToSelector:@selector(tableView:shouldEditTableColumn:row:)]) - { - return [delegate tableView:self shouldEditTableColumn:column row:row]; - } - else - { - return YES; - } -} - -/* Public Methods */ - -// Returns whether the responder chain should be stopeed or not -- (BOOL)dispatchSpecialKeys:(NSEvent *)event -{ - id delegate = [self delegate]; - if ([delegate respondsToSelector:@selector(tableView:receivedKeyEvent:)]) { - if ([delegate tableView:self receivedKeyEvent:event]) { - return YES; - } - } - BOOL stopChain = NO; - if ([event isDeleteOrBackspace] && [delegate respondsToSelector:@selector(tableViewHadDeletePressed:)]) - { - stopChain = [delegate tableViewHadDeletePressed:self]; - } - if ([event isReturnOrEnter] && [delegate respondsToSelector:@selector(tableViewHadReturnPressed:)]) - { - stopChain = [delegate tableViewHadReturnPressed:self]; - } - if ([event isSpace] && [delegate respondsToSelector:@selector(tableViewHadSpacePressed:)]) - { - stopChain = [delegate tableViewHadSpacePressed:self]; - } - if ([event isTab]) - { - stopChain = YES; - [[self window] makeFirstResponder:[self nextValidKeyView]]; - } - if ([event isBackTab]) - { - stopChain = YES; - // Ok, this is a big hack. the normal handling of NSTableView must handle this, but we must skip over - // a NSClipView and a NSScrollView before getting to the actual previousValidKeyView. - // However, when we are not in Full Keyboard Access mode, there's no problem. Thus, we assume that - // when previousValidKeyView's class is a NSClipView, it means we must perform the hack - NSView *previous = [self previousValidKeyView]; - if ([[previous className] isEqualTo:@"NSClipView"]) // Can't use isKindOfClass, we don't want to test for a subclass - previous = [[previous previousValidKeyView] previousValidKeyView]; - [[self window] makeFirstResponder:previous]; - } - return stopChain; -} - -- (NSNotification *)processTextDidEndEditing:(NSNotification *)notification; -{ - NSDictionary *userInfo = [notification userInfo]; - int textMovement = [[userInfo valueForKey:@"NSTextMovement"] intValue]; - if (textMovement == NSReturnTextMovement) - { - // Stop editing - NSMutableDictionary *newInfo; - newInfo = [NSMutableDictionary dictionaryWithDictionary:userInfo]; - [newInfo setObject:[NSNumber numberWithInt:NSIllegalTextMovement] forKey:@"NSTextMovement"]; - notification = [NSNotification notificationWithName:[notification name] object:[notification object] userInfo:newInfo]; - } - return notification; -} - -- (void)startEditing -{ - // Make sure one row is selected - if ([self selectedRow] == -1) - { - return; - } - - // We only want to edit columns that are editable. If there aren't any, don't edit. - for (NSInteger i=0;i<[[self tableColumns] count];i++) { - NSTableColumn *col = [[self tableColumns] objectAtIndex:i]; - if ([col isHidden]) { - continue; - } - if (![self shouldEditTableColumn:col row:[self selectedRow]]) { - continue; - } - // We only want one row to be selected. - NSIndexSet *selection = [NSIndexSet indexSetWithIndex:[self selectedRow]]; - [self selectRowIndexes:selection byExtendingSelection:NO]; - [self editColumn:i row:[self selectedRow] withEvent:nil select:YES]; - break; - } -} -@end diff --git a/hscommon b/hscommon new file mode 160000 index 00000000..ea634cef --- /dev/null +++ b/hscommon @@ -0,0 +1 @@ +Subproject commit ea634cefdf78ae9e4c7470e571fce859760f6f38 diff --git a/hscommon/.gitignore b/hscommon/.gitignore deleted file mode 100644 index 642f9e32..00000000 --- a/hscommon/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -*.pyc -*.mo -*.so -.DS_Store -/docs_html \ No newline at end of file diff --git a/hscommon/LICENSE b/hscommon/LICENSE deleted file mode 100644 index 5a8d3ceb..00000000 --- a/hscommon/LICENSE +++ /dev/null @@ -1,10 +0,0 @@ -Copyright 2014, 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. - -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/hscommon/README b/hscommon/README deleted file mode 100644 index acc2ccbc..00000000 --- a/hscommon/README +++ /dev/null @@ -1,3 +0,0 @@ -This module is common code used in all Hardcoded Software applications. It has no stable API so -it is not recommended to actually depend on it. But if you want to copy bits and pieces for your own -apps, be my guest. diff --git a/hscommon/__init__.py b/hscommon/__init__.py deleted file mode 100755 index e69de29b..00000000 diff --git a/hscommon/build.py b/hscommon/build.py deleted file mode 100644 index 1547454e..00000000 --- a/hscommon/build.py +++ /dev/null @@ -1,486 +0,0 @@ -# Created By: Virgil Dupras -# Created On: 2009-03-03 -# Copyright 2015 Hardcoded Software (http://www.hardcoded.net) - -# This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at -# http://www.gnu.org/licenses/gpl-3.0.html - -"""This module is a collection of function to help in HS apps build process. -""" - -import os -import sys -import os.path as op -import shutil -import tempfile -import plistlib -from subprocess import Popen -import re -import importlib -from datetime import datetime -import glob -import sysconfig -import modulefinder - -from setuptools import setup, Extension - -from .plat import ISWINDOWS -from .util import modified_after, find_in_path, ensure_folder, delete_files_with_pattern - -def print_and_do(cmd): - """Prints ``cmd`` and executes it in the shell. - """ - print(cmd) - p = Popen(cmd, shell=True) - return p.wait() - -def _perform(src, dst, action, actionname): - if not op.lexists(src): - print("Copying %s failed: it doesn't exist." % src) - return - if op.lexists(dst): - if op.isdir(dst): - shutil.rmtree(dst) - else: - os.remove(dst) - print('%s %s --> %s' % (actionname, src, dst)) - action(src, dst) - -def copy_file_or_folder(src, dst): - if op.isdir(src): - shutil.copytree(src, dst, symlinks=True) - else: - shutil.copy(src, dst) - -def move(src, dst): - _perform(src, dst, os.rename, 'Moving') - -def copy(src, dst): - _perform(src, dst, copy_file_or_folder, 'Copying') - -def symlink(src, dst): - _perform(src, dst, os.symlink, 'Symlinking') - -def hardlink(src, dst): - _perform(src, dst, os.link, 'Hardlinking') - -def _perform_on_all(pattern, dst, action): - # pattern is a glob pattern, example "folder/foo*". The file is moved directly in dst, no folder - # structure from src is kept. - filenames = glob.glob(pattern) - for fn in filenames: - destpath = op.join(dst, op.basename(fn)) - action(fn, destpath) - -def move_all(pattern, dst): - _perform_on_all(pattern, dst, move) - -def copy_all(pattern, dst): - _perform_on_all(pattern, dst, copy) - -def ensure_empty_folder(path): - """Make sure that the path exists and that it's an empty folder. - """ - if op.exists(path): - shutil.rmtree(path) - os.mkdir(path) - -def filereplace(filename, outfilename=None, **kwargs): - """Reads `filename`, replaces all {variables} in kwargs, and writes the result to `outfilename`. - """ - if outfilename is None: - outfilename = filename - fp = open(filename, 'rt', encoding='utf-8') - contents = fp.read() - fp.close() - # We can't use str.format() because in some files, there might be {} characters that mess with it. - for key, item in kwargs.items(): - contents = contents.replace('{{{}}}'.format(key), item) - fp = open(outfilename, 'wt', encoding='utf-8') - fp.write(contents) - fp.close() - -def get_module_version(modulename): - mod = importlib.import_module(modulename) - return mod.__version__ - -def setup_package_argparser(parser): - parser.add_argument( - '--sign', dest='sign_identity', - help="Sign app under specified identity before packaging (OS X only)" - ) - parser.add_argument( - '--nosign', action='store_true', dest='nosign', - help="Don't sign the packaged app (OS X only)" - ) - parser.add_argument( - '--src-pkg', action='store_true', dest='src_pkg', - help="Build a tar.gz of the current source." - ) - parser.add_argument( - '--arch-pkg', action='store_true', dest='arch_pkg', - help="Force Arch Linux packaging type, regardless of distro name." - ) - -# `args` come from an ArgumentParser updated with setup_package_argparser() -def package_cocoa_app_in_dmg(app_path, destfolder, args): - # Rather than signing our app in XCode during the build phase, we sign it during the package - # phase because running the app before packaging can modify it and we want to be sure to have - # a valid signature. - if args.sign_identity: - sign_identity = "Developer ID Application: {}".format(args.sign_identity) - result = print_and_do('codesign --force --deep --sign "{}" "{}"'.format(sign_identity, app_path)) - if result != 0: - print("ERROR: Signing failed. Aborting packaging.") - return - elif not args.nosign: - print("ERROR: Either --nosign or --sign argument required.") - return - build_dmg(app_path, destfolder) - -def build_dmg(app_path, destfolder): - """Builds a DMG volume with application at ``app_path`` and puts it in ``dest_path``. - - The name of the resulting DMG volume is determined by the app's name and version. - """ - print(repr(op.join(app_path, 'Contents', 'Info.plist'))) - plist = plistlib.readPlist(op.join(app_path, 'Contents', 'Info.plist')) - workpath = tempfile.mkdtemp() - dmgpath = op.join(workpath, plist['CFBundleName']) - os.mkdir(dmgpath) - print_and_do('cp -R "%s" "%s"' % (app_path, dmgpath)) - print_and_do('ln -s /Applications "%s"' % op.join(dmgpath, 'Applications')) - dmgname = '%s_osx_%s.dmg' % (plist['CFBundleName'].lower().replace(' ', '_'), plist['CFBundleVersion'].replace('.', '_')) - print('Building %s' % dmgname) - # UDBZ = bzip compression. UDZO (zip compression) was used before, but it compresses much less. - print_and_do('hdiutil create "%s" -format UDBZ -nocrossdev -srcdir "%s"' % (op.join(destfolder, dmgname), dmgpath)) - print('Build Complete') - -def copy_sysconfig_files_for_embed(destpath): - # This normally shouldn't be needed for Python 3.3+. - makefile = sysconfig.get_makefile_filename() - configh = sysconfig.get_config_h_filename() - shutil.copy(makefile, destpath) - shutil.copy(configh, destpath) - with open(op.join(destpath, 'site.py'), 'w') as fp: - fp.write(""" -import os.path as op -from distutils import sysconfig -sysconfig.get_makefile_filename = lambda: op.join(op.dirname(__file__), 'Makefile') -sysconfig.get_config_h_filename = lambda: op.join(op.dirname(__file__), 'pyconfig.h') -""") - -def add_to_pythonpath(path): - """Adds ``path`` to both ``PYTHONPATH`` env and ``sys.path``. - """ - abspath = op.abspath(path) - pythonpath = os.environ.get('PYTHONPATH', '') - pathsep = ';' if ISWINDOWS else ':' - pythonpath = pathsep.join([abspath, pythonpath]) if pythonpath else abspath - os.environ['PYTHONPATH'] = pythonpath - sys.path.insert(1, abspath) - -# This is a method to hack around those freakingly tricky data inclusion/exlusion rules -# in setuptools. We copy the packages *without data* in a build folder and then build the plugin -# from there. -def copy_packages(packages_names, dest, create_links=False, extra_ignores=None): - """Copy python packages ``packages_names`` to ``dest``, spurious data. - - Copy will happen without tests, testdata, mercurial data or C extension module source with it. - ``py2app`` include and exclude rules are **quite** funky, and doing this is the only reliable - way to make sure we don't end up with useless stuff in our app. - """ - if ISWINDOWS: - create_links = False - if not extra_ignores: - extra_ignores = [] - ignore = shutil.ignore_patterns('.hg*', 'tests', 'testdata', 'modules', 'docs', 'locale', *extra_ignores) - for package_name in packages_names: - if op.exists(package_name): - source_path = package_name - else: - mod = __import__(package_name) - source_path = mod.__file__ - if mod.__file__.endswith('__init__.py'): - source_path = op.dirname(source_path) - dest_name = op.basename(source_path) - dest_path = op.join(dest, dest_name) - if op.exists(dest_path): - if op.islink(dest_path): - os.unlink(dest_path) - else: - shutil.rmtree(dest_path) - print("Copying package at {0} to {1}".format(source_path, dest_path)) - if create_links: - os.symlink(op.abspath(source_path), dest_path) - else: - if op.isdir(source_path): - shutil.copytree(source_path, dest_path, ignore=ignore) - else: - shutil.copy(source_path, dest_path) - -def copy_qt_plugins(folder_names, dest): # This is only for Windows - from PyQt5.QtCore import QLibraryInfo - qt_plugin_dir = QLibraryInfo.location(QLibraryInfo.PluginsPath) - def ignore(path, names): - if path == qt_plugin_dir: - return [n for n in names if n not in folder_names] - else: - return [n for n in names if not n.endswith('.dll')] - shutil.copytree(qt_plugin_dir, dest, ignore=ignore) - -def build_debian_changelog(changelogpath, destfile, pkgname, from_version=None, - distribution='precise', fix_version=None): - """Builds a debian changelog out of a YAML changelog. - - Use fix_version to patch the top changelog to that version (if, for example, there was a - packaging error and you need to quickly fix it) - """ - def desc2list(desc): - # We take each item, enumerated with the '*' character, and transform it into a list. - desc = desc.replace('\n', ' ') - desc = desc.replace(' ', ' ') - result = desc.split('*') - return [s.strip() for s in result if s.strip()] - - ENTRY_MODEL = "{pkg} ({version}~{distribution}) {distribution}; urgency=low\n\n{changes}\n -- Virgil Dupras {date}\n\n" - CHANGE_MODEL = " * {description}\n" - changelogs = read_changelog_file(changelogpath) - if from_version: - # We only want logs from a particular version - for index, log in enumerate(changelogs): - if log['version'] == from_version: - changelogs = changelogs[:index+1] - break - if fix_version: - changelogs[0]['version'] = fix_version - rendered_logs = [] - for log in changelogs: - version = log['version'] - logdate = log['date'] - desc = log['description'] - rendered_date = logdate.strftime('%a, %d %b %Y 00:00:00 +0000') - rendered_descs = [CHANGE_MODEL.format(description=d) for d in desc2list(desc)] - changes = ''.join(rendered_descs) - rendered_log = ENTRY_MODEL.format(pkg=pkgname, version=version, changes=changes, - date=rendered_date, distribution=distribution) - rendered_logs.append(rendered_log) - result = ''.join(rendered_logs) - fp = open(destfile, 'w') - fp.write(result) - fp.close() - -re_changelog_header = re.compile(r'=== ([\d.b]*) \(([\d\-]*)\)') -def read_changelog_file(filename): - def iter_by_three(it): - while True: - version = next(it) - date = next(it) - description = next(it) - yield version, date, description - - with open(filename, 'rt', encoding='utf-8') as fp: - contents = fp.read() - splitted = re_changelog_header.split(contents)[1:] # the first item is empty - # splitted = [version1, date1, desc1, version2, date2, ...] - result = [] - for version, date_str, description in iter_by_three(iter(splitted)): - date = datetime.strptime(date_str, '%Y-%m-%d').date() - d = {'date': date, 'date_str': date_str, 'version': version, 'description': description.strip()} - result.append(d) - return result - -class OSXAppStructure: - def __init__(self, dest): - self.dest = dest - self.contents = op.join(dest, 'Contents') - self.macos = op.join(self.contents, 'MacOS') - self.resources = op.join(self.contents, 'Resources') - self.frameworks = op.join(self.contents, 'Frameworks') - self.infoplist = op.join(self.contents, 'Info.plist') - - def create(self, infoplist): - ensure_empty_folder(self.dest) - os.makedirs(self.macos) - os.mkdir(self.resources) - os.mkdir(self.frameworks) - copy(infoplist, self.infoplist) - open(op.join(self.contents, 'PkgInfo'), 'wt').write("APPLxxxx") - - def copy_executable(self, executable): - info = plistlib.readPlist(self.infoplist) - self.executablename = info['CFBundleExecutable'] - self.executablepath = op.join(self.macos, self.executablename) - copy(executable, self.executablepath) - - def copy_resources(self, *resources, use_symlinks=False): - for path in resources: - resource_dest = op.join(self.resources, op.basename(path)) - action = symlink if use_symlinks else copy - action(op.abspath(path), resource_dest) - - def copy_frameworks(self, *frameworks): - for path in frameworks: - framework_dest = op.join(self.frameworks, op.basename(path)) - copy(path, framework_dest) - - -def create_osx_app_structure(dest, executable, infoplist, resources=None, frameworks=None, - symlink_resources=False): - # `dest`: A path to the destination .app folder - # `executable`: the path of the executable file that goes in "MacOS" - # `infoplist`: The path to your Info.plist file. - # `resources`: A list of paths of files or folders going in the "Resources" folder. - # `frameworks`: Same as above for "Frameworks". - # `symlink_resources`: If True, will symlink resources into the structure instead of copying them. - app = OSXAppStructure(dest, infoplist) - app.create() - app.copy_executable(executable) - app.copy_resources(*resources, use_symlinks=symlink_resources) - app.copy_frameworks(*frameworks) - -class OSXFrameworkStructure: - def __init__(self, dest): - self.dest = dest - self.contents = op.join(dest, 'Versions', 'A') - self.resources = op.join(self.contents, 'Resources') - self.headers = op.join(self.contents, 'Headers') - self.infoplist = op.join(self.resources, 'Info.plist') - self._update_executable_path() - - def _update_executable_path(self): - if not op.exists(self.infoplist): - self.executablename = self.executablepath = None - return - info = plistlib.readPlist(self.infoplist) - self.executablename = info['CFBundleExecutable'] - self.executablepath = op.join(self.contents, self.executablename) - - def create(self, infoplist): - ensure_empty_folder(self.dest) - os.makedirs(self.contents) - os.mkdir(self.resources) - os.mkdir(self.headers) - copy(infoplist, self.infoplist) - self._update_executable_path() - - def create_symlinks(self): - # Only call this after create() and copy_executable() - rel = lambda path: op.relpath(path, self.dest) - os.symlink('A', op.join(self.dest, 'Versions', 'Current')) - os.symlink(rel(self.executablepath), op.join(self.dest, self.executablename)) - os.symlink(rel(self.headers), op.join(self.dest, 'Headers')) - os.symlink(rel(self.resources), op.join(self.dest, 'Resources')) - - def copy_executable(self, executable): - copy(executable, self.executablepath) - - def copy_resources(self, *resources, use_symlinks=False): - for path in resources: - resource_dest = op.join(self.resources, op.basename(path)) - action = symlink if use_symlinks else copy - action(op.abspath(path), resource_dest) - - def copy_headers(self, *headers, use_symlinks=False): - for path in headers: - header_dest = op.join(self.headers, op.basename(path)) - action = symlink if use_symlinks else copy - action(op.abspath(path), header_dest) - - -def build_cocoalib_xibless(dest='cocoa/autogen'): - import xibless - ensure_folder(dest) - FNPAIRS = [ - ('progress.py', 'ProgressController_UI'), - ('error_report.py', 'HSErrorReportWindow_UI'), - ('about.py', 'HSAboutBox_UI'), - ] - for srcname, dstname in FNPAIRS: - srcpath = op.join('cocoalib', 'ui', srcname) - dstpath = op.join(dest, dstname) - if modified_after(srcpath, dstpath + '.h'): - xibless.generate(srcpath, dstpath, localizationTable='cocoalib') - -def copy_embeddable_python_dylib(dst): - runtime = op.join(sysconfig.get_config_var('PYTHONFRAMEWORKPREFIX'), sysconfig.get_config_var('LDLIBRARY')) - filedest = op.join(dst, 'Python') - shutil.copy(runtime, filedest) - os.chmod(filedest, 0o774) # We need write permission to use install_name_tool - cmd = 'install_name_tool -id @rpath/Python %s' % filedest - print_and_do(cmd) - -def collect_stdlib_dependencies(script, dest_folder, extra_deps=None): - sysprefix = sys.prefix # could be a virtualenv - real_lib_prefix = sysconfig.get_config_var('LIBDEST') - def is_stdlib_path(path): - # A module path is only a stdlib path if it's in either sys.prefix or - # sysconfig.get_config_var('prefix') (the 2 are different if we are in a virtualenv) and if - # there's no "site-package in the path. - if not path: - return False - if 'site-package' in path: - return False - if not (path.startswith(sysprefix) or path.startswith(real_lib_prefix)): - return False - return True - - ensure_folder(dest_folder) - mf = modulefinder.ModuleFinder() - mf.run_script(script) - modpaths = [mod.__file__ for mod in mf.modules.values()] - modpaths = filter(is_stdlib_path, modpaths) - for p in modpaths: - if p.startswith(real_lib_prefix): - relpath = op.relpath(p, real_lib_prefix) - elif p.startswith(sysprefix): - relpath = op.relpath(p, sysprefix) - assert relpath.startswith('lib/python3.') # we want to get rid of that lib/python3.x part - relpath = relpath[len('lib/python3.X/'):] - else: - raise AssertionError() - if relpath.startswith('lib-dynload'): # We copy .so files in lib-dynload directly in our dest - relpath = relpath[len('lib-dynload/'):] - if relpath.startswith('encodings') or relpath.startswith('distutils'): - # We force their inclusion later. - continue - dest_path = op.join(dest_folder, relpath) - ensure_folder(op.dirname(dest_path)) - copy(p, dest_path) - # stringprep is used by encodings. - # We use real_lib_prefix with distutils because virtualenv messes with it and we need to refer - # to the original distutils folder. - FORCED_INCLUSION = ['encodings', 'stringprep', op.join(real_lib_prefix, 'distutils')] - if extra_deps: - FORCED_INCLUSION += extra_deps - copy_packages(FORCED_INCLUSION, dest_folder) - # There's a couple of rather big exe files in the distutils folder that we absolutely don't - # need. Remove them. - delete_files_with_pattern(op.join(dest_folder, 'distutils'), '*.exe') - # And, finally, create an empty "site.py" that Python needs around on startup. - open(op.join(dest_folder, 'site.py'), 'w').close() - -def fix_qt_resource_file(path): - # pyrcc5 under Windows, if the locale is non-english, can produce a source file with a date - # containing accented characters. If it does, the encoding is wrong and it prevents the file - # from being correctly frozen by cx_freeze. To work around that, we open the file, strip all - # comments, and save. - with open(path, 'rb') as fp: - contents = fp.read() - lines = contents.split(b'\n') - lines = [l for l in lines if not l.startswith(b'#')] - with open(path, 'wb') as fp: - fp.write(b'\n'.join(lines)) - -def build_cocoa_ext(extname, dest, source_files, extra_frameworks=(), extra_includes=()): - extra_link_args = ["-framework", "CoreFoundation", "-framework", "Foundation"] - for extra in extra_frameworks: - extra_link_args += ['-framework', extra] - ext = Extension(extname, source_files, extra_link_args=extra_link_args, include_dirs=extra_includes) - setup(script_args=['build_ext', '--inplace'], ext_modules=[ext]) - # Our problem here is to get the fully qualified filename of the resulting .so but I couldn't - # find a documented way to do so. The only thing I could find is this below :( - fn = ext._file_name - assert op.exists(fn) - move(fn, op.join(dest, fn)) diff --git a/hscommon/conflict.py b/hscommon/conflict.py deleted file mode 100644 index 9a62aa90..00000000 --- a/hscommon/conflict.py +++ /dev/null @@ -1,79 +0,0 @@ -# Created By: Virgil Dupras -# Created On: 2008-01-08 -# Copyright 2015 Hardcoded Software (http://www.hardcoded.net) - -# This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at -# http://www.gnu.org/licenses/gpl-3.0.html - -"""When you have to deal with names that have to be unique and can conflict together, you can use -this module that deals with conflicts by prepending unique numbers in ``[]`` brackets to the name. -""" - -import re -import os -import shutil - -from .path import Path, pathify - -#This matches [123], but not [12] (3 digits being the minimum). -#It also matches [1234] [12345] etc.. -#And only at the start of the string -re_conflict = re.compile(r'^\[\d{3}\d*\] ') - -def get_conflicted_name(other_names, name): - """Returns name with a ``[000]`` number in front of it. - - The number between brackets depends on how many conlicted filenames - there already are in other_names. - """ - name = get_unconflicted_name(name) - if name not in other_names: - return name - i = 0 - while True: - newname = '[%03d] %s' % (i, name) - if newname not in other_names: - return newname - i += 1 - -def get_unconflicted_name(name): - """Returns ``name`` without ``[]`` brackets. - - Brackets which, of course, might have been added by func:`get_conflicted_name`. - """ - return re_conflict.sub('',name,1) - -def is_conflicted(name): - """Returns whether ``name`` is prepended with a bracketed number. - """ - return re_conflict.match(name) is not None - -@pathify -def _smart_move_or_copy(operation, source_path: Path, dest_path: Path): - """Use move() or copy() to move and copy file with the conflict management. - """ - if dest_path.isdir() and not source_path.isdir(): - dest_path = dest_path[source_path.name] - if dest_path.exists(): - filename = dest_path.name - dest_dir_path = dest_path.parent() - newname = get_conflicted_name(os.listdir(str(dest_dir_path)), filename) - dest_path = dest_dir_path[newname] - operation(str(source_path), str(dest_path)) - -def smart_move(source_path, dest_path): - """Same as :func:`smart_copy`, but it moves files instead. - """ - _smart_move_or_copy(shutil.move, source_path, dest_path) - -def smart_copy(source_path, dest_path): - """Copies ``source_path`` to ``dest_path``, recursively and with conflict resolution. - """ - try: - _smart_move_or_copy(shutil.copy, source_path, dest_path) - except IOError as e: - if e.errno in {21, 13}: # it's a directory, code is 21 on OS X / Linux and 13 on Windows - _smart_move_or_copy(shutil.copytree, source_path, dest_path) - else: - raise \ No newline at end of file diff --git a/hscommon/currency.py b/hscommon/currency.py deleted file mode 100644 index 9c6c1363..00000000 --- a/hscommon/currency.py +++ /dev/null @@ -1,533 +0,0 @@ -# Created By: Virgil Dupras -# Created On: 2008-04-20 -# Copyright 2015 Hardcoded Software (http://www.hardcoded.net) - -# This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at -# http://www.gnu.org/licenses/gpl-3.0.html - -"""This module facilitates currencies management. It exposes :class:`Currency` which lets you -easily figure out their exchange value. -""" - -import os -from datetime import datetime, date, timedelta -import logging -import sqlite3 as sqlite -import threading -from queue import Queue, Empty - -from .path import Path -from .util import iterdaterange - -class Currency: - """Represents a currency and allow easy exchange rate lookups. - - A ``Currency`` instance is created with either a 3-letter ISO code or with a full name. If it's - present in the database, an instance will be returned. If not, ``ValueError`` is raised. The - easiest way to access a currency instance, however, if by using module-level constants. For - example:: - - >>> from hscommon.currency import USD, EUR - >>> from datetime import date - >>> USD.value_in(EUR, date.today()) - 0.6339119851386843 - - Unless a :class:`RatesDB` global instance is set through :meth:`Currency.set_rate_db` however, - only fallback values will be used as exchange rates. - """ - all = [] - by_code = {} - by_name = {} - rates_db = None - - def __new__(cls, code=None, name=None): - """Returns the currency with the given code.""" - assert (code is None and name is not None) or (code is not None and name is None) - if code is not None: - try: - return cls.by_code[code] - except KeyError: - raise ValueError('Unknown currency code: %r' % code) - else: - try: - return cls.by_name[name] - except KeyError: - raise ValueError('Unknown currency name: %r' % name) - - def __getnewargs__(self): - return (self.code,) - - def __getstate__(self): - return None - - def __setstate__(self, state): - pass - - def __repr__(self): - return '' % self.code - - @staticmethod - def register(code, name, exponent=2, start_date=None, start_rate=1, stop_date=None, latest_rate=1): - """Registers a new currency and returns it.""" - assert code not in Currency.by_code - assert name not in Currency.by_name - currency = object.__new__(Currency) - currency.code = code - currency.name = name - currency.exponent = exponent - currency.start_date = start_date - currency.start_rate = start_rate - currency.stop_date = stop_date - currency.latest_rate = latest_rate - Currency.by_code[code] = currency - Currency.by_name[name] = currency - Currency.all.append(currency) - return currency - - @staticmethod - def set_rates_db(db): - """Sets a new currency ``RatesDB`` instance to be used with all ``Currency`` instances. - """ - Currency.rates_db = db - - @staticmethod - def get_rates_db(): - """Returns the current ``RatesDB`` instance. - """ - if Currency.rates_db is None: - Currency.rates_db = RatesDB() # Make sure we always have some db to work with - return Currency.rates_db - - def rates_date_range(self): - """Returns the range of date for which rates are available for this currency.""" - return self.get_rates_db().date_range(self.code) - - def value_in(self, currency, date): - """Returns the value of this currency in terms of the other currency on the given date.""" - if self.start_date is not None and date < self.start_date: - return self.start_rate - elif self.stop_date is not None and date > self.stop_date: - return self.latest_rate - else: - return self.get_rates_db().get_rate(date, self.code, currency.code) - - def set_CAD_value(self, value, date): - """Sets the currency's value in CAD on the given date.""" - self.get_rates_db().set_CAD_value(date, self.code, value) - - -BUILTIN_CURRENCIES = { -# In order we want to list them -Currency.register('USD', 'U.S. dollar', - start_date=date(1998, 1, 2), start_rate=1.425, latest_rate=1.0128), -Currency.register('EUR', 'European Euro', - start_date=date(1999, 1, 4), start_rate=1.8123, latest_rate=1.3298), -Currency.register('GBP', 'U.K. pound sterling', - start_date=date(1998, 1, 2), start_rate=2.3397, latest_rate=1.5349), -Currency.register('CAD', 'Canadian dollar', - latest_rate=1), -Currency.register('AUD', 'Australian dollar', - start_date=date(1998, 1, 2), start_rate=0.9267, latest_rate=0.9336), -Currency.register('JPY', 'Japanese yen', - exponent=0, start_date=date(1998, 1, 2), start_rate=0.01076, latest_rate=0.01076), -Currency.register('INR', 'Indian rupee', - start_date=date(1998, 1, 2), start_rate=0.03627, latest_rate=0.02273), -Currency.register('NZD', 'New Zealand dollar', - start_date=date(1998, 1, 2), start_rate=0.8225, latest_rate=0.7257), -Currency.register('CHF', 'Swiss franc', - start_date=date(1998, 1, 2), start_rate=0.9717, latest_rate=0.9273), -Currency.register('ZAR', 'South African rand', - start_date=date(1998, 1, 2), start_rate=0.292, latest_rate=0.1353), -# The rest, alphabetical -Currency.register('AED', 'U.A.E. dirham', - start_date=date(2007, 9, 4), start_rate=0.2858, latest_rate=0.2757), -Currency.register('ANG', 'Neth. Antilles florin', - start_date=date(1998, 1, 2), start_rate=0.7961, latest_rate=0.5722), -Currency.register('ARS', 'Argentine peso', - start_date=date(1998, 1, 2), start_rate=1.4259, latest_rate=0.2589), -Currency.register('ATS', 'Austrian schilling', - start_date=date(1998, 1, 2), start_rate=0.1123, stop_date=date(2001, 12, 31), latest_rate=0.10309), # obsolete (euro) -Currency.register('BBD', 'Barbadian dollar', - start_date=date(2010, 4, 30), start_rate=0.5003, latest_rate=0.5003), -Currency.register('BEF', 'Belgian franc', - start_date=date(1998, 1, 2), start_rate=0.03832, stop_date=date(2001, 12, 31), latest_rate=0.03516), # obsolete (euro) -Currency.register('BHD', 'Bahraini dinar', - exponent=3, start_date=date(2008, 11, 8), start_rate=3.1518, latest_rate=2.6603), -Currency.register('BRL', 'Brazilian real', - start_date=date(1998, 1, 2), start_rate=1.2707, latest_rate=0.5741), -Currency.register('BSD', 'Bahamian dollar', - start_date=date(1998, 1, 2), start_rate=1.425, latest_rate=1.0128), -Currency.register('CLP', 'Chilean peso', - exponent=0, start_date=date(1998, 1, 2), start_rate=0.003236, latest_rate=0.001923), -Currency.register('CNY', 'Chinese renminbi', - start_date=date(1998, 1, 2), start_rate=0.1721, latest_rate=0.1484), -Currency.register('COP', 'Colombian peso', - start_date=date(1998, 1, 2), start_rate=0.00109, latest_rate=0.000513), -Currency.register('CZK', 'Czech Republic koruna', - start_date=date(1998, 2, 2), start_rate=0.04154, latest_rate=0.05202), -Currency.register('DEM', 'German deutsche mark', - start_date=date(1998, 1, 2), start_rate=0.7904, stop_date=date(2001, 12, 31), latest_rate=0.7253), # obsolete (euro) -Currency.register('DKK', 'Danish krone', - start_date=date(1998, 1, 2), start_rate=0.2075, latest_rate=0.1785), -Currency.register('EGP', 'Egyptian Pound', - start_date=date(2008, 11, 27), start_rate=0.2232, latest_rate=0.1805), -Currency.register('ESP', 'Spanish peseta', - exponent=0, start_date=date(1998, 1, 2), start_rate=0.009334, stop_date=date(2001, 12, 31), latest_rate=0.008526), # obsolete (euro) -Currency.register('FIM', 'Finnish markka', - start_date=date(1998, 1, 2), start_rate=0.2611, stop_date=date(2001, 12, 31), latest_rate=0.2386), # obsolete (euro) -Currency.register('FJD', 'Fiji dollar', - start_date=date(1998, 1, 2), start_rate=0.9198, latest_rate=0.5235), -Currency.register('FRF', 'French franc', - start_date=date(1998, 1, 2), start_rate=0.2362, stop_date=date(2001, 12, 31), latest_rate=0.2163), # obsolete (euro) -Currency.register('GHC', 'Ghanaian cedi (old)', - start_date=date(1998, 1, 2), start_rate=0.00063, stop_date=date(2007, 6, 29), latest_rate=0.000115), # obsolete -Currency.register('GHS', 'Ghanaian cedi', - start_date=date(2007, 7, 3), start_rate=1.1397, latest_rate=0.7134), -Currency.register('GRD', 'Greek drachma', - start_date=date(1998, 1, 2), start_rate=0.005, stop_date=date(2001, 12, 31), latest_rate=0.004163), # obsolete (euro) -Currency.register('GTQ', 'Guatemalan quetzal', - start_date=date(2004, 12, 21), start_rate=0.15762, latest_rate=0.1264), -Currency.register('HKD', 'Hong Kong dollar', - start_date=date(1998, 1, 2), start_rate=0.1838, latest_rate=0.130385), -Currency.register('HNL', 'Honduran lempira', - start_date=date(1998, 1, 2), start_rate=0.108, latest_rate=0.0536), -Currency.register('HRK', 'Croatian kuna', - start_date=date(2002, 3, 1), start_rate=0.1863, latest_rate=0.1837), -Currency.register('HUF', 'Hungarian forint', - start_date=date(1998, 2, 2), start_rate=0.007003, latest_rate=0.00493), -Currency.register('IDR', 'Indonesian rupiah', - start_date=date(1998, 2, 2), start_rate=0.000145, latest_rate=0.000112), -Currency.register('IEP', 'Irish pound', - start_date=date(1998, 1, 2), start_rate=2.0235, stop_date=date(2001, 12, 31), latest_rate=1.8012), # obsolete (euro) -Currency.register('ILS', 'Israeli new shekel', - start_date=date(1998, 1, 2), start_rate=0.4021, latest_rate=0.2706), -Currency.register('ISK', 'Icelandic krona', - exponent=0, start_date=date(1998, 1, 2), start_rate=0.01962, latest_rate=0.00782), -Currency.register('ITL', 'Italian lira', - exponent=0, start_date=date(1998, 1, 2), start_rate=0.000804, stop_date=date(2001, 12, 31), latest_rate=0.000733), # obsolete (euro) -Currency.register('JMD', 'Jamaican dollar', - start_date=date(2001, 6, 25), start_rate=0.03341, latest_rate=0.01145), -Currency.register('KRW', 'South Korean won', - exponent=0, start_date=date(1998, 1, 2), start_rate=0.000841, latest_rate=0.000905), -Currency.register('LKR', 'Sri Lanka rupee', - start_date=date(1998, 1, 2), start_rate=0.02304, latest_rate=0.0089), -Currency.register('LTL', 'Lithuanian litas', - start_date=date(2010, 4, 29), start_rate=0.384, latest_rate=0.384), -Currency.register('LVL', 'Latvian lats', - start_date=date(2011, 2, 6), start_rate=1.9136, latest_rate=1.9136), -Currency.register('MAD', 'Moroccan dirham', - start_date=date(1998, 1, 2), start_rate=0.1461, latest_rate=0.1195), -Currency.register('MMK', 'Myanmar (Burma) kyat', - start_date=date(1998, 1, 2), start_rate=0.226, latest_rate=0.1793), -Currency.register('MXN', 'Mexican peso', - start_date=date(1998, 1, 2), start_rate=0.1769, latest_rate=0.08156), -Currency.register('MYR', 'Malaysian ringgit', - start_date=date(1998, 1, 2), start_rate=0.3594, latest_rate=0.3149), -# MZN in not supported in any of my sources, so I'm just creating it with a fixed rate. -Currency.register('MZN', 'Mozambican metical', - start_date=date(2011, 2, 6), start_rate=0.03, stop_date=date(2011, 2, 5), latest_rate=0.03), -Currency.register('NIO', 'Nicaraguan córdoba', - start_date=date(2011, 10, 12), start_rate=0.0448, latest_rate=0.0448), -Currency.register('NLG', 'Netherlands guilder', - start_date=date(1998, 1, 2), start_rate=0.7013, stop_date=date(2001, 12, 31), latest_rate=0.6437), # obsolete (euro) -Currency.register('NOK', 'Norwegian krone', - start_date=date(1998, 1, 2), start_rate=0.1934, latest_rate=0.1689), -Currency.register('PAB', 'Panamanian balboa', - start_date=date(1998, 1, 2), start_rate=1.425, latest_rate=1.0128), -Currency.register('PEN', 'Peruvian new sol', - start_date=date(1998, 1, 2), start_rate=0.5234, latest_rate=0.3558), -Currency.register('PHP', 'Philippine peso', - start_date=date(1998, 1, 2), start_rate=0.0345, latest_rate=0.02262), -Currency.register('PKR', 'Pakistan rupee', - start_date=date(1998, 1, 2), start_rate=0.03238, latest_rate=0.01206), -Currency.register('PLN', 'Polish zloty', - start_date=date(1998, 2, 2), start_rate=0.4108, latest_rate=0.3382), -Currency.register('PTE', 'Portuguese escudo', - exponent=0, start_date=date(1998, 1, 2), start_rate=0.007726, stop_date=date(2001, 12, 31), latest_rate=0.007076), # obsolete (euro) -Currency.register('RON', 'Romanian new leu', - start_date=date(2007, 9, 4), start_rate=0.4314, latest_rate=0.3215), -Currency.register('RSD', 'Serbian dinar', - start_date=date(2007, 9, 4), start_rate=0.0179, latest_rate=0.01338), -Currency.register('RUB', 'Russian rouble', - start_date=date(1998, 1, 2), start_rate=0.2375, latest_rate=0.03443), -Currency.register('SAR', 'Saudi riyal', - start_date=date(2012, 9, 13), start_rate=0.26, latest_rate=0.26), -Currency.register('SEK', 'Swedish krona', - start_date=date(1998, 1, 2), start_rate=0.1787, latest_rate=0.1378), -Currency.register('SGD', 'Singapore dollar', - start_date=date(1998, 1, 2), start_rate=0.84, latest_rate=0.7358), -Currency.register('SIT', 'Slovenian tolar', - start_date=date(2002, 3, 1), start_rate=0.006174, stop_date=date(2006, 12, 29), latest_rate=0.006419), # obsolete (euro) -Currency.register('SKK', 'Slovak koruna', - start_date=date(2002, 3, 1), start_rate=0.03308, stop_date=date(2008, 12, 31), latest_rate=0.05661), # obsolete (euro) -Currency.register('THB', 'Thai baht', - start_date=date(1998, 1, 2), start_rate=0.0296, latest_rate=0.03134), -Currency.register('TND', 'Tunisian dinar', - exponent=3, start_date=date(1998, 1, 2), start_rate=1.2372, latest_rate=0.7037), -Currency.register('TRL', 'Turkish lira', - exponent=0, start_date=date(1998, 1, 2), start_rate=7.0e-06, stop_date=date(2004, 12, 31), latest_rate=8.925e-07), # obsolete -Currency.register('TWD', 'Taiwanese new dollar', - start_date=date(1998, 1, 2), start_rate=0.04338, latest_rate=0.03218), -Currency.register('UAH', 'Ukrainian hryvnia', - start_date=date(2010, 4, 29), start_rate=0.1266, latest_rate=0.1266), -Currency.register('VEB', 'Venezuelan bolivar', - exponent=0, start_date=date(1998, 1, 2), start_rate=0.002827, stop_date=date(2007, 12, 31), latest_rate=0.00046), # obsolete -Currency.register('VEF', 'Venezuelan bolivar fuerte', - start_date=date(2008, 1, 2), start_rate=0.4623, latest_rate=0.2358), -Currency.register('VND', 'Vietnamese dong', - start_date=date(2004, 1, 1), start_rate=8.2e-05, latest_rate=5.3e-05), -Currency.register('XAF', 'CFA franc', - exponent=0, start_date=date(1998, 1, 2), start_rate=0.002362, latest_rate=0.002027), -Currency.register('XCD', 'East Caribbean dollar', - start_date=date(1998, 1, 2), start_rate=0.5278, latest_rate=0.3793), -Currency.register('XPF', 'CFP franc', - exponent=0, start_date=date(1998, 1, 2), start_rate=0.01299, latest_rate=0.01114), -} -BUILTIN_CURRENCY_CODES = {c.code for c in BUILTIN_CURRENCIES} - -# For legacy purpose, we need to maintain these global variables -CAD = Currency(code='CAD') -USD = Currency(code='USD') -EUR = Currency(code='EUR') - -class CurrencyNotSupportedException(Exception): - """The current exchange rate provider doesn't support the requested currency.""" - -class RateProviderUnavailable(Exception): - """The rate provider is temporarily unavailable.""" - -def date2str(date): - return '%d%02d%02d' % (date.year, date.month, date.day) - -class RatesDB: - """Stores exchange rates for currencies. - - The currencies are identified with ISO 4217 code (USD, CAD, EUR, etc.). - The rates are represented as float and represent the value of the currency in CAD. - """ - def __init__(self, db_or_path=':memory:', async=True): - self._cache = {} # {(date, currency): CAD value - self.db_or_path = db_or_path - if isinstance(db_or_path, (str, Path)): - self.con = sqlite.connect(str(db_or_path)) - else: - self.con = db_or_path - self._execute("select * from rates where 1=2") - self._rate_providers = [] - self.async = async - self._fetched_values = Queue() - self._fetched_ranges = {} # a currency --> (start, end) map - - def _execute(self, *args, **kwargs): - def create_tables(): - # date is stored as a TEXT YYYYMMDD - sql = "create table rates(date TEXT, currency TEXT, rate REAL NOT NULL)" - self.con.execute(sql) - sql = "create unique index idx_rate on rates (date, currency)" - self.con.execute(sql) - - try: - return self.con.execute(*args, **kwargs) - except sqlite.OperationalError: # new db, or other problems - try: - create_tables() - except Exception: - logging.warning("Messy problems with the currency db, starting anew with a memory db") - self.con = sqlite.connect(':memory:') - create_tables() - except sqlite.DatabaseError: # corrupt db - logging.warning("Corrupt currency database at {0}. Starting over.".format(repr(self.db_or_path))) - if isinstance(self.db_or_path, (str, Path)): - self.con.close() - os.remove(str(self.db_or_path)) - self.con = sqlite.connect(str(self.db_or_path)) - else: - logging.warning("Can't re-use the file, using a memory table") - self.con = sqlite.connect(':memory:') - create_tables() - return self.con.execute(*args, **kwargs) # try again - - def _seek_value_in_CAD(self, str_date, currency_code): - if currency_code == 'CAD': - return 1 - def seek(date_op, desc): - sql = "select rate from rates where date %s ? and currency = ? order by date %s limit 1" % (date_op, desc) - cur = self._execute(sql, [str_date, currency_code]) - row = cur.fetchone() - if row: - return row[0] - return seek('<=', 'desc') or seek('>=', '') or Currency(currency_code).latest_rate - - def _ensure_filled(self, date_start, date_end, currency_code): - """Make sure that the cache contains *something* for each of the dates in the range. - - Sometimes, our provider doesn't return us the range we sought. When it does, it usually - means that it never will and to avoid repeatedly querying those ranges forever, we have to - fill them. We use the closest rate for this. - """ - # We don't want to fill today, because we want to repeatedly fetch that one until the - # provider gives it to us. - if date_end >= date.today(): - date_end = date.today() - timedelta(1) - sql = "select rate from rates where date = ? and currency = ?" - for curdate in iterdaterange(date_start, date_end): - cur = self._execute(sql, [date2str(curdate), currency_code]) - if cur.fetchone() is None: - nearby_rate = self._seek_value_in_CAD(date2str(curdate), currency_code) - self.set_CAD_value(curdate, currency_code, nearby_rate) - logging.debug("Filled currency void for %s at %s (value: %2.2f)", currency_code, curdate, nearby_rate) - - def _save_fetched_rates(self): - while True: - try: - rates, currency, fetch_start, fetch_end = self._fetched_values.get_nowait() - logging.debug("Saving %d rates for the currency %s", len(rates), currency) - for rate_date, rate in rates: - logging.debug("Saving rate %2.2f for %s", rate, rate_date) - self.set_CAD_value(rate_date, currency, rate) - self._ensure_filled(fetch_start, fetch_end, currency) - logging.debug("Finished saving rates for currency %s", currency) - except Empty: - break - - def clear_cache(self): - self._cache = {} - - def date_range(self, currency_code): - """Returns (start, end) of the cached rates for currency. - - Returns a tuple ``(start_date, end_date)`` representing dates covered in the database for - currency ``currency_code``. If there are gaps, they are not accounted for (subclasses that - automatically update themselves are not supposed to introduce gaps in the db). - """ - sql = "select min(date), max(date) from rates where currency = '%s'" % currency_code - cur = self._execute(sql) - start, end = cur.fetchone() - if start and end: - convert = lambda s: datetime.strptime(s, '%Y%m%d').date() - return convert(start), convert(end) - else: - return None - - def get_rate(self, date, currency1_code, currency2_code): - """Returns the exchange rate between currency1 and currency2 for date. - - The rate returned means '1 unit of currency1 is worth X units of currency2'. - The rate of the nearest date that is smaller than 'date' is returned. If - there is none, a seek for a rate with a higher date will be made. - """ - # We want to check self._fetched_values for rates to add. - if not self._fetched_values.empty(): - self._save_fetched_rates() - # This method is a bottleneck and has been optimized for speed. - value1 = None - value2 = None - if currency1_code == 'CAD': - value1 = 1 - else: - value1 = self._cache.get((date, currency1_code)) - if currency2_code == 'CAD': - value2 = 1 - else: - value2 = self._cache.get((date, currency2_code)) - if value1 is None or value2 is None: - str_date = date2str(date) - if value1 is None: - value1 = self._seek_value_in_CAD(str_date, currency1_code) - self._cache[(date, currency1_code)] = value1 - if value2 is None: - value2 = self._seek_value_in_CAD(str_date, currency2_code) - self._cache[(date, currency2_code)] = value2 - return value1 / value2 - - def set_CAD_value(self, date, currency_code, value): - """Sets the daily value in CAD for currency at date""" - # we must clear the whole cache because there might be other dates affected by this change - # (dates when the currency server has no rates). - self.clear_cache() - str_date = date2str(date) - sql = "replace into rates(date, currency, rate) values(?, ?, ?)" - self._execute(sql, [str_date, currency_code, value]) - self.con.commit() - - def register_rate_provider(self, rate_provider): - """Adds `rate_provider` to the list of providers supported by this DB. - - A provider if a function(currency, start_date, end_date) that returns a list of - (rate_date, float_rate) as a result. This function will be called asyncronously, so it's ok - if it takes a long time to return. - - The rates returned must be the value of 1 `currency` in CAD (Canadian Dollars) at the - specified date. - - The provider can be asked for any currency. If it doesn't support it, it has to raise - CurrencyNotSupportedException. - - If we support the currency but that there is no rate available for the specified range, - simply return an empty list or None. - """ - self._rate_providers.append(rate_provider) - - def ensure_rates(self, start_date, currencies): - """Ensures that the DB has all the rates it needs for 'currencies' between 'start_date' and today - - If there is any rate missing, a request will be made to the currency server. The requests - are made asynchronously. - """ - def do(): - for currency, fetch_start, fetch_end in currencies_and_range: - logging.debug("Fetching rates for %s for date range %s to %s", currency, fetch_start, fetch_end) - for rate_provider in self._rate_providers: - try: - values = rate_provider(currency, fetch_start, fetch_end) - except CurrencyNotSupportedException: - continue - except RateProviderUnavailable: - logging.debug("Fetching failed due to temporary problems.") - break - else: - if not values: - # We didn't get any value from the server, which means that we asked for - # rates that couldn't be delivered. Still, we report empty values so - # that the cache can correctly remember this unavailability so that we - # don't repeatedly fetch those ranges. - values = [] - self._fetched_values.put((values, currency, fetch_start, fetch_end)) - logging.debug("Fetching successful!") - break - else: - logging.debug("Fetching failed!") - - currencies_and_range = [] - for currency in currencies: - if currency == 'CAD': - continue - try: - cached_range = self._fetched_ranges[currency] - except KeyError: - cached_range = self.date_range(currency) - range_start = start_date - # Don't try to fetch today's rate, it's never there and results in useless server - # hitting. - range_end = date.today() - timedelta(1) - if cached_range is not None: - cached_start, cached_end = cached_range - if range_start >= cached_start: - # Make a forward fetch - range_start = cached_end + timedelta(days=1) - else: - # Make a backward fetch - range_end = cached_start - timedelta(days=1) - # We don't want to fetch ranges that are too big. It can cause various problems, such - # as hangs. We prefer to take smaller bites. - if (range_end - range_start).days > 30: - range_start = range_end - timedelta(days=30) - if range_start <= range_end: - currencies_and_range.append((currency, range_start, range_end)) - self._fetched_ranges[currency] = (start_date, date.today()) - if self.async: - threading.Thread(target=do).start() - else: - do() - diff --git a/hscommon/debug.py b/hscommon/debug.py deleted file mode 100644 index e7903152..00000000 --- a/hscommon/debug.py +++ /dev/null @@ -1,22 +0,0 @@ -# Created By: Virgil Dupras -# Created On: 2011-04-19 -# Copyright 2015 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at -# http://www.gnu.org/licenses/gpl-3.0.html - -import sys -import traceback - -# Taken from http://bzimmer.ziclix.com/2008/12/17/python-thread-dumps/ -def stacktraces(): - code = [] - for threadId, stack in sys._current_frames().items(): - code.append("\n# ThreadID: %s" % threadId) - for filename, lineno, name, line in traceback.extract_stack(stack): - code.append('File: "%s", line %d, in %s' % (filename, lineno, name)) - if line: - code.append(" %s" % (line.strip())) - - return "\n".join(code) \ No newline at end of file diff --git a/hscommon/desktop.py b/hscommon/desktop.py deleted file mode 100644 index 71d8078d..00000000 --- a/hscommon/desktop.py +++ /dev/null @@ -1,93 +0,0 @@ -# Created By: Virgil Dupras -# Created On: 2013-10-12 -# Copyright 2015 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at -# http://www.gnu.org/licenses/gpl-3.0.html - -import os.path as op -import logging - -class SpecialFolder: - AppData = 1 - Cache = 2 - -def open_url(url): - """Open ``url`` with the default browser. - """ - _open_url(url) - -def open_path(path): - """Open ``path`` with its associated application. - """ - _open_path(str(path)) - -def reveal_path(path): - """Open the folder containing ``path`` with the default file browser. - """ - _reveal_path(str(path)) - -def special_folder_path(special_folder, appname=None): - """Returns the path of ``special_folder``. - - ``special_folder`` is a SpecialFolder.* const. The result is the special folder for the current - application. The running process' application info is used to determine relevant information. - - You can override the application name with ``appname``. This argument is ingored under Qt. - """ - return _special_folder_path(special_folder, appname) - -try: - # Normally, we would simply do "from cocoa import proxy", but due to a bug in pytest (currently - # at v2.4.2), our test suite is broken when we do that. This below is a workaround until that - # bug is fixed. - import cocoa - if not hasattr(cocoa, 'proxy'): - raise ImportError() - proxy = cocoa.proxy - _open_url = proxy.openURL_ - _open_path = proxy.openPath_ - _reveal_path = proxy.revealPath_ - - def _special_folder_path(special_folder, appname=None): - if special_folder == SpecialFolder.Cache: - base = proxy.getCachePath() - else: - base = proxy.getAppdataPath() - if not appname: - appname = proxy.bundleInfo_('CFBundleName') - return op.join(base, appname) - -except ImportError: - try: - from PyQt5.QtCore import QUrl, QStandardPaths - from PyQt5.QtGui import QDesktopServices - def _open_url(url): - QDesktopServices.openUrl(QUrl(url)) - - def _open_path(path): - url = QUrl.fromLocalFile(str(path)) - QDesktopServices.openUrl(url) - - def _reveal_path(path): - _open_path(op.dirname(str(path))) - - def _special_folder_path(special_folder, appname=None): - if special_folder == SpecialFolder.Cache: - qtfolder = QStandardPaths.CacheLocation - else: - qtfolder = QStandardPaths.DataLocation - return QStandardPaths.standardLocations(qtfolder)[0] - except ImportError: - # We're either running tests, and these functions don't matter much or we're in a really - # weird situation. Let's just have dummy fallbacks. - logging.warning("Can't setup desktop functions!") - def _open_path(path): - pass - - def _reveal_path(path): - pass - - def _special_folder_path(special_folder, appname=None): - return '/tmp' diff --git a/hscommon/geometry.py b/hscommon/geometry.py deleted file mode 100644 index d67738bd..00000000 --- a/hscommon/geometry.py +++ /dev/null @@ -1,218 +0,0 @@ -# Created By: Virgil Dupras -# Created On: 2011-08-05 -# Copyright 2015 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at -# http://www.gnu.org/licenses/gpl-3.0.html - -from sys import maxsize as INF -from math import sqrt - -VERY_SMALL = 0.0000001 - -class Point: - def __init__(self, x, y): - self.x = x - self.y = y - - def __repr__(self): - return ''.format(*self) - - def __iter__(self): - yield self.x - yield self.y - - def distance_to(self, other): - return Line(self, other).length() - - -class Line: - def __init__(self, p1, p2): - self.p1 = p1 - self.p2 = p2 - - def __repr__(self): - return ''.format(*self) - - def __iter__(self): - yield self.p1 - yield self.p2 - - def dx(self): - return self.p2.x - self.p1.x - - def dy(self): - return self.p2.y - self.p1.y - - def length(self): - return sqrt(self.dx() ** 2 + self.dy() ** 2) - - def slope(self): - if self.dx() == 0: - return INF if self.dy() > 0 else -INF - else: - return self.dy() / self.dx() - - def intersection_point(self, other): - # with help from http://paulbourke.net/geometry/lineline2d/ - if abs(self.slope() - other.slope()) < VERY_SMALL: - # parallel. Even if coincident, we return nothing - return None - - A, B = self - C, D = other - - denom = (D.y-C.y) * (B.x-A.x) - (D.x-C.x) * (B.y-A.y) - if denom == 0: - return None - numera = (D.x-C.x) * (A.y-C.y) - (D.y-C.y) * (A.x-C.x) - numerb = (B.x-A.x) * (A.y-C.y) - (B.y-A.y) * (A.x-C.x) - - mua = numera / denom; - mub = numerb / denom; - if (0 <= mua <= 1) and (0 <= mub <= 1): - x = A.x + mua * (B.x - A.x) - y = A.y + mua * (B.y - A.y) - return Point(x, y) - else: - return None - - -class Rect: - def __init__(self, x, y, w, h): - self.x = x - self.y = y - self.w = w - self.h = h - - def __iter__(self): - yield self.x - yield self.y - yield self.w - yield self.h - - def __repr__(self): - return ''.format(*self) - - @classmethod - def from_center(cls, center, width, height): - x = center.x - width / 2 - y = center.y - height / 2 - return cls(x, y, width, height) - - @classmethod - def from_corners(cls, pt1, pt2): - x1, y1 = pt1 - x2, y2 = pt2 - return cls(min(x1, x2), min(y1, y2), abs(x1-x2), abs(y1-y2)) - - def center(self): - return Point(self.x + self.w/2, self.y + self.h/2) - - def contains_point(self, point): - x, y = point - (x1, y1), (x2, y2) = self.corners() - return (x1 <= x <= x2) and (y1 <= y <= y2) - - def contains_rect(self, rect): - pt1, pt2 = rect.corners() - return self.contains_point(pt1) and self.contains_point(pt2) - - def corners(self): - return Point(self.x, self.y), Point(self.x+self.w, self.y+self.h) - - def intersects(self, other): - r1pt1, r1pt2 = self.corners() - r2pt1, r2pt2 = other.corners() - if r1pt1.x < r2pt1.x: - xinter = r1pt2.x >= r2pt1.x - else: - xinter = r2pt2.x >= r1pt1.x - if not xinter: - return False - if r1pt1.y < r2pt1.y: - yinter = r1pt2.y >= r2pt1.y - else: - yinter = r2pt2.y >= r1pt1.y - return yinter - - def lines(self): - pt1, pt4 = self.corners() - pt2 = Point(pt4.x, pt1.y) - pt3 = Point(pt1.x, pt4.y) - l1 = Line(pt1, pt2) - l2 = Line(pt2, pt4) - l3 = Line(pt3, pt4) - l4 = Line(pt1, pt3) - return l1, l2, l3, l4 - - def scaled_rect(self, dx, dy): - """Returns a rect that has the same borders at self, but grown/shrunk by dx/dy on each side. - """ - x, y, w, h = self - x -= dx - y -= dy - w += dx * 2 - h += dy * 2 - return Rect(x, y, w, h) - - def united(self, other): - """Returns the bounding rectangle of this rectangle and `other`. - """ - # ul=upper left lr=lower right - ulcorner1, lrcorner1 = self.corners() - ulcorner2, lrcorner2 = other.corners() - corner1 = Point(min(ulcorner1.x, ulcorner2.x), min(ulcorner1.y, ulcorner2.y)) - corner2 = Point(max(lrcorner1.x, lrcorner2.x), max(lrcorner1.y, lrcorner2.y)) - return Rect.from_corners(corner1, corner2) - - #--- Properties - @property - def top(self): - return self.y - - @top.setter - def top(self, value): - self.y = value - - @property - def bottom(self): - return self.y + self.h - - @bottom.setter - def bottom(self, value): - self.y = value - self.h - - @property - def left(self): - return self.x - - @left.setter - def left(self, value): - self.x = value - - @property - def right(self): - return self.x + self.w - - @right.setter - def right(self, value): - self.x = value - self.w - - @property - def width(self): - return self.w - - @width.setter - def width(self, value): - self.w = value - - @property - def height(self): - return self.h - - @height.setter - def height(self, value): - self.h = value - diff --git a/hscommon/gui/__init__.py b/hscommon/gui/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/hscommon/gui/base.py b/hscommon/gui/base.py deleted file mode 100644 index 4a7ba2d0..00000000 --- a/hscommon/gui/base.py +++ /dev/null @@ -1,74 +0,0 @@ -# Created By: Virgil Dupras -# Created On: 2011/09/09 -# Copyright 2015 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at -# http://www.gnu.org/licenses/gpl-3.0.html - -def noop(*args, **kwargs): - pass - -class NoopGUI: - def __getattr__(self, func_name): - return noop - -class GUIObject: - """Cross-toolkit "model" representation of a GUI layer object. - - A ``GUIObject`` is a cross-toolkit "model" representation of a GUI layer object, for example, a - table. It acts as a cross-toolkit interface to what we call here a :attr:`view`. That - view is a toolkit-specific controller to the actual view (an ``NSTableView``, a ``QTableView``, - etc.). In our GUIObject, we need a reference to that toolkit-specific controller because some - actions have effects on it (for example, prompting it to refresh its data). The ``GUIObject`` - is typically instantiated before its :attr:`view`, that is why we set it to ``None`` on init. - However, the GUI layer is supposed to set the view as soon as its toolkit-specific controller is - instantiated. - - When you subclass ``GUIObject``, you will likely want to update its view on instantiation. That - is why we call ``self.view.refresh()`` in :meth:`_view_updated`. If you need another type of - action on view instantiation, just override the method. - """ - def __init__(self): - self._view = None - - def _view_updated(self): - """(Virtual) Called after :attr:`view` has been set. - - Doing nothing by default, this method is called after :attr:`view` has been set (it isn't - called when it's unset, however). Use this for initialization code that requires a view - (which is often the whole of the initialization code). - """ - - def has_view(self): - return (self._view is not None) and (not isinstance(self._view, NoopGUI)) - - @property - def view(self): - """A reference to our toolkit-specific view controller. - - *view answering to GUIObject sublass's view protocol*. *get/set* - - This view starts as ``None`` and has to be set "manually". There's two times at which we set - the view property: On initialization, where we set the view that we'll use for our lifetime, - and just before the view is deallocated. We need to unset our view at that time to avoid - calls to a deallocated instance (which means a crash). - - To unset our view, we simple assign it to ``None``. - """ - return self._view - - @view.setter - def view(self, value): - if self._view is None: - # Initial view assignment - if value is None: - return - self._view = value - self._view_updated() - else: - assert value is None - # Instead of None, we put a NoopGUI() there to avoid rogue view callback raising an - # exception. - self._view = NoopGUI() - diff --git a/hscommon/gui/column.py b/hscommon/gui/column.py deleted file mode 100644 index 66e22cfd..00000000 --- a/hscommon/gui/column.py +++ /dev/null @@ -1,289 +0,0 @@ -# Created By: Virgil Dupras -# Created On: 2010-07-25 -# Copyright 2015 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at -# http://www.gnu.org/licenses/gpl-3.0.html - -import copy - -from .base import GUIObject - -class Column: - """Holds column attributes such as its name, width, visibility, etc. - - These attributes are then used to correctly configure the column on the "view" side. - """ - def __init__(self, name, display='', visible=True, optional=False): - #: "programmatical" (not for display) name. Used as a reference in a couple of place, such - #: as :meth:`Columns.column_by_name`. - self.name = name - #: Immutable index of the column. Doesn't change even when columns are re-ordered. Used in - #: :meth:`Columns.column_by_index`. - self.logical_index = 0 - #: Index of the column in the ordered set of columns. - self.ordered_index = 0 - #: Width of the column. - self.width = 0 - #: Default width of the column. This value usually depends on the platform and is set on - #: columns initialisation. It will be used if column restoration doesn't contain any - #: "remembered" widths. - self.default_width = 0 - #: Display name (title) of the column. - self.display = display - #: Whether the column is visible. - self.visible = visible - #: Whether the column is visible by default. It will be used if column restoration doesn't - #: contain any "remembered" widths. - self.default_visible = visible - #: Whether the column can have :attr:`visible` set to false. - self.optional = optional - -class ColumnsView: - """Expected interface for :class:`Columns`'s view. - - *Not actually used in the code. For documentation purposes only.* - - Our view, the columns controller of a table or outline, is expected to properly respond to - callbacks. - """ - def restore_columns(self): - """Update all columns according to the model. - - When this is called, our view has to update the columns title, order and visibility of all - columns. - """ - - def set_column_visible(self, colname, visible): - """Update visibility of column ``colname``. - - Called when the user toggles the visibility of a column, we must update the column - ``colname``'s visibility status to ``visible``. - """ - -class PrefAccessInterface: - """Expected interface for :class:`Columns`'s prefaccess. - - *Not actually used in the code. For documentation purposes only.* - """ - def get_default(self, key, fallback_value): - """Retrieve the value for ``key`` in the currently running app's preference store. - - If the key doesn't exist, return ``fallback_value``. - """ - - def set_default(self, key, value): - """Set the value ``value`` for ``key`` in the currently running app's preference store. - """ - -class Columns(GUIObject): - """Cross-toolkit GUI-enabled column set for tables or outlines. - - Manages a column set's order, visibility and width. We also manage the persistence of these - attributes so that we can restore them on the next run. - - Subclasses :class:`.GUIObject`. Expected view: :class:`ColumnsView`. - - :param table: The table the columns belong to. It's from there that we retrieve our column - configuration and it must have a ``COLUMNS`` attribute which is a list of - :class:`Column`. We also call :meth:`~.GUITable.save_edits` on it from time to - time. Technically, this argument can also be a tree, but there's probably some - sorting in the code to do to support this option cleanly. - :param prefaccess: An object giving access to user preferences for the currently running app. - We use this to make column attributes persistent. Must follow - :class:`PrefAccessInterface`. - :param str savename: The name under which column preferences will be saved. This name is in fact - a prefix. Preferences are saved under more than one name, but they will all - have that same prefix. - """ - def __init__(self, table, prefaccess=None, savename=None): - GUIObject.__init__(self) - self.table = table - self.prefaccess = prefaccess - self.savename = savename - # We use copy here for test isolation. If we don't, changing a column affects all tests. - self.column_list = list(map(copy.copy, table.COLUMNS)) - for i, column in enumerate(self.column_list): - column.logical_index = i - column.ordered_index = i - self.coldata = {col.name: col for col in self.column_list} - - #--- Private - def _get_colname_attr(self, colname, attrname, default): - try: - return getattr(self.coldata[colname], attrname) - except KeyError: - return default - - def _set_colname_attr(self, colname, attrname, value): - try: - col = self.coldata[colname] - setattr(col, attrname, value) - except KeyError: - pass - - def _optional_columns(self): - return [c for c in self.column_list if c.optional] - - #--- Override - def _view_updated(self): - self.restore_columns() - - #--- Public - def column_by_index(self, index): - """Return the :class:`Column` having the :attr:`~Column.logical_index` ``index``. - """ - return self.column_list[index] - - def column_by_name(self, name): - """Return the :class:`Column` having the :attr:`~Column.name` ``name``. - """ - return self.coldata[name] - - def columns_count(self): - """Returns the number of columns in our set. - """ - return len(self.column_list) - - def column_display(self, colname): - """Returns display name for column named ``colname``, or ``''`` if there's none. - """ - return self._get_colname_attr(colname, 'display', '') - - def column_is_visible(self, colname): - """Returns visibility for column named ``colname``, or ``True`` if there's none. - """ - return self._get_colname_attr(colname, 'visible', True) - - def column_width(self, colname): - """Returns width for column named ``colname``, or ``0`` if there's none. - """ - return self._get_colname_attr(colname, 'width', 0) - - def columns_to_right(self, colname): - """Returns the list of all columns to the right of ``colname``. - - "right" meaning "having a higher :attr:`Column.ordered_index`" in our left-to-right - civilization. - """ - column = self.coldata[colname] - index = column.ordered_index - return [col.name for col in self.column_list if (col.visible and col.ordered_index > index)] - - def menu_items(self): - """Returns a list of items convenient for quick visibility menu generation. - - Returns a list of ``(display_name, is_marked)`` items for each optional column in the - current view (``is_marked`` means that it's visible). - - You can use this to generate a menu to let the user toggle the visibility of an optional - column. That is why we only show optional column, because the visibility of mandatory - columns can't be toggled. - """ - return [(c.display, c.visible) for c in self._optional_columns()] - - def move_column(self, colname, index): - """Moves column ``colname`` to ``index``. - - The column will be placed just in front of the column currently having that index, or to the - end of the list if there's none. - """ - colnames = self.colnames - colnames.remove(colname) - colnames.insert(index, colname) - self.set_column_order(colnames) - - def reset_to_defaults(self): - """Reset all columns' width and visibility to their default values. - """ - self.set_column_order([col.name for col in self.column_list]) - for col in self._optional_columns(): - col.visible = col.default_visible - col.width = col.default_width - self.view.restore_columns() - - def resize_column(self, colname, newwidth): - """Set column ``colname``'s width to ``newwidth``. - """ - self._set_colname_attr(colname, 'width', newwidth) - - def restore_columns(self): - """Restore's column persistent attributes from the last :meth:`save_columns`. - """ - if not (self.prefaccess and self.savename and self.coldata): - if (not self.savename) and (self.coldata): - # This is a table that will not have its coldata saved/restored. we should - # "restore" its default column attributes. - self.view.restore_columns() - return - for col in self.column_list: - pref_name = '{}.Columns.{}'.format(self.savename, col.name) - coldata = self.prefaccess.get_default(pref_name, fallback_value={}) - if 'index' in coldata: - col.ordered_index = coldata['index'] - if 'width' in coldata: - col.width = coldata['width'] - if col.optional and 'visible' in coldata: - col.visible = coldata['visible'] - self.view.restore_columns() - - def save_columns(self): - """Save column attributes in persistent storage for restoration in :meth:`restore_columns`. - """ - if not (self.prefaccess and self.savename and self.coldata): - return - for col in self.column_list: - pref_name = '{}.Columns.{}'.format(self.savename, col.name) - coldata = {'index': col.ordered_index, 'width': col.width} - if col.optional: - coldata['visible'] = col.visible - self.prefaccess.set_default(pref_name, coldata) - - def set_column_order(self, colnames): - """Change the columns order so it matches the order in ``colnames``. - - :param colnames: A list of column names in the desired order. - """ - colnames = (name for name in colnames if name in self.coldata) - for i, colname in enumerate(colnames): - col = self.coldata[colname] - col.ordered_index = i - - def set_column_visible(self, colname, visible): - """Set the visibility of column ``colname``. - """ - self.table.save_edits() # the table on the GUI side will stop editing when the columns change - self._set_colname_attr(colname, 'visible', visible) - self.view.set_column_visible(colname, visible) - - def set_default_width(self, colname, width): - """Set the default width or column ``colname``. - """ - self._set_colname_attr(colname, 'default_width', width) - - def toggle_menu_item(self, index): - """Toggles the visibility of an optional column. - - You know, that optional column menu you've generated in :meth:`menu_items`? Well, ``index`` - is the index of them menu item in *that* menu that the user has clicked on to toggle it. - - Returns whether the column in question ends up being visible or not. - """ - col = self._optional_columns()[index] - self.set_column_visible(col.name, not col.visible) - return col.visible - - #--- Properties - @property - def ordered_columns(self): - """List of :class:`Column` in visible order. - """ - return [col for col in sorted(self.column_list, key=lambda col: col.ordered_index)] - - @property - def colnames(self): - """List of column names in visible order. - """ - return [col.name for col in self.ordered_columns] - diff --git a/hscommon/gui/progress_window.py b/hscommon/gui/progress_window.py deleted file mode 100644 index c28c1794..00000000 --- a/hscommon/gui/progress_window.py +++ /dev/null @@ -1,122 +0,0 @@ -# Created On: 2013/07/01 -# Copyright 2015 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at -# http://www.gnu.org/licenses/gpl-3.0.html - -from ..jobprogress.performer import ThreadedJobPerformer -from .base import GUIObject -from .text_field import TextField - -class ProgressWindowView: - """Expected interface for :class:`ProgressWindow`'s view. - - *Not actually used in the code. For documentation purposes only.* - - Our view, some kind window with a progress bar, two labels and a cancel button, is expected - to properly respond to its callbacks. - - It's also expected to call :meth:`ProgressWindow.cancel` when the cancel button is clicked. - """ - def show(self): - """Show the dialog. - """ - - def close(self): - """Close the dialog. - """ - - def set_progress(self, progress): - """Set the progress of the progress bar to ``progress``. - - Not all jobs are equally responsive on their job progress report and it is recommended that - you put your progressbar in "indeterminate" mode as long as you haven't received the first - ``set_progress()`` call to avoid letting the user think that the app is frozen. - - :param int progress: a value between ``0`` and ``100``. - """ - -class ProgressWindow(GUIObject, ThreadedJobPerformer): - """Cross-toolkit GUI-enabled progress window. - - This class allows you to run a long running, job enabled function in a separate thread and - allow the user to follow its progress with a progress dialog. - - To use it, you start your long-running job with :meth:`run` and then have your UI layer - regularly call :meth:`pulse` to refresh the job status in the UI. It is advised that you call - :meth:`pulse` in the main thread because GUI toolkit usually only support calling UI-related - functions from the main thread. - - We subclass :class:`.GUIObject` and :class:`.ThreadedJobPerformer`. - Expected view: :class:`ProgressWindowView`. - - :param finishfunc: A function ``f(jobid)`` that is called when a job is completed. ``jobid`` is - an arbitrary id passed to :meth:`run`. - """ - def __init__(self, finish_func): - # finish_func(jobid) is the function that is called when a job is completed. - GUIObject.__init__(self) - ThreadedJobPerformer.__init__(self) - self._finish_func = finish_func - #: :class:`.TextField`. It contains that title you gave the job on :meth:`run`. - self.jobdesc_textfield = TextField() - #: :class:`.TextField`. It contains the job textual update that the function might yield - #: during its course. - self.progressdesc_textfield = TextField() - self.jobid = None - - def cancel(self): - """Call for a user-initiated job cancellation. - """ - # The UI is sometimes a bit buggy and calls cancel() on self.view.close(). We just want to - # make sure that this doesn't lead us to think that the user acually cancelled the task, so - # we verify that the job is still running. - if self._job_running: - self.job_cancelled = True - - def pulse(self): - """Update progress reports in the GUI. - - Call this regularly from the GUI main run loop. The values might change before - :meth:`ProgressWindowView.set_progress` happens. - - If the job is finished, ``pulse()`` will take care of closing the window and re-raising any - exception that might have been raised during the job (in the main thread this time). If - there was no exception, ``finish_func(jobid)`` is called to let you take appropriate action. - """ - last_progress = self.last_progress - last_desc = self.last_desc - if not self._job_running or last_progress is None: - self.view.close() - self.reraise_if_error() - if not self.job_cancelled: - self._finish_func(self.jobid) - return - if self.job_cancelled: - return - if last_desc: - self.progressdesc_textfield.text = last_desc - self.view.set_progress(last_progress) - - def run(self, jobid, title, target, args=()): - """Starts a threaded job. - - The ``target`` function will be sent, as its first argument, a :class:`.Job` instance which - it can use to report on its progress. - - :param jobid: Arbitrary identifier which will be passed to ``finish_func()`` at the end. - :param title: A title for the task you're starting. - :param target: The function that does your famous long running job. - :param args: additional arguments that you want to send to ``target``. - """ - # target is a function with its first argument being a Job. It can then be followed by other - # arguments which are passed as `args`. - self.jobid = jobid - self.progressdesc_textfield.text = '' - j = self.create_job() - args = tuple([j] + list(args)) - self.run_threaded(target, args) - self.jobdesc_textfield.text = title - self.view.show() - diff --git a/hscommon/gui/selectable_list.py b/hscommon/gui/selectable_list.py deleted file mode 100644 index df6ed357..00000000 --- a/hscommon/gui/selectable_list.py +++ /dev/null @@ -1,208 +0,0 @@ -# Created By: Virgil Dupras -# Created On: 2011-09-06 -# Copyright 2015 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at -# http://www.gnu.org/licenses/gpl-3.0.html - -from collections import Sequence, MutableSequence - -from .base import GUIObject - -class Selectable(Sequence): - """Mix-in for a ``Sequence`` that manages its selection status. - - When mixed in with a ``Sequence``, we enable it to manage its selection status. The selection - is held as a list of ``int`` indexes. Multiple selection is supported. - """ - def __init__(self): - self._selected_indexes = [] - - #--- Private - def _check_selection_range(self): - if not self: - self._selected_indexes = [] - if not self._selected_indexes: - return - self._selected_indexes = [index for index in self._selected_indexes if index < len(self)] - if not self._selected_indexes: - self._selected_indexes = [len(self) - 1] - - #--- Virtual - def _update_selection(self): - """(Virtual) Updates the model's selection appropriately. - - Called after selection has been updated. Takes the table's selection and does appropriates - updates on the view and/or model. Common sense would dictate that when the selection doesn't - change, we don't update anything (and thus don't call ``_update_selection()`` at all), but - there are cases where it's false. For example, if our list updates its items but doesn't - change its selection, we probably want to update the model's selection. - - By default, does nothing. - - Important note: This is only called on :meth:`select`, not on changes to - :attr:`selected_indexes`. - """ - # A redesign of how this whole thing works is probably in order, but not now, there's too - # much breakage at once involved. - - #--- Public - def select(self, indexes): - """Update selection to ``indexes``. - - :meth:`_update_selection` is called afterwards. - - :param list indexes: List of ``int`` that is to become the new selection. - """ - if isinstance(indexes, int): - indexes = [indexes] - self.selected_indexes = indexes - self._update_selection() - - #--- Properties - @property - def selected_index(self): - """Points to the first selected index. - - *int*. *get/set*. - - Thin wrapper around :attr:`selected_indexes`. ``None`` if selection is empty. Using this - property only makes sense if your selectable sequence supports single selection only. - """ - return self._selected_indexes[0] if self._selected_indexes else None - - @selected_index.setter - def selected_index(self, value): - self.selected_indexes = [value] - - @property - def selected_indexes(self): - """List of selected indexes. - - *list of int*. *get/set*. - - When setting the value, automatically removes out-of-bounds indexes. The list is kept - sorted. - """ - return self._selected_indexes - - @selected_indexes.setter - def selected_indexes(self, value): - self._selected_indexes = value - self._selected_indexes.sort() - self._check_selection_range() - - -class SelectableList(MutableSequence, Selectable): - """A list that can manage selection of its items. - - Subclasses :class:`Selectable`. Behaves like a ``list``. - """ - def __init__(self, items=None): - Selectable.__init__(self) - if items: - self._items = list(items) - else: - self._items = [] - - def __delitem__(self, key): - self._items.__delitem__(key) - self._check_selection_range() - self._on_change() - - def __getitem__(self, key): - return self._items.__getitem__(key) - - def __len__(self): - return len(self._items) - - def __setitem__(self, key, value): - self._items.__setitem__(key, value) - self._on_change() - - #--- Override - def append(self, item): - self._items.append(item) - self._on_change() - - def insert(self, index, item): - self._items.insert(index, item) - self._on_change() - - def remove(self, row): - self._items.remove(row) - self._check_selection_range() - self._on_change() - - #--- Virtual - def _on_change(self): - """(Virtual) Called whenever the contents of the list changes. - - By default, does nothing. - """ - - #--- Public - def search_by_prefix(self, prefix): - # XXX Why the heck is this method here? - prefix = prefix.lower() - for index, s in enumerate(self): - if s.lower().startswith(prefix): - return index - return -1 - - -class GUISelectableListView: - """Expected interface for :class:`GUISelectableList`'s view. - - *Not actually used in the code. For documentation purposes only.* - - Our view, some kind of list view or combobox, is expected to sync with the list's contents by - appropriately behave to all callbacks in this interface. - """ - def refresh(self): - """Refreshes the contents of the list widget. - - Ensures that the contents of the list widget is synced with the model. - """ - - def update_selection(self): - """Update selection status. - - Ensures that the list widget's selection is in sync with the model. - """ - -class GUISelectableList(SelectableList, GUIObject): - """Cross-toolkit GUI-enabled list view. - - Represents a UI element presenting the user with a selectable list of items. - - Subclasses :class:`SelectableList` and :class:`.GUIObject`. Expected view: - :class:`GUISelectableListView`. - - :param iterable items: If specified, items to fill the list with initially. - """ - def __init__(self, items=None): - SelectableList.__init__(self, items) - GUIObject.__init__(self) - - def _view_updated(self): - """Refreshes the view contents with :meth:`GUISelectableListView.refresh`. - - Overrides :meth:`~hscommon.gui.base.GUIObject._view_updated`. - """ - self.view.refresh() - - def _update_selection(self): - """Refreshes the view selection with :meth:`GUISelectableListView.update_selection`. - - Overrides :meth:`Selectable._update_selection`. - """ - self.view.update_selection() - - def _on_change(self): - """Refreshes the view contents with :meth:`GUISelectableListView.refresh`. - - Overrides :meth:`SelectableList._on_change`. - """ - self.view.refresh() diff --git a/hscommon/gui/table.py b/hscommon/gui/table.py deleted file mode 100644 index fd4c202e..00000000 --- a/hscommon/gui/table.py +++ /dev/null @@ -1,543 +0,0 @@ -# Created By: Eric Mc Sween -# Created On: 2008-05-29 -# Copyright 2015 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at -# http://www.gnu.org/licenses/gpl-3.0.html - -from collections import MutableSequence, namedtuple - -from .base import GUIObject -from .selectable_list import Selectable - -# We used to directly subclass list, but it caused problems at some point with deepcopy -class Table(MutableSequence, Selectable): - """Sortable and selectable sequence of :class:`Row`. - - In fact, the Table is very similar to :class:`.SelectableList` in - practice and differs mostly in principle. Their difference lies in the nature of their items - they manage. With the Table, rows usually have many properties, presented in columns, and they - have to subclass :class:`Row`. - - Usually used with :class:`~hscommon.gui.column.Column`. - - Subclasses :class:`.Selectable`. - """ - def __init__(self): - Selectable.__init__(self) - self._rows = [] - self._header = None - self._footer = None - - def __delitem__(self, key): - self._rows.__delitem__(key) - if self._header is not None and ((not self) or (self[0] is not self._header)): - self._header = None - if self._footer is not None and ((not self) or (self[-1] is not self._footer)): - self._footer = None - self._check_selection_range() - - def __getitem__(self, key): - return self._rows.__getitem__(key) - - def __len__(self): - return len(self._rows) - - def __setitem__(self, key, value): - self._rows.__setitem__(key, value) - - def append(self, item): - """Appends ``item`` at the end of the table. - - If there's a footer, the item is inserted before it. - """ - if self._footer is not None: - self._rows.insert(-1, item) - else: - self._rows.append(item) - - def insert(self, index, item): - """Inserts ``item`` at ``index`` in the table. - - If there's a header, will make sure we don't insert before it, and if there's a footer, will - make sure that we don't insert after it. - """ - if (self._header is not None) and (index == 0): - index = 1 - if (self._footer is not None) and (index >= len(self)): - index = len(self) - 1 - self._rows.insert(index, item) - - def remove(self, row): - """Removes ``row`` from table. - - If ``row`` is a header or footer, that header or footer will be set to ``None``. - """ - if row is self._header: - self._header = None - if row is self._footer: - self._footer = None - self._rows.remove(row) - self._check_selection_range() - - def sort_by(self, column_name, desc=False): - """Sort table by ``column_name``. - - Sort key for each row is computed from :meth:`Row.sort_key_for_column`. - - If ``desc`` is ``True``, sort order is reversed. - - If present, header and footer will always be first and last, respectively. - """ - if self._header is not None: - self._rows.pop(0) - if self._footer is not None: - self._rows.pop() - key = lambda row: row.sort_key_for_column(column_name) - self._rows.sort(key=key, reverse=desc) - if self._header is not None: - self._rows.insert(0, self._header) - if self._footer is not None: - self._rows.append(self._footer) - - #--- Properties - @property - def footer(self): - """If set, a row that always stay at the bottom of the table. - - :class:`Row`. *get/set*. - - When set to something else than ``None``, ``header`` and ``footer`` represent rows that will - always be kept in first and/or last position, regardless of sorting. ``len()`` and indexing - will include them, which means that if there's a header, ``table[0]`` returns it and if - there's a footer, ``table[-1]`` returns it. To make things short, all list-like functions - work with header and footer "on". But things get fuzzy for ``append()`` and ``insert()`` - because these will ensure that no "normal" row gets inserted before the header or after the - footer. - - Adding and removing footer here and there might seem (and is) hackish, but it's much simpler - than the alternative (when, of course, you need such a feature), which is to override magic - methods and adjust the results. When we do that, there the slice stuff that we have to - implement and it gets quite complex. Moreover, the most frequent operation on a table is - ``__getitem__``, and making checks to know whether the key is a header or footer at each - call would make that operation, which is the most used, slower. - """ - return self._footer - - @footer.setter - def footer(self, value): - if self._footer is not None: - self._rows.pop() - if value is not None: - self._rows.append(value) - self._footer = value - - @property - def header(self): - """If set, a row that always stay at the bottom of the table. - - See :attr:`footer` for details. - """ - return self._header - - @header.setter - def header(self, value): - if self._header is not None: - self._rows.pop(0) - if value is not None: - self._rows.insert(0, value) - self._header = value - - @property - def row_count(self): - """Number or rows in the table (without counting header and footer). - - *int*. *read-only*. - """ - result = len(self) - if self._footer is not None: - result -= 1 - if self._header is not None: - result -= 1 - return result - - @property - def rows(self): - """List of rows in the table, excluding header and footer. - - List of :class:`Row`. *read-only*. - """ - start = None - end = None - if self._footer is not None: - end = -1 - if self._header is not None: - start = 1 - return self[start:end] - - @property - def selected_row(self): - """Selected row according to :attr:`.selected_index`. - - :class:`Row`. *get/set*. - - When setting this attribute, we look up the index of the row and set the selected index from - there. If the row isn't in the list, selection isn't changed. - """ - return self[self.selected_index] if self.selected_index is not None else None - - @selected_row.setter - def selected_row(self, value): - try: - self.selected_index = self.index(value) - except ValueError: - pass - - @property - def selected_rows(self): - """List of selected rows based on :attr:`.selected_indexes`. - - List of :class:`Row`. *read-only*. - """ - return [self[index] for index in self.selected_indexes] - - -class GUITableView: - """Expected interface for :class:`GUITable`'s view. - - *Not actually used in the code. For documentation purposes only.* - - Our view, some kind of table view, is expected to sync with the table's contents by - appropriately behave to all callbacks in this interface. - - When in edit mode, the content types by the user is expected to be sent as soon as possible - to the :class:`Row`. - - Whenever the user changes the selection, we expect the view to call :meth:`Table.select`. - """ - def refresh(self): - """Refreshes the contents of the table widget. - - Ensures that the contents of the table widget is synced with the model. This includes - selection. - """ - - def start_editing(self): - """Start editing the currently selected row. - - Begin whatever inline editing support that the view supports. - """ - - def stop_editing(self): - """Stop editing if there's an inline editing in effect. - - There's no "aborting" implied in this call, so it's appropriate to send whatever the user - has typed and might not have been sent down to the :class:`Row` yet. After you've done that, - stop the editing mechanism. - """ - - -SortDescriptor = namedtuple('SortDescriptor', 'column desc') -class GUITable(Table, GUIObject): - """Cross-toolkit GUI-enabled table view. - - Represents a UI element presenting the user with a sortable, selectable, possibly editable, - table view. - - Behaves like the :class:`Table` which it subclasses, but is more focused on being the presenter - of some model data to its :attr:`.GUIObject.view`. There's a :meth:`refresh` - mechanism which ensures fresh data while preserving sorting order and selection. There's also an - editing mechanism which tracks whether (and which) row is being edited (or added) and - save/cancel edits when appropriate. - - Subclasses :class:`Table` and :class:`.GUIObject`. Expected view: - :class:`GUITableView`. - """ - def __init__(self): - GUIObject.__init__(self) - Table.__init__(self) - #: The row being currently edited by the user. ``None`` if no edit is taking place. - self.edited = None - self._sort_descriptor = None - - #--- Virtual - def _do_add(self): - """(Virtual) Creates a new row, adds it in the table. - - Returns ``(row, insert_index)``. - """ - raise NotImplementedError() - - def _do_delete(self): - """(Virtual) Delete the selected rows. - """ - pass - - def _fill(self): - """(Virtual/Required) Fills the table with all the rows that this table is supposed to have. - - Called by :meth:`refresh`. Does nothing by default. - """ - pass - - def _is_edited_new(self): - """(Virtual) Returns whether the currently edited row should be considered "new". - - This is used in :meth:`cancel_edits` to know whether the cancellation of the edit means a - revert of the row's value or the removal of the row. - - By default, always false. - """ - return False - - def _restore_selection(self, previous_selection): - """(Virtual) Restores row selection after a contents-changing operation. - - Before each contents changing operation, we store our previously selected indexes because in - many cases, such as in :meth:`refresh`, our selection will be lost. After the operation is - over, we call this method with our previously selected indexes (in ``previous_selection``). - - The default behavior is (if we indeed have an empty :attr:`.selected_indexes`) to re-select - ``previous_selection``. If it was empty, we select the last row of the table. - - This behavior can, of course, be overriden. - """ - if not self.selected_indexes: - if previous_selection: - self.select(previous_selection) - else: - self.select([len(self) - 1]) - - #--- Public - def add(self): - """Add a new row in edit mode. - - Requires :meth:`do_add` to be implemented. The newly added row will be selected and in edit - mode. - """ - self.view.stop_editing() - if self.edited is not None: - self.save_edits() - row, insert_index = self._do_add() - self.insert(insert_index, row) - self.select([insert_index]) - self.edited = row - self.view.refresh() - self.view.start_editing() - - def can_edit_cell(self, column_name, row_index): - """Returns whether the cell at ``row_index`` and ``column_name`` can be edited. - - A row is, by default, editable as soon as it has an attr with the same name as `column`. - If :meth:`Row.can_edit` returns False, the row is not editable at all. You can set - editability of rows at the attribute level with can_edit_* properties. - - Mostly just a shortcut to :meth:`Row.can_edit_cell`. - """ - row = self[row_index] - return row.can_edit_cell(column_name) - - def cancel_edits(self): - """Cancels the current edit operation. - - If there's an :attr:`edited` row, it will be re-initialized (with :meth:`Row.load`). - """ - if self.edited is None: - return - self.view.stop_editing() - if self._is_edited_new(): - previous_selection = self.selected_indexes - self.remove(self.edited) - self._restore_selection(previous_selection) - self._update_selection() - else: - self.edited.load() - self.edited = None - self.view.refresh() - - def delete(self): - """Delete the currently selected rows. - - Requires :meth:`_do_delete` for this to have any effect on the model. Cancels editing if - relevant. - """ - self.view.stop_editing() - if self.edited is not None: - self.cancel_edits() - return - if self: - self._do_delete() - - def refresh(self, refresh_view=True): - """Empty the table and re-create its rows. - - :meth:`_fill` is called after we emptied the table to create our rows. Previous sort order - will be preserved, regardless of the order in which the rows were filled. If there was any - edit operation taking place, it's cancelled. - - :param bool refresh_view: Whether we tell our view to refresh after our refill operation. - Most of the time, it's what we want, but there's some cases where - we don't. - """ - self.cancel_edits() - previous_selection = self.selected_indexes - del self[:] - self._fill() - sd = self._sort_descriptor - if sd is not None: - Table.sort_by(self, column_name=sd.column, desc=sd.desc) - self._restore_selection(previous_selection) - if refresh_view: - self.view.refresh() - - def save_edits(self): - """Commit user edits to the model. - - This is done by calling :meth:`Row.save`. - """ - if self.edited is None: - return - row = self.edited - self.edited = None - row.save() - - def sort_by(self, column_name, desc=False): - """Sort table by ``column_name``. - - Overrides :meth:`Table.sort_by`. After having performed sorting, calls - :meth:`~.Selectable._update_selection` to give you the chance, - if appropriate, to update your selected indexes according to, maybe, the selection that you - have in your model. - - Then, we refresh our view. - """ - Table.sort_by(self, column_name=column_name, desc=desc) - self._sort_descriptor = SortDescriptor(column_name, desc) - self._update_selection() - self.view.refresh() - - -class Row: - """Represents a row in a :class:`Table`. - - It holds multiple values to be represented through columns. It's its role to prepare data - fetched from model instances into ready-to-present-in-a-table fashion. You will do this in - :meth:`load`. - - When you do this, you'll put the result into arbitrary attributes, which will later be fetched - by your table for presentation to the user. - - You can organize your attributes in whatever way you want, but there's a convention you can - follow if you want to minimize subclassing and use default behavior: - - 1. Attribute name = column name. If your attribute is ``foobar``, whenever we refer to - ``column_name``, you refer to that attribute with the column name ``foobar``. - 2. Public attributes are for *formatted* value, that is, user readable strings. - 3. Underscore prefix is the unformatted (computable) value. For example, you could have - ``_foobar`` at ``42`` and ``foobar`` at ``"42 seconds"`` (what you present to the user). - 4. Unformatted values are used for sorting. - 5. If your column name is a python keyword, add an underscore suffix (``from_``). - - Of course, this is only default behavior. This can be overriden. - """ - def __init__(self, table): - super(Row, self).__init__() - self.table = table - - def _edit(self): - if self.table.edited is self: - return - assert self.table.edited is None - self.table.edited = self - - #--- Virtual - def can_edit(self): - """(Virtual) Whether the whole row can be edited. - - By default, always returns ``True``. This is for the *whole* row. For individual cells, it's - :meth:`can_edit_cell`. - """ - return True - - def load(self): - """(Virtual/Required) Loads up values from the model to be presented in the table. - - Usually, our model instances contain values that are not quite ready for display. If you - have number formatting, display calculations and other whatnots to perform, you do it here - and then you put the result in an arbitrary attribute of the row. - """ - raise NotImplementedError() - - def save(self): - """(Virtual/Required) Saves user edits into your model. - - If your table is editable, this is called when the user commits his changes. Usually, these - are typed up stuff, or selected indexes. You have to do proper parsing and reference - linking, and save that stuff into your model. - """ - raise NotImplementedError() - - def sort_key_for_column(self, column_name): - """(Virtual) Return the value that is to be used to sort by column ``column_name``. - - By default, looks for an attribute with the same name as ``column_name``, but with an - underscore prefix ("unformatted value"). If there's none, tries without the underscore. If - there's none, raises ``AttributeError``. - """ - try: - return getattr(self, '_' + column_name) - except AttributeError: - return getattr(self, column_name) - - #--- Public - def can_edit_cell(self, column_name): - """Returns whether cell for column ``column_name`` can be edited. - - By the default, the check is done in many steps: - - 1. We check whether the whole row can be edited with :meth:`can_edit`. If it can't, the cell - can't either. - 2. If the column doesn't exist as an attribute, we can't edit. - 3. If we have an attribute ``can_edit_``, return that. - 4. Check if our attribute is a property. If it's not, it's not editable. - 5. If our attribute is in fact a property, check whether the property is "settable" (has a - ``fset`` method). The cell is editable only if the property is "settable". - """ - if not self.can_edit(): - return False - # '_' is in case column is a python keyword - if not hasattr(self, column_name): - if hasattr(self, column_name + '_'): - column_name = column_name + '_' - else: - return False - if hasattr(self, 'can_edit_' + column_name): - return getattr(self, 'can_edit_' + column_name) - # If the row has a settable property, we can edit the cell - rowclass = self.__class__ - prop = getattr(rowclass, column_name, None) - if prop is None: - return False - return bool(getattr(prop, 'fset', None)) - - def get_cell_value(self, attrname): - """Get cell value for ``attrname``. - - By default, does a simple ``getattr()``, but it is used to allow subclasses to have - alternative value storage mechanisms. - """ - if attrname == 'from': - attrname = 'from_' - return getattr(self, attrname) - - def set_cell_value(self, attrname, value): - """Set cell value to ``value`` for ``attrname``. - - By default, does a simple ``setattr()``, but it is used to allow subclasses to have - alternative value storage mechanisms. - """ - if attrname == 'from': - attrname = 'from_' - setattr(self, attrname, value) - diff --git a/hscommon/gui/text_field.py b/hscommon/gui/text_field.py deleted file mode 100644 index e918c58a..00000000 --- a/hscommon/gui/text_field.py +++ /dev/null @@ -1,108 +0,0 @@ -# Created On: 2012/01/23 -# Copyright 2015 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at -# http://www.gnu.org/licenses/gpl-3.0.html - -from .base import GUIObject -from ..util import nonone - -class TextFieldView: - """Expected interface for :class:`TextField`'s view. - - *Not actually used in the code. For documentation purposes only.* - - Our view is expected to sync with :attr:`TextField.text` "both ways", that is, update the - model's text when the user types something, but also update the text field when :meth:`refresh` - is called. - """ - def refresh(self): - """Refreshes the contents of the input widget. - - Ensures that the contents of the input widget is actually :attr:`TextField.text`. - """ - -class TextField(GUIObject): - """Cross-toolkit text field. - - Represents a UI element allowing the user to input a text value. Its main attribute is - :attr:`text` which acts as the store of the said value. - - When our model value isn't a string, we have a built-in parsing/formatting mechanism allowing - us to directly retrieve/set our non-string value through :attr:`value`. - - Subclasses :class:`.GUIObject`. Expected view: :class:`TextFieldView`. - """ - def __init__(self): - GUIObject.__init__(self) - self._text = '' - self._value = None - - #--- Virtual - def _parse(self, text): - """(Virtual) Parses ``text`` to put into :attr:`value`. - - Returns the parsed version of ``text``. Called whenever :attr:`text` changes. - """ - return text - - def _format(self, value): - """(Virtual) Formats ``value`` to put into :attr:`text`. - - Returns the formatted version of ``value``. Called whenever :attr:`value` changes. - """ - return value - - def _update(self, newvalue): - """(Virtual) Called whenever we have a new value. - - Whenever our text/value store changes to a new value (different from the old one), this - method is called. By default, it does nothing but you can override it if you want. - """ - - #--- Override - def _view_updated(self): - self.view.refresh() - - #--- Public - def refresh(self): - """Triggers a view :meth:`~TextFieldView.refresh`. - """ - self.view.refresh() - - @property - def text(self): - """The text that is currently displayed in the widget. - - *str*. *get/set*. - - This property can be set. When it is, :meth:`refresh` is called and the view is synced with - our value. Always in sync with :attr:`value`. - """ - return self._text - - @text.setter - def text(self, newtext): - self.value = self._parse(nonone(newtext, '')) - - @property - def value(self): - """The "parsed" representation of :attr:`text`. - - *arbitrary type*. *get/set*. - - By default, it's a mirror of :attr:`text`, but a subclass can override :meth:`_parse` and - :meth:`_format` to have anything else. Always in sync with :attr:`text`. - """ - return self._value - - @value.setter - def value(self, newvalue): - if newvalue == self._value: - return - self._value = newvalue - self._text = self._format(newvalue) - self._update(self._value) - self.refresh() - diff --git a/hscommon/gui/tree.py b/hscommon/gui/tree.py deleted file mode 100644 index 5d58d36a..00000000 --- a/hscommon/gui/tree.py +++ /dev/null @@ -1,251 +0,0 @@ -# Copyright 2015 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at -# http://www.gnu.org/licenses/gpl-3.0.html - -from collections import MutableSequence - -from .base import GUIObject - -class Node(MutableSequence): - """Pretty bland node implementation to be used in a :class:`Tree`. - - It has a :attr:`parent`, behaves like a list, its content being its children. Link integrity - is somewhat enforced (adding a child to a node will set the child's :attr:`parent`, but that's - pretty much as far as we go, integrity-wise. Nodes don't tend to move around much in a GUI - tree). We don't even check for infinite node loops. Don't play around these grounds too much. - - Nodes are designed to be subclassed and given meaningful attributes (those you'll want to - display in your tree view), but they all have a :attr:`name`, which is given on initialization. - """ - def __init__(self, name): - self._name = name - self._parent = None - self._path = None - self._children = [] - - def __repr__(self): - return '' % self.name - - #--- MutableSequence overrides - def __delitem__(self, key): - self._children.__delitem__(key) - - def __getitem__(self, key): - return self._children.__getitem__(key) - - def __len__(self): - return len(self._children) - - def __setitem__(self, key, value): - self._children.__setitem__(key, value) - - def append(self, node): - self._children.append(node) - node._parent = self - node._path = None - - def insert(self, index, node): - self._children.insert(index, node) - node._parent = self - node._path = None - - #--- Public - def clear(self): - """Clears the node of all its children. - """ - del self[:] - - def find(self, predicate, include_self=True): - """Return the first child to match ``predicate``. - - See :meth:`findall`. - """ - try: - return next(self.findall(predicate, include_self=include_self)) - except StopIteration: - return None - - def findall(self, predicate, include_self=True): - """Yield all children matching ``predicate``. - - :param predicate: ``f(node) --> bool`` - :param include_self: Whether we can return ``self`` or we return only children. - """ - if include_self and predicate(self): - yield self - for child in self: - for found in child.findall(predicate, include_self=True): - yield found - - def get_node(self, index_path): - """Returns the node at ``index_path``. - - :param index_path: a list of int indexes leading to our node. See :attr:`path`. - """ - result = self - if index_path: - for index in index_path: - result = result[index] - return result - - def get_path(self, target_node): - """Returns the :attr:`path` of ``target_node``. - - If ``target_node`` is ``None``, returns ``None``. - """ - if target_node is None: - return None - return target_node.path - - @property - def children_count(self): - """Same as ``len(self)``. - """ - return len(self) - - @property - def name(self): - """Name for the node, supplied on init. - """ - return self._name - - @property - def parent(self): - """Parent of the node. - - If ``None``, we have a root node. - """ - return self._parent - - @property - def path(self): - """A list of node indexes leading from the root node to ``self``. - - The path of a node is always related to its :attr:`root`. It's the sequences of index that - we have to take to get to our node, starting from the root. For example, if - ``node.path == [1, 2, 3, 4]``, it means that ``node.root[1][2][3][4] is node``. - """ - if self._path is None: - if self._parent is None: - self._path = [] - else: - self._path = self._parent.path + [self._parent.index(self)] - return self._path - - @property - def root(self): - """Root node of current node. - - To get it, we recursively follow our :attr:`parent` chain until we have ``None``. - """ - if self._parent is None: - return self - else: - return self._parent.root - - -class Tree(Node, GUIObject): - """Cross-toolkit GUI-enabled tree view. - - This class is a bit too thin to be used as a tree view controller out of the box and HS apps - that subclasses it each add quite a bit of logic to it to make it workable. Making this more - usable out of the box is a work in progress. - - This class is here (in addition to being a :class:`Node`) mostly to handle selection. - - Subclasses :class:`Node` (it is the root node of all its children) and :class:`.GUIObject`. - """ - def __init__(self): - Node.__init__(self, '') - GUIObject.__init__(self) - #: Where we store selected nodes (as a list of :class:`Node`) - self._selected_nodes = [] - - #--- Virtual - def _select_nodes(self, nodes): - """(Virtual) Customize node selection behavior. - - By default, simply set :attr:`_selected_nodes`. - """ - self._selected_nodes = nodes - - #--- Override - def _view_updated(self): - self.view.refresh() - - def clear(self): - self._selected_nodes = [] - Node.clear(self) - - #--- Public - @property - def selected_node(self): - """Currently selected node. - - *:class:`Node`*. *get/set*. - - First of :attr:`selected_nodes`. ``None`` if empty. - """ - return self._selected_nodes[0] if self._selected_nodes else None - - @selected_node.setter - def selected_node(self, node): - if node is not None: - self._select_nodes([node]) - else: - self._select_nodes([]) - - @property - def selected_nodes(self): - """List of selected nodes in the tree. - - *List of :class:`Node`*. *get/set*. - - We use nodes instead of indexes to store selection because it's simpler when it's time to - manage selection of multiple node levels. - """ - return self._selected_nodes - - @selected_nodes.setter - def selected_nodes(self, nodes): - self._select_nodes(nodes) - - @property - def selected_path(self): - """Currently selected path. - - *:attr:`Node.path`*. *get/set*. - - First of :attr:`selected_paths`. ``None`` if empty. - """ - return self.get_path(self.selected_node) - - @selected_path.setter - def selected_path(self, index_path): - if index_path is not None: - self.selected_paths = [index_path] - else: - self._select_nodes([]) - - @property - def selected_paths(self): - """List of selected paths in the tree. - - *List of :attr:`Node.path`*. *get/set* - - Computed from :attr:`selected_nodes`. - """ - return list(map(self.get_path, self._selected_nodes)) - - @selected_paths.setter - def selected_paths(self, index_paths): - nodes = [] - for path in index_paths: - try: - nodes.append(self.get_node(path)) - except IndexError: - pass - self._select_nodes(nodes) - diff --git a/hscommon/jobprogress/__init__.py b/hscommon/jobprogress/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/hscommon/jobprogress/job.py b/hscommon/jobprogress/job.py deleted file mode 100644 index 214a9889..00000000 --- a/hscommon/jobprogress/job.py +++ /dev/null @@ -1,166 +0,0 @@ -# Created By: Virgil Dupras -# Created On: 2004/12/20 -# Copyright 2011 Hardcoded Software (http://www.hardcoded.net) - -# This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at -# http://www.gnu.org/licenses/gpl-3.0.html - -class JobCancelled(Exception): - "The user has cancelled the job" - -class JobInProgressError(Exception): - "A job is already being performed, you can't perform more than one at the same time." - -class JobCountError(Exception): - "The number of jobs started have exceeded the number of jobs allowed" - -class Job: - """Manages a job's progression and return it's progression through a callback. - - Note that this class is not foolproof. For example, you could call - start_subjob, and then call add_progress from the parent job, and nothing - would stop you from doing it. However, it would mess your progression - because it is the sub job that is supposed to drive the progression. - Another example would be to start a subjob, then start another, and call - add_progress from the old subjob. Once again, it would mess your progression. - There are no stops because it would remove the lightweight aspect of the - class (A Job would need to have a Parent instead of just a callback, - and the parent could be None. A lot of checks for nothing.). - Another one is that nothing stops you from calling add_progress right after - SkipJob. - """ - #---Magic functions - def __init__(self, job_proportions, callback): - """Initialize the Job with 'jobcount' jobs. Start every job with - start_job(). Every time the job progress is updated, 'callback' is called - 'callback' takes a 'progress' int param, and a optional 'desc' - parameter. Callback must return false if the job must be cancelled. - """ - if not hasattr(callback, '__call__'): - raise TypeError("'callback' MUST be set when creating a Job") - if isinstance(job_proportions, int): - job_proportions = [1] * job_proportions - self._job_proportions = list(job_proportions) - self._jobcount = sum(job_proportions) - self._callback = callback - self._current_job = 0 - self._passed_jobs = 0 - self._progress = 0 - self._currmax = 1 - - #---Private - def _subjob_callback(self, progress, desc=''): - """This is the callback passed to children jobs. - """ - self.set_progress(progress, desc) - return True #if JobCancelled has to be raised, it will be at the highest level - - def _do_update(self, desc): - """Calls the callback function with a % progress as a parameter. - - The parameter is a int in the 0-100 range. - """ - if self._current_job: - passed_progress = self._passed_jobs * self._currmax - current_progress = self._current_job * self._progress - total_progress = self._jobcount * self._currmax - progress = ((passed_progress + current_progress) * 100) // total_progress - else: - progress = -1 # indeterminate - # It's possible that callback doesn't support a desc arg - result = self._callback(progress, desc) if desc else self._callback(progress) - if not result: - raise JobCancelled() - - #---Public - def add_progress(self, progress=1, desc=''): - self.set_progress(self._progress + progress, desc) - - def check_if_cancelled(self): - self._do_update('') - - def iter_with_progress(self, iterable, desc_format=None, every=1, count=None): - """Iterate through ``iterable`` while automatically adding progress. - - WARNING: We need our iterable's length. If ``iterable`` is not a sequence (that is, - something we can call ``len()`` on), you *have* to specify a count through the ``count`` - argument. If ``count`` is ``None``, ``len(iterable)`` is used. - """ - if count is None: - count = len(iterable) - desc = '' - if desc_format: - desc = desc_format % (0, count) - self.start_job(count, desc) - for i, element in enumerate(iterable, start=1): - yield element - if i % every == 0: - if desc_format: - desc = desc_format % (i, count) - self.add_progress(progress=every, desc=desc) - if desc_format: - desc = desc_format % (count, count) - self.set_progress(100, desc) - - def start_job(self, max_progress=100, desc=''): - """Begin work on the next job. You must not call start_job more than - 'jobcount' (in __init__) times. - 'max' is the job units you are to perform. - 'desc' is the description of the job. - """ - self._passed_jobs += self._current_job - try: - self._current_job = self._job_proportions.pop(0) - except IndexError: - raise JobCountError() - self._progress = 0 - self._currmax = max(1, max_progress) - self._do_update(desc) - - def start_subjob(self, job_proportions, desc=''): - """Starts a sub job. Use this when you want to split a job into - multiple smaller jobs. Pretty handy when starting a process where you - know how many subjobs you will have, but don't know the work unit count - for every of them. - returns the Job object - """ - self.start_job(100, desc) - return Job(job_proportions, self._subjob_callback) - - def set_progress(self, progress, desc=''): - """Sets the progress of the current job to 'progress', and call the - callback - """ - self._progress = progress - if self._progress > self._currmax: - self._progress = self._currmax - if self._progress < 0: - self._progress = 0 - self._do_update(desc) - - -class NullJob: - def __init__(self, *args, **kwargs): - pass - - def add_progress(self, *args, **kwargs): - pass - - def check_if_cancelled(self): - pass - - def iter_with_progress(self, sequence, *args, **kwargs): - return iter(sequence) - - def start_job(self, *args, **kwargs): - pass - - def start_subjob(self, *args, **kwargs): - return NullJob() - - def set_progress(self, *args, **kwargs): - pass - - -nulljob = NullJob() diff --git a/hscommon/jobprogress/performer.py b/hscommon/jobprogress/performer.py deleted file mode 100644 index 12e0dc52..00000000 --- a/hscommon/jobprogress/performer.py +++ /dev/null @@ -1,72 +0,0 @@ -# Created By: Virgil Dupras -# Created On: 2010-11-19 -# Copyright 2011 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at -# http://www.gnu.org/licenses/gpl-3.0.html - -from threading import Thread -import sys - -from .job import Job, JobInProgressError, JobCancelled - -class ThreadedJobPerformer: - """Run threaded jobs and track progress. - - To run a threaded job, first create a job with _create_job(), then call _run_threaded(), with - your work function as a parameter. - - Example: - - j = self._create_job() - self._run_threaded(self.some_work_func, (arg1, arg2, j)) - """ - _job_running = False - last_error = None - - #--- Protected - def create_job(self): - if self._job_running: - raise JobInProgressError() - self.last_progress = -1 - self.last_desc = '' - self.job_cancelled = False - return Job(1, self._update_progress) - - def _async_run(self, *args): - target = args[0] - args = tuple(args[1:]) - self._job_running = True - self.last_error = None - try: - target(*args) - except JobCancelled: - pass - except Exception as e: - self.last_error = e - self.last_traceback = sys.exc_info()[2] - finally: - self._job_running = False - self.last_progress = None - - def reraise_if_error(self): - """Reraises the error that happened in the thread if any. - - Call this after the caller of run_threaded detected that self._job_running returned to False - """ - if self.last_error is not None: - raise self.last_error.with_traceback(self.last_traceback) - - def _update_progress(self, newprogress, newdesc=''): - self.last_progress = newprogress - if newdesc: - self.last_desc = newdesc - return not self.job_cancelled - - def run_threaded(self, target, args=()): - if self._job_running: - raise JobInProgressError() - args = (target, ) + args - Thread(target=self._async_run, args=args).start() - diff --git a/hscommon/jobprogress/qt.py b/hscommon/jobprogress/qt.py deleted file mode 100644 index 70901385..00000000 --- a/hscommon/jobprogress/qt.py +++ /dev/null @@ -1,52 +0,0 @@ -# Created By: Virgil Dupras -# Created On: 2009-09-14 -# Copyright 2011 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at -# http://www.gnu.org/licenses/gpl-3.0.html - -from PyQt5.QtCore import pyqtSignal, Qt, QTimer -from PyQt5.QtWidgets import QProgressDialog - -from . import performer - -class Progress(QProgressDialog, performer.ThreadedJobPerformer): - finished = pyqtSignal(['QString']) - - def __init__(self, parent): - flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint - QProgressDialog.__init__(self, '', "Cancel", 0, 100, parent, flags) - self.setModal(True) - self.setAutoReset(False) - self.setAutoClose(False) - self._timer = QTimer() - self._jobid = '' - self._timer.timeout.connect(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() - if not self.job_cancelled: - self.finished.emit(self._jobid) - 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) - diff --git a/hscommon/loc.py b/hscommon/loc.py deleted file mode 100644 index d6bf3bbd..00000000 --- a/hscommon/loc.py +++ /dev/null @@ -1,197 +0,0 @@ -import os -import os.path as op -import shutil -import re -import tempfile - -import polib - -from . import pygettext -from .util import modified_after, dedupe, ensure_folder, ensure_file -from .build import print_and_do, ensure_empty_folder, copy - -LC_MESSAGES = 'LC_MESSAGES' - -def get_langs(folder): - return [name for name in os.listdir(folder) if op.isdir(op.join(folder, name))] - -def files_with_ext(folder, ext): - return [op.join(folder, fn) for fn in os.listdir(folder) if fn.endswith(ext)] - -def generate_pot(folders, outpath, keywords, merge=False): - if merge and not op.exists(outpath): - merge = False - if merge: - _, genpath = tempfile.mkstemp() - else: - genpath = outpath - pyfiles = [] - for folder in folders: - for root, dirs, filenames in os.walk(folder): - keep = [fn for fn in filenames if fn.endswith('.py')] - pyfiles += [op.join(root, fn) for fn in keep] - pygettext.main(pyfiles, outpath=genpath, keywords=keywords) - if merge: - merge_po_and_preserve(genpath, outpath) - os.remove(genpath) - -def compile_all_po(base_folder): - langs = get_langs(base_folder) - for lang in langs: - pofolder = op.join(base_folder, lang, LC_MESSAGES) - pofiles = files_with_ext(pofolder, '.po') - for pofile in pofiles: - p = polib.pofile(pofile) - p.save_as_mofile(pofile[:-3] + '.mo') - -def merge_locale_dir(target, mergeinto): - langs = get_langs(target) - for lang in langs: - if not op.exists(op.join(mergeinto, lang)): - continue - mofolder = op.join(target, lang, LC_MESSAGES) - mofiles = files_with_ext(mofolder, '.mo') - for mofile in mofiles: - shutil.copy(mofile, op.join(mergeinto, lang, LC_MESSAGES)) - -def merge_pots_into_pos(folder): - # We're going to take all pot files in `folder` and for each lang, merge it with the po file - # with the same name. - potfiles = files_with_ext(folder, '.pot') - for potfile in potfiles: - refpot = polib.pofile(potfile) - refname = op.splitext(op.basename(potfile))[0] - for lang in get_langs(folder): - po = polib.pofile(op.join(folder, lang, LC_MESSAGES, refname + '.po')) - po.merge(refpot) - po.save() - -def merge_po_and_preserve(source, dest): - # Merges source entries into dest, but keep old entries intact - sourcepo = polib.pofile(source) - destpo = polib.pofile(dest) - for entry in sourcepo: - if destpo.find(entry.msgid) is not None: - # The entry is already there - continue - destpo.append(entry) - destpo.save() - -def normalize_all_pos(base_folder): - """Normalize the format of .po files in base_folder. - - When getting POs from external sources, such as Transifex, we end up with spurious diffs because - of a difference in the way line wrapping is handled. It wouldn't be a big deal if it happened - once, but these spurious diffs keep overwriting each other, and it's annoying. - - Our PO files will keep polib's format. Call this function to ensure that freshly pulled POs - are of the right format before committing them. - """ - langs = get_langs(base_folder) - for lang in langs: - pofolder = op.join(base_folder, lang, LC_MESSAGES) - pofiles = files_with_ext(pofolder, '.po') - for pofile in pofiles: - p = polib.pofile(pofile) - p.save() - -#--- Cocoa -def all_lproj_paths(folder): - return files_with_ext(folder, '.lproj') - -def escape_cocoa_strings(s): - return s.replace('\\', '\\\\').replace('"', '\\"').replace('\n', '\\n') - -def unescape_cocoa_strings(s): - return s.replace('\\\\', '\\').replace('\\"', '"').replace('\\n', '\n') - -def strings2pot(target, dest): - with open(target, 'rt', encoding='utf-8') as fp: - contents = fp.read() - # We're reading an en.lproj file. We only care about the righthand part of the translation. - re_trans = re.compile(r'".*" = "(.*)";') - strings = re_trans.findall(contents) - if op.exists(dest): - po = polib.pofile(dest) - else: - po = polib.POFile() - for s in dedupe(strings): - s = unescape_cocoa_strings(s) - entry = po.find(s) - if entry is None: - entry = polib.POEntry(msgid=s) - po.append(entry) - # we don't know or care about a line number so we put 0 - entry.occurrences.append((target, '0')) - entry.occurrences = dedupe(entry.occurrences) - po.save(dest) - -def allstrings2pot(lprojpath, dest, excludes=None): - allstrings = files_with_ext(lprojpath, '.strings') - if excludes: - allstrings = [p for p in allstrings if op.splitext(op.basename(p))[0] not in excludes] - for strings_path in allstrings: - strings2pot(strings_path, dest) - -def po2strings(pofile, en_strings, dest): - # Takes en_strings and replace all righthand parts of "foo" = "bar"; entries with translations - # in pofile, then puts the result in dest. - po = polib.pofile(pofile) - if not modified_after(pofile, dest): - return - ensure_folder(op.dirname(dest)) - print("Creating {} from {}".format(dest, pofile)) - with open(en_strings, 'rt', encoding='utf-8') as fp: - contents = fp.read() - re_trans = re.compile(r'(?<= = ").*(?=";\n)') - def repl(match): - s = match.group(0) - unescaped = unescape_cocoa_strings(s) - entry = po.find(unescaped) - if entry is None: - print("WARNING: Could not find entry '{}' in .po file".format(s)) - return s - trans = entry.msgstr - return escape_cocoa_strings(trans) if trans else s - contents = re_trans.sub(repl, contents) - with open(dest, 'wt', encoding='utf-8') as fp: - fp.write(contents) - -def generate_cocoa_strings_from_code(code_folder, dest_folder): - # Uses the "genstrings" command to generate strings file from all .m files in "code_folder". - # The strings file (their name depends on the localization table used in the source) will be - # placed in "dest_folder". - # genstrings produces utf-16 files with comments. After having generated the files, we convert - # them to utf-8 and remove the comments. - ensure_empty_folder(dest_folder) - print_and_do('genstrings -o "{}" `find "{}" -name *.m | xargs`'.format(dest_folder, code_folder)) - for stringsfile in os.listdir(dest_folder): - stringspath = op.join(dest_folder, stringsfile) - with open(stringspath, 'rt', encoding='utf-16') as fp: - content = fp.read() - content = re.sub('/\*.*?\*/', '', content) - content = re.sub('\n{2,}', '\n', content) - # I have no idea why, but genstrings seems to have problems with "%" character in strings - # and inserts (number)$ after it. Find these bogus inserts and remove them. - content = re.sub('%\d\$', '%', content) - with open(stringspath, 'wt', encoding='utf-8') as fp: - fp.write(content) - -def build_cocoa_localizations(app, en_stringsfile=op.join('cocoa', 'en.lproj', 'Localizable.strings')): - # Creates .lproj folders with Localizable.strings and cocoalib.strings based on cocoalib.po and - # ui.po for all available languages as well as base strings files in en.lproj. These lproj - # folders are created in `app`'s (a OSXAppStructure) resource folder. - print("Creating lproj folders based on .po files") - en_cocoastringsfile = op.join('cocoalib', 'en.lproj', 'cocoalib.strings') - for lang in get_langs('locale'): - pofile = op.join('locale', lang, 'LC_MESSAGES', 'ui.po') - dest_lproj = op.join(app.resources, lang + '.lproj') - ensure_folder(dest_lproj) - po2strings(pofile, en_stringsfile, op.join(dest_lproj, 'Localizable.strings')) - pofile = op.join('cocoalib', 'locale', lang, 'LC_MESSAGES', 'cocoalib.po') - po2strings(pofile, en_cocoastringsfile, op.join(dest_lproj, 'cocoalib.strings')) - # We also have to copy the "en.lproj" strings - en_lproj = op.join(app.resources, 'en.lproj') - ensure_folder(en_lproj) - copy(en_stringsfile, en_lproj) - copy(en_cocoastringsfile, en_lproj) diff --git a/hscommon/notify.py b/hscommon/notify.py deleted file mode 100644 index 92b5e3dd..00000000 --- a/hscommon/notify.py +++ /dev/null @@ -1,89 +0,0 @@ -# Copyright 2015 Hardcoded Software (http://www.hardcoded.net) - -# This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at -# http://www.gnu.org/licenses/gpl-3.0.html - -"""Very simple inter-object notification system. - -This module is a brain-dead simple notification system involving a :class:`Broadcaster` and a -:class:`Listener`. A listener can only listen to one broadcaster. A broadcaster can have multiple -listeners. If the listener is connected, whenever the broadcaster calls :meth:`~Broadcaster.notify`, -the method with the same name as the broadcasted message is called on the listener. -""" - -from collections import defaultdict - -class Broadcaster: - """Broadcasts messages that are received by all listeners. - """ - def __init__(self): - self.listeners = set() - - def add_listener(self, listener): - self.listeners.add(listener) - - def notify(self, msg): - """Notify all connected listeners of ``msg``. - - That means that each listeners will have their method with the same name as ``msg`` called. - """ - for listener in self.listeners.copy(): # listeners can change during iteration - if listener in self.listeners: # disconnected during notification - listener.dispatch(msg) - - def remove_listener(self, listener): - self.listeners.discard(listener) - - -class Listener: - """A listener is initialized with the broadcaster it's going to listen to. Initially, it is not connected. - """ - def __init__(self, broadcaster): - self.broadcaster = broadcaster - self._bound_notifications = defaultdict(list) - - def bind_messages(self, messages, func): - """Binds multiple message to the same function. - - Often, we perform the same thing on multiple messages. Instead of having the same function - repeated again and agin in our class, we can use this method to bind multiple messages to - the same function. - """ - for message in messages: - self._bound_notifications[message].append(func) - - def connect(self): - """Connects the listener to its broadcaster. - """ - self.broadcaster.add_listener(self) - - def disconnect(self): - """Disconnects the listener from its broadcaster. - """ - self.broadcaster.remove_listener(self) - - def dispatch(self, msg): - if msg in self._bound_notifications: - for func in self._bound_notifications[msg]: - func() - if hasattr(self, msg): - method = getattr(self, msg) - method() - - -class Repeater(Broadcaster, Listener): - REPEATED_NOTIFICATIONS = None - - def __init__(self, broadcaster): - Broadcaster.__init__(self) - Listener.__init__(self, broadcaster) - - def _repeat_message(self, msg): - if not self.REPEATED_NOTIFICATIONS or msg in self.REPEATED_NOTIFICATIONS: - self.notify(msg) - - def dispatch(self, msg): - Listener.dispatch(self, msg) - self._repeat_message(msg) - diff --git a/hscommon/path.py b/hscommon/path.py deleted file mode 100644 index b508e6fe..00000000 --- a/hscommon/path.py +++ /dev/null @@ -1,243 +0,0 @@ -# Created By: Virgil Dupras -# Created On: 2006/02/21 -# Copyright 2015 Hardcoded Software (http://www.hardcoded.net) - -# This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at -# http://www.gnu.org/licenses/gpl-3.0.html - -import logging -import os -import os.path as op -import shutil -import sys -from itertools import takewhile -from functools import wraps -from inspect import signature - -class Path(tuple): - """A handy class to work with paths. - - We subclass ``tuple``, each element of the tuple represents an element of the path. - - * ``Path('/foo/bar/baz')[1]`` --> ``'bar'`` - * ``Path('/foo/bar/baz')[1:2]`` --> ``Path('bar/baz')`` - * ``Path('/foo/bar')['baz']`` --> ``Path('/foo/bar/baz')`` - * ``str(Path('/foo/bar/baz'))`` --> ``'/foo/bar/baz'`` - """ - # Saves a little bit of memory usage - __slots__ = () - - def __new__(cls, value, separator=None): - def unicode_if_needed(s): - if isinstance(s, str): - return s - else: - try: - return str(s, sys.getfilesystemencoding()) - except UnicodeDecodeError: - logging.warning("Could not decode %r", s) - raise - - if isinstance(value, Path): - return value - if not separator: - separator = os.sep - if isinstance(value, bytes): - value = unicode_if_needed(value) - if isinstance(value, str): - if value: - if (separator not in value) and ('/' in value): - separator = '/' - value = value.split(separator) - else: - value = () - else: - if any(isinstance(x, bytes) for x in value): - value = [unicode_if_needed(x) for x in value] - #value is a tuple/list - if any(separator in x for x in value): - #We have a component with a separator in it. Let's rejoin it, and generate another path. - return Path(separator.join(value), separator) - if (len(value) > 1) and (not value[-1]): - value = value[:-1] #We never want a path to end with a '' (because Path() can be called with a trailing slash ending path) - return tuple.__new__(cls, value) - - def __add__(self, other): - other = Path(other) - if other and (not other[0]): - other = other[1:] - return Path(tuple.__add__(self, other)) - - def __contains__(self, item): - if isinstance(item, Path): - return item[:len(self)] == self - else: - return tuple.__contains__(self, item) - - def __eq__(self, other): - return tuple.__eq__(self, Path(other)) - - def __getitem__(self, key): - if isinstance(key, slice): - if isinstance(key.start, Path): - equal_elems = list(takewhile(lambda pair: pair[0] == pair[1], zip(self, key.start))) - key = slice(len(equal_elems), key.stop, key.step) - if isinstance(key.stop, Path): - equal_elems = list(takewhile(lambda pair: pair[0] == pair[1], zip(reversed(self), reversed(key.stop)))) - stop = -len(equal_elems) if equal_elems else None - key = slice(key.start, stop, key.step) - return Path(tuple.__getitem__(self, key)) - elif isinstance(key, (str, Path)): - return self + key - else: - return tuple.__getitem__(self, key) - - def __hash__(self): - return tuple.__hash__(self) - - def __ne__(self, other): - return not self.__eq__(other) - - def __radd__(self, other): - return Path(other) + self - - def __str__(self): - if len(self) == 1: - first = self[0] - if (len(first) == 2) and (first[1] == ':'): #Windows drive letter - return first + '\\' - elif not len(first): #root directory - return '/' - return os.sep.join(self) - - def has_drive_letter(self): - if not self: - return False - first = self[0] - return (len(first) == 2) and (first[1] == ':') - - def is_parent_of(self, other): - """Whether ``other`` is a subpath of ``self``. - - Almost the same as ``other in self``, but it's a bit more self-explicative and when - ``other == self``, returns False. - """ - if other == self: - return False - else: - return other in self - - def remove_drive_letter(self): - if self.has_drive_letter(): - return self[1:] - else: - return self - - def tobytes(self): - return str(self).encode(sys.getfilesystemencoding()) - - def parent(self): - """Returns the parent path. - - ``Path('/foo/bar/baz').parent()`` --> ``Path('/foo/bar')`` - """ - return self[:-1] - - @property - def name(self): - """Last element of the path (filename), with extension. - - ``Path('/foo/bar/baz').name`` --> ``'baz'`` - """ - return self[-1] - - # OS method wrappers - def exists(self): - return op.exists(str(self)) - - def copy(self, dest_path): - return shutil.copy(str(self), str(dest_path)) - - def copytree(self, dest_path, *args, **kwargs): - return shutil.copytree(str(self), str(dest_path), *args, **kwargs) - - def isdir(self): - return op.isdir(str(self)) - - def isfile(self): - return op.isfile(str(self)) - - def islink(self): - return op.islink(str(self)) - - def listdir(self): - return [self[name] for name in os.listdir(str(self))] - - def mkdir(self, *args, **kwargs): - return os.mkdir(str(self), *args, **kwargs) - - def makedirs(self, *args, **kwargs): - return os.makedirs(str(self), *args, **kwargs) - - def move(self, dest_path): - return shutil.move(str(self), str(dest_path)) - - def open(self, *args, **kwargs): - return open(str(self), *args, **kwargs) - - def remove(self): - return os.remove(str(self)) - - def rename(self, dest_path): - return os.rename(str(self), str(dest_path)) - - def rmdir(self): - return os.rmdir(str(self)) - - def rmtree(self): - return shutil.rmtree(str(self)) - - def stat(self): - return os.stat(str(self)) - -def pathify(f): - """Ensure that every annotated :class:`Path` arguments are actually paths. - - When a function is decorated with ``@pathify``, every argument with annotated as Path will be - converted to a Path if it wasn't already. Example:: - - @pathify - def foo(path: Path, otherarg): - return path.listdir() - - Calling ``foo('/bar', 0)`` will convert ``'/bar'`` to ``Path('/bar')``. - """ - sig = signature(f) - pindexes = {i for i, p in enumerate(sig.parameters.values()) if p.annotation is Path} - pkeys = {k: v for k, v in sig.parameters.items() if v.annotation is Path} - def path_or_none(p): - return None if p is None else Path(p) - - @wraps(f) - def wrapped(*args, **kwargs): - args = tuple((path_or_none(a) if i in pindexes else a) for i, a in enumerate(args)) - kwargs = {k: (path_or_none(v) if k in pkeys else v) for k, v in kwargs.items()} - return f(*args, **kwargs) - - return wrapped - -def log_io_error(func): - """ Catches OSError, IOError and WindowsError and log them - """ - @wraps(func) - def wrapper(path, *args, **kwargs): - try: - return func(path, *args, **kwargs) - except (IOError, OSError) as e: - msg = 'Error "{0}" during operation "{1}" on "{2}": "{3}"' - classname = e.__class__.__name__ - funcname = func.__name__ - logging.warn(msg.format(classname, funcname, str(path), str(e))) - - return wrapper diff --git a/hscommon/plat.py b/hscommon/plat.py deleted file mode 100644 index fa5f1737..00000000 --- a/hscommon/plat.py +++ /dev/null @@ -1,16 +0,0 @@ -# Created On: 2011/09/22 -# Copyright 2015 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at -# http://www.gnu.org/licenses/gpl-3.0.html - -# Yes, I know, there's the 'platform' unit for this kind of stuff, but the thing is that I got a -# crash on startup once simply for importing this module and since then I don't trust it. One day, -# I'll investigate the cause of that crash further. - -import sys - -ISWINDOWS = sys.platform == 'win32' -ISOSX = sys.platform == 'darwin' -ISLINUX = sys.platform.startswith('linux') \ No newline at end of file diff --git a/hscommon/pygettext.py b/hscommon/pygettext.py deleted file mode 100755 index ad3157b6..00000000 --- a/hscommon/pygettext.py +++ /dev/null @@ -1,417 +0,0 @@ -# This module was taken from CPython's Tools/i18n and dirtily hacked to bypass the need for cmdline -# invocation. - -# Originally written by Barry Warsaw -# -# Minimally patched to make it even more xgettext compatible -# by Peter Funk -# -# 2002-11-22 Jürgen Hermann -# Added checks that _() only contains string literals, and -# command line args are resolved to module lists, i.e. you -# can now pass a filename, a module or package name, or a -# directory (including globbing chars, important for Win32). -# Made docstring fit in 80 chars wide displays using pydoc. -# - -import os -import imp -import sys -import glob -import time -import token -import tokenize -import operator - -__version__ = '1.5' - -default_keywords = ['_'] -DEFAULTKEYWORDS = ', '.join(default_keywords) - -EMPTYSTRING = '' - - - -# The normal pot-file header. msgmerge and Emacs's po-mode work better if it's -# there. -pot_header = """ -msgid "" -msgstr "" -"Content-Type: text/plain; charset=utf-8\\n" -"Content-Transfer-Encoding: utf-8\\n" -""" - - -def usage(code, msg=''): - print(__doc__ % globals(), file=sys.stderr) - if msg: - print(msg, file=sys.stderr) - sys.exit(code) - - - -escapes = [] - -def make_escapes(pass_iso8859): - global escapes - if pass_iso8859: - # Allow iso-8859 characters to pass through so that e.g. 'msgid - # "H?he"' would result not result in 'msgid "H\366he"'. Otherwise we - # escape any character outside the 32..126 range. - mod = 128 - else: - mod = 256 - for i in range(256): - if 32 <= (i % mod) <= 126: - escapes.append(chr(i)) - else: - escapes.append("\\%03o" % i) - escapes[ord('\\')] = '\\\\' - escapes[ord('\t')] = '\\t' - escapes[ord('\r')] = '\\r' - escapes[ord('\n')] = '\\n' - escapes[ord('\"')] = '\\"' - - -def escape(s): - global escapes - s = list(s) - for i in range(len(s)): - s[i] = escapes[ord(s[i])] - return EMPTYSTRING.join(s) - - -def safe_eval(s): - # unwrap quotes, safely - return eval(s, {'__builtins__':{}}, {}) - - -def normalize(s): - # This converts the various Python string types into a format that is - # appropriate for .po files, namely much closer to C style. - lines = s.split('\n') - if len(lines) == 1: - s = '"' + escape(s) + '"' - else: - if not lines[-1]: - del lines[-1] - lines[-1] = lines[-1] + '\n' - for i in range(len(lines)): - lines[i] = escape(lines[i]) - lineterm = '\\n"\n"' - s = '""\n"' + lineterm.join(lines) + '"' - return s - - -def containsAny(str, set): - """Check whether 'str' contains ANY of the chars in 'set'""" - return 1 in [c in str for c in set] - - -def _visit_pyfiles(list, dirname, names): - """Helper for getFilesForName().""" - # get extension for python source files - if '_py_ext' not in globals(): - global _py_ext - _py_ext = [triple[0] for triple in imp.get_suffixes() - if triple[2] == imp.PY_SOURCE][0] - - # don't recurse into CVS directories - if 'CVS' in names: - names.remove('CVS') - - # add all *.py files to list - list.extend( - [os.path.join(dirname, file) for file in names - if os.path.splitext(file)[1] == _py_ext] - ) - - -def _get_modpkg_path(dotted_name, pathlist=None): - """Get the filesystem path for a module or a package. - - Return the file system path to a file for a module, and to a directory for - a package. Return None if the name is not found, or is a builtin or - extension module. - """ - # split off top-most name - parts = dotted_name.split('.', 1) - - if len(parts) > 1: - # we have a dotted path, import top-level package - try: - file, pathname, description = imp.find_module(parts[0], pathlist) - if file: file.close() - except ImportError: - return None - - # check if it's indeed a package - if description[2] == imp.PKG_DIRECTORY: - # recursively handle the remaining name parts - pathname = _get_modpkg_path(parts[1], [pathname]) - else: - pathname = None - else: - # plain name - try: - file, pathname, description = imp.find_module( - dotted_name, pathlist) - if file: - file.close() - if description[2] not in [imp.PY_SOURCE, imp.PKG_DIRECTORY]: - pathname = None - except ImportError: - pathname = None - - return pathname - - -def getFilesForName(name): - """Get a list of module files for a filename, a module or package name, - or a directory. - """ - if not os.path.exists(name): - # check for glob chars - if containsAny(name, "*?[]"): - files = glob.glob(name) - list = [] - for file in files: - list.extend(getFilesForName(file)) - return list - - # try to find module or package - name = _get_modpkg_path(name) - if not name: - return [] - - if os.path.isdir(name): - # find all python files in directory - list = [] - os.walk(name, _visit_pyfiles, list) - return list - elif os.path.exists(name): - # a single file - return [name] - - return [] - - -class TokenEater: - def __init__(self, options): - self.__options = options - self.__messages = {} - self.__state = self.__waiting - self.__data = [] - self.__lineno = -1 - self.__freshmodule = 1 - self.__curfile = None - - def __call__(self, ttype, tstring, stup, etup, line): - # dispatch -## import token -## print >> sys.stderr, 'ttype:', token.tok_name[ttype], \ -## 'tstring:', tstring - self.__state(ttype, tstring, stup[0]) - - def __waiting(self, ttype, tstring, lineno): - opts = self.__options - # Do docstring extractions, if enabled - if opts.docstrings and not opts.nodocstrings.get(self.__curfile): - # module docstring? - if self.__freshmodule: - if ttype == tokenize.STRING: - self.__addentry(safe_eval(tstring), lineno, isdocstring=1) - self.__freshmodule = 0 - elif ttype not in (tokenize.COMMENT, tokenize.NL): - self.__freshmodule = 0 - return - # class docstring? - if ttype == tokenize.NAME and tstring in ('class', 'def'): - self.__state = self.__suiteseen - return - if ttype == tokenize.NAME and tstring in opts.keywords: - self.__state = self.__keywordseen - - def __suiteseen(self, ttype, tstring, lineno): - # ignore anything until we see the colon - if ttype == tokenize.OP and tstring == ':': - self.__state = self.__suitedocstring - - def __suitedocstring(self, ttype, tstring, lineno): - # ignore any intervening noise - if ttype == tokenize.STRING: - self.__addentry(safe_eval(tstring), lineno, isdocstring=1) - self.__state = self.__waiting - elif ttype not in (tokenize.NEWLINE, tokenize.INDENT, - tokenize.COMMENT): - # there was no class docstring - self.__state = self.__waiting - - def __keywordseen(self, ttype, tstring, lineno): - if ttype == tokenize.OP and tstring == '(': - self.__data = [] - self.__lineno = lineno - self.__state = self.__openseen - else: - self.__state = self.__waiting - - def __openseen(self, ttype, tstring, lineno): - if ttype == tokenize.OP and tstring == ')': - # We've seen the last of the translatable strings. Record the - # line number of the first line of the strings and update the list - # of messages seen. Reset state for the next batch. If there - # were no strings inside _(), then just ignore this entry. - if self.__data: - self.__addentry(EMPTYSTRING.join(self.__data)) - self.__state = self.__waiting - elif ttype == tokenize.STRING: - self.__data.append(safe_eval(tstring)) - elif ttype not in [tokenize.COMMENT, token.INDENT, token.DEDENT, - token.NEWLINE, tokenize.NL]: - # warn if we see anything else than STRING or whitespace - print('*** %(file)s:%(lineno)s: Seen unexpected token "%(token)s"' % { - 'token': tstring, - 'file': self.__curfile, - 'lineno': self.__lineno - }, file=sys.stderr) - self.__state = self.__waiting - - def __addentry(self, msg, lineno=None, isdocstring=0): - if lineno is None: - lineno = self.__lineno - if not msg in self.__options.toexclude: - entry = (self.__curfile, lineno) - self.__messages.setdefault(msg, {})[entry] = isdocstring - - def set_filename(self, filename): - self.__curfile = filename - self.__freshmodule = 1 - - def write(self, fp): - options = self.__options - timestamp = time.strftime('%Y-%m-%d %H:%M+%Z') - # The time stamp in the header doesn't have the same format as that - # generated by xgettext... - print(pot_header, file=fp) - # Sort the entries. First sort each particular entry's keys, then - # sort all the entries by their first item. - reverse = {} - for k, v in self.__messages.items(): - keys = sorted(v.keys()) - reverse.setdefault(tuple(keys), []).append((k, v)) - rkeys = sorted(reverse.keys()) - for rkey in rkeys: - rentries = reverse[rkey] - rentries.sort() - for k, v in rentries: - # If the entry was gleaned out of a docstring, then add a - # comment stating so. This is to aid translators who may wish - # to skip translating some unimportant docstrings. - isdocstring = any(v.values()) - # k is the message string, v is a dictionary-set of (filename, - # lineno) tuples. We want to sort the entries in v first by - # file name and then by line number. - v = sorted(v.keys()) - if not options.writelocations: - pass - # location comments are different b/w Solaris and GNU: - elif options.locationstyle == options.SOLARIS: - for filename, lineno in v: - d = {'filename': filename, 'lineno': lineno} - print('# File: %(filename)s, line: %(lineno)d' % d, file=fp) - elif options.locationstyle == options.GNU: - # fit as many locations on one line, as long as the - # resulting line length doesn't exceeds 'options.width' - locline = '#:' - for filename, lineno in v: - d = {'filename': filename, 'lineno': lineno} - s = ' %(filename)s:%(lineno)d' % d - if len(locline) + len(s) <= options.width: - locline = locline + s - else: - print(locline, file=fp) - locline = "#:" + s - if len(locline) > 2: - print(locline, file=fp) - if isdocstring: - print('#, docstring', file=fp) - print('msgid', normalize(k), file=fp) - print('msgstr ""\n', file=fp) - - - -def main(source_files, outpath, keywords=None): - global default_keywords - # for holding option values - class Options: - # constants - GNU = 1 - SOLARIS = 2 - # defaults - extractall = 0 # FIXME: currently this option has no effect at all. - escape = 0 - keywords = [] - outfile = 'messages.pot' - writelocations = 1 - locationstyle = GNU - verbose = 0 - width = 78 - excludefilename = '' - docstrings = 0 - nodocstrings = {} - - options = Options() - locations = {'gnu' : options.GNU, - 'solaris' : options.SOLARIS, - } - - options.outfile = outpath - if keywords: - options.keywords = keywords - - # calculate escapes - make_escapes(options.escape) - - # calculate all keywords - options.keywords.extend(default_keywords) - - # initialize list of strings to exclude - if options.excludefilename: - try: - fp = open(options.excludefilename, encoding='utf-8') - options.toexclude = fp.readlines() - fp.close() - except IOError: - print("Can't read --exclude-file: %s" % options.excludefilename, file=sys.stderr) - sys.exit(1) - else: - options.toexclude = [] - - # slurp through all the files - eater = TokenEater(options) - for filename in source_files: - if options.verbose: - print('Working on %s' % filename) - fp = open(filename, encoding='utf-8') - closep = 1 - try: - eater.set_filename(filename) - try: - tokens = tokenize.generate_tokens(fp.readline) - for _token in tokens: - eater(*_token) - except tokenize.TokenError as e: - print('%s: %s, line %d, column %d' % ( - e.args[0], filename, e.args[1][0], e.args[1][1]), - file=sys.stderr) - finally: - if closep: - fp.close() - - fp = open(options.outfile, 'w', encoding='utf-8') - closep = 1 - try: - eater.write(fp) - finally: - if closep: - fp.close() diff --git a/hscommon/sphinxgen.py b/hscommon/sphinxgen.py deleted file mode 100644 index 9992c2fe..00000000 --- a/hscommon/sphinxgen.py +++ /dev/null @@ -1,73 +0,0 @@ -# Created By: Virgil Dupras -# Created On: 2011-01-12 -# Copyright 2015 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at -# http://www.gnu.org/licenses/gpl-3.0.html - -import os.path as op -import re - -from pkg_resources import load_entry_point - -from .build import read_changelog_file, filereplace - -CHANGELOG_FORMAT = """ -{version} ({date}) ----------------------- - -{description} -""" - -def tixgen(tixurl): - """This is a filter *generator*. tixurl is a url pattern for the tix with a {0} placeholder - for the tix # - """ - urlpattern = tixurl.format('\\1') # will be replaced buy the content of the first group in re - R = re.compile(r'#(\d+)') - repl = '`#\\1 <{}>`__'.format(urlpattern) - return lambda text: R.sub(repl, text) - -def gen(basepath, destpath, changelogpath, tixurl, confrepl=None, confpath=None, changelogtmpl=None): - """Generate sphinx docs with all bells and whistles. - - basepath: The base sphinx source path. - destpath: The final path of html files - changelogpath: The path to the changelog file to insert in changelog.rst. - tixurl: The URL (with one formattable argument for the tix number) to the ticket system. - confrepl: Dictionary containing replacements that have to be made in conf.py. {name: replacement} - """ - if confrepl is None: - confrepl = {} - if confpath is None: - confpath = op.join(basepath, 'conf.tmpl') - if changelogtmpl is None: - changelogtmpl = op.join(basepath, 'changelog.tmpl') - changelog = read_changelog_file(changelogpath) - tix = tixgen(tixurl) - rendered_logs = [] - for log in changelog: - description = tix(log['description']) - # The format of the changelog descriptions is in markdown, but since we only use bulled list - # and links, it's not worth depending on the markdown package. A simple regexp suffice. - description = re.sub(r'\[(.*?)\]\((.*?)\)', '`\\1 <\\2>`__', description) - rendered = CHANGELOG_FORMAT.format(version=log['version'], date=log['date_str'], - description=description) - rendered_logs.append(rendered) - confrepl['version'] = changelog[0]['version'] - changelog_out = op.join(basepath, 'changelog.rst') - filereplace(changelogtmpl, changelog_out, changelog='\n'.join(rendered_logs)) - conf_out = op.join(basepath, 'conf.py') - filereplace(confpath, conf_out, **confrepl) - # We used to call sphinx-build with print_and_do(), but the problem was that the virtualenv - # of the calling python wasn't correctly considered and caused problems with documentation - # relying on autodoc (which tries to import the module to auto-document, but fail because of - # missing dependencies which are in the virtualenv). Here, we do exactly what is done when - # calling the command from bash. - cmd = load_entry_point('Sphinx', 'console_scripts', 'sphinx-build') - try: - cmd(['sphinx-build', basepath, destpath]) - except SystemExit: - print("Sphinx called sys.exit(), but we're cancelling it because we don't actually want to exit") - diff --git a/hscommon/sqlite.py b/hscommon/sqlite.py deleted file mode 100644 index 30af4b7c..00000000 --- a/hscommon/sqlite.py +++ /dev/null @@ -1,141 +0,0 @@ -# Created By: Virgil Dupras -# Created On: 2007/05/19 -# Copyright 2015 Hardcoded Software (http://www.hardcoded.net) - -# This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at -# http://www.gnu.org/licenses/gpl-3.0.html - -import sys -import os -import os.path as op -import threading -from queue import Queue -import time -import sqlite3 as sqlite - -STOP = object() -COMMIT = object() -ROLLBACK = object() - -class FakeCursor(list): - # It's not possible to use sqlite cursors on another thread than the connection. Thus, - # we can't directly return the cursor. We have to fatch all results, and support its interface. - def fetchall(self): - return self - - def fetchone(self): - try: - return self.pop(0) - except IndexError: - return None - - -class _ActualThread(threading.Thread): - ''' We can't use this class directly because thread object are not automatically freed when - nothing refers to it, making it hang the application if not explicitely closed. - ''' - def __init__(self, dbname, autocommit): - threading.Thread.__init__(self) - self._queries = Queue() - self._results = Queue() - self._dbname = dbname - self._autocommit = autocommit - self._waiting_list = set() - self._lock = threading.Lock() - self._run = True - self.lastrowid = -1 - self.setDaemon(True) - self.start() - - def _query(self, query): - with self._lock: - wait_token = object() - self._waiting_list.add(wait_token) - self._queries.put(query) - self._waiting_list.remove(wait_token) - result = self._results.get() - return result - - def close(self): - if not self._run: - return - self._query(STOP) - - def commit(self): - if not self._run: - return None # Connection closed - self._query(COMMIT) - - def execute(self, sql, values=()): - if not self._run: - return None # Connection closed - result = self._query((sql, values)) - if isinstance(result, Exception): - raise result - return result - - def rollback(self): - if not self._run: - return None # Connection closed - self._query(ROLLBACK) - - def run(self): - # The whole chdir thing is because sqlite doesn't handle directory names with non-asci char in the AT ALL. - oldpath = os.getcwd() - dbdir, dbname = op.split(self._dbname) - if dbdir: - os.chdir(dbdir) - if self._autocommit: - con = sqlite.connect(dbname, isolation_level=None) - else: - con = sqlite.connect(dbname) - os.chdir(oldpath) - while self._run or self._waiting_list: - query = self._queries.get() - result = None - if query is STOP: - self._run = False - elif query is COMMIT: - con.commit() - elif query is ROLLBACK: - con.rollback() - else: - sql, values = query - try: - cur = con.execute(sql, values) - self.lastrowid = cur.lastrowid - result = FakeCursor(cur.fetchall()) - result.lastrowid = cur.lastrowid - except Exception as e: - result = e - self._results.put(result) - con.close() - - -class ThreadedConn: - """``sqlite`` connections can't be used across threads. ``TheadedConn`` opens a sqlite - connection in its own thread and sends it queries through a queue, making it suitable in - multi-threaded environment. - """ - def __init__(self, dbname, autocommit): - self._t = _ActualThread(dbname, autocommit) - self.lastrowid = -1 - - def __del__(self): - self.close() - - def close(self): - self._t.close() - - def commit(self): - self._t.commit() - - def execute(self, sql, values=()): - result = self._t.execute(sql, values) - self.lastrowid = self._t.lastrowid - return result - - def rollback(self): - self._t.rollback() - diff --git a/hscommon/tests/__init__.py b/hscommon/tests/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/hscommon/tests/conflict_test.py b/hscommon/tests/conflict_test.py deleted file mode 100644 index 2b5d0f1a..00000000 --- a/hscommon/tests/conflict_test.py +++ /dev/null @@ -1,104 +0,0 @@ -# Created By: Virgil Dupras -# Created On: 2008-01-08 -# Copyright 2015 Hardcoded Software (http://www.hardcoded.net) - -# This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at -# http://www.gnu.org/licenses/gpl-3.0.html - -from ..conflict import * -from ..path import Path -from ..testutil import eq_ - -class TestCase_GetConflictedName: - def test_simple(self): - name = get_conflicted_name(['bar'], 'bar') - eq_('[000] bar', name) - name = get_conflicted_name(['bar', '[000] bar'], 'bar') - eq_('[001] bar', name) - - def test_no_conflict(self): - name = get_conflicted_name(['bar'], 'foobar') - eq_('foobar', name) - - def test_fourth_digit(self): - # This test is long because every time we have to add a conflicted name, - # a test must be made for every other conflicted name existing... - # Anyway, this has very few chances to happen. - names = ['bar'] + ['[%03d] bar' % i for i in range(1000)] - name = get_conflicted_name(names, 'bar') - eq_('[1000] bar', name) - - def test_auto_unconflict(self): - # Automatically unconflict the name if it's already conflicted. - name = get_conflicted_name([], '[000] foobar') - eq_('foobar', name) - name = get_conflicted_name(['bar'], '[001] bar') - eq_('[000] bar', name) - - -class TestCase_GetUnconflictedName: - def test_main(self): - eq_('foobar',get_unconflicted_name('[000] foobar')) - eq_('foobar',get_unconflicted_name('[9999] foobar')) - eq_('[000]foobar',get_unconflicted_name('[000]foobar')) - eq_('[000a] foobar',get_unconflicted_name('[000a] foobar')) - eq_('foobar',get_unconflicted_name('foobar')) - eq_('foo [000] bar',get_unconflicted_name('foo [000] bar')) - - -class TestCase_IsConflicted: - def test_main(self): - assert is_conflicted('[000] foobar') - assert is_conflicted('[9999] foobar') - assert not is_conflicted('[000]foobar') - assert not is_conflicted('[000a] foobar') - assert not is_conflicted('foobar') - assert not is_conflicted('foo [000] bar') - - -class TestCase_move_copy: - def pytest_funcarg__do_setup(self, request): - tmpdir = request.getfuncargvalue('tmpdir') - self.path = Path(str(tmpdir)) - self.path['foo'].open('w').close() - self.path['bar'].open('w').close() - self.path['dir'].mkdir() - - def test_move_no_conflict(self, do_setup): - smart_move(self.path + 'foo', self.path + 'baz') - assert self.path['baz'].exists() - assert not self.path['foo'].exists() - - def test_copy_no_conflict(self, do_setup): # No need to duplicate the rest of the tests... Let's just test on move - smart_copy(self.path + 'foo', self.path + 'baz') - assert self.path['baz'].exists() - assert self.path['foo'].exists() - - def test_move_no_conflict_dest_is_dir(self, do_setup): - smart_move(self.path + 'foo', self.path + 'dir') - assert self.path['dir']['foo'].exists() - assert not self.path['foo'].exists() - - def test_move_conflict(self, do_setup): - smart_move(self.path + 'foo', self.path + 'bar') - assert self.path['[000] bar'].exists() - assert not self.path['foo'].exists() - - def test_move_conflict_dest_is_dir(self, do_setup): - smart_move(self.path['foo'], self.path['dir']) - smart_move(self.path['bar'], self.path['foo']) - smart_move(self.path['foo'], self.path['dir']) - assert self.path['dir']['foo'].exists() - assert self.path['dir']['[000] foo'].exists() - assert not self.path['foo'].exists() - assert not self.path['bar'].exists() - - def test_copy_folder(self, tmpdir): - # smart_copy also works on folders - path = Path(str(tmpdir)) - path['foo'].mkdir() - path['bar'].mkdir() - smart_copy(path['foo'], path['bar']) # no crash - assert path['[000] bar'].exists() - diff --git a/hscommon/tests/currency_test.py b/hscommon/tests/currency_test.py deleted file mode 100644 index 30bbfda9..00000000 --- a/hscommon/tests/currency_test.py +++ /dev/null @@ -1,210 +0,0 @@ -# Created By: Virgil Dupras -# Created On: 2008-04-20 -# Copyright 2015 Hardcoded Software (http://www.hardcoded.net) - -# This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at -# http://www.gnu.org/licenses/gpl-3.0.html - -from datetime import date -import sqlite3 as sqlite - -from ..testutil import eq_, assert_almost_equal -from ..currency import Currency, RatesDB, CAD, EUR, USD - -PLN = Currency(code='PLN') - -def setup_module(module): - global FOO - global BAR - FOO = Currency.register('FOO', 'Currency with start date', start_date=date(2009, 1, 12), start_rate=2) - BAR = Currency.register('BAR', 'Currency with stop date', stop_date=date(2010, 1, 12), latest_rate=2) - -def teardown_module(module): - # We must unset our test currencies or else we might mess up with other tests. - from .. import currency - import imp - imp.reload(currency) - -def teardown_function(function): - Currency.set_rates_db(None) - -def test_currency_creation(): - # Different ways to create a currency. - eq_(Currency('CAD'), CAD) - eq_(Currency(name='Canadian dollar'), CAD) - -def test_currency_copy(): - # Currencies can be copied. - import copy - eq_(copy.copy(CAD), CAD) - eq_(copy.deepcopy(CAD), CAD) - -def test_get_rate_on_empty_db(): - # When there is no data available, use the start_rate. - eq_(CAD.value_in(USD, date(2008, 4, 20)), 1 / USD.latest_rate) - -def test_physical_rates_db_remember_rates(tmpdir): - # When a rates db uses a real file, rates are remembered - dbpath = str(tmpdir.join('foo.db')) - db = RatesDB(dbpath) - db.set_CAD_value(date(2008, 4, 20), 'USD', 1/0.996115) - db = RatesDB(dbpath) - assert_almost_equal(db.get_rate(date(2008, 4, 20), 'CAD', 'USD'), 0.996115) - -def test_db_with_connection(): - # the supplied connection is used by the rates db. - con = sqlite.connect(':memory:') - db = RatesDB(con) - try: - con.execute("select * from rates where 1=2") - except sqlite.OperationalError: # new db - raise AssertionError() - -def test_corrupt_db(tmpdir): - dbpath = str(tmpdir.join('foo.db')) - fh = open(dbpath, 'w') - fh.write('corrupted') - fh.close() - db = RatesDB(dbpath) # no crash. deletes the old file and start a new db - db.set_CAD_value(date(2008, 4, 20), 'USD', 42) - db = RatesDB(dbpath) - eq_(db.get_rate(date(2008, 4, 20), 'USD', 'CAD'), 42) - -#--- Daily rate -def setup_daily_rate(): - USD.set_CAD_value(1/0.996115, date(2008, 4, 20)) - -def test_get_rate_with_daily_rate(): - # Getting the rate exactly as set_rate happened returns the same rate. - setup_daily_rate() - assert_almost_equal(CAD.value_in(USD, date(2008, 4, 20)), 0.996115) - -def test_get_rate_different_currency(): - # Use fallback rates when necessary. - setup_daily_rate() - eq_(CAD.value_in(EUR, date(2008, 4, 20)), 1 / EUR.latest_rate) - eq_(EUR.value_in(USD, date(2008, 4, 20)), EUR.latest_rate * 0.996115) - -def test_get_rate_reverse(): - # It's possible to get the reverse value of a rate using the same data. - setup_daily_rate() - assert_almost_equal(USD.value_in(CAD, date(2008, 4, 20)), 1 / 0.996115) - -def test_set_rate_twice(): - # When setting a rate for an index that already exists, the old rate is replaced by the new. - setup_daily_rate() - USD.set_CAD_value(1/42, date(2008, 4, 20)) - assert_almost_equal(CAD.value_in(USD, date(2008, 4, 20)), 42) - -def test_set_rate_after_get(): - # When setting a rate after a get of the same rate, the rate cache is correctly updated. - setup_daily_rate() - CAD.value_in(USD, date(2008, 4, 20)) # value will be cached - USD.set_CAD_value(1/42, date(2008, 4, 20)) - assert_almost_equal(CAD.value_in(USD, date(2008, 4, 20)), 42) - -def test_set_rate_after_get_the_day_after(): - # When setting a rate, the cache for the whole currency is reset, or else we get old fallback - # values for dates where the currency server returned no value. - setup_daily_rate() - CAD.value_in(USD, date(2008, 4, 21)) # value will be cached - USD.set_CAD_value(1/42, date(2008, 4, 20)) - assert_almost_equal(CAD.value_in(USD, date(2008, 4, 21)), 42) - -#--- Two daily rates -def setup_two_daily_rate(): - # Don't change the set order, it's important for the tests - USD.set_CAD_value(1/0.997115, date(2008, 4, 25)) - USD.set_CAD_value(1/0.996115, date(2008, 4, 20)) - -def test_date_range_range(): - # USD.rates_date_range() returns the USD's limits. - setup_two_daily_rate() - eq_(USD.rates_date_range(), (date(2008, 4, 20), date(2008, 4, 25))) - -def test_date_range_for_unfetched_currency(): - # If the curency is not in the DB, return None. - setup_two_daily_rate() - assert PLN.rates_date_range() is None - -def test_seek_rate_middle(): - # A rate request with seek in the middle will return the lowest date. - setup_two_daily_rate() - eq_(USD.value_in(CAD, date(2008, 4, 24)), 1/0.996115) - -def test_seek_rate_after(): - # Make sure that the *nearest* lowest rate is returned. Because the 25th have been set - # before the 20th, an order by clause is required in the seek SQL to make this test pass. - setup_two_daily_rate() - eq_(USD.value_in(CAD, date(2008, 4, 26)), 1/0.997115) - -def test_seek_rate_before(): - # If there are no rate in the past, seek for a rate in the future. - setup_two_daily_rate() - eq_(USD.value_in(CAD, date(2008, 4, 19)), 1/0.996115) - -#--- Rates of multiple currencies -def setup_rates_of_multiple_currencies(): - USD.set_CAD_value(1/0.996115, date(2008, 4, 20)) - EUR.set_CAD_value(1/0.633141, date(2008, 4, 20)) - -def test_get_rate_multiple_currencies(): - # Don't mix currency rates up. - setup_rates_of_multiple_currencies() - assert_almost_equal(CAD.value_in(USD, date(2008, 4, 20)), 0.996115) - assert_almost_equal(CAD.value_in(EUR, date(2008, 4, 20)), 0.633141) - -def test_get_rate_with_pivotal(): - # It's possible to get a rate by using 2 records. - # if 1 CAD = 0.996115 USD and 1 CAD = 0.633141 then 0.996115 USD = 0.633141 then 1 USD = 0.633141 / 0.996115 EUR - setup_rates_of_multiple_currencies() - assert_almost_equal(USD.value_in(EUR, date(2008, 4, 20)), 0.633141 / 0.996115) - -def test_get_rate_doesnt_exist(): - # Don't crash when trying to do pivotal calculation with non-existing currencies. - setup_rates_of_multiple_currencies() - eq_(USD.value_in(PLN, date(2008, 4, 20)), 1 / 0.996115 / PLN.latest_rate) - -#--- Problems after connection -def get_problematic_db(): - class MockConnection(sqlite.Connection): # can't mock sqlite3.Connection's attribute, so we subclass it - mocking = False - def execute(self, *args, **kwargs): - if self.mocking: - raise sqlite.OperationalError() - else: - return sqlite.Connection.execute(self, *args, **kwargs) - - con = MockConnection(':memory:') - db = RatesDB(con) - con.mocking = True - return db - -def test_date_range_with_problematic_db(): - db = get_problematic_db() - db.date_range('USD') # no crash - -def test_get_rate_with_problematic_db(): - db = get_problematic_db() - db.get_rate(date(2008, 4, 20), 'USD', 'CAD') # no crash - -def test_set_rate_with_problematic_db(): - db = get_problematic_db() - db.set_CAD_value(date(2008, 4, 20), 'USD', 42) # no crash - -#--- DB that doesn't allow get_rate calls -def setup_db_raising_error_on_getrate(): - db = RatesDB() - def mock_get_rate(*args, **kwargs): - raise AssertionError() - db.get_rate = mock_get_rate - Currency.set_rates_db(db) - -def test_currency_with_start_date(): - setup_db_raising_error_on_getrate() - eq_(FOO.value_in(CAD, date(2009, 1, 11)), 2) - -def test_currency_with_stop_date(): - setup_db_raising_error_on_getrate() - eq_(BAR.value_in(CAD, date(2010, 1, 13)), 2) diff --git a/hscommon/tests/notify_test.py b/hscommon/tests/notify_test.py deleted file mode 100644 index efc1b59e..00000000 --- a/hscommon/tests/notify_test.py +++ /dev/null @@ -1,140 +0,0 @@ -# Copyright 2015 Hardcoded Software (http://www.hardcoded.net) - -# This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at -# http://www.gnu.org/licenses/gpl-3.0.html - -from ..testutil import eq_ -from ..notify import Broadcaster, Listener, Repeater - -class HelloListener(Listener): - def __init__(self, broadcaster): - Listener.__init__(self, broadcaster) - self.hello_count = 0 - - def hello(self): - self.hello_count += 1 - -class HelloRepeater(Repeater): - def __init__(self, broadcaster): - Repeater.__init__(self, broadcaster) - self.hello_count = 0 - - def hello(self): - self.hello_count += 1 - -def create_pair(): - b = Broadcaster() - l = HelloListener(b) - return b, l - -def test_disconnect_during_notification(): - # When a listener disconnects another listener the other listener will not receive a - # notification. - # This whole complication scheme below is because the order of the notification is not - # guaranteed. We could disconnect everything from self.broadcaster.listeners, but this - # member is supposed to be private. Hence, the '.other' scheme - class Disconnecter(Listener): - def __init__(self, broadcaster): - Listener.__init__(self, broadcaster) - self.hello_count = 0 - - def hello(self): - self.hello_count += 1 - self.other.disconnect() - - broadcaster = Broadcaster() - first = Disconnecter(broadcaster) - second = Disconnecter(broadcaster) - first.other, second.other = second, first - first.connect() - second.connect() - broadcaster.notify('hello') - # only one of them was notified - eq_(first.hello_count + second.hello_count, 1) - -def test_disconnect(): - # After a disconnect, the listener doesn't hear anything. - b, l = create_pair() - l.connect() - l.disconnect() - b.notify('hello') - eq_(l.hello_count, 0) - -def test_disconnect_when_not_connected(): - # When disconnecting an already disconnected listener, nothing happens. - b, l = create_pair() - l.disconnect() - -def test_not_connected_on_init(): - # A listener is not initialized connected. - b, l = create_pair() - b.notify('hello') - eq_(l.hello_count, 0) - -def test_notify(): - # The listener listens to the broadcaster. - b, l = create_pair() - l.connect() - b.notify('hello') - eq_(l.hello_count, 1) - -def test_reconnect(): - # It's possible to reconnect a listener after disconnection. - b, l = create_pair() - l.connect() - l.disconnect() - l.connect() - b.notify('hello') - eq_(l.hello_count, 1) - -def test_repeater(): - b = Broadcaster() - r = HelloRepeater(b) - l = HelloListener(r) - r.connect() - l.connect() - b.notify('hello') - eq_(r.hello_count, 1) - eq_(l.hello_count, 1) - -def test_repeater_with_repeated_notifications(): - # If REPEATED_NOTIFICATIONS is not empty, only notifs in this set are repeated (but they're - # still dispatched locally). - class MyRepeater(HelloRepeater): - REPEATED_NOTIFICATIONS = set(['hello']) - def __init__(self, broadcaster): - HelloRepeater.__init__(self, broadcaster) - self.foo_count = 0 - def foo(self): - self.foo_count += 1 - - b = Broadcaster() - r = MyRepeater(b) - l = HelloListener(r) - r.connect() - l.connect() - b.notify('hello') - b.notify('foo') # if the repeater repeated this notif, we'd get a crash on HelloListener - eq_(r.hello_count, 1) - eq_(l.hello_count, 1) - eq_(r.foo_count, 1) - -def test_repeater_doesnt_try_to_dispatch_to_self_if_it_cant(): - # if a repeater doesn't handle a particular message, it doesn't crash and simply repeats it. - b = Broadcaster() - r = Repeater(b) # doesnt handle hello - l = HelloListener(r) - r.connect() - l.connect() - b.notify('hello') # no crash - eq_(l.hello_count, 1) - -def test_bind_messages(): - b, l = create_pair() - l.bind_messages({'foo', 'bar'}, l.hello) - l.connect() - b.notify('foo') - b.notify('bar') - b.notify('hello') # Normal dispatching still work - eq_(l.hello_count, 3) diff --git a/hscommon/tests/path_test.py b/hscommon/tests/path_test.py deleted file mode 100644 index a1ee2805..00000000 --- a/hscommon/tests/path_test.py +++ /dev/null @@ -1,256 +0,0 @@ -# Created By: Virgil Dupras -# Created On: 2006/02/21 -# Copyright 2015 Hardcoded Software (http://www.hardcoded.net) - -# This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at -# http://www.gnu.org/licenses/gpl-3.0.html - -import sys -import os - -from pytest import raises, mark - -from ..path import Path, pathify -from ..testutil import eq_ - -def pytest_funcarg__force_ossep(request): - monkeypatch = request.getfuncargvalue('monkeypatch') - monkeypatch.setattr(os, 'sep', '/') - -def test_empty(force_ossep): - path = Path('') - eq_('',str(path)) - eq_(0,len(path)) - path = Path(()) - eq_('',str(path)) - eq_(0,len(path)) - -def test_single(force_ossep): - path = Path('foobar') - eq_('foobar',path) - eq_(1,len(path)) - -def test_multiple(force_ossep): - path = Path('foo/bar') - eq_('foo/bar',path) - eq_(2,len(path)) - -def test_init_with_tuple_and_list(force_ossep): - path = Path(('foo','bar')) - eq_('foo/bar',path) - path = Path(['foo','bar']) - eq_('foo/bar',path) - -def test_init_with_invalid_value(force_ossep): - try: - path = Path(42) - assert False - except TypeError: - pass - -def test_access(force_ossep): - path = Path('foo/bar/bleh') - eq_('foo',path[0]) - eq_('foo',path[-3]) - eq_('bar',path[1]) - eq_('bar',path[-2]) - eq_('bleh',path[2]) - eq_('bleh',path[-1]) - -def test_slicing(force_ossep): - path = Path('foo/bar/bleh') - subpath = path[:2] - eq_('foo/bar',subpath) - assert isinstance(subpath,Path) - -def test_parent(force_ossep): - path = Path('foo/bar/bleh') - subpath = path.parent() - eq_('foo/bar', subpath) - assert isinstance(subpath, Path) - -def test_filename(force_ossep): - path = Path('foo/bar/bleh.ext') - eq_(path.name, 'bleh.ext') - -def test_deal_with_empty_components(force_ossep): - """Keep ONLY a leading space, which means we want a leading slash. - """ - eq_('foo//bar',str(Path(('foo','','bar')))) - eq_('/foo/bar',str(Path(('','foo','bar')))) - eq_('foo/bar',str(Path('foo/bar/'))) - -def test_old_compare_paths(force_ossep): - eq_(Path('foobar'),Path('foobar')) - eq_(Path('foobar/'),Path('foobar\\','\\')) - eq_(Path('/foobar/'),Path('\\foobar\\','\\')) - eq_(Path('/foo/bar'),Path('\\foo\\bar','\\')) - eq_(Path('/foo/bar'),Path('\\foo\\bar\\','\\')) - assert Path('/foo/bar') != Path('\\foo\\foo','\\') - #We also have to test __ne__ - assert not (Path('foobar') != Path('foobar')) - assert Path('/a/b/c.x') != Path('/a/b/c.y') - -def test_old_split_path(force_ossep): - eq_(Path('foobar'),('foobar',)) - eq_(Path('foo/bar'),('foo','bar')) - eq_(Path('/foo/bar/'),('','foo','bar')) - eq_(Path('\\foo\\bar','\\'),('','foo','bar')) - -def test_representation(force_ossep): - eq_("('foo', 'bar')",repr(Path(('foo','bar')))) - -def test_add(force_ossep): - eq_('foo/bar/bar/foo',Path(('foo','bar')) + Path('bar/foo')) - eq_('foo/bar/bar/foo',Path('foo/bar') + 'bar/foo') - eq_('foo/bar/bar/foo',Path('foo/bar') + ('bar','foo')) - eq_('foo/bar/bar/foo',('foo','bar') + Path('bar/foo')) - eq_('foo/bar/bar/foo','foo/bar' + Path('bar/foo')) - #Invalid concatenation - try: - Path(('foo','bar')) + 1 - assert False - except TypeError: - pass - -def test_path_slice(force_ossep): - foo = Path('foo') - bar = Path('bar') - foobar = Path('foo/bar') - eq_('bar',foobar[foo:]) - eq_('foo',foobar[:bar]) - eq_('foo/bar',foobar[bar:]) - eq_('foo/bar',foobar[:foo]) - eq_((),foobar[foobar:]) - eq_((),foobar[:foobar]) - abcd = Path('a/b/c/d') - a = Path('a') - b = Path('b') - c = Path('c') - d = Path('d') - z = Path('z') - eq_('b/c',abcd[a:d]) - eq_('b/c/d',abcd[a:d+z]) - eq_('b/c',abcd[a:z+d]) - eq_('a/b/c/d',abcd[:z]) - -def test_add_with_root_path(force_ossep): - """if I perform /a/b/c + /d/e/f, I want /a/b/c/d/e/f, not /a/b/c//d/e/f - """ - eq_('/foo/bar',str(Path('/foo') + Path('/bar'))) - -def test_create_with_tuple_that_have_slash_inside(force_ossep, monkeypatch): - eq_(('','foo','bar'), Path(('/foo','bar'))) - monkeypatch.setattr(os, 'sep', '\\') - eq_(('','foo','bar'), Path(('\\foo','bar'))) - -def test_auto_decode_os_sep(force_ossep, monkeypatch): - """Path should decode any either / or os.sep, but always encode in os.sep. - """ - eq_(('foo\\bar','bleh'),Path('foo\\bar/bleh')) - monkeypatch.setattr(os, 'sep', '\\') - eq_(('foo','bar/bleh'),Path('foo\\bar/bleh')) - path = Path('foo/bar') - eq_(('foo','bar'),path) - eq_('foo\\bar',str(path)) - -def test_contains(force_ossep): - p = Path(('foo','bar')) - assert Path(('foo','bar','bleh')) in p - assert Path(('foo','bar')) in p - assert 'foo' in p - assert 'bleh' not in p - assert Path('foo') not in p - -def test_is_parent_of(force_ossep): - assert Path(('foo','bar')).is_parent_of(Path(('foo','bar','bleh'))) - assert not Path(('foo','bar')).is_parent_of(Path(('foo','baz'))) - assert not Path(('foo','bar')).is_parent_of(Path(('foo','bar'))) - -def test_windows_drive_letter(force_ossep): - p = Path(('c:',)) - eq_('c:\\',str(p)) - -def test_root_path(force_ossep): - p = Path('/') - eq_('/',str(p)) - -def test_str_encodes_unicode_to_getfilesystemencoding(force_ossep): - p = Path(('foo','bar\u00e9')) - eq_('foo/bar\u00e9'.encode(sys.getfilesystemencoding()), p.tobytes()) - -def test_unicode(force_ossep): - p = Path(('foo','bar\u00e9')) - eq_('foo/bar\u00e9',str(p)) - -def test_str_repr_of_mix_between_non_ascii_str_and_unicode(force_ossep): - u = 'foo\u00e9' - encoded = u.encode(sys.getfilesystemencoding()) - p = Path((encoded,'bar')) - print(repr(tuple(p))) - eq_('foo\u00e9/bar'.encode(sys.getfilesystemencoding()), p.tobytes()) - -def test_Path_of_a_Path_returns_self(force_ossep): - #if Path() is called with a path as value, just return value. - p = Path('foo/bar') - assert Path(p) is p - -def test_getitem_str(force_ossep): - # path['something'] returns the child path corresponding to the name - p = Path('/foo/bar') - eq_(p['baz'], Path('/foo/bar/baz')) - -def test_getitem_path(force_ossep): - # path[Path('something')] returns the child path corresponding to the name (or subpath) - p = Path('/foo/bar') - eq_(p[Path('baz/bleh')], Path('/foo/bar/baz/bleh')) - -@mark.xfail(reason="pytest's capture mechanism is flaky, I have to investigate") -def test_log_unicode_errors(force_ossep, monkeypatch, capsys): - # When an there's a UnicodeDecodeError on path creation, log it so it can be possible - # to debug the cause of it. - monkeypatch.setattr(sys, 'getfilesystemencoding', lambda: 'ascii') - with raises(UnicodeDecodeError): - Path(['', b'foo\xe9']) - out, err = capsys.readouterr() - assert repr(b'foo\xe9') in err - -def test_has_drive_letter(monkeypatch): - monkeypatch.setattr(os, 'sep', '\\') - p = Path('foo\\bar') - assert not p.has_drive_letter() - p = Path('C:\\') - assert p.has_drive_letter() - p = Path('z:\\foo') - assert p.has_drive_letter() - -def test_remove_drive_letter(monkeypatch): - monkeypatch.setattr(os, 'sep', '\\') - p = Path('foo\\bar') - eq_(p.remove_drive_letter(), Path('foo\\bar')) - p = Path('C:\\') - eq_(p.remove_drive_letter(), Path('')) - p = Path('z:\\foo') - eq_(p.remove_drive_letter(), Path('foo')) - -def test_pathify(): - @pathify - def foo(a: Path, b, c:Path): - return a, b, c - - a, b, c = foo('foo', 0, c=Path('bar')) - assert isinstance(a, Path) - assert a == Path('foo') - assert b == 0 - assert isinstance(c, Path) - assert c == Path('bar') - -def test_pathify_preserve_none(): - # @pathify preserves None value and doesn't try to return a Path - @pathify - def foo(a: Path): - return a - - a = foo(None) - assert a is None diff --git a/hscommon/tests/selectable_list_test.py b/hscommon/tests/selectable_list_test.py deleted file mode 100644 index 10b36ef0..00000000 --- a/hscommon/tests/selectable_list_test.py +++ /dev/null @@ -1,65 +0,0 @@ -# Created By: Virgil Dupras -# Created On: 2011-09-06 -# Copyright 2015 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at -# http://www.gnu.org/licenses/gpl-3.0.html - -from ..testutil import eq_, callcounter, CallLogger -from ..gui.selectable_list import SelectableList, GUISelectableList - -def test_in(): - # When a SelectableList is in a list, doing "in list" with another instance returns false, even - # if they're the same as lists. - sl = SelectableList() - some_list = [sl] - assert SelectableList() not in some_list - -def test_selection_range(): - # selection is correctly adjusted on deletion - sl = SelectableList(['foo', 'bar', 'baz']) - sl.selected_index = 3 - eq_(sl.selected_index, 2) - del sl[2] - eq_(sl.selected_index, 1) - -def test_update_selection_called(): - # _update_selection_is called after a change in selection. However, we only do so on select() - # calls. I follow the old behavior of the Table class. At the moment, I don't quite remember - # why there was a specific select() method for triggering _update_selection(), but I think I - # remember there was a reason, so I keep it that way. - sl = SelectableList(['foo', 'bar']) - sl._update_selection = callcounter() - sl.select(1) - eq_(sl._update_selection.callcount, 1) - sl.selected_index = 0 - eq_(sl._update_selection.callcount, 1) # no call - -def test_guicalls(): - # A GUISelectableList appropriately calls its view. - sl = GUISelectableList(['foo', 'bar']) - sl.view = CallLogger() - sl.view.check_gui_calls(['refresh']) # Upon setting the view, we get a call to refresh() - sl[1] = 'baz' - sl.view.check_gui_calls(['refresh']) - sl.append('foo') - sl.view.check_gui_calls(['refresh']) - del sl[2] - sl.view.check_gui_calls(['refresh']) - sl.remove('baz') - sl.view.check_gui_calls(['refresh']) - sl.insert(0, 'foo') - sl.view.check_gui_calls(['refresh']) - sl.select(1) - sl.view.check_gui_calls(['update_selection']) - # XXX We have to give up on this for now because of a breakage it causes in the tables. - # sl.select(1) # don't update when selection stays the same - # gui.check_gui_calls([]) - -def test_search_by_prefix(): - sl = SelectableList(['foo', 'bAr', 'baZ']) - eq_(sl.search_by_prefix('b'), 1) - eq_(sl.search_by_prefix('BA'), 1) - eq_(sl.search_by_prefix('BAZ'), 2) - eq_(sl.search_by_prefix('BAZZ'), -1) \ No newline at end of file diff --git a/hscommon/tests/sqlite_test.py b/hscommon/tests/sqlite_test.py deleted file mode 100644 index 58bea9e3..00000000 --- a/hscommon/tests/sqlite_test.py +++ /dev/null @@ -1,126 +0,0 @@ -# Created By: Virgil Dupras -# Created On: 2007/05/19 -# Copyright 2015 Hardcoded Software (http://www.hardcoded.net) - -# This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at -# http://www.gnu.org/licenses/gpl-3.0.html - -import time -import threading -import os -import sqlite3 as sqlite - -from pytest import raises - -from ..testutil import eq_ -from ..sqlite import ThreadedConn - -# Threading is hard to test. In a lot of those tests, a failure means that the test run will -# hang forever. Well... I don't know a better alternative. - -def test_can_access_from_multiple_threads(): - def run(): - con.execute('insert into foo(bar) values(\'baz\')') - - con = ThreadedConn(':memory:', True) - con.execute('create table foo(bar TEXT)') - t = threading.Thread(target=run) - t.start() - t.join() - result = con.execute('select * from foo') - eq_(1, len(result)) - eq_('baz', result[0][0]) - -def test_exception_during_query(): - con = ThreadedConn(':memory:', True) - con.execute('create table foo(bar TEXT)') - with raises(sqlite.OperationalError): - con.execute('select * from bleh') - -def test_not_autocommit(tmpdir): - dbpath = str(tmpdir.join('foo.db')) - con = ThreadedConn(dbpath, False) - con.execute('create table foo(bar TEXT)') - con.execute('insert into foo(bar) values(\'baz\')') - del con - #The data shouldn't have been inserted - con = ThreadedConn(dbpath, False) - result = con.execute('select * from foo') - eq_(0, len(result)) - con.execute('insert into foo(bar) values(\'baz\')') - con.commit() - del con - # Now the data should be there - con = ThreadedConn(dbpath, False) - result = con.execute('select * from foo') - eq_(1, len(result)) - -def test_rollback(): - con = ThreadedConn(':memory:', False) - con.execute('create table foo(bar TEXT)') - con.execute('insert into foo(bar) values(\'baz\')') - con.rollback() - result = con.execute('select * from foo') - eq_(0, len(result)) - -def test_query_palceholders(): - con = ThreadedConn(':memory:', True) - con.execute('create table foo(bar TEXT)') - con.execute('insert into foo(bar) values(?)', ['baz']) - result = con.execute('select * from foo') - eq_(1, len(result)) - eq_('baz', result[0][0]) - -def test_make_sure_theres_no_messup_between_queries(): - def run(expected_rowid): - time.sleep(0.1) - result = con.execute('select rowid from foo where rowid = ?', [expected_rowid]) - assert expected_rowid == result[0][0] - - con = ThreadedConn(':memory:', True) - con.execute('create table foo(bar TEXT)') - for i in range(100): - con.execute('insert into foo(bar) values(\'baz\')') - threads = [] - for i in range(1, 101): - t = threading.Thread(target=run, args=(i,)) - t.start - threads.append(t) - while threads: - time.sleep(0.1) - threads = [t for t in threads if t.isAlive()] - -def test_query_after_close(): - con = ThreadedConn(':memory:', True) - con.close() - con.execute('select 1') - -def test_lastrowid(): - # It's not possible to return a cursor because of the threading, but lastrowid should be - # fetchable from the connection itself - con = ThreadedConn(':memory:', True) - con.execute('create table foo(bar TEXT)') - con.execute('insert into foo(bar) values(\'baz\')') - eq_(1, con.lastrowid) - -def test_add_fetchone_fetchall_interface_to_results(): - con = ThreadedConn(':memory:', True) - con.execute('create table foo(bar TEXT)') - con.execute('insert into foo(bar) values(\'baz1\')') - con.execute('insert into foo(bar) values(\'baz2\')') - result = con.execute('select * from foo') - ref = result[:] - eq_(ref, result.fetchall()) - eq_(ref[0], result.fetchone()) - eq_(ref[1], result.fetchone()) - assert result.fetchone() is None - -def test_non_ascii_dbname(tmpdir): - ThreadedConn(str(tmpdir.join('foo\u00e9.db')), True) - -def test_non_ascii_dbdir(tmpdir): - # when this test fails, it doesn't fail gracefully, it brings the whole test suite with it. - dbdir = tmpdir.join('foo\u00e9') - os.mkdir(str(dbdir)) - ThreadedConn(str(dbdir.join('foo.db')), True) diff --git a/hscommon/tests/table_test.py b/hscommon/tests/table_test.py deleted file mode 100644 index d612853e..00000000 --- a/hscommon/tests/table_test.py +++ /dev/null @@ -1,313 +0,0 @@ -# Created By: Virgil Dupras -# Created On: 2008-08-12 -# Copyright 2015 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at -# http://www.gnu.org/licenses/gpl-3.0.html - -from ..testutil import CallLogger, eq_ -from ..gui.table import Table, GUITable, Row - -class TestRow(Row): - def __init__(self, table, index, is_new=False): - Row.__init__(self, table) - self.is_new = is_new - self._index = index - - def load(self): - pass - - def save(self): - self.is_new = False - - @property - def index(self): - return self._index - - -class TestGUITable(GUITable): - def __init__(self, rowcount): - GUITable.__init__(self) - self.view = CallLogger() - self.rowcount = rowcount - self.updated_rows = None - - def _do_add(self): - return TestRow(self, len(self), is_new=True), len(self) - - def _is_edited_new(self): - return self.edited is not None and self.edited.is_new - - def _fill(self): - for i in range(self.rowcount): - self.append(TestRow(self, i)) - - def _update_selection(self): - self.updated_rows = self.selected_rows[:] - - -def table_with_footer(): - table = Table() - table.append(TestRow(table, 0)) - footer = TestRow(table, 1) - table.footer = footer - return table, footer - -def table_with_header(): - table = Table() - table.append(TestRow(table, 1)) - header = TestRow(table, 0) - table.header = header - return table, header - -#--- Tests -def test_allow_edit_when_attr_is_property_with_fset(): - # When a row has a property that has a fset, by default, make that cell editable. - class TestRow(Row): - @property - def foo(self): - pass - @property - def bar(self): - pass - @bar.setter - def bar(self, value): - pass - - row = TestRow(Table()) - assert row.can_edit_cell('bar') - assert not row.can_edit_cell('foo') - assert not row.can_edit_cell('baz') # doesn't exist, can't edit - -def test_can_edit_prop_has_priority_over_fset_checks(): - # When a row has a cen_edit_* property, it's the result of that property that is used, not the - # result of a fset check. - class TestRow(Row): - @property - def bar(self): - pass - @bar.setter - def bar(self, value): - pass - can_edit_bar = False - - row = TestRow(Table()) - assert not row.can_edit_cell('bar') - -def test_in(): - # When a table is in a list, doing "in list" with another instance returns false, even if - # they're the same as lists. - table = Table() - some_list = [table] - assert Table() not in some_list - -def test_footer_del_all(): - # Removing all rows doesn't crash when doing the footer check. - table, footer = table_with_footer() - del table[:] - assert table.footer is None - -def test_footer_del_row(): - # Removing the footer row sets it to None - table, footer = table_with_footer() - del table[-1] - assert table.footer is None - eq_(len(table), 1) - -def test_footer_is_appened_to_table(): - # A footer is appended at the table's bottom - table, footer = table_with_footer() - eq_(len(table), 2) - assert table[1] is footer - -def test_footer_remove(): - # remove() on footer sets it to None - table, footer = table_with_footer() - table.remove(footer) - assert table.footer is None - -def test_footer_replaces_old_footer(): - table, footer = table_with_footer() - other = Row(table) - table.footer = other - assert table.footer is other - eq_(len(table), 2) - assert table[1] is other - -def test_footer_rows_and_row_count(): - # rows() and row_count() ignore footer. - table, footer = table_with_footer() - eq_(table.row_count, 1) - eq_(table.rows, table[:-1]) - -def test_footer_setting_to_none_removes_old_one(): - table, footer = table_with_footer() - table.footer = None - assert table.footer is None - eq_(len(table), 1) - -def test_footer_stays_there_on_append(): - # Appending another row puts it above the footer - table, footer = table_with_footer() - table.append(Row(table)) - eq_(len(table), 3) - assert table[2] is footer - -def test_footer_stays_there_on_insert(): - # Inserting another row puts it above the footer - table, footer = table_with_footer() - table.insert(3, Row(table)) - eq_(len(table), 3) - assert table[2] is footer - -def test_header_del_all(): - # Removing all rows doesn't crash when doing the header check. - table, header = table_with_header() - del table[:] - assert table.header is None - -def test_header_del_row(): - # Removing the header row sets it to None - table, header = table_with_header() - del table[0] - assert table.header is None - eq_(len(table), 1) - -def test_header_is_inserted_in_table(): - # A header is inserted at the table's top - table, header = table_with_header() - eq_(len(table), 2) - assert table[0] is header - -def test_header_remove(): - # remove() on header sets it to None - table, header = table_with_header() - table.remove(header) - assert table.header is None - -def test_header_replaces_old_header(): - table, header = table_with_header() - other = Row(table) - table.header = other - assert table.header is other - eq_(len(table), 2) - assert table[0] is other - -def test_header_rows_and_row_count(): - # rows() and row_count() ignore header. - table, header = table_with_header() - eq_(table.row_count, 1) - eq_(table.rows, table[1:]) - -def test_header_setting_to_none_removes_old_one(): - table, header = table_with_header() - table.header = None - assert table.header is None - eq_(len(table), 1) - -def test_header_stays_there_on_insert(): - # Inserting another row at the top puts it below the header - table, header = table_with_header() - table.insert(0, Row(table)) - eq_(len(table), 3) - assert table[0] is header - -def test_refresh_view_on_refresh(): - # If refresh_view is not False, we refresh the table's view on refresh() - table = TestGUITable(1) - table.refresh() - table.view.check_gui_calls(['refresh']) - table.view.clear_calls() - table.refresh(refresh_view=False) - table.view.check_gui_calls([]) - -def test_restore_selection(): - # By default, after a refresh, selection goes on the last row - table = TestGUITable(10) - table.refresh() - eq_(table.selected_indexes, [9]) - -def test_restore_selection_after_cancel_edits(): - # _restore_selection() is called after cancel_edits(). Previously, only _update_selection would - # be called. - class MyTable(TestGUITable): - def _restore_selection(self, previous_selection): - self.selected_indexes = [6] - - table = MyTable(10) - table.refresh() - table.add() - table.cancel_edits() - eq_(table.selected_indexes, [6]) - -def test_restore_selection_with_previous_selection(): - # By default, we try to restore the selection that was there before a refresh - table = TestGUITable(10) - table.refresh() - table.selected_indexes = [2, 4] - table.refresh() - eq_(table.selected_indexes, [2, 4]) - -def test_restore_selection_custom(): - # After a _fill() called, the virtual _restore_selection() is called so that it's possible for a - # GUITable subclass to customize its post-refresh selection behavior. - class MyTable(TestGUITable): - def _restore_selection(self, previous_selection): - self.selected_indexes = [6] - - table = MyTable(10) - table.refresh() - eq_(table.selected_indexes, [6]) - -def test_row_cell_value(): - # *_cell_value() correctly mangles attrnames that are Python reserved words. - row = Row(Table()) - row.from_ = 'foo' - eq_(row.get_cell_value('from'), 'foo') - row.set_cell_value('from', 'bar') - eq_(row.get_cell_value('from'), 'bar') - -def test_sort_table_also_tries_attributes_without_underscores(): - # When determining a sort key, after having unsuccessfully tried the attribute with the, - # underscore, try the one without one. - table = Table() - row1 = Row(table) - row1._foo = 'a' # underscored attr must be checked first - row1.foo = 'b' - row1.bar = 'c' - row2 = Row(table) - row2._foo = 'b' - row2.foo = 'a' - row2.bar = 'b' - table.append(row1) - table.append(row2) - table.sort_by('foo') - assert table[0] is row1 - assert table[1] is row2 - table.sort_by('bar') - assert table[0] is row2 - assert table[1] is row1 - -def test_sort_table_updates_selection(): - table = TestGUITable(10) - table.refresh() - table.select([2, 4]) - table.sort_by('index', desc=True) - # Now, the updated rows should be 7 and 5 - eq_(len(table.updated_rows), 2) - r1, r2 = table.updated_rows - eq_(r1.index, 7) - eq_(r2.index, 5) - -def test_sort_table_with_footer(): - # Sorting a table with a footer keeps it at the bottom - table, footer = table_with_footer() - table.sort_by('index', desc=True) - assert table[-1] is footer - -def test_sort_table_with_header(): - # Sorting a table with a header keeps it at the top - table, header = table_with_header() - table.sort_by('index', desc=True) - assert table[0] is header \ No newline at end of file diff --git a/hscommon/tests/tree_test.py b/hscommon/tests/tree_test.py deleted file mode 100644 index b3bada73..00000000 --- a/hscommon/tests/tree_test.py +++ /dev/null @@ -1,109 +0,0 @@ -# Created By: Virgil Dupras -# Created On: 2010-02-12 -# Copyright 2015 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at -# http://www.gnu.org/licenses/gpl-3.0.html - -from ..testutil import eq_ -from ..gui.tree import Tree, Node - -def tree_with_some_nodes(): - t = Tree() - t.append(Node('foo')) - t.append(Node('bar')) - t.append(Node('baz')) - t[0].append(Node('sub1')) - t[0].append(Node('sub2')) - return t - -def test_selection(): - t = tree_with_some_nodes() - assert t.selected_node is None - eq_(t.selected_nodes, []) - assert t.selected_path is None - eq_(t.selected_paths, []) - -def test_select_one_node(): - t = tree_with_some_nodes() - t.selected_node = t[0][0] - assert t.selected_node is t[0][0] - eq_(t.selected_nodes, [t[0][0]]) - eq_(t.selected_path, [0, 0]) - eq_(t.selected_paths, [[0, 0]]) - -def test_select_one_path(): - t = tree_with_some_nodes() - t.selected_path = [0, 1] - assert t.selected_node is t[0][1] - -def test_select_multiple_nodes(): - t = tree_with_some_nodes() - t.selected_nodes = [t[0], t[1]] - eq_(t.selected_paths, [[0], [1]]) - -def test_select_multiple_paths(): - t = tree_with_some_nodes() - t.selected_paths = [[0], [1]] - eq_(t.selected_nodes, [t[0], t[1]]) - -def test_select_none_path(): - # setting selected_path to None clears the selection - t = Tree() - t.selected_path = None - assert t.selected_path is None - -def test_select_none_node(): - # setting selected_node to None clears the selection - t = Tree() - t.selected_node = None - eq_(t.selected_nodes, []) - -def test_clear_removes_selection(): - # When clearing a tree, we want to clear the selection as well or else we end up with a crash - # when calling selected_paths. - t = tree_with_some_nodes() - t.selected_path = [0] - t.clear() - assert t.selected_node is None - -def test_selection_override(): - # All selection changed pass through the _select_node() method so it's easy for subclasses to - # customize the tree's behavior. - class MyTree(Tree): - called = False - def _select_nodes(self, nodes): - self.called = True - - - t = MyTree() - t.selected_paths = [] - assert t.called - t.called = False - t.selected_node = None - assert t.called - -def test_findall(): - t = tree_with_some_nodes() - r = t.findall(lambda n: n.name.startswith('sub')) - eq_(set(r), set([t[0][0], t[0][1]])) - -def test_findall_dont_include_self(): - # When calling findall with include_self=False, the node itself is never evaluated. - t = tree_with_some_nodes() - del t._name # so that if the predicate is called on `t`, we crash - r = t.findall(lambda n: not n.name.startswith('sub'), include_self=False) # no crash - eq_(set(r), set([t[0], t[1], t[2]])) - -def test_find_dont_include_self(): - # When calling find with include_self=False, the node itself is never evaluated. - t = tree_with_some_nodes() - del t._name # so that if the predicate is called on `t`, we crash - r = t.find(lambda n: not n.name.startswith('sub'), include_self=False) # no crash - assert r is t[0] - -def test_find_none(): - # when find() yields no result, return None - t = Tree() - assert t.find(lambda n: False) is None # no StopIteration exception diff --git a/hscommon/tests/util_test.py b/hscommon/tests/util_test.py deleted file mode 100644 index 73c860b8..00000000 --- a/hscommon/tests/util_test.py +++ /dev/null @@ -1,325 +0,0 @@ -# Created By: Virgil Dupras -# Created On: 2011-01-11 -# Copyright 2015 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at -# http://www.gnu.org/licenses/gpl-3.0.html - -from io import StringIO - -from pytest import raises - -from ..testutil import eq_ -from ..path import Path -from ..util import * - -def test_nonone(): - eq_('foo', nonone('foo', 'bar')) - eq_('bar', nonone(None, 'bar')) - -def test_tryint(): - eq_(42,tryint('42')) - eq_(0,tryint('abc')) - eq_(0,tryint(None)) - eq_(42,tryint(None, 42)) - -def test_minmax(): - eq_(minmax(2, 1, 3), 2) - eq_(minmax(0, 1, 3), 1) - eq_(minmax(4, 1, 3), 3) - -#--- Sequence - -def test_first(): - eq_(first([3, 2, 1]), 3) - eq_(first(i for i in [3, 2, 1] if i < 3), 2) - -def test_flatten(): - eq_([1,2,3,4],flatten([[1,2],[3,4]])) - eq_([],flatten([])) - -def test_dedupe(): - reflist = [0,7,1,2,3,4,4,5,6,7,1,2,3] - eq_(dedupe(reflist),[0,7,1,2,3,4,5,6]) - -def test_stripfalse(): - eq_([1, 2, 3], stripfalse([None, 0, 1, 2, 3, None])) - -def test_extract(): - wheat, shaft = extract(lambda n: n % 2 == 0, list(range(10))) - eq_(wheat, [0, 2, 4, 6, 8]) - eq_(shaft, [1, 3, 5, 7, 9]) - -def test_allsame(): - assert allsame([42, 42, 42]) - assert not allsame([42, 43, 42]) - assert not allsame([43, 42, 42]) - # Works on non-sequence as well - assert allsame(iter([42, 42, 42])) - -def test_trailiter(): - eq_(list(trailiter([])), []) - eq_(list(trailiter(['foo'])), [(None, 'foo')]) - eq_(list(trailiter(['foo', 'bar'])), [(None, 'foo'), ('foo', 'bar')]) - eq_(list(trailiter(['foo', 'bar'], skipfirst=True)), [('foo', 'bar')]) - eq_(list(trailiter([], skipfirst=True)), []) # no crash - -def test_iterconsume(): - # We just want to make sure that we return *all* items and that we're not mistakenly skipping - # one. - eq_(list(range(2500)), list(iterconsume(list(range(2500))))) - eq_(list(reversed(range(2500))), list(iterconsume(list(range(2500)), reverse=False))) - -#--- String - -def test_escape(): - eq_('f\\o\\ob\\ar', escape('foobar', 'oa')) - eq_('f*o*ob*ar', escape('foobar', 'oa', '*')) - eq_('f*o*ob*ar', escape('foobar', set('oa'), '*')) - -def test_get_file_ext(): - eq_(get_file_ext("foobar"), "") - eq_(get_file_ext("foo.bar"), "bar") - eq_(get_file_ext("foobar."), "") - eq_(get_file_ext(".foobar"), "foobar") - -def test_rem_file_ext(): - eq_(rem_file_ext("foobar"), "foobar") - eq_(rem_file_ext("foo.bar"), "foo") - eq_(rem_file_ext("foobar."), "foobar") - eq_(rem_file_ext(".foobar"), "") - -def test_pluralize(): - eq_('0 song', pluralize(0,'song')) - eq_('1 song', pluralize(1,'song')) - eq_('2 songs', pluralize(2,'song')) - eq_('1 song', pluralize(1.1,'song')) - eq_('2 songs', pluralize(1.5,'song')) - eq_('1.1 songs', pluralize(1.1,'song',1)) - eq_('1.5 songs', pluralize(1.5,'song',1)) - eq_('2 entries', pluralize(2,'entry', plural_word='entries')) - -def test_format_time(): - eq_(format_time(0),'00:00:00') - eq_(format_time(1),'00:00:01') - eq_(format_time(23),'00:00:23') - eq_(format_time(60),'00:01:00') - eq_(format_time(101),'00:01:41') - eq_(format_time(683),'00:11:23') - eq_(format_time(3600),'01:00:00') - eq_(format_time(3754),'01:02:34') - eq_(format_time(36000),'10:00:00') - eq_(format_time(366666),'101:51:06') - eq_(format_time(0, with_hours=False),'00:00') - eq_(format_time(1, with_hours=False),'00:01') - eq_(format_time(23, with_hours=False),'00:23') - eq_(format_time(60, with_hours=False),'01:00') - eq_(format_time(101, with_hours=False),'01:41') - eq_(format_time(683, with_hours=False),'11:23') - eq_(format_time(3600, with_hours=False),'60:00') - eq_(format_time(6036, with_hours=False),'100:36') - eq_(format_time(60360, with_hours=False),'1006:00') - -def test_format_time_decimal(): - eq_(format_time_decimal(0), '0.0 second') - eq_(format_time_decimal(1), '1.0 second') - eq_(format_time_decimal(23), '23.0 seconds') - eq_(format_time_decimal(60), '1.0 minute') - eq_(format_time_decimal(101), '1.7 minutes') - eq_(format_time_decimal(683), '11.4 minutes') - eq_(format_time_decimal(3600), '1.0 hour') - eq_(format_time_decimal(6036), '1.7 hours') - eq_(format_time_decimal(86400), '1.0 day') - eq_(format_time_decimal(160360), '1.9 days') - -def test_format_size(): - eq_(format_size(1024), '1 KB') - eq_(format_size(1024,2), '1.00 KB') - eq_(format_size(1024,0,2), '1 MB') - eq_(format_size(1024,2,2), '0.01 MB') - eq_(format_size(1024,3,2), '0.001 MB') - eq_(format_size(1024,3,2,False), '0.001') - eq_(format_size(1023), '1023 B') - eq_(format_size(1023,0,1), '1 KB') - eq_(format_size(511,0,1), '1 KB') - eq_(format_size(9), '9 B') - eq_(format_size(99), '99 B') - eq_(format_size(999), '999 B') - eq_(format_size(9999), '10 KB') - eq_(format_size(99999), '98 KB') - eq_(format_size(999999), '977 KB') - eq_(format_size(9999999), '10 MB') - eq_(format_size(99999999), '96 MB') - eq_(format_size(999999999), '954 MB') - eq_(format_size(9999999999), '10 GB') - eq_(format_size(99999999999), '94 GB') - eq_(format_size(999999999999), '932 GB') - eq_(format_size(9999999999999), '10 TB') - eq_(format_size(99999999999999), '91 TB') - eq_(format_size(999999999999999), '910 TB') - eq_(format_size(9999999999999999), '9 PB') - eq_(format_size(99999999999999999), '89 PB') - eq_(format_size(999999999999999999), '889 PB') - eq_(format_size(9999999999999999999), '9 EB') - eq_(format_size(99999999999999999999), '87 EB') - eq_(format_size(999999999999999999999), '868 EB') - eq_(format_size(9999999999999999999999), '9 ZB') - eq_(format_size(99999999999999999999999), '85 ZB') - eq_(format_size(999999999999999999999999), '848 ZB') - -def test_remove_invalid_xml(): - eq_(remove_invalid_xml('foo\0bar\x0bbaz'), 'foo bar baz') - # surrogate blocks have to be replaced, but not the rest - eq_(remove_invalid_xml('foo\ud800bar\udfffbaz\ue000'), 'foo bar baz\ue000') - # replace with something else - eq_(remove_invalid_xml('foo\0baz', replace_with='bar'), 'foobarbaz') - -def test_multi_replace(): - eq_('136',multi_replace('123456',('2','45'))) - eq_('1 3 6',multi_replace('123456',('2','45'),' ')) - eq_('1 3 6',multi_replace('123456','245',' ')) - eq_('173896',multi_replace('123456','245','789')) - eq_('173896',multi_replace('123456','245',('7','8','9'))) - eq_('17386',multi_replace('123456',('2','45'),'78')) - eq_('17386',multi_replace('123456',('2','45'),('7','8'))) - with raises(ValueError): - multi_replace('123456',('2','45'),('7','8','9')) - eq_('17346',multi_replace('12346',('2','45'),'78')) - -#--- Files - -class TestCase_modified_after: - def test_first_is_modified_after(self, monkeyplus): - monkeyplus.patch_osstat('first', st_mtime=42) - monkeyplus.patch_osstat('second', st_mtime=41) - assert modified_after('first', 'second') - - def test_second_is_modified_after(self, monkeyplus): - monkeyplus.patch_osstat('first', st_mtime=42) - monkeyplus.patch_osstat('second', st_mtime=43) - assert not modified_after('first', 'second') - - def test_same_mtime(self, monkeyplus): - monkeyplus.patch_osstat('first', st_mtime=42) - monkeyplus.patch_osstat('second', st_mtime=42) - assert not modified_after('first', 'second') - - def test_first_file_does_not_exist(self, monkeyplus): - # when the first file doesn't exist, we return False - monkeyplus.patch_osstat('second', st_mtime=42) - assert not modified_after('does_not_exist', 'second') # no crash - - def test_second_file_does_not_exist(self, monkeyplus): - # when the second file doesn't exist, we return True - monkeyplus.patch_osstat('first', st_mtime=42) - assert modified_after('first', 'does_not_exist') # no crash - - def test_first_file_is_none(self, monkeyplus): - # when the first file is None, we return False - monkeyplus.patch_osstat('second', st_mtime=42) - assert not modified_after(None, 'second') # no crash - - def test_second_file_is_none(self, monkeyplus): - # when the second file is None, we return True - monkeyplus.patch_osstat('first', st_mtime=42) - assert modified_after('first', None) # no crash - - -class TestCase_delete_if_empty: - def test_is_empty(self, tmpdir): - testpath = Path(str(tmpdir)) - assert delete_if_empty(testpath) - assert not testpath.exists() - - def test_not_empty(self, tmpdir): - testpath = Path(str(tmpdir)) - testpath['foo'].mkdir() - assert not delete_if_empty(testpath) - assert testpath.exists() - - def test_with_files_to_delete(self, tmpdir): - testpath = Path(str(tmpdir)) - testpath['foo'].open('w') - testpath['bar'].open('w') - assert delete_if_empty(testpath, ['foo', 'bar']) - assert not testpath.exists() - - def test_directory_in_files_to_delete(self, tmpdir): - testpath = Path(str(tmpdir)) - testpath['foo'].mkdir() - assert not delete_if_empty(testpath, ['foo']) - assert testpath.exists() - - def test_delete_files_to_delete_only_if_dir_is_empty(self, tmpdir): - testpath = Path(str(tmpdir)) - testpath['foo'].open('w') - testpath['bar'].open('w') - assert not delete_if_empty(testpath, ['foo']) - assert testpath.exists() - assert testpath['foo'].exists() - - def test_doesnt_exist(self): - # When the 'path' doesn't exist, just do nothing. - delete_if_empty(Path('does_not_exist')) # no crash - - def test_is_file(self, tmpdir): - # When 'path' is a file, do nothing. - p = Path(str(tmpdir)) + 'filename' - p.open('w').close() - delete_if_empty(p) # no crash - - def test_ioerror(self, tmpdir, monkeypatch): - # if an IO error happens during the operation, ignore it. - def do_raise(*args, **kw): - raise OSError() - - monkeypatch.setattr(Path, 'rmdir', do_raise) - delete_if_empty(Path(str(tmpdir))) # no crash - - -class TestCase_open_if_filename: - def test_file_name(self, tmpdir): - filepath = str(tmpdir.join('test.txt')) - open(filepath, 'wb').write(b'test_data') - file, close = open_if_filename(filepath) - assert close - eq_(b'test_data', file.read()) - file.close() - - def test_opened_file(self): - sio = StringIO() - sio.write('test_data') - sio.seek(0) - file, close = open_if_filename(sio) - assert not close - eq_('test_data', file.read()) - - def test_mode_is_passed_to_open(self, tmpdir): - filepath = str(tmpdir.join('test.txt')) - open(filepath, 'w').close() - file, close = open_if_filename(filepath, 'a') - eq_('a', file.mode) - file.close() - - -class TestCase_FileOrPath: - def test_path(self, tmpdir): - filepath = str(tmpdir.join('test.txt')) - open(filepath, 'wb').write(b'test_data') - with FileOrPath(filepath) as fp: - eq_(b'test_data', fp.read()) - - def test_opened_file(self): - sio = StringIO() - sio.write('test_data') - sio.seek(0) - with FileOrPath(sio) as fp: - eq_('test_data', fp.read()) - - def test_mode_is_passed_to_open(self, tmpdir): - filepath = str(tmpdir.join('test.txt')) - open(filepath, 'w').close() - with FileOrPath(filepath, 'a') as fp: - eq_('a', fp.mode) - diff --git a/hscommon/testutil.py b/hscommon/testutil.py deleted file mode 100644 index bcb4fa70..00000000 --- a/hscommon/testutil.py +++ /dev/null @@ -1,212 +0,0 @@ -# Created By: Virgil Dupras -# Created On: 2010-11-14 -# Copyright 2015 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at -# http://www.gnu.org/licenses/gpl-3.0.html - -import threading -import py.path - -def eq_(a, b, msg=None): - __tracebackhide__ = True - assert a == b, msg or "%r != %r" % (a, b) - -def assert_almost_equal(a, b, places=7): - __tracebackhide__ = True - assert round(a, ndigits=places) == round(b, ndigits=places) - -def callcounter(): - def f(*args, **kwargs): - f.callcount += 1 - - f.callcount = 0 - return f - -class TestData: - def __init__(self, datadirpath): - self.datadirpath = py.path.local(datadirpath) - - def filepath(self, relative_path, *args): - """Returns the path of a file in testdata. - - 'relative_path' can be anything that can be added to a Path - if args is not empty, it will be joined to relative_path - """ - resultpath = self.datadirpath.join(relative_path) - if args: - resultpath = resultpath.join(*args) - assert resultpath.check() - return str(resultpath) - - -class CallLogger: - """This is a dummy object that logs all calls made to it. - - It is used to simulate the GUI layer. - """ - def __init__(self): - self.calls = [] - - def __getattr__(self, func_name): - def func(*args, **kw): - self.calls.append(func_name) - return func - - def clear_calls(self): - del self.calls[:] - - def check_gui_calls(self, expected, verify_order=False): - """Checks that the expected calls have been made to 'self', then clears the log. - - `expected` is an iterable of strings representing method names. - If `verify_order` is True, the order of the calls matters. - """ - __tracebackhide__ = True - if verify_order: - eq_(self.calls, expected) - else: - eq_(set(self.calls), set(expected)) - self.clear_calls() - - def check_gui_calls_partial(self, expected=None, not_expected=None, verify_order=False): - """Checks that the expected calls have been made to 'self', then clears the log. - - `expected` is an iterable of strings representing method names. Order doesn't matter. - Moreover, if calls have been made that are not in expected, no failure occur. - `not_expected` can be used for a more explicit check (rather than calling `check_gui_calls` - with an empty `expected`) to assert that calls have *not* been made. - """ - __tracebackhide__ = True - if expected is not None: - not_called = set(expected) - set(self.calls) - assert not not_called, "These calls haven't been made: {0}".format(not_called) - if verify_order: - max_index = 0 - for call in expected: - index = self.calls.index(call) - if index < max_index: - raise AssertionError("The call {0} hasn't been made in the correct order".format(call)) - max_index = index - if not_expected is not None: - called = set(not_expected) & set(self.calls) - assert not called, "These calls shouldn't have been made: {0}".format(called) - self.clear_calls() - - -class TestApp: - def __init__(self): - self._call_loggers = [] - - def clear_gui_calls(self): - for logger in self._call_loggers: - logger.clear_calls() - - def make_logger(self, class_=CallLogger, *initargs): - logger = class_(*initargs) - self._call_loggers.append(logger) - return logger - - def make_gui(self, name, class_, view=None, parent=None, holder=None): - if view is None: - view = self.make_logger() - if parent is None: - # The attribute "default_parent" has to be set for this to work correctly - parent = self.default_parent - if holder is None: - holder = self - setattr(holder, '{0}_gui'.format(name), view) - gui = class_(parent) - gui.view = view - setattr(holder, name, gui) - return gui - - -# To use @with_app, you have to import pytest_funcarg__app in your conftest.py file. -def with_app(setupfunc): - def decorator(func): - func.setupfunc = setupfunc - return func - return decorator - -def pytest_funcarg__app(request): - setupfunc = request.function.setupfunc - if hasattr(setupfunc, '__code__'): - argnames = setupfunc.__code__.co_varnames[:setupfunc.__code__.co_argcount] - def getarg(name): - if name == 'self': - return request.function.__self__ - else: - return request.getfuncargvalue(name) - args = [getarg(argname) for argname in argnames] - else: - args = [] - app = setupfunc(*args) - return app - -def jointhreads(): - """Join all threads to the main thread""" - for thread in threading.enumerate(): - if hasattr(thread, 'BUGGY'): - continue - if thread.getName() != 'MainThread' and thread.isAlive(): - if hasattr(thread, 'close'): - thread.close() - thread.join(1) - if thread.isAlive(): - print("Thread problem. Some thread doesn't want to stop.") - thread.BUGGY = True - -def _unify_args(func, args, kwargs, args_to_ignore=None): - ''' Unify args and kwargs in the same dictionary. - - The result is kwargs with args added to it. func.func_code.co_varnames is used to determine - under what key each elements of arg will be mapped in kwargs. - - if you want some arguments not to be in the results, supply a list of arg names in - args_to_ignore. - - if f is a function that takes *args, func_code.co_varnames is empty, so args will be put - under 'args' in kwargs. - - def foo(bar, baz) - _unifyArgs(foo, (42,), {'baz': 23}) --> {'bar': 42, 'baz': 23} - _unifyArgs(foo, (42,), {'baz': 23}, ['bar']) --> {'baz': 23} - ''' - result = kwargs.copy() - if hasattr(func, '__code__'): # built-in functions don't have func_code - args = list(args) - if getattr(func, '__self__', None) is not None: # bound method, we have to add self to args list - args = [func.__self__] + args - defaults = list(func.__defaults__) if func.__defaults__ is not None else [] - arg_count = func.__code__.co_argcount - arg_names = list(func.__code__.co_varnames) - if len(args) < arg_count: # We have default values - required_arg_count = arg_count - len(args) - args = args + defaults[-required_arg_count:] - for arg_name, arg in zip(arg_names, args): - # setdefault is used because if the arg is already in kwargs, we don't want to use default values - result.setdefault(arg_name, arg) - else: - #'func' has a *args argument - result['args'] = args - if args_to_ignore: - for kw in args_to_ignore: - del result[kw] - return result - -def log_calls(func): - ''' Logs all func calls' arguments under func.calls. - - func.calls is a list of _unify_args() result (dict). - - Mostly used for unit testing. - ''' - def wrapper(*args, **kwargs): - unifiedArgs = _unify_args(func, args, kwargs) - wrapper.calls.append(unifiedArgs) - return func(*args, **kwargs) - - wrapper.calls = [] - return wrapper diff --git a/hscommon/trans.py b/hscommon/trans.py deleted file mode 100644 index ab039685..00000000 --- a/hscommon/trans.py +++ /dev/null @@ -1,162 +0,0 @@ -# Created By: Virgil Dupras -# Created On: 2010-06-23 -# Copyright 2015 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at -# http://www.gnu.org/licenses/gpl-3.0.html - -# Doing i18n with GNU gettext for the core text gets complicated, so what I do is that I make the -# GUI layer responsible for supplying a tr() function. - -import locale -import logging -import os.path as op - -from .plat import ISWINDOWS, ISLINUX - -_trfunc = None -_trget = None -installed_lang = None - -def tr(s, context=None): - if _trfunc is None: - return s - else: - if context: - return _trfunc(s, context) - else: - return _trfunc(s) - -def trget(domain): - # Returns a tr() function for the specified domain. - if _trget is None: - return lambda s: tr(s, domain) - else: - return _trget(domain) - -def set_tr(new_tr, new_trget=None): - global _trfunc, _trget - _trfunc = new_tr - if new_trget is not None: - _trget = new_trget - -def get_locale_name(lang): - if ISWINDOWS: - # http://msdn.microsoft.com/en-us/library/39cwe7zf(vs.71).aspx - LANG2LOCALENAME = { - 'cs': 'czy', - 'de': 'deu', - 'es': 'esn', - 'fr': 'fra', - 'it': 'ita', - 'ko': 'korean', - 'nl': 'nld', - 'pl_PL': 'polish_poland', - 'pt_BR': 'ptb', - 'ru': 'rus', - 'zh_CN': 'chs', - } - else: - LANG2LOCALENAME = { - 'cs': 'cs_CZ', - 'de': 'de_DE', - 'es': 'es_ES', - 'fr': 'fr_FR', - 'it': 'it_IT', - 'nl': 'nl_NL', - 'hy': 'hy_AM', - 'ko': 'ko_KR', - 'pl_PL': 'pl_PL', - 'pt_BR': 'pt_BR', - 'ru': 'ru_RU', - 'uk': 'uk_UA', - 'vi': 'vi_VN', - 'zh_CN': 'zh_CN', - } - if lang not in LANG2LOCALENAME: - return None - result = LANG2LOCALENAME[lang] - if ISLINUX: - result += '.UTF-8' - return result - -#--- Qt -def install_qt_trans(lang=None): - from PyQt5.QtCore import QCoreApplication, QTranslator, QLocale - if not lang: - lang = str(QLocale.system().name())[:2] - localename = get_locale_name(lang) - if localename is not None: - try: - locale.setlocale(locale.LC_ALL, localename) - except locale.Error: - logging.warning("Couldn't set locale %s", localename) - else: - lang = 'en' - qtr1 = QTranslator(QCoreApplication.instance()) - qtr1.load(':/qt_%s' % lang) - QCoreApplication.installTranslator(qtr1) - qtr2 = QTranslator(QCoreApplication.instance()) - qtr2.load(':/%s' % lang) - QCoreApplication.installTranslator(qtr2) - def qt_tr(s, context='core'): - return str(QCoreApplication.translate(context, s, None)) - set_tr(qt_tr) - -#--- gettext -def install_gettext_trans(base_folder, lang): - import gettext - def gettext_trget(domain): - if not lang: - return lambda s: s - try: - return gettext.translation(domain, localedir=base_folder, languages=[lang]).gettext - except IOError: - return lambda s: s - - default_gettext = gettext_trget('core') - def gettext_tr(s, context=None): - if not context: - return default_gettext(s) - else: - trfunc = gettext_trget(context) - return trfunc(s) - set_tr(gettext_tr, gettext_trget) - global installed_lang - installed_lang = lang - -def install_gettext_trans_under_cocoa(): - from cocoa import proxy - resFolder = proxy.getResourcePath() - baseFolder = op.join(resFolder, 'locale') - currentLang = proxy.systemLang() - install_gettext_trans(baseFolder, currentLang) - localename = get_locale_name(currentLang) - if localename is not None: - locale.setlocale(locale.LC_ALL, localename) - -def install_gettext_trans_under_qt(base_folder, lang=None): - # So, we install the gettext locale, great, but we also should try to install qt_*.qm if - # available so that strings that are inside Qt itself over which I have no control are in the - # right language. - from PyQt5.QtCore import QCoreApplication, QTranslator, QLocale, QLibraryInfo - if not lang: - lang = str(QLocale.system().name())[:2] - localename = get_locale_name(lang) - if localename is not None: - try: - locale.setlocale(locale.LC_ALL, localename) - except locale.Error: - logging.warning("Couldn't set locale %s", localename) - qmname = 'qt_%s' % lang - if ISLINUX: - # Under linux, a full Qt installation is already available in the system, we didn't bundle - # up the qm files in our package, so we have to load translations from the system. - qmpath = op.join(QLibraryInfo.location(QLibraryInfo.TranslationsPath), qmname) - else: - qmpath = op.join(base_folder, qmname) - qtr = QTranslator(QCoreApplication.instance()) - qtr.load(qmpath) - QCoreApplication.installTranslator(qtr) - install_gettext_trans(base_folder, lang) diff --git a/hscommon/util.py b/hscommon/util.py deleted file mode 100644 index 801824dd..00000000 --- a/hscommon/util.py +++ /dev/null @@ -1,407 +0,0 @@ -# Created By: Virgil Dupras -# Created On: 2011-01-11 -# Copyright 2015 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at -# http://www.gnu.org/licenses/gpl-3.0.html - -import sys -import os -import os.path as op -import re -from math import ceil -import glob -import shutil -from datetime import timedelta - -from .path import Path, pathify, log_io_error - -def nonone(value, replace_value): - """Returns ``value`` if ``value`` is not ``None``. Returns ``replace_value`` otherwise. - """ - if value is None: - return replace_value - else: - return value - -def tryint(value, default=0): - """Tries to convert ``value`` to in ``int`` and returns ``default`` if it fails. - """ - try: - return int(value) - except (TypeError, ValueError): - return default - -def minmax(value, min_value, max_value): - """Returns `value` or one of the min/max bounds if `value` is not between them. - """ - return min(max(value, min_value), max_value) - -#--- Sequence related - -def dedupe(iterable): - """Returns a list of elements in ``iterable`` with all dupes removed. - - The order of the elements is preserved. - """ - result = [] - seen = {} - for item in iterable: - if item in seen: - continue - seen[item] = 1 - result.append(item) - return result - -def flatten(iterables, start_with=None): - """Takes a list of lists ``iterables`` and returns a list containing elements of every list. - - If ``start_with`` is not ``None``, the result will start with ``start_with`` items, exactly as - if ``start_with`` would be the first item of lists. - """ - result = [] - if start_with: - result.extend(start_with) - for iterable in iterables: - result.extend(iterable) - return result - -def first(iterable): - """Returns the first item of ``iterable``. - """ - try: - return next(iter(iterable)) - except StopIteration: - return None - -def stripfalse(seq): - """Returns a sequence with all false elements stripped out of seq. - """ - return [x for x in seq if x] - -def extract(predicate, iterable): - """Separates the wheat from the shaft (`predicate` defines what's the wheat), and returns both. - """ - wheat = [] - shaft = [] - for item in iterable: - if predicate(item): - wheat.append(item) - else: - shaft.append(item) - return wheat, shaft - -def allsame(iterable): - """Returns whether all elements of 'iterable' are the same. - """ - it = iter(iterable) - try: - first_item = next(it) - except StopIteration: - raise ValueError("iterable cannot be empty") - return all(element == first_item for element in it) - -def trailiter(iterable, skipfirst=False): - """Yields (prev_element, element), starting with (None, first_element). - - If skipfirst is True, there will be no (None, item1) element and we'll start - directly with (item1, item2). - """ - it = iter(iterable) - if skipfirst: - prev = next(it) - else: - prev = None - for item in it: - yield prev, item - prev = item - -def iterconsume(seq, reverse=True): - """Iterate over ``seq`` and pops yielded objects. - - Because we use the ``pop()`` method, we reverse ``seq`` before proceeding. If you don't need - to do that, set ``reverse`` to ``False``. - - This is useful in tight memory situation where you are looping over a sequence of objects that - are going to be discarded afterwards. If you're creating other objects during that iteration - you might want to use this to avoid ``MemoryError``. - """ - if reverse: - seq.reverse() - while seq: - yield seq.pop() - -#--- String related - -def escape(s, to_escape, escape_with='\\'): - """Returns ``s`` with characters in ``to_escape`` all prepended with ``escape_with``. - """ - return ''.join((escape_with + c if c in to_escape else c) for c in s) - -def get_file_ext(filename): - """Returns the lowercase extension part of filename, without the dot. - """ - pos = filename.rfind('.') - if pos > -1: - return filename[pos + 1:].lower() - else: - return '' - -def rem_file_ext(filename): - """Returns the filename without extension. - """ - pos = filename.rfind('.') - if pos > -1: - return filename[:pos] - else: - return filename - -def pluralize(number, word, decimals=0, plural_word=None): - """Returns a pluralized string with ``number`` in front of ``word``. - - Adds a 's' to s if ``number`` > 1. - ``number``: The number to go in front of s - ``word``: The word to go after number - ``decimals``: The number of digits after the dot - ``plural_word``: If the plural rule for word is more complex than adding a 's', specify a plural - """ - number = round(number, decimals) - format = "%%1.%df %%s" % decimals - if number > 1: - if plural_word is None: - word += 's' - else: - word = plural_word - return format % (number, word) - -def format_time(seconds, with_hours=True): - """Transforms seconds in a hh:mm:ss string. - - If ``with_hours`` if false, the format is mm:ss. - """ - minus = seconds < 0 - if minus: - seconds *= -1 - m, s = divmod(seconds, 60) - if with_hours: - h, m = divmod(m, 60) - r = '%02d:%02d:%02d' % (h, m, s) - else: - r = '%02d:%02d' % (m,s) - if minus: - return '-' + r - else: - return r - -def format_time_decimal(seconds): - """Transforms seconds in a strings like '3.4 minutes'. - """ - minus = seconds < 0 - if minus: - seconds *= -1 - if seconds < 60: - r = pluralize(seconds, 'second', 1) - elif seconds < 3600: - r = pluralize(seconds / 60.0, 'minute', 1) - elif seconds < 86400: - r = pluralize(seconds / 3600.0, 'hour', 1) - else: - r = pluralize(seconds / 86400.0, 'day', 1) - if minus: - return '-' + r - else: - return r - -SIZE_DESC = ('B','KB','MB','GB','TB','PB','EB','ZB','YB') -SIZE_VALS = tuple(1024 ** i for i in range(1,9)) -def format_size(size, decimal=0, forcepower=-1, showdesc=True): - """Transform a byte count in a formatted string (KB, MB etc..). - - ``size`` is the number of bytes to format. - ``decimal`` is the number digits after the dot. - ``forcepower`` is the desired suffix. 0 is B, 1 is KB, 2 is MB etc.. if kept at -1, the suffix - will be automatically chosen (so the resulting number is always below 1024). - if ``showdesc`` is ``True``, the suffix will be shown after the number. - Usage example:: - - >>> format_size(1234, decimal=2, showdesc=True) - '1.21 KB' - """ - if forcepower < 0: - i = 0 - while size >= SIZE_VALS[i]: - i += 1 - else: - i = forcepower - if i > 0: - div = SIZE_VALS[i-1] - else: - div = 1 - format = '%%%d.%df' % (decimal,decimal) - negative = size < 0 - divided_size = ((0.0 + abs(size)) / div) - if decimal == 0: - divided_size = ceil(divided_size) - else: - divided_size = ceil(divided_size * (10 ** decimal)) / (10 ** decimal) - if negative: - divided_size *= -1 - result = format % divided_size - if showdesc: - result += ' ' + SIZE_DESC[i] - return result - -_valid_xml_range = '\x09\x0A\x0D\x20-\uD7FF\uE000-\uFFFD' -if sys.maxunicode > 0x10000: - _valid_xml_range += '%s-%s' % (chr(0x10000), chr(min(sys.maxunicode, 0x10FFFF))) -RE_INVALID_XML_SUB = re.compile('[^%s]' % _valid_xml_range, re.U).sub - -def remove_invalid_xml(s, replace_with=' '): - return RE_INVALID_XML_SUB(replace_with, s) - -def multi_replace(s, replace_from, replace_to=''): - """A function like str.replace() with multiple replacements. - - ``replace_from`` is a list of things you want to replace. Ex: ['a','bc','d'] - ``replace_to`` is a list of what you want to replace to. - If ``replace_to`` is a list and has the same length as ``replace_from``, ``replace_from`` - items will be translated to corresponding ``replace_to``. A ``replace_to`` list must - have the same length as ``replace_from`` - If ``replace_to`` is a string, all ``replace_from`` occurence will be replaced - by that string. - ``replace_from`` can also be a str. If it is, every char in it will be translated - as if ``replace_from`` would be a list of chars. If ``replace_to`` is a str and has - the same length as ``replace_from``, it will be transformed into a list. - """ - if isinstance(replace_to, str) and (len(replace_from) != len(replace_to)): - replace_to = [replace_to for r in replace_from] - if len(replace_from) != len(replace_to): - raise ValueError('len(replace_from) must be equal to len(replace_to)') - replace = list(zip(replace_from, replace_to)) - for r_from, r_to in [r for r in replace if r[0] in s]: - s = s.replace(r_from, r_to) - return s - -#--- Date related - -def iterdaterange(start, end): - """Yields every day between ``start`` and ``end``. - """ - date = start - while date <= end: - yield date - date += timedelta(1) - -#--- Files related - -@pathify -def modified_after(first_path: Path, second_path: Path): - """Returns ``True`` if first_path's mtime is higher than second_path's mtime. - - If one of the files doesn't exist or is ``None``, it is considered "never modified". - """ - try: - first_mtime = first_path.stat().st_mtime - except (EnvironmentError, AttributeError): - return False - try: - second_mtime = second_path.stat().st_mtime - except (EnvironmentError, AttributeError): - return True - return first_mtime > second_mtime - -def find_in_path(name, paths=None): - """Search for `name` in all directories of `paths` and return the absolute path of the first - occurrence. If `paths` is None, $PATH is used. - """ - if paths is None: - paths = os.environ['PATH'] - if isinstance(paths, str): # if it's not a string, it's already a list - paths = paths.split(os.pathsep) - for path in paths: - if op.exists(op.join(path, name)): - return op.join(path, name) - return None - -@log_io_error -@pathify -def delete_if_empty(path: Path, files_to_delete=[]): - """Deletes the directory at 'path' if it is empty or if it only contains files_to_delete. - """ - if not path.exists() or not path.isdir(): - return - contents = path.listdir() - if any(p for p in contents if (p.name not in files_to_delete) or p.isdir()): - return False - for p in contents: - p.remove() - path.rmdir() - return True - -def open_if_filename(infile, mode='rb'): - """If ``infile`` is a string, it opens and returns it. If it's already a file object, it simply returns it. - - This function returns ``(file, should_close_flag)``. The should_close_flag is True is a file has - effectively been opened (if we already pass a file object, we assume that the responsibility for - closing the file has already been taken). Example usage:: - - fp, shouldclose = open_if_filename(infile) - dostuff() - if shouldclose: - fp.close() - """ - if isinstance(infile, Path): - return (infile.open(mode), True) - if isinstance(infile, str): - return (open(infile, mode), True) - else: - return (infile, False) - -def ensure_folder(path): - "Create `path` as a folder if it doesn't exist." - if not op.exists(path): - os.makedirs(path) - -def ensure_file(path): - "Create `path` as an empty file if it doesn't exist." - if not op.exists(path): - open(path, 'w').close() - -def delete_files_with_pattern(folder_path, pattern, recursive=True): - """Delete all files (or folders) in `folder_path` that match the glob `pattern`. - """ - to_delete = glob.glob(op.join(folder_path, pattern)) - for fn in to_delete: - if op.isdir(fn): - shutil.rmtree(fn) - else: - os.remove(fn) - if recursive: - subpaths = [op.join(folder_path, fn) for fn in os.listdir(folder_path)] - subfolders = [p for p in subpaths if op.isdir(p)] - for p in subfolders: - delete_files_with_pattern(p, pattern, True) - -class FileOrPath: - """Does the same as :func:`open_if_filename`, but it can be used with a ``with`` statement. - - Example:: - - with FileOrPath(infile): - dostuff() - """ - def __init__(self, file_or_path, mode='rb'): - self.file_or_path = file_or_path - self.mode = mode - self.mustclose = False - self.fp = None - - def __enter__(self): - self.fp, self.mustclose = open_if_filename(self.file_or_path, self.mode) - return self.fp - - def __exit__(self, exc_type, exc_value, traceback): - if self.fp and self.mustclose: - self.fp.close() - diff --git a/qt/base/preferences_dialog.py b/qt/base/preferences_dialog.py index d5f009a7..82d771b1 100644 --- a/qt/base/preferences_dialog.py +++ b/qt/base/preferences_dialog.py @@ -15,7 +15,7 @@ from PyQt5.QtWidgets import ( from hscommon.plat import ISOSX, ISLINUX from hscommon.trans import trget from qtlib.util import horizontalWrap -from qtlib.preferences import LANGNAMES +from qtlib.preferences import get_langnames tr = trget('ui') @@ -29,7 +29,8 @@ class PreferencesDialogBase(QDialog): flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint super().__init__(parent, flags, **kwargs) self.app = app - self.supportedLanguages = sorted(SUPPORTED_LANGUAGES, key=lambda lang: LANGNAMES[lang]) + all_languages = get_langnames() + self.supportedLanguages = sorted(SUPPORTED_LANGUAGES, key=lambda lang: all_languages[lang]) self._setupUi() self.filterHardnessSlider.valueChanged['int'].connect(self.filterHardnessLabel.setNum) @@ -97,7 +98,7 @@ class PreferencesDialogBase(QDialog): self.languageLabel = QLabel(tr("Language:"), self) self.languageComboBox = QComboBox(self) for lang in self.supportedLanguages: - self.languageComboBox.addItem(LANGNAMES[lang]) + self.languageComboBox.addItem(get_langnames()[lang]) self.widgetsVLayout.addLayout(horizontalWrap([self.languageLabel, self.languageComboBox, None])) self.copyMoveLabel = QLabel(self) self.copyMoveLabel.setText(tr("Copy and Move:")) diff --git a/qtlib b/qtlib new file mode 160000 index 00000000..d4e3c4ba --- /dev/null +++ b/qtlib @@ -0,0 +1 @@ +Subproject commit d4e3c4ba643deffc0b4f21ecd9fdf9a14fcc80f9 diff --git a/qtlib/.gitignore b/qtlib/.gitignore deleted file mode 100644 index 7f1e166c..00000000 --- a/qtlib/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -__pycache__ -*.pyc -*.mo -.DS_Store \ No newline at end of file diff --git a/qtlib/.tx/config b/qtlib/.tx/config deleted file mode 100644 index 67e19649..00000000 --- a/qtlib/.tx/config +++ /dev/null @@ -1,8 +0,0 @@ -[main] -host = https://www.transifex.com - -[hscommon.qtlib] -file_filter = locale//LC_MESSAGES/qtlib.po -source_file = locale/qtlib.pot -source_lang = en -type = PO diff --git a/qtlib/LICENSE b/qtlib/LICENSE deleted file mode 100644 index 5a8d3ceb..00000000 --- a/qtlib/LICENSE +++ /dev/null @@ -1,10 +0,0 @@ -Copyright 2014, 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. - -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/qtlib/__init__.py b/qtlib/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/qtlib/about_box.py b/qtlib/about_box.py deleted file mode 100644 index d7acabc6..00000000 --- a/qtlib/about_box.py +++ /dev/null @@ -1,76 +0,0 @@ -# Created By: Virgil Dupras -# Created On: 2009-05-09 -# Copyright 2015 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at -# http://www.gnu.org/licenses/gpl-3.0.html - -from PyQt5.QtCore import Qt, QCoreApplication -from PyQt5.QtGui import QPixmap, QFont -from PyQt5.QtWidgets import (QDialog, QDialogButtonBox, QSizePolicy, QHBoxLayout, QVBoxLayout, - QLabel, QApplication) - -from hscommon.trans import trget - -tr = trget('qtlib') - -class AboutBox(QDialog): - def __init__(self, parent, app, **kwargs): - flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint | Qt.MSWindowsFixedSizeDialogHint - super().__init__(parent, flags, **kwargs) - self.app = app - self._setupUi() - - self.buttonBox.accepted.connect(self.accept) - self.buttonBox.rejected.connect(self.reject) - - def _setupUi(self): - self.setWindowTitle(tr("About {}").format(QCoreApplication.instance().applicationName())) - self.resize(400, 190) - sizePolicy = QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.sizePolicy().hasHeightForWidth()) - self.setSizePolicy(sizePolicy) - self.horizontalLayout = QHBoxLayout(self) - self.logoLabel = QLabel(self) - self.logoLabel.setPixmap(QPixmap(':/%s_big' % self.app.LOGO_NAME)) - self.horizontalLayout.addWidget(self.logoLabel) - self.verticalLayout = QVBoxLayout() - self.nameLabel = QLabel(self) - font = QFont() - font.setWeight(75) - font.setBold(True) - self.nameLabel.setFont(font) - self.nameLabel.setText(QCoreApplication.instance().applicationName()) - self.verticalLayout.addWidget(self.nameLabel) - self.versionLabel = QLabel(self) - self.versionLabel.setText(tr("Version {}").format(QCoreApplication.instance().applicationVersion())) - self.verticalLayout.addWidget(self.versionLabel) - self.label_3 = QLabel(self) - self.verticalLayout.addWidget(self.label_3) - self.label_3.setText(tr("Copyright Hardcoded Software 2014")) - self.label = QLabel(self) - font = QFont() - font.setWeight(75) - font.setBold(True) - self.label.setFont(font) - self.verticalLayout.addWidget(self.label) - self.buttonBox = QDialogButtonBox(self) - self.buttonBox.setOrientation(Qt.Horizontal) - self.buttonBox.setStandardButtons(QDialogButtonBox.Ok) - self.verticalLayout.addWidget(self.buttonBox) - self.horizontalLayout.addLayout(self.verticalLayout) - - -if __name__ == '__main__': - import sys - app = QApplication([]) - QCoreApplication.setOrganizationName('Hardcoded Software') - QCoreApplication.setApplicationName('FooApp') - QCoreApplication.setApplicationVersion('1.2.3') - app.LOGO_NAME = '' - dialog = AboutBox(None, app) - dialog.show() - sys.exit(app.exec_()) diff --git a/qtlib/app.py b/qtlib/app.py deleted file mode 100644 index a1a0df20..00000000 --- a/qtlib/app.py +++ /dev/null @@ -1,19 +0,0 @@ -# -*- coding: utf-8 -*- -# Created By: Virgil Dupras -# Created On: 2009-10-16 -# Copyright 2015 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at -# http://www.gnu.org/licenses/gpl-3.0.html - -from PyQt5.QtCore import SIGNAL, QTimer, QObject - -class Application(QObject): - def __init__(self): - QObject.__init__(self) - QTimer.singleShot(0, self.__launchTimerTimedOut) - - def __launchTimerTimedOut(self): - self.emit(SIGNAL('applicationFinishedLaunching()')) - diff --git a/qtlib/column.py b/qtlib/column.py deleted file mode 100644 index b50aec1e..00000000 --- a/qtlib/column.py +++ /dev/null @@ -1,86 +0,0 @@ -# Created By: Virgil Dupras -# Created On: 2009-11-25 -# Copyright 2015 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at -# http://www.gnu.org/licenses/gpl-3.0.html - -from PyQt5.QtCore import Qt - -class Column: - def __init__(self, attrname, defaultWidth, editor=None, alignment=Qt.AlignLeft, cantTruncate=False): - self.attrname = attrname - self.defaultWidth = defaultWidth - self.editor = editor - self.alignment = alignment - # This is to indicate, during printing, that a column can't have its data truncated. - self.cantTruncate = cantTruncate - - -class Columns: - def __init__(self, model, columns, headerView): - self.model = model - self._headerView = headerView - self._headerView.setDefaultAlignment(Qt.AlignLeft) - def setspecs(col, modelcol): - modelcol.default_width = col.defaultWidth - modelcol.editor = col.editor - modelcol.alignment = col.alignment - modelcol.cantTruncate = col.cantTruncate - if columns: - for col in columns: - modelcol = self.model.column_by_name(col.attrname) - setspecs(col, modelcol) - else: - col = Column('', 100) - for modelcol in self.model.column_list: - setspecs(col, modelcol) - self.model.view = self - self._headerView.sectionMoved.connect(self.headerSectionMoved) - self._headerView.sectionResized.connect(self.headerSectionResized) - - #--- Public - def setColumnsWidth(self, widths): - #`widths` can be None. If it is, then default widths are set. - columns = self.model.column_list - if not widths: - widths = [column.default_width for column in columns] - for column, width in zip(columns, widths): - if width == 0: # column was hidden before. - width = column.default_width - self._headerView.resizeSection(column.logical_index, width) - - def setColumnsOrder(self, columnIndexes): - if not columnIndexes: - return - for destIndex, columnIndex in enumerate(columnIndexes): - # moveSection takes 2 visual index arguments, so we have to get our visual index first - visualIndex = self._headerView.visualIndex(columnIndex) - self._headerView.moveSection(visualIndex, destIndex) - - #--- Events - def headerSectionMoved(self, logicalIndex, oldVisualIndex, newVisualIndex): - attrname = self.model.column_by_index(logicalIndex).name - self.model.move_column(attrname, newVisualIndex) - - def headerSectionResized(self, logicalIndex, oldSize, newSize): - attrname = self.model.column_by_index(logicalIndex).name - self.model.resize_column(attrname, newSize) - - #--- model --> view - def restore_columns(self): - columns = self.model.ordered_columns - indexes = [col.logical_index for col in columns] - self.setColumnsOrder(indexes) - widths = [col.width for col in self.model.column_list] - if not any(widths): - widths = None - self.setColumnsWidth(widths) - for column in self.model.column_list: - visible = self.model.column_is_visible(column.name) - self._headerView.setSectionHidden(column.logical_index, not visible) - - def set_column_visible(self, colname, visible): - column = self.model.column_by_name(colname) - self._headerView.setSectionHidden(column.logical_index, not visible) diff --git a/qtlib/error_report_dialog.py b/qtlib/error_report_dialog.py deleted file mode 100644 index 58a03cac..00000000 --- a/qtlib/error_report_dialog.py +++ /dev/null @@ -1,84 +0,0 @@ -# Created By: Virgil Dupras -# Created On: 2009-05-23 -# Copyright 2015 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at -# http://www.gnu.org/licenses/gpl-3.0.html - -import traceback -import sys -import os - -from PyQt5.QtCore import Qt, QCoreApplication, QSize -from PyQt5.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPlainTextEdit, QPushButton - -from hscommon.trans import trget -from hscommon.desktop import open_url -from .util import horizontalSpacer - -tr = trget('qtlib') - -class ErrorReportDialog(QDialog): - def __init__(self, parent, github_url, error, **kwargs): - flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint - super().__init__(parent, flags, **kwargs) - self._setupUi() - name = QCoreApplication.applicationName() - version = QCoreApplication.applicationVersion() - errorText = "Application Name: {}\nVersion: {}\n\n{}".format(name, version, error) - # Under windows, we end up with an error report without linesep if we don't mangle it - errorText = errorText.replace('\n', os.linesep) - self.errorTextEdit.setPlainText(errorText) - self.github_url = github_url - - self.sendButton.clicked.connect(self.goToGithub) - self.dontSendButton.clicked.connect(self.reject) - - def _setupUi(self): - self.setWindowTitle(tr("Error Report")) - self.resize(553, 349) - self.verticalLayout = QVBoxLayout(self) - self.label = QLabel(self) - self.label.setText(tr("Something went wrong. How about reporting the error?")) - self.label.setWordWrap(True) - self.verticalLayout.addWidget(self.label) - self.errorTextEdit = QPlainTextEdit(self) - self.errorTextEdit.setReadOnly(True) - self.verticalLayout.addWidget(self.errorTextEdit) - msg = tr( - "Error reports should be reported as Github issues. You can copy the error traceback " - "above and paste it in a new issue (bonus point if you run a search to make sure the " - "issue doesn't already exist). What usually really helps is if you add a description " - "of how you got the error. Thanks!" - "\n\n" - "Although the application should continue to run after this error, it may be in an " - "unstable state, so it is recommended that you restart the application." - ) - self.label2 = QLabel(msg) - self.label2.setWordWrap(True) - self.verticalLayout.addWidget(self.label2) - self.horizontalLayout = QHBoxLayout() - self.horizontalLayout.addItem(horizontalSpacer()) - self.dontSendButton = QPushButton(self) - self.dontSendButton.setText(tr("Close")) - self.dontSendButton.setMinimumSize(QSize(110, 0)) - self.horizontalLayout.addWidget(self.dontSendButton) - self.sendButton = QPushButton(self) - self.sendButton.setText(tr("Go to Github")) - self.sendButton.setMinimumSize(QSize(110, 0)) - self.sendButton.setDefault(True) - self.horizontalLayout.addWidget(self.sendButton) - self.verticalLayout.addLayout(self.horizontalLayout) - - def goToGithub(self): - open_url(self.github_url) - - -def install_excepthook(github_url): - def my_excepthook(exctype, value, tb): - s = ''.join(traceback.format_exception(exctype, value, tb)) - dialog = ErrorReportDialog(None, github_url, s) - dialog.exec_() - - sys.excepthook = my_excepthook diff --git a/qtlib/images/search_clear_13.png b/qtlib/images/search_clear_13.png deleted file mode 100644 index 99e5b04dd5567929b8b1876fcee04cece8e5e9d3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 398 zcmeAS@N?(olHy`uVBq!ia0vp@Ak4u6ByT*@`3|I5(j9#r85lP9bN@+X1@aY=J%W50 z7^>757#dm_7=8hT8eT9klo~KFyh>nTu$sZZAYL$MSD+10f+@+{-G$+Qd;gjJKptm- zM`SSr1Gg{;GcwGYBLNir=IP=XB5^tQ!d~qbMuy`b&wrjXJveS&#~zVprJFk%_snCI zY?-p@6Z?tbKE1q8HQbXRLKCY*%)3^3F(G^D=Fjxo7y<(5iKkn|}#kD=9WGy0h_4W#kDz zo^8t(y{!&mFLT)vI)CZ!COgkHA^SyZ!=Fh?x?p=L3f89pQ+BAr*Yrp)fTUP{Uh{dUPGw%zgyFN o74$EB&n}5Pwz2>Hhx6_8*SdKID6H|80fqpBr>mdKI;Vst0AKB!!2kdN diff --git a/qtlib/locale/cs/LC_MESSAGES/qtlib.po b/qtlib/locale/cs/LC_MESSAGES/qtlib.po deleted file mode 100644 index 5c8f67a0..00000000 --- a/qtlib/locale/cs/LC_MESSAGES/qtlib.po +++ /dev/null @@ -1,106 +0,0 @@ -# Translators: -msgid "" -msgstr "" -"Project-Id-Version: hscommon\n" -"PO-Revision-Date: 2014-03-30 14:55+0000\n" -"Last-Translator: Virgil Dupras \n" -"Language-Team: Czech (http://www.transifex.com/p/hscommon/language/cs/)\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: utf-8\n" -"Language: cs\n" -"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" - -#: qtlib/about_box.py:29 -msgid "About {}" -msgstr "" - -#: qtlib/about_box.py:49 -msgid "Version {}" -msgstr "" - -#: qtlib/about_box.py:53 -msgid "Copyright Hardcoded Software 2014" -msgstr "" - -#: qtlib/error_report_dialog.py:39 -msgid "Error Report" -msgstr "" - -#: qtlib/error_report_dialog.py:43 -msgid "Something went wrong. How about reporting the error?" -msgstr "" - -#: qtlib/error_report_dialog.py:49 -msgid "" -"Error reports should be reported as Github issues. You can copy the error traceback above and paste it in a new issue (bonus point if you run a search to make sure the issue doesn't already exist). What usually really helps is if you add a description of how you got the error. Thanks!\n" -"\n" -"Although the application should continue to run after this error, it may be in an unstable state, so it is recommended that you restart the application." -msgstr "" - -#: qtlib/error_report_dialog.py:64 -msgid "Close" -msgstr "" - -#: qtlib/error_report_dialog.py:68 -msgid "Go to Github" -msgstr "" - -#: qtlib/preferences.py:17 -msgid "English" -msgstr "" - -#: qtlib/preferences.py:18 -msgid "French" -msgstr "" - -#: qtlib/preferences.py:19 -msgid "German" -msgstr "" - -#: qtlib/preferences.py:20 -msgid "Chinese (Simplified)" -msgstr "" - -#: qtlib/preferences.py:21 -msgid "Czech" -msgstr "" - -#: qtlib/preferences.py:22 -msgid "Italian" -msgstr "" - -#: qtlib/preferences.py:23 -msgid "Armenian" -msgstr "" - -#: qtlib/preferences.py:24 -msgid "Russian" -msgstr "" - -#: qtlib/preferences.py:25 -msgid "Ukrainian" -msgstr "" - -#: qtlib/preferences.py:26 -msgid "Dutch" -msgstr "" - -#: qtlib/preferences.py:27 -msgid "Brazilian" -msgstr "" - -#: qtlib/preferences.py:28 -msgid "Spanish" -msgstr "" - -#: qtlib/preferences.py:29 -msgid "Vietnamese" -msgstr "" - -#: qtlib/recent.py:53 -msgid "Clear List" -msgstr "" - -#: qtlib/search_edit.py:41 -msgid "Search..." -msgstr "" diff --git a/qtlib/locale/de/LC_MESSAGES/qtlib.po b/qtlib/locale/de/LC_MESSAGES/qtlib.po deleted file mode 100644 index 5642772c..00000000 --- a/qtlib/locale/de/LC_MESSAGES/qtlib.po +++ /dev/null @@ -1,106 +0,0 @@ -# Translators: -msgid "" -msgstr "" -"Project-Id-Version: hscommon\n" -"PO-Revision-Date: 2014-03-30 14:55+0000\n" -"Last-Translator: Virgil Dupras \n" -"Language-Team: German (http://www.transifex.com/p/hscommon/language/de/)\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: utf-8\n" -"Language: de\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#: qtlib/about_box.py:29 -msgid "About {}" -msgstr "" - -#: qtlib/about_box.py:49 -msgid "Version {}" -msgstr "" - -#: qtlib/about_box.py:53 -msgid "Copyright Hardcoded Software 2014" -msgstr "" - -#: qtlib/error_report_dialog.py:39 -msgid "Error Report" -msgstr "" - -#: qtlib/error_report_dialog.py:43 -msgid "Something went wrong. How about reporting the error?" -msgstr "" - -#: qtlib/error_report_dialog.py:49 -msgid "" -"Error reports should be reported as Github issues. You can copy the error traceback above and paste it in a new issue (bonus point if you run a search to make sure the issue doesn't already exist). What usually really helps is if you add a description of how you got the error. Thanks!\n" -"\n" -"Although the application should continue to run after this error, it may be in an unstable state, so it is recommended that you restart the application." -msgstr "" - -#: qtlib/error_report_dialog.py:64 -msgid "Close" -msgstr "" - -#: qtlib/error_report_dialog.py:68 -msgid "Go to Github" -msgstr "" - -#: qtlib/preferences.py:17 -msgid "English" -msgstr "Englisch" - -#: qtlib/preferences.py:18 -msgid "French" -msgstr "Französisch" - -#: qtlib/preferences.py:19 -msgid "German" -msgstr "Deutsch" - -#: qtlib/preferences.py:20 -msgid "Chinese (Simplified)" -msgstr "" - -#: qtlib/preferences.py:21 -msgid "Czech" -msgstr "" - -#: qtlib/preferences.py:22 -msgid "Italian" -msgstr "" - -#: qtlib/preferences.py:23 -msgid "Armenian" -msgstr "" - -#: qtlib/preferences.py:24 -msgid "Russian" -msgstr "" - -#: qtlib/preferences.py:25 -msgid "Ukrainian" -msgstr "" - -#: qtlib/preferences.py:26 -msgid "Dutch" -msgstr "" - -#: qtlib/preferences.py:27 -msgid "Brazilian" -msgstr "" - -#: qtlib/preferences.py:28 -msgid "Spanish" -msgstr "" - -#: qtlib/preferences.py:29 -msgid "Vietnamese" -msgstr "" - -#: qtlib/recent.py:53 -msgid "Clear List" -msgstr "" - -#: qtlib/search_edit.py:41 -msgid "Search..." -msgstr "" diff --git a/qtlib/locale/es/LC_MESSAGES/qtlib.po b/qtlib/locale/es/LC_MESSAGES/qtlib.po deleted file mode 100644 index 5d0aa0d6..00000000 --- a/qtlib/locale/es/LC_MESSAGES/qtlib.po +++ /dev/null @@ -1,106 +0,0 @@ -# Translators: -msgid "" -msgstr "" -"Project-Id-Version: hscommon\n" -"PO-Revision-Date: 2014-03-30 14:55+0000\n" -"Last-Translator: Virgil Dupras \n" -"Language-Team: Spanish (http://www.transifex.com/p/hscommon/language/es/)\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: utf-8\n" -"Language: es\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#: qtlib/about_box.py:29 -msgid "About {}" -msgstr "Acerca de {}" - -#: qtlib/about_box.py:49 -msgid "Version {}" -msgstr "Versión {}" - -#: qtlib/about_box.py:53 -msgid "Copyright Hardcoded Software 2014" -msgstr "" - -#: qtlib/error_report_dialog.py:39 -msgid "Error Report" -msgstr "Informe de error" - -#: qtlib/error_report_dialog.py:43 -msgid "Something went wrong. How about reporting the error?" -msgstr "" - -#: qtlib/error_report_dialog.py:49 -msgid "" -"Error reports should be reported as Github issues. You can copy the error traceback above and paste it in a new issue (bonus point if you run a search to make sure the issue doesn't already exist). What usually really helps is if you add a description of how you got the error. Thanks!\n" -"\n" -"Although the application should continue to run after this error, it may be in an unstable state, so it is recommended that you restart the application." -msgstr "" - -#: qtlib/error_report_dialog.py:64 -msgid "Close" -msgstr "" - -#: qtlib/error_report_dialog.py:68 -msgid "Go to Github" -msgstr "" - -#: qtlib/preferences.py:17 -msgid "English" -msgstr "Inglés" - -#: qtlib/preferences.py:18 -msgid "French" -msgstr "Francés" - -#: qtlib/preferences.py:19 -msgid "German" -msgstr "Alemán" - -#: qtlib/preferences.py:20 -msgid "Chinese (Simplified)" -msgstr "Chino (simplificado)" - -#: qtlib/preferences.py:21 -msgid "Czech" -msgstr "Checo" - -#: qtlib/preferences.py:22 -msgid "Italian" -msgstr "Italiano" - -#: qtlib/preferences.py:23 -msgid "Armenian" -msgstr "Armenio" - -#: qtlib/preferences.py:24 -msgid "Russian" -msgstr "Ruso" - -#: qtlib/preferences.py:25 -msgid "Ukrainian" -msgstr "Ucraniano" - -#: qtlib/preferences.py:26 -msgid "Dutch" -msgstr "Holandés" - -#: qtlib/preferences.py:27 -msgid "Brazilian" -msgstr "Brasileño" - -#: qtlib/preferences.py:28 -msgid "Spanish" -msgstr "Español" - -#: qtlib/preferences.py:29 -msgid "Vietnamese" -msgstr "" - -#: qtlib/recent.py:53 -msgid "Clear List" -msgstr "Limpiar lista" - -#: qtlib/search_edit.py:41 -msgid "Search..." -msgstr "Búsqueda..." diff --git a/qtlib/locale/fr/LC_MESSAGES/qtlib.po b/qtlib/locale/fr/LC_MESSAGES/qtlib.po deleted file mode 100644 index 4c6c6bcc..00000000 --- a/qtlib/locale/fr/LC_MESSAGES/qtlib.po +++ /dev/null @@ -1,106 +0,0 @@ -# Translators: -msgid "" -msgstr "" -"Project-Id-Version: hscommon\n" -"PO-Revision-Date: 2014-03-30 14:55+0000\n" -"Last-Translator: Virgil Dupras \n" -"Language-Team: French (http://www.transifex.com/p/hscommon/language/fr/)\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: utf-8\n" -"Language: fr\n" -"Plural-Forms: nplurals=2; plural=(n > 1);\n" - -#: qtlib/about_box.py:29 -msgid "About {}" -msgstr "" - -#: qtlib/about_box.py:49 -msgid "Version {}" -msgstr "" - -#: qtlib/about_box.py:53 -msgid "Copyright Hardcoded Software 2014" -msgstr "" - -#: qtlib/error_report_dialog.py:39 -msgid "Error Report" -msgstr "" - -#: qtlib/error_report_dialog.py:43 -msgid "Something went wrong. How about reporting the error?" -msgstr "" - -#: qtlib/error_report_dialog.py:49 -msgid "" -"Error reports should be reported as Github issues. You can copy the error traceback above and paste it in a new issue (bonus point if you run a search to make sure the issue doesn't already exist). What usually really helps is if you add a description of how you got the error. Thanks!\n" -"\n" -"Although the application should continue to run after this error, it may be in an unstable state, so it is recommended that you restart the application." -msgstr "" - -#: qtlib/error_report_dialog.py:64 -msgid "Close" -msgstr "" - -#: qtlib/error_report_dialog.py:68 -msgid "Go to Github" -msgstr "" - -#: qtlib/preferences.py:17 -msgid "English" -msgstr "Anglais" - -#: qtlib/preferences.py:18 -msgid "French" -msgstr "Français" - -#: qtlib/preferences.py:19 -msgid "German" -msgstr "Allemand" - -#: qtlib/preferences.py:20 -msgid "Chinese (Simplified)" -msgstr "Chinois (Simplifié)" - -#: qtlib/preferences.py:21 -msgid "Czech" -msgstr "Tchèque" - -#: qtlib/preferences.py:22 -msgid "Italian" -msgstr "Italien" - -#: qtlib/preferences.py:23 -msgid "Armenian" -msgstr "Arménien" - -#: qtlib/preferences.py:24 -msgid "Russian" -msgstr "Russe" - -#: qtlib/preferences.py:25 -msgid "Ukrainian" -msgstr "Ukrainien" - -#: qtlib/preferences.py:26 -msgid "Dutch" -msgstr "Néerlandais" - -#: qtlib/preferences.py:27 -msgid "Brazilian" -msgstr "" - -#: qtlib/preferences.py:28 -msgid "Spanish" -msgstr "" - -#: qtlib/preferences.py:29 -msgid "Vietnamese" -msgstr "" - -#: qtlib/recent.py:53 -msgid "Clear List" -msgstr "Vider la liste" - -#: qtlib/search_edit.py:41 -msgid "Search..." -msgstr "Recherche..." diff --git a/qtlib/locale/hy/LC_MESSAGES/qtlib.po b/qtlib/locale/hy/LC_MESSAGES/qtlib.po deleted file mode 100755 index c9f1284f..00000000 --- a/qtlib/locale/hy/LC_MESSAGES/qtlib.po +++ /dev/null @@ -1,106 +0,0 @@ -# Translators: -msgid "" -msgstr "" -"Project-Id-Version: hscommon\n" -"PO-Revision-Date: 2014-03-30 14:55+0000\n" -"Last-Translator: Virgil Dupras \n" -"Language-Team: Armenian (http://www.transifex.com/p/hscommon/language/hy/)\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: utf-8\n" -"Language: hy\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#: qtlib/about_box.py:29 -msgid "About {}" -msgstr "" - -#: qtlib/about_box.py:49 -msgid "Version {}" -msgstr "" - -#: qtlib/about_box.py:53 -msgid "Copyright Hardcoded Software 2014" -msgstr "" - -#: qtlib/error_report_dialog.py:39 -msgid "Error Report" -msgstr "Սխալի զեկույցը" - -#: qtlib/error_report_dialog.py:43 -msgid "Something went wrong. How about reporting the error?" -msgstr "" - -#: qtlib/error_report_dialog.py:49 -msgid "" -"Error reports should be reported as Github issues. You can copy the error traceback above and paste it in a new issue (bonus point if you run a search to make sure the issue doesn't already exist). What usually really helps is if you add a description of how you got the error. Thanks!\n" -"\n" -"Although the application should continue to run after this error, it may be in an unstable state, so it is recommended that you restart the application." -msgstr "" - -#: qtlib/error_report_dialog.py:64 -msgid "Close" -msgstr "" - -#: qtlib/error_report_dialog.py:68 -msgid "Go to Github" -msgstr "" - -#: qtlib/preferences.py:17 -msgid "English" -msgstr "Անգլերեն" - -#: qtlib/preferences.py:18 -msgid "French" -msgstr "Ֆրանսերեն" - -#: qtlib/preferences.py:19 -msgid "German" -msgstr "Գերմաներեն" - -#: qtlib/preferences.py:20 -msgid "Chinese (Simplified)" -msgstr "Չինարեն (Պարզեցված)" - -#: qtlib/preferences.py:21 -msgid "Czech" -msgstr "Չեխերեն" - -#: qtlib/preferences.py:22 -msgid "Italian" -msgstr "Իտալերեն" - -#: qtlib/preferences.py:23 -msgid "Armenian" -msgstr "" - -#: qtlib/preferences.py:24 -msgid "Russian" -msgstr "" - -#: qtlib/preferences.py:25 -msgid "Ukrainian" -msgstr "" - -#: qtlib/preferences.py:26 -msgid "Dutch" -msgstr "" - -#: qtlib/preferences.py:27 -msgid "Brazilian" -msgstr "" - -#: qtlib/preferences.py:28 -msgid "Spanish" -msgstr "" - -#: qtlib/preferences.py:29 -msgid "Vietnamese" -msgstr "" - -#: qtlib/recent.py:53 -msgid "Clear List" -msgstr "Մաքրել ցանկը" - -#: qtlib/search_edit.py:41 -msgid "Search..." -msgstr "" diff --git a/qtlib/locale/it/LC_MESSAGES/qtlib.po b/qtlib/locale/it/LC_MESSAGES/qtlib.po deleted file mode 100644 index bc69bff7..00000000 --- a/qtlib/locale/it/LC_MESSAGES/qtlib.po +++ /dev/null @@ -1,106 +0,0 @@ -# Translators: -msgid "" -msgstr "" -"Project-Id-Version: hscommon\n" -"PO-Revision-Date: 2014-03-30 14:55+0000\n" -"Last-Translator: Virgil Dupras \n" -"Language-Team: Italian (http://www.transifex.com/p/hscommon/language/it/)\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: utf-8\n" -"Language: it\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#: qtlib/about_box.py:29 -msgid "About {}" -msgstr "" - -#: qtlib/about_box.py:49 -msgid "Version {}" -msgstr "" - -#: qtlib/about_box.py:53 -msgid "Copyright Hardcoded Software 2014" -msgstr "" - -#: qtlib/error_report_dialog.py:39 -msgid "Error Report" -msgstr "" - -#: qtlib/error_report_dialog.py:43 -msgid "Something went wrong. How about reporting the error?" -msgstr "" - -#: qtlib/error_report_dialog.py:49 -msgid "" -"Error reports should be reported as Github issues. You can copy the error traceback above and paste it in a new issue (bonus point if you run a search to make sure the issue doesn't already exist). What usually really helps is if you add a description of how you got the error. Thanks!\n" -"\n" -"Although the application should continue to run after this error, it may be in an unstable state, so it is recommended that you restart the application." -msgstr "" - -#: qtlib/error_report_dialog.py:64 -msgid "Close" -msgstr "" - -#: qtlib/error_report_dialog.py:68 -msgid "Go to Github" -msgstr "" - -#: qtlib/preferences.py:17 -msgid "English" -msgstr "" - -#: qtlib/preferences.py:18 -msgid "French" -msgstr "" - -#: qtlib/preferences.py:19 -msgid "German" -msgstr "" - -#: qtlib/preferences.py:20 -msgid "Chinese (Simplified)" -msgstr "" - -#: qtlib/preferences.py:21 -msgid "Czech" -msgstr "" - -#: qtlib/preferences.py:22 -msgid "Italian" -msgstr "" - -#: qtlib/preferences.py:23 -msgid "Armenian" -msgstr "" - -#: qtlib/preferences.py:24 -msgid "Russian" -msgstr "" - -#: qtlib/preferences.py:25 -msgid "Ukrainian" -msgstr "" - -#: qtlib/preferences.py:26 -msgid "Dutch" -msgstr "" - -#: qtlib/preferences.py:27 -msgid "Brazilian" -msgstr "" - -#: qtlib/preferences.py:28 -msgid "Spanish" -msgstr "" - -#: qtlib/preferences.py:29 -msgid "Vietnamese" -msgstr "" - -#: qtlib/recent.py:53 -msgid "Clear List" -msgstr "" - -#: qtlib/search_edit.py:41 -msgid "Search..." -msgstr "" diff --git a/qtlib/locale/ko/LC_MESSAGES/qtlib.po b/qtlib/locale/ko/LC_MESSAGES/qtlib.po deleted file mode 100644 index e8db011e..00000000 --- a/qtlib/locale/ko/LC_MESSAGES/qtlib.po +++ /dev/null @@ -1,106 +0,0 @@ -# Translators: -msgid "" -msgstr "" -"Project-Id-Version: hscommon\n" -"PO-Revision-Date: 2015-07-20 16:42+0000\n" -"Last-Translator: Virgil Dupras \n" -"Language-Team: Korean (http://www.transifex.com/p/hscommon/language/ko/)\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: utf-8\n" -"Language: ko\n" -"Plural-Forms: nplurals=1; plural=0;\n" - -#: qtlib/about_box.py:29 -msgid "About {}" -msgstr "" - -#: qtlib/about_box.py:49 -msgid "Version {}" -msgstr "" - -#: qtlib/about_box.py:53 -msgid "Copyright Hardcoded Software 2014" -msgstr "" - -#: qtlib/error_report_dialog.py:39 -msgid "Error Report" -msgstr "" - -#: qtlib/error_report_dialog.py:43 -msgid "Something went wrong. How about reporting the error?" -msgstr "" - -#: qtlib/error_report_dialog.py:49 -msgid "" -"Error reports should be reported as Github issues. You can copy the error traceback above and paste it in a new issue (bonus point if you run a search to make sure the issue doesn't already exist). What usually really helps is if you add a description of how you got the error. Thanks!\n" -"\n" -"Although the application should continue to run after this error, it may be in an unstable state, so it is recommended that you restart the application." -msgstr "" - -#: qtlib/error_report_dialog.py:64 -msgid "Close" -msgstr "" - -#: qtlib/error_report_dialog.py:68 -msgid "Go to Github" -msgstr "" - -#: qtlib/preferences.py:17 -msgid "English" -msgstr "" - -#: qtlib/preferences.py:18 -msgid "French" -msgstr "" - -#: qtlib/preferences.py:19 -msgid "German" -msgstr "" - -#: qtlib/preferences.py:20 -msgid "Chinese (Simplified)" -msgstr "" - -#: qtlib/preferences.py:21 -msgid "Czech" -msgstr "" - -#: qtlib/preferences.py:22 -msgid "Italian" -msgstr "" - -#: qtlib/preferences.py:23 -msgid "Armenian" -msgstr "" - -#: qtlib/preferences.py:24 -msgid "Russian" -msgstr "" - -#: qtlib/preferences.py:25 -msgid "Ukrainian" -msgstr "" - -#: qtlib/preferences.py:26 -msgid "Dutch" -msgstr "" - -#: qtlib/preferences.py:27 -msgid "Brazilian" -msgstr "" - -#: qtlib/preferences.py:28 -msgid "Spanish" -msgstr "" - -#: qtlib/preferences.py:29 -msgid "Vietnamese" -msgstr "" - -#: qtlib/recent.py:53 -msgid "Clear List" -msgstr "" - -#: qtlib/search_edit.py:41 -msgid "Search..." -msgstr "" diff --git a/qtlib/locale/nl/LC_MESSAGES/qtlib.po b/qtlib/locale/nl/LC_MESSAGES/qtlib.po deleted file mode 100644 index bc86416d..00000000 --- a/qtlib/locale/nl/LC_MESSAGES/qtlib.po +++ /dev/null @@ -1,106 +0,0 @@ -# Translators: -msgid "" -msgstr "" -"Project-Id-Version: hscommon\n" -"PO-Revision-Date: 2014-03-30 14:55+0000\n" -"Last-Translator: Virgil Dupras \n" -"Language-Team: Dutch (http://www.transifex.com/p/hscommon/language/nl/)\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: utf-8\n" -"Language: nl\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#: qtlib/about_box.py:29 -msgid "About {}" -msgstr "" - -#: qtlib/about_box.py:49 -msgid "Version {}" -msgstr "" - -#: qtlib/about_box.py:53 -msgid "Copyright Hardcoded Software 2014" -msgstr "" - -#: qtlib/error_report_dialog.py:39 -msgid "Error Report" -msgstr "" - -#: qtlib/error_report_dialog.py:43 -msgid "Something went wrong. How about reporting the error?" -msgstr "" - -#: qtlib/error_report_dialog.py:49 -msgid "" -"Error reports should be reported as Github issues. You can copy the error traceback above and paste it in a new issue (bonus point if you run a search to make sure the issue doesn't already exist). What usually really helps is if you add a description of how you got the error. Thanks!\n" -"\n" -"Although the application should continue to run after this error, it may be in an unstable state, so it is recommended that you restart the application." -msgstr "" - -#: qtlib/error_report_dialog.py:64 -msgid "Close" -msgstr "" - -#: qtlib/error_report_dialog.py:68 -msgid "Go to Github" -msgstr "" - -#: qtlib/preferences.py:17 -msgid "English" -msgstr "Engels" - -#: qtlib/preferences.py:18 -msgid "French" -msgstr "Frans" - -#: qtlib/preferences.py:19 -msgid "German" -msgstr "Duits" - -#: qtlib/preferences.py:20 -msgid "Chinese (Simplified)" -msgstr "" - -#: qtlib/preferences.py:21 -msgid "Czech" -msgstr "Tsjechisch" - -#: qtlib/preferences.py:22 -msgid "Italian" -msgstr "Italiaans" - -#: qtlib/preferences.py:23 -msgid "Armenian" -msgstr "" - -#: qtlib/preferences.py:24 -msgid "Russian" -msgstr "" - -#: qtlib/preferences.py:25 -msgid "Ukrainian" -msgstr "" - -#: qtlib/preferences.py:26 -msgid "Dutch" -msgstr "" - -#: qtlib/preferences.py:27 -msgid "Brazilian" -msgstr "" - -#: qtlib/preferences.py:28 -msgid "Spanish" -msgstr "" - -#: qtlib/preferences.py:29 -msgid "Vietnamese" -msgstr "" - -#: qtlib/recent.py:53 -msgid "Clear List" -msgstr "" - -#: qtlib/search_edit.py:41 -msgid "Search..." -msgstr "" diff --git a/qtlib/locale/pl_PL/LC_MESSAGES/qtlib.po b/qtlib/locale/pl_PL/LC_MESSAGES/qtlib.po deleted file mode 100644 index c79769fe..00000000 --- a/qtlib/locale/pl_PL/LC_MESSAGES/qtlib.po +++ /dev/null @@ -1,107 +0,0 @@ -# -# Translators: -msgid "" -msgstr "" -"Project-Id-Version: hscommon\n" -"PO-Revision-Date: 2015-07-20 16:53+0000\n" -"Last-Translator: Virgil Dupras \n" -"Language-Team: Polish (Poland) (http://www.transifex.com/p/hscommon/language/pl_PL/)\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: utf-8\n" -"Language: pl_PL\n" -"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" - -#: qtlib/about_box.py:29 -msgid "About {}" -msgstr "" - -#: qtlib/about_box.py:49 -msgid "Version {}" -msgstr "" - -#: qtlib/about_box.py:53 -msgid "Copyright Hardcoded Software 2014" -msgstr "" - -#: qtlib/error_report_dialog.py:39 -msgid "Error Report" -msgstr "" - -#: qtlib/error_report_dialog.py:43 -msgid "Something went wrong. How about reporting the error?" -msgstr "" - -#: qtlib/error_report_dialog.py:49 -msgid "" -"Error reports should be reported as Github issues. You can copy the error traceback above and paste it in a new issue (bonus point if you run a search to make sure the issue doesn't already exist). What usually really helps is if you add a description of how you got the error. Thanks!\n" -"\n" -"Although the application should continue to run after this error, it may be in an unstable state, so it is recommended that you restart the application." -msgstr "" - -#: qtlib/error_report_dialog.py:64 -msgid "Close" -msgstr "" - -#: qtlib/error_report_dialog.py:68 -msgid "Go to Github" -msgstr "" - -#: qtlib/preferences.py:17 -msgid "English" -msgstr "" - -#: qtlib/preferences.py:18 -msgid "French" -msgstr "" - -#: qtlib/preferences.py:19 -msgid "German" -msgstr "" - -#: qtlib/preferences.py:20 -msgid "Chinese (Simplified)" -msgstr "" - -#: qtlib/preferences.py:21 -msgid "Czech" -msgstr "" - -#: qtlib/preferences.py:22 -msgid "Italian" -msgstr "" - -#: qtlib/preferences.py:23 -msgid "Armenian" -msgstr "" - -#: qtlib/preferences.py:24 -msgid "Russian" -msgstr "" - -#: qtlib/preferences.py:25 -msgid "Ukrainian" -msgstr "" - -#: qtlib/preferences.py:26 -msgid "Dutch" -msgstr "" - -#: qtlib/preferences.py:27 -msgid "Brazilian" -msgstr "" - -#: qtlib/preferences.py:28 -msgid "Spanish" -msgstr "" - -#: qtlib/preferences.py:29 -msgid "Vietnamese" -msgstr "" - -#: qtlib/recent.py:53 -msgid "Clear List" -msgstr "" - -#: qtlib/search_edit.py:41 -msgid "Search..." -msgstr "" diff --git a/qtlib/locale/pt_BR/LC_MESSAGES/qtlib.po b/qtlib/locale/pt_BR/LC_MESSAGES/qtlib.po deleted file mode 100644 index fb94e291..00000000 --- a/qtlib/locale/pt_BR/LC_MESSAGES/qtlib.po +++ /dev/null @@ -1,110 +0,0 @@ -# Translators: -# Vitu , 2013-2014 -msgid "" -msgstr "" -"Project-Id-Version: hscommon\n" -"PO-Revision-Date: 2014-04-30 18:58+0000\n" -"Last-Translator: Vitu \n" -"Language-Team: Portuguese (Brazil) (http://www.transifex.com/p/hscommon/language/pt_BR/)\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: utf-8\n" -"Language: pt_BR\n" -"Plural-Forms: nplurals=2; plural=(n > 1);\n" - -#: qtlib/about_box.py:29 -msgid "About {}" -msgstr "Sobre o {}" - -#: qtlib/about_box.py:49 -msgid "Version {}" -msgstr "Versão {}" - -#: qtlib/about_box.py:53 -msgid "Copyright Hardcoded Software 2014" -msgstr "Copyright Hardcoded Software 2014" - -#: qtlib/error_report_dialog.py:39 -msgid "Error Report" -msgstr "Relatório de Erro" - -#: qtlib/error_report_dialog.py:43 -msgid "Something went wrong. How about reporting the error?" -msgstr "Algo deu errado. Deseja relatar o erro?" - -#: qtlib/error_report_dialog.py:49 -msgid "" -"Error reports should be reported as Github issues. You can copy the error traceback above and paste it in a new issue (bonus point if you run a search to make sure the issue doesn't already exist). What usually really helps is if you add a description of how you got the error. Thanks!\n" -"\n" -"Although the application should continue to run after this error, it may be in an unstable state, so it is recommended that you restart the application." -msgstr "" -"Os erros devem ser relatados como problemas no Github. Copie o código de rastreamento acima e cole-o em um problema novo (pontos bônus caso você busque o erro, certificando-se de que ele ainda não exista). O que mais ajuda é adicionar uma descrição de como o erro ocorreu. Obrigado!\n" -"\n" -"Embora o aplicativo continue a funcionar após esse erro, ele pode estar “instável”. É recomendável reiniciá-lo." - -#: qtlib/error_report_dialog.py:64 -msgid "Close" -msgstr "Fechar" - -#: qtlib/error_report_dialog.py:68 -msgid "Go to Github" -msgstr "Ir para o Github" - -#: qtlib/preferences.py:17 -msgid "English" -msgstr "Inglês" - -#: qtlib/preferences.py:18 -msgid "French" -msgstr "Francês" - -#: qtlib/preferences.py:19 -msgid "German" -msgstr "Alemão" - -#: qtlib/preferences.py:20 -msgid "Chinese (Simplified)" -msgstr "Chinês (Simplificado)" - -#: qtlib/preferences.py:21 -msgid "Czech" -msgstr "Tcheco" - -#: qtlib/preferences.py:22 -msgid "Italian" -msgstr "Italiano" - -#: qtlib/preferences.py:23 -msgid "Armenian" -msgstr "Armênio" - -#: qtlib/preferences.py:24 -msgid "Russian" -msgstr "Russo" - -#: qtlib/preferences.py:25 -msgid "Ukrainian" -msgstr "Ucraniano" - -#: qtlib/preferences.py:26 -msgid "Dutch" -msgstr "Holandês" - -#: qtlib/preferences.py:27 -msgid "Brazilian" -msgstr "Português Brasileiro" - -#: qtlib/preferences.py:28 -msgid "Spanish" -msgstr "Espanhol" - -#: qtlib/preferences.py:29 -msgid "Vietnamese" -msgstr "Vietnamita" - -#: qtlib/recent.py:53 -msgid "Clear List" -msgstr "Limpar Lista" - -#: qtlib/search_edit.py:41 -msgid "Search..." -msgstr "Buscar…" diff --git a/qtlib/locale/qtlib.pot b/qtlib/locale/qtlib.pot deleted file mode 100644 index d554b8f2..00000000 --- a/qtlib/locale/qtlib.pot +++ /dev/null @@ -1,101 +0,0 @@ - -msgid "" -msgstr "" -"Content-Type: text/plain; charset=utf-8\n" -"Content-Transfer-Encoding: utf-8\n" - -#: qtlib/about_box.py:29 -msgid "About {}" -msgstr "" - -#: qtlib/about_box.py:49 -msgid "Version {}" -msgstr "" - -#: qtlib/about_box.py:53 -msgid "Copyright Hardcoded Software 2014" -msgstr "" - -#: qtlib/error_report_dialog.py:39 -msgid "Error Report" -msgstr "" - -#: qtlib/error_report_dialog.py:43 -msgid "Something went wrong. How about reporting the error?" -msgstr "" - -#: qtlib/error_report_dialog.py:49 -msgid "" -"Error reports should be reported as Github issues. You can copy the error traceback above and paste it in a new issue (bonus point if you run a search to make sure the issue doesn't already exist). What usually really helps is if you add a description of how you got the error. Thanks!\n" -"\n" -"Although the application should continue to run after this error, it may be in an unstable state, so it is recommended that you restart the application." -msgstr "" - -#: qtlib/error_report_dialog.py:64 -msgid "Close" -msgstr "" - -#: qtlib/error_report_dialog.py:68 -msgid "Go to Github" -msgstr "" - -#: qtlib/preferences.py:17 -msgid "English" -msgstr "" - -#: qtlib/preferences.py:18 -msgid "French" -msgstr "" - -#: qtlib/preferences.py:19 -msgid "German" -msgstr "" - -#: qtlib/preferences.py:20 -msgid "Chinese (Simplified)" -msgstr "" - -#: qtlib/preferences.py:21 -msgid "Czech" -msgstr "" - -#: qtlib/preferences.py:22 -msgid "Italian" -msgstr "" - -#: qtlib/preferences.py:23 -msgid "Armenian" -msgstr "" - -#: qtlib/preferences.py:24 -msgid "Russian" -msgstr "" - -#: qtlib/preferences.py:25 -msgid "Ukrainian" -msgstr "" - -#: qtlib/preferences.py:26 -msgid "Dutch" -msgstr "" - -#: qtlib/preferences.py:27 -msgid "Brazilian" -msgstr "" - -#: qtlib/preferences.py:28 -msgid "Spanish" -msgstr "" - -#: qtlib/preferences.py:29 -msgid "Vietnamese" -msgstr "" - -#: qtlib/recent.py:53 -msgid "Clear List" -msgstr "" - -#: qtlib/search_edit.py:41 -msgid "Search..." -msgstr "" - diff --git a/qtlib/locale/ru/LC_MESSAGES/qtlib.po b/qtlib/locale/ru/LC_MESSAGES/qtlib.po deleted file mode 100644 index 25122423..00000000 --- a/qtlib/locale/ru/LC_MESSAGES/qtlib.po +++ /dev/null @@ -1,110 +0,0 @@ -# Translators: -# Igor Fokusov , 2015 -msgid "" -msgstr "" -"Project-Id-Version: hscommon\n" -"PO-Revision-Date: 2015-04-14 21:16+0000\n" -"Last-Translator: Igor Fokusov \n" -"Language-Team: Russian (http://www.transifex.com/p/hscommon/language/ru/)\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: utf-8\n" -"Language: ru\n" -"Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n%100>=11 && n%100<=14)? 2 : 3);\n" - -#: qtlib/about_box.py:29 -msgid "About {}" -msgstr "О {}" - -#: qtlib/about_box.py:49 -msgid "Version {}" -msgstr "Версия {}" - -#: qtlib/about_box.py:53 -msgid "Copyright Hardcoded Software 2014" -msgstr "Copyright Hardcoded Software 2014" - -#: qtlib/error_report_dialog.py:39 -msgid "Error Report" -msgstr "Сообщение об ошибке" - -#: qtlib/error_report_dialog.py:43 -msgid "Something went wrong. How about reporting the error?" -msgstr "Что-то пошло не так. Хотите отправить отчёт об ошибке?" - -#: qtlib/error_report_dialog.py:49 -msgid "" -"Error reports should be reported as Github issues. You can copy the error traceback above and paste it in a new issue (bonus point if you run a search to make sure the issue doesn't already exist). What usually really helps is if you add a description of how you got the error. Thanks!\n" -"\n" -"Although the application should continue to run after this error, it may be in an unstable state, so it is recommended that you restart the application." -msgstr "" -"Отчеты об ошибках нужно отправлять в Github issues проекта. Скопируйте текст ошибки выше и вставьте в созданную заметку о проблеме (перед этим желательно проверить - не создано ли уже такой проблемы до вас). Также нам очень поможет краткое описание как вы получили такую ошибку. Спасибо!\n" -"\n" -"В принципе, программа может продолжать работу, но стабильная работа не гарантируется. Поэтому желательно перезапустить программу." - -#: qtlib/error_report_dialog.py:64 -msgid "Close" -msgstr "Закрыть" - -#: qtlib/error_report_dialog.py:68 -msgid "Go to Github" -msgstr "Перейти на Github" - -#: qtlib/preferences.py:17 -msgid "English" -msgstr "Английский" - -#: qtlib/preferences.py:18 -msgid "French" -msgstr "Французский" - -#: qtlib/preferences.py:19 -msgid "German" -msgstr "Немецкий" - -#: qtlib/preferences.py:20 -msgid "Chinese (Simplified)" -msgstr "Китайский (упрощенный)" - -#: qtlib/preferences.py:21 -msgid "Czech" -msgstr "Чешский" - -#: qtlib/preferences.py:22 -msgid "Italian" -msgstr "Итальянский" - -#: qtlib/preferences.py:23 -msgid "Armenian" -msgstr "Армянский" - -#: qtlib/preferences.py:24 -msgid "Russian" -msgstr "Русский" - -#: qtlib/preferences.py:25 -msgid "Ukrainian" -msgstr "Украинский" - -#: qtlib/preferences.py:26 -msgid "Dutch" -msgstr "Голландский" - -#: qtlib/preferences.py:27 -msgid "Brazilian" -msgstr "Бразильский" - -#: qtlib/preferences.py:28 -msgid "Spanish" -msgstr "Испанский" - -#: qtlib/preferences.py:29 -msgid "Vietnamese" -msgstr "Вьетнамский" - -#: qtlib/recent.py:53 -msgid "Clear List" -msgstr "Очистить список" - -#: qtlib/search_edit.py:41 -msgid "Search..." -msgstr "Искать..." diff --git a/qtlib/locale/uk/LC_MESSAGES/qtlib.po b/qtlib/locale/uk/LC_MESSAGES/qtlib.po deleted file mode 100755 index 768ceb81..00000000 --- a/qtlib/locale/uk/LC_MESSAGES/qtlib.po +++ /dev/null @@ -1,106 +0,0 @@ -# Translators: -msgid "" -msgstr "" -"Project-Id-Version: hscommon\n" -"PO-Revision-Date: 2014-03-30 14:55+0000\n" -"Last-Translator: Virgil Dupras \n" -"Language-Team: Ukrainian (http://www.transifex.com/p/hscommon/language/uk/)\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: utf-8\n" -"Language: uk\n" -"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" - -#: qtlib/about_box.py:29 -msgid "About {}" -msgstr "" - -#: qtlib/about_box.py:49 -msgid "Version {}" -msgstr "" - -#: qtlib/about_box.py:53 -msgid "Copyright Hardcoded Software 2014" -msgstr "" - -#: qtlib/error_report_dialog.py:39 -msgid "Error Report" -msgstr "Повідомлення про помилки" - -#: qtlib/error_report_dialog.py:43 -msgid "Something went wrong. How about reporting the error?" -msgstr "" - -#: qtlib/error_report_dialog.py:49 -msgid "" -"Error reports should be reported as Github issues. You can copy the error traceback above and paste it in a new issue (bonus point if you run a search to make sure the issue doesn't already exist). What usually really helps is if you add a description of how you got the error. Thanks!\n" -"\n" -"Although the application should continue to run after this error, it may be in an unstable state, so it is recommended that you restart the application." -msgstr "" - -#: qtlib/error_report_dialog.py:64 -msgid "Close" -msgstr "" - -#: qtlib/error_report_dialog.py:68 -msgid "Go to Github" -msgstr "" - -#: qtlib/preferences.py:17 -msgid "English" -msgstr "" - -#: qtlib/preferences.py:18 -msgid "French" -msgstr "" - -#: qtlib/preferences.py:19 -msgid "German" -msgstr "" - -#: qtlib/preferences.py:20 -msgid "Chinese (Simplified)" -msgstr "" - -#: qtlib/preferences.py:21 -msgid "Czech" -msgstr "" - -#: qtlib/preferences.py:22 -msgid "Italian" -msgstr "" - -#: qtlib/preferences.py:23 -msgid "Armenian" -msgstr "" - -#: qtlib/preferences.py:24 -msgid "Russian" -msgstr "" - -#: qtlib/preferences.py:25 -msgid "Ukrainian" -msgstr "" - -#: qtlib/preferences.py:26 -msgid "Dutch" -msgstr "" - -#: qtlib/preferences.py:27 -msgid "Brazilian" -msgstr "" - -#: qtlib/preferences.py:28 -msgid "Spanish" -msgstr "" - -#: qtlib/preferences.py:29 -msgid "Vietnamese" -msgstr "" - -#: qtlib/recent.py:53 -msgid "Clear List" -msgstr "Очистити список" - -#: qtlib/search_edit.py:41 -msgid "Search..." -msgstr "Шукати..." diff --git a/qtlib/locale/vi/LC_MESSAGES/qtlib.po b/qtlib/locale/vi/LC_MESSAGES/qtlib.po deleted file mode 100644 index 07359c04..00000000 --- a/qtlib/locale/vi/LC_MESSAGES/qtlib.po +++ /dev/null @@ -1,106 +0,0 @@ -# Translators: -msgid "" -msgstr "" -"Project-Id-Version: hscommon\n" -"PO-Revision-Date: 2014-03-30 14:55+0000\n" -"Last-Translator: Virgil Dupras \n" -"Language-Team: Vietnamese (http://www.transifex.com/p/hscommon/language/vi/)\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: utf-8\n" -"Language: vi\n" -"Plural-Forms: nplurals=1; plural=0;\n" - -#: qtlib/about_box.py:29 -msgid "About {}" -msgstr "" - -#: qtlib/about_box.py:49 -msgid "Version {}" -msgstr "" - -#: qtlib/about_box.py:53 -msgid "Copyright Hardcoded Software 2014" -msgstr "" - -#: qtlib/error_report_dialog.py:39 -msgid "Error Report" -msgstr "" - -#: qtlib/error_report_dialog.py:43 -msgid "Something went wrong. How about reporting the error?" -msgstr "" - -#: qtlib/error_report_dialog.py:49 -msgid "" -"Error reports should be reported as Github issues. You can copy the error traceback above and paste it in a new issue (bonus point if you run a search to make sure the issue doesn't already exist). What usually really helps is if you add a description of how you got the error. Thanks!\n" -"\n" -"Although the application should continue to run after this error, it may be in an unstable state, so it is recommended that you restart the application." -msgstr "" - -#: qtlib/error_report_dialog.py:64 -msgid "Close" -msgstr "" - -#: qtlib/error_report_dialog.py:68 -msgid "Go to Github" -msgstr "" - -#: qtlib/preferences.py:17 -msgid "English" -msgstr "" - -#: qtlib/preferences.py:18 -msgid "French" -msgstr "" - -#: qtlib/preferences.py:19 -msgid "German" -msgstr "" - -#: qtlib/preferences.py:20 -msgid "Chinese (Simplified)" -msgstr "" - -#: qtlib/preferences.py:21 -msgid "Czech" -msgstr "" - -#: qtlib/preferences.py:22 -msgid "Italian" -msgstr "" - -#: qtlib/preferences.py:23 -msgid "Armenian" -msgstr "" - -#: qtlib/preferences.py:24 -msgid "Russian" -msgstr "" - -#: qtlib/preferences.py:25 -msgid "Ukrainian" -msgstr "" - -#: qtlib/preferences.py:26 -msgid "Dutch" -msgstr "" - -#: qtlib/preferences.py:27 -msgid "Brazilian" -msgstr "" - -#: qtlib/preferences.py:28 -msgid "Spanish" -msgstr "" - -#: qtlib/preferences.py:29 -msgid "Vietnamese" -msgstr "" - -#: qtlib/recent.py:53 -msgid "Clear List" -msgstr "" - -#: qtlib/search_edit.py:41 -msgid "Search..." -msgstr "" diff --git a/qtlib/locale/zh_CN/LC_MESSAGES/qtlib.po b/qtlib/locale/zh_CN/LC_MESSAGES/qtlib.po deleted file mode 100644 index 32ff6967..00000000 --- a/qtlib/locale/zh_CN/LC_MESSAGES/qtlib.po +++ /dev/null @@ -1,106 +0,0 @@ -# Translators: -msgid "" -msgstr "" -"Project-Id-Version: hscommon\n" -"PO-Revision-Date: 2014-03-30 14:55+0000\n" -"Last-Translator: Virgil Dupras \n" -"Language-Team: Chinese (China) (http://www.transifex.com/p/hscommon/language/zh_CN/)\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: utf-8\n" -"Language: zh_CN\n" -"Plural-Forms: nplurals=1; plural=0;\n" - -#: qtlib/about_box.py:29 -msgid "About {}" -msgstr "" - -#: qtlib/about_box.py:49 -msgid "Version {}" -msgstr "" - -#: qtlib/about_box.py:53 -msgid "Copyright Hardcoded Software 2014" -msgstr "" - -#: qtlib/error_report_dialog.py:39 -msgid "Error Report" -msgstr "" - -#: qtlib/error_report_dialog.py:43 -msgid "Something went wrong. How about reporting the error?" -msgstr "" - -#: qtlib/error_report_dialog.py:49 -msgid "" -"Error reports should be reported as Github issues. You can copy the error traceback above and paste it in a new issue (bonus point if you run a search to make sure the issue doesn't already exist). What usually really helps is if you add a description of how you got the error. Thanks!\n" -"\n" -"Although the application should continue to run after this error, it may be in an unstable state, so it is recommended that you restart the application." -msgstr "" - -#: qtlib/error_report_dialog.py:64 -msgid "Close" -msgstr "" - -#: qtlib/error_report_dialog.py:68 -msgid "Go to Github" -msgstr "" - -#: qtlib/preferences.py:17 -msgid "English" -msgstr "英语" - -#: qtlib/preferences.py:18 -msgid "French" -msgstr "法语" - -#: qtlib/preferences.py:19 -msgid "German" -msgstr "德语" - -#: qtlib/preferences.py:20 -msgid "Chinese (Simplified)" -msgstr "简体中文" - -#: qtlib/preferences.py:21 -msgid "Czech" -msgstr "" - -#: qtlib/preferences.py:22 -msgid "Italian" -msgstr "" - -#: qtlib/preferences.py:23 -msgid "Armenian" -msgstr "" - -#: qtlib/preferences.py:24 -msgid "Russian" -msgstr "" - -#: qtlib/preferences.py:25 -msgid "Ukrainian" -msgstr "" - -#: qtlib/preferences.py:26 -msgid "Dutch" -msgstr "" - -#: qtlib/preferences.py:27 -msgid "Brazilian" -msgstr "" - -#: qtlib/preferences.py:28 -msgid "Spanish" -msgstr "" - -#: qtlib/preferences.py:29 -msgid "Vietnamese" -msgstr "" - -#: qtlib/recent.py:53 -msgid "Clear List" -msgstr "清空列表" - -#: qtlib/search_edit.py:41 -msgid "Search..." -msgstr "" diff --git a/qtlib/preferences.py b/qtlib/preferences.py deleted file mode 100644 index 15a506e4..00000000 --- a/qtlib/preferences.py +++ /dev/null @@ -1,129 +0,0 @@ -# Created By: Virgil Dupras -# Created On: 2009-05-03 -# Copyright 2015 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at -# http://www.gnu.org/licenses/gpl-3.0.html - -from PyQt5.QtCore import Qt, QSettings, QRect - -from hscommon.trans import trget -from hscommon.util import tryint - -tr = trget('qtlib') - -LANGNAMES = { - 'en': tr("English"), - 'fr': tr("French"), - 'de': tr("German"), - 'zh_CN': tr("Chinese (Simplified)"), - 'cs': tr("Czech"), - 'it': tr("Italian"), - 'hy': tr("Armenian"), - 'ko': tr("Korean"), - 'ru': tr("Russian"), - 'uk': tr("Ukrainian"), - 'nl': tr('Dutch'), - 'pl_PL': tr("Polish"), - 'pt_BR': tr("Brazilian"), - 'es': tr("Spanish"), - 'vi': tr("Vietnamese"), -} - -def normalize_for_serialization(v): - # QSettings doesn't consider set/tuple as "native" typs for serialization, so if we don't - # change them into a list, we get a weird serialized QVariant value which isn't a very - # "portable" value. - if isinstance(v, (set, tuple)): - v = list(v) - if isinstance(v, list): - v = [normalize_for_serialization(item) for item in v] - return v - -def adjust_after_deserialization(v): - # In some cases, when reading from prefs, we end up with strings that are supposed to be - # bool or int. Convert these. - if isinstance(v, list): - return [adjust_after_deserialization(sub) for sub in v] - if isinstance(v, str): - # might be bool or int, try them - if v == 'true': - return True - elif v == 'false': - return False - else: - return tryint(v, v) - return v - -# About QRect conversion: -# I think Qt supports putting basic structures like QRect directly in QSettings, but I prefer not -# to rely on it and stay with generic structures. - -class Preferences: - def __init__(self): - self.reset() - self._settings = QSettings() - - def _load_values(self, settings, get): - pass - - def get_rect(self, name, default=None): - r = self.get_value(name, default) - if r is not None: - return QRect(*r) - else: - return None - - def get_value(self, name, default=None): - if self._settings.contains(name): - result = adjust_after_deserialization(self._settings.value(name)) - if result is not None: - return result - else: - # If result is None, but still present in self._settings, it usually means a value - # like "@Invalid". - return default - else: - return default - - def load(self): - self.reset() - self._load_values(self._settings) - - def reset(self): - pass - - def _save_values(self, settings, set_): - pass - - def save(self): - self._save_values(self._settings) - self._settings.sync() - - def set_rect(self, name, r): - if isinstance(r, QRect): - rectAsList = [r.x(), r.y(), r.width(), r.height()] - self.set_value(name, rectAsList) - - def set_value(self, name, value): - self._settings.setValue(name, normalize_for_serialization(value)) - - def saveGeometry(self, name, widget): - # We save geometry under a 5-sized int array: first item is a flag for whether the widget - # is maximized and the other 4 are (x, y, w, h). - m = 1 if widget.isMaximized() else 0 - r = widget.geometry() - rectAsList = [r.x(), r.y(), r.width(), r.height()] - self.set_value(name, [m] + rectAsList) - - def restoreGeometry(self, name, widget): - l = self.get_value(name) - if l and len(l) == 5: - m, x, y, w, h = l - if m: - widget.setWindowState(Qt.WindowMaximized) - else: - r = QRect(x, y, w, h) - widget.setGeometry(r) - diff --git a/qtlib/progress_window.py b/qtlib/progress_window.py deleted file mode 100644 index af6c6c65..00000000 --- a/qtlib/progress_window.py +++ /dev/null @@ -1,48 +0,0 @@ -# Created By: Virgil Dupras -# Created On: 2013-07-01 -# Copyright 2015 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at -# http://www.gnu.org/licenses/gpl-3.0.html - -from PyQt5.QtCore import Qt, QTimer -from PyQt5.QtWidgets import QProgressDialog - -class ProgressWindow(QProgressDialog): - def __init__(self, parent, model, **kwargs): - flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint - super().__init__('', "Cancel", 0, 100, parent, flags, **kwargs) - self.model = model - model.view = self - # We don't have access to QProgressDialog's labels directly, so we se the model label's view - # to self and we'll refresh them together. - self.model.jobdesc_textfield.view = self - self.model.progressdesc_textfield.view = self - self.setModal(True) - self.setAutoReset(False) - self.setAutoClose(False) - self._timer = QTimer(self) - self._timer.timeout.connect(self.model.pulse) - - # --- Callbacks - def refresh(self): # Labels - self.setWindowTitle(self.model.jobdesc_textfield.text) - self.setLabelText(self.model.progressdesc_textfield.text) - - def set_progress(self, last_progress): - self.setValue(last_progress) - - def show(self): - self.reset() - super().show() - self.canceled.connect(self.model.cancel) - self._timer.start(500) - - def close(self): - self._timer.stop() - # For some weird reason, canceled() signal is sent upon close, whether the user canceled - # or not. If we don't want a false cancellation, we have to disconnect it. - self.canceled.disconnect() - super().close() - diff --git a/qtlib/radio_box.py b/qtlib/radio_box.py deleted file mode 100644 index feabf357..00000000 --- a/qtlib/radio_box.py +++ /dev/null @@ -1,88 +0,0 @@ -# Created On: 2010-06-02 -# Copyright 2015 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at -# http://www.gnu.org/licenses/gpl-3.0.html - -from PyQt5.QtCore import pyqtSignal -from PyQt5.QtWidgets import QWidget, QHBoxLayout, QRadioButton - -from .util import horizontalSpacer - -class RadioBox(QWidget): - def __init__(self, parent=None, items=None, spread=True, **kwargs): - # If spread is False, insert a spacer in the layout so that the items don't use all the - # space they're given but rather align left. - if items is None: - items = [] - super().__init__(parent, **kwargs) - self._buttons = [] - self._labels = items - self._selected_index = 0 - self._spacer = horizontalSpacer() if not spread else None - self._layout = QHBoxLayout(self) - self._update_buttons() - - #--- Private - def _update_buttons(self): - if self._spacer is not None: - self._layout.removeItem(self._spacer) - to_remove = self._buttons[len(self._labels):] - for button in to_remove: - self._layout.removeWidget(button) - button.setParent(None) - del self._buttons[len(self._labels):] - to_add = self._labels[len(self._buttons):] - for _ in to_add: - button = QRadioButton(self) - self._buttons.append(button) - self._layout.addWidget(button) - button.toggled.connect(self.buttonToggled) - if self._spacer is not None: - self._layout.addItem(self._spacer) - if not self._buttons: - return - for button, label in zip(self._buttons, self._labels): - button.setText(label) - self._update_selection() - - def _update_selection(self): - self._selected_index = max(0, min(self._selected_index, len(self._buttons)-1)) - selected = self._buttons[self._selected_index] - selected.setChecked(True) - - #--- Event Handlers - def buttonToggled(self): - for i, button in enumerate(self._buttons): - if button.isChecked(): - self._selected_index = i - self.itemSelected.emit(i) - break - - #--- Signals - itemSelected = pyqtSignal(int) - - #--- Properties - @property - def buttons(self): - return self._buttons[:] - - @property - def items(self): - return self._labels[:] - - @items.setter - def items(self, value): - self._labels = value - self._update_buttons() - - @property - def selected_index(self): - return self._selected_index - - @selected_index.setter - def selected_index(self, value): - self._selected_index = value - self._update_selection() - diff --git a/qtlib/recent.py b/qtlib/recent.py deleted file mode 100644 index e1524e54..00000000 --- a/qtlib/recent.py +++ /dev/null @@ -1,95 +0,0 @@ -# Created By: Virgil Dupras -# Created On: 2009-11-12 -# Copyright 2015 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at -# http://www.gnu.org/licenses/gpl-3.0.html - -from collections import namedtuple - -from PyQt5.QtCore import pyqtSignal, QObject -from PyQt5.QtWidgets import QAction - -from hscommon.trans import trget -from hscommon.util import dedupe - -tr = trget('qtlib') - -MenuEntry = namedtuple('MenuEntry', 'menu fixedItemCount') - -class Recent(QObject): - def __init__(self, app, prefName, maxItemCount=10, **kwargs): - super().__init__(**kwargs) - self._app = app - self._menuEntries = [] - self._prefName = prefName - self._maxItemCount = maxItemCount - self._items = [] - self._loadFromPrefs() - - self._app.willSavePrefs.connect(self._saveToPrefs) - - #--- Private - def _loadFromPrefs(self): - items = getattr(self._app.prefs, self._prefName) - if not isinstance(items, list): - items = [] - self._items = items - - def _insertItem(self, item): - self._items = dedupe([item] + self._items)[:self._maxItemCount] - - def _refreshMenu(self, menuEntry): - menu, fixedItemCount = menuEntry - for action in menu.actions()[fixedItemCount:]: - menu.removeAction(action) - for item in self._items: - action = QAction(item, menu) - action.setData(item) - action.triggered.connect(self.menuItemWasClicked) - menu.addAction(action) - menu.addSeparator() - action = QAction(tr("Clear List"), menu) - action.triggered.connect(self.clear) - menu.addAction(action) - - def _refreshAllMenus(self): - for menuEntry in self._menuEntries: - self._refreshMenu(menuEntry) - - def _saveToPrefs(self): - setattr(self._app.prefs, self._prefName, self._items) - - #--- Public - def addMenu(self, menu): - menuEntry = MenuEntry(menu, len(menu.actions())) - self._menuEntries.append(menuEntry) - self._refreshMenu(menuEntry) - - def clear(self): - self._items = [] - self._refreshAllMenus() - self.itemsChanged.emit() - - def insertItem(self, item): - self._insertItem(str(item)) - self._refreshAllMenus() - self.itemsChanged.emit() - - def isEmpty(self): - return not bool(self._items) - - #--- Event Handlers - def menuItemWasClicked(self): - action = self.sender() - if action is not None: - item = action.data() - self.mustOpenItem.emit(item) - self._refreshAllMenus() - - #--- Signals - mustOpenItem = pyqtSignal(str) - itemsChanged = pyqtSignal() - - diff --git a/qtlib/search_edit.py b/qtlib/search_edit.py deleted file mode 100644 index a8559784..00000000 --- a/qtlib/search_edit.py +++ /dev/null @@ -1,98 +0,0 @@ -# Created By: Virgil Dupras -# Created On: 2009-12-10 -# Copyright 2015 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at -# http://www.gnu.org/licenses/gpl-3.0.html - -from PyQt5.QtCore import pyqtSignal, Qt -from PyQt5.QtGui import QIcon, QPixmap, QPainter, QPalette -from PyQt5.QtWidgets import QToolButton, QLineEdit, QStyle, QStyleOptionFrame - -from hscommon.trans import trget - -tr = trget('qtlib') - -# IMPORTANT: For this widget to work propertly, you have to add "search_clear_13" from the -# "images" folder in your resources. - -class LineEditButton(QToolButton): - def __init__(self, parent, **kwargs): - super().__init__(parent, **kwargs) - pixmap = QPixmap(':/search_clear_13') - self.setIcon(QIcon(pixmap)) - self.setIconSize(pixmap.size()) - self.setCursor(Qt.ArrowCursor) - self.setPopupMode(QToolButton.InstantPopup) - stylesheet = "QToolButton { border: none; padding: 0px; }" - self.setStyleSheet(stylesheet) - - -class SearchEdit(QLineEdit): - def __init__(self, parent=None, immediate=False, **kwargs): - # immediate: send searchChanged signals at each keystroke. - super().__init__(parent, **kwargs) - self._clearButton = LineEditButton(self) - frameWidth = self.style().pixelMetric(QStyle.PM_DefaultFrameWidth) - paddingRight = self._clearButton.sizeHint().width() + frameWidth + 1 - stylesheet = "QLineEdit {{ padding-right:{0}px; }}".format(paddingRight) - self.setStyleSheet(stylesheet) - self.inactiveText = tr("Search...") - self.immediate = immediate - self._updateClearButton(self.text()) - - self._clearButton.clicked.connect(self._clearSearch) - self.returnPressed.connect(self._returnPressed) - self.textChanged.connect(self._textChanged) - - #--- Private - def _clearSearch(self): - self.clear() - self.searchChanged.emit() - - def _updateClearButton(self, text): - self._clearButton.setVisible(bool(text)) - - #--- QLineEdit overrides - def resizeEvent(self, event): - frameWidth = self.style().pixelMetric(QStyle.PM_DefaultFrameWidth) - rect = self.rect() - rightHint = self._clearButton.sizeHint() - rightX = rect.right() - frameWidth - rightHint.width() - rightY = (rect.bottom() - rightHint.height()) // 2 - self._clearButton.move(rightX, rightY) - - def paintEvent(self, event): - QLineEdit.paintEvent(self, event) - if not bool(self.text()) and self.inactiveText and not self.hasFocus(): - panel = QStyleOptionFrame() - self.initStyleOption(panel) - textRect = self.style().subElementRect(QStyle.SE_LineEditContents, panel, self) - leftMargin = 2 - rightMargin = self._clearButton.iconSize().width() - textRect.adjust(leftMargin, 0, -rightMargin, 0) - painter = QPainter(self) - disabledColor = self.palette().brush(QPalette.Disabled, QPalette.Text).color() - painter.setPen(disabledColor) - painter.drawText(textRect, Qt.AlignLeft|Qt.AlignVCenter, self.inactiveText) - - def keyPressEvent(self, event): - key = event.key() - if key == Qt.Key_Escape: - self._clearSearch() - else: - QLineEdit.keyPressEvent(self, event) - - #--- Event Handlers - def _returnPressed(self): - if not self.immediate: - self.searchChanged.emit() - - def _textChanged(self, text): - self._updateClearButton(text) - if self.immediate: - self.searchChanged.emit() - - #--- Signals - searchChanged = pyqtSignal() # Emitted when return is pressed or when the test is cleared diff --git a/qtlib/selectable_list.py b/qtlib/selectable_list.py deleted file mode 100644 index cb789298..00000000 --- a/qtlib/selectable_list.py +++ /dev/null @@ -1,98 +0,0 @@ -# Created By: Virgil Dupras -# Created On: 2011-09-06 -# Copyright 2015 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at -# http://www.gnu.org/licenses/gpl-3.0.html - -from PyQt5.QtCore import Qt, QAbstractListModel, QItemSelection, QItemSelectionModel - -class SelectableList(QAbstractListModel): - def __init__(self, model, view, **kwargs): - super().__init__(**kwargs) - self._updating = False - self.view = view - self.model = model - self.view.setModel(self) - self.model.view = self - - #--- Override - def data(self, index, role): - if not index.isValid(): - return None - # We need EditRole for QComboBoxes with setEditable(True) - if role in {Qt.DisplayRole, Qt.EditRole}: - return self.model[index.row()] - return None - - def rowCount(self, index): - if index.isValid(): - return 0 - return len(self.model) - - #--- Virtual - def _updateSelection(self): - raise NotImplementedError() - - def _restoreSelection(self): - raise NotImplementedError() - - #--- model --> view - def refresh(self): - self._updating = True - self.beginResetModel() - self.endResetModel() - self._updating = False - self._restoreSelection() - - def update_selection(self): - self._restoreSelection() - -class ComboboxModel(SelectableList): - def __init__(self, model, view, **kwargs): - super().__init__(model, view, **kwargs) - self.view.currentIndexChanged[int].connect(self.selectionChanged) - - #--- Override - def _updateSelection(self): - index = self.view.currentIndex() - if index != self.model.selected_index: - self.model.select(index) - - def _restoreSelection(self): - index = self.model.selected_index - if index is not None: - self.view.setCurrentIndex(index) - - #--- Events - def selectionChanged(self, index): - if not self._updating: - self._updateSelection() - -class ListviewModel(SelectableList): - def __init__(self, model, view, **kwargs): - super().__init__(model, view, **kwargs) - self.view.selectionModel().selectionChanged[(QItemSelection, QItemSelection)].connect( - self.selectionChanged) - - #--- Override - def _updateSelection(self): - newIndexes = [modelIndex.row() for modelIndex in self.view.selectionModel().selectedRows()] - if newIndexes != self.model.selected_indexes: - self.model.select(newIndexes) - - def _restoreSelection(self): - newSelection = QItemSelection() - for index in self.model.selected_indexes: - newSelection.select(self.createIndex(index, 0), self.createIndex(index, 0)) - self.view.selectionModel().select(newSelection, QItemSelectionModel.ClearAndSelect) - if len(newSelection.indexes()): - currentIndex = newSelection.indexes()[0] - self.view.selectionModel().setCurrentIndex(currentIndex, QItemSelectionModel.Current) - self.view.scrollTo(currentIndex) - #--- Events - def selectionChanged(self, index): - if not self._updating: - self._updateSelection() - diff --git a/qtlib/table.py b/qtlib/table.py deleted file mode 100644 index 251842cd..00000000 --- a/qtlib/table.py +++ /dev/null @@ -1,152 +0,0 @@ -# Created By: Virgil Dupras -# Created On: 2009-11-01 -# Copyright 2015 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at -# http://www.gnu.org/licenses/gpl-3.0.html - -from PyQt5.QtCore import Qt, QAbstractTableModel, QModelIndex, QItemSelectionModel, QItemSelection - -from .column import Columns - -class Table(QAbstractTableModel): - # Flags you want when index.isValid() is False. In those cases, _getFlags() is never called. - INVALID_INDEX_FLAGS = Qt.ItemIsEnabled - COLUMNS = [] - - def __init__(self, model, view, **kwargs): - super().__init__(**kwargs) - self.model = model - self.view = view - self.view.setModel(self) - self.model.view = self - if hasattr(self.model, 'columns'): - self.columns = Columns(self.model.columns, self.COLUMNS, view.horizontalHeader()) - - self.view.selectionModel().selectionChanged[(QItemSelection, QItemSelection)].connect(self.selectionChanged) - - def _updateModelSelection(self): - # Takes the selection on the view's side and update the model with it. - # an _updateViewSelection() call will normally result in an _updateModelSelection() call. - # to avoid infinite loops, we check that the selection will actually change before calling - # model.select() - newIndexes = [modelIndex.row() for modelIndex in self.view.selectionModel().selectedRows()] - if newIndexes != self.model.selected_indexes: - self.model.select(newIndexes) - - def _updateViewSelection(self): - # Takes the selection on the model's side and update the view with it. - newSelection = QItemSelection() - columnCount = self.columnCount(QModelIndex()) - for index in self.model.selected_indexes: - newSelection.select(self.createIndex(index, 0), self.createIndex(index, columnCount-1)) - self.view.selectionModel().select(newSelection, QItemSelectionModel.ClearAndSelect) - if len(newSelection.indexes()): - currentIndex = newSelection.indexes()[0] - self.view.selectionModel().setCurrentIndex(currentIndex, QItemSelectionModel.Current) - self.view.scrollTo(currentIndex) - - #--- Data Model methods - # Virtual - def _getData(self, row, column, role): - if role in (Qt.DisplayRole, Qt.EditRole): - attrname = column.name - return row.get_cell_value(attrname) - elif role == Qt.TextAlignmentRole: - return column.alignment - return None - - # Virtual - def _getFlags(self, row, column): - flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable - if row.can_edit_cell(column.name): - flags |= Qt.ItemIsEditable - return flags - - # Virtual - def _setData(self, row, column, value, role): - if role == Qt.EditRole: - attrname = column.name - if attrname == 'from': - attrname = 'from_' - setattr(row, attrname, value) - return True - return False - - def columnCount(self, index): - return self.model.columns.columns_count() - - def data(self, index, role): - if not index.isValid(): - return None - row = self.model[index.row()] - column = self.model.columns.column_by_index(index.column()) - return self._getData(row, column, role) - - def flags(self, index): - if not index.isValid(): - return self.INVALID_INDEX_FLAGS - row = self.model[index.row()] - column = self.model.columns.column_by_index(index.column()) - return self._getFlags(row, column) - - def headerData(self, section, orientation, role): - if orientation != Qt.Horizontal: - return None - if section >= self.model.columns.columns_count(): - return None - column = self.model.columns.column_by_index(section) - if role == Qt.DisplayRole: - return column.display - elif role == Qt.TextAlignmentRole: - return column.alignment - else: - return None - - def revert(self): - self.model.cancel_edits() - - def rowCount(self, index): - if index.isValid(): - return 0 - return len(self.model) - - def setData(self, index, value, role): - if not index.isValid(): - return False - row = self.model[index.row()] - column = self.model.columns.column_by_index(index.column()) - return self._setData(row, column, value, role) - - def sort(self, section, order): - column = self.model.columns.column_by_index(section) - attrname = column.name - self.model.sort_by(attrname, desc=order==Qt.DescendingOrder) - - def submit(self): - self.model.save_edits() - return True - - #--- Events - def selectionChanged(self, selected, deselected): - self._updateModelSelection() - - #--- model --> view - def refresh(self): - self.beginResetModel() - self.endResetModel() - self._updateViewSelection() - - def show_selected_row(self): - if self.model.selected_index is not None: - self.view.showRow(self.model.selected_index) - - def start_editing(self): - self.view.editSelected() - - def stop_editing(self): - self.view.setFocus() # enough to stop editing - - def update_selection(self): - self._updateViewSelection() diff --git a/qtlib/text_field.py b/qtlib/text_field.py deleted file mode 100644 index a0be3fd2..00000000 --- a/qtlib/text_field.py +++ /dev/null @@ -1,23 +0,0 @@ -# Created On: 2012/01/23 -# Copyright 2015 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at -# http://www.gnu.org/licenses/gpl-3.0.html - -class TextField: - def __init__(self, model, view): - self.model = model - self.view = view - self.model.view = self - # Make TextField also work for QLabel, which doesn't allow editing - if hasattr(self.view, 'editingFinished'): - self.view.editingFinished.connect(self.editingFinished) - - def editingFinished(self): - self.model.text = self.view.text() - - # model --> view - def refresh(self): - self.view.setText(self.model.text) - diff --git a/qtlib/tree_model.py b/qtlib/tree_model.py deleted file mode 100644 index da93cb36..00000000 --- a/qtlib/tree_model.py +++ /dev/null @@ -1,173 +0,0 @@ -# Created By: Virgil Dupras -# Created On: 2009-09-14 -# Copyright 2015 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at -# http://www.gnu.org/licenses/gpl-3.0.html - -import logging - -from PyQt5.QtCore import QAbstractItemModel, QModelIndex - -class NodeContainer: - def __init__(self): - self._subnodes = None - self._ref2node = {} - - #--- Protected - def _createNode(self, ref, row): - # This returns a TreeNode instance from ref - raise NotImplementedError() - - def _getChildren(self): - # This returns a list of ref instances, not TreeNode instances - raise NotImplementedError() - - #--- Public - def invalidate(self): - # Invalidates cached data and list of subnodes without resetting ref2node. - self._subnodes = None - - #--- Properties - @property - def subnodes(self): - if self._subnodes is None: - children = self._getChildren() - self._subnodes = [] - for index, child in enumerate(children): - if child in self._ref2node: - node = self._ref2node[child] - node.row = index - else: - node = self._createNode(child, index) - self._ref2node[child] = node - self._subnodes.append(node) - return self._subnodes - - -class TreeNode(NodeContainer): - def __init__(self, model, parent, row): - NodeContainer.__init__(self) - self.model = model - self.parent = parent - self.row = row - - @property - def index(self): - return self.model.createIndex(self.row, 0, self) - - -class RefNode(TreeNode): - """Node pointing to a reference node. - - Use this if your Qt model wraps around a tree model that has iterable nodes. - """ - def __init__(self, model, parent, ref, row): - TreeNode.__init__(self, model, parent, row) - self.ref = ref - - def _createNode(self, ref, row): - return RefNode(self.model, self, ref, row) - - def _getChildren(self): - return list(self.ref) - - -# We use a specific TreeNode subclass to easily spot dummy nodes, especially in exception tracebacks. -class DummyNode(TreeNode): - pass - -class TreeModel(QAbstractItemModel, NodeContainer): - def __init__(self, **kwargs): - super().__init__(**kwargs) - self._dummyNodes = set() # dummy nodes' reference have to be kept to avoid segfault - - #--- Private - def _createDummyNode(self, parent, row): - # In some cases (drag & drop row removal, to be precise), there's a temporary discrepancy - # between a node's subnodes and what the model think it has. This leads to invalid indexes - # being queried. Rather than going through complicated row removal crap, it's simpler to - # just have rows with empty data replacing removed rows for the millisecond that the drag & - # drop lasts. Override this to return a node of the correct type. - return DummyNode(self, parent, row) - - def _lastIndex(self): - """Index of the very last item in the tree. - """ - currentIndex = QModelIndex() - rowCount = self.rowCount(currentIndex) - while rowCount > 0: - currentIndex = self.index(rowCount-1, 0, currentIndex) - rowCount = self.rowCount(currentIndex) - return currentIndex - - #--- Overrides - def index(self, row, column, parent): - if not self.subnodes: - return QModelIndex() - node = parent.internalPointer() if parent.isValid() else self - try: - return self.createIndex(row, column, node.subnodes[row]) - except IndexError: - logging.debug("Wrong tree index called (%r, %r, %r). Returning DummyNode", - row, column, node) - parentNode = parent.internalPointer() if parent.isValid() else None - dummy = self._createDummyNode(parentNode, row) - self._dummyNodes.add(dummy) - return self.createIndex(row, column, dummy) - - 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): - super().beginResetModel() - self.invalidate() - self._ref2node = {} - self._dummyNodes = set() - super().endResetModel() - - def rowCount(self, parent=QModelIndex()): - node = parent.internalPointer() if parent.isValid() else self - return len(node.subnodes) - - #--- Public - def findIndex(self, rowPath): - """Returns the QModelIndex at `rowPath` - - `rowPath` is a sequence of node rows. For example, [1, 2, 1] is the 2nd child of the - 3rd child of the 2nd child of the root. - """ - result = QModelIndex() - for row in rowPath: - result = self.index(row, 0, result) - return result - - @staticmethod - def pathForIndex(index): - reversedPath = [] - while index.isValid(): - reversedPath.append(index.row()) - index = index.parent() - return list(reversed(reversedPath)) - - def refreshData(self): - """Updates the data on all nodes, but without having to perform a full reset. - - A full reset on a tree makes us lose selection and expansion states. When all we ant to do - is to refresh the data on the nodes without adding or removing a node, a call on - dataChanged() is better. But of course, Qt makes our life complicated by asking us topLeft - and bottomRight indexes. This is a convenience method refreshing the whole tree. - """ - columnCount = self.columnCount() - topLeft = self.index(0, 0, QModelIndex()) - bottomLeft = self._lastIndex() - bottomRight = self.sibling(bottomLeft.row(), columnCount-1, bottomLeft) - self.dataChanged.emit(topLeft, bottomRight) - diff --git a/qtlib/util.py b/qtlib/util.py deleted file mode 100644 index 7d588351..00000000 --- a/qtlib/util.py +++ /dev/null @@ -1,104 +0,0 @@ -# Created By: Virgil Dupras -# Created On: 2011-02-01 -# Copyright 2015 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at -# http://www.gnu.org/licenses/gpl-3.0.html - -import sys -import io -import os.path as op -import os -import logging - -from hscommon.util import first - -from PyQt5.QtCore import QStandardPaths -from PyQt5.QtGui import QPixmap, QIcon -from PyQt5.QtWidgets import QDesktopWidget, QSpacerItem, QSizePolicy, QAction, QHBoxLayout - -def moveToScreenCenter(widget): - frame = widget.frameGeometry() - frame.moveCenter(QDesktopWidget().availableGeometry().center()) - widget.move(frame.topLeft()) - -def verticalSpacer(size=None): - if size: - return QSpacerItem(1, size, QSizePolicy.Fixed, QSizePolicy.Fixed) - else: - return QSpacerItem(1, 1, QSizePolicy.Fixed, QSizePolicy.MinimumExpanding) - -def horizontalSpacer(size=None): - if size: - return QSpacerItem(size, 1, QSizePolicy.Fixed, QSizePolicy.Fixed) - else: - return QSpacerItem(1, 1, QSizePolicy.MinimumExpanding, QSizePolicy.Fixed) - -def horizontalWrap(widgets): - """Wrap all widgets in `widgets` in a horizontal layout. - - If, instead of placing a widget in your list, you place an int or None, an horizontal spacer - with the width corresponding to the int will be placed (0 or None means an expanding spacer). - """ - layout = QHBoxLayout() - for widget in widgets: - if widget is None or isinstance(widget, int): - layout.addItem(horizontalSpacer(size=widget)) - else: - layout.addWidget(widget) - return layout - -def createActions(actions, target): - # actions = [(name, shortcut, icon, desc, func)] - for name, shortcut, icon, desc, func in actions: - action = QAction(target) - if icon: - action.setIcon(QIcon(QPixmap(':/' + icon))) - if shortcut: - action.setShortcut(shortcut) - action.setText(desc) - action.triggered.connect(func) - setattr(target, name, action) - -def setAccelKeys(menu): - actions = menu.actions() - titles = [a.text() for a in actions] - available_characters = {c.lower() for s in titles for c in s if c.isalpha()} - for action in actions: - text = action.text() - c = first(c for c in text if c.lower() in available_characters) - if c is None: - continue - i = text.index(c) - newtext = text[:i] + '&' + text[i:] - available_characters.remove(c.lower()) - action.setText(newtext) - -def getAppData(): - return QStandardPaths.standardLocations(QStandardPaths.DataLocation)[0] - -class SysWrapper(io.IOBase): - def write(self, s): - if s.strip(): # don't log empty stuff - logging.warning(s) - -def setupQtLogging(level=logging.WARNING): - # Under Qt, we log in "debug.log" in appdata. Moreover, when under cx_freeze, we have a - # problem because sys.stdout and sys.stderr are None, so we need to replace them with a - # wrapper that logs with the logging module. - appdata = getAppData() - 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=level, - format='%(asctime)s - %(levelname)s - %(message)s') - if sys.stderr is None: # happens under a cx_freeze environment - sys.stderr = SysWrapper() - if sys.stdout is None: - sys.stdout = SysWrapper() - -def escapeamp(s): - # Returns `s` with escaped ampersand (& --> &&). QAction text needs to have & escaped because - # that character is used to define "accel keys". - return s.replace('&', '&&')