diff --git a/build.py b/build.py index 5ae90608..2cab8856 100644 --- a/build.py +++ b/build.py @@ -135,6 +135,7 @@ def build_cocoa(edition, dev): print_and_do(cocoa_compile_command(edition)) os.chdir('..') app.copy_executable('cocoa/build/dupeGuru') + build_help(edition) print("Copying resources and frameworks") image_path = ed('cocoa/{}/dupeguru.icns') resources = [image_path, 'cocoa/base/dsa_pub.pem', 'build/dg_cocoa.py', 'build/help'] @@ -151,6 +152,7 @@ def build_qt(edition, dev, conf): print("Building Qt stuff") print_and_do("pyrcc4 -py3 {0} > {1}".format(op.join('qt', 'base', 'dg.qrc'), op.join('qt', 'base', 'dg_rc.py'))) fix_qt_resource_file(op.join('qt', 'base', 'dg_rc.py')) + build_help(edition) print("Creating the run.py file") filereplace(op.join('qt', 'run_template.py'), 'run.py', edition=edition) @@ -168,17 +170,12 @@ def build_help(edition): conftmpl = op.join(current_path, 'help', 'conf.tmpl') sphinxgen.gen(help_basepath, help_destpath, changelog_path, tixurl, confrepl, conftmpl, changelogtmpl) -def build_base_localizations(): - loc.compile_all_po('locale') - loc.compile_all_po(op.join('hscommon', 'locale')) - loc.merge_locale_dir(op.join('hscommon', 'locale'), 'locale') - def build_qt_localizations(): loc.compile_all_po(op.join('qtlib', 'locale')) loc.merge_locale_dir(op.join('qtlib', 'locale'), 'locale') def build_localizations(ui, edition): - build_base_localizations() + loc.compile_all_po('locale') if ui == 'cocoa': app = cocoa_app(edition) loc.build_cocoa_localizations(app, en_stringsfile=op.join('cocoa', 'base', 'en.lproj', 'Localizable.strings')) @@ -220,8 +217,6 @@ def build_updatepot(): # We want to merge the generated pot with the old pot in the most preserving way possible. ui_packages = ['qt', op.join('cocoa', 'inter')] loc.generate_pot(ui_packages, op.join('locale', 'ui.pot'), ['tr'], merge=(not ISOSX)) - print("Building hscommon.pot") - loc.generate_pot(['hscommon'], op.join('hscommon', 'locale', 'hscommon.pot'), ['tr']) print("Building qtlib.pot") loc.generate_pot(['qtlib'], op.join('qtlib', 'locale', 'qtlib.pot'), ['tr']) if ISOSX: @@ -236,13 +231,11 @@ def build_updatepot(): def build_mergepot(): print("Updating .po files using .pot files") loc.merge_pots_into_pos('locale') - loc.merge_pots_into_pos(op.join('hscommon', 'locale')) loc.merge_pots_into_pos(op.join('qtlib', 'locale')) loc.merge_pots_into_pos(op.join('cocoalib', 'locale')) def build_normpo(): loc.normalize_all_pos('locale') - loc.normalize_all_pos(op.join('hscommon', 'locale')) loc.normalize_all_pos(op.join('qtlib', 'locale')) loc.normalize_all_pos(op.join('cocoalib', 'locale')) @@ -264,7 +257,7 @@ def build_cocoa_bridging_interfaces(edition): add_to_pythonpath('cocoalib') from cocoa.inter import (PyGUIObject, GUIObjectView, PyColumns, ColumnsView, PyOutline, OutlineView, PySelectableList, SelectableListView, PyTable, TableView, PyBaseApp, - PyFairware, PyTextField, ProgressWindowView, PyProgressWindow) + PyTextField, ProgressWindowView, PyProgressWindow) from inter.deletion_options import PyDeletionOptions, DeletionOptionsView from inter.details_panel import PyDetailsPanel, DetailsPanelView from inter.directory_outline import PyDirectoryOutline, DirectoryOutlineView @@ -276,7 +269,7 @@ def build_cocoa_bridging_interfaces(edition): from inter.stats_label import PyStatsLabel, StatsLabelView from inter.app import PyDupeGuruBase, DupeGuruView appmod = importlib.import_module('inter.app_{}'.format(edition)) - allclasses = [PyGUIObject, PyColumns, PyOutline, PySelectableList, PyTable, PyBaseApp, PyFairware, + allclasses = [PyGUIObject, PyColumns, PyOutline, PySelectableList, PyTable, PyBaseApp, PyDetailsPanel, PyDirectoryOutline, PyPrioritizeDialog, PyPrioritizeList, PyProblemDialog, PyIgnoreListDialog, PyDeletionOptions, PyResultTable, PyStatsLabel, PyDupeGuruBase, PyTextField, PyProgressWindow, appmod.PyDupeGuru] @@ -317,7 +310,6 @@ def build_pe_modules(ui): def build_normal(edition, ui, dev, conf): print("Building dupeGuru {0} with UI {1}".format(edition.upper(), ui)) add_to_pythonpath('.') - build_help(edition) print("Building dupeGuru") if edition == 'pe': build_pe_modules(ui) @@ -335,8 +327,9 @@ def main(): if dev: print("Building in Dev mode") if options.clean: - if op.exists('build'): - shutil.rmtree('build') + for path in ['build', op.join('cocoa', 'build'), op.join('cocoa', 'autogen')]: + if op.exists(path): + shutil.rmtree(path) if not op.exists('build'): os.mkdir('build') if options.doc: diff --git a/cocoa/base/AppDelegateBase.h b/cocoa/base/AppDelegateBase.h index f3c7f219..d7f2e957 100644 --- a/cocoa/base/AppDelegateBase.h +++ b/cocoa/base/AppDelegateBase.h @@ -13,7 +13,7 @@ http://www.hardcoded.net/licenses/bsd_license #import "DetailsPanel.h" #import "DirectoryPanel.h" #import "IgnoreListDialog.h" -#import "HSFairwareAboutBox.h" +#import "HSAboutBox.h" #import "HSRecentFiles.h" #import "HSProgressWindow.h" @@ -30,7 +30,7 @@ http://www.hardcoded.net/licenses/bsd_license IgnoreListDialog *_ignoreListDialog; HSProgressWindow *_progressWindow; NSWindowController *_preferencesPanel; - HSFairwareAboutBox *_aboutBox; + HSAboutBox *_aboutBox; HSRecentFiles *_recentResults; } @@ -73,6 +73,4 @@ http://www.hardcoded.net/licenses/bsd_license /* model --> view */ - (void)showMessage:(NSString *)msg; -- (void)setupAsRegistered; -- (void)showDemoNagWithPrompt:(NSString *)prompt; @end diff --git a/cocoa/base/AppDelegateBase.m b/cocoa/base/AppDelegateBase.m index 7c906390..3b44c8cd 100644 --- a/cocoa/base/AppDelegateBase.m +++ b/cocoa/base/AppDelegateBase.m @@ -8,7 +8,6 @@ http://www.hardcoded.net/licenses/bsd_license #import "AppDelegateBase.h" #import "ProgressController.h" -#import "HSFairwareReminder.h" #import "HSPyUtil.h" #import "Consts.h" #import "Dialogs.h" @@ -140,7 +139,7 @@ http://www.hardcoded.net/licenses/bsd_license [op setAllowedFileTypes:[NSArray arrayWithObject:@"dupeguru"]]; [op setTitle:NSLocalizedString(@"Select a results file to load", @"")]; if ([op runModal] == NSOKButton) { - NSString *filename = [[op filenames] objectAtIndex:0]; + NSString *filename = [[[op URLs] objectAtIndex:0] path]; [model loadResultsFrom:filename]; [[self recentResults] addFile:filename]; } @@ -162,7 +161,7 @@ http://www.hardcoded.net/licenses/bsd_license - (void)showAboutBox { if (_aboutBox == nil) { - _aboutBox = [[HSFairwareAboutBox alloc] initWithApp:model]; + _aboutBox = [[HSAboutBox alloc] initWithApp:model]; } [[_aboutBox window] makeKeyAndOrderFront:nil]; } @@ -199,7 +198,6 @@ http://www.hardcoded.net/licenses/bsd_license /* Delegate */ - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { - [model initialRegistrationSetup]; [model loadSession]; } @@ -261,16 +259,6 @@ http://www.hardcoded.net/licenses/bsd_license [[self resultWindow] showProblemDialog]; } -- (void)setupAsRegistered -{ - // Nothing to do. -} - -- (void)showDemoNagWithPrompt:(NSString *)prompt -{ - [HSFairwareReminder showDemoNagWithApp:[self model] prompt:prompt]; -} - - (NSString *)selectDestFolderWithPrompt:(NSString *)prompt { NSOpenPanel *op = [NSOpenPanel openPanel]; @@ -280,7 +268,7 @@ http://www.hardcoded.net/licenses/bsd_license [op setAllowsMultipleSelection:NO]; [op setTitle:prompt]; if ([op runModal] == NSOKButton) { - return [[op filenames] objectAtIndex:0]; + return [[[op URLs] objectAtIndex:0] path]; } else { return nil; @@ -294,7 +282,7 @@ http://www.hardcoded.net/licenses/bsd_license [sp setAllowedFileTypes:[NSArray arrayWithObject:extension]]; [sp setTitle:prompt]; if ([sp runModal] == NSOKButton) { - return [sp filename]; + return [[sp URL] path]; } else { return nil; diff --git a/cocoa/base/DeletionOptions.m b/cocoa/base/DeletionOptions.m index 99432016..22294fa8 100644 --- a/cocoa/base/DeletionOptions.m +++ b/cocoa/base/DeletionOptions.m @@ -64,4 +64,9 @@ http://www.hardcoded.net/licenses/bsd_license [[self window] close]; return r == NSOKButton; } + +- (void)setHardlinkOptionEnabled:(BOOL)enabled +{ + [linkTypeRadio setEnabled:enabled]; +} @end \ No newline at end of file diff --git a/cocoa/base/DirectoryOutline.h b/cocoa/base/DirectoryOutline.h index 83b334bf..67dd9150 100644 --- a/cocoa/base/DirectoryOutline.h +++ b/cocoa/base/DirectoryOutline.h @@ -16,4 +16,6 @@ http://www.hardcoded.net/licenses/bsd_license @interface DirectoryOutline : HSOutline {} - (id)initWithPyRef:(PyObject *)aPyRef outlineView:(HSOutlineView *)aOutlineView; - (PyDirectoryOutline *)model; + +- (void)selectAll; @end; \ No newline at end of file diff --git a/cocoa/base/DirectoryOutline.m b/cocoa/base/DirectoryOutline.m index 7a4431dc..fc0e7dcf 100644 --- a/cocoa/base/DirectoryOutline.m +++ b/cocoa/base/DirectoryOutline.m @@ -22,6 +22,12 @@ http://www.hardcoded.net/licenses/bsd_license return (PyDirectoryOutline *)model; } +/* Public */ +- (void)selectAll +{ + [[self model] selectAll]; +} + /* Delegate */ - (NSDragOperation)outlineView:(NSOutlineView *)outlineView validateDrop:(id < NSDraggingInfo >)info proposedItem:(id)item proposedChildIndex:(NSInteger)index { diff --git a/cocoa/base/DirectoryPanel.h b/cocoa/base/DirectoryPanel.h index 07cccb3d..4caaeff9 100644 --- a/cocoa/base/DirectoryPanel.h +++ b/cocoa/base/DirectoryPanel.h @@ -46,4 +46,6 @@ http://www.hardcoded.net/licenses/bsd_license - (void)addDirectory:(NSString *)directory; - (void)refreshRemoveButtonText; +- (void)markAll; + @end diff --git a/cocoa/base/DirectoryPanel.m b/cocoa/base/DirectoryPanel.m index 8a156208..9026aeb5 100644 --- a/cocoa/base/DirectoryPanel.m +++ b/cocoa/base/DirectoryPanel.m @@ -91,8 +91,8 @@ http://www.hardcoded.net/licenses/bsd_license [op setTitle:NSLocalizedString(@"Select a folder to add to the scanning list", @"")]; [op setDelegate:self]; if ([op runModal] == NSOKButton) { - for (NSString *directory in [op filenames]) { - [self addDirectory:directory]; + for (NSURL *directoryURL in [op URLs]) { + [self addDirectory:[directoryURL path]]; } } } @@ -158,6 +158,14 @@ http://www.hardcoded.net/licenses/bsd_license } } +- (void)markAll +{ + /* markAll isn't very descriptive of what we do, but since we re-use the Mark All button from + the result window, we don't have much choice. + */ + [outline selectAll]; +} + /* Delegate */ - (BOOL)panel:(id)sender shouldShowFilename:(NSString *)path { @@ -171,6 +179,14 @@ http://www.hardcoded.net/licenses/bsd_license [self addDirectory:path]; } +- (BOOL)validateMenuItem:(NSMenuItem *)item +{ + if ([item action] == @selector(markAll)) { + [item setTitle:NSLocalizedString(@"Select All", @"")]; + } + return YES; +} + /* Notifications */ - (void)directorySelectionChanged:(NSNotification *)aNotification diff --git a/cocoa/base/ResultWindowBase.m b/cocoa/base/ResultWindowBase.m index 08a43c1a..d77c3e7e 100644 --- a/cocoa/base/ResultWindowBase.m +++ b/cocoa/base/ResultWindowBase.m @@ -258,8 +258,8 @@ http://www.hardcoded.net/licenses/bsd_license [sp setAllowedFileTypes:[NSArray arrayWithObject:@"dupeguru"]]; [sp setTitle:NSLocalizedString(@"Select a file to save your results to", @"")]; if ([sp runModal] == NSOKButton) { - [model saveResultsAs:[sp filename]]; - [[app recentResults] addFile:[sp filename]]; + [model saveResultsAs:[[sp URL] path]]; + [[app recentResults] addFile:[[sp URL] path]]; } } @@ -344,6 +344,9 @@ http://www.hardcoded.net/licenses/bsd_license - (BOOL)validateMenuItem:(NSMenuItem *)item { + if ([item action] == @selector(markAll)) { + [item setTitle:NSLocalizedString(@"Mark All", @"")]; + } return ![[ProgressController mainProgressController] isShown]; } @end diff --git a/cocoa/base/en.lproj/Localizable.strings b/cocoa/base/en.lproj/Localizable.strings index 477383ed..96a1550b 100644 --- a/cocoa/base/en.lproj/Localizable.strings +++ b/cocoa/base/en.lproj/Localizable.strings @@ -1,6 +1,5 @@ "%@ Results" = "%@ Results"; -"A previous action is still hanging in there. You can't start a new one yet. Wait a few seconds, then try again." = "A previous action is still hanging in there. You can't start a new one yet. Wait a few seconds, then try again."; "About dupeGuru" = "About dupeGuru"; "Action" = "Action"; "Actions" = "Actions"; diff --git a/cocoa/inter/app.py b/cocoa/inter/app.py index 51768c74..f04ccf41 100644 --- a/cocoa/inter/app.py +++ b/cocoa/inter/app.py @@ -1,24 +1,23 @@ import logging from objp.util import pyref, dontwrap -from cocoa import install_exception_hook, install_cocoa_logger, patch_threaded_job_performer, proxy -from cocoa.inter import PyFairware, FairwareView +from cocoa import install_exception_hook, install_cocoa_logger, patch_threaded_job_performer +from cocoa.inter import PyBaseApp, BaseAppView -class DupeGuruView(FairwareView): +class DupeGuruView(BaseAppView): def askYesNoWithPrompt_(self, prompt: str) -> bool: pass def showProblemDialog(self): pass def selectDestFolderWithPrompt_(self, prompt: str) -> str: pass def selectDestFileWithPrompt_extension_(self, prompt: str, extension: str) -> str: pass -class PyDupeGuruBase(PyFairware): +class PyDupeGuruBase(PyBaseApp): @dontwrap def _init(self, modelclass): logging.basicConfig(level=logging.WARNING, format='%(levelname)s %(message)s') install_exception_hook() install_cocoa_logger() patch_threaded_job_performer() - appdata = proxy.getAppdataPath() - self.model = modelclass(self, appdata) + self.model = modelclass(self) #---Sub-proxies def detailsPanel(self) -> pyref: @@ -144,14 +143,6 @@ class PyDupeGuruBase(PyFairware): self.model.options['copymove_dest_type'] = copymove_dest_type #--- model --> view - @dontwrap - def open_path(self, path): - proxy.openPath_(str(path)) - - @dontwrap - def reveal_path(self, path): - proxy.revealPath_(str(path)) - @dontwrap def ask_yes_no(self, prompt): return self.callback.askYesNoWithPrompt_(prompt) diff --git a/cocoa/inter/app_me.py b/cocoa/inter/app_me.py index f0010609..c6f89428 100644 --- a/cocoa/inter/app_me.py +++ b/cocoa/inter/app_me.py @@ -143,9 +143,8 @@ class Directories(directories.Directories): class DupeGuruME(DupeGuruBase): - def __init__(self, view, appdata): - appdata = op.join(appdata, 'dupeGuru Music Edition') - DupeGuruBase.__init__(self, view, appdata) + def __init__(self, view): + DupeGuruBase.__init__(self, view) # Use fileclasses set in DupeGuruBase.__init__() self.directories = Directories(fileclasses=self.directories.fileclasses) self.dead_tracks = [] @@ -174,7 +173,7 @@ class DupeGuruME(DupeGuruBase): DupeGuruBase._do_delete_dupe(self, dupe, *args) def _create_file(self, path): - if (self.directories.itunes_libpath is not None) and (path in self.directories.itunes_libpath[:-1]): + if (self.directories.itunes_libpath is not None) and (path in self.directories.itunes_libpath.parent()): if not hasattr(self, 'itunes_songs'): songs = get_itunes_songs(self.directories.itunes_libpath) self.itunes_songs = {song.path: song for song in songs} diff --git a/cocoa/inter/app_pe.py b/cocoa/inter/app_pe.py index f9104baf..6a7326b6 100644 --- a/cocoa/inter/app_pe.py +++ b/cocoa/inter/app_pe.py @@ -6,16 +6,14 @@ # which should be included with this package. The terms are also available at # http://www.hardcoded.net/licenses/bsd_license -import os.path as op import plistlib import logging import re from appscript import app, its, k, CommandError, ApplicationNotFoundError -from hscommon import io from hscommon.util import remove_invalid_xml, first -from hscommon.path import Path +from hscommon.path import Path, pathify from hscommon.trans import trget from cocoa import proxy @@ -48,6 +46,16 @@ class Photo(PhotoBase): raise IOError('The picture %s could not be read' % str(self.path)) return blocks + def _get_exif_timestamp(self): + exifdata = proxy.readExifData_(str(self.path)) + if exifdata: + try: + return exifdata['{Exif}']['DateTimeOriginal'] + except KeyError: + return '' + else: + return '' + class IPhoto(Photo): def __init__(self, path, db_id): @@ -67,11 +75,12 @@ class AperturePhoto(Photo): def display_folder_path(self): return APERTURE_PATH -def get_iphoto_or_aperture_pictures(plistpath, photo_class): +@pathify +def get_iphoto_or_aperture_pictures(plistpath: Path, photo_class): # The structure of iPhoto and Aperture libraries for the base photo list are excactly the same. - if not io.exists(plistpath): + if not plistpath.exists(): return [] - s = io.open(plistpath, 'rt', encoding='utf-8').read() + s = plistpath.open('rt', encoding='utf-8').read() # There was a case where a guy had 0x10 chars in his plist, causing expat errors on loading s = remove_invalid_xml(s, replace_with='') # It seems that iPhoto sometimes doesn't properly escape & chars. The regexp below is to find @@ -114,12 +123,12 @@ class Directories(directories.Directories): directories.Directories.__init__(self, fileclasses=[Photo]) try: self.iphoto_libpath = get_iphoto_database_path() - self.set_state(self.iphoto_libpath[:-1], directories.DirectoryState.Excluded) + self.set_state(self.iphoto_libpath.parent(), directories.DirectoryState.Excluded) except directories.InvalidPathError: self.iphoto_libpath = None try: self.aperture_libpath = get_aperture_database_path() - self.set_state(self.aperture_libpath[:-1], directories.DirectoryState.Excluded) + self.set_state(self.aperture_libpath.parent(), directories.DirectoryState.Excluded) except directories.InvalidPathError: self.aperture_libpath = None @@ -171,9 +180,8 @@ class Directories(directories.Directories): class DupeGuruPE(DupeGuruBase): - def __init__(self, view, appdata): - appdata = op.join(appdata, 'dupeGuru Picture Edition') - DupeGuruBase.__init__(self, view, appdata) + def __init__(self, view): + DupeGuruBase.__init__(self, view) self.directories = Directories() def _do_delete(self, j, *args): @@ -247,12 +255,12 @@ class DupeGuruPE(DupeGuruBase): DupeGuruBase._do_delete_dupe(self, dupe, *args) def _create_file(self, path): - if (self.directories.iphoto_libpath is not None) and (path in self.directories.iphoto_libpath[:-1]): + if (self.directories.iphoto_libpath is not None) and (path in self.directories.iphoto_libpath.parent()): if not hasattr(self, 'path2iphoto'): photos = get_iphoto_pictures(self.directories.iphoto_libpath) self.path2iphoto = {p.path: p for p in photos} return self.path2iphoto.get(path) - if (self.directories.aperture_libpath is not None) and (path in self.directories.aperture_libpath[:-1]): + if (self.directories.aperture_libpath is not None) and (path in self.directories.aperture_libpath.parent()): if not hasattr(self, 'path2aperture'): photos = get_aperture_pictures(self.directories.aperture_libpath) self.path2aperture = {p.path: p for p in photos} diff --git a/cocoa/inter/app_se.py b/cocoa/inter/app_se.py index da77170a..9a22b6b7 100644 --- a/cocoa/inter/app_se.py +++ b/cocoa/inter/app_se.py @@ -9,8 +9,7 @@ import logging import os.path as op -from hscommon import io -from hscommon.path import Path +from hscommon.path import Path, pathify from cocoa import proxy from core.scanner import ScanType @@ -27,8 +26,9 @@ def is_bundle(str_path): class Bundle(fs.Folder): @classmethod - def can_handle(cls, path): - return not io.islink(path) and io.isdir(path) and is_bundle(str(path)) + @pathify + def can_handle(cls, path: Path): + return not path.islink() and path.isdir() and is_bundle(str(path)) class Directories(DirectoriesBase): @@ -68,9 +68,10 @@ class Directories(DirectoriesBase): class DupeGuru(DupeGuruBase): - def __init__(self, view, appdata): - appdata = op.join(appdata, 'dupeGuru') - DupeGuruBase.__init__(self, view, appdata) + def __init__(self, view): + # appdata = op.join(appdata, 'dupeGuru') + # print(repr(appdata)) + DupeGuruBase.__init__(self, view) self.directories = Directories() diff --git a/cocoa/inter/deletion_options.py b/cocoa/inter/deletion_options.py index ce8a55a8..d5310253 100644 --- a/cocoa/inter/deletion_options.py +++ b/cocoa/inter/deletion_options.py @@ -11,6 +11,7 @@ from cocoa.inter import PyGUIObject, GUIObjectView class DeletionOptionsView(GUIObjectView): def updateMsg_(self, msg: str): pass def show(self) -> bool: pass + def setHardlinkOptionEnabled_(self, enabled: bool): pass class PyDeletionOptions(PyGUIObject): def setLinkDeleted_(self, link_deleted: bool): @@ -31,3 +32,6 @@ class PyDeletionOptions(PyGUIObject): def show(self): return self.callback.show() + @dontwrap + def set_hardlink_option_enabled(self, enabled): + self.callback.setHardlinkOptionEnabled_(enabled) diff --git a/cocoa/inter/directory_outline.py b/cocoa/inter/directory_outline.py index 0b83692d..a25a13a8 100644 --- a/cocoa/inter/directory_outline.py +++ b/cocoa/inter/directory_outline.py @@ -11,6 +11,9 @@ class PyDirectoryOutline(PyOutline): def removeSelectedDirectory(self): self.model.remove_selected() + def selectAll(self): + self.model.select_all() + # python --> cocoa @dontwrap def refresh_states(self): diff --git a/cocoa/wscript b/cocoa/wscript index b97df48d..058ea27a 100644 --- a/cocoa/wscript +++ b/cocoa/wscript @@ -44,7 +44,7 @@ def build(ctx): cocoalib_node = ctx.srcnode.find_dir('..').find_dir('cocoalib') cocoalib_folders = ['controllers', 'views'] cocoalib_includes = [cocoalib_node] + [cocoalib_node.find_dir(folder) for folder in cocoalib_folders] - cocoalib_uses = ['NSEventAdditions', 'Dialogs', 'HSFairwareAboutBox', 'HSFairwareReminder', 'Utils', + cocoalib_uses = ['NSEventAdditions', 'Dialogs', 'HSAboutBox', 'Utils', 'HSPyUtil', 'ProgressController', 'HSRecentFiles', 'HSQuicklook', 'ValueTransformers', 'NSImageAdditions', 'NSNotificationAdditions', 'views/HSTableView', 'views/HSOutlineView', 'views/NSIndexPathAdditions', diff --git a/cocoalib/HSAboutBox.h b/cocoalib/HSAboutBox.h index bdb02acf..89d53aae 100644 --- a/cocoalib/HSAboutBox.h +++ b/cocoalib/HSAboutBox.h @@ -14,8 +14,6 @@ http://www.hardcoded.net/licenses/bsd_license NSTextField *titleTextField; NSTextField *versionTextField; NSTextField *copyrightTextField; - NSTextField *registeredTextField; - NSButton *registerButton; PyBaseApp *app; } diff --git a/cocoalib/HSFairware.h b/cocoalib/HSFairware.h deleted file mode 100644 index bb09475f..00000000 --- a/cocoalib/HSFairware.h +++ /dev/null @@ -1,19 +0,0 @@ -/* -Copyright 2013 Hardcoded Software (http://www.hardcoded.net) - -This software is licensed under the "BSD" License as described in the "LICENSE" file, -which should be included with this package. The terms are also available at -http://www.hardcoded.net/licenses/bsd_license -*/ - -#import -#import "HSFairwareProtocol.h" - -@interface HSFairware : NSObject -{ - NSInteger appId; - NSString *name; - BOOL registered; -} -- (id)initWithAppId:(NSInteger)aAppId name:(NSString *)aName; -@end \ No newline at end of file diff --git a/cocoalib/HSFairware.m b/cocoalib/HSFairware.m deleted file mode 100644 index 60bcde2b..00000000 --- a/cocoalib/HSFairware.m +++ /dev/null @@ -1,150 +0,0 @@ -/* -Copyright 2013 Hardcoded Software (http://www.hardcoded.net) - -This software is licensed under the "BSD" License as described in the "LICENSE" file, -which should be included with this package. The terms are also available at -http://www.hardcoded.net/licenses/bsd_license -*/ - -#import "HSFairware.h" -#import -#import "HSFairwareReminder.h" -#import "Dialogs.h" -#import "Utils.h" - -NSString* md5str(NSString *source) -{ - const char *cSource = [source UTF8String]; - unsigned char result[16]; - CC_MD5(cSource, strlen(cSource), result); - return fmt(@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x", - result[0], result[1], result[2], result[3], result[4], result[5], result[6], result[7], - result[8], result[9], result[10], result[11], result[12], result[13], result[14], result[15] - ); -} - -BOOL validateCode(NSString *code, NSString *email, NSInteger appId) -{ - if ([code length] != 32) { - return NO; - } - NSInteger i; - for (i=0; i<=100; i++) { - NSString *blob = fmt(@"%i%@%iaybabtu", appId, email, i); - if ([md5str(blob) isEqualTo:code]) { - return YES; - } - } - return NO; -} - -NSString* normalizeString(NSString *str) -{ - return [[str stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] lowercaseString]; -} - -@implementation HSFairware -- (id)initWithAppId:(NSInteger)aAppId name:(NSString *)aName; -{ - self = [super init]; - appId = aAppId; - name = [aName retain]; - registered = NO; - return self; -} - -- (void)dealloc -{ - [name release]; - [super dealloc]; -} - -/* Private */ -- (void)setRegistrationCode:(NSString *)aCode email:(NSString *)aEmail -{ - registered = validateCode(aCode, aEmail, appId); -} - -/* Public */ -- (void)initialRegistrationSetup -{ - NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; - NSString *code = [ud stringForKey:@"RegistrationCode"]; - NSString *email = [ud stringForKey:@"RegistrationEmail"]; - if (code && email) { - [self setRegistrationCode:code email:email]; - } - if (!registered) { - BOOL fairwareMode = [ud boolForKey:@"FairwareMode"]; - if (!fairwareMode) { - NSString *prompt = @"%@ is fairware, which means \"open source software developed " - "with expectation of fair contributions from users\". It's a very interesting " - "concept, but one year of fairware has shown that most people just want to know " - "how much it costs and not be bothered with theories about intellectual property." - "\n\n" - "So I won't bother you and will be very straightforward: You can try %@ for " - "free but you have to buy it in order to use it without limitations. In demo mode, " - "%@ will show this dialog on startup." - "\n\n" - "So it's as simple as this. If you're curious about fairware, however, I encourage " - "you to read more about it by clicking on the \"Fairware?\" button."; - [HSFairwareReminder showDemoNagWithApp:self prompt:fmt(prompt, name, name, name)]; - } - } -} - -- (NSString *)appName -{ - return name; -} - -- (NSString *)appLongName -{ - return name; -} - -- (BOOL)isRegistered -{ - return registered; -} - -- (BOOL)setRegisteredCode:(NSString *)code andEmail:(NSString *)email -{ - code = normalizeString(code); - email = normalizeString(email); - NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; - if (([code isEqualTo:@"fairware"]) || ([email isEqualTo:@"fairware"])) { - [ud setBool:YES forKey:@"FairwareMode"]; - [Dialogs showMessage:@"Fairware mode enabled."]; - return YES; - } - [self setRegistrationCode:code email:email]; - if (registered) { - [ud setObject:code forKey:@"RegistrationCode"]; - [ud setObject:email forKey:@"RegistrationEmail"]; - [Dialogs showMessage:@"Your code is valid, thanks!"]; - return YES; - } - else { - [Dialogs showMessage:@"Your code is invalid. Make sure that you wrote the good code. Also " - "make sure that the e-mail you gave is the same as the e-mail you used for your purchase."]; - return NO; - } -} - -- (void)contribute -{ - [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"http://open.hardcoded.net/contribute/"]]; -} - -- (void)buy -{ - [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"http://www.hardcoded.net/purchase.htm"]]; -} - -- (void)aboutFairware -{ - [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"http://open.hardcoded.net/about/"]]; -} - -@end \ No newline at end of file diff --git a/cocoalib/HSFairwareAboutBox.h b/cocoalib/HSFairwareAboutBox.h deleted file mode 100644 index ed22cb09..00000000 --- a/cocoalib/HSFairwareAboutBox.h +++ /dev/null @@ -1,33 +0,0 @@ -/* -Copyright 2013 Hardcoded Software (http://www.hardcoded.net) - -This software is licensed under the "BSD" License as described in the "LICENSE" file, -which should be included with this package. The terms are also available at -http://www.hardcoded.net/licenses/bsd_license -*/ - -#import -#import "PyFairware.h" - -@interface HSFairwareAboutBox : NSWindowController -{ - NSTextField *titleTextField; - NSTextField *versionTextField; - NSTextField *copyrightTextField; - NSTextField *registeredTextField; - NSButton *registerButton; - - PyFairware *app; -} - -@property (readwrite, retain) NSTextField *titleTextField; -@property (readwrite, retain) NSTextField *versionTextField; -@property (readwrite, retain) NSTextField *copyrightTextField; -@property (readwrite, retain) NSTextField *registeredTextField; -@property (readwrite, retain) NSButton *registerButton; - -- (id)initWithApp:(PyFairware *)app; -- (void)updateFields; - -- (void)showRegisterDialog; -@end \ No newline at end of file diff --git a/cocoalib/HSFairwareAboutBox.m b/cocoalib/HSFairwareAboutBox.m deleted file mode 100644 index 20302721..00000000 --- a/cocoalib/HSFairwareAboutBox.m +++ /dev/null @@ -1,60 +0,0 @@ -/* -Copyright 2013 Hardcoded Software (http://www.hardcoded.net) - -This software is licensed under the "BSD" License as described in the "LICENSE" file, -which should be included with this package. The terms are also available at -http://www.hardcoded.net/licenses/bsd_license -*/ - -#import "HSFairwareAboutBox.h" -#import "HSFairwareAboutBox_UI.h" -#import "HSFairwareReminder.h" - -@implementation HSFairwareAboutBox - -@synthesize titleTextField; -@synthesize versionTextField; -@synthesize copyrightTextField; -@synthesize registeredTextField; -@synthesize registerButton; - -- (id)initWithApp:(PyFairware *)aApp -{ - self = [super initWithWindow:nil]; - [self setWindow:createHSFairwareAboutBox_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]; - if ([app isRegistered]) { - [registeredTextField setHidden:NO]; - [registerButton setHidden:YES]; - } - else { - [registeredTextField setHidden:YES]; - [registerButton setHidden:NO]; - } -} - -- (void)showRegisterDialog -{ - HSFairwareReminder *fr = [[HSFairwareReminder alloc] initWithApp:app]; - [fr enterCode]; - [fr release]; - [self updateFields]; -} -@end diff --git a/cocoalib/HSFairwareProtocol.h b/cocoalib/HSFairwareProtocol.h deleted file mode 100644 index e78767ea..00000000 --- a/cocoalib/HSFairwareProtocol.h +++ /dev/null @@ -1,20 +0,0 @@ -/* -Copyright 2013 Hardcoded Software (http://www.hardcoded.net) - -This software is licensed under the "BSD" License as described in the "LICENSE" file, -which should be included with this package. The terms are also available at -http://www.hardcoded.net/licenses/bsd_license -*/ - -#import - -@protocol HSFairwareProtocol -- (void)initialRegistrationSetup; -- (NSString *)appName; -- (NSString *)appLongName; -- (BOOL)isRegistered; -- (BOOL)setRegisteredCode:(NSString *)code andEmail:(NSString *)email; -- (void)contribute; -- (void)buy; -- (void)aboutFairware; -@end \ No newline at end of file diff --git a/cocoalib/HSFairwareReminder.h b/cocoalib/HSFairwareReminder.h deleted file mode 100644 index 947945c3..00000000 --- a/cocoalib/HSFairwareReminder.h +++ /dev/null @@ -1,46 +0,0 @@ -/* -Copyright 2013 Hardcoded Software (http://www.hardcoded.net) - -This software is licensed under the "BSD" License as described in the "LICENSE" file, -which should be included with this package. The terms are also available at -http://www.hardcoded.net/licenses/bsd_license -*/ - -#import -#import "HSFairwareProtocol.h" - -@interface HSFairwareReminder : NSObject -{ - NSWindow *codePanel; - NSTextField *codePromptTextField; - NSTextField *codeTextField; - NSTextField *emailTextField; - NSWindow *demoNagPanel; - NSTextField *demoPromptTextField; - - id app; -} - -@property (readwrite, retain) NSWindow *codePanel; -@property (readwrite, retain) NSTextField *codePromptTextField; -@property (readwrite, retain) NSTextField *codeTextField; -@property (readwrite, retain) NSTextField *emailTextField; -@property (readwrite, retain) NSWindow *demoNagPanel; -@property (readwrite, retain) NSTextField *demoPromptTextField; - -//Show nag only if needed -+ (BOOL)showDemoNagWithApp:(id )app prompt:(NSString *)prompt; -- (id)initWithApp:(id )app; - -- (void)contribute; -- (void)buy; -- (void)moreInfo; -- (void)cancelCode; -- (void)showEnterCode; -- (void)submitCode; -- (void)closeDialog; - -- (BOOL)showNagPanel:(NSWindow *)panel; //YES: The code has been sucessfully submitted NO: The use wan't to try the demo. -- (BOOL)showDemoNagPanelWithPrompt:(NSString *)prompt; -- (NSInteger)enterCode; //returns the modal code. -@end diff --git a/cocoalib/HSFairwareReminder.m b/cocoalib/HSFairwareReminder.m deleted file mode 100644 index 3d61541a..00000000 --- a/cocoalib/HSFairwareReminder.m +++ /dev/null @@ -1,115 +0,0 @@ -/* -Copyright 2013 Hardcoded Software (http://www.hardcoded.net) - -This software is licensed under the "BSD" License as described in the "LICENSE" file, -which should be included with this package. The terms are also available at -http://www.hardcoded.net/licenses/bsd_license -*/ - -#import "HSFairwareReminder.h" -#import "HSDemoReminder_UI.h" -#import "HSEnterCode_UI.h" -#import "Dialogs.h" -#import "Utils.h" - -@implementation HSFairwareReminder - -@synthesize codePanel; -@synthesize codePromptTextField; -@synthesize codeTextField; -@synthesize emailTextField; -@synthesize demoNagPanel; -@synthesize demoPromptTextField; - -+ (BOOL)showDemoNagWithApp:(id )app prompt:(NSString *)prompt -{ - HSFairwareReminder *fr = [[HSFairwareReminder alloc] initWithApp:app]; - BOOL r = [fr showDemoNagPanelWithPrompt:prompt]; - [fr release]; - return r; -} - -- (id)initWithApp:(id )aApp -{ - self = [super init]; - app = aApp; - [self setDemoNagPanel:createHSDemoReminder_UI(self)]; - [self setCodePanel:createHSEnterCode_UI(self)]; - [codePanel update]; - [codePromptTextField setStringValue:fmt([codePromptTextField stringValue],[app appName])]; - return self; -} - -- (void)contribute -{ - [app contribute]; -} - -- (void)buy -{ - [app buy]; -} - -- (void)moreInfo -{ - [app aboutFairware]; -} - -- (void)cancelCode -{ - [codePanel close]; - [NSApp stopModalWithCode:NSCancelButton]; -} - -- (void)showEnterCode -{ - [demoNagPanel close]; - [NSApp stopModalWithCode:NSOKButton]; -} - -- (void)submitCode -{ - NSString *code = [codeTextField stringValue]; - NSString *email = [emailTextField stringValue]; - if ([app setRegisteredCode:code andEmail:email]) { - [codePanel close]; - [NSApp stopModalWithCode:NSOKButton]; - } -} - -- (void)closeDialog -{ - [demoNagPanel close]; - [NSApp stopModalWithCode:NSCancelButton]; -} - -- (BOOL)showNagPanel:(NSWindow *)panel; -{ - NSInteger r; - while (YES) { - r = [NSApp runModalForWindow:panel]; - if (r == NSOKButton) { - r = [self enterCode]; - if (r == NSOKButton) { - return YES; - } - } - else { - return NO; - } - } -} - -- (BOOL)showDemoNagPanelWithPrompt:(NSString *)prompt -{ - [demoNagPanel setTitle:fmt([demoNagPanel title],[app appName])]; - [demoPromptTextField setStringValue:prompt]; - return [self showNagPanel:demoNagPanel]; -} - -- (NSInteger)enterCode -{ - return [NSApp runModalForWindow:codePanel]; -} - -@end diff --git a/cocoalib/cocoa/CocoaProxy.h b/cocoalib/cocoa/CocoaProxy.h index 613cabb9..58614550 100644 --- a/cocoalib/cocoa/CocoaProxy.h +++ b/cocoalib/cocoa/CocoaProxy.h @@ -20,6 +20,7 @@ - (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; @@ -29,4 +30,5 @@ - (void)destroyPool; - (void)reportCrash:(NSString *)crashReport; - (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 index 27948324..6610dbed 100644 --- a/cocoalib/cocoa/CocoaProxy.m +++ b/cocoalib/cocoa/CocoaProxy.m @@ -1,5 +1,4 @@ #import "CocoaProxy.h" -#import #import "HSErrorReportWindow.h" @implementation CocoaProxy @@ -92,13 +91,14 @@ return [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"]; } +- (NSString *)bundleInfo:(NSString *)key +{ + return [[NSBundle mainBundle] objectForInfoDictionaryKey:key]; +} + - (NSString *)osxVersion { - SInt32 major, minor, bugfix; - Gestalt(gestaltSystemVersionMajor, &major); - Gestalt(gestaltSystemVersionMinor, &minor); - Gestalt(gestaltSystemVersionBugFix, &bugfix); - return [NSString stringWithFormat:@"%d.%d.%d", major, minor, bugfix]; + return [[NSProcessInfo processInfo] operatingSystemVersionString]; } - (void)postNotification:(NSString *)name userInfo:(NSDictionary *)userInfo @@ -152,4 +152,20 @@ { 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/inter.py b/cocoalib/cocoa/inter.py index 625f2c5f..01d7e77e 100644 --- a/cocoalib/cocoa/inter.py +++ b/cocoalib/cocoa/inter.py @@ -294,45 +294,7 @@ class PyBaseApp(PyGUIObject): def set_default(self, key_name, value): proxy.setPrefValue_value_(key_name, value) - @dontwrap - def open_url(self, url): - proxy.openURL_(url) - @dontwrap def show_message(self, msg): self.callback.showMessage_(msg) -class FairwareView(BaseAppView): - def setupAsRegistered(self): pass - def showDemoNagWithPrompt_(self, prompt: str): pass - -class PyFairware(PyBaseApp): - FOLLOW_PROTOCOLS = ['HSFairwareProtocol'] - - def initialRegistrationSetup(self): - self.model.initial_registration_setup() - - def isRegistered(self) -> bool: - return self.model.registered - - def setRegisteredCode_andEmail_(self, code: str, email: str) -> bool: - return self.model.set_registration(code, email, False) - - def contribute(self): - self.model.contribute() - - def buy(self): - self.model.buy() - - def aboutFairware(self): - self.model.about_fairware() - - #--- Python --> Cocoa - @dontwrap - def setup_as_registered(self): - self.callback.setupAsRegistered() - - @dontwrap - def show_demo_nag(self, prompt): - self.callback.showDemoNagWithPrompt_(prompt) - diff --git a/cocoalib/controllers/HSOutline.m b/cocoalib/controllers/HSOutline.m index 41c52ed8..a39a566d 100644 --- a/cocoalib/controllers/HSOutline.m +++ b/cocoalib/controllers/HSOutline.m @@ -101,7 +101,13 @@ http://www.hardcoded.net/licenses/bsd_license [[self view] setDelegate:nil]; [[self view] reloadData]; [[self view] setDelegate:self]; - [oldRetainer release]; + /* 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]; } diff --git a/cocoalib/en.lproj/cocoalib.strings b/cocoalib/en.lproj/cocoalib.strings index 7ce758c1..aac62297 100644 --- a/cocoalib/en.lproj/cocoalib.strings +++ b/cocoalib/en.lproj/cocoalib.strings @@ -1,28 +1,15 @@ -"%@ is Fairware" = "%@ is Fairware"; "Although the application should continue to run after this error, it may be in an instable state, so it is recommended that you restart the application." = "Although the application should continue to run after this error, it may be in an instable state, so it is recommended that you restart the application."; -"Buy" = "Buy"; "Cancel" = "Cancel"; "Clear List" = "Clear List"; -"Contribute" = "Contribute"; "Don't Send" = "Don't Send"; -"Enter Key" = "Enter Key"; -"Enter your key" = "Enter your key"; "Error Report" = "Error Report"; -"Fairware?" = "Fairware?"; "No" = "No"; "OK" = "OK"; "Please wait..." = "Please wait..."; -"Register" = "Register"; -"Registration e-mail:" = "Registration e-mail:"; -"Registration key:" = "Registration key:"; "Send" = "Send"; "Something went wrong. Would you like to send the error report to Hardcoded Software?" = "Something went wrong. Would you like to send the error report to Hardcoded Software?"; "Status: Working..." = "Status: Working..."; -"Submit" = "Submit"; -"This app is registered, thanks!" = "This app is registered, thanks!"; -"Try" = "Try"; -"Type the key you received when you contributed to %@, as well as the e-mail used as a reference for the purchase." = "Type the key you received when you contributed to %@, as well as the e-mail used as a reference for the purchase."; "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 index 6490ccdd..25f48fbd 100644 --- a/cocoalib/locale/cocoalib.pot +++ b/cocoalib/locale/cocoalib.pot @@ -1,12 +1,6 @@ # msgid "" msgstr "" -"Content-Type: text/plain; charset=utf-8\n" -"Content-Transfer-Encoding: utf-8\n" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "%@ is Fairware" -msgstr "" #: cocoalib/en.lproj/cocoalib.strings:0 msgid "" @@ -14,10 +8,6 @@ msgid "" "in an instable state, so it is recommended that you restart the application." msgstr "" -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Buy" -msgstr "" - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "Cancel" msgstr "" @@ -26,30 +16,14 @@ msgstr "" msgid "Clear List" msgstr "" -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Contribute" -msgstr "" - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "Don't Send" msgstr "" -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Enter Key" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Enter your key" -msgstr "" - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "Error Report" msgstr "" -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Fairware?" -msgstr "" - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "No" msgstr "" @@ -62,18 +36,6 @@ msgstr "" msgid "Please wait..." msgstr "" -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Register" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Registration e-mail:" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Registration key:" -msgstr "" - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "Send" msgstr "" @@ -88,24 +50,6 @@ msgstr "" msgid "Status: Working..." msgstr "" -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Submit" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "This app is registered, thanks!" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Try" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "" -"Type the key you received when you contributed to %@, as well as the e-mail " -"used as a reference for the purchase." -msgstr "" - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "Work in progress, please wait." msgstr "" diff --git a/cocoalib/locale/cs/LC_MESSAGES/cocoalib.po b/cocoalib/locale/cs/LC_MESSAGES/cocoalib.po index 76b7f731..f3c35f79 100644 --- a/cocoalib/locale/cs/LC_MESSAGES/cocoalib.po +++ b/cocoalib/locale/cs/LC_MESSAGES/cocoalib.po @@ -9,20 +9,12 @@ msgstr "" "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 "%@ is Fairware" -msgstr "%@ is Fairware" - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "" "Although the application should continue to run after this error, it may be " "in an instable state, so it is recommended that you restart the application." msgstr "" -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Buy" -msgstr "Buy" - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "Cancel" msgstr "Cancel" @@ -31,30 +23,14 @@ msgstr "Cancel" msgid "Clear List" msgstr "" -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Contribute" -msgstr "Contribute" - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "Don't Send" msgstr "Don't Send" -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Enter Key" -msgstr "Enter Key" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Enter your key" -msgstr "Enter your key" - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "Error Report" msgstr "" -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Fairware?" -msgstr "Fairware?" - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "No" msgstr "" @@ -67,18 +43,6 @@ msgstr "" msgid "Please wait..." msgstr "" -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Register" -msgstr "Register" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Registration e-mail:" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Registration key:" -msgstr "Registration key:" - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "Send" msgstr "Send" @@ -95,26 +59,6 @@ msgstr "" msgid "Status: Working..." msgstr "" -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Submit" -msgstr "Submit" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "This app is registered, thanks!" -msgstr "This app is registered, thanks!" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Try" -msgstr "Try" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "" -"Type the key you received when you contributed to %@, as well as the e-mail " -"used as a reference for the purchase." -msgstr "" -"Type the key you received when you contributed to %@, as well as the e-mail " -"used as a reference for the purchase." - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "Work in progress, please wait." msgstr "" diff --git a/cocoalib/locale/de/LC_MESSAGES/cocoalib.po b/cocoalib/locale/de/LC_MESSAGES/cocoalib.po index 3b83c007..32500a51 100644 --- a/cocoalib/locale/de/LC_MESSAGES/cocoalib.po +++ b/cocoalib/locale/de/LC_MESSAGES/cocoalib.po @@ -9,20 +9,12 @@ msgstr "" "Language: de\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "%@ is Fairware" -msgstr "%@ is Fairware" - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "" "Although the application should continue to run after this error, it may be " "in an instable state, so it is recommended that you restart the application." msgstr "" -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Buy" -msgstr "Buy" - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "Cancel" msgstr "Abbrechen" @@ -31,30 +23,14 @@ msgstr "Abbrechen" msgid "Clear List" msgstr "" -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Contribute" -msgstr "Spenden" - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "Don't Send" msgstr "Don't Send" -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Enter Key" -msgstr "Registrieren" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Enter your key" -msgstr "Schlüssel eingeben" - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "Error Report" msgstr "" -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Fairware?" -msgstr "Fairware?" - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "No" msgstr "" @@ -67,18 +43,6 @@ msgstr "" msgid "Please wait..." msgstr "" -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Register" -msgstr "Register" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Registration e-mail:" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Registration key:" -msgstr "Registrierungsschlüssel:" - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "Send" msgstr "Send" @@ -95,26 +59,6 @@ msgstr "" msgid "Status: Working..." msgstr "" -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Submit" -msgstr "Abschicken" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "This app is registered, thanks!" -msgstr "This app is registered, thanks!" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Try" -msgstr "Try" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "" -"Type the key you received when you contributed to %@, as well as the e-mail " -"used as a reference for the purchase." -msgstr "" -"Geben Sie den empfangenen Schlüssel und die E-Mail-Adresse als Referenz für " -"den Kauf an, wenn Sie für %@ gespendet haben." - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "Work in progress, please wait." msgstr "" diff --git a/cocoalib/locale/es/LC_MESSAGES/cocoalib.po b/cocoalib/locale/es/LC_MESSAGES/cocoalib.po index 5676f2c8..87084b29 100755 --- a/cocoalib/locale/es/LC_MESSAGES/cocoalib.po +++ b/cocoalib/locale/es/LC_MESSAGES/cocoalib.po @@ -9,10 +9,6 @@ msgstr "" "Language: es\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "%@ is Fairware" -msgstr "%@ es Fairware" - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "" "Although the application should continue to run after this error, it may be " @@ -21,10 +17,6 @@ msgstr "" "Aunque la aplicación debería continuar funcionado tras el fallo, sin embargo" " podría volverse inestable. Se recomienda reiniciar la aplicación." -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Buy" -msgstr "Comprar" - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "Cancel" msgstr "Cancelar" @@ -33,30 +25,14 @@ msgstr "Cancelar" msgid "Clear List" msgstr "Limpiar lista" -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Contribute" -msgstr "Donar" - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "Don't Send" msgstr "No envíar" -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Enter Key" -msgstr "Introducir clave" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Enter your key" -msgstr "Introduzca su clave" - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "Error Report" msgstr "Informe de error" -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Fairware?" -msgstr "¿Fairware?" - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "No" msgstr "No" @@ -69,18 +45,6 @@ msgstr "Aceptar" msgid "Please wait..." msgstr "Por favor, espere..." -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Register" -msgstr "Registrar" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Registration e-mail:" -msgstr "Correo electrónico de registro:" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Registration key:" -msgstr "Clave de registro" - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "Send" msgstr "Enviar" @@ -97,26 +61,6 @@ msgstr "" msgid "Status: Working..." msgstr "Estado: procesando..." -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Submit" -msgstr "Enviar" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "This app is registered, thanks!" -msgstr "La aplicación está registrada. ¡Gracias!" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Try" -msgstr "Probar" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "" -"Type the key you received when you contributed to %@, as well as the e-mail " -"used as a reference for the purchase." -msgstr "" -"Escriba la clave que recibió al donar a %@, así como el correo electrónico " -"que usó en el proceso." - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "Work in progress, please wait." msgstr "En proceso, por favor, espere." diff --git a/cocoalib/locale/fr/LC_MESSAGES/cocoalib.po b/cocoalib/locale/fr/LC_MESSAGES/cocoalib.po index 9365a540..2c63b49f 100644 --- a/cocoalib/locale/fr/LC_MESSAGES/cocoalib.po +++ b/cocoalib/locale/fr/LC_MESSAGES/cocoalib.po @@ -9,20 +9,12 @@ msgstr "" "Language: fr\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "%@ is Fairware" -msgstr "%@ est Fairware" - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "" "Although the application should continue to run after this error, it may be " "in an instable state, so it is recommended that you restart the application." msgstr "" -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Buy" -msgstr "Acheter" - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "Cancel" msgstr "Annuler" @@ -31,30 +23,14 @@ msgstr "Annuler" msgid "Clear List" msgstr "" -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Contribute" -msgstr "Contribuer" - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "Don't Send" msgstr "Ignorer" -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Enter Key" -msgstr "Enregistrer" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Enter your key" -msgstr "Entrez votre clé" - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "Error Report" msgstr "" -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Fairware?" -msgstr "Fairware?" - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "No" msgstr "" @@ -67,18 +43,6 @@ msgstr "" msgid "Please wait..." msgstr "" -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Register" -msgstr "Enregistrer" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Registration e-mail:" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Registration key:" -msgstr "Clé d'enregistrement:" - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "Send" msgstr "Envoyer" @@ -94,26 +58,6 @@ msgstr "" msgid "Status: Working..." msgstr "" -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Submit" -msgstr "Soumettre" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "This app is registered, thanks!" -msgstr "L'application est enregistrée, merci!" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Try" -msgstr "Essayer" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "" -"Type the key you received when you contributed to %@, as well as the e-mail " -"used as a reference for the purchase." -msgstr "" -"Entrez la clé que vous avez reçue en contribuant à %@, ainsi que le courriel" -" utilisé pour la contribution." - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "Work in progress, please wait." msgstr "" diff --git a/cocoalib/locale/hy/LC_MESSAGES/cocoalib.po b/cocoalib/locale/hy/LC_MESSAGES/cocoalib.po index bb4d9cbc..35d338b8 100755 --- a/cocoalib/locale/hy/LC_MESSAGES/cocoalib.po +++ b/cocoalib/locale/hy/LC_MESSAGES/cocoalib.po @@ -9,20 +9,12 @@ msgstr "" "Language: hy\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "%@ is Fairware" -msgstr "$appname-ը Fairware է" - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "" "Although the application should continue to run after this error, it may be " "in an instable state, so it is recommended that you restart the application." msgstr "" -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Buy" -msgstr "Գնել" - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "Cancel" msgstr "Չեղարկել" @@ -31,30 +23,14 @@ msgstr "Չեղարկել" msgid "Clear List" msgstr "" -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Contribute" -msgstr "Մասնակցել" - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "Don't Send" msgstr "Չուղարկել" -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Enter Key" -msgstr "Գրել բանալին" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Enter your key" -msgstr "Շարունակել" - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "Error Report" msgstr "" -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Fairware?" -msgstr "Fairware է՞" - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "No" msgstr "" @@ -67,18 +43,6 @@ msgstr "" msgid "Please wait..." msgstr "" -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Register" -msgstr "Գրանցել" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Registration e-mail:" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Registration key:" -msgstr "Գրանցման բանալին." - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "Send" msgstr "Ուղարկել" @@ -94,26 +58,6 @@ msgstr "" msgid "Status: Working..." msgstr "" -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Submit" -msgstr "Հաստատել" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "This app is registered, thanks!" -msgstr "Այս ծրագիրը գրանցված է, շնորհակալություն!" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Try" -msgstr "Փորձել" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "" -"Type the key you received when you contributed to %@, as well as the e-mail " -"used as a reference for the purchase." -msgstr "" -"Մուտքագրեք այն բանալին, որը ստացել եք %@-ին աջակցելիս, քանզի Ձեր էլ. հասցեն " -"օգտագործվել է գնման ժամանակ:" - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "Work in progress, please wait." msgstr "" diff --git a/cocoalib/locale/it/LC_MESSAGES/cocoalib.po b/cocoalib/locale/it/LC_MESSAGES/cocoalib.po index 22789cf4..06ce3db1 100644 --- a/cocoalib/locale/it/LC_MESSAGES/cocoalib.po +++ b/cocoalib/locale/it/LC_MESSAGES/cocoalib.po @@ -9,20 +9,12 @@ msgstr "" "Language: it\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "%@ is Fairware" -msgstr "%@ è Fairware" - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "" "Although the application should continue to run after this error, it may be " "in an instable state, so it is recommended that you restart the application." msgstr "" -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Buy" -msgstr "Acquista" - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "Cancel" msgstr "Annulla" @@ -31,30 +23,14 @@ msgstr "Annulla" msgid "Clear List" msgstr "" -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Contribute" -msgstr "Contribuisci" - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "Don't Send" msgstr "Non inviare" -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Enter Key" -msgstr "Inserisci Codice" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Enter your key" -msgstr "Inserisci il tuo codice" - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "Error Report" msgstr "" -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Fairware?" -msgstr "Fairware?" - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "No" msgstr "" @@ -67,18 +43,6 @@ msgstr "" msgid "Please wait..." msgstr "" -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Register" -msgstr "Registra" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Registration e-mail:" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Registration key:" -msgstr "Codice di registrazione:" - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "Send" msgstr "Invia" @@ -95,26 +59,6 @@ msgstr "" msgid "Status: Working..." msgstr "" -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Submit" -msgstr "Invia" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "This app is registered, thanks!" -msgstr "Questa applicazione è registrata, grazie!" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Try" -msgstr "Prova" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "" -"Type the key you received when you contributed to %@, as well as the e-mail " -"used as a reference for the purchase." -msgstr "" -"Inserisci il codice che hai ricevuto quando hai contribuito a %@, così come " -"l'email di riferimento utilizzata per l'acquisto." - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "Work in progress, please wait." msgstr "" diff --git a/cocoalib/locale/nl/LC_MESSAGES/cocoalib.po b/cocoalib/locale/nl/LC_MESSAGES/cocoalib.po index 2d0d06a6..cd841a1a 100644 --- a/cocoalib/locale/nl/LC_MESSAGES/cocoalib.po +++ b/cocoalib/locale/nl/LC_MESSAGES/cocoalib.po @@ -9,20 +9,12 @@ msgstr "" "Language: nl\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "%@ is Fairware" -msgstr "" - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "" "Although the application should continue to run after this error, it may be " "in an instable state, so it is recommended that you restart the application." msgstr "" -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Buy" -msgstr "" - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "Cancel" msgstr "" @@ -31,30 +23,14 @@ msgstr "" msgid "Clear List" msgstr "" -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Contribute" -msgstr "" - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "Don't Send" msgstr "" -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Enter Key" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Enter your key" -msgstr "" - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "Error Report" msgstr "" -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Fairware?" -msgstr "" - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "No" msgstr "" @@ -67,18 +43,6 @@ msgstr "" msgid "Please wait..." msgstr "" -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Register" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Registration e-mail:" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Registration key:" -msgstr "" - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "Send" msgstr "" @@ -93,24 +57,6 @@ msgstr "" msgid "Status: Working..." msgstr "" -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Submit" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "This app is registered, thanks!" -msgstr "This app is registered, thanks!" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Try" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "" -"Type the key you received when you contributed to %@, as well as the e-mail " -"used as a reference for the purchase." -msgstr "" - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "Work in progress, please wait." msgstr "" diff --git a/cocoalib/locale/pt_BR/LC_MESSAGES/cocoalib.po b/cocoalib/locale/pt_BR/LC_MESSAGES/cocoalib.po index 108c45da..209539d4 100644 --- a/cocoalib/locale/pt_BR/LC_MESSAGES/cocoalib.po +++ b/cocoalib/locale/pt_BR/LC_MESSAGES/cocoalib.po @@ -9,10 +9,6 @@ msgstr "" "Language: pt_BR\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "%@ is Fairware" -msgstr "%@ é Fairware" - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "" "Although the application should continue to run after this error, it may be " @@ -21,10 +17,6 @@ msgstr "" "Embora o aplicativo continue a funcionar após este erro, ele pode estar " "instável. É recomendável reiniciá-lo." -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Buy" -msgstr "Comprar" - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "Cancel" msgstr "Cancelar" @@ -33,30 +25,14 @@ msgstr "Cancelar" msgid "Clear List" msgstr "Limpar Lista" -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Contribute" -msgstr "Contribuir" - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "Don't Send" msgstr "Não Enviar" -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Enter Key" -msgstr "Entrar Chave" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Enter your key" -msgstr "Entre sua chave" - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "Error Report" msgstr "" -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Fairware?" -msgstr "Fairware?" - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "No" msgstr "Não" @@ -69,18 +45,6 @@ msgstr "" msgid "Please wait..." msgstr "Aguarde..." -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Register" -msgstr "Registrar" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Registration e-mail:" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Registration key:" -msgstr "Chave de registro:" - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "Send" msgstr "Enviar" @@ -96,26 +60,6 @@ msgstr "" msgid "Status: Working..." msgstr "" -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Submit" -msgstr "Enviar" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "This app is registered, thanks!" -msgstr "O app está registrado, obrigado!" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Try" -msgstr "Testar" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "" -"Type the key you received when you contributed to %@, as well as the e-mail " -"used as a reference for the purchase." -msgstr "" -"Digite a chave que você recebeu ao contribuir com o %@, assim como o e-mail " -"usado para a compra." - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "Work in progress, please wait." msgstr "" diff --git a/cocoalib/locale/ru/LC_MESSAGES/cocoalib.po b/cocoalib/locale/ru/LC_MESSAGES/cocoalib.po index ccf0a64d..8fd15ba2 100755 --- a/cocoalib/locale/ru/LC_MESSAGES/cocoalib.po +++ b/cocoalib/locale/ru/LC_MESSAGES/cocoalib.po @@ -9,20 +9,12 @@ msgstr "" "Language: ru\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 "%@ is Fairware" -msgstr "%@ является Fairware" - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "" "Although the application should continue to run after this error, it may be " "in an instable state, so it is recommended that you restart the application." msgstr "" -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Buy" -msgstr "Купить" - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "Cancel" msgstr "Отменить" @@ -31,30 +23,14 @@ msgstr "Отменить" msgid "Clear List" msgstr "" -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Contribute" -msgstr "Способствовайте" - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "Don't Send" msgstr "Не отправлять" -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Enter Key" -msgstr "Видите ключ" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Enter your key" -msgstr "Видите Ваш ключ" - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "Error Report" msgstr "" -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Fairware?" -msgstr "Fairware?" - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "No" msgstr "" @@ -67,18 +43,6 @@ msgstr "" msgid "Please wait..." msgstr "" -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Register" -msgstr "Регистрация" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Registration e-mail:" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Registration key:" -msgstr "Регистрационный ключ:" - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "Send" msgstr "Отправить" @@ -94,26 +58,6 @@ msgstr "" msgid "Status: Working..." msgstr "" -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Submit" -msgstr "Передать" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "This app is registered, thanks!" -msgstr "Это приложение зарегистрировано, спасибо!" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Try" -msgstr "Пробовать" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "" -"Type the key you received when you contributed to %@, as well as the e-mail " -"used as a reference for the purchase." -msgstr "" -"Тип ключа, который вы получили при способствовала %@, а также электронной " -"почты используется в качестве ссылки для покупки." - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "Work in progress, please wait." msgstr "" diff --git a/cocoalib/locale/uk/LC_MESSAGES/cocoalib.po b/cocoalib/locale/uk/LC_MESSAGES/cocoalib.po index a6ad8d6b..07d85580 100755 --- a/cocoalib/locale/uk/LC_MESSAGES/cocoalib.po +++ b/cocoalib/locale/uk/LC_MESSAGES/cocoalib.po @@ -9,10 +9,6 @@ msgstr "" "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 "%@ is Fairware" -msgstr "%@ це Fairware" - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "" "Although the application should continue to run after this error, it may be " @@ -21,10 +17,6 @@ msgstr "" "Хоча програма має продовжувати роботу після цієї помилки, вона може " "перебувати у нестабільному стані, тож рекомендується перезапустити програму." -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Buy" -msgstr "Купити" - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "Cancel" msgstr "Відмінити" @@ -33,30 +25,14 @@ msgstr "Відмінити" msgid "Clear List" msgstr "" -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Contribute" -msgstr "Зробити внесок" - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "Don't Send" msgstr "Не надсилати" -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Enter Key" -msgstr "Введіть ключ" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Enter your key" -msgstr "Введіть Ваш ключ" - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "Error Report" msgstr "" -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Fairware?" -msgstr "Fairware?" - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "No" msgstr "" @@ -69,18 +45,6 @@ msgstr "" msgid "Please wait..." msgstr "" -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Register" -msgstr "Зареєструвати" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Registration e-mail:" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Registration key:" -msgstr "Реєстраційний ключ:" - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "Send" msgstr "Надіслати" @@ -96,26 +60,6 @@ msgstr "" msgid "Status: Working..." msgstr "" -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Submit" -msgstr "Надіслати" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "This app is registered, thanks!" -msgstr "Програму зареєстровано, дякуємо!" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Try" -msgstr "Спробувати" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "" -"Type the key you received when you contributed to %@, as well as the e-mail " -"used as a reference for the purchase." -msgstr "" -"Введіть ключ, який Ви отримали зробивши внесок за %@, а також адресу " -"електронної пошти, яка була вказана під час покупки." - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "Work in progress, please wait." msgstr "" diff --git a/cocoalib/locale/vi/LC_MESSAGES/cocoalib.po b/cocoalib/locale/vi/LC_MESSAGES/cocoalib.po index f8e8834e..8da64fae 100644 --- a/cocoalib/locale/vi/LC_MESSAGES/cocoalib.po +++ b/cocoalib/locale/vi/LC_MESSAGES/cocoalib.po @@ -10,20 +10,12 @@ msgstr "" "Language: vi\n" "Plural-Forms: nplurals=1; plural=0;\n" -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "%@ is Fairware" -msgstr "" - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "" "Although the application should continue to run after this error, it may be " "in an instable state, so it is recommended that you restart the application." msgstr "" -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Buy" -msgstr "" - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "Cancel" msgstr "" @@ -32,30 +24,14 @@ msgstr "" msgid "Clear List" msgstr "" -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Contribute" -msgstr "" - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "Don't Send" msgstr "" -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Enter Key" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Enter your key" -msgstr "" - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "Error Report" msgstr "" -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Fairware?" -msgstr "" - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "No" msgstr "" @@ -68,18 +44,6 @@ msgstr "" msgid "Please wait..." msgstr "" -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Register" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Registration e-mail:" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Registration key:" -msgstr "" - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "Send" msgstr "" @@ -94,24 +58,6 @@ msgstr "" msgid "Status: Working..." msgstr "" -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Submit" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "This app is registered, thanks!" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Try" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "" -"Type the key you received when you contributed to %@, as well as the e-mail " -"used as a reference for the purchase." -msgstr "" - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "Work in progress, please wait." msgstr "" diff --git a/cocoalib/locale/zh_CN/LC_MESSAGES/cocoalib.po b/cocoalib/locale/zh_CN/LC_MESSAGES/cocoalib.po index 2285f065..c08a7aee 100644 --- a/cocoalib/locale/zh_CN/LC_MESSAGES/cocoalib.po +++ b/cocoalib/locale/zh_CN/LC_MESSAGES/cocoalib.po @@ -9,20 +9,12 @@ msgstr "" "Language: zh_CN\n" "Plural-Forms: nplurals=1; plural=0;\n" -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "%@ is Fairware" -msgstr "%@ is Fairware" - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "" "Although the application should continue to run after this error, it may be " "in an instable state, so it is recommended that you restart the application." msgstr "" -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Buy" -msgstr "Buy" - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "Cancel" msgstr "取消" @@ -31,30 +23,14 @@ msgstr "取消" msgid "Clear List" msgstr "" -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Contribute" -msgstr "捐助" - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "Don't Send" msgstr "Don't Send" -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Enter Key" -msgstr "输入密钥" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Enter your key" -msgstr "Enter your key" - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "Error Report" msgstr "" -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Fairware?" -msgstr "Fairware?" - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "No" msgstr "" @@ -67,18 +43,6 @@ msgstr "" msgid "Please wait..." msgstr "" -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Register" -msgstr "Register" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Registration e-mail:" -msgstr "" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Registration key:" -msgstr "密钥:" - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "Send" msgstr "Send" @@ -95,24 +59,6 @@ msgstr "" msgid "Status: Working..." msgstr "" -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Submit" -msgstr "提交" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "This app is registered, thanks!" -msgstr "This app is registered, thanks!" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "Try" -msgstr "Try" - -#: cocoalib/en.lproj/cocoalib.strings:0 -msgid "" -"Type the key you received when you contributed to %@, as well as the e-mail " -"used as a reference for the purchase." -msgstr "当您捐助 %@ 后,请输入收到的注册密钥以及电子邮件,这将作为购买凭证。" - #: cocoalib/en.lproj/cocoalib.strings:0 msgid "Work in progress, please wait." msgstr "" diff --git a/cocoalib/ui/demo_reminder.py b/cocoalib/ui/demo_reminder.py deleted file mode 100644 index d4f36502..00000000 --- a/cocoalib/ui/demo_reminder.py +++ /dev/null @@ -1,32 +0,0 @@ -ownerclass = 'HSFairwareReminder' -ownerimport = 'HSFairwareReminder.h' - -result = Window(528, 253, "%@ is Fairware") -result.canClose = False -result.canResize = False -result.canMinimize = False -demoPromptLabel = Label(result, NLSTR("")) -tryButton = Button(result, "Try") -enterKeyButton = Button(result, "Enter Key") -buyButton = Button(result, "Buy") -fairwareButton = Button(result, "Fairware?") - -owner.demoPromptTextField = demoPromptLabel -result.initialFirstResponder = tryButton -demoPromptLabel.font = Font(FontFamily.Label, FontSize.SmallControl) -tryButton.action = Action(owner, 'closeDialog') -tryButton.keyEquivalent = "\\r" -enterKeyButton.action = Action(owner, 'showEnterCode') -buyButton.action = Action(owner, 'buy') -fairwareButton.action = Action(owner, 'moreInfo') - -for button in (tryButton, enterKeyButton, buyButton, fairwareButton): - button.width = 113 -demoPromptLabel.height = 185 - -demoPromptLabel.packToCorner(Pack.UpperLeft) -demoPromptLabel.fill(Pack.Right) -tryButton.packRelativeTo(demoPromptLabel, Pack.Below, Pack.Left) -enterKeyButton.packRelativeTo(tryButton, Pack.Right, Pack.Middle) -buyButton.packRelativeTo(enterKeyButton, Pack.Right, Pack.Middle) -fairwareButton.packRelativeTo(buyButton, Pack.Right, Pack.Middle) diff --git a/cocoalib/ui/enter_code.py b/cocoalib/ui/enter_code.py deleted file mode 100644 index 70988713..00000000 --- a/cocoalib/ui/enter_code.py +++ /dev/null @@ -1,52 +0,0 @@ -ownerclass = 'HSFairwareReminder' -ownerimport = 'HSFairwareReminder.h' - -result = Window(450, 185, "Enter Key") -result.canClose = False -result.canResize = False -result.canMinimize = False -titleLabel = Label(result, "Enter your key") -promptLabel = Label(result, "Type the key you received when you contributed to %@, as well as the e-mail used as a reference for the purchase.") -regkeyLabel = Label(result, "Registration key:") -regkeyField = TextField(result, "") -regemailLabel = Label(result, "Registration e-mail:") -regemailField = TextField(result, "") -contributeButton = Button(result, "Contribute") -cancelButton = Button(result, "Cancel") -submitButton = Button(result, "Submit") - -owner.codePromptTextField = promptLabel -owner.codeTextField = regkeyField -owner.emailTextField = regemailField -result.initialFirstResponder = regkeyField - -titleLabel.font = Font(FontFamily.Label, FontSize.RegularControl, traits=[FontTrait.Bold]) -smallerFont = Font(FontFamily.Label, FontSize.SmallControl) -for control in (promptLabel, regkeyLabel, regemailLabel): - control.font = smallerFont -regkeyField.usesSingleLineMode = regemailField.usesSingleLineMode = True -contributeButton.action = Action(owner, 'contribute') -cancelButton.action = Action(owner, 'cancelCode') -cancelButton.keyEquivalent = "\\E" -submitButton.action = Action(owner, 'submitCode') -submitButton.keyEquivalent = "\\r" - -for button in (contributeButton, cancelButton, submitButton): - button.width = 100 -regkeyLabel.width = 128 -regemailLabel.width = 128 -promptLabel.height = 32 - -titleLabel.packToCorner(Pack.UpperLeft) -titleLabel.fill(Pack.Right) -promptLabel.packRelativeTo(titleLabel, Pack.Below, Pack.Left) -promptLabel.fill(Pack.Right) -regkeyField.packRelativeTo(promptLabel, Pack.Below, Pack.Right) -regkeyLabel.packRelativeTo(regkeyField, Pack.Left, Pack.Middle) -regkeyField.fill(Pack.Left) -regemailField.packRelativeTo(regkeyField, Pack.Below, Pack.Right) -regemailLabel.packRelativeTo(regemailField, Pack.Left, Pack.Middle) -regemailField.fill(Pack.Left) -contributeButton.packRelativeTo(regemailLabel, Pack.Below, Pack.Left) -submitButton.packRelativeTo(regemailField, Pack.Below, Pack.Right) -cancelButton.packRelativeTo(submitButton, Pack.Left, Pack.Middle) diff --git a/cocoalib/ui/fairware_about.py b/cocoalib/ui/fairware_about.py deleted file mode 100644 index 9168bb72..00000000 --- a/cocoalib/ui/fairware_about.py +++ /dev/null @@ -1,46 +0,0 @@ -ownerclass = 'HSFairwareAboutBox' -ownerimport = 'HSFairwareAboutBox.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")) -registeredLabel = Label(result, "This app is registered, thanks!") -registerButton = Button(result, "Register") - -owner.window = result -owner.titleTextField = titleLabel -owner.versionTextField = versionLabel -owner.copyrightTextField = copyrightLabel -owner.registeredTextField = registeredLabel -owner.registerButton = registerButton -for label in (titleLabel, versionLabel, copyrightLabel, registeredLabel): - label.alignment = const.NSCenterTextAlignment -titleLabel.font = Font(FontFamily.Label, FontSize.RegularControl, traits=[FontTrait.Bold]) -for label in (versionLabel, copyrightLabel, registeredLabel): - label.font = Font(FontFamily.Label, FontSize.SmallControl) - label.height = 14 -registerButton.bezelStyle = const.NSRoundRectBezelStyle -registerButton.action = Action(owner, 'showRegisterDialog') - -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) -registeredLabel.packRelativeTo(copyrightLabel, Pack.Below, Pack.Left) -registeredLabel.fill(Pack.Right) -registeredLabel.setAnchor(Pack.UpperLeft, growX=True) -registerButton.packRelativeTo(copyrightLabel, Pack.Below, Pack.Middle) diff --git a/core/app.py b/core/app.py index 8ad03f96..73aeae8c 100644 --- a/core/app.py +++ b/core/app.py @@ -16,15 +16,14 @@ import shutil from send2trash import send2trash from jobprogress import job -from hscommon.reg import RegistrableApplication from hscommon.notify import Broadcaster from hscommon.path import Path from hscommon.conflict import smart_move, smart_copy from hscommon.gui.progress_window import ProgressWindow -from hscommon.util import (delete_if_empty, first, escape, nonone, format_time_decimal, allsame, - rem_file_ext) +from hscommon.util import delete_if_empty, first, escape, nonone, format_time_decimal, allsame from hscommon.trans import tr from hscommon.plat import ISWINDOWS +from hscommon import desktop from . import directories, results, scanner, export, fs from .gui.deletion_options import DeletionOptions @@ -89,10 +88,7 @@ def format_dupe_count(c): return str(c) if c else '---' def cmp_value(dupe, attrname): - if attrname == 'name': - value = rem_file_ext(dupe.name) - else: - value = getattr(dupe, attrname, '') + value = getattr(dupe, attrname, '') return value.lower() if isinstance(value, str) else value def fix_surrogate_encoding(s, encoding='utf-8'): @@ -112,7 +108,7 @@ def fix_surrogate_encoding(s, encoding='utf-8'): else: return s -class DupeGuru(RegistrableApplication, Broadcaster): +class DupeGuru(Broadcaster): """Holds everything together. Instantiated once per running application, it holds a reference to every high-level object @@ -144,6 +140,10 @@ class DupeGuru(RegistrableApplication, Broadcaster): Instance of :mod:`meta-gui ` table listing the results from :attr:`results` """ #--- View interface + # get_default(key_name) + # set_default(key_name, value) + # show_message(msg) + # open_url(url) # open_path(path) # reveal_path(path) # ask_yes_no(prompt) --> bool @@ -154,15 +154,14 @@ class DupeGuru(RegistrableApplication, Broadcaster): # in fairware prompts, we don't mention the edition, it's too long. PROMPT_NAME = "dupeGuru" - DEMO_LIMITATION = tr("will only be able to delete, move or copy 10 duplicates at once") - def __init__(self, view, appdata): + def __init__(self, view): if view.get_default(DEBUG_MODE_PREFERENCE): logging.getLogger().setLevel(logging.DEBUG) logging.debug("Debug mode enabled") - RegistrableApplication.__init__(self, view, appid=1) Broadcaster.__init__(self) - self.appdata = appdata + self.view = view + self.appdata = desktop.special_folder_path(desktop.SpecialFolder.AppData, appname=self.NAME) if not op.exists(self.appdata): os.makedirs(self.appdata) self.directories = directories.Directories() @@ -206,13 +205,11 @@ class DupeGuru(RegistrableApplication, Broadcaster): else: result = cmp_value(dupe, key) if delta: - refval = getattr(get_group().ref, key) + refval = cmp_value(get_group().ref, key) if key in self.result_table.DELTA_COLUMNS: result -= refval else: - # We use directly getattr() because cmp_value() does thing that we don't want to do - # when we want to determine whether two values are exactly the same. - same = getattr(dupe, key) == refval + same = cmp_value(dupe, key) == refval result = (same, result) return result @@ -250,7 +247,7 @@ class DupeGuru(RegistrableApplication, Broadcaster): ref = group.ref linkfunc = os.link if use_hardlinks else os.symlink linkfunc(str(ref.path), str_path) - self.clean_empty_dirs(dupe.path[:-1]) + self.clean_empty_dirs(dupe.path.parent()) def _create_file(self, path): # We add fs.Folder to fileclasses in case the file we're loading contains folder paths. @@ -337,13 +334,6 @@ class DupeGuru(RegistrableApplication, Broadcaster): self.selected_dupes = dupes self.notify('dupes_selected') - def _check_demo(self): - if self.should_apply_demo_limitation and self.results.mark_count > 10: - msg = tr("You cannot delete, move or copy more than 10 duplicates at once in demo mode.") - self.view.show_message(msg) - return False - return True - #--- Public def add_directory(self, d): """Adds folder ``d`` to :attr:`directories`. @@ -393,7 +383,7 @@ class DupeGuru(RegistrableApplication, Broadcaster): def clean_empty_dirs(self, path): if self.options['clean_empty_dirs']: while delete_if_empty(path, ['.DS_Store']): - path = path[:-1] + path = path.parent() def copy_or_move(self, dupe, copy: bool, destination: str, dest_type: DestType): source_path = dupe.path @@ -401,21 +391,21 @@ class DupeGuru(RegistrableApplication, Broadcaster): dest_path = Path(destination) if dest_type in {DestType.Relative, DestType.Absolute}: # no filename, no windows drive letter - source_base = source_path.remove_drive_letter()[:-1] + source_base = source_path.remove_drive_letter().parent() if dest_type == DestType.Relative: source_base = source_base[location_path:] - dest_path = dest_path + source_base + dest_path = dest_path[source_base] if not dest_path.exists(): dest_path.makedirs() # Add filename to dest_path. For file move/copy, it's not required, but for folders, yes. - dest_path = dest_path + source_path[-1] + dest_path = dest_path[source_path.name] logging.debug("Copy/Move operation from '%s' to '%s'", source_path, dest_path) # Raises an EnvironmentError if there's a problem if copy: smart_copy(source_path, dest_path) else: smart_move(source_path, dest_path) - self.clean_empty_dirs(source_path[:-1]) + self.clean_empty_dirs(source_path.parent()) def copy_or_move_marked(self, copy): """Start an async move (or copy) job on marked duplicates. @@ -430,8 +420,6 @@ class DupeGuru(RegistrableApplication, Broadcaster): j.start_job(self.results.mark_count) self.results.perform_on_marked(op, not copy) - if not self._check_demo(): - return if not self.results.mark_count: self.view.show_message(MSG_NO_MARKED_DUPES) return @@ -446,8 +434,6 @@ class DupeGuru(RegistrableApplication, Broadcaster): def delete_marked(self): """Start an async job to send marked duplicates to the trash. """ - if not self._check_demo(): - return if not self.results.mark_count: self.view.show_message(MSG_NO_MARKED_DUPES) return @@ -467,7 +453,7 @@ class DupeGuru(RegistrableApplication, Broadcaster): """ colnames, rows = self._get_export_data() export_path = export.export_to_xhtml(colnames, rows) - self.view.open_path(export_path) + desktop.open_path(export_path) def export_to_csv(self): """Export current results to CSV. @@ -613,7 +599,7 @@ class DupeGuru(RegistrableApplication, Broadcaster): if not self.view.ask_yes_no(MSG_MANY_FILES_TO_OPEN): return for dupe in self.selected_dupes: - self.view.open_path(dupe.path) + desktop.open_path(dupe.path) def purge_ignore_list(self): """Remove files that don't exist from :attr:`ignore_list`. @@ -704,7 +690,7 @@ class DupeGuru(RegistrableApplication, Broadcaster): def reveal_selected(self): if self.selected_dupes: - self.view.reveal_path(self.selected_dupes[0].path) + desktop.reveal_path(self.selected_dupes[0].path) def save(self): if not op.exists(self.appdata): diff --git a/core/directories.py b/core/directories.py index fa605dec..6b84295b 100644 --- a/core/directories.py +++ b/core/directories.py @@ -73,7 +73,7 @@ class Directories: #---Private def _default_state_for_path(self, path): # Override this in subclasses to specify the state of some special folders. - if path[-1].startswith('.'): # hidden + if path.name.startswith('.'): # hidden return DirectoryState.Excluded def _get_files(self, from_path, j): @@ -94,9 +94,8 @@ class Directories: file.is_ref = state == DirectoryState.Reference filepaths.add(file.path) yield file - subpaths = [from_path + name for name in from_path.listdir()] # it's possible that a folder (bundle) gets into the file list. in that case, we don't want to recurse into it - subfolders = [p for p in subpaths if not p.islink() and p.isdir() and p not in filepaths] + subfolders = [p for p in from_path.listdir() if not p.islink() and p.isdir() and p not in filepaths] for subfolder in subfolders: for file in self._get_files(subfolder, j): yield file @@ -143,9 +142,9 @@ class Directories: :rtype: list of Path """ try: - names = [name for name in path.listdir() if (path + name).isdir()] - names.sort(key=lambda x:x.lower()) - return [path + name for name in names] + subpaths = [p for p in path.listdir() if p.isdir()] + subpaths.sort(key=lambda x:x.name.lower()) + return subpaths except EnvironmentError: return [] @@ -178,7 +177,7 @@ class Directories: default_state = self._default_state_for_path(path) if default_state is not None: return default_state - parent = path[:-1] + parent = path.parent() if parent in self: return self.get_state(parent) else: diff --git a/core/fs.py b/core/fs.py index a0330e32..fddcd411 100644 --- a/core/fs.py +++ b/core/fs.py @@ -150,9 +150,9 @@ class File: def rename(self, newname): if newname == self.name: return - destpath = self.path[:-1] + newname + destpath = self.path.parent()[newname] if destpath.exists(): - raise AlreadyExistsError(newname, self.path[:-1]) + raise AlreadyExistsError(newname, self.path.parent()) try: self.path.rename(destpath) except EnvironmentError: @@ -173,11 +173,11 @@ class File: @property def name(self): - return self.path[-1] + return self.path.name @property def folder_path(self): - return self.path[:-1] + return self.path.parent() class Folder(File): @@ -219,8 +219,7 @@ class Folder(File): @property def subfolders(self): if self._subfolders is None: - subpaths = [self.path + name for name in self.path.listdir()] - subfolders = [p for p in subpaths if not p.islink() and p.isdir()] + subfolders = [p for p in self.path.listdir() if not p.islink() and p.isdir()] self._subfolders = [self.__class__(p) for p in subfolders] return self._subfolders @@ -248,18 +247,9 @@ def get_files(path, fileclasses=[File]): :param fileclasses: List of candidate :class:`File` classes """ assert all(issubclass(fileclass, File) for fileclass in fileclasses) - def combine_paths(p1, p2): - try: - return p1 + p2 - except Exception: - # This is temporary debug logging for #84. - logging.warning("Failed to combine %r and %r.", p1, p2) - raise - try: - paths = [combine_paths(path, name) for name in path.listdir()] result = [] - for path in paths: + for path in path.listdir(): file = get_file(path, fileclasses=fileclasses) if file is not None: result.append(file) diff --git a/core/gui/deletion_options.py b/core/gui/deletion_options.py index 217089c2..54dcdd86 100644 --- a/core/gui/deletion_options.py +++ b/core/gui/deletion_options.py @@ -10,14 +10,60 @@ import os from hscommon.gui.base import GUIObject from hscommon.trans import tr +class DeletionOptionsView: + """Expected interface for :class:`DeletionOptions`'s view. + + *Not actually used in the code. For documentation purposes only.* + + Our view presents the user with an appropriate way (probably a mix of checkboxes and radio + buttons) to set the different flags in :class:`DeletionOptions`. Note that + :attr:`DeletionOptions.use_hardlinks` is only relevant if :attr:`DeletionOptions.link_deleted` + is true. This is why we toggle the "enabled" state of that flag. + + We expect the view to set :attr:`DeletionOptions.link_deleted` immediately as the user changes + its value because it will toggle :meth:`set_hardlink_option_enabled` + + Other than the flags, there's also a prompt message which has a dynamic content, defined by + :meth:`update_msg`. + """ + def update_msg(self, msg: str): + """Update the dialog's prompt with ``str``. + """ + + def show(self): + """Show the dialog in a modal fashion. + + Returns whether the dialog was "accepted" (the user pressed OK). + """ + + def set_hardlink_option_enabled(self, is_enabled: bool): + """Enable or disable the widget controlling :attr:`DeletionOptions.use_hardlinks`. + """ + class DeletionOptions(GUIObject): - #--- View interface - # update_msg(msg: str) - # show() - # + """Present the user with deletion options before proceeding. + + When the user activates "Send to trash", we present him with a couple of options that changes + the behavior of that deletion operation. + """ + def __init__(self): + GUIObject.__init__(self) + #: Whether symlinks or hardlinks are used when doing :attr:`link_deleted`. + #: *bool*. *get/set* + self.use_hardlinks = False + #: Delete dupes directly and don't send to trash. + #: *bool*. *get/set* + self.direct = False def show(self, mark_count): - self.link_deleted = False + """Prompt the user with a modal dialog offering our deletion options. + + :param int mark_count: Number of dupes marked for deletion. + :rtype: bool + :returns: Whether the user accepted the dialog (we cancel deletion if false). + """ + self._link_deleted = False + self.view.set_hardlink_option_enabled(False) self.use_hardlinks = False self.direct = False msg = tr("You are sending {} file(s) to the Trash.").format(mark_count) @@ -25,6 +71,8 @@ class DeletionOptions(GUIObject): return self.view.show() def supports_links(self): + """Returns whether our platform supports symlinks. + """ # When on a platform that doesn't implement it, calling os.symlink() (with the wrong number # of arguments) raises NotImplementedError, which allows us to gracefully check for the # feature. @@ -40,3 +88,20 @@ class DeletionOptions(GUIObject): # wrong number of arguments return True + @property + def link_deleted(self): + """Replace deleted dupes with symlinks (or hardlinks) to the dupe group reference. + + *bool*. *get/set* + + Whether the link is a symlink or hardlink is decided by :attr:`use_hardlinks`. + """ + return self._link_deleted + + @link_deleted.setter + def link_deleted(self, value): + self._link_deleted = value + hardlinks_enabled = value and self.supports_links() + self.view.set_hardlink_option_enabled(hardlinks_enabled) + + diff --git a/core/gui/directory_tree.py b/core/gui/directory_tree.py index 4d7c142f..cc427b1c 100644 --- a/core/gui/directory_tree.py +++ b/core/gui/directory_tree.py @@ -31,7 +31,7 @@ class DirectoryNode(Node): self.clear() subpaths = self._tree.app.directories.get_subfolders(self._directory_path) for path in subpaths: - self.append(DirectoryNode(self._tree, path, path[-1])) + self.append(DirectoryNode(self._tree, path, path.name)) self._loaded = True def update_all_states(self): @@ -91,6 +91,10 @@ class DirectoryTree(Tree, DupeGuruGUIObject): for node in nodes: node.state = newstate + def select_all(self): + self.selected_nodes = list(self) + self.view.refresh() + def update_all_states(self): for node in self: node.update_all_states() diff --git a/core/gui/problem_dialog.py b/core/gui/problem_dialog.py index d458c022..9271ae83 100644 --- a/core/gui/problem_dialog.py +++ b/core/gui/problem_dialog.py @@ -6,6 +6,8 @@ # which should be included with this package. The terms are also available at # http://www.hardcoded.net/licenses/bsd_license +from hscommon import desktop + from .problem_table import ProblemTable class ProblemDialog: @@ -20,7 +22,7 @@ class ProblemDialog: def reveal_selected_dupe(self): if self._selected_dupe is not None: - self.app.view.reveal_path(self._selected_dupe.path) + desktop.reveal_path(self._selected_dupe.path) def select_dupe(self, dupe): self._selected_dupe = dupe diff --git a/core/gui/result_table.py b/core/gui/result_table.py index a9862f71..155db06f 100644 --- a/core/gui/result_table.py +++ b/core/gui/result_table.py @@ -42,7 +42,7 @@ class DupeRow(Row): dupe_info = self.data ref_info = self._group.ref.get_display_info(group=self._group, delta=False) for key, value in dupe_info.items(): - if ref_info[key] != value: + if (key not in self._delta_columns) and (ref_info[key].lower() != value.lower()): self._delta_columns.add(key) return column_name in self._delta_columns diff --git a/core/tests/app_test.py b/core/tests/app_test.py index ee90ba15..0d07e225 100644 --- a/core/tests/app_test.py +++ b/core/tests/app_test.py @@ -11,7 +11,6 @@ import os.path as op import logging from pytest import mark -from hscommon import io from hscommon.path import Path import hscommon.conflict import hscommon.util @@ -57,7 +56,7 @@ class TestCaseDupeGuru: # for this unit is pathetic. What's done is done. My approach now is to add tests for # every change I want to make. The blowup was caused by a missing import. p = Path(str(tmpdir)) - io.open(p + 'foo', 'w').close() + p['foo'].open('w').close() monkeypatch.setattr(hscommon.conflict, 'smart_copy', log_calls(lambda source_path, dest_path: None)) # XXX This monkeypatch is temporary. will be fixed in a better monkeypatcher. monkeypatch.setattr(app, 'smart_copy', hscommon.conflict.smart_copy) @@ -73,14 +72,14 @@ class TestCaseDupeGuru: def test_copy_or_move_clean_empty_dirs(self, tmpdir, monkeypatch): tmppath = Path(str(tmpdir)) - sourcepath = tmppath + 'source' - io.mkdir(sourcepath) - io.open(sourcepath + 'myfile', 'w') + sourcepath = tmppath['source'] + sourcepath.mkdir() + sourcepath['myfile'].open('w') app = TestApp().app app.directories.add_path(tmppath) [myfile] = app.directories.get_files() monkeypatch.setattr(app, 'clean_empty_dirs', log_calls(lambda path: None)) - app.copy_or_move(myfile, False, tmppath + 'dest', 0) + app.copy_or_move(myfile, False, tmppath['dest'], 0) calls = app.clean_empty_dirs.calls eq_(1, len(calls)) eq_(sourcepath, calls[0]['path']) @@ -104,8 +103,8 @@ class TestCaseDupeGuru: # If the ignore_hardlink_matches option is set, don't match files hardlinking to the same # inode. tmppath = Path(str(tmpdir)) - io.open(tmppath + 'myfile', 'w').write('foo') - os.link(str(tmppath + 'myfile'), str(tmppath + 'hardlink')) + tmppath['myfile'].open('w').write('foo') + os.link(str(tmppath['myfile']), str(tmppath['hardlink'])) app = TestApp().app app.directories.add_path(tmppath) app.scanner.scan_type = ScanType.Contents @@ -171,8 +170,8 @@ class TestCaseDupeGuruWithResults: self.rtable.refresh() tmpdir = request.getfuncargvalue('tmpdir') tmppath = Path(str(tmpdir)) - io.mkdir(tmppath + 'foo') - io.mkdir(tmppath + 'bar') + tmppath['foo'].mkdir() + tmppath['bar'].mkdir() self.app.directories.add_path(tmppath) def test_GetObjects(self, do_setup): @@ -417,11 +416,11 @@ class TestCaseDupeGuru_renameSelected: def pytest_funcarg__do_setup(self, request): tmpdir = request.getfuncargvalue('tmpdir') p = Path(str(tmpdir)) - fp = open(str(p + 'foo bar 1'),mode='w') + fp = open(str(p['foo bar 1']),mode='w') fp.close() - fp = open(str(p + 'foo bar 2'),mode='w') + fp = open(str(p['foo bar 2']),mode='w') fp.close() - fp = open(str(p + 'foo bar 3'),mode='w') + fp = open(str(p['foo bar 3']),mode='w') fp.close() files = fs.get_files(p) for f in files: @@ -444,7 +443,7 @@ class TestCaseDupeGuru_renameSelected: g = self.groups[0] self.rtable.select([1]) assert app.rename_selected('renamed') - names = io.listdir(self.p) + names = [p.name for p in self.p.listdir()] assert 'renamed' in names assert 'foo bar 2' not in names eq_(g.dupes[0].name, 'renamed') @@ -457,7 +456,7 @@ class TestCaseDupeGuru_renameSelected: assert not app.rename_selected('renamed') msg = logging.warning.calls[0]['msg'] eq_('dupeGuru Warning: list index out of range', msg) - names = io.listdir(self.p) + names = [p.name for p in self.p.listdir()] assert 'renamed' not in names assert 'foo bar 2' in names eq_(g.dupes[0].name, 'foo bar 2') @@ -470,7 +469,7 @@ class TestCaseDupeGuru_renameSelected: assert not app.rename_selected('foo bar 1') msg = logging.warning.calls[0]['msg'] assert msg.startswith('dupeGuru Warning: \'foo bar 1\' already exists in') - names = io.listdir(self.p) + names = [p.name for p in self.p.listdir()] assert 'foo bar 1' in names assert 'foo bar 2' in names eq_(g.dupes[0].name, 'foo bar 2') @@ -480,9 +479,9 @@ class TestAppWithDirectoriesInTree: def pytest_funcarg__do_setup(self, request): tmpdir = request.getfuncargvalue('tmpdir') p = Path(str(tmpdir)) - io.mkdir(p + 'sub1') - io.mkdir(p + 'sub2') - io.mkdir(p + 'sub3') + p['sub1'].mkdir() + p['sub2'].mkdir() + p['sub3'].mkdir() app = TestApp() self.app = app.app self.dtree = app.dtree diff --git a/core/tests/base.py b/core/tests/base.py index a8fa2814..0b45e29f 100644 --- a/core/tests/base.py +++ b/core/tests/base.py @@ -57,10 +57,12 @@ class ResultTable(ResultTableBase): DELTA_COLUMNS = {'size', } class DupeGuru(DupeGuruBase): + NAME = 'dupeGuru' METADATA_TO_READ = ['size'] def __init__(self): - DupeGuruBase.__init__(self, DupeGuruView(), '/tmp') + DupeGuruBase.__init__(self, DupeGuruView()) + self.appdata = '/tmp' def _prioritization_categories(self): return prioritize.all_categories() @@ -100,11 +102,11 @@ class NamedObject: @property def path(self): - return self._folder + self.name + return self._folder[self.name] @property def folder_path(self): - return self.path[:-1] + return self.path.parent() @property def extension(self): diff --git a/core/tests/directories_test.py b/core/tests/directories_test.py index d5e05796..0c789d46 100644 --- a/core/tests/directories_test.py +++ b/core/tests/directories_test.py @@ -12,7 +12,6 @@ import tempfile import shutil from pytest import raises -from hscommon import io from hscommon.path import Path from hscommon.testutil import eq_ @@ -20,27 +19,27 @@ from ..directories import * def create_fake_fs(rootpath): # We have it as a separate function because other units are using it. - rootpath = rootpath + 'fs' - io.mkdir(rootpath) - io.mkdir(rootpath + 'dir1') - io.mkdir(rootpath + 'dir2') - io.mkdir(rootpath + 'dir3') - fp = io.open(rootpath + 'file1.test', 'w') + rootpath = rootpath['fs'] + rootpath.mkdir() + rootpath['dir1'].mkdir() + rootpath['dir2'].mkdir() + rootpath['dir3'].mkdir() + fp = rootpath['file1.test'].open('w') fp.write('1') fp.close() - fp = io.open(rootpath + 'file2.test', 'w') + fp = rootpath['file2.test'].open('w') fp.write('12') fp.close() - fp = io.open(rootpath + 'file3.test', 'w') + fp = rootpath['file3.test'].open('w') fp.write('123') fp.close() - fp = io.open(rootpath + ('dir1', 'file1.test'), 'w') + fp = rootpath['dir1']['file1.test'].open('w') fp.write('1') fp.close() - fp = io.open(rootpath + ('dir2', 'file2.test'), 'w') + fp = rootpath['dir2']['file2.test'].open('w') fp.write('12') fp.close() - fp = io.open(rootpath + ('dir3', 'file3.test'), 'w') + fp = rootpath['dir3']['file3.test'].open('w') fp.write('123') fp.close() return rootpath @@ -50,9 +49,9 @@ def setup_module(module): # and another with a more complex structure. testpath = Path(tempfile.mkdtemp()) module.testpath = testpath - rootpath = testpath + 'onefile' - io.mkdir(rootpath) - fp = io.open(rootpath + 'test.txt', 'w') + rootpath = testpath['onefile'] + rootpath.mkdir() + fp = rootpath['test.txt'].open('w') fp.write('test_data') fp.close() create_fake_fs(testpath) @@ -67,30 +66,30 @@ def test_empty(): def test_add_path(): d = Directories() - p = testpath + 'onefile' + p = testpath['onefile'] d.add_path(p) eq_(1,len(d)) assert p in d - assert (p + 'foobar') in d - assert p[:-1] not in d - p = testpath + 'fs' + assert (p['foobar']) in d + assert p.parent() not in d + p = testpath['fs'] d.add_path(p) eq_(2,len(d)) assert p in d def test_AddPath_when_path_is_already_there(): d = Directories() - p = testpath + 'onefile' + p = testpath['onefile'] d.add_path(p) with raises(AlreadyThereError): d.add_path(p) with raises(AlreadyThereError): - d.add_path(p + 'foobar') + d.add_path(p['foobar']) eq_(1, len(d)) def test_add_path_containing_paths_already_there(): d = Directories() - d.add_path(testpath + 'onefile') + d.add_path(testpath['onefile']) eq_(1, len(d)) d.add_path(testpath) eq_(len(d), 1) @@ -98,7 +97,7 @@ def test_add_path_containing_paths_already_there(): def test_AddPath_non_latin(tmpdir): p = Path(str(tmpdir)) - to_add = p + 'unicode\u201a' + to_add = p['unicode\u201a'] os.mkdir(str(to_add)) d = Directories() try: @@ -108,24 +107,24 @@ def test_AddPath_non_latin(tmpdir): def test_del(): d = Directories() - d.add_path(testpath + 'onefile') + d.add_path(testpath['onefile']) try: del d[1] assert False except IndexError: pass - d.add_path(testpath + 'fs') + d.add_path(testpath['fs']) del d[1] eq_(1, len(d)) def test_states(): d = Directories() - p = testpath + 'onefile' + p = testpath['onefile'] d.add_path(p) eq_(DirectoryState.Normal ,d.get_state(p)) d.set_state(p, DirectoryState.Reference) eq_(DirectoryState.Reference ,d.get_state(p)) - eq_(DirectoryState.Reference ,d.get_state(p + 'dir1')) + eq_(DirectoryState.Reference ,d.get_state(p['dir1'])) eq_(1,len(d.states)) eq_(p,list(d.states.keys())[0]) eq_(DirectoryState.Reference ,d.states[p]) @@ -133,67 +132,67 @@ def test_states(): def test_get_state_with_path_not_there(): # When the path's not there, just return DirectoryState.Normal d = Directories() - d.add_path(testpath + 'onefile') + d.add_path(testpath['onefile']) eq_(d.get_state(testpath), DirectoryState.Normal) def test_states_remain_when_larger_directory_eat_smaller_ones(): d = Directories() - p = testpath + 'onefile' + p = testpath['onefile'] d.add_path(p) d.set_state(p, DirectoryState.Excluded) d.add_path(testpath) d.set_state(testpath, DirectoryState.Reference) eq_(DirectoryState.Excluded ,d.get_state(p)) - eq_(DirectoryState.Excluded ,d.get_state(p + 'dir1')) + eq_(DirectoryState.Excluded ,d.get_state(p['dir1'])) eq_(DirectoryState.Reference ,d.get_state(testpath)) def test_set_state_keep_state_dict_size_to_minimum(): d = Directories() - p = testpath + 'fs' + p = testpath['fs'] d.add_path(p) d.set_state(p, DirectoryState.Reference) - d.set_state(p + 'dir1', DirectoryState.Reference) + d.set_state(p['dir1'], DirectoryState.Reference) eq_(1,len(d.states)) - eq_(DirectoryState.Reference ,d.get_state(p + 'dir1')) - d.set_state(p + 'dir1', DirectoryState.Normal) + eq_(DirectoryState.Reference ,d.get_state(p['dir1'])) + d.set_state(p['dir1'], DirectoryState.Normal) eq_(2,len(d.states)) - eq_(DirectoryState.Normal ,d.get_state(p + 'dir1')) - d.set_state(p + 'dir1', DirectoryState.Reference) + eq_(DirectoryState.Normal ,d.get_state(p['dir1'])) + d.set_state(p['dir1'], DirectoryState.Reference) eq_(1,len(d.states)) - eq_(DirectoryState.Reference ,d.get_state(p + 'dir1')) + eq_(DirectoryState.Reference ,d.get_state(p['dir1'])) def test_get_files(): d = Directories() - p = testpath + 'fs' + p = testpath['fs'] d.add_path(p) - d.set_state(p + 'dir1', DirectoryState.Reference) - d.set_state(p + 'dir2', DirectoryState.Excluded) + d.set_state(p['dir1'], DirectoryState.Reference) + d.set_state(p['dir2'], DirectoryState.Excluded) files = list(d.get_files()) eq_(5, len(files)) for f in files: - if f.path[:-1] == p + 'dir1': + if f.path.parent() == p['dir1']: assert f.is_ref else: assert not f.is_ref def test_get_folders(): d = Directories() - p = testpath + 'fs' + p = testpath['fs'] d.add_path(p) - d.set_state(p + 'dir1', DirectoryState.Reference) - d.set_state(p + 'dir2', DirectoryState.Excluded) + d.set_state(p['dir1'], DirectoryState.Reference) + d.set_state(p['dir2'], DirectoryState.Excluded) folders = list(d.get_folders()) eq_(len(folders), 3) ref = [f for f in folders if f.is_ref] not_ref = [f for f in folders if not f.is_ref] eq_(len(ref), 1) - eq_(ref[0].path, p + 'dir1') + eq_(ref[0].path, p['dir1']) eq_(len(not_ref), 2) eq_(ref[0].size, 1) def test_get_files_with_inherited_exclusion(): d = Directories() - p = testpath + 'onefile' + p = testpath['onefile'] d.add_path(p) d.set_state(p, DirectoryState.Excluded) eq_([], list(d.get_files())) @@ -202,19 +201,19 @@ def test_save_and_load(tmpdir): d1 = Directories() d2 = Directories() p1 = Path(str(tmpdir.join('p1'))) - io.mkdir(p1) + p1.mkdir() p2 = Path(str(tmpdir.join('p2'))) - io.mkdir(p2) + p2.mkdir() d1.add_path(p1) d1.add_path(p2) d1.set_state(p1, DirectoryState.Reference) - d1.set_state(p1 + 'dir1', DirectoryState.Excluded) + d1.set_state(p1['dir1'], DirectoryState.Excluded) tmpxml = str(tmpdir.join('directories_testunit.xml')) d1.save_to_file(tmpxml) d2.load_from_file(tmpxml) eq_(2, len(d2)) eq_(DirectoryState.Reference ,d2.get_state(p1)) - eq_(DirectoryState.Excluded ,d2.get_state(p1 + 'dir1')) + eq_(DirectoryState.Excluded ,d2.get_state(p1['dir1'])) def test_invalid_path(): d = Directories() @@ -234,12 +233,12 @@ def test_load_from_file_with_invalid_path(tmpdir): #This test simulates a load from file resulting in a #InvalidPath raise. Other directories must be loaded. d1 = Directories() - d1.add_path(testpath + 'onefile') + d1.add_path(testpath['onefile']) #Will raise InvalidPath upon loading p = Path(str(tmpdir.join('toremove'))) - io.mkdir(p) + p.mkdir() d1.add_path(p) - io.rmdir(p) + p.rmdir() tmpxml = str(tmpdir.join('directories_testunit.xml')) d1.save_to_file(tmpxml) d2 = Directories() @@ -248,11 +247,11 @@ def test_load_from_file_with_invalid_path(tmpdir): def test_unicode_save(tmpdir): d = Directories() - p1 = Path(str(tmpdir)) + 'hello\xe9' - io.mkdir(p1) - io.mkdir(p1 + 'foo\xe9') + p1 = Path(str(tmpdir))['hello\xe9'] + p1.mkdir() + p1['foo\xe9'].mkdir() d.add_path(p1) - d.set_state(p1 + 'foo\xe9', DirectoryState.Excluded) + d.set_state(p1['foo\xe9'], DirectoryState.Excluded) tmpxml = str(tmpdir.join('directories_testunit.xml')) try: d.save_to_file(tmpxml) @@ -261,12 +260,12 @@ def test_unicode_save(tmpdir): def test_get_files_refreshes_its_directories(): d = Directories() - p = testpath + 'fs' + p = testpath['fs'] d.add_path(p) files = d.get_files() eq_(6, len(list(files))) time.sleep(1) - os.remove(str(p + ('dir1','file1.test'))) + os.remove(str(p['dir1']['file1.test'])) files = d.get_files() eq_(5, len(list(files))) @@ -274,14 +273,14 @@ def test_get_files_does_not_choke_on_non_existing_directories(tmpdir): d = Directories() p = Path(str(tmpdir)) d.add_path(p) - io.rmtree(p) + p.rmtree() eq_([], list(d.get_files())) def test_get_state_returns_excluded_by_default_for_hidden_directories(tmpdir): d = Directories() p = Path(str(tmpdir)) - hidden_dir_path = p + '.foo' - io.mkdir(p + '.foo') + hidden_dir_path = p['.foo'] + p['.foo'].mkdir() d.add_path(p) eq_(d.get_state(hidden_dir_path), DirectoryState.Excluded) # But it can be overriden @@ -297,16 +296,16 @@ def test_default_path_state_override(tmpdir): d = MyDirectories() p1 = Path(str(tmpdir)) - io.mkdir(p1 + 'foobar') - io.open(p1 + 'foobar/somefile', 'w').close() - io.mkdir(p1 + 'foobaz') - io.open(p1 + 'foobaz/somefile', 'w').close() + p1['foobar'].mkdir() + p1['foobar/somefile'].open('w').close() + p1['foobaz'].mkdir() + p1['foobaz/somefile'].open('w').close() d.add_path(p1) - eq_(d.get_state(p1 + 'foobaz'), DirectoryState.Normal) - eq_(d.get_state(p1 + 'foobar'), DirectoryState.Excluded) + eq_(d.get_state(p1['foobaz']), DirectoryState.Normal) + eq_(d.get_state(p1['foobar']), DirectoryState.Excluded) eq_(len(list(d.get_files())), 1) # only the 'foobaz' file is there # However, the default state can be changed - d.set_state(p1 + 'foobar', DirectoryState.Normal) - eq_(d.get_state(p1 + 'foobar'), DirectoryState.Normal) + d.set_state(p1['foobar'], DirectoryState.Normal) + eq_(d.get_state(p1['foobar']), DirectoryState.Normal) eq_(len(list(d.get_files())), 2) diff --git a/core/tests/fs_test.py b/core/tests/fs_test.py index f4186cec..059e2020 100644 --- a/core/tests/fs_test.py +++ b/core/tests/fs_test.py @@ -25,12 +25,12 @@ def test_md5_aggregate_subfiles_sorted(tmpdir): #same order everytime. p = create_fake_fs(Path(str(tmpdir))) b = fs.Folder(p) - md51 = fs.File(p + ('dir1', 'file1.test')).md5 - md52 = fs.File(p + ('dir2', 'file2.test')).md5 - md53 = fs.File(p + ('dir3', 'file3.test')).md5 - md54 = fs.File(p + 'file1.test').md5 - md55 = fs.File(p + 'file2.test').md5 - md56 = fs.File(p + 'file3.test').md5 + md51 = fs.File(p['dir1']['file1.test']).md5 + md52 = fs.File(p['dir2']['file2.test']).md5 + md53 = fs.File(p['dir3']['file3.test']).md5 + md54 = fs.File(p['file1.test']).md5 + md55 = fs.File(p['file2.test']).md5 + md56 = fs.File(p['file3.test']).md5 # The expected md5 is the md5 of md5s for folders and the direct md5 for files folder_md51 = hashlib.md5(md51).digest() folder_md52 = hashlib.md5(md52).digest() diff --git a/core/tests/result_table_test.py b/core/tests/result_table_test.py index 9640a433..82b0636a 100644 --- a/core/tests/result_table_test.py +++ b/core/tests/result_table_test.py @@ -44,3 +44,13 @@ def test_delta_flags_delta_mode_on_non_delta_columns(): assert not app.rtable[3].is_cell_delta('name') # "ibabtu" == "ibabtu", flag off assert not app.rtable[4].is_cell_delta('name') + +def test_delta_flags_delta_mode_on_non_delta_columns_case_insensitive(): + # Comparison that occurs for non-numeric columns to check whether they're delta is case + # insensitive + app = app_with_results() + app.app.results.groups[1].ref.name = "ibAbtu" + app.app.results.groups[1].dupes[0].name = "IBaBTU" + app.rtable.delta_values = True + # "ibAbtu" == "IBaBTU", flag off + assert not app.rtable[4].is_cell_delta('name') diff --git a/core/tests/results_test.py b/core/tests/results_test.py index bade68c1..e6803564 100644 --- a/core/tests/results_test.py +++ b/core/tests/results_test.py @@ -230,6 +230,23 @@ class TestCaseResultsWithSomeGroups: # also remove group ref assert self.results.get_group_of_duplicate(ref) is None + def test_dupe_list_sort_delta_values_nonnumeric(self): + # When sorting dupes in delta mode on a non-numeric column, our first sort criteria is if + # the string is the same as its ref. + g1r, g1d1, g1d2, g2r, g2d1 = self.objects + # "aaa" makes our dupe go first in alphabetical order, but since we have the same value as + # ref, we're going last. + g2r.name = g2d1.name = "aaa" + self.results.sort_dupes('name', delta=True) + eq_("aaa", self.results.dupes[2].name) + + def test_dupe_list_sort_delta_values_nonnumeric_case_insensitive(self): + # Non-numeric delta sorting comparison is case insensitive + g1r, g1d1, g1d2, g2r, g2d1 = self.objects + g2r.name = "AaA" + g2d1.name = "aAa" + self.results.sort_dupes('name', delta=True) + eq_("aAa", self.results.dupes[2].name) class TestCaseResultsWithSavedResults: def setup_method(self, method): diff --git a/core/tests/scanner_test.py b/core/tests/scanner_test.py index 08594282..a2c322a1 100644 --- a/core/tests/scanner_test.py +++ b/core/tests/scanner_test.py @@ -7,7 +7,6 @@ # http://www.hardcoded.net/licenses/bsd_license from jobprogress import job -from hscommon import io from hscommon.path import Path from hscommon.testutil import eq_ @@ -21,7 +20,7 @@ class NamedObject: if path is None: path = Path(name) else: - path = Path(path) + name + path = Path(path)[name] self.name = name self.size = size self.path = path @@ -37,7 +36,6 @@ def pytest_funcarg__fake_fileexists(request): # This is a hack to avoid invalidating all previous tests since the scanner started to test # for file existence before doing the match grouping. monkeypatch = request.getfuncargvalue('monkeypatch') - monkeypatch.setattr(io, 'exists', lambda _: True) monkeypatch.setattr(Path, 'exists', lambda _: True) def test_empty(fake_fileexists): @@ -471,11 +469,11 @@ def test_dont_group_files_that_dont_exist(tmpdir): s = Scanner() s.scan_type = ScanType.Contents p = Path(str(tmpdir)) - io.open(p + 'file1', 'w').write('foo') - io.open(p + 'file2', 'w').write('foo') + p['file1'].open('w').write('foo') + p['file2'].open('w').write('foo') file1, file2 = fs.get_files(p) def getmatches(*args, **kw): - io.remove(file2.path) + file2.path.remove() return [Match(file1, file2, 100)] s._getmatches = getmatches diff --git a/core_me/app.py b/core_me/app.py index eb9124da..a49499ea 100644 --- a/core_me/app.py +++ b/core_me/app.py @@ -16,8 +16,8 @@ class DupeGuru(DupeGuruBase): METADATA_TO_READ = ['size', 'mtime', 'duration', 'bitrate', 'samplerate', 'title', 'artist', 'album', 'genre', 'year', 'track', 'comment'] - def __init__(self, view, appdata): - DupeGuruBase.__init__(self, view, appdata) + def __init__(self, view): + DupeGuruBase.__init__(self, view) self.scanner = scanner.ScannerME() self.directories.fileclasses = [fs.MusicFile] diff --git a/core_me/fs.py b/core_me/fs.py index 528f060d..27314628 100644 --- a/core_me/fs.py +++ b/core_me/fs.py @@ -36,7 +36,7 @@ class MusicFile(fs.File): def can_handle(cls, path): if not fs.File.can_handle(path): return False - return get_file_ext(path[-1]) in auto.EXT2CLASS + return get_file_ext(path.name) in auto.EXT2CLASS def get_display_info(self, group, delta): size = self.size diff --git a/core_pe/app.py b/core_pe/app.py index 05b871b2..05927096 100644 --- a/core_pe/app.py +++ b/core_pe/app.py @@ -18,8 +18,8 @@ class DupeGuru(DupeGuruBase): NAME = __appname__ METADATA_TO_READ = ['size', 'mtime', 'dimensions', 'exif_timestamp'] - def __init__(self, view, appdata): - DupeGuruBase.__init__(self, view, appdata) + def __init__(self, view): + DupeGuruBase.__init__(self, view) self.scanner = ScannerPE() self.scanner.cache_path = op.join(self.appdata, 'cached_pictures.db') diff --git a/core_pe/modules/block_osx.m b/core_pe/modules/block_osx.m index f4280dae..e94117ad 100644 --- a/core_pe/modules/block_osx.m +++ b/core_pe/modules/block_osx.m @@ -113,7 +113,7 @@ MyCreateBitmapContext(int width, int height) } context = CGBitmapContextCreate(bitmapData, width, height, 8, bitmapBytesPerRow, colorSpace, - kCGImageAlphaNoneSkipLast); + (CGBitmapInfo)kCGImageAlphaNoneSkipLast); if (context== NULL) { free(bitmapData); fprintf(stderr, "Context not created!"); diff --git a/core_pe/photo.py b/core_pe/photo.py index f7352011..09185c92 100644 --- a/core_pe/photo.py +++ b/core_pe/photo.py @@ -49,9 +49,18 @@ class Photo(fs.File): self._cached_orientation = 0 return self._cached_orientation + def _get_exif_timestamp(self): + try: + with self.path.open('rb') as fp: + exifdata = exif.get_fields(fp) + return exifdata['DateTimeOriginal'] + except Exception: + logging.info("Couldn't read EXIF of picture: %s", self.path) + return '' + @classmethod def can_handle(cls, path): - return fs.File.can_handle(path) and get_file_ext(path[-1]) in cls.HANDLED_EXTS + return fs.File.can_handle(path) and get_file_ext(path.name) in cls.HANDLED_EXTS def get_display_info(self, group, delta): size = self.size @@ -89,12 +98,7 @@ class Photo(fs.File): if self._get_orientation() in {5, 6, 7, 8}: self.dimensions = (self.dimensions[1], self.dimensions[0]) elif field == 'exif_timestamp': - try: - with self.path.open('rb') as fp: - exifdata = exif.get_fields(fp) - self.exif_timestamp = exifdata['DateTimeOriginal'] - except Exception: - logging.info("Couldn't read EXIF of picture: %s", self.path) + self.exif_timestamp = self._get_exif_timestamp() def get_blocks(self, block_count_per_side): return self._plat_get_blocks(block_count_per_side, self._get_orientation()) diff --git a/core_se/__init__.py b/core_se/__init__.py index 7aea0ee2..3f5de611 100644 --- a/core_se/__init__.py +++ b/core_se/__init__.py @@ -1,2 +1,2 @@ -__version__ = '3.7.1' +__version__ = '3.8.0' __appname__ = 'dupeGuru' diff --git a/core_se/app.py b/core_se/app.py index 42d80d9b..cacd2e7d 100644 --- a/core_se/app.py +++ b/core_se/app.py @@ -14,8 +14,8 @@ class DupeGuru(DupeGuruBase): NAME = __appname__ METADATA_TO_READ = ['size', 'mtime'] - def __init__(self, view, appdata): - DupeGuruBase.__init__(self, view, appdata) + def __init__(self, view): + DupeGuruBase.__init__(self, view) self.directories.fileclasses = [fs.File] self.directories.folderclass = fs.Folder diff --git a/help/changelog_se b/help/changelog_se index a41ba795..8c88a27b 100644 --- a/help/changelog_se +++ b/help/changelog_se @@ -1,3 +1,14 @@ +=== 3.8.0 (2013-12-07) + +* Disable symlink/hardlink deletion option when not relevant. (#247) +* Make Cmd+A select all folders in the Folder Selection dialog. [Mac] (#228) +* Make non-numeric delta comparison case insensitive. (#239) +* Fix surrogate-related UnicodeEncodeError on CSV export. (#210) +* Fixed crash on Dupe Count sorting with Delta + Dupes Only. (#238) +* Improved documentation. +* Important internal refactorings. +* Removed the fairware dialog ([More Info](http://www.hardcoded.net/articles/phasing-out-fairware)). + === 3.7.1 (2013-08-19) * Fixed folder scan type, which was broken in v3.7.0. diff --git a/help/conf.tmpl b/help/conf.tmpl index 7c66d5ef..3519e575 100644 --- a/help/conf.tmpl +++ b/help/conf.tmpl @@ -31,6 +31,8 @@ def fix_nulljob_in_sig(app, what, name, obj, options, signature, return_annotati def setup(app): app.connect('autodoc-process-signature', fix_nulljob_in_sig) +autodoc_member_order = 'groupwise' + # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. @@ -38,7 +40,7 @@ def setup(app): # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.todo', 'sphinx.ext.autodoc'] +extensions = ['sphinx.ext.todo', 'sphinx.ext.autodoc', 'sphinx.ext.autosummary'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -158,10 +160,10 @@ html_theme = 'haiku' #html_additional_pages = {} # If false, no module index is generated. -html_domain_indices = False +# html_domain_indices = False # If false, no index is generated. -html_use_index = False +# html_use_index = False # If true, the index is split into individual pages for each letter. #html_split_index = False diff --git a/help/en/developer/core/gui.rst b/help/en/developer/core/gui.rst deleted file mode 100644 index 6db5cafd..00000000 --- a/help/en/developer/core/gui.rst +++ /dev/null @@ -1,5 +0,0 @@ -core.gui -======== - -.. automodule:: core.gui - :members: \ No newline at end of file diff --git a/help/en/developer/core/gui/deletion_options.rst b/help/en/developer/core/gui/deletion_options.rst new file mode 100644 index 00000000..2fbc7f8a --- /dev/null +++ b/help/en/developer/core/gui/deletion_options.rst @@ -0,0 +1,5 @@ +core.gui.deletion_options +========================= + +.. automodule:: core.gui.deletion_options + :members: diff --git a/help/en/developer/core/gui/index.rst b/help/en/developer/core/gui/index.rst new file mode 100644 index 00000000..0298f4b9 --- /dev/null +++ b/help/en/developer/core/gui/index.rst @@ -0,0 +1,10 @@ +core.gui +======== + +.. automodule:: core.gui + :members: + +.. toctree:: + :maxdepth: 2 + + deletion_options diff --git a/help/en/developer/core/index.rst b/help/en/developer/core/index.rst new file mode 100644 index 00000000..8c88e7e1 --- /dev/null +++ b/help/en/developer/core/index.rst @@ -0,0 +1,12 @@ +core +==== + +.. toctree:: + :maxdepth: 2 + + app + fs + engine + directories + results + gui/index diff --git a/help/en/developer/hscommon/build.rst b/help/en/developer/hscommon/build.rst new file mode 100644 index 00000000..b3b106bd --- /dev/null +++ b/help/en/developer/hscommon/build.rst @@ -0,0 +1,5 @@ +hscommon.build +============== + +.. automodule:: hscommon.build + :members: diff --git a/help/en/developer/hscommon/conflict.rst b/help/en/developer/hscommon/conflict.rst new file mode 100644 index 00000000..34724d10 --- /dev/null +++ b/help/en/developer/hscommon/conflict.rst @@ -0,0 +1,5 @@ +hscommon.conflict +================= + +.. automodule:: hscommon.conflict + :members: diff --git a/help/en/developer/hscommon/desktop.rst b/help/en/developer/hscommon/desktop.rst new file mode 100644 index 00000000..e30ccafd --- /dev/null +++ b/help/en/developer/hscommon/desktop.rst @@ -0,0 +1,5 @@ +hscommon.desktop +================ + +.. automodule:: hscommon.desktop + :members: diff --git a/help/en/developer/hscommon/gui/base.rst b/help/en/developer/hscommon/gui/base.rst new file mode 100644 index 00000000..0a20b963 --- /dev/null +++ b/help/en/developer/hscommon/gui/base.rst @@ -0,0 +1,12 @@ +hscommon.gui.base +================= + +.. automodule:: hscommon.gui.base + + .. autosummary:: + + GUIObject + + .. autoclass:: GUIObject + :members: + :private-members: diff --git a/help/en/developer/hscommon/gui/column.rst b/help/en/developer/hscommon/gui/column.rst new file mode 100644 index 00000000..5780a19d --- /dev/null +++ b/help/en/developer/hscommon/gui/column.rst @@ -0,0 +1,25 @@ +hscommon.gui.column +============================ + +.. automodule:: hscommon.gui.column + + .. autosummary:: + + Columns + Column + ColumnsView + PrefAccessInterface + + .. autoclass:: Columns + :members: + :private-members: + + .. autoclass:: Column + :members: + :private-members: + + .. autoclass:: ColumnsView + :members: + + .. autoclass:: PrefAccessInterface + :members: diff --git a/help/en/developer/hscommon/gui/progress_window.rst b/help/en/developer/hscommon/gui/progress_window.rst new file mode 100644 index 00000000..2453b705 --- /dev/null +++ b/help/en/developer/hscommon/gui/progress_window.rst @@ -0,0 +1,18 @@ +hscommon.gui.progress_window +============================ + +.. automodule:: hscommon.gui.progress_window + + .. autosummary:: + + ProgressWindow + ProgressWindowView + + .. autoclass:: ProgressWindow + :members: + :private-members: + + .. autoclass:: ProgressWindowView + :members: + :private-members: + diff --git a/help/en/developer/hscommon/gui/selectable_list.rst b/help/en/developer/hscommon/gui/selectable_list.rst new file mode 100644 index 00000000..e9d6c9c1 --- /dev/null +++ b/help/en/developer/hscommon/gui/selectable_list.rst @@ -0,0 +1,26 @@ +hscommon.gui.selectable_list +============================ + +.. automodule:: hscommon.gui.selectable_list + + .. autosummary:: + + Selectable + SelectableList + GUISelectableList + GUISelectableListView + + .. autoclass:: Selectable + :members: + :private-members: + + .. autoclass:: SelectableList + :members: + :private-members: + + .. autoclass:: GUISelectableList + :members: + :private-members: + + .. autoclass:: GUISelectableListView + :members: diff --git a/help/en/developer/hscommon/gui/table.rst b/help/en/developer/hscommon/gui/table.rst new file mode 100644 index 00000000..6f539d04 --- /dev/null +++ b/help/en/developer/hscommon/gui/table.rst @@ -0,0 +1,26 @@ +hscommon.gui.table +================== + +.. automodule:: hscommon.gui.table + + .. autosummary:: + + Table + Row + GUITable + GUITableView + + .. autoclass:: Table + :members: + :private-members: + + .. autoclass:: Row + :members: + :private-members: + + .. autoclass:: GUITable + :members: + :private-members: + + .. autoclass:: GUITableView + :members: diff --git a/help/en/developer/hscommon/gui/text_field.rst b/help/en/developer/hscommon/gui/text_field.rst new file mode 100644 index 00000000..7f142bc0 --- /dev/null +++ b/help/en/developer/hscommon/gui/text_field.rst @@ -0,0 +1,16 @@ +hscommon.gui.text_field +======================= + +.. automodule:: hscommon.gui.text_field + + .. autosummary:: + + TextField + TextFieldView + + .. autoclass:: TextField + :members: + :private-members: + + .. autoclass:: TextFieldView + :members: diff --git a/help/en/developer/hscommon/gui/tree.rst b/help/en/developer/hscommon/gui/tree.rst new file mode 100644 index 00000000..1c1e02b2 --- /dev/null +++ b/help/en/developer/hscommon/gui/tree.rst @@ -0,0 +1,18 @@ +hscommon.gui.tree +================= + +.. automodule:: hscommon.gui.tree + + .. autosummary:: + + Tree + Node + + .. autoclass:: Tree + :members: + :private-members: + + .. autoclass:: Node + :members: + :private-members: + diff --git a/help/en/developer/hscommon/index.rst b/help/en/developer/hscommon/index.rst new file mode 100644 index 00000000..38f7afca --- /dev/null +++ b/help/en/developer/hscommon/index.rst @@ -0,0 +1,19 @@ +hscommon +======== + +.. toctree:: + :maxdepth: 2 + + build + conflict + desktop + notify + path + util + gui/base + gui/text_field + gui/selectable_list + gui/table + gui/tree + gui/column + gui/progress_window diff --git a/help/en/developer/hscommon/notify.rst b/help/en/developer/hscommon/notify.rst new file mode 100644 index 00000000..4d61d584 --- /dev/null +++ b/help/en/developer/hscommon/notify.rst @@ -0,0 +1,5 @@ +hscommon.notify +=============== + +.. automodule:: hscommon.notify + :members: diff --git a/help/en/developer/hscommon/path.rst b/help/en/developer/hscommon/path.rst new file mode 100644 index 00000000..e2bcab3d --- /dev/null +++ b/help/en/developer/hscommon/path.rst @@ -0,0 +1,5 @@ +hscommon.path +============= + +.. automodule:: hscommon.path + :members: diff --git a/help/en/developer/hscommon/util.rst b/help/en/developer/hscommon/util.rst new file mode 100644 index 00000000..3dc3f539 --- /dev/null +++ b/help/en/developer/hscommon/util.rst @@ -0,0 +1,5 @@ +hscommon.util +============= + +.. automodule:: hscommon.util + :members: diff --git a/help/en/developer/index.rst b/help/en/developer/index.rst index f242b49c..098d973c 100644 --- a/help/en/developer/index.rst +++ b/help/en/developer/index.rst @@ -53,9 +53,5 @@ API .. toctree:: :maxdepth: 2 - core/app - core/fs - core/engine - core/directories - core/results - core/gui + core/index + hscommon/index diff --git a/help/en/faq.rst b/help/en/faq.rst index a41ed1af..87d393f1 100644 --- a/help/en/faq.rst +++ b/help/en/faq.rst @@ -29,7 +29,7 @@ What makes it better than other duplicate scanners? --------------------------------------------------- The scanning engine is extremely flexible. You can tweak it to really get the kind of results you -want. You can read more about dupeGuru tweaking option at the :doc:`Preferences page `. +want. You can read more about dupeGuru tweaking option in :doc:`scan`. How safe is it to use dupeGuru? ------------------------------- diff --git a/help/en/folders.rst b/help/en/folders.rst index 20eb035a..a3afb018 100644 --- a/help/en/folders.rst +++ b/help/en/folders.rst @@ -1,53 +1,67 @@ Folder Selection ================ -The first window you see when you launch dupeGuru is the folder selection window. This windows contains the list of the folders that will be scanned when you click on **Scan**. +The first window you see when you launch dupeGuru is the folder selection window. This windows +contains the list of the folders that will be scanned when you click on **Scan**. -This window is quite straightforward to use. If you want to add a folder, click on the **+** button. If you added folder before, a popup menu with a list of recent folders you added will pop. You can click on one of them to add it directly to your list. If you click on the first item of the popup menu, **Add New Folder...**, you will be prompted for a folder to add. If you never added a folder, no menu will pop and you will directly be prompted for a new folder to add. +This window is quite straightforward to use. If you want to add a folder, click on the **+** button. +If you added folder before, a popup menu with a list of recent folders you added will pop. You can +click on one of them to add it directly to your list. If you click on the first item of the popup +menu, **Add New Folder...**, you will be prompted for a folder to add. If you never added a folder, +no menu will pop and you will directly be prompted for a new folder to add. An alternate way to add folders to the list is to drag them in the list. -To remove a folder, select the folder to remove and click on **-**. If a subfolder is selected when you click the button, the selected folder will be set to **excluded** state (see below) instead of being removed. +To remove a folder, select the folder to remove and click on **-**. If a subfolder is selected when +you click the button, the selected folder will be set to **excluded** state (see below) instead of +being removed. Folder states ------------- Every folder can be in one of these 3 states: -* **Normal:** Duplicates found in this folder can be deleted. -* **Reference:** Duplicates found in this folder **cannot** be deleted. Files from this folder can only end up in **reference** position in the dupe group. If more than one file from reference folders end up in the same dupe group, only one will be kept. The others will be removed from the group. -* **Excluded:** Files in this directory will not be included in the scan. +**Normal:** + Duplicates found in this folder can be deleted. +**Reference:** + Duplicates found in this folder **cannot** be deleted. Files from this folder can + only end up in **reference** position in the dupe group. If more than one file from reference + folders end up in the same dupe group, only one will be kept. The others will be removed from + the group. +**Excluded:** + Files in this directory will not be included in the scan. -The default state of a folder is, of course, **Normal**. You can use **Reference** state for a folder if you want to be sure that you won't delete any file from it. +The default state of a folder is, of course, **Normal**. You can use **Reference** state for a +folder if you want to be sure that you won't delete any file from it. -When you set the state of a directory, all subfolders of this folder automatically inherit this state unless you explicitly set a subfolder's state. +When you set the state of a directory, all subfolders of this folder automatically inherit this +state unless you explicitly set a subfolder's state. -.. only:: edition_pe +.. _iphoto: - iPhoto and Aperture libraries - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - - dupeGuru PE supports iPhoto and Aperture, which means that it knows how to read these libraries - and how to communicate with iPhoto and Aperture to remove photos from them. To use this feature, - use the special "Add iPhoto Library" and "Add Aperture Library" buttons in the menu that pops - up when you click the "+" button. This will then add a special folder for those libraries. - - When duplicates are deleted from an iPhoto library, it's sent to iPhoto's trash. - - When duplicates are deleted from an Aperture library, it unfortunately can't send it directly - to trash, but it creates a special project called "dupeGuru Trash" in Aperture and send all - photos in there. You can then send this project to the trash manually. +iPhoto and Aperture libraries +----------------------------- -.. only:: edition_me +dupeGuru Picture Edition supports iPhoto and Aperture, which means that it knows how to read these +libraries and how to communicate with iPhoto and Aperture to remove photos from them. To use this +feature, use the special "Add iPhoto Library" and "Add Aperture Library" buttons in the menu that +pops up when you click the "+" button. This will then add a special folder for those libraries. - iTunes library - ^^^^^^^^^^^^^^ - - dupeGuru ME supports iTunes, which means that it knows how to read its libraries and how to - communicate with iTunes to remove songs from it. To use this feature, use the special - "Add iTunes Library" button in the menu that pops up when you click the "+" button. This will - then add a special folder for those libraries. - - When duplicates are deleted from an iTunes library, it's sent to the system trash, like a - normal file, but it's also removed from iTunes, thus avoiding ending up with missing entries - (entries with the "!" logo next to them). +When duplicates are deleted (sent to trash) from an iPhoto library, it's sent to iPhoto's +trash. + +When duplicates are deleted (sent to trash) from an Aperture library, it unfortunately can't +send it directly to trash, but it creates a special project called "dupeGuru Trash" in Aperture +and send all photos in there. You can then send this project to the trash manually. + +iTunes library +-------------- + +dupeGuru Music Edition supports iTunes, which means that it knows how to read its libraries and how +to communicate with iTunes to remove songs from it. To use this feature, use the special +"Add iTunes Library" button in the menu that pops up when you click the "+" button. This will +then add a special folder for those libraries. + +When duplicates are deleted from an iTunes library, it's sent to the system trash, like a +normal file, but it's also removed from iTunes, thus avoiding ending up with missing entries +(entries with the "!" logo next to them). diff --git a/help/en/index.rst b/help/en/index.rst index b758be2a..746ed55c 100644 --- a/help/en/index.rst +++ b/help/en/index.rst @@ -51,9 +51,16 @@ Contents: quick_start folders preferences + scan results reprioritize faq developer/index changelog credits + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`search` diff --git a/help/en/preferences.rst b/help/en/preferences.rst index 7fb6a41d..1ba55091 100644 --- a/help/en/preferences.rst +++ b/help/en/preferences.rst @@ -1,63 +1,87 @@ Preferences =========== -.. only:: edition_se - - **Scan Type:** This option determines what aspect of the files will be compared in the duplicate scan. If you select **Filename**, dupeGuru will compare every filenames word-by-word and, depending on the other settings below, it will determine if enough words are matching to consider 2 files duplicates. If you select **Content**, only files with the exact same content will match. - - The **Folders** scan type is a bit special. When you choose it, dupeGuru will scan for duplicate *folders* instead of duplicate files. To determine whether two folders are duplicates, all files contained in the folders will be scanned, and if the contents of **all** files in the folders match, the folders will be considered duplicates. - - **Filter Hardness:** If you chose the **Filename** scan type, this option determines how similar two filenames must be for dupeGuru to consider them duplicates. If the filter hardness is, for example 80, it means that 80% of the words of two filenames must match. To determine the matching percentage, dupeGuru first counts the total number of words in **both** filenames, then count the number of words matching (every word matching count as 2), and then divide the number of words matching by the total number of words. If the result is higher or equal to the filter hardness, we have a duplicate match. For example, "a b c d" and "c d e" have a matching percentage of 57 (4 words matching, 7 total words). +**Scan Type:** + Basic scan type to use. See :doc:`scan` for details. .. only:: edition_me - **Scan Type:** This option determines what aspect of the files will be compared in the duplicate scan. The nature of the duplicate scan varies greatly depending on what you select for this option. - - * **Filename:** Every song will have its filename split into words, and then every word will be compared to compute a matching percentage. If this percentage is higher or equal to the **Filter Hardness** (see below for more details), dupeGuru will consider the 2 songs duplicates. - * **Filename - Fields:** Like **Filename**, except that once filename have been split into words, these words are then grouped into fields. The field separator is " - ". The final matching percentage will be the lowest matching percentage among the fields. Thus, "An Artist - The Title" and "An Artist - Other Title" would have a matching percentage of 50 (With a **Filename** scan, it would be 75). - * **Filename - Fields (No Order):** Like **Filename - Fields**, except that field order doesn't matter. For example, "An Artist - The Title" and "The Title - An Artist" would have a matching percentage of 100 instead of 0. - * **Tags:** This method reads the tag (metadata) of every song and compare their fields. This method, like the **Filename - Fields**, considers the lowest matching field as its final matching percentage. - * **Content:** This scan method use the actual content of the songs to determine which are duplicates. For 2 songs to match with this method, they must have the **exact same content**. - * **Audio Content:** Same as content, but only the audio content is compared (without metadata). - - **Filter Hardness:** If you chose a filename or tag based scan type, this option determines how similar two filenames/tags must be for dupeGuru to consider them duplicates. If the filter hardness is, for example 80, it means that 80% of the words of two filenames must match. To determine the matching percentage, dupeGuru first counts the total number of words in **both** filenames, then count the number of words matching (every word matching count as 2), and then divide the number of words matching by the total number of words. If the result is higher or equal to the filter hardness, we have a duplicate match. For example, "a b c d" and "c d e" have a matching percentage of 57 (4 words matching, 7 total words). - - **Tags to scan:** When using the **Tags** scan type, you can select the tags that will be used for comparison. + **Tags to scan:** + When using the **Tags** scan type, you can select the tags that will be used for comparison. .. only:: edition_se or edition_me - **Word weighting:** If you chose the **Filename** scan type, this option slightly changes how matching percentage is calculated. With word weighting, instead of having a value of 1 in the duplicate count and total word count, every word have a value equal to the number of characters they have. With word weighting, "ab cde fghi" and "ab cde fghij" would have a matching percentage of 53% (19 total characters, 10 characters matching (4 for "ab" and 6 for "cde")). + **Word weighting:** + See :ref:`word-weighting`. - **Match similar words:** If you turn this option on, similar words will be counted as matches. For example "The White Stripes" and "The White Stripe" would have a match % of 100 instead of 66 with that option turned on. **Warning:** Use this option with caution. It is likely that you will get a lot of false positives in your results when turning it on. However, it will help you to find duplicates that you wouldn't have found otherwise. The scan process also is significantly slower with this option turned on. + **Match similar words:** + See :ref:`similarity-matching`. .. only:: edition_pe - **Scan Type:** This option determines the type of scan that will be made on your pictures. The **Contents** scan type compares the actual contents of the pictures in a fuzzy way (making it possible to find not only exact duplicates, but also similar ones). The **EXIF Timestamp** scan type looks at the EXIF metadata of the picture (if it exists) and matches pictures that have the same one. It's much faster than the Contents scan. **Warning:** Modified pictures often keep the same EXIF timestamp, so watch out for false positives when you use that scan type. - - **Filter Hardness:** *Contents scan type only.* The higher is this setting, the "harder" is the filter (In other words, the less results you get). Most pictures of the same quality match at 100% even if the format is different (PNG and JPG for example.). However, if you want to make a PNG match with a lower quality JPG, you will have to set the filer hardness to lower than 100. The default, 95, is a sweet spot. + **Match pictures of different dimensions:** + If you check this box, pictures of different dimensions will be allowed in the same + duplicate group. - **Match pictures of different dimensions:** If you check this box, pictures of different dimensions will be allowed in the same duplicate group. +.. _filter-hardness: -**Can mix file kind:** If you check this box, duplicate groups are allowed to have files with different extensions. If you don't check it, well, they aren't! +**Filter Hardness:** + The threshold needed for two files to be considered duplicates. A lower value means more + duplicates. The meaning of the threshold depends on the scanning type (see :doc:`scan`). + Only works for :ref:`worded ` and :ref:`picture blocks ` + scans. -**Ignore duplicates hardlinking to the same file:** If this option is enabled, dupeGuru will verify duplicates to see if they refer to the same `inode `_. If they do, they will not be considered duplicates. (Only for OS X and Linux) +**Can mix file kind:** + If you check this box, duplicate groups are allowed to have files with different extensions. If + you don't check it, well, they aren't! -**Use regular expressions when filtering:** If you check this box, the filtering feature will treat your filter query as a **regular expression**. Explaining them is beyond the scope of this document. A good place to start learning it is `regular-expressions.info `_. +**Ignore duplicates hardlinking to the same file:** + If this option is enabled, dupeGuru will verify duplicates to see if they refer to the same + `inode`_. If they do, they will not be considered duplicates. (Only for OS X and Linux) -**Remove empty folders after delete or move:** When this option is enabled, folders are deleted after a file is deleted or moved and the folder is empty. +**Use regular expressions when filtering:** + If you check this box, the filtering feature will treat your filter query as a + **regular expression**. Explaining them is beyond the scope of this document. A good place to + start learning it is `regular-expressions.info`_. -**Copy and Move:** Determines how the Copy and Move operations (in the Action menu) will behave. +**Remove empty folders after delete or move:** + When this option is enabled, folders are deleted after a file is deleted or moved and the folder + is empty. -* **Right in destination:** All files will be sent directly in the selected destination, without trying to recreate the source path at all. -* **Recreate relative path:** The source file's path will be re-created in the destination folder up to the root selection in the Directories panel. For example, if you added ``/Users/foobar/SomeFolder`` to your Directories panel and you move ``/Users/foobar/SomeFolder/SubFolder/SomeFile.ext`` to the destination ``/Users/foobar/MyDestination``, the final destination for the file will be ``/Users/foobar/MyDestination/SubFolder`` (``SomeFolder`` has been trimmed from source's path in the final destination.). -* **Recreate absolute path:** The source file's path will be re-created in the destination folder in it's entirety. For example, if you move ``/Users/foobar/SomeFolder/SubFolder/SomeFile.ext`` to the destination ``/Users/foobar/MyDestination``, the final destination for the file will be ``/Users/foobar/MyDestination/Users/foobar/SomeFolder/SubFolder``. +**Copy and Move:** + Determines how the Copy and Move operations (in the Action menu) will behave. -In all cases, dupeGuru nicely handles naming conflicts by prepending a number to the destination filename if the filename already exists in the destination. +* **Right in destination:** All files will be sent directly in the selected destination, without + trying to recreate the source path at all. +* **Recreate relative path:** The source file's path will be re-created in the destination folder up + to the root selection in the Directories panel. For example, if you added + ``/Users/foobar/SomeFolder`` to your Directories panel and you move + ``/Users/foobar/SomeFolder/SubFolder/SomeFile.ext`` to the destination + ``/Users/foobar/MyDestination``, the final destination for the file will be + ``/Users/foobar/MyDestination/SubFolder`` (``SomeFolder`` has been trimmed from source's path in + the final destination.). +* **Recreate absolute path:** The source file's path will be re-created in the destination folder in + its entirety. For example, if you move ``/Users/foobar/SomeFolder/SubFolder/SomeFile.ext`` to the + destination ``/Users/foobar/MyDestination``, the final destination for the file will be + ``/Users/foobar/MyDestination/Users/foobar/SomeFolder/SubFolder``. -**Custom Command:** This preference determines the command that will be invoked by the "Invoke Custom Command" action. You can invoke any external application through this action. This can be useful if, for example, you have a nice diffing application installed. +In all cases, dupeGuru nicely handles naming conflicts by prepending a number to the destination +filename if the filename already exists in the destination. -The format of the command is the same as what you would write in the command line, except that there are 2 placeholders: **%d** and **%r**. These placeholders will be replaced by the path of the selected dupe (%d) and the path of the selected dupe's reference file (%r). +**Custom Command:** + This preference determines the command that will be invoked by the "Invoke Custom Command" + action. You can invoke any external application through this action. This can be useful if, + for example, you have a nice diffing application installed. + +The format of the command is the same as what you would write in the command line, except that there +are 2 placeholders: **%d** and **%r**. These placeholders will be replaced by the path of the +selected dupe (%d) and the path of the selected dupe's reference file (%r). -If the path to your executable contains space characters, you should enclose it in "" quotes. You should also enclose placeholders in quotes because it's very possible that paths to dupes and refs will contain spaces. Here's an example custom command:: +If the path to your executable contains space characters, you should enclose it in "" quotes. You +should also enclose placeholders in quotes because it's very possible that paths to dupes and refs +will contain spaces. Here's an example custom command:: "C:\Program Files\SuperDiffProg\SuperDiffProg.exe" "%d" "%r" + +.. _inode: http://en.wikipedia.org/wiki/Inode +.. _regular-expressions.info: http://www.regular-expressions.info \ No newline at end of file diff --git a/help/en/results.rst b/help/en/results.rst index 515b8a31..49c3eba6 100644 --- a/help/en/results.rst +++ b/help/en/results.rst @@ -1,6 +1,8 @@ Results ======= +.. contents:: + When dupeGuru is finished scanning for duplicates, it will show its results in the form of duplicate group list. About duplicate groups @@ -118,42 +120,54 @@ filtered duplicates. Action Menu ----------- -* **Clear Ignore List:** Remove all ignored matches you added. You have to start a new scan for the - newly cleared ignore list to be effective. -* **Export Results to XHTML:** Take the current results, and create an XHTML file out of it. The - columns that are visible when you click on this button will be the columns present in the XHTML - file. The file will automatically be opened in your default browser. -* **Send Marked to Trash:** Send all marked duplicates to trash, obviously. Before proceeding, - you'll be presented deletion options (see below). -* **Move Marked to...:** Prompt you for a destination, and then move all marked files to that - destination. Source file's path might be re-created in destination, depending on the - "Copy and Move" preference. -* **Copy Marked to...:** Prompt you for a destination, and then copy all marked files to that - destination. Source file's path might be re-created in destination, depending on the - "Copy and Move" preference. -* **Remove Marked from Results:** Remove all marked duplicates from results. The actual files will - not be touched and will stay where they are. -* **Remove Selected from Results:** Remove all selected duplicates from results. Note that all - selected reference files will be ignored, only duplicates can be removed with this action. -* **Make Selected into Reference:** Promote all selected duplicates to reference. If a duplicate is - a part of a group having a reference file coming from a reference folder (in blue color), no - action will be taken for this duplicate. If more than one duplicate among the same group are - selected, only the first of each group will be promoted. -* **Add Selected to Ignore List:** This first removes all selected duplicates from results, and - then add the match of that duplicate and the current reference in the ignore list. This match - will not come up again in further scan. The duplicate itself might come back, but it will be - matched with another reference file. You can clear the ignore list with the Clear Ignore List - command. -* **Open Selected with Default Application:** Open the file with the application associated with - selected file's type. -* **Reveal Selected in Finder:** Open the folder containing selected file. -* **Invoke Custom Command:** Invokes the external application you've set up in your preferences - using the current selection as arguments in the invocation. -* **Rename Selected:** Prompts you for a new name, and then rename the selected file. +**Clear Ignore List:** + Remove all ignored matches you added. You have to start a new scan for the + newly cleared ignore list to be effective. +**Export Results to XHTML:** + Take the current results, and create an XHTML file out of it. The + columns that are visible when you click on this button will be the columns present in the XHTML + file. The file will automatically be opened in your default browser. +**Send Marked to Trash:** + Send all marked duplicates to trash, obviously. Before proceeding, + you'll be presented deletion options (see below). +**Move Marked to...:** + Prompt you for a destination, and then move all marked files to that + destination. Source file's path might be re-created in destination, depending on the + "Copy and Move" preference. +**Copy Marked to...:** + Prompt you for a destination, and then copy all marked files to that + destination. Source file's path might be re-created in destination, depending on the + "Copy and Move" preference. +**Remove Marked from Results:** + Remove all marked duplicates from results. The actual files will + not be touched and will stay where they are. +**Remove Selected from Results:** + Remove all selected duplicates from results. Note that all + selected reference files will be ignored, only duplicates can be removed with this action. +**Make Selected into Reference:** + Promote all selected duplicates to reference. If a duplicate is + a part of a group having a reference file coming from a reference folder (in blue color), no + action will be taken for this duplicate. If more than one duplicate among the same group are + selected, only the first of each group will be promoted. +**Add Selected to Ignore List:** + This first removes all selected duplicates from results, and + then add the match of that duplicate and the current reference in the ignore list. This match + will not come up again in further scan. The duplicate itself might come back, but it will be + matched with another reference file. You can clear the ignore list with the Clear Ignore List + command. +**Open Selected with Default Application:** + Open the file with the application associated with selected file's type. +**Reveal Selected in Finder:** + Open the folder containing selected file. +**Invoke Custom Command:** + Invokes the external application you've set up in your preferences using the current selection + as arguments in the invocation. +**Rename Selected:** + Prompts you for a new name, and then rename the selected file. -**Warning about moving files in iPhoto/iTunes:** When using the "Move Marked" action on duplicates -that come from iPhoto or iTunes, files are copied, not moved. dupeGuru cannot use the Move action -on those files. +**Warning about moving files in iPhoto/iTunes/Aperture:** When using the "Move Marked" action on +duplicates that come from iPhoto, Aperture or iTunes, files are copied, not moved. dupeGuru cannot +use the Move action on those files. Deletion Options ---------------- @@ -161,21 +175,23 @@ Deletion Options These options affect how duplicate deletion takes place. Most of the time, you don't need to enable any of them. -* **Link deleted files:** The deleted files are replaced by a link to the reference file. You have - a choice of replacing it either with a `symlink`_ or a `hardlink`_. It's better to read the whole - wikipedia pages about them to make a informed choice, but in short, a symlink is a shortcut to - the file's path. If the original file is deleted or moved, the link is broken. A hardlink is a - link to the file *itself*. That link is as good as a "real" file. Only when *all* hardlinks to a - file are deleted is the file itself deleted. +**Link deleted files:** + The deleted files are replaced by a link to the reference file. You have a choice of replacing + it either with a `symlink`_ or a `hardlink`_. It's better to read the whole + wikipedia pages about them to make a informed choice, but in short, a symlink is a shortcut to + the file's path. If the original file is deleted or moved, the link is broken. A hardlink is a + link to the file *itself*. That link is as good as a "real" file. Only when *all* hardlinks to a + file are deleted is the file itself deleted. - On OSX and Linux, this feature is supported fully, but under Windows, it's a bit complicated. - Windows XP doesn't support it, but Vista and up support it. However, for the feature to work, - dupeGuru has to run with administrative privileges. + On OSX and Linux, this feature is supported fully, but under Windows, it's a bit complicated. + Windows XP doesn't support it, but Vista and up support it. However, for the feature to work, + dupeGuru has to run with administrative privileges. -* **Directly delete files:** Instead of sending files to trash, directly delete them. This is used - for troubleshooting and you normally don't need to enable this unless dupeGuru has problems - deleting files normally, something that can happens when you try to delete files on network - storage (NAS). +**Directly delete files:** + Instead of sending files to trash, directly delete them. This is used + for troubleshooting and you normally don't need to enable this unless dupeGuru has problems + deleting files normally, something that can happens when you try to delete files on network + storage (NAS). .. _regular-expressions.info: http://www.regular-expressions.info .. _hardlink: http://en.wikipedia.org/wiki/Hard_link diff --git a/help/en/scan.rst b/help/en/scan.rst new file mode 100644 index 00000000..689af049 --- /dev/null +++ b/help/en/scan.rst @@ -0,0 +1,186 @@ +The scanning process +==================== + +.. contents:: + +dupeGuru has 3 basic ways of scanning: :ref:`worded-scan` and :ref:`contents-scan` and +:ref:`picture blocks `. The first two modes are for the Standard and Music +editions, the last is for the Picture edition. The scanning process is configured through the +:doc:`Preference pane `. + +.. _worded-scan: + +Worded scans +------------ + +*Standard and Music Editions only*. + +Worded scans extract a string from each file and split it into words. The string can come from two +different sources: **Filename** or **Tags** (Music Edition only). + +When our source is music tags, we have to choose which tags to use. If, for example, we choose to +analyse *artist* and *title* tags, we'd end up with strings like +"The White Stripes - Seven Nation Army". + +Words are split by space characters, with all punctuation removed (some are replaced by spaces, some +by nothing) and all words lowercased. For example, the string "This guy's song(remix)" yields +*this*, *guys*, *song* and *remix*. + +Once this is done, the scanning dance begins. Finding duplicates is only a matter of finding how +many words in common two given strings have. If the :ref:`filter hardness ` is, +for example, ``80``, it means that 80% of the words of two strings must match. To determine the +matching percentage, dupeGuru first counts the total number of words in **both** strings, then count +the number of words matching (every word matching count as 2), and then divide the number of words +matching by the total number of words. If the result is higher or equal than the filter hardness, +we have a duplicate match. For example, "a b c d" and "c d e" have a matching percentage of 57 +(4 words matching, 7 total words). + +Fields +^^^^^^ + +*Music Edition only*. + +Song filenames often come with multiple and distinct parts and this can cause problems. For example, +let's take these two songs: "Dolly Parton - I Will Always Love You" and +"Whitney Houston - I Will Always Love You". They are clearly not the same song (they come from +different artists), but they still still have a matching score of 71%! This means that, with a naive +scanning method, we would get these songs as a false positive as soon as we try to dig a bit deeper +in our dupe hunt by lowering the threshold a bit. + +This is why we have the "Fields" concept. Fields are separated by dashes (``-``). When the +"Filename - Fields" scan type is chosen, each field is compared separately. Our final matching score +will only be the lowest of all the fields. In our example, the title has a 100% match, but the +artist has a 0% match, making our final match score 0. + +Sometimes, our song filename policy isn't completely homogenous, which means that we can end up with +"The White Stripes - Seven Nation Army" and "Seven Nation Army - The White Stripes". This is why +we have the "Filename - Fields (No Order)" scan type. With this scan type, all fields are compared +with each other, and the highest score is kept. Then, the final matching score is the lowest of them +all. In our case, the final matching score is 100. + +Note: Each field is used once. Thus, "The White Stripes - The White Stripes" and +"The White Stripes - Seven Nation Army" have a match score of 0 because the second +"The White Stripes" can't be compared with the first field of the other name because it has already +been "used up" by the first field. Our final match score would be 0. + +*Tags* scanning method is always "fielded". When choosing this scan method, we also choose which +tags are going to be compared, each being a field. + +.. _word-weighting: + +Word weighting +^^^^^^^^^^^^^^ + +When enabled, this option slightly changes how matching percentage is calculated by making bigger +words worth more. With word weighting, instead of having a value of 1 in the duplicate count and +total word count, every word have a value equal to the number of characters they have. With word +weighting, "ab cde fghi" and "ab cde fghij" would have a matching percentage of 53% (19 total +characters, 10 characters matching (4 for "ab" and 6 for "cde")). + +.. _similarity-matching: + +Similarity matching +^^^^^^^^^^^^^^^^^^^ + +When enabled, similar words will be counted as matches. For example "The White Stripes" and +"The White Stripe" would have a match score of 100 instead of 66 with that option turned on. + +Two words are considered similar if they can be made equal with only a few edit operations (removing +a letter, adding one etc.). The process used is not unlike the +`Levenshtein distance`_. For the technically inclined, the actual function used is +Python's `get_close_matches`_ with a ``0.8`` cutoff. + +**Warning:** Use this option with caution. It is likely that you will get a lot of false positives +in your results when turning it on. However, it will help you to find duplicates that you wouldn't +have found otherwise. The scan process also is significantly slower with this option turned on. + +.. _contents-scan: + +Contents scans +-------------- + +Contents scans are much simpler than worded scans. We read files and if the contents is exactly the +same, we consider the two files duplicates. + +This is, of course, quite longer than comparing filenames and, to avoid needlessly reading whole +file contents, we start by looking at file sizes. After having grouped our files by size, we discard +every file that is alone in its group. Then, we proceed to read the contents of our remaining files. + +MD5 hashes are used to compute compare contents. Yes, it is widely known that forging files having +the same MD5 hash is easy, but this file has to be knowingly forged. The possibilities of two files +having the same MD5 hash *and* the same size by accident is still very, very small. + +The :ref:`filter hardness ` preference is ignored in this scan. + +Audio contents +^^^^^^^^^^^^^^ + +*Music Edition only*. + +This mode is very much like the normal contents scan. The only difference is that it ignores +metadata included in the file and only compares audio data. *It doesn't do audio data fuzzy +matching, only exact matching. It would be really cool to have that, but we aren't there yet.* + +Folders +^^^^^^^ + +*Standard Edition only*. + +This is a special Contents scan type. It works like a normal contens scan, but instead of trying to +find duplicate files, it tries to find duplicate folders. A folder is duplicate to another if all +files it contains have the same contents as the other folder's file. + +This scan is, of course, recursive and subfolders are checked. dupeGuru keeps only the biggest +fishes. Therefore, if two folders that are considered as matching contain subfolders, these +subfolders will not be included in the final results. + +With this mode, we end up with folders as results instead of files. + +.. _picture-blocks-scan: + +Picture blocks +-------------- + +*Picture Edition only*. + +dupeGuru Picture Edition stands apart of its two friends. Its scan types are completely different. +The first one is its "Contents" scan, which is a bit too generic, hence the name we use here, +"Picture blocks". + +We start by opening every picture in RGB bitmap mode, then we "blockify" the picture. We create a +15x15 grid and compute the average color of each grid tile. This is the "picture analysis" phase. +It's very time consuming and the result is cached in a database (the "picture cache"). + +Once we've done that, we can start comparing them. Each tile in the grid (an average color) is +compared to its corresponding grid on the other picture and a color diff is computer (it's simply +a sum of the difference of R, G and B on each side). All these sums are added up to a final "score". + +If that score is smaller or equal to ``100 - threshold``, we have a match. + +A threshold of 100 adds an additional constraint that pictures have to be exactly the same (it's +possible, due to averaging, that the tile comparison yields ``0`` for pictures that aren't exactly +the same, but since "100%" suggests "exactly the same", we discard those ocurrences). If you want +to get pictures that are very, very similar but still allow a bit of fuzzy differences, go for 99%. + +This second part of the scan is CPU intensive and can take quite a bit of time. This task has been +made to take advatange of multi-core CPUs and has been optimized to the best of my abilities, but +the fact of the matter is that, due to the fuzziness of the task, we still have to compare every picture +to every other, making the algorithm quadratic (if ``N`` is the number of pictures to compare, the +number of comparisons to perform is ``N*N``). + +This algorithm is very naive, but in the field, it works rather well. If you master a better +algorithm and want to improve dupeGuru, by all means, let me know! + +EXIF Timestamp +-------------- + +*Picture Edition only*. + +This one is easy. We read the EXIF information of every picture and extract the ``DateTimeOriginal`` +tag. If the tag is the same for two pictures, they're considered duplicates. + +**Warning:** Modified pictures often keep the same EXIF timestamp, so watch out for false positives +when you use that scan type. + +.. _Levenshtein distance: http://en.wikipedia.org/wiki/Levenshtein_distance +.. _get_close_matches: http://docs.python.org/3/library/difflib.html#difflib.get_close_matches diff --git a/hscommon/.tx/config b/hscommon/.tx/config deleted file mode 100644 index 2b53bdee..00000000 --- a/hscommon/.tx/config +++ /dev/null @@ -1,8 +0,0 @@ -[main] -host = https://www.transifex.com - -[hscommon.hscommon] -file_filter = locale//LC_MESSAGES/hscommon.po -source_file = locale/hscommon.pot -source_lang = en -type = PO diff --git a/hscommon/README b/hscommon/README index efee44a7..acc2ccbc 100644 --- a/hscommon/README +++ b/hscommon/README @@ -1,9 +1,3 @@ -The documentation has to be built with Sphinx. You can get Sphinx at http://sphinx.pocoo.org/ - -Once you installed it, you can build the documentation with: - -cd docs -sphinx-build . ../docs_html - -The reason why you have to move in 'docs' is because hscommon.io conflicts with the builtin 'io' -module. The documentation is also available online at http://www.hardcoded.net/docs/hscommon \ No newline at end of file +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/build.py b/hscommon/build.py index dcd7ece0..0ede9d0b 100644 --- a/hscommon/build.py +++ b/hscommon/build.py @@ -6,6 +6,9 @@ # which should be included with this package. The terms are also available at # http://www.hardcoded.net/licenses/bsd_license +"""This module is a collection of function to help in HS apps build process. +""" + import os import sys import os.path as op @@ -26,6 +29,8 @@ 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() @@ -125,6 +130,10 @@ def package_cocoa_app_in_dmg(app_path, destfolder, args): 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() @@ -153,7 +162,7 @@ sysconfig.get_config_h_filename = lambda: op.join(op.dirname(__file__), 'pyconfi """) def add_to_pythonpath(path): - """Adds `path` to both PYTHONPATH env and sys.path. + """Adds ``path`` to both ``PYTHONPATH`` env and ``sys.path``. """ abspath = op.abspath(path) pythonpath = os.environ.get('PYTHONPATH', '') @@ -166,6 +175,12 @@ def add_to_pythonpath(path): # 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: @@ -364,23 +379,14 @@ class OSXFrameworkStructure: action(op.abspath(path), header_dest) -def build_cocoalib_xibless(dest='cocoa/autogen', withfairware=True): +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'), ] - if withfairware: - FNPAIRS += [ - ('fairware_about.py', 'HSFairwareAboutBox_UI'), - ('demo_reminder.py', 'HSDemoReminder_UI'), - ('enter_code.py', 'HSEnterCode_UI'), - ] - else: - FNPAIRS += [ - ('about.py', 'HSAboutBox_UI'), - ] for srcname, dstname in FNPAIRS: srcpath = op.join('cocoalib', 'ui', srcname) dstpath = op.join(dest, dstname) diff --git a/hscommon/conflict.py b/hscommon/conflict.py index 7eeb76cb..3e556345 100644 --- a/hscommon/conflict.py +++ b/hscommon/conflict.py @@ -6,8 +6,15 @@ # which should be included with this package. The terms are also available at # http://www.hardcoded.net/licenses/bsd_license +"""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 -from . import io +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.. @@ -15,7 +22,7 @@ from . import io 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. + """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. @@ -31,32 +38,42 @@ def get_conflicted_name(other_names, name): 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 -def _smart_move_or_copy(operation, source_path, dest_path): - ''' Use move() or copy() to move and copy file with the conflict management, but without the - slowness of the fs system. - ''' - if io.isdir(dest_path) and not io.isdir(source_path): - dest_path = dest_path + source_path[-1] - if io.exists(dest_path): - filename = dest_path[-1] - dest_dir_path = dest_path[:-1] - newname = get_conflicted_name(io.listdir(dest_dir_path), filename) - dest_path = dest_dir_path + newname - operation(source_path, dest_path) +@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): - _smart_move_or_copy(io.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(io.copy, source_path, dest_path) + _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(io.copytree, source_path, dest_path) + _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 index 9ffcd313..d73ea5c1 100644 --- a/hscommon/currency.py +++ b/hscommon/currency.py @@ -6,16 +6,36 @@ # which should be included with this package. The terms are also available at # http://www.hardcoded.net/licenses/bsd_license +"""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 . import io 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 = {} @@ -67,12 +87,16 @@ class 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 + Currency.rates_db = RatesDB() # Make sure we always have some db to work with return Currency.rates_db def rates_date_range(self): @@ -270,6 +294,12 @@ 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. @@ -310,7 +340,7 @@ class RatesDB: 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() - io.remove(Path(self.db_or_path)) + 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") @@ -329,12 +359,35 @@ class RatesDB: 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 = self._fetched_values.get_nowait() + 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 @@ -342,7 +395,12 @@ class RatesDB: self._cache = {} def date_range(self, currency_code): - """Returns (start, end) of the cached rates for currency""" + """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() @@ -374,7 +432,7 @@ class RatesDB: else: value2 = self._cache.get((date, currency2_code)) if value1 is None or value2 is None: - str_date = '%d%02d%02d' % (date.year, date.month, date.day) + str_date = date2str(date) if value1 is None: value1 = self._seek_value_in_CAD(str_date, currency1_code) self._cache[(date, currency1_code)] = value1 @@ -388,7 +446,7 @@ class RatesDB: # 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 = '%d%02d%02d' % (date.year, date.month, date.day) + str_date = date2str(date) sql = "replace into rates(date, currency, rate) values(?, ?, ?)" self._execute(sql, [str_date, currency_code, value]) self.con.commit() @@ -419,14 +477,27 @@ class RatesDB: """ 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 values: - self._fetched_values.put((values, currency)) + 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: @@ -437,7 +508,9 @@ class RatesDB: except KeyError: cached_range = self.date_range(currency) range_start = start_date - range_end = date.today() + # 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: @@ -446,6 +519,10 @@ class RatesDB: 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()) diff --git a/hscommon/desktop.py b/hscommon/desktop.py new file mode 100644 index 00000000..745e115b --- /dev/null +++ b/hscommon/desktop.py @@ -0,0 +1,91 @@ +# Created By: Virgil Dupras +# Created On: 2013-10-12 +# Copyright 2013 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "BSD" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/bsd_license + +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 PyQt4.QtCore import QUrl + from PyQt4.QtGui import QDesktopServices + 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 = QDesktopServices.CacheLocation + else: + qtfolder = QDesktopServices.DataLocation + return str(QDesktopServices.storageLocation(qtfolder)) + + 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/docs/build.rst b/hscommon/docs/build.rst deleted file mode 100644 index 4db5316d..00000000 --- a/hscommon/docs/build.rst +++ /dev/null @@ -1,25 +0,0 @@ -========================================== -:mod:`build` - Build utilities for HS apps -========================================== - -This module is a collection of function to help in HS apps build process. - -.. function:: print_and_do(cmd) - - Prints ``cmd`` and executes it in the shell. - -.. function:: build_all_qt_ui(base_dir='.') - - Calls Qt's ``pyuic4`` for each file in ``base_dir`` with a ".ui" extension. The resulting file is saved under ``{base_name}_ui.py``. - -.. function:: build_dmg(app_path, dest_path) - - 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. - -.. function:: add_to_pythonpath(path) - - Adds ``path`` to both ``PYTHONPATH`` env variable and ``sys.path``. - -.. function:: copy_packages(packages_names, dest) - - Copy python packages ``packages_names`` to ``dest``, but 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. \ No newline at end of file diff --git a/hscommon/docs/conf.py b/hscommon/docs/conf.py deleted file mode 100644 index 9ec64693..00000000 --- a/hscommon/docs/conf.py +++ /dev/null @@ -1,194 +0,0 @@ -# -*- coding: utf-8 -*- -# -# hscommon documentation build configuration file, created by -# sphinx-quickstart on Fri Mar 12 16:00:37 2010. -# -# This file is execfile()d with the current directory set to its containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. - -import sys, os - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.append(os.path.abspath('.')) - -# -- General configuration ----------------------------------------------------- - -# Add any Sphinx extension module names here, as strings. They can be extensions -# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = [] - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -# The suffix of source filenames. -source_suffix = '.rst' - -# The encoding of source files. -#source_encoding = 'utf-8' - -# The master toctree document. -master_doc = 'index' - -# General information about the project. -project = 'hscommon' -copyright = '2011, Hardcoded Software' - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The short X.Y version. -version = '1.0.0' -# The full version, including alpha/beta/rc tags. -release = '1.0.0' - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -#language = None - -# There are two options for replacing |today|: either, you set today to some -# non-false value, then it is used: -#today = '' -# Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' - -# List of documents that shouldn't be included in the build. -#unused_docs = [] - -# List of directories, relative to source directory, that shouldn't be searched -# for source files. -exclude_trees = ['_build'] - -# The reST default role (used for this markup: `text`) to use for all documents. -#default_role = None - -# If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True - -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -#add_module_names = True - -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -#show_authors = False - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' - -# A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] - - -# -- Options for HTML output --------------------------------------------------- - -# The theme to use for HTML and HTML Help pages. Major themes that come with -# Sphinx are currently 'default' and 'sphinxdoc'. -html_theme = 'default' - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -#html_theme_options = {} - -# Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] - -# The name for this set of Sphinx documents. If None, it defaults to -# " v documentation". -#html_title = None - -# A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None - -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. -#html_logo = None - -# The name of an image file (within the static path) to use as favicon of the -# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 -# pixels large. -#html_favicon = None - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -# html_static_path = ['_static'] - -# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, -# using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' - -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -#html_use_smartypants = True - -# Custom sidebar templates, maps document names to template names. -#html_sidebars = {} - -# Additional templates that should be rendered to pages, maps page names to -# template names. -#html_additional_pages = {} - -# If false, no module index is generated. -#html_use_modindex = True - -# If false, no index is generated. -#html_use_index = True - -# If true, the index is split into individual pages for each letter. -#html_split_index = False - -# If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True - -# If true, an OpenSearch description file will be output, and all pages will -# contain a tag referring to it. The value of this option must be the -# base URL from which the finished HTML is served. -#html_use_opensearch = '' - -# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = '' - -# Output file base name for HTML help builder. -htmlhelp_basename = 'hscommondoc' - - -# -- Options for LaTeX output -------------------------------------------------- - -# The paper size ('letter' or 'a4'). -#latex_paper_size = 'letter' - -# The font size ('10pt', '11pt' or '12pt'). -#latex_font_size = '10pt' - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, author, documentclass [howto/manual]). -latex_documents = [ - ('index', 'hscommon.tex', 'hscommon Documentation', - 'Hardcoded Software', 'manual'), -] - -# The name of an image file (relative to this directory) to place at the top of -# the title page. -#latex_logo = None - -# For "manual" documents, if this is true, then toplevel headings are parts, -# not chapters. -#latex_use_parts = False - -# Additional stuff for the LaTeX preamble. -#latex_preamble = '' - -# Documents to append as an appendix to all manuals. -#latex_appendices = [] - -# If false, no module index is generated. -#latex_use_modindex = True diff --git a/hscommon/docs/conflict.rst b/hscommon/docs/conflict.rst deleted file mode 100644 index c186d809..00000000 --- a/hscommon/docs/conflict.rst +++ /dev/null @@ -1,27 +0,0 @@ -=================================================== -:mod:`conflict` - Detect and resolve name conflicts -=================================================== - -.. module:: conflict - -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. - -.. function:: get_conflicted_name(other_names, name) - - Returns a name based on ``name`` that is guaranteed not to be in ``other_names``. Name conflicts are resolved by prepending numbers in ``[]`` brackets to the name. - -.. function:: get_unconflicted_name(name) - - Returns ``name`` without ``[]`` brackets. - -.. function:: is_conflicted(name) - - Returns whether ``name`` is prepended with a bracketed number. - -.. function:: smart_copy(source_path, dest_path) - - Copies ``source_path`` to ``dest_path``, recursively. However, it does conflict resolution using functions in this module. - -.. function:: smart_move(source_path, dest_path) - - Same as :func:`smart_copy`, but it moves files instead. diff --git a/hscommon/docs/currency.rst b/hscommon/docs/currency.rst deleted file mode 100644 index 13d2992f..00000000 --- a/hscommon/docs/currency.rst +++ /dev/null @@ -1,62 +0,0 @@ -=================================== -:mod:`currency` - Manage currencies -=================================== - -This module facilitates currencies management. It exposes :class:`Currency` which lets you easily figure out their exchange value. - -The ``Currency`` class -====================== - -.. class:: Currency(code=None, name=None) - - 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:`currency.RatesDB` global instance is set through :meth:`Currency.set_rate_db` however, only fallback values will be used as exchange rates. - - .. staticmethod:: Currency.register(code, name, exponent=2, fallback_rate=1) - - Register a new currency in the currency list. - - .. staticmethod:: Currency.set_rates_db(db) - - Sets a new currency ``RatesDB`` instance to be used with all ``Currency`` instances. - - .. staticmethod:: Currency.set_rates_db() - - Returns the current ``RatesDB`` instance. - - .. method:: Currency.rates_date_range() - - Returns the range of date for which rates are available for this currency. - - .. method:: Currency.value_in(currency, date) - - Returns the value of this currency in terms of the other currency on the given date. - - .. method:: Currency.set_CAD_value(value, date) - - Sets currency's value in CAD on the given date. - -The ``RatesDB`` class -===================== - -.. class:: RatesDB(db_or_path=':memory:') - - A sqlite database that stores currency/date/value pairs, "value" being the value of the currency in CAD at the given date. Currencies are referenced by their 3-letter ISO code in the database and it its arguments (so ``currency_code`` arguments must be 3-letter strings). - - .. method:: RatesDB.date_range(currency_code) - - 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). - - .. method:: RatesDB.get_rate(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. - - .. method:: RatesDB.set_CAD_value(date, currency_code, value) - - Sets the CAD value of ``currency_code`` at ``date`` to ``value`` in the database. diff --git a/hscommon/docs/index.rst b/hscommon/docs/index.rst deleted file mode 100644 index ce4cd37b..00000000 --- a/hscommon/docs/index.rst +++ /dev/null @@ -1,32 +0,0 @@ -============================================== -hscommon - Common code used throughout HS apps -============================================== - -:Author: `Hardcoded Software `_ -:Dev website: http://hg.hardcoded.net/hscommon -:License: BSD License - -Introduction -============ - -``hscommon`` is a collection of tools used throughout HS apps. - -Dependencies -============ - -Python 3.1 is required. `py.test `_ is required to run the tests. - -API Documentation -================= - -.. toctree:: - :maxdepth: 2 - - build - conflict - currency - notify - path - reg - sqlite - util diff --git a/hscommon/docs/notify.rst b/hscommon/docs/notify.rst deleted file mode 100644 index af7dbdfe..00000000 --- a/hscommon/docs/notify.rst +++ /dev/null @@ -1,26 +0,0 @@ -========================================== -:mod:`notify` - Simple notification system -========================================== - -.. module:: notify - -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. - -.. class:: Broadcaster - - .. method:: notify(msg) - - Notify all connected listeners of ``msg``. That means that each listeners will have their method with the same name as ``msg`` called. - -.. class:: Listener(broadcaster) - - A listener is initialized with the broadcaster it's going to listen to. Initially, it is not connected. - - .. method:: connect() - - Connects the listener to its broadcaster. - - .. method:: disconnect() - - Disconnects the listener from its broadcaster. - diff --git a/hscommon/docs/path.rst b/hscommon/docs/path.rst deleted file mode 100644 index 8c594fae..00000000 --- a/hscommon/docs/path.rst +++ /dev/null @@ -1,13 +0,0 @@ -======================================== -:mod:`path` - Work with paths -======================================== - -.. module:: path - -.. class:: Path(value, separator=None) - - ``Path`` instances can be created from strings, other ``Path`` instances or tuples. If ``separator`` is not specified, the one from the OS is used. Once created, paths can be manipulated like a tuple, each element being an element of the path. It makes a few common operations easy, such as getting the filename (``path[-1]``) or the directory name or parent (``path[:-1]``). - - HS apps pretty much always refer to ``Path`` instances when a variable name ends with ``path``. If a variable is of another type, that type is usually explicited in the name. - - To make common operations (from ``os.path``, ``os`` and ``shutil``) convenient, the :mod:`io` module wraps these functions and converts paths to strings. diff --git a/hscommon/docs/reg.rst b/hscommon/docs/reg.rst deleted file mode 100644 index 0d074ebc..00000000 --- a/hscommon/docs/reg.rst +++ /dev/null @@ -1,25 +0,0 @@ -======================================== -:mod:`reg` - Manage app registration -======================================== - -.. module:: reg - -.. class:: RegistrableApplication - - HS main application classes subclass this. It provides an easy interface for managing whether the app should be in demo mode or not. - - .. method:: _setup_as_registered() - - Virtual. This is called whenever the app is unlocked. This is the one place to put code that changes to UI to indicate that the app is unlocked. - - .. method:: validate_code(code, email) - - Validates ``code`` with email. If it's valid, it does nothing. Otherwise, it raises ``InvalidCodeError`` with a message indicating why it's invalid (wrong product, wrong code format, fields swapped). - - .. method:: set_registration(code, email) - - If ``code`` and ``email`` are valid, sets ``registered`` to True as well as ``registration_code`` and ``registration_email`` and then calls :meth:`_setup_as_registered`. - -.. exception:: InvalidCodeError - - Raised during :meth:`RegistrableApplication.validate_code`. diff --git a/hscommon/docs/sqlite.rst b/hscommon/docs/sqlite.rst deleted file mode 100644 index 1faa8cdf..00000000 --- a/hscommon/docs/sqlite.rst +++ /dev/null @@ -1,9 +0,0 @@ -========================================== -:mod:`sqlite` - Threaded sqlite connection -========================================== - -.. module:: sqlite - -.. class:: ThreadedConn(dbname, autocommit) - - ``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. \ No newline at end of file diff --git a/hscommon/docs/util.rst b/hscommon/docs/util.rst deleted file mode 100644 index ac5d54f1..00000000 --- a/hscommon/docs/util.rst +++ /dev/null @@ -1,88 +0,0 @@ -======================================== -:mod:`util` - Miscellaneous utilities -======================================== - -.. module:: misc - -.. function:: nonone(value, replace_value) - - Returns ``value`` if value is not None. Returns ``replace_value`` otherwise. - -.. function:: dedupe(iterable) - - Returns a list of elements in ``iterable`` with all dupes removed. The order of the elements is preserved. - -.. function:: flatten(iterables, start_with=None) - - Takes the list of iterable ``iterables`` and returns a list containing elements of every iterable. - - 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. - -.. function:: first(iterable) - - Returns the first item of ``iterable`` or ``None`` if empty. - -.. function:: tryint(value, default=0) - - Tries to convert ``value`` to in ``int`` and returns ``default`` if it fails. - -.. function:: escape(s, to_escape, escape_with='\\') - - Returns ``s`` with characters in ``to_escape`` all prepended with ``escape_with``. - -.. function:: format_size(size, decimal=0, forcepower=-1, showdesc=True) - - Transform a byte count ``size`` in a formatted string (KB, MB etc..). ``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' - -.. function:: format_time(seconds, with_hours=True) - - Transforms seconds in a hh:mm:ss string. - - If `with_hours` if false, the format is mm:ss. - -.. function:: format_time_decimal(seconds) - - Transforms seconds in a strings like '3.4 minutes'. - -.. function:: get_file_ext(filename) - - Returns the lowercase extension part of ``filename``, without the dot. - -.. function:: pluralize(number, word, decimals=0, plural_word=None) - - Returns a string with ``number`` in front of ``word``, and adds a 's' to ``word`` if ``number`` > 1. If ``plural_word`` is defined, it will replace ``word`` in plural cases instead of appending a 's'. - -.. function:: rem_file_ext(filename) - - Returns ``filename`` without extension. - -.. function:: 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`` occurences will be replaced by that string. ``replace_from`` can also be a string. 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 string and has the same length as ``replace_from``, it will be transformed into a list. - -.. function:: 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() - -.. class:: FileOrPath(file_or_path, mode='rb') - - Does the same as :func:`open_if_filename`, but it can be used with a ``with`` statement. Example:: - - with FileOrPath(infile): - dostuff() - -.. function:: delete_if_empty(path, files_to_delete=[]) - - Same as with :func:`clean_empty_dirs`, but not recursive. - -.. function:: modified_after(first_path, second_path) - - Returns True if ``first_path``'s mtime is higher than ``second_path``'s mtime. \ No newline at end of file diff --git a/hscommon/gui/base.py b/hscommon/gui/base.py index 31b94db9..1b08d759 100644 --- a/hscommon/gui/base.py +++ b/hscommon/gui/base.py @@ -13,37 +13,53 @@ class NoopGUI: def __getattr__(self, func_name): return noop -# 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 multiple what we call here a "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 "view", that is why we set it as 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 _view_updated(). If you need another type of action on -# view instantiation, just override the method. 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): - pass #virtual + """(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): - # There's two times at which we set the view property: On initialization, where we set the - # view that we'll use for your 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). if self._view is None: # Initial view assignment if value is None: diff --git a/hscommon/gui/column.py b/hscommon/gui/column.py index 2204e9bf..5b25d58f 100644 --- a/hscommon/gui/column.py +++ b/hscommon/gui/column.py @@ -11,19 +11,92 @@ 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 @@ -59,40 +132,71 @@ class Columns(GUIObject): #--- 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 (display_name, marked) items for each optional column in the current - # view (marked means that it's visible). + """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 @@ -100,9 +204,13 @@ class Columns(GUIObject): 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 @@ -121,6 +229,8 @@ class Columns(GUIObject): 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: @@ -131,20 +241,35 @@ class Columns(GUIObject): 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 @@ -152,9 +277,13 @@ class Columns(GUIObject): #--- 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 index d27bede4..b57c92d1 100644 --- a/hscommon/gui/progress_window.py +++ b/hscommon/gui/progress_window.py @@ -10,17 +10,68 @@ 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 ``ThreadedJobPerformer`` (from the ``jobprogress`` library). + 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`. + + .. _job enabled: https://pypi.python.org/pypi/jobprogress + """ 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. @@ -28,8 +79,15 @@ class ProgressWindow(GUIObject, ThreadedJobPerformer): self.job_cancelled = True def pulse(self): - # Call this regularly from the GUI main run loop. - # the values might change before setValue happens + """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: @@ -45,6 +103,16 @@ class ProgressWindow(GUIObject, ThreadedJobPerformer): 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 ``Job`` instance (from the + ``jobprogress`` library) 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 diff --git a/hscommon/gui/selectable_list.py b/hscommon/gui/selectable_list.py index 9bf457f0..f8c4c761 100644 --- a/hscommon/gui/selectable_list.py +++ b/hscommon/gui/selectable_list.py @@ -11,6 +11,11 @@ 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 = [] @@ -26,16 +31,30 @@ class Selectable(Sequence): #--- Virtual def _update_selection(self): - # Takes the table's selection and does appropriates updates on the view and/or model, when - # appropriate. 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. A redesign of how this whole - # thing works is probably in order, but not now, there's too much breakage at once involved. - pass + """(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 @@ -44,6 +63,13 @@ class Selectable(Sequence): #--- 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 @@ -52,6 +78,13 @@ class Selectable(Sequence): @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 @@ -62,6 +95,10 @@ class Selectable(Sequence): 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: @@ -100,10 +137,14 @@ class SelectableList(MutableSequence, Selectable): #--- Virtual def _on_change(self): - pass + """(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): @@ -111,21 +152,57 @@ class SelectableList(MutableSequence, Selectable): return -1 -class GUISelectableList(SelectableList, GUIObject): - #--- View interface - # refresh() - # update_selection() - # +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 index 0192eb5e..f8ed91f7 100644 --- a/hscommon/gui/table.py +++ b/hscommon/gui/table.py @@ -12,14 +12,18 @@ from .base import GUIObject from .selectable_list import Selectable # We used to directly subclass list, but it caused problems at some point with deepcopy - -# Adding and removing footer here and there might seem (and is) hackish, but it's much simpler than -# the alternative, 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. 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 = [] @@ -44,12 +48,21 @@ class Table(MutableSequence, Selectable): 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)): @@ -57,6 +70,10 @@ class Table(MutableSequence, Selectable): 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: @@ -65,6 +82,14 @@ class Table(MutableSequence, Selectable): 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: @@ -79,6 +104,25 @@ class Table(MutableSequence, Selectable): #--- 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 @@ -91,6 +135,10 @@ class Table(MutableSequence, Selectable): @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 @@ -103,6 +151,10 @@ class Table(MutableSequence, Selectable): @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 @@ -112,6 +164,10 @@ class Table(MutableSequence, Selectable): @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: @@ -122,6 +178,13 @@ class Table(MutableSequence, Selectable): @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 @@ -133,35 +196,113 @@ class Table(MutableSequence, Selectable): @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): - # Creates a new row, adds it in the table and returns (row, insert_index) + """(Virtual) Creates a new row, adds it in the table. + + Returns ``(row, insert_index)``. + """ raise NotImplementedError() def _do_delete(self): - # Delete the selected rows + """(Virtual) Delete the selected rows. + """ pass def _fill(self): - # Called by refresh() - # Fills the table with all the rows that this table is supposed to have. + """(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) @@ -170,6 +311,11 @@ class GUITable(Table, GUIObject): #--- 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() @@ -181,13 +327,22 @@ class GUITable(Table, GUIObject): self.view.start_editing() def can_edit_cell(self, column_name, row_index): - # A row is, by default, editable as soon as it has an attr with the same name as `column`. - # If 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 + """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() @@ -202,6 +357,11 @@ class GUITable(Table, GUIObject): 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() @@ -210,6 +370,16 @@ class GUITable(Table, GUIObject): 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[:] @@ -222,6 +392,10 @@ class GUITable(Table, GUIObject): 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 @@ -229,6 +403,15 @@ class GUITable(Table, GUIObject): 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() @@ -236,6 +419,28 @@ class GUITable(Table, GUIObject): 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 @@ -248,19 +453,38 @@ class Row: #--- 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): - # Most of the time, the adequate sort key for a column is the column name with '_' prepended - # to it. This member usually corresponds to the unformated version of the column. If it's - # not there, we try the column_name without underscores - # Of course, override for exceptions. + """(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: @@ -268,6 +492,18 @@ class Row: #--- 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 @@ -286,11 +522,21 @@ class Row: 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 index ab636bd2..958dafcc 100644 --- a/hscommon/gui/text_field.py +++ b/hscommon/gui/text_field.py @@ -8,7 +8,32 @@ 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 = '' @@ -16,13 +41,25 @@ class TextField(GUIObject): #--- 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): - pass + """(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): @@ -30,10 +67,19 @@ class TextField(GUIObject): #--- 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 @@ -42,6 +88,13 @@ class TextField(GUIObject): @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 diff --git a/hscommon/gui/tree.py b/hscommon/gui/tree.py index ec84d67b..273584a7 100644 --- a/hscommon/gui/tree.py +++ b/hscommon/gui/tree.py @@ -9,6 +9,16 @@ 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 @@ -43,15 +53,26 @@ class Node(MutableSequence): #--- 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: @@ -59,6 +80,10 @@ class Node(MutableSequence): 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: @@ -66,24 +91,42 @@ class Node(MutableSequence): 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 = [] @@ -93,6 +136,10 @@ class Node(MutableSequence): @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: @@ -100,28 +147,47 @@ class Node(MutableSequence): 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): - # all selection changes go through this method, so you can override this if you want to - # customize the tree's behavior. + """(Virtual) Customize node selection behavior. + + By default, simply set :attr:`_selected_nodes`. + """ self._selected_nodes = nodes #--- Override def _view_updated(self): self.view.refresh() - #--- Public 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 @@ -133,6 +199,13 @@ class Tree(Node, GUIObject): @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 @@ -141,6 +214,12 @@ class Tree(Node, GUIObject): @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 @@ -152,6 +231,12 @@ class Tree(Node, GUIObject): @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 diff --git a/hscommon/io.py b/hscommon/io.py deleted file mode 100644 index dd0fb4f4..00000000 --- a/hscommon/io.py +++ /dev/null @@ -1,79 +0,0 @@ -# Created By: Virgil Dupras -# Created On: 2007-10-23 -# Copyright 2013 Hardcoded Software (http://www.hardcoded.net) - -# This software is licensed under the "BSD" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at -# http://www.hardcoded.net/licenses/bsd_license - -# HS code should only deal with Path instances, not string paths. One of the annoyances of this -# is to always have to convert Path instances with unicode() when calling open() or listdir() etc.. -# this unit takes care of this - -import builtins -import os -import os.path -import shutil -import logging - -def log_io_error(func): - """ Catches OSError, IOError and WindowsError and log them - """ - 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 - -def copy(source_path, dest_path): - return shutil.copy(str(source_path), str(dest_path)) - -def copytree(source_path, dest_path, *args, **kwargs): - return shutil.copytree(str(source_path), str(dest_path), *args, **kwargs) - -def exists(path): - return os.path.exists(str(path)) - -def isdir(path): - return os.path.isdir(str(path)) - -def isfile(path): - return os.path.isfile(str(path)) - -def islink(path): - return os.path.islink(str(path)) - -def listdir(path): - return os.listdir(str(path)) - -def mkdir(path, *args, **kwargs): - return os.mkdir(str(path), *args, **kwargs) - -def makedirs(path, *args, **kwargs): - return os.makedirs(str(path), *args, **kwargs) - -def move(source_path, dest_path): - return shutil.move(str(source_path), str(dest_path)) - -def open(path, *args, **kwargs): - return builtins.open(str(path), *args, **kwargs) - -def remove(path): - return os.remove(str(path)) - -def rename(source_path, dest_path): - return os.rename(str(source_path), str(dest_path)) - -def rmdir(path): - return os.rmdir(str(path)) - -def rmtree(path): - return shutil.rmtree(str(path)) - -def stat(path): - return os.stat(str(path)) diff --git a/hscommon/locale/cs/LC_MESSAGES/hscommon.po b/hscommon/locale/cs/LC_MESSAGES/hscommon.po deleted file mode 100644 index b7ec835b..00000000 --- a/hscommon/locale/cs/LC_MESSAGES/hscommon.po +++ /dev/null @@ -1,24 +0,0 @@ -# Translators: -msgid "" -msgstr "" -"Project-Id-Version: hscommon\n" -"PO-Revision-Date: 2013-04-28 18:29+0000\n" -"Last-Translator: hsoft \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" - -#: hscommon/reg.py:32 -msgid "" -"{name} is fairware, which means \"open source software developed with expectation of fair contributions from users\". It's a very interesting concept, but one year of fairware has shown that most people just want to know how much it costs and not be bothered with theories about intellectual property.\n" -"\n" -"So I won't bother you and will be very straightforward: You can try {name} for free but you have to buy it in order to use it without limitations. In demo mode, {name} {limitation}.\n" -"\n" -"So it's as simple as this. If you're curious about fairware, however, I encourage you to read more about it by clicking on the \"Fairware?\" button." -msgstr "" -"{name} je fairware, což znamená \"open source software vyvíjený v očekávání poctivých příspěvků od uživatelů\". Jde o velmi zajímavý nápad, a po roce se ukazuje, že většina lidí se zajímá o cenu vývoje, ale jsou jim ukradené povídačky o duševním vlastnictví.\n" -"\n" -"Takže vás nebudu otravovat a řeknu to bez okolků: {name} si můžete zdarma vyzkoušet, ale pokud ho chcete používat bez omezení, musíte si ho koupit. V demo režimu, {name} {limitation}.\n" -"\n" -"A to je celé. Pokud se o fairware chcete dozvědět více, klepněte na tlačítko \"Fairware?\"." diff --git a/hscommon/locale/de/LC_MESSAGES/hscommon.po b/hscommon/locale/de/LC_MESSAGES/hscommon.po deleted file mode 100644 index 73b37a0b..00000000 --- a/hscommon/locale/de/LC_MESSAGES/hscommon.po +++ /dev/null @@ -1,31 +0,0 @@ -# -msgid "" -msgstr "" -"Content-Type: text/plain; charset=utf-8\n" -"Content-Transfer-Encoding: utf-8\n" - -#: hscommon/reg.py:39 -msgid "" -"{name} is Fairware, which means \"open source software developed with expectation of fair contributions from users\". Hours have been invested in this software with the expectation that users will be fair enough to compensate them. The \"Unpaid hours\" figure you see below is the hours that have yet to be compensated for this project.\n" -"\n" -"If you like this application, please make a contribution that you consider fair. Thanks!\n" -"\n" -"If you cannot afford to contribute, you can either ignore this reminder or send an e-mail at support@hardcoded.net so I can send you a registration key.\n" -"\n" -"This dialog doesn't show when there are no unpaid hours or when you have a valid contribution key." -msgstr "" -"{name} ist Fairware, das bedeutet \"Open Source Software, entwickelt in der Hoffnung auf einen fairen Beitrag von den Benutzern\". Viel Zeit wurde in die Software investiert, mit der Erwartung der Nutzer möge fair genug sein die Entwickler für ihren Einsatz zu kompensieren. Die \"Unbezahlte Stunden\" Abbildung zeigt die Anzahl der Stunden die noch nicht bezahlt wurden.\n" -"Wenn Sie diese Anwendung mögen, so spenden Sie bitte einen Ihrer Ansicht nach angemessenen Betrag. Danke!\n" -"\n" -"Wenn Sie es sich nicht leisten können zu spenden, können Sie diese Erinnerung entweder ignorieren oder mir eine Anfrage an hsoft@hardcoded.net schicken, mit der Bitte für einen Registrierungsschlüssel.\n" -"\n" -"Dieser Dialog erscheint nicht, wenn es keine unbezahlten Stunden gibt oder Sie einen gültigen Registrierungsschlüssel besitzen." - -#: hscommon/reg.py:51 -msgid "" -"{name} is fairware, which means \"open source software developed with expectation of fair contributions from users\". It's a very interesting concept, but one year of fairware has shown that most people just want to know how much it costs and not be bothered with theories about intellectual property.\n" -"\n" -"So I won't bother you and will be very straightforward: You can try {name} for free but you have to buy it in order to use it without limitations. In demo mode, {name} {limitation}.\n" -"\n" -"So it's as simple as this. If you're curious about fairware, however, I encourage you to read more about it by clicking on the \"Fairware?\" button." -msgstr "" diff --git a/hscommon/locale/es/LC_MESSAGES/hscommon.po b/hscommon/locale/es/LC_MESSAGES/hscommon.po deleted file mode 100644 index add7ba35..00000000 --- a/hscommon/locale/es/LC_MESSAGES/hscommon.po +++ /dev/null @@ -1,24 +0,0 @@ -# Translators: -msgid "" -msgstr "" -"Project-Id-Version: hscommon\n" -"PO-Revision-Date: 2013-04-28 18:29+0000\n" -"Last-Translator: hsoft \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" - -#: hscommon/reg.py:32 -msgid "" -"{name} is fairware, which means \"open source software developed with expectation of fair contributions from users\". It's a very interesting concept, but one year of fairware has shown that most people just want to know how much it costs and not be bothered with theories about intellectual property.\n" -"\n" -"So I won't bother you and will be very straightforward: You can try {name} for free but you have to buy it in order to use it without limitations. In demo mode, {name} {limitation}.\n" -"\n" -"So it's as simple as this. If you're curious about fairware, however, I encourage you to read more about it by clicking on the \"Fairware?\" button." -msgstr "" -"{name} es Fairware, es decir \"software de código abierto desarrollado con la expectativa de recibir una retribución justa por parte de los usuarios\". Es una idea muy interesante, aunque tras más de un año de uso es evidente que la mayoría de los usuarios sólo están interesados en el precio del producto y no en teorías sobre la propiedad intelectual.\n" -"\n" -"Así pues seré claro: puede probar {name} gratuitamente pero debe comprarlo para un uso completo sin limitaciones. En el modo de prueba, {name} {limitation}.\n" -"\n" -"En resumen, si tiene curiosidad por conocer fairware le animo a que lea sobre ello pulsando el botón de \"¿Fairware?\"." diff --git a/hscommon/locale/fr/LC_MESSAGES/hscommon.po b/hscommon/locale/fr/LC_MESSAGES/hscommon.po deleted file mode 100644 index 6e67a113..00000000 --- a/hscommon/locale/fr/LC_MESSAGES/hscommon.po +++ /dev/null @@ -1,24 +0,0 @@ -# Translators: -msgid "" -msgstr "" -"Project-Id-Version: hscommon\n" -"PO-Revision-Date: 2013-04-28 18:29+0000\n" -"Last-Translator: hsoft \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" - -#: hscommon/reg.py:32 -msgid "" -"{name} is fairware, which means \"open source software developed with expectation of fair contributions from users\". It's a very interesting concept, but one year of fairware has shown that most people just want to know how much it costs and not be bothered with theories about intellectual property.\n" -"\n" -"So I won't bother you and will be very straightforward: You can try {name} for free but you have to buy it in order to use it without limitations. In demo mode, {name} {limitation}.\n" -"\n" -"So it's as simple as this. If you're curious about fairware, however, I encourage you to read more about it by clicking on the \"Fairware?\" button." -msgstr "" -"{name} est Fairware, ce qui signifie \"open source développé avec des attentes de contributions justes de la part des utilisateurs\". C'est un concept excessivement intéressant, mais un an de fairware a révélé que la plupart des gens ne sont que peu intéressés à des discours sur la propriété intellectuelle et veulent simplement savoir combien ça coûte.\n" -"\n" -"Donc, je serai bref et direct: Vous pouvez essayer {name} gratuitement, mais un achat est requis pour un usage sans limitation. En mode démo, {name} {limitation}.\n" -"\n" -"C'est aussi simple que ça. Par contre, si vous êtes curieux, je vous encourage à cliquer sur le bouton \"Fairware?\" pour en savoir plus." diff --git a/hscommon/locale/hscommon.pot b/hscommon/locale/hscommon.pot deleted file mode 100644 index 88252696..00000000 --- a/hscommon/locale/hscommon.pot +++ /dev/null @@ -1,15 +0,0 @@ - -msgid "" -msgstr "" -"Content-Type: text/plain; charset=utf-8\n" -"Content-Transfer-Encoding: utf-8\n" - -#: hscommon/reg.py:32 -msgid "" -"{name} is fairware, which means \"open source software developed with expectation of fair contributions from users\". It's a very interesting concept, but one year of fairware has shown that most people just want to know how much it costs and not be bothered with theories about intellectual property.\n" -"\n" -"So I won't bother you and will be very straightforward: You can try {name} for free but you have to buy it in order to use it without limitations. In demo mode, {name} {limitation}.\n" -"\n" -"So it's as simple as this. If you're curious about fairware, however, I encourage you to read more about it by clicking on the \"Fairware?\" button." -msgstr "" - diff --git a/hscommon/locale/hy/LC_MESSAGES/hscommon.po b/hscommon/locale/hy/LC_MESSAGES/hscommon.po deleted file mode 100644 index dd55ccb9..00000000 --- a/hscommon/locale/hy/LC_MESSAGES/hscommon.po +++ /dev/null @@ -1,24 +0,0 @@ -# Translators: -msgid "" -msgstr "" -"Project-Id-Version: hscommon\n" -"PO-Revision-Date: 2013-04-28 18:29+0000\n" -"Last-Translator: hsoft \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" - -#: hscommon/reg.py:32 -msgid "" -"{name} is fairware, which means \"open source software developed with expectation of fair contributions from users\". It's a very interesting concept, but one year of fairware has shown that most people just want to know how much it costs and not be bothered with theories about intellectual property.\n" -"\n" -"So I won't bother you and will be very straightforward: You can try {name} for free but you have to buy it in order to use it without limitations. In demo mode, {name} {limitation}.\n" -"\n" -"So it's as simple as this. If you're curious about fairware, however, I encourage you to read more about it by clicking on the \"Fairware?\" button." -msgstr "" -"{name}-ը fairware է, ինչը նշանակում է \"ազատ կոդով ծրագիր, որը զարգացվում է՝ ակնկալելով օգտվողների աջակցությունը\": Սա շատ հետաքրքիր սկզբունք է, սակայն մեկ տարվա fairware-ի արդյունքը ցույց է տալիս, որ շատ մարդիկ պարզապես ցանկանում են իմանալ, թե այն ինչ արժե, բայց չեն մտահոգվում ինտելեկտուալ սեփականության մասին:\n" -"\n" -"Ուստի ես չեմ ցանկանում խանգարել Ձեզ և կլինեմ շատ պարզ. Կարող եք փորձել {name}-ը ազատորեն, բայց պետք է գնեք ծրագիրը՝ հանելու համար բոլոր սահմանափակումները: Փորձնական եղանակում {name} {limitation}:\n" -"\n" -"Ամեն ինչ պարզ է, եթե Ձեզ հետաքրքիր է fairware-ը, ապա կարող եք մանրամասն կարդաք՝ սեղմելով այս հղմանը՝ \"Fairware է՞\":" diff --git a/hscommon/locale/it/LC_MESSAGES/hscommon.po b/hscommon/locale/it/LC_MESSAGES/hscommon.po deleted file mode 100644 index 765d470b..00000000 --- a/hscommon/locale/it/LC_MESSAGES/hscommon.po +++ /dev/null @@ -1,24 +0,0 @@ -# Translators: -msgid "" -msgstr "" -"Project-Id-Version: hscommon\n" -"PO-Revision-Date: 2013-04-28 18:29+0000\n" -"Last-Translator: hsoft \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" - -#: hscommon/reg.py:32 -msgid "" -"{name} is fairware, which means \"open source software developed with expectation of fair contributions from users\". It's a very interesting concept, but one year of fairware has shown that most people just want to know how much it costs and not be bothered with theories about intellectual property.\n" -"\n" -"So I won't bother you and will be very straightforward: You can try {name} for free but you have to buy it in order to use it without limitations. In demo mode, {name} {limitation}.\n" -"\n" -"So it's as simple as this. If you're curious about fairware, however, I encourage you to read more about it by clicking on the \"Fairware?\" button." -msgstr "" -"{name} è denominato \"Fairware\", che significa \"software 'open source' sviluppato aspettandosi un contributo equo e corretto da parte degli utilizzatori\". E' un concetto molto interessante, ma un anno di 'fairware' ha dimostrato che la maggior parte della gente vuole solo sapere 'quanto costa' e non essere scocciata con delle teorie sulla proprietà intellettuale.\n" -"\n" -"Così non vi disturberò oltre e sarò diretto: potete provare {name} gratuitamente ma dovrete acquistarlo per usarlo senza limitazioni. In modalità 'demo', {name} {limitation}.\n" -"\n" -"In questo modo è semplice. Se siete curiosi e volete approfondire il concetto di 'fairware', vi invito a leggere di più sull'argomento cliccando sul pulsante \"Fairware?\"." diff --git a/hscommon/locale/nl/LC_MESSAGES/hscommon.po b/hscommon/locale/nl/LC_MESSAGES/hscommon.po deleted file mode 100644 index bb2f1f08..00000000 --- a/hscommon/locale/nl/LC_MESSAGES/hscommon.po +++ /dev/null @@ -1,24 +0,0 @@ -# Translators: -msgid "" -msgstr "" -"Project-Id-Version: hscommon\n" -"PO-Revision-Date: 2013-04-28 18:29+0000\n" -"Last-Translator: hsoft \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" - -#: hscommon/reg.py:32 -msgid "" -"{name} is fairware, which means \"open source software developed with expectation of fair contributions from users\". It's a very interesting concept, but one year of fairware has shown that most people just want to know how much it costs and not be bothered with theories about intellectual property.\n" -"\n" -"So I won't bother you and will be very straightforward: You can try {name} for free but you have to buy it in order to use it without limitations. In demo mode, {name} {limitation}.\n" -"\n" -"So it's as simple as this. If you're curious about fairware, however, I encourage you to read more about it by clicking on the \"Fairware?\" button." -msgstr "" -"{name} is Fairware, dit betekent \"open source software ontwikkeld in de hoop op een redelijke bijdrage van gebruikers\". Het is een interessant concept, maar na een jaar fairware is gebleken dat de meeste mensen gewoon willen weten wat het kost en niet lastig gevallen willen worden met theorieën over intellectueel eigendom.\n" -"\n" -"Ik zal u dus niet lastig vallen en duidelijk zijn: U kunt {name} gratis proberen, maar moet het kopen om het zonder beperkingen te kunnen gebruiken. In demo mode {name} {limitation}.\n" -"\n" -"Het is dus eigenlijk heel simpel. Als u toch geïnteresseerd bent in fairware, raad ik u aan hier meer over te lezen door op de knop \"Fairware?\" te klikken." diff --git a/hscommon/locale/pt_BR/LC_MESSAGES/hscommon.po b/hscommon/locale/pt_BR/LC_MESSAGES/hscommon.po deleted file mode 100644 index 6a30fd2b..00000000 --- a/hscommon/locale/pt_BR/LC_MESSAGES/hscommon.po +++ /dev/null @@ -1,24 +0,0 @@ -# Translators: -msgid "" -msgstr "" -"Project-Id-Version: hscommon\n" -"PO-Revision-Date: 2013-04-28 18:29+0000\n" -"Last-Translator: hsoft \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" - -#: hscommon/reg.py:32 -msgid "" -"{name} is fairware, which means \"open source software developed with expectation of fair contributions from users\". It's a very interesting concept, but one year of fairware has shown that most people just want to know how much it costs and not be bothered with theories about intellectual property.\n" -"\n" -"So I won't bother you and will be very straightforward: You can try {name} for free but you have to buy it in order to use it without limitations. In demo mode, {name} {limitation}.\n" -"\n" -"So it's as simple as this. If you're curious about fairware, however, I encourage you to read more about it by clicking on the \"Fairware?\" button." -msgstr "" -"{name} é fairware, o que quer dizer \"software de código aberto desenvolvido sob a expectativa de contribuição justa de seus usuários\". É um conceito muito interessante, mas um ano de fairware mostrou que a maioria das pessoas só deseja saber quanto o software custa, sem ser incomodada com teorias sobre propriedade intelectual.\n" -"\n" -"Portanto não o incomodarei e serei bem direto: você pode testar {name} de graça, mas deverá comprá-lo para usá-lo sem limitações. Em modo demo, {name} {limitation}.\n" -"\n" -"É simples assim. Caso você tenha curiosidade sobre fairware, recomendo que leia mais sobre o assunto clicando o botão \"Fairware?\"." diff --git a/hscommon/locale/ru/LC_MESSAGES/hscommon.po b/hscommon/locale/ru/LC_MESSAGES/hscommon.po deleted file mode 100644 index 8af3bb77..00000000 --- a/hscommon/locale/ru/LC_MESSAGES/hscommon.po +++ /dev/null @@ -1,24 +0,0 @@ -# Translators: -msgid "" -msgstr "" -"Project-Id-Version: hscommon\n" -"PO-Revision-Date: 2013-04-28 18:29+0000\n" -"Last-Translator: hsoft \n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: utf-8\n" -"Language: ru\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" - -#: hscommon/reg.py:32 -msgid "" -"{name} is fairware, which means \"open source software developed with expectation of fair contributions from users\". It's a very interesting concept, but one year of fairware has shown that most people just want to know how much it costs and not be bothered with theories about intellectual property.\n" -"\n" -"So I won't bother you and will be very straightforward: You can try {name} for free but you have to buy it in order to use it without limitations. In demo mode, {name} {limitation}.\n" -"\n" -"So it's as simple as this. If you're curious about fairware, however, I encourage you to read more about it by clicking on the \"Fairware?\" button." -msgstr "" -"{name} является fairware, что означает \"программное обеспечение с открытым исходным кодом, с ожиданием справедливого вклада от пользователей\". Это очень интересная концепция, но один год fairware показал, что большинство людей просто хотят знать, сколько это стоит, а не возиться с теориями об интеллектуальной собственности.\n" -"\n" -"Так что, я не буду утомлять вас и буду очень краток: вы можете попробовать {name} бесплатно, но вам придётся купить её, чтобы использовать без ограничений. В демо-режиме {name} {limitation}.\n" -"\n" -"То есть, это просто. Если вам интересно, о fairware вы можете больше узнать нажав на кнопку \"Fairware?\"." diff --git a/hscommon/locale/uk/LC_MESSAGES/hscommon.po b/hscommon/locale/uk/LC_MESSAGES/hscommon.po deleted file mode 100644 index 11edb0da..00000000 --- a/hscommon/locale/uk/LC_MESSAGES/hscommon.po +++ /dev/null @@ -1,24 +0,0 @@ -# Translators: -msgid "" -msgstr "" -"Project-Id-Version: hscommon\n" -"PO-Revision-Date: 2013-04-28 18:29+0000\n" -"Last-Translator: hsoft \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" - -#: hscommon/reg.py:32 -msgid "" -"{name} is fairware, which means \"open source software developed with expectation of fair contributions from users\". It's a very interesting concept, but one year of fairware has shown that most people just want to know how much it costs and not be bothered with theories about intellectual property.\n" -"\n" -"So I won't bother you and will be very straightforward: You can try {name} for free but you have to buy it in order to use it without limitations. In demo mode, {name} {limitation}.\n" -"\n" -"So it's as simple as this. If you're curious about fairware, however, I encourage you to read more about it by clicking on the \"Fairware?\" button." -msgstr "" -"{name} є fairware, що означає \"програмне забезпечення з відкритим вихідним кодом, з очікуванням справедливого вкладу від користувачів\". Це дуже цікава концепція, але один рік fairware показав, що більшість людей просто хочуть знати, скільки це коштує, а не возитися з теоріями про інтелектуальну власність.\n" -"\n" -"Тож я не буду втомлювати Вас і поясню просто: Ви можете спробувати {name} безкоштовно, але Вам доведеться купити його, щоб використовувати її без обмежень. У демо-режимі, {name} {limitation}.\n" -"\n" -"Ось так це просто. Якщо Вас цікавить ідея fairware, то я запрошую Вас дізнатися більше про це, натиснувши на кнопку \"Fairware?\"." diff --git a/hscommon/locale/vi/LC_MESSAGES/hscommon.po b/hscommon/locale/vi/LC_MESSAGES/hscommon.po deleted file mode 100644 index 3bbb5e10..00000000 --- a/hscommon/locale/vi/LC_MESSAGES/hscommon.po +++ /dev/null @@ -1,20 +0,0 @@ -# Translators: -msgid "" -msgstr "" -"Project-Id-Version: hscommon\n" -"PO-Revision-Date: 2013-07-05 11:23+0000\n" -"Last-Translator: hsoft \n" -"Language-Team: Vietnamese (http://www.transifex.com/projects/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" - -#: hscommon/reg.py:32 -msgid "" -"{name} is fairware, which means \"open source software developed with expectation of fair contributions from users\". It's a very interesting concept, but one year of fairware has shown that most people just want to know how much it costs and not be bothered with theories about intellectual property.\n" -"\n" -"So I won't bother you and will be very straightforward: You can try {name} for free but you have to buy it in order to use it without limitations. In demo mode, {name} {limitation}.\n" -"\n" -"So it's as simple as this. If you're curious about fairware, however, I encourage you to read more about it by clicking on the \"Fairware?\" button." -msgstr "" diff --git a/hscommon/locale/zh_CN/LC_MESSAGES/hscommon.po b/hscommon/locale/zh_CN/LC_MESSAGES/hscommon.po deleted file mode 100644 index 50ece4ef..00000000 --- a/hscommon/locale/zh_CN/LC_MESSAGES/hscommon.po +++ /dev/null @@ -1,32 +0,0 @@ -# -msgid "" -msgstr "" -"Content-Type: text/plain; charset=utf-8\n" -"Content-Transfer-Encoding: utf-8\n" - -#: hscommon/reg.py:39 -msgid "" -"{name} is Fairware, which means \"open source software developed with expectation of fair contributions from users\". Hours have been invested in this software with the expectation that users will be fair enough to compensate them. The \"Unpaid hours\" figure you see below is the hours that have yet to be compensated for this project.\n" -"\n" -"If you like this application, please make a contribution that you consider fair. Thanks!\n" -"\n" -"If you cannot afford to contribute, you can either ignore this reminder or send an e-mail at support@hardcoded.net so I can send you a registration key.\n" -"\n" -"This dialog doesn't show when there are no unpaid hours or when you have a valid contribution key." -msgstr "" -"{name} 是一款捐助软件,也就是说 \"用户对研发开源软件所花费的时间进行符合用户意愿的捐助\"。用户可以根据研发人员花费在开发软件上的时间进行合理的补偿。用户在下面看到的 \"未支付的时间\" (Unpaid hours)表示需要对该软件进行补偿的时间。\n" -" \n" -"如果您喜欢这款软件,我诚挚的希望您可以进行必要的捐助。谢谢!\n" -"\n" -"如果您无法承担捐助,您也可以忽略此提醒,或者发送电子邮件至 support@hardcoded.net ,我会发送给您一个注册密钥。\n" -"\n" -"当软件没有未支付的时间或您已使用一个有效的注册密钥,此对话框将不会再显示。" - -#: hscommon/reg.py:51 -msgid "" -"{name} is fairware, which means \"open source software developed with expectation of fair contributions from users\". It's a very interesting concept, but one year of fairware has shown that most people just want to know how much it costs and not be bothered with theories about intellectual property.\n" -"\n" -"So I won't bother you and will be very straightforward: You can try {name} for free but you have to buy it in order to use it without limitations. In demo mode, {name} {limitation}.\n" -"\n" -"So it's as simple as this. If you're curious about fairware, however, I encourage you to read more about it by clicking on the \"Fairware?\" button." -msgstr "" diff --git a/hscommon/notify.py b/hscommon/notify.py index 13421f01..b61e5f8c 100644 --- a/hscommon/notify.py +++ b/hscommon/notify.py @@ -4,9 +4,19 @@ # which should be included with this package. The terms are also available at # http://www.hardcoded.net/licenses/bsd_license +"""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() @@ -14,6 +24,10 @@ class Broadcaster: 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) @@ -23,6 +37,8 @@ class Broadcaster: 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) @@ -38,9 +54,13 @@ class Listener: 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): diff --git a/hscommon/path.py b/hscommon/path.py index a6f1675a..36abda08 100755 --- a/hscommon/path.py +++ b/hscommon/path.py @@ -12,24 +12,18 @@ 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. - path[index] returns a string - path[start:stop] returns a Path - start and stop can be int, but the can also be path instances. When start - or stop are Path like in refpath[p1:p2], it is the same thing as typing - refpath[len(p1):-len(p2)], except that it will only slice out stuff that are - equal. For example, 'a/b/c/d'['a/z':'z/d'] returns 'b/c', not ''. - See the test units for more details. + We subclass ``tuple``, each element of the tuple represents an element of the path. - You can use the + operator, which is the same thing as with tuples, but - returns a Path. - - In HS applications, all paths variable should be Path instances. These Path instances should - be converted to str only at the last moment (when it is needed in an external function, such - as os.rename) + * ``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__ = () @@ -94,12 +88,11 @@ class Path(tuple): 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 __getslice__(self, i, j): #I have to override it because tuple uses it. - return Path(tuple.__getslice__(self, i, j)) - def __hash__(self): return tuple.__hash__(self) @@ -133,6 +126,21 @@ class Path(tuple): 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)) @@ -153,7 +161,7 @@ class Path(tuple): return op.islink(str(self)) def listdir(self): - return os.listdir(str(self)) + return [self[name] for name in os.listdir(str(self))] def mkdir(self, *args, **kwargs): return os.mkdir(str(self), *args, **kwargs) @@ -182,3 +190,43 @@ class Path(tuple): 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/pygettext.py b/hscommon/pygettext.py index 5d2305ba..ad3157b6 100755 --- a/hscommon/pygettext.py +++ b/hscommon/pygettext.py @@ -378,7 +378,7 @@ def main(source_files, outpath, keywords=None): # initialize list of strings to exclude if options.excludefilename: try: - fp = open(options.excludefilename) + fp = open(options.excludefilename, encoding='utf-8') options.toexclude = fp.readlines() fp.close() except IOError: @@ -392,7 +392,7 @@ def main(source_files, outpath, keywords=None): for filename in source_files: if options.verbose: print('Working on %s' % filename) - fp = open(filename) + fp = open(filename, encoding='utf-8') closep = 1 try: eater.set_filename(filename) @@ -408,7 +408,7 @@ def main(source_files, outpath, keywords=None): if closep: fp.close() - fp = open(options.outfile, 'w') + fp = open(options.outfile, 'w', encoding='utf-8') closep = 1 try: eater.write(fp) diff --git a/hscommon/reg.py b/hscommon/reg.py index 3b5a57e0..98e84d56 100644 --- a/hscommon/reg.py +++ b/hscommon/reg.py @@ -9,6 +9,7 @@ import re from hashlib import md5 +from . import desktop from .trans import trget tr = trget('hscommon') @@ -47,7 +48,6 @@ class RegistrableApplication: # setup_as_registered() # show_message(msg) # show_demo_nag(prompt) - # open_url(url) PROMPT_NAME = "" DEMO_LIMITATION = "" @@ -154,13 +154,13 @@ class RegistrableApplication: pass def contribute(self): - self.view.open_url("http://open.hardcoded.net/contribute/") + desktop.open_url("http://open.hardcoded.net/contribute/") def buy(self): - self.view.open_url("http://www.hardcoded.net/purchase.htm") + desktop.open_url("http://www.hardcoded.net/purchase.htm") def about_fairware(self): - self.view.open_url("http://open.hardcoded.net/about/") + desktop.open_url("http://open.hardcoded.net/about/") @property def should_show_fairware_reminder(self): diff --git a/hscommon/sqlite.py b/hscommon/sqlite.py index f61314d8..f4978237 100644 --- a/hscommon/sqlite.py +++ b/hscommon/sqlite.py @@ -114,6 +114,10 @@ class _ActualThread(threading.Thread): 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 diff --git a/hscommon/tests/conflict_test.py b/hscommon/tests/conflict_test.py index 825c48a2..cce294e8 100644 --- a/hscommon/tests/conflict_test.py +++ b/hscommon/tests/conflict_test.py @@ -61,44 +61,44 @@ class TestCase_move_copy: def pytest_funcarg__do_setup(self, request): tmpdir = request.getfuncargvalue('tmpdir') self.path = Path(str(tmpdir)) - io.open(self.path + 'foo', 'w').close() - io.open(self.path + 'bar', 'w').close() - io.mkdir(self.path + 'dir') + 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 io.exists(self.path + 'baz') - assert not io.exists(self.path + 'foo') + 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 io.exists(self.path + 'baz') - assert io.exists(self.path + 'foo') + 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 io.exists(self.path + ('dir', 'foo')) - assert not io.exists(self.path + 'foo') + 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 io.exists(self.path + '[000] bar') - assert not io.exists(self.path + 'foo') + 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 io.exists(self.path + ('dir', 'foo')) - assert io.exists(self.path + ('dir', '[000] foo')) - assert not io.exists(self.path + 'foo') - assert not io.exists(self.path + 'bar') + 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)) - io.mkdir(path + 'foo') - io.mkdir(path + 'bar') - smart_copy(path + 'foo', path + 'bar') # no crash - assert io.exists(path + '[000] bar') + 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 index 5c940a01..7b1c5fb3 100644 --- a/hscommon/tests/currency_test.py +++ b/hscommon/tests/currency_test.py @@ -9,7 +9,6 @@ from datetime import date import sqlite3 as sqlite -from .. import io from ..testutil import eq_, assert_almost_equal from ..currency import Currency, RatesDB, CAD, EUR, USD @@ -64,7 +63,7 @@ def test_db_with_connection(): def test_corrupt_db(tmpdir): dbpath = str(tmpdir.join('foo.db')) - fh = io.open(dbpath, 'w') + fh = open(dbpath, 'w') fh.write('corrupted') fh.close() db = RatesDB(dbpath) # no crash. deletes the old file and start a new db diff --git a/hscommon/tests/path_test.py b/hscommon/tests/path_test.py index 75c41665..8181d318 100644 --- a/hscommon/tests/path_test.py +++ b/hscommon/tests/path_test.py @@ -7,10 +7,11 @@ # http://www.hardcoded.net/licenses/bsd_license import sys +import os from pytest import raises, mark -from ..path import * +from ..path import Path, pathify from ..testutil import eq_ def pytest_funcarg__force_ossep(request): @@ -44,7 +45,7 @@ def test_init_with_tuple_and_list(force_ossep): def test_init_with_invalid_value(force_ossep): try: path = Path(42) - self.fail() + assert False except TypeError: pass @@ -63,6 +64,16 @@ def test_slicing(force_ossep): 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. """ @@ -99,7 +110,7 @@ def test_add(force_ossep): #Invalid concatenation try: Path(('foo','bar')) + 1 - self.fail() + assert False except TypeError: pass @@ -180,6 +191,16 @@ def test_Path_of_a_Path_returns_self(force_ossep): 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 @@ -206,4 +227,25 @@ def test_remove_drive_letter(monkeypatch): p = Path('C:\\') eq_(p.remove_drive_letter(), Path('')) p = Path('z:\\foo') - eq_(p.remove_drive_letter(), Path('foo')) \ No newline at end of file + 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/reg_test.py b/hscommon/tests/reg_test.py deleted file mode 100644 index 49d89872..00000000 --- a/hscommon/tests/reg_test.py +++ /dev/null @@ -1,68 +0,0 @@ -# Created By: Virgil Dupras -# Created On: 2010-01-31 -# Copyright 2013 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "BSD" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at -# http://www.hardcoded.net/licenses/bsd_license - -from hashlib import md5 - -from ..testutil import CallLogger -from ..reg import RegistrableApplication, InvalidCodeError - -def md5s(s): - return md5(s.encode('utf-8')).hexdigest() - -def assert_valid(appid, code, email): - app = RegistrableApplication(CallLogger(), appid) - try: - app.validate_code(code, email) - except InvalidCodeError as e: - raise AssertionError("Registration failed: {0}".format(str(e))) - -def assert_invalid(appid, code, email, msg_contains=None): - app = RegistrableApplication(CallLogger(), appid) - try: - app.validate_code(code, email) - except InvalidCodeError as e: - if msg_contains: - assert msg_contains in str(e) - else: - raise AssertionError("InvalidCodeError not raised") - -def test_valid_code(): - email = 'foo@bar.com' - appid = 42 - code = md5s('42' + email + '43' + 'aybabtu') - assert_valid(appid, code, email) - -def test_invalid_code(): - email = 'foo@bar.com' - appid = 42 - code = md5s('43' + email + '43' + 'aybabtu') - assert_invalid(appid, code, email) - -def test_suggest_other_apps(): - # If a code is valid for another app, say so in the error message. - email = 'foo@bar.com' - appid = 42 - # 2 is moneyGuru's appid - code = md5s('2' + email + '43' + 'aybabtu') - assert_invalid(appid, code, email, msg_contains="moneyGuru") - -def test_invert_code_and_email(): - # Try inverting code and email during validation in case the user mixed the fields up. - # We still show an error here. It kind of sucks, but if we don't, the email and code fields - # end up mixed up in the preferences. It's not as if this kind of error happened all the time... - email = 'foo@bar.com' - appid = 42 - code = md5s('42' + email + '43' + 'aybabtu') - assert_invalid(appid, email, code, msg_contains="inverted") - -def test_paypal_transaction(): - # If the code looks like a paypal transaction, mention it in the error message. - email = 'foo@bar.com' - appid = 42 - code = '2A693827WX9676888' - assert_invalid(appid, code, email, 'Paypal transaction') diff --git a/hscommon/tests/util_test.py b/hscommon/tests/util_test.py index 4d9967fd..d9fd1d7f 100644 --- a/hscommon/tests/util_test.py +++ b/hscommon/tests/util_test.py @@ -11,7 +11,6 @@ from io import StringIO from pytest import raises from ..testutil import eq_ -from .. import io from ..path import Path from ..util import * @@ -210,39 +209,49 @@ class TestCase_modified_after: 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 io.exists(testpath) + assert not testpath.exists() def test_not_empty(self, tmpdir): testpath = Path(str(tmpdir)) - io.mkdir(testpath + 'foo') + testpath['foo'].mkdir() assert not delete_if_empty(testpath) - assert io.exists(testpath) + assert testpath.exists() def test_with_files_to_delete(self, tmpdir): testpath = Path(str(tmpdir)) - io.open(testpath + 'foo', 'w') - io.open(testpath + 'bar', 'w') + testpath['foo'].open('w') + testpath['bar'].open('w') assert delete_if_empty(testpath, ['foo', 'bar']) - assert not io.exists(testpath) + assert not testpath.exists() def test_directory_in_files_to_delete(self, tmpdir): testpath = Path(str(tmpdir)) - io.mkdir(testpath + 'foo') + testpath['foo'].mkdir() assert not delete_if_empty(testpath, ['foo']) - assert io.exists(testpath) + assert testpath.exists() def test_delete_files_to_delete_only_if_dir_is_empty(self, tmpdir): testpath = Path(str(tmpdir)) - io.open(testpath + 'foo', 'w') - io.open(testpath + 'bar', 'w') + testpath['foo'].open('w') + testpath['bar'].open('w') assert not delete_if_empty(testpath, ['foo']) - assert io.exists(testpath) - assert io.exists(testpath + 'foo') + assert testpath.exists() + assert testpath['foo'].exists() def test_doesnt_exist(self): # When the 'path' doesn't exist, just do nothing. @@ -251,7 +260,7 @@ class TestCase_delete_if_empty: def test_is_file(self, tmpdir): # When 'path' is a file, do nothing. p = Path(str(tmpdir)) + 'filename' - io.open(p, 'w').close() + p.open('w').close() delete_if_empty(p) # no crash def test_ioerror(self, tmpdir, monkeypatch): @@ -259,7 +268,7 @@ class TestCase_delete_if_empty: def do_raise(*args, **kw): raise OSError() - monkeypatch.setattr(io, 'rmdir', do_raise) + monkeypatch.setattr(Path, 'rmdir', do_raise) delete_if_empty(Path(str(tmpdir))) # no crash diff --git a/hscommon/util.py b/hscommon/util.py index 8d562d89..48011540 100644 --- a/hscommon/util.py +++ b/hscommon/util.py @@ -13,19 +13,21 @@ import re from math import ceil import glob import shutil +from datetime import timedelta -from . import io -from .path import Path +from .path import Path, pathify, log_io_error def nonone(value, replace_value): - ''' Returns value if value is not None. Returns replace_value otherwise. - ''' + """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): @@ -39,8 +41,10 @@ def minmax(value, min_value, max_value): #--- Sequence related def dedupe(iterable): - '''Returns a list of elements in iterable with all dupes removed. - ''' + """Returns a list of elements in ``iterable`` with all dupes removed. + + The order of the elements is preserved. + """ result = [] seen = {} for item in iterable: @@ -51,11 +55,11 @@ def dedupe(iterable): return result def flatten(iterables, start_with=None): - '''Takes a list of lists 'lists' and returns a list containing elements of every list. + """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. - ''' + 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) @@ -64,7 +68,7 @@ def flatten(iterables, start_with=None): return result def first(iterable): - """Returns the first item of 'iterable' + """Returns the first item of ``iterable``. """ try: return next(iter(iterable)) @@ -116,10 +120,13 @@ def trailiter(iterable, skipfirst=False): #--- 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.""" + """Returns the lowercase extension part of filename, without the dot. + """ pos = filename.rfind('.') if pos > -1: return filename[pos + 1:].lower() @@ -127,7 +134,8 @@ def get_file_ext(filename): return '' def rem_file_ext(filename): - """Returns the filename without extension.""" + """Returns the filename without extension. + """ pos = filename.rfind('.') if pos > -1: return filename[:pos] @@ -135,12 +143,13 @@ def rem_file_ext(filename): return filename def pluralize(number, word, decimals=0, plural_word=None): - """Returns a string with number in front of s, and adds a 's' to s if number > 1 + """Returns a pluralized string with ``number`` in front of ``word``. - 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 + 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 @@ -154,7 +163,7 @@ def pluralize(number, word, decimals=0, plural_word=None): 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. + If ``with_hours`` if false, the format is mm:ss. """ minus = seconds < 0 if minus: @@ -171,7 +180,8 @@ def format_time(seconds, with_hours=True): return r def format_time_decimal(seconds): - """Transforms seconds in a strings like '3.4 minutes'""" + """Transforms seconds in a strings like '3.4 minutes'. + """ minus = seconds < 0 if minus: seconds *= -1 @@ -193,11 +203,15 @@ 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. + ``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 @@ -234,16 +248,16 @@ def remove_invalid_xml(s, replace_with=' '): 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 basestring, all replace_from occurence will be replaced + ``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. + ``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] @@ -254,17 +268,31 @@ def multi_replace(s, replace_from, replace_to=''): 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 -def modified_after(first_path, second_path): - """Returns True if first_path's mtime is higher than second_path's mtime.""" +@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 = io.stat(first_path).st_mtime - except EnvironmentError: + first_mtime = first_path.stat().st_mtime + except (EnvironmentError, AttributeError): return False try: - second_mtime = io.stat(second_path).st_mtime - except EnvironmentError: + second_mtime = second_path.stat().st_mtime + except (EnvironmentError, AttributeError): return True return first_mtime > second_mtime @@ -281,28 +309,35 @@ def find_in_path(name, paths=None): return op.join(path, name) return None -@io.log_io_error -def delete_if_empty(path, files_to_delete=[]): - ''' Deletes the directory at 'path' if it is empty or if it only contains files_to_delete. - ''' - if not io.exists(path) or not io.isdir(path): +@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 = io.listdir(path) - if any(name for name in contents if (name not in files_to_delete) or io.isdir(path + name)): + contents = path.listdir() + if any(p for p in contents if (p.name not in files_to_delete) or p.isdir()): return False - for name in contents: - io.remove(path + name) - io.rmdir(path) + for p in contents: + p.remove() + path.rmdir() return True def open_if_filename(infile, mode='rb'): - """ - infile can be either a string or a file-like object. - if it is a string, a file will be opened with mode. - Returns a tuple (shouldbeclosed,infile) infile is a file object + """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 (io.open(infile, mode), True) + return (infile.open(mode), True) if isinstance(infile, str): return (open(infile, mode), True) else: @@ -334,6 +369,13 @@ def delete_files_with_pattern(folder_path, pattern, recursive=True): 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 diff --git a/locale/core.pot b/locale/core.pot index 15405da4..47f8979a 100644 --- a/locale/core.pot +++ b/locale/core.pot @@ -4,135 +4,127 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: utf-8\n" -#: core/app.py:40 +#: core/app.py:39 msgid "There are no marked duplicates. Nothing has been done." msgstr "" -#: core/app.py:41 +#: core/app.py:40 msgid "There are no selected duplicates. Nothing has been done." msgstr "" -#: core/app.py:42 +#: core/app.py:41 msgid "You're about to open many files at once. Depending on what those files are opened with, doing so can create quite a mess. Continue?" msgstr "" -#: core/app.py:58 +#: core/app.py:57 msgid "Scanning for duplicates" msgstr "" -#: core/app.py:59 +#: core/app.py:58 msgid "Loading" msgstr "" -#: core/app.py:60 +#: core/app.py:59 msgid "Moving" msgstr "" -#: core/app.py:61 +#: core/app.py:60 msgid "Copying" msgstr "" -#: core/app.py:62 +#: core/app.py:61 msgid "Sending to Trash" msgstr "" -#: core/app.py:65 +#: core/app.py:64 msgid "Sending files to the recycle bin" msgstr "" -#: core/app.py:110 -msgid "will only be able to delete, move or copy 10 duplicates at once" -msgstr "" - -#: core/app.py:246 +#: core/app.py:295 msgid "A previous action is still hanging in there. You can't start a new one yet. Wait a few seconds, then try again." msgstr "" -#: core/app.py:254 +#: core/app.py:302 msgid "No duplicates found." msgstr "" -#: core/app.py:267 +#: core/app.py:315 msgid "All marked files were copied successfully." msgstr "" -#: core/app.py:268 +#: core/app.py:316 msgid "All marked files were moved successfully." msgstr "" -#: core/app.py:269 +#: core/app.py:317 msgid "All marked files were successfully sent to Trash." msgstr "" -#: core/app.py:296 -msgid "You cannot delete, move or copy more than 10 duplicates at once in demo mode." -msgstr "" - -#: core/app.py:307 +#: core/app.py:354 msgid "'{}' already is in the list." msgstr "" -#: core/app.py:309 +#: core/app.py:356 msgid "'{}' does not exist." msgstr "" -#: core/app.py:316 +#: core/app.py:365 msgid "All selected %d matches are going to be ignored in all subsequent scans. Continue?" msgstr "" -#: core/app.py:376 +#: core/app.py:431 msgid "copy" msgstr "" -#: core/app.py:376 +#: core/app.py:431 msgid "move" msgstr "" -#: core/app.py:377 +#: core/app.py:432 msgid "Select a directory to {} marked files to" msgstr "" -#: core/app.py:403 +#: core/app.py:469 msgid "Select a destination for your exported CSV" msgstr "" -#: core/app.py:428 +#: core/app.py:494 msgid "You have no custom command set up. Set it up in your preferences." msgstr "" -#: core/app.py:535 core/app.py:546 +#: core/app.py:646 core/app.py:659 msgid "You are about to remove %d files from results. Continue?" msgstr "" -#: core/app.py:566 +#: core/app.py:693 msgid "{} duplicate groups were changed by the re-prioritization." msgstr "" -#: core/app.py:586 +#: core/app.py:721 msgid "Collecting files to scan" msgstr "" -#: core/app.py:597 +#: core/app.py:732 msgid "The selected directories contain no scannable file." msgstr "" -#: core/app.py:636 +#: core/app.py:773 msgid "%s (%d discarded)" msgstr "" -#: core/engine.py:178 core/engine.py:215 +#: core/engine.py:220 core/engine.py:265 msgid "0 matches found" msgstr "" -#: core/engine.py:196 core/engine.py:223 +#: core/engine.py:238 core/engine.py:273 msgid "%d matches found" msgstr "" -#: core/engine.py:208 core/scanner.py:79 +#: core/engine.py:258 core/scanner.py:79 msgid "Read size of %d/%d files" msgstr "" -#: core/engine.py:361 +#: core/engine.py:464 msgid "Grouped %d/%d matches" msgstr "" @@ -180,11 +172,11 @@ msgstr "" msgid "Oldest" msgstr "" -#: core/results.py:113 +#: core/results.py:126 msgid "%d / %d (%s / %s) duplicates marked." msgstr "" -#: core/results.py:120 +#: core/results.py:133 msgid " filter: %s" msgstr "" diff --git a/locale/cs/LC_MESSAGES/columns.po b/locale/cs/LC_MESSAGES/columns.po index 251b3a61..e507a454 100644 --- a/locale/cs/LC_MESSAGES/columns.po +++ b/locale/cs/LC_MESSAGES/columns.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: dupeGuru\n" -"PO-Revision-Date: 2012-09-05 15:23+0000\n" +"PO-Revision-Date: 2013-11-20 11:53+0000\n" "Last-Translator: hsoft \n" "Language-Team: Czech (http://www.transifex.com/projects/p/dupeguru/language/cs/)\n" "Content-Type: text/plain; charset=UTF-8\n" diff --git a/locale/cs/LC_MESSAGES/core.po b/locale/cs/LC_MESSAGES/core.po index 2f2a0368..e2c8a487 100644 --- a/locale/cs/LC_MESSAGES/core.po +++ b/locale/cs/LC_MESSAGES/core.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: dupeGuru\n" -"PO-Revision-Date: 2013-08-03 20:35+0000\n" +"PO-Revision-Date: 2013-12-07 15:22+0000\n" "Last-Translator: hsoft \n" "Language-Team: Czech (http://www.transifex.com/projects/p/dupeguru/language/cs/)\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -10,49 +10,45 @@ msgstr "" "Language: cs\n" "Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" -#: core/app.py:40 +#: core/app.py:39 msgid "There are no marked duplicates. Nothing has been done." msgstr "" -#: core/app.py:41 +#: core/app.py:40 msgid "There are no selected duplicates. Nothing has been done." msgstr "" -#: core/app.py:42 +#: core/app.py:41 msgid "" "You're about to open many files at once. Depending on what those files are " "opened with, doing so can create quite a mess. Continue?" msgstr "" -#: core/app.py:58 +#: core/app.py:57 msgid "Scanning for duplicates" msgstr "Vyhledávám duplicity" -#: core/app.py:59 +#: core/app.py:58 msgid "Loading" msgstr "Nahrávám" -#: core/app.py:60 +#: core/app.py:59 msgid "Moving" msgstr "Přesouvám" -#: core/app.py:61 +#: core/app.py:60 msgid "Copying" msgstr "Kopíruji" -#: core/app.py:62 +#: core/app.py:61 msgid "Sending to Trash" msgstr "Vyhazuji do koše" -#: core/app.py:65 +#: core/app.py:64 msgid "Sending files to the recycle bin" msgstr "" -#: core/app.py:110 -msgid "will only be able to delete, move or copy 10 duplicates at once" -msgstr "" - -#: core/app.py:246 +#: core/app.py:295 msgid "" "A previous action is still hanging in there. You can't start a new one yet. " "Wait a few seconds, then try again." @@ -60,37 +56,31 @@ msgstr "" "Předchozí akce stále nebyla ukončena. Novou zatím nemůžete spustit. Počkejte" " pár sekund a zkuste to znovu." -#: core/app.py:254 +#: core/app.py:302 msgid "No duplicates found." msgstr "Nebyli nalezeny žádné duplicity." -#: core/app.py:267 +#: core/app.py:315 msgid "All marked files were copied successfully." msgstr "" -#: core/app.py:268 +#: core/app.py:316 msgid "All marked files were moved successfully." msgstr "" -#: core/app.py:269 +#: core/app.py:317 msgid "All marked files were successfully sent to Trash." msgstr "" -#: core/app.py:296 -msgid "" -"You cannot delete, move or copy more than 10 duplicates at once in demo " -"mode." -msgstr "" - -#: core/app.py:307 +#: core/app.py:354 msgid "'{}' already is in the list." msgstr "" -#: core/app.py:309 +#: core/app.py:356 msgid "'{}' does not exist." msgstr "" -#: core/app.py:316 +#: core/app.py:365 msgid "" "All selected %d matches are going to be ignored in all subsequent scans. " "Continue?" @@ -98,60 +88,60 @@ msgstr "" "Všech %d vybraných shod bude v následujících hledáních ignorováno. " "Pokračovat?" -#: core/app.py:376 +#: core/app.py:431 msgid "copy" msgstr "" -#: core/app.py:376 +#: core/app.py:431 msgid "move" msgstr "" -#: core/app.py:377 +#: core/app.py:432 msgid "Select a directory to {} marked files to" msgstr "" -#: core/app.py:403 +#: core/app.py:469 msgid "Select a destination for your exported CSV" msgstr "" -#: core/app.py:428 +#: core/app.py:494 msgid "You have no custom command set up. Set it up in your preferences." msgstr "" "Nedefinoval jste žádný uživatelský příkaz. Nadefinujete ho v předvolbách." -#: core/app.py:535 core/app.py:546 +#: core/app.py:646 core/app.py:659 msgid "You are about to remove %d files from results. Continue?" msgstr "Chystáte se z výsledků odstranit %d souborů. Pokračovat?" -#: core/app.py:566 +#: core/app.py:693 msgid "{} duplicate groups were changed by the re-prioritization." msgstr "" -#: core/app.py:586 +#: core/app.py:721 msgid "Collecting files to scan" msgstr "Shromažďuji prohlížené soubory" -#: core/app.py:597 +#: core/app.py:732 msgid "The selected directories contain no scannable file." msgstr "Vybrané adresáře neobsahují žádné soubory vhodné k prohledávání." -#: core/app.py:636 +#: core/app.py:773 msgid "%s (%d discarded)" msgstr "%s (%d vyřazeno)" -#: core/engine.py:178 core/engine.py:215 +#: core/engine.py:220 core/engine.py:265 msgid "0 matches found" msgstr "Nalezeno 0 shod" -#: core/engine.py:196 core/engine.py:223 +#: core/engine.py:238 core/engine.py:273 msgid "%d matches found" msgstr "Nalezeno %d shod" -#: core/engine.py:208 core/scanner.py:79 +#: core/engine.py:258 core/scanner.py:79 msgid "Read size of %d/%d files" msgstr "Read size of %d/%d files" -#: core/engine.py:361 +#: core/engine.py:464 msgid "Grouped %d/%d matches" msgstr "Grouped %d/%d matches" @@ -199,11 +189,11 @@ msgstr "Nejnovější" msgid "Oldest" msgstr "Nejstarší" -#: core/results.py:113 +#: core/results.py:126 msgid "%d / %d (%s / %s) duplicates marked." msgstr "%d / %d (%s / %s) duplicit označeno." -#: core/results.py:120 +#: core/results.py:133 msgid " filter: %s" msgstr " filtr: %s" diff --git a/locale/cs/LC_MESSAGES/ui.po b/locale/cs/LC_MESSAGES/ui.po index 2517fbff..5f58a104 100644 --- a/locale/cs/LC_MESSAGES/ui.po +++ b/locale/cs/LC_MESSAGES/ui.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: dupeGuru\n" -"PO-Revision-Date: 2013-08-03 20:36+0000\n" +"PO-Revision-Date: 2013-12-07 15:22+0000\n" "Last-Translator: hsoft \n" "Language-Team: Czech (http://www.transifex.com/projects/p/dupeguru/language/cs/)\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -18,124 +18,122 @@ msgstr "Odstraňuji mrtvé stopy z Vaší knihovny iTunes" msgid "Scanning the iTunes Library" msgstr "Procházím knihovnu iTunes" -#: cocoa/inter/app_me.py:161 cocoa/inter/app_pe.py:186 +#: cocoa/inter/app_me.py:160 cocoa/inter/app_pe.py:194 msgid "Sending dupes to the Trash" msgstr "Vyhazuji kopie do koše" -#: cocoa/inter/app_me.py:163 +#: cocoa/inter/app_me.py:162 msgid "Talking to iTunes. Don't touch it!" msgstr "" -#: cocoa/inter/app_me.py:189 -msgid "" -"There were communication problems with iTunes. The operation couldn't be " -"completed." -msgstr "" - -#: cocoa/inter/app_me.py:195 +#: cocoa/inter/app_me.py:197 msgid "" "Your iTunes Library contains %d dead tracks ready to be removed. Continue?" msgstr "" "Your iTunes Library contains %d dead tracks ready to be removed. Continue?" -#: cocoa/inter/app_me.py:199 +#: cocoa/inter/app_me.py:201 msgid "You have no dead tracks in your iTunes Library" msgstr "You have no dead tracks in your iTunes Library" -#: cocoa/inter/app_me.py:217 +#: cocoa/inter/app_me.py:219 msgid "The iTunes application couldn't be found." msgstr "" -#: cocoa/inter/app_pe.py:188 +#: cocoa/inter/app_pe.py:196 msgid "Talking to iPhoto. Don't touch it!" msgstr "" -#: cocoa/inter/app_pe.py:197 +#: cocoa/inter/app_pe.py:205 msgid "Talking to Aperture. Don't touch it!" msgstr "" -#: cocoa/inter/app_pe.py:270 +#: cocoa/inter/app_pe.py:278 msgid "Deleted Aperture photos were sent to a project called \"dupeGuru Trash\"." msgstr "" -#: cocoa/inter/app_pe.py:296 +#: cocoa/inter/app_pe.py:304 msgid "The iPhoto application couldn't be found." msgstr "Nelze najít aplikaci iPhoto." -#: qt/base/app.py:95 +#: qt/base/app.py:83 msgid "Quit" msgstr "" -#: qt/base/app.py:96 qt/base/preferences_dialog.py:123 +#: qt/base/app.py:84 qt/base/preferences_dialog.py:123 msgid "Preferences" msgstr "" -#: qt/base/app.py:97 qt/base/ignore_list_dialog.py:32 +#: qt/base/app.py:85 qt/base/ignore_list_dialog.py:32 #: cocoa/base/en.lproj/Localizable.strings:0 msgid "Ignore List" msgstr "" -#: qt/base/app.py:98 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/app.py:86 cocoa/base/en.lproj/Localizable.strings:0 msgid "dupeGuru Help" msgstr "Nápověda dupeGuru" -#: qt/base/app.py:99 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/app.py:87 cocoa/base/en.lproj/Localizable.strings:0 msgid "About dupeGuru" msgstr "O aplikaci" -#: qt/base/app.py:100 +#: qt/base/app.py:88 msgid "Register dupeGuru" msgstr "" -#: qt/base/app.py:101 +#: qt/base/app.py:89 msgid "Check for Update" msgstr "" -#: qt/base/app.py:102 +#: qt/base/app.py:90 msgid "Open Debug Log" msgstr "" -#: qt/base/app.py:257 +#: qt/base/app.py:217 msgid "{} file (*.{})" msgstr "" -#: qt/base/deletion_options.py:30 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/deletion_options.py:29 cocoa/base/en.lproj/Localizable.strings:0 msgid "Deletion Options" msgstr "" -#: qt/base/deletion_options.py:35 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/deletion_options.py:34 cocoa/base/en.lproj/Localizable.strings:0 msgid "Link deleted files" msgstr "" -#: qt/base/deletion_options.py:37 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/deletion_options.py:36 cocoa/base/en.lproj/Localizable.strings:0 msgid "" "After having deleted a duplicate, place a link targeting the reference file " "to replace the deleted file." msgstr "" -#: qt/base/deletion_options.py:42 +#: qt/base/deletion_options.py:41 msgid "Hardlink" msgstr "" -#: qt/base/deletion_options.py:42 +#: qt/base/deletion_options.py:41 msgid "Symlink" msgstr "" -#: qt/base/deletion_options.py:48 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/deletion_options.py:46 +msgid " (unsupported)" +msgstr "" + +#: qt/base/deletion_options.py:47 cocoa/base/en.lproj/Localizable.strings:0 msgid "Directly delete files" msgstr "" -#: qt/base/deletion_options.py:50 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/deletion_options.py:49 cocoa/base/en.lproj/Localizable.strings:0 msgid "" "Instead of sending files to trash, delete them directly. This option is " "usually used as a workaround when the normal deletion method doesn't work." msgstr "" -#: qt/base/deletion_options.py:56 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/deletion_options.py:55 cocoa/base/en.lproj/Localizable.strings:0 msgid "Proceed" msgstr "" -#: qt/base/deletion_options.py:57 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/deletion_options.py:56 cocoa/base/en.lproj/Localizable.strings:0 msgid "Cancel" msgstr "Zrušit" @@ -556,16 +554,16 @@ msgstr "EXIF razítko" msgid "Match pictures of different dimensions" msgstr "Porovnávat snímky s různými rozměry" -#: qt/pe/result_window.py:19 qt/pe/result_window.py:24 +#: qt/pe/result_window.py:19 qt/pe/result_window.py:25 #: cocoa/base/en.lproj/Localizable.strings:0 msgid "Clear Picture Cache" msgstr "Vyčistit cache snímků" -#: qt/pe/result_window.py:25 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/pe/result_window.py:26 cocoa/base/en.lproj/Localizable.strings:0 msgid "Do you really want to remove all your cached picture analysis?" msgstr "Opravdu chcete odstranit veškeré uložené analýzy snímků?" -#: qt/pe/result_window.py:28 +#: qt/pe/result_window.py:29 msgid "Picture cache cleared." msgstr "Picture cache cleared." @@ -797,6 +795,10 @@ msgstr "" msgid "Reveal Selected in Finder" msgstr "Vybrané otevřít ve Finderu" +#: cocoa/base/en.lproj/Localizable.strings:0 +msgid "Select All" +msgstr "" + #: cocoa/base/en.lproj/Localizable.strings:0 msgid "Send Marked to Trash..." msgstr "Vyhodit označené do koše..." @@ -824,7 +826,3 @@ msgstr "Okno" #: cocoa/base/en.lproj/Localizable.strings:0 msgid "Zoom" msgstr "Zoom" - -#: qt/base/deletion_options.py:46 -msgid " (unsupported)" -msgstr "" diff --git a/locale/de/LC_MESSAGES/columns.po b/locale/de/LC_MESSAGES/columns.po index aa6c037a..9aa09baf 100644 --- a/locale/de/LC_MESSAGES/columns.po +++ b/locale/de/LC_MESSAGES/columns.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: dupeGuru\n" -"PO-Revision-Date: 2012-09-05 15:23+0000\n" +"PO-Revision-Date: 2013-11-20 11:53+0000\n" "Last-Translator: hsoft \n" "Language-Team: German (http://www.transifex.com/projects/p/dupeguru/language/de/)\n" "Content-Type: text/plain; charset=UTF-8\n" diff --git a/locale/de/LC_MESSAGES/core.po b/locale/de/LC_MESSAGES/core.po index 0b6303f2..04caa209 100644 --- a/locale/de/LC_MESSAGES/core.po +++ b/locale/de/LC_MESSAGES/core.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: dupeGuru\n" -"PO-Revision-Date: 2013-08-03 20:35+0000\n" +"PO-Revision-Date: 2013-12-07 15:22+0000\n" "Last-Translator: hsoft \n" "Language-Team: German (http://www.transifex.com/projects/p/dupeguru/language/de/)\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -10,49 +10,45 @@ msgstr "" "Language: de\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: core/app.py:40 +#: core/app.py:39 msgid "There are no marked duplicates. Nothing has been done." msgstr "" -#: core/app.py:41 +#: core/app.py:40 msgid "There are no selected duplicates. Nothing has been done." msgstr "" -#: core/app.py:42 +#: core/app.py:41 msgid "" "You're about to open many files at once. Depending on what those files are " "opened with, doing so can create quite a mess. Continue?" msgstr "" -#: core/app.py:58 +#: core/app.py:57 msgid "Scanning for duplicates" msgstr "Suche nach Duplikaten" -#: core/app.py:59 +#: core/app.py:58 msgid "Loading" msgstr "Laden" -#: core/app.py:60 +#: core/app.py:59 msgid "Moving" msgstr "Verschieben" -#: core/app.py:61 +#: core/app.py:60 msgid "Copying" msgstr "Kopieren" -#: core/app.py:62 +#: core/app.py:61 msgid "Sending to Trash" msgstr "Verschiebe in den Mülleimer" -#: core/app.py:65 +#: core/app.py:64 msgid "Sending files to the recycle bin" msgstr "Sende Dateien in den Mülleimer" -#: core/app.py:110 -msgid "will only be able to delete, move or copy 10 duplicates at once" -msgstr "" - -#: core/app.py:246 +#: core/app.py:295 msgid "" "A previous action is still hanging in there. You can't start a new one yet. " "Wait a few seconds, then try again." @@ -60,97 +56,91 @@ msgstr "" "Eine vorherige Aktion ist noch in der Bearbeitung. Sie können noch keine " "Neue starten. Warten Sie einige Sekunden und versuchen es erneut." -#: core/app.py:254 +#: core/app.py:302 msgid "No duplicates found." msgstr "Keine Duplikate gefunden." -#: core/app.py:267 +#: core/app.py:315 msgid "All marked files were copied successfully." msgstr "" -#: core/app.py:268 +#: core/app.py:316 msgid "All marked files were moved successfully." msgstr "" -#: core/app.py:269 +#: core/app.py:317 msgid "All marked files were successfully sent to Trash." msgstr "" -#: core/app.py:296 -msgid "" -"You cannot delete, move or copy more than 10 duplicates at once in demo " -"mode." -msgstr "" - -#: core/app.py:307 +#: core/app.py:354 msgid "'{}' already is in the list." msgstr "" -#: core/app.py:309 +#: core/app.py:356 msgid "'{}' does not exist." msgstr "" -#: core/app.py:316 +#: core/app.py:365 msgid "" "All selected %d matches are going to be ignored in all subsequent scans. " "Continue?" msgstr "%d Dateien werden in zukünftigen Scans ignoriert werden. Fortfahren?" -#: core/app.py:376 +#: core/app.py:431 msgid "copy" msgstr "kopieren" -#: core/app.py:376 +#: core/app.py:431 msgid "move" msgstr "verschieben" -#: core/app.py:377 +#: core/app.py:432 msgid "Select a directory to {} marked files to" msgstr "Wählen sie einen Ordner zum {} der ausgewählten Dateien" -#: core/app.py:403 +#: core/app.py:469 msgid "Select a destination for your exported CSV" msgstr "" -#: core/app.py:428 +#: core/app.py:494 msgid "You have no custom command set up. Set it up in your preferences." msgstr "" "Sie haben keinen eigenen Befehl erstellt. Bitte in den Einstellungen " "konfigurieren." -#: core/app.py:535 core/app.py:546 +#: core/app.py:646 core/app.py:659 msgid "You are about to remove %d files from results. Continue?" msgstr "%d Dateien werden aus der Ergebnisliste entfernt. Fortfahren?" -#: core/app.py:566 +#: core/app.py:693 msgid "{} duplicate groups were changed by the re-prioritization." msgstr "" -#: core/app.py:586 +#: core/app.py:721 msgid "Collecting files to scan" msgstr "Sammle Dateien zum Scannen" -#: core/app.py:597 +#: core/app.py:732 msgid "The selected directories contain no scannable file." msgstr "Der ausgewählte Ordner enthält keine scannbare Dateien." -#: core/app.py:636 +#: core/app.py:773 msgid "%s (%d discarded)" msgstr "%s (%d verworfen)" -#: core/engine.py:178 core/engine.py:215 +#: core/engine.py:220 core/engine.py:265 msgid "0 matches found" msgstr "0 Paare gefunden" -#: core/engine.py:196 core/engine.py:223 +#: core/engine.py:238 core/engine.py:273 msgid "%d matches found" msgstr "%d Paare gefunden" -#: core/engine.py:208 core/scanner.py:79 +#: core/engine.py:258 core/scanner.py:79 msgid "Read size of %d/%d files" msgstr "Lese Größe von %d/%d Dateien" -#: core/engine.py:361 +#: core/engine.py:464 msgid "Grouped %d/%d matches" msgstr "%d/%d Paare gruppiert" @@ -198,11 +188,11 @@ msgstr "Newest" msgid "Oldest" msgstr "Oldest" -#: core/results.py:113 +#: core/results.py:126 msgid "%d / %d (%s / %s) duplicates marked." msgstr "%d / %d (%s / %s) Duplikate markiert." -#: core/results.py:120 +#: core/results.py:133 msgid " filter: %s" msgstr " Filter: %s" diff --git a/locale/de/LC_MESSAGES/ui.po b/locale/de/LC_MESSAGES/ui.po index 97e76678..4858181b 100644 --- a/locale/de/LC_MESSAGES/ui.po +++ b/locale/de/LC_MESSAGES/ui.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: dupeGuru\n" -"PO-Revision-Date: 2013-08-03 20:36+0000\n" +"PO-Revision-Date: 2013-12-07 15:22+0000\n" "Last-Translator: hsoft \n" "Language-Team: German (http://www.transifex.com/projects/p/dupeguru/language/de/)\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -18,124 +18,122 @@ msgstr "Entferne tote Stücke aus Ihrer iTunes Bibliothek." msgid "Scanning the iTunes Library" msgstr "Scanne die iTunes Bibiliothek" -#: cocoa/inter/app_me.py:161 cocoa/inter/app_pe.py:186 +#: cocoa/inter/app_me.py:160 cocoa/inter/app_pe.py:194 msgid "Sending dupes to the Trash" msgstr "Verschiebe Duplikate in den Mülleimer" -#: cocoa/inter/app_me.py:163 +#: cocoa/inter/app_me.py:162 msgid "Talking to iTunes. Don't touch it!" msgstr "" -#: cocoa/inter/app_me.py:189 -msgid "" -"There were communication problems with iTunes. The operation couldn't be " -"completed." -msgstr "" - -#: cocoa/inter/app_me.py:195 +#: cocoa/inter/app_me.py:197 msgid "" "Your iTunes Library contains %d dead tracks ready to be removed. Continue?" msgstr "" "Your iTunes Library contains %d dead tracks ready to be removed. Continue?" -#: cocoa/inter/app_me.py:199 +#: cocoa/inter/app_me.py:201 msgid "You have no dead tracks in your iTunes Library" msgstr "You have no dead tracks in your iTunes Library" -#: cocoa/inter/app_me.py:217 +#: cocoa/inter/app_me.py:219 msgid "The iTunes application couldn't be found." msgstr "" -#: cocoa/inter/app_pe.py:188 +#: cocoa/inter/app_pe.py:196 msgid "Talking to iPhoto. Don't touch it!" msgstr "" -#: cocoa/inter/app_pe.py:197 +#: cocoa/inter/app_pe.py:205 msgid "Talking to Aperture. Don't touch it!" msgstr "" -#: cocoa/inter/app_pe.py:270 +#: cocoa/inter/app_pe.py:278 msgid "Deleted Aperture photos were sent to a project called \"dupeGuru Trash\"." msgstr "" -#: cocoa/inter/app_pe.py:296 +#: cocoa/inter/app_pe.py:304 msgid "The iPhoto application couldn't be found." msgstr "The iPhoto application couldn't be found." -#: qt/base/app.py:95 +#: qt/base/app.py:83 msgid "Quit" msgstr "Beenden" -#: qt/base/app.py:96 qt/base/preferences_dialog.py:123 +#: qt/base/app.py:84 qt/base/preferences_dialog.py:123 msgid "Preferences" msgstr "Einstellungen" -#: qt/base/app.py:97 qt/base/ignore_list_dialog.py:32 +#: qt/base/app.py:85 qt/base/ignore_list_dialog.py:32 #: cocoa/base/en.lproj/Localizable.strings:0 msgid "Ignore List" msgstr "" -#: qt/base/app.py:98 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/app.py:86 cocoa/base/en.lproj/Localizable.strings:0 msgid "dupeGuru Help" msgstr "dupeGuru Hilfe" -#: qt/base/app.py:99 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/app.py:87 cocoa/base/en.lproj/Localizable.strings:0 msgid "About dupeGuru" msgstr "Über dupeGuru" -#: qt/base/app.py:100 +#: qt/base/app.py:88 msgid "Register dupeGuru" msgstr "Registriere dupeGuru" -#: qt/base/app.py:101 +#: qt/base/app.py:89 msgid "Check for Update" msgstr "Auf Updates überprüfen" -#: qt/base/app.py:102 +#: qt/base/app.py:90 msgid "Open Debug Log" msgstr "Debug Log öffnen" -#: qt/base/app.py:257 +#: qt/base/app.py:217 msgid "{} file (*.{})" msgstr "" -#: qt/base/deletion_options.py:30 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/deletion_options.py:29 cocoa/base/en.lproj/Localizable.strings:0 msgid "Deletion Options" msgstr "" -#: qt/base/deletion_options.py:35 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/deletion_options.py:34 cocoa/base/en.lproj/Localizable.strings:0 msgid "Link deleted files" msgstr "" -#: qt/base/deletion_options.py:37 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/deletion_options.py:36 cocoa/base/en.lproj/Localizable.strings:0 msgid "" "After having deleted a duplicate, place a link targeting the reference file " "to replace the deleted file." msgstr "" -#: qt/base/deletion_options.py:42 +#: qt/base/deletion_options.py:41 msgid "Hardlink" msgstr "" -#: qt/base/deletion_options.py:42 +#: qt/base/deletion_options.py:41 msgid "Symlink" msgstr "" -#: qt/base/deletion_options.py:48 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/deletion_options.py:46 +msgid " (unsupported)" +msgstr "" + +#: qt/base/deletion_options.py:47 cocoa/base/en.lproj/Localizable.strings:0 msgid "Directly delete files" msgstr "" -#: qt/base/deletion_options.py:50 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/deletion_options.py:49 cocoa/base/en.lproj/Localizable.strings:0 msgid "" "Instead of sending files to trash, delete them directly. This option is " "usually used as a workaround when the normal deletion method doesn't work." msgstr "" -#: qt/base/deletion_options.py:56 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/deletion_options.py:55 cocoa/base/en.lproj/Localizable.strings:0 msgid "Proceed" msgstr "" -#: qt/base/deletion_options.py:57 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/deletion_options.py:56 cocoa/base/en.lproj/Localizable.strings:0 msgid "Cancel" msgstr "Cancel" @@ -555,17 +553,17 @@ msgstr "EXIF Timestamp" msgid "Match pictures of different dimensions" msgstr "Vergleiche Bilder mit unterschiedlicher Auflösung" -#: qt/pe/result_window.py:19 qt/pe/result_window.py:24 +#: qt/pe/result_window.py:19 qt/pe/result_window.py:25 #: cocoa/base/en.lproj/Localizable.strings:0 msgid "Clear Picture Cache" msgstr "Bildzwischenspeicher leeren" -#: qt/pe/result_window.py:25 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/pe/result_window.py:26 cocoa/base/en.lproj/Localizable.strings:0 msgid "Do you really want to remove all your cached picture analysis?" msgstr "" "Möchten Sie wirklich alle zwischengespeicherten Bildanalysen entfernen?" -#: qt/pe/result_window.py:28 +#: qt/pe/result_window.py:29 msgid "Picture cache cleared." msgstr "Bildzwischenspeicher geleert." @@ -797,6 +795,10 @@ msgstr "" msgid "Reveal Selected in Finder" msgstr "Reveal Selected in Finder" +#: cocoa/base/en.lproj/Localizable.strings:0 +msgid "Select All" +msgstr "" + #: cocoa/base/en.lproj/Localizable.strings:0 msgid "Send Marked to Trash..." msgstr "" @@ -824,7 +826,3 @@ msgstr "Fenster" #: cocoa/base/en.lproj/Localizable.strings:0 msgid "Zoom" msgstr "Zoomen" - -#: qt/base/deletion_options.py:46 -msgid " (unsupported)" -msgstr "" diff --git a/locale/fr/LC_MESSAGES/columns.po b/locale/fr/LC_MESSAGES/columns.po index 9a6ef137..3be184f1 100644 --- a/locale/fr/LC_MESSAGES/columns.po +++ b/locale/fr/LC_MESSAGES/columns.po @@ -4,7 +4,7 @@ msgid "" msgstr "" "Project-Id-Version: dupeGuru\n" -"PO-Revision-Date: 2013-04-27 14:40+0000\n" +"PO-Revision-Date: 2013-11-20 11:53+0000\n" "Last-Translator: hsoft \n" "Language-Team: French (http://www.transifex.com/projects/p/dupeguru/language/fr/)\n" "Content-Type: text/plain; charset=UTF-8\n" diff --git a/locale/fr/LC_MESSAGES/core.po b/locale/fr/LC_MESSAGES/core.po index 0207b1fe..de5dfe5e 100644 --- a/locale/fr/LC_MESSAGES/core.po +++ b/locale/fr/LC_MESSAGES/core.po @@ -4,7 +4,7 @@ msgid "" msgstr "" "Project-Id-Version: dupeGuru\n" -"PO-Revision-Date: 2013-08-03 20:35+0000\n" +"PO-Revision-Date: 2013-12-07 15:22+0000\n" "Last-Translator: hsoft \n" "Language-Team: French (http://www.transifex.com/projects/p/dupeguru/language/fr/)\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -12,15 +12,15 @@ msgstr "" "Language: fr\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" -#: core/app.py:40 +#: core/app.py:39 msgid "There are no marked duplicates. Nothing has been done." msgstr "Aucun doublon marqué. Rien à faire." -#: core/app.py:41 +#: core/app.py:40 msgid "There are no selected duplicates. Nothing has been done." msgstr "Aucun doublon sélectionné. Rien à faire." -#: core/app.py:42 +#: core/app.py:41 msgid "" "You're about to open many files at once. Depending on what those files are " "opened with, doing so can create quite a mess. Continue?" @@ -28,35 +28,31 @@ msgstr "" "Beaucoup de fichiers seront ouverts en même temps. Cela peut gravement " "encombrer votre système. Continuer?" -#: core/app.py:58 +#: core/app.py:57 msgid "Scanning for duplicates" msgstr "Scan de doublons en cours" -#: core/app.py:59 +#: core/app.py:58 msgid "Loading" msgstr "Chargement en cours" -#: core/app.py:60 +#: core/app.py:59 msgid "Moving" msgstr "Déplacement en cours" -#: core/app.py:61 +#: core/app.py:60 msgid "Copying" msgstr "Copie en cours" -#: core/app.py:62 +#: core/app.py:61 msgid "Sending to Trash" msgstr "Envoi de fichiers à la corbeille" -#: core/app.py:65 +#: core/app.py:64 msgid "Sending files to the recycle bin" msgstr "Envoi de fichiers à la corbeille" -#: core/app.py:110 -msgid "will only be able to delete, move or copy 10 duplicates at once" -msgstr "ne peut effacer, déplacer ou copier que 10 doublons à la fois" - -#: core/app.py:246 +#: core/app.py:295 msgid "" "A previous action is still hanging in there. You can't start a new one yet. " "Wait a few seconds, then try again." @@ -64,99 +60,91 @@ msgstr "" "Une action précédente est encore en cours. Attendez quelques secondes avant " "d'en repartir une nouvelle." -#: core/app.py:254 +#: core/app.py:302 msgid "No duplicates found." msgstr "Aucun doublon trouvé." -#: core/app.py:267 +#: core/app.py:315 msgid "All marked files were copied successfully." msgstr "Tous les fichiers marqués ont été copiés correctement." -#: core/app.py:268 +#: core/app.py:316 msgid "All marked files were moved successfully." msgstr "Tous les fichiers marqués ont été déplacés correctement." -#: core/app.py:269 +#: core/app.py:317 msgid "All marked files were successfully sent to Trash." msgstr "" "Tous les fichiers marqués ont été correctement envoyés à la corbeille." -#: core/app.py:296 -msgid "" -"You cannot delete, move or copy more than 10 duplicates at once in demo " -"mode." -msgstr "" -"Vous ne pouvez pas effacer, déplacer ou copier plus de 10 doublons à la fois" -" en mode démo." - -#: core/app.py:307 +#: core/app.py:354 msgid "'{}' already is in the list." msgstr "'{}' est déjà dans la liste." -#: core/app.py:309 +#: core/app.py:356 msgid "'{}' does not exist." msgstr "'{}' n'existe pas." -#: core/app.py:316 +#: core/app.py:365 msgid "" "All selected %d matches are going to be ignored in all subsequent scans. " "Continue?" msgstr "%d fichiers seront ignorés des prochains scans. Continuer?" -#: core/app.py:376 +#: core/app.py:431 msgid "copy" msgstr "copier" -#: core/app.py:376 +#: core/app.py:431 msgid "move" msgstr "déplacer" -#: core/app.py:377 +#: core/app.py:432 msgid "Select a directory to {} marked files to" msgstr "Sélectionnez un dossier vers lequel {} les fichiers marqués." -#: core/app.py:403 +#: core/app.py:469 msgid "Select a destination for your exported CSV" msgstr "Choisissez une destination pour votre exportation CSV" -#: core/app.py:428 +#: core/app.py:494 msgid "You have no custom command set up. Set it up in your preferences." msgstr "" "Vous n'avez pas de commande personnalisée. Ajoutez-la dans vos préférences." -#: core/app.py:535 core/app.py:546 +#: core/app.py:646 core/app.py:659 msgid "You are about to remove %d files from results. Continue?" msgstr "%d fichiers seront retirés des résultats. Continuer?" -#: core/app.py:566 +#: core/app.py:693 msgid "{} duplicate groups were changed by the re-prioritization." msgstr "{} groupes de doublons ont été modifiés par la re-prioritisation." -#: core/app.py:586 +#: core/app.py:721 msgid "Collecting files to scan" msgstr "Collecte des fichiers à scanner" -#: core/app.py:597 +#: core/app.py:732 msgid "The selected directories contain no scannable file." msgstr "Les dossiers sélectionnés ne contiennent pas de fichiers valides." -#: core/app.py:636 +#: core/app.py:773 msgid "%s (%d discarded)" msgstr "%s (%d hors-groupe)" -#: core/engine.py:178 core/engine.py:215 +#: core/engine.py:220 core/engine.py:265 msgid "0 matches found" msgstr "0 paires trouvées" -#: core/engine.py:196 core/engine.py:223 +#: core/engine.py:238 core/engine.py:273 msgid "%d matches found" msgstr "%d paires trouvées" -#: core/engine.py:208 core/scanner.py:79 +#: core/engine.py:258 core/scanner.py:79 msgid "Read size of %d/%d files" msgstr "Lu la taille de %d/%d fichiers" -#: core/engine.py:361 +#: core/engine.py:464 msgid "Grouped %d/%d matches" msgstr "%d/%d paires groupées" @@ -206,11 +194,11 @@ msgstr "Plus récent" msgid "Oldest" msgstr "Moins récent" -#: core/results.py:113 +#: core/results.py:126 msgid "%d / %d (%s / %s) duplicates marked." msgstr "%d / %d (%s / %s) doublons marqués." -#: core/results.py:120 +#: core/results.py:133 msgid " filter: %s" msgstr " filtre: %s" diff --git a/locale/fr/LC_MESSAGES/ui.po b/locale/fr/LC_MESSAGES/ui.po index 4046f755..658c2f1e 100644 --- a/locale/fr/LC_MESSAGES/ui.po +++ b/locale/fr/LC_MESSAGES/ui.po @@ -5,7 +5,7 @@ msgid "" msgstr "" "Project-Id-Version: dupeGuru\n" -"PO-Revision-Date: 2013-08-03 20:36+0000\n" +"PO-Revision-Date: 2013-12-07 15:22+0000\n" "Last-Translator: hsoft \n" "Language-Team: French (http://www.transifex.com/projects/p/dupeguru/language/fr/)\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -21,101 +21,93 @@ msgstr "Retrait des tracks mortes de votre librairie iTunes" msgid "Scanning the iTunes Library" msgstr "Scan de la librairie iTunes en cours" -#: cocoa/inter/app_me.py:161 cocoa/inter/app_pe.py:186 +#: cocoa/inter/app_me.py:160 cocoa/inter/app_pe.py:194 msgid "Sending dupes to the Trash" msgstr "Envoi de doublons à la corbeille en cours" -#: cocoa/inter/app_me.py:163 +#: cocoa/inter/app_me.py:162 msgid "Talking to iTunes. Don't touch it!" msgstr "En communication avec iTunes. N'y touchez pas!" -#: cocoa/inter/app_me.py:189 -msgid "" -"There were communication problems with iTunes. The operation couldn't be " -"completed." -msgstr "" -"Il y a eu des problèmes de communication avec iTunes. L'opération n'a pas pu" -" être complétée." - -#: cocoa/inter/app_me.py:195 +#: cocoa/inter/app_me.py:197 msgid "" "Your iTunes Library contains %d dead tracks ready to be removed. Continue?" msgstr "" "Votre librairie iTunes contient %d tracks mortes qui seront retirées. " "Continuer?" -#: cocoa/inter/app_me.py:199 +#: cocoa/inter/app_me.py:201 msgid "You have no dead tracks in your iTunes Library" msgstr "Votre librairie iTunes ne contient aucune track morte." -#: cocoa/inter/app_me.py:217 +#: cocoa/inter/app_me.py:219 msgid "The iTunes application couldn't be found." msgstr "L'application iTunes n'a pas pu être trouvée" -#: cocoa/inter/app_pe.py:188 +#: cocoa/inter/app_pe.py:196 msgid "Talking to iPhoto. Don't touch it!" msgstr "En communication avec iPhoto. N'y touchez pas!" -#: cocoa/inter/app_pe.py:197 +#: cocoa/inter/app_pe.py:205 msgid "Talking to Aperture. Don't touch it!" msgstr "En communication avec Aperture. N'y touchez pas!" -#: cocoa/inter/app_pe.py:270 +#: cocoa/inter/app_pe.py:278 msgid "Deleted Aperture photos were sent to a project called \"dupeGuru Trash\"." msgstr "" "Les photos supprimés d'Aperture sont dans le projet nommé \"dupeGuru " "Trash\"." -#: cocoa/inter/app_pe.py:296 +#: cocoa/inter/app_pe.py:304 msgid "The iPhoto application couldn't be found." msgstr "iPhoto n'a pas pu être trouvée dans vos applications." -#: qt/base/app.py:95 +#: qt/base/app.py:83 msgid "Quit" msgstr "Quitter" -#: qt/base/app.py:96 qt/base/preferences_dialog.py:123 +#: qt/base/app.py:84 qt/base/preferences_dialog.py:123 msgid "Preferences" msgstr "Préférences" -#: qt/base/app.py:97 qt/base/ignore_list_dialog.py:32 +#: qt/base/app.py:85 qt/base/ignore_list_dialog.py:32 #: cocoa/base/en.lproj/Localizable.strings:0 msgid "Ignore List" msgstr "Liste de doublons ignorés" -#: qt/base/app.py:98 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/app.py:86 cocoa/base/en.lproj/Localizable.strings:0 msgid "dupeGuru Help" msgstr "Aide dupeGuru" -#: qt/base/app.py:99 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/app.py:87 cocoa/base/en.lproj/Localizable.strings:0 msgid "About dupeGuru" msgstr "À propos de dupeGuru" -#: qt/base/app.py:100 +#: qt/base/app.py:88 msgid "Register dupeGuru" msgstr "Enregistrer dupeGuru" -#: qt/base/app.py:101 +#: qt/base/app.py:89 msgid "Check for Update" msgstr "Vérifier les mises à jour" -#: qt/base/app.py:102 +#: qt/base/app.py:90 msgid "Open Debug Log" msgstr "Ouvrir logs de déboguage" -#: qt/base/app.py:257 +#: qt/base/app.py:217 msgid "{} file (*.{})" msgstr "Fichier {} (*.{})" -#: qt/base/deletion_options.py:30 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/deletion_options.py:29 cocoa/base/en.lproj/Localizable.strings:0 msgid "Deletion Options" msgstr "Options de suppression" -#: qt/base/deletion_options.py:35 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/deletion_options.py:34 cocoa/base/en.lproj/Localizable.strings:0 msgid "Link deleted files" msgstr "Remplacer les fichiers effacés par des liens" -#: qt/base/deletion_options.py:37 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/deletion_options.py:36 cocoa/base/en.lproj/Localizable.strings:0 msgid "" "After having deleted a duplicate, place a link targeting the reference file " "to replace the deleted file." @@ -123,19 +115,23 @@ msgstr "" "Après avoir effacé un fichier, remplacer celui-ci par un lien vers le " "fichier référence." -#: qt/base/deletion_options.py:42 +#: qt/base/deletion_options.py:41 msgid "Hardlink" msgstr "Hardlink" -#: qt/base/deletion_options.py:42 +#: qt/base/deletion_options.py:41 msgid "Symlink" msgstr "Symlink" -#: qt/base/deletion_options.py:48 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/deletion_options.py:46 +msgid " (unsupported)" +msgstr "" + +#: qt/base/deletion_options.py:47 cocoa/base/en.lproj/Localizable.strings:0 msgid "Directly delete files" msgstr "Supprimer les fichiers directement" -#: qt/base/deletion_options.py:50 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/deletion_options.py:49 cocoa/base/en.lproj/Localizable.strings:0 msgid "" "Instead of sending files to trash, delete them directly. This option is " "usually used as a workaround when the normal deletion method doesn't work." @@ -143,11 +139,11 @@ msgstr "" "Au lieu de passer par la corbeille, supprimer directement. Cette option " "n'est généralement utilisée qu'en cas de problème." -#: qt/base/deletion_options.py:56 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/deletion_options.py:55 cocoa/base/en.lproj/Localizable.strings:0 msgid "Proceed" msgstr "Continuer" -#: qt/base/deletion_options.py:57 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/deletion_options.py:56 cocoa/base/en.lproj/Localizable.strings:0 msgid "Cancel" msgstr "Annuler" @@ -569,16 +565,16 @@ msgstr "EXIF Timestamp" msgid "Match pictures of different dimensions" msgstr "Comparer les images de tailles différentes" -#: qt/pe/result_window.py:19 qt/pe/result_window.py:24 +#: qt/pe/result_window.py:19 qt/pe/result_window.py:25 #: cocoa/base/en.lproj/Localizable.strings:0 msgid "Clear Picture Cache" msgstr "Vider la cache d'images" -#: qt/pe/result_window.py:25 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/pe/result_window.py:26 cocoa/base/en.lproj/Localizable.strings:0 msgid "Do you really want to remove all your cached picture analysis?" msgstr "Voulez-vous vraiment vider la cache de vos analyses précédentes?" -#: qt/pe/result_window.py:28 +#: qt/pe/result_window.py:29 msgid "Picture cache cleared." msgstr "La cache des analyses précédentes a été vidée." @@ -810,6 +806,10 @@ msgstr "Révéler" msgid "Reveal Selected in Finder" msgstr "Révéler sélectionné dans Finder" +#: cocoa/base/en.lproj/Localizable.strings:0 +msgid "Select All" +msgstr "" + #: cocoa/base/en.lproj/Localizable.strings:0 msgid "Send Marked to Trash..." msgstr "Envoyer marqués à la corbeille..." @@ -837,7 +837,3 @@ msgstr "Fenêtre" #: cocoa/base/en.lproj/Localizable.strings:0 msgid "Zoom" msgstr "Réduire/agrandir" - -#: qt/base/deletion_options.py:46 -msgid " (unsupported)" -msgstr "" diff --git a/locale/hy/LC_MESSAGES/columns.po b/locale/hy/LC_MESSAGES/columns.po index 94df390a..b3e76aed 100755 --- a/locale/hy/LC_MESSAGES/columns.po +++ b/locale/hy/LC_MESSAGES/columns.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: dupeGuru\n" -"PO-Revision-Date: 2012-09-05 15:22+0000\n" +"PO-Revision-Date: 2013-11-20 11:53+0000\n" "Last-Translator: hsoft \n" "Language-Team: Armenian (http://www.transifex.com/projects/p/dupeguru/language/hy/)\n" "Content-Type: text/plain; charset=UTF-8\n" diff --git a/locale/hy/LC_MESSAGES/core.po b/locale/hy/LC_MESSAGES/core.po index 1d5fffb8..4a0b085f 100755 --- a/locale/hy/LC_MESSAGES/core.po +++ b/locale/hy/LC_MESSAGES/core.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: dupeGuru\n" -"PO-Revision-Date: 2013-08-03 20:35+0000\n" +"PO-Revision-Date: 2013-12-07 15:22+0000\n" "Last-Translator: hsoft \n" "Language-Team: Armenian (http://www.transifex.com/projects/p/dupeguru/language/hy/)\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -10,50 +10,45 @@ msgstr "" "Language: hy\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: core/app.py:40 +#: core/app.py:39 msgid "There are no marked duplicates. Nothing has been done." msgstr "" -#: core/app.py:41 +#: core/app.py:40 msgid "There are no selected duplicates. Nothing has been done." msgstr "" -#: core/app.py:42 +#: core/app.py:41 msgid "" "You're about to open many files at once. Depending on what those files are " "opened with, doing so can create quite a mess. Continue?" msgstr "" -#: core/app.py:58 +#: core/app.py:57 msgid "Scanning for duplicates" msgstr "Ստուգվում են կրկնօրինակները" -#: core/app.py:59 +#: core/app.py:58 msgid "Loading" msgstr "Բացվում է" -#: core/app.py:60 +#: core/app.py:59 msgid "Moving" msgstr "Տեղափոխվում է" -#: core/app.py:61 +#: core/app.py:60 msgid "Copying" msgstr "Պատճենվում է" -#: core/app.py:62 +#: core/app.py:61 msgid "Sending to Trash" msgstr "Ուղարկվում է Աղբարկղ" -#: core/app.py:65 +#: core/app.py:64 msgid "Sending files to the recycle bin" msgstr "Ֆայլերը ուղարկվում են Աղբարկղ" -#: core/app.py:110 -msgid "will only be able to delete, move or copy 10 duplicates at once" -msgstr "" -"միաժամանակ հնարավոր է ջնջել, տեղափոխել կամ պատճենել միայն 10 օրինակներ" - -#: core/app.py:246 +#: core/app.py:295 msgid "" "A previous action is still hanging in there. You can't start a new one yet. " "Wait a few seconds, then try again." @@ -61,98 +56,90 @@ msgstr "" "Նախորդ գործողությունը դեռևս ձեռադրում է այստեղ: Չեք կարող սկսել մեկ ուրիշը: " "Սպասեք մի քանի վայրկյան և կրկին փորձեք:" -#: core/app.py:254 +#: core/app.py:302 msgid "No duplicates found." msgstr "Կրկնօրինակներ չկան:" -#: core/app.py:267 +#: core/app.py:315 msgid "All marked files were copied successfully." msgstr "Բոլոր նշված ֆայլերը հաջողությամբ պատճենվել են:" -#: core/app.py:268 +#: core/app.py:316 msgid "All marked files were moved successfully." msgstr "Բոլոր նշված ֆայլերը հաջողությամբ տեղափոխվել են:" -#: core/app.py:269 +#: core/app.py:317 msgid "All marked files were successfully sent to Trash." msgstr "Բոլոր նշված ֆայլերը հաջողությամբ Ջնջվել են:" -#: core/app.py:296 -msgid "" -"You cannot delete, move or copy more than 10 duplicates at once in demo " -"mode." -msgstr "" -"Չեք կարող ջնջել, տեղափձոխել կամ պատճենել ավելի քան 10 օրինակներ փորձնական " -"եղանակում:" - -#: core/app.py:307 +#: core/app.py:354 msgid "'{}' already is in the list." msgstr "'{}'-ը արդեն առկա է ցանկում:" -#: core/app.py:309 +#: core/app.py:356 msgid "'{}' does not exist." msgstr "'{}'-ը գոյություն չունի:" -#: core/app.py:316 +#: core/app.py:365 msgid "" "All selected %d matches are going to be ignored in all subsequent scans. " "Continue?" msgstr "" "Ընտրված %d համընկնումները կանտեսվեն հետագա բոլոր ստուգումներից: Շարունակե՞լ:" -#: core/app.py:376 +#: core/app.py:431 msgid "copy" msgstr "պատճենել" -#: core/app.py:376 +#: core/app.py:431 msgid "move" msgstr "տեղափոխել" -#: core/app.py:377 +#: core/app.py:432 msgid "Select a directory to {} marked files to" msgstr "Ընտրել թղթապանակ՝ {} նշված ֆայլերի համար" -#: core/app.py:403 +#: core/app.py:469 msgid "Select a destination for your exported CSV" msgstr "" -#: core/app.py:428 +#: core/app.py:494 msgid "You have no custom command set up. Set it up in your preferences." msgstr "Դուք չեք կատարել Հրամանի ընտրություն: Կատարեք այն կարգավորումներում:" -#: core/app.py:535 core/app.py:546 +#: core/app.py:646 core/app.py:659 msgid "You are about to remove %d files from results. Continue?" msgstr "Դուք պատրաստվում եք ջնջելու %d ֆայլեր: Շարունակե՞լ:" -#: core/app.py:566 +#: core/app.py:693 msgid "{} duplicate groups were changed by the re-prioritization." msgstr "" -#: core/app.py:586 +#: core/app.py:721 msgid "Collecting files to scan" msgstr "Հավաքվում են ֆայլեր՝ ստուգելու համար" -#: core/app.py:597 +#: core/app.py:732 msgid "The selected directories contain no scannable file." msgstr "Ընտրված թղթապանակները պարունակում են չստուգվող ֆայլ:" -#: core/app.py:636 +#: core/app.py:773 msgid "%s (%d discarded)" msgstr "%s (%d անպիտան)" -#: core/engine.py:178 core/engine.py:215 +#: core/engine.py:220 core/engine.py:265 msgid "0 matches found" msgstr "0 համընկնում է գտնվել" -#: core/engine.py:196 core/engine.py:223 +#: core/engine.py:238 core/engine.py:273 msgid "%d matches found" msgstr "%d համընկնում է գտնվել" -#: core/engine.py:208 core/scanner.py:79 +#: core/engine.py:258 core/scanner.py:79 msgid "Read size of %d/%d files" msgstr "Կարդալ %d/%d ֆայլերի չափը" -#: core/engine.py:361 +#: core/engine.py:464 msgid "Grouped %d/%d matches" msgstr "Խմբավորվել է %d/%d համընկնում" @@ -200,11 +187,11 @@ msgstr "Նորագույնը" msgid "Oldest" msgstr "Ամենահինը" -#: core/results.py:113 +#: core/results.py:126 msgid "%d / %d (%s / %s) duplicates marked." msgstr "%d / %d (%s / %s) նշված կրկնօրինակներ:" -#: core/results.py:120 +#: core/results.py:133 msgid " filter: %s" msgstr "ֆիլտր. %s" diff --git a/locale/hy/LC_MESSAGES/ui.po b/locale/hy/LC_MESSAGES/ui.po index 932ac43e..1b5d85b6 100755 --- a/locale/hy/LC_MESSAGES/ui.po +++ b/locale/hy/LC_MESSAGES/ui.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: dupeGuru\n" -"PO-Revision-Date: 2013-08-03 20:36+0000\n" +"PO-Revision-Date: 2013-12-07 15:22+0000\n" "Last-Translator: hsoft \n" "Language-Team: Armenian (http://www.transifex.com/projects/p/dupeguru/language/hy/)\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -18,125 +18,123 @@ msgstr "Հեռացվում են վնասված շավիղները iTunes-ի Շտ msgid "Scanning the iTunes Library" msgstr "Ստուգվում է iTunes-ի Շտեմարանը" -#: cocoa/inter/app_me.py:161 cocoa/inter/app_pe.py:186 +#: cocoa/inter/app_me.py:160 cocoa/inter/app_pe.py:194 msgid "Sending dupes to the Trash" msgstr "Խաբկանքները տեղափոխվում են Աղբարկղ" -#: cocoa/inter/app_me.py:163 +#: cocoa/inter/app_me.py:162 msgid "Talking to iTunes. Don't touch it!" msgstr "" -#: cocoa/inter/app_me.py:189 -msgid "" -"There were communication problems with iTunes. The operation couldn't be " -"completed." -msgstr "" - -#: cocoa/inter/app_me.py:195 +#: cocoa/inter/app_me.py:197 msgid "" "Your iTunes Library contains %d dead tracks ready to be removed. Continue?" msgstr "" "Ձեր iTunes- Շտեմարանը պարունակում է %d մահացած շավիղներ, որոնք կարող են " "ջնջվել: Շարունակե՞լ:" -#: cocoa/inter/app_me.py:199 +#: cocoa/inter/app_me.py:201 msgid "You have no dead tracks in your iTunes Library" msgstr "Դուք չունեք շավիղներ Ձեր iTunes Շտեմարանում" -#: cocoa/inter/app_me.py:217 +#: cocoa/inter/app_me.py:219 msgid "The iTunes application couldn't be found." msgstr "" -#: cocoa/inter/app_pe.py:188 +#: cocoa/inter/app_pe.py:196 msgid "Talking to iPhoto. Don't touch it!" msgstr "Զրույց iPhoto-ի հետ: Մի կպեք! " -#: cocoa/inter/app_pe.py:197 +#: cocoa/inter/app_pe.py:205 msgid "Talking to Aperture. Don't touch it!" msgstr "" -#: cocoa/inter/app_pe.py:270 +#: cocoa/inter/app_pe.py:278 msgid "Deleted Aperture photos were sent to a project called \"dupeGuru Trash\"." msgstr "" -#: cocoa/inter/app_pe.py:296 +#: cocoa/inter/app_pe.py:304 msgid "The iPhoto application couldn't be found." msgstr "iPhoto ծրագիրը չի գտնվել:" -#: qt/base/app.py:95 +#: qt/base/app.py:83 msgid "Quit" msgstr "Փակել" -#: qt/base/app.py:96 qt/base/preferences_dialog.py:123 +#: qt/base/app.py:84 qt/base/preferences_dialog.py:123 msgid "Preferences" msgstr "Կարգավորումներ" -#: qt/base/app.py:97 qt/base/ignore_list_dialog.py:32 +#: qt/base/app.py:85 qt/base/ignore_list_dialog.py:32 #: cocoa/base/en.lproj/Localizable.strings:0 msgid "Ignore List" msgstr "" -#: qt/base/app.py:98 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/app.py:86 cocoa/base/en.lproj/Localizable.strings:0 msgid "dupeGuru Help" msgstr "dupeGuru-ի Օգնությունը" -#: qt/base/app.py:99 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/app.py:87 cocoa/base/en.lproj/Localizable.strings:0 msgid "About dupeGuru" msgstr "dupeGuru-ի մասին" -#: qt/base/app.py:100 +#: qt/base/app.py:88 msgid "Register dupeGuru" msgstr "Գրանցել dupeGuru-ն" -#: qt/base/app.py:101 +#: qt/base/app.py:89 msgid "Check for Update" msgstr "Ստուգել թարմացումները" -#: qt/base/app.py:102 +#: qt/base/app.py:90 msgid "Open Debug Log" msgstr "Բացել Սխալների մատյանը" -#: qt/base/app.py:257 +#: qt/base/app.py:217 msgid "{} file (*.{})" msgstr "" -#: qt/base/deletion_options.py:30 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/deletion_options.py:29 cocoa/base/en.lproj/Localizable.strings:0 msgid "Deletion Options" msgstr "" -#: qt/base/deletion_options.py:35 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/deletion_options.py:34 cocoa/base/en.lproj/Localizable.strings:0 msgid "Link deleted files" msgstr "" -#: qt/base/deletion_options.py:37 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/deletion_options.py:36 cocoa/base/en.lproj/Localizable.strings:0 msgid "" "After having deleted a duplicate, place a link targeting the reference file " "to replace the deleted file." msgstr "" -#: qt/base/deletion_options.py:42 +#: qt/base/deletion_options.py:41 msgid "Hardlink" msgstr "" -#: qt/base/deletion_options.py:42 +#: qt/base/deletion_options.py:41 msgid "Symlink" msgstr "" -#: qt/base/deletion_options.py:48 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/deletion_options.py:46 +msgid " (unsupported)" +msgstr "" + +#: qt/base/deletion_options.py:47 cocoa/base/en.lproj/Localizable.strings:0 msgid "Directly delete files" msgstr "" -#: qt/base/deletion_options.py:50 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/deletion_options.py:49 cocoa/base/en.lproj/Localizable.strings:0 msgid "" "Instead of sending files to trash, delete them directly. This option is " "usually used as a workaround when the normal deletion method doesn't work." msgstr "" -#: qt/base/deletion_options.py:56 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/deletion_options.py:55 cocoa/base/en.lproj/Localizable.strings:0 msgid "Proceed" msgstr "" -#: qt/base/deletion_options.py:57 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/deletion_options.py:56 cocoa/base/en.lproj/Localizable.strings:0 msgid "Cancel" msgstr "Չեղարկել" @@ -556,16 +554,16 @@ msgstr "EXIF Timestamp" msgid "Match pictures of different dimensions" msgstr "Նկարների համընկնում տարբեր չափերով" -#: qt/pe/result_window.py:19 qt/pe/result_window.py:24 +#: qt/pe/result_window.py:19 qt/pe/result_window.py:25 #: cocoa/base/en.lproj/Localizable.strings:0 msgid "Clear Picture Cache" msgstr "Մաքրել նկարի պահոցը" -#: qt/pe/result_window.py:25 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/pe/result_window.py:26 cocoa/base/en.lproj/Localizable.strings:0 msgid "Do you really want to remove all your cached picture analysis?" msgstr "Ցանկանու՞մ եք հեռացնել բոլոր պահված նկարները ստուգելուց:" -#: qt/pe/result_window.py:28 +#: qt/pe/result_window.py:29 msgid "Picture cache cleared." msgstr "Նկարի պահոցը մաքրվել է:" @@ -797,6 +795,10 @@ msgstr "" msgid "Reveal Selected in Finder" msgstr "Ցուցադրել ընտրվածը Գտնվածում" +#: cocoa/base/en.lproj/Localizable.strings:0 +msgid "Select All" +msgstr "" + #: cocoa/base/en.lproj/Localizable.strings:0 msgid "Send Marked to Trash..." msgstr "Ուղարկել նշվածները Աղբարկղ..." @@ -824,7 +826,3 @@ msgstr "Պատուհանը" #: cocoa/base/en.lproj/Localizable.strings:0 msgid "Zoom" msgstr "Չափը" - -#: qt/base/deletion_options.py:46 -msgid " (unsupported)" -msgstr "" diff --git a/locale/it/LC_MESSAGES/columns.po b/locale/it/LC_MESSAGES/columns.po index f293aefb..f0bd72bc 100644 --- a/locale/it/LC_MESSAGES/columns.po +++ b/locale/it/LC_MESSAGES/columns.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: dupeGuru\n" -"PO-Revision-Date: 2012-09-05 15:23+0000\n" +"PO-Revision-Date: 2013-11-20 11:53+0000\n" "Last-Translator: hsoft \n" "Language-Team: Italian (http://www.transifex.com/projects/p/dupeguru/language/it/)\n" "Content-Type: text/plain; charset=UTF-8\n" diff --git a/locale/it/LC_MESSAGES/core.po b/locale/it/LC_MESSAGES/core.po index 65d31804..9d7dfbb4 100644 --- a/locale/it/LC_MESSAGES/core.po +++ b/locale/it/LC_MESSAGES/core.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: dupeGuru\n" -"PO-Revision-Date: 2013-08-03 20:35+0000\n" +"PO-Revision-Date: 2013-12-07 15:22+0000\n" "Last-Translator: hsoft \n" "Language-Team: Italian (http://www.transifex.com/projects/p/dupeguru/language/it/)\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -10,49 +10,45 @@ msgstr "" "Language: it\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: core/app.py:40 +#: core/app.py:39 msgid "There are no marked duplicates. Nothing has been done." msgstr "" -#: core/app.py:41 +#: core/app.py:40 msgid "There are no selected duplicates. Nothing has been done." msgstr "" -#: core/app.py:42 +#: core/app.py:41 msgid "" "You're about to open many files at once. Depending on what those files are " "opened with, doing so can create quite a mess. Continue?" msgstr "" -#: core/app.py:58 +#: core/app.py:57 msgid "Scanning for duplicates" msgstr "Scansione per i duplicati" -#: core/app.py:59 +#: core/app.py:58 msgid "Loading" msgstr "Caricamento" -#: core/app.py:60 +#: core/app.py:59 msgid "Moving" msgstr "Spostamento" -#: core/app.py:61 +#: core/app.py:60 msgid "Copying" msgstr "Copia in corso" -#: core/app.py:62 +#: core/app.py:61 msgid "Sending to Trash" msgstr "Spostamento nel cestino" -#: core/app.py:65 +#: core/app.py:64 msgid "Sending files to the recycle bin" msgstr "Spostamento nel cestino" -#: core/app.py:110 -msgid "will only be able to delete, move or copy 10 duplicates at once" -msgstr "" - -#: core/app.py:246 +#: core/app.py:295 msgid "" "A previous action is still hanging in there. You can't start a new one yet. " "Wait a few seconds, then try again." @@ -60,37 +56,31 @@ msgstr "" "Un'azione precedente è ancora in corso. Non puoi cominciarne una nuova. " "Aspetta qualche secondo e quindi riprova." -#: core/app.py:254 +#: core/app.py:302 msgid "No duplicates found." msgstr "Non sono stati trovati dei duplicati." -#: core/app.py:267 +#: core/app.py:315 msgid "All marked files were copied successfully." msgstr "Tutti i file marcati sono stati copiati correttamente." -#: core/app.py:268 +#: core/app.py:316 msgid "All marked files were moved successfully." msgstr "Tutti i file marcati sono stati spostati correttamente." -#: core/app.py:269 +#: core/app.py:317 msgid "All marked files were successfully sent to Trash." msgstr "Tutti i file marcati sono stati inviati nel cestino." -#: core/app.py:296 -msgid "" -"You cannot delete, move or copy more than 10 duplicates at once in demo " -"mode." -msgstr "" - -#: core/app.py:307 +#: core/app.py:354 msgid "'{}' already is in the list." msgstr "'{}' è già nella lista." -#: core/app.py:309 +#: core/app.py:356 msgid "'{}' does not exist." msgstr "'{}' non esiste." -#: core/app.py:316 +#: core/app.py:365 msgid "" "All selected %d matches are going to be ignored in all subsequent scans. " "Continue?" @@ -98,61 +88,61 @@ msgstr "" "Tutti i %d elementi che coincidono verranno ignorati in tutte le scansioni " "successive. Continuare?" -#: core/app.py:376 +#: core/app.py:431 msgid "copy" msgstr "" -#: core/app.py:376 +#: core/app.py:431 msgid "move" msgstr "" -#: core/app.py:377 +#: core/app.py:432 msgid "Select a directory to {} marked files to" msgstr "" -#: core/app.py:403 +#: core/app.py:469 msgid "Select a destination for your exported CSV" msgstr "" -#: core/app.py:428 +#: core/app.py:494 msgid "You have no custom command set up. Set it up in your preferences." msgstr "" "Non hai impostato nessun comando personalizzato. Impostalo nelle tue " "preferenze." -#: core/app.py:535 core/app.py:546 +#: core/app.py:646 core/app.py:659 msgid "You are about to remove %d files from results. Continue?" msgstr "Stai per rimuovere %d file dai risultati. Continuare?" -#: core/app.py:566 +#: core/app.py:693 msgid "{} duplicate groups were changed by the re-prioritization." msgstr "" -#: core/app.py:586 +#: core/app.py:721 msgid "Collecting files to scan" msgstr "Raccolta file da scansionare" -#: core/app.py:597 +#: core/app.py:732 msgid "The selected directories contain no scannable file." msgstr "Le cartelle selezionate non contengono file da scansionare." -#: core/app.py:636 +#: core/app.py:773 msgid "%s (%d discarded)" msgstr "%s (%d scartati)" -#: core/engine.py:178 core/engine.py:215 +#: core/engine.py:220 core/engine.py:265 msgid "0 matches found" msgstr "Nessun duplicato trovato" -#: core/engine.py:196 core/engine.py:223 +#: core/engine.py:238 core/engine.py:273 msgid "%d matches found" msgstr "Trovato/i %d duplicato/i" -#: core/engine.py:208 core/scanner.py:79 +#: core/engine.py:258 core/scanner.py:79 msgid "Read size of %d/%d files" msgstr "Lettura dimensione di %d/%d file" -#: core/engine.py:361 +#: core/engine.py:464 msgid "Grouped %d/%d matches" msgstr "Raggruppati %d/%d duplicati" @@ -202,11 +192,11 @@ msgstr "Il più nuovo" msgid "Oldest" msgstr "Il più vecchio" -#: core/results.py:113 +#: core/results.py:126 msgid "%d / %d (%s / %s) duplicates marked." msgstr "%d / %d (%s / %s) duplicati marcati." -#: core/results.py:120 +#: core/results.py:133 msgid " filter: %s" msgstr " filtro: %s" diff --git a/locale/it/LC_MESSAGES/ui.po b/locale/it/LC_MESSAGES/ui.po index 7f43e5e8..2b172875 100644 --- a/locale/it/LC_MESSAGES/ui.po +++ b/locale/it/LC_MESSAGES/ui.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: dupeGuru\n" -"PO-Revision-Date: 2013-08-03 20:36+0000\n" +"PO-Revision-Date: 2013-12-07 15:22+0000\n" "Last-Translator: hsoft \n" "Language-Team: Italian (http://www.transifex.com/projects/p/dupeguru/language/it/)\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -18,125 +18,123 @@ msgstr "Rimozione delle tracce insistenti dalla libreria di iTunes" msgid "Scanning the iTunes Library" msgstr "Scansione della libreria di iTunes" -#: cocoa/inter/app_me.py:161 cocoa/inter/app_pe.py:186 +#: cocoa/inter/app_me.py:160 cocoa/inter/app_pe.py:194 msgid "Sending dupes to the Trash" msgstr "Spostamento dei duplicati nel cestino" -#: cocoa/inter/app_me.py:163 +#: cocoa/inter/app_me.py:162 msgid "Talking to iTunes. Don't touch it!" msgstr "" -#: cocoa/inter/app_me.py:189 -msgid "" -"There were communication problems with iTunes. The operation couldn't be " -"completed." -msgstr "" - -#: cocoa/inter/app_me.py:195 +#: cocoa/inter/app_me.py:197 msgid "" "Your iTunes Library contains %d dead tracks ready to be removed. Continue?" msgstr "" "La tua libreria di iTunes contiene %d tracce inesistenti pronte per essere " "rimosse. Continuare?" -#: cocoa/inter/app_me.py:199 +#: cocoa/inter/app_me.py:201 msgid "You have no dead tracks in your iTunes Library" msgstr "La tua libreria di iTunes non contiene tracce inesistenti" -#: cocoa/inter/app_me.py:217 +#: cocoa/inter/app_me.py:219 msgid "The iTunes application couldn't be found." msgstr "" -#: cocoa/inter/app_pe.py:188 +#: cocoa/inter/app_pe.py:196 msgid "Talking to iPhoto. Don't touch it!" msgstr "" -#: cocoa/inter/app_pe.py:197 +#: cocoa/inter/app_pe.py:205 msgid "Talking to Aperture. Don't touch it!" msgstr "" -#: cocoa/inter/app_pe.py:270 +#: cocoa/inter/app_pe.py:278 msgid "Deleted Aperture photos were sent to a project called \"dupeGuru Trash\"." msgstr "" -#: cocoa/inter/app_pe.py:296 +#: cocoa/inter/app_pe.py:304 msgid "The iPhoto application couldn't be found." msgstr "Non trovo l'applicazione iPhoto." -#: qt/base/app.py:95 +#: qt/base/app.py:83 msgid "Quit" msgstr "" -#: qt/base/app.py:96 qt/base/preferences_dialog.py:123 +#: qt/base/app.py:84 qt/base/preferences_dialog.py:123 msgid "Preferences" msgstr "" -#: qt/base/app.py:97 qt/base/ignore_list_dialog.py:32 +#: qt/base/app.py:85 qt/base/ignore_list_dialog.py:32 #: cocoa/base/en.lproj/Localizable.strings:0 msgid "Ignore List" msgstr "" -#: qt/base/app.py:98 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/app.py:86 cocoa/base/en.lproj/Localizable.strings:0 msgid "dupeGuru Help" msgstr "Aiuto di dupeGuru" -#: qt/base/app.py:99 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/app.py:87 cocoa/base/en.lproj/Localizable.strings:0 msgid "About dupeGuru" msgstr "Informazioni su dupeGuru" -#: qt/base/app.py:100 +#: qt/base/app.py:88 msgid "Register dupeGuru" msgstr "" -#: qt/base/app.py:101 +#: qt/base/app.py:89 msgid "Check for Update" msgstr "" -#: qt/base/app.py:102 +#: qt/base/app.py:90 msgid "Open Debug Log" msgstr "" -#: qt/base/app.py:257 +#: qt/base/app.py:217 msgid "{} file (*.{})" msgstr "" -#: qt/base/deletion_options.py:30 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/deletion_options.py:29 cocoa/base/en.lproj/Localizable.strings:0 msgid "Deletion Options" msgstr "" -#: qt/base/deletion_options.py:35 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/deletion_options.py:34 cocoa/base/en.lproj/Localizable.strings:0 msgid "Link deleted files" msgstr "" -#: qt/base/deletion_options.py:37 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/deletion_options.py:36 cocoa/base/en.lproj/Localizable.strings:0 msgid "" "After having deleted a duplicate, place a link targeting the reference file " "to replace the deleted file." msgstr "" -#: qt/base/deletion_options.py:42 +#: qt/base/deletion_options.py:41 msgid "Hardlink" msgstr "" -#: qt/base/deletion_options.py:42 +#: qt/base/deletion_options.py:41 msgid "Symlink" msgstr "" -#: qt/base/deletion_options.py:48 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/deletion_options.py:46 +msgid " (unsupported)" +msgstr "" + +#: qt/base/deletion_options.py:47 cocoa/base/en.lproj/Localizable.strings:0 msgid "Directly delete files" msgstr "" -#: qt/base/deletion_options.py:50 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/deletion_options.py:49 cocoa/base/en.lproj/Localizable.strings:0 msgid "" "Instead of sending files to trash, delete them directly. This option is " "usually used as a workaround when the normal deletion method doesn't work." msgstr "" -#: qt/base/deletion_options.py:56 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/deletion_options.py:55 cocoa/base/en.lproj/Localizable.strings:0 msgid "Proceed" msgstr "" -#: qt/base/deletion_options.py:57 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/deletion_options.py:56 cocoa/base/en.lproj/Localizable.strings:0 msgid "Cancel" msgstr "Cancella" @@ -560,18 +558,18 @@ msgstr "Data e ora EXIF" msgid "Match pictures of different dimensions" msgstr "Includi immagini di dimensione differente" -#: qt/pe/result_window.py:19 qt/pe/result_window.py:24 +#: qt/pe/result_window.py:19 qt/pe/result_window.py:25 #: cocoa/base/en.lproj/Localizable.strings:0 msgid "Clear Picture Cache" msgstr "Cancella la cache delle immagini" -#: qt/pe/result_window.py:25 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/pe/result_window.py:26 cocoa/base/en.lproj/Localizable.strings:0 msgid "Do you really want to remove all your cached picture analysis?" msgstr "" "Vuoi veramente rimuovere tutte le analisi delle immagini memorizzate nella " "cache?" -#: qt/pe/result_window.py:28 +#: qt/pe/result_window.py:29 msgid "Picture cache cleared." msgstr "" @@ -804,6 +802,10 @@ msgstr "" msgid "Reveal Selected in Finder" msgstr "Mostra gli elementi selezionati nel Finder" +#: cocoa/base/en.lproj/Localizable.strings:0 +msgid "Select All" +msgstr "" + #: cocoa/base/en.lproj/Localizable.strings:0 msgid "Send Marked to Trash..." msgstr "Sposta gli elementi marcati nel cestino..." @@ -831,7 +833,3 @@ msgstr "Finestra" #: cocoa/base/en.lproj/Localizable.strings:0 msgid "Zoom" msgstr "Zoom" - -#: qt/base/deletion_options.py:46 -msgid " (unsupported)" -msgstr "" diff --git a/locale/pt_BR/LC_MESSAGES/columns.po b/locale/pt_BR/LC_MESSAGES/columns.po index 4ce1ca1b..ad363015 100644 --- a/locale/pt_BR/LC_MESSAGES/columns.po +++ b/locale/pt_BR/LC_MESSAGES/columns.po @@ -5,7 +5,7 @@ msgid "" msgstr "" "Project-Id-Version: dupeGuru\n" -"PO-Revision-Date: 2013-04-28 18:52+0000\n" +"PO-Revision-Date: 2013-11-20 11:53+0000\n" "Last-Translator: Vitu \n" "Language-Team: Portuguese (Brazil) (http://www.transifex.com/projects/p/dupeguru/language/pt_BR/)\n" "Content-Type: text/plain; charset=UTF-8\n" diff --git a/locale/pt_BR/LC_MESSAGES/core.po b/locale/pt_BR/LC_MESSAGES/core.po index 242d30a3..45122271 100644 --- a/locale/pt_BR/LC_MESSAGES/core.po +++ b/locale/pt_BR/LC_MESSAGES/core.po @@ -6,160 +6,148 @@ msgid "" msgstr "" "Project-Id-Version: dupeGuru\n" -"PO-Revision-Date: 2013-08-03 20:43+0000\n" -"Last-Translator: Vitu \n" +"PO-Revision-Date: 2013-12-07 15:22+0000\n" +"Last-Translator: hsoft \n" "Language-Team: Portuguese (Brazil) (http://www.transifex.com/projects/p/dupeguru/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" -#: core/app.py:40 +#: core/app.py:39 msgid "There are no marked duplicates. Nothing has been done." msgstr "Não há duplicatas marcadas. Nada foi feito." -#: core/app.py:41 +#: core/app.py:40 msgid "There are no selected duplicates. Nothing has been done." msgstr "Não há duplicatas selecionadas. Nada foi feito." -#: core/app.py:42 +#: core/app.py:41 msgid "" "You're about to open many files at once. Depending on what those files are " "opened with, doing so can create quite a mess. Continue?" msgstr "" -"Você pretende abrir muitos arquivos de uma vez. Problemas podem surgir " -"dependendo de qual app seja usado para abri-los. Continuar?" +"Você está prestes a abrir muitos arquivos de uma vez. Problemas podem surgir" +" dependendo de qual app seja usado para abri-los. Deseja continuar?" -#: core/app.py:58 +#: core/app.py:57 msgid "Scanning for duplicates" msgstr "Buscando por duplicatas" -#: core/app.py:59 +#: core/app.py:58 msgid "Loading" msgstr "Carregando" -#: core/app.py:60 +#: core/app.py:59 msgid "Moving" msgstr "Movendo" -#: core/app.py:61 +#: core/app.py:60 msgid "Copying" msgstr "Copiando" -#: core/app.py:62 +#: core/app.py:61 msgid "Sending to Trash" msgstr "Movendo para o Lixo" -#: core/app.py:65 +#: core/app.py:64 msgid "Sending files to the recycle bin" msgstr "Movendo arquivos para o Lixo" -#: core/app.py:110 -msgid "will only be able to delete, move or copy 10 duplicates at once" -msgstr "poderá apagar, mover ou copiar somente 10 duplicatas por vez" - -#: core/app.py:246 +#: core/app.py:295 msgid "" "A previous action is still hanging in there. You can't start a new one yet. " "Wait a few seconds, then try again." msgstr "" -"Ainda há uma ação em execução. Não é possível iniciar outra agora. Espere " +"Ainda há uma ação em andamento. Não é possível iniciar outra agora. Espere " "alguns segundos e tente novamente." -#: core/app.py:254 +#: core/app.py:302 msgid "No duplicates found." msgstr "Nenhuma duplicata encontrada." -#: core/app.py:267 +#: core/app.py:315 msgid "All marked files were copied successfully." -msgstr "Todos os arquivos marcados foram copiados com sucesso." - -#: core/app.py:268 -msgid "All marked files were moved successfully." -msgstr "Todos os arquivos marcados foram relocados com sucesso." - -#: core/app.py:269 -msgid "All marked files were successfully sent to Trash." -msgstr "Todos os arquivos marcados foram movidos para o Lixo com sucesso." - -#: core/app.py:296 -msgid "" -"You cannot delete, move or copy more than 10 duplicates at once in demo " -"mode." -msgstr "" -"Em modo demo, você não pode apagar, mover ou copiar mais do que 10 " -"duplicatas por vez." - -#: core/app.py:307 -msgid "'{}' already is in the list." -msgstr "'{}' já está na lista." - -#: core/app.py:309 -msgid "'{}' does not exist." -msgstr "'{}' não existe." +msgstr "Todos os arquivos marcados foram copiados corretamente." #: core/app.py:316 +msgid "All marked files were moved successfully." +msgstr "Todos os arquivos marcados foram relocados corretamente." + +#: core/app.py:317 +msgid "All marked files were successfully sent to Trash." +msgstr "Todos os arquivos marcados foram movidos para o Lixo corretamente." + +#: core/app.py:354 +msgid "'{}' already is in the list." +msgstr "‘{}’ já está na lista." + +#: core/app.py:356 +msgid "'{}' does not exist." +msgstr "‘{}’ não existe." + +#: core/app.py:365 msgid "" "All selected %d matches are going to be ignored in all subsequent scans. " "Continue?" msgstr "Excluir %d duplicata(s) selecionada(s) de escaneamentos posteriores?" -#: core/app.py:376 +#: core/app.py:431 msgid "copy" msgstr "copiar" -#: core/app.py:376 +#: core/app.py:431 msgid "move" msgstr "mover" -#: core/app.py:377 +#: core/app.py:432 msgid "Select a directory to {} marked files to" msgstr "Selecione uma pasta para {} os arquivos marcados" -#: core/app.py:403 +#: core/app.py:469 msgid "Select a destination for your exported CSV" msgstr "Selecione uma pasta para o CSV exportado" -#: core/app.py:428 +#: core/app.py:494 msgid "You have no custom command set up. Set it up in your preferences." msgstr "" "Você não possui nenhum comando personalizado. Crie um nas preferências." -#: core/app.py:535 core/app.py:546 +#: core/app.py:646 core/app.py:659 msgid "You are about to remove %d files from results. Continue?" msgstr "Remover %d arquivo(s) dos resultados?" -#: core/app.py:566 +#: core/app.py:693 msgid "{} duplicate groups were changed by the re-prioritization." -msgstr "{} grupos de duplicatas alterados ao re-priorizar." +msgstr "{} grupos de duplicatas alterados ao repriorizar." -#: core/app.py:586 +#: core/app.py:721 msgid "Collecting files to scan" msgstr "Juntando arquivos para escanear" -#: core/app.py:597 +#: core/app.py:732 msgid "The selected directories contain no scannable file." msgstr "As pastas selecionadas não contém arquivos escaneáveis." -#: core/app.py:636 +#: core/app.py:773 msgid "%s (%d discarded)" msgstr "%s (%d rejeitado(s))" -#: core/engine.py:178 core/engine.py:215 +#: core/engine.py:220 core/engine.py:265 msgid "0 matches found" -msgstr "0 coincidentes encontrados" +msgstr "0 resultados encontrados" -#: core/engine.py:196 core/engine.py:223 +#: core/engine.py:238 core/engine.py:273 msgid "%d matches found" -msgstr "%d coincidentes encontrados" +msgstr "%d resultados encontrados" -#: core/engine.py:208 core/scanner.py:79 +#: core/engine.py:258 core/scanner.py:79 msgid "Read size of %d/%d files" -msgstr "Tamanho de leitura de %d/%d arquivos" +msgstr "Tamanho lido em %d/%d arquivos" -#: core/engine.py:361 +#: core/engine.py:464 msgid "Grouped %d/%d matches" -msgstr "%d/%d coincidentes agrupados" +msgstr "%d/%d resultados agrupados" #: core/gui/deletion_options.py:23 msgid "You are sending {} file(s) to the Trash." @@ -167,7 +155,7 @@ msgstr "Você está movendo {} arquivo(s) para o Lixo." #: core/gui/ignore_list_dialog.py:24 msgid "Do you really want to remove all %d items from the ignore list?" -msgstr "Deseja remover todos os %d itens da lista Ignorar?" +msgstr "Tem certeza de que deseja remover todos os %d itens da lista Ignorar?" #: core/prioritize.py:68 msgid "None" @@ -205,11 +193,11 @@ msgstr "Mais recente" msgid "Oldest" msgstr "Mais antigo" -#: core/results.py:113 +#: core/results.py:126 msgid "%d / %d (%s / %s) duplicates marked." msgstr "%d / %d (%s / %s) duplicatas marcadas." -#: core/results.py:120 +#: core/results.py:133 msgid " filter: %s" msgstr " filtro: %s" @@ -219,11 +207,11 @@ msgstr "Metadados lidos em %d/%d arquivos" #: core/scanner.py:130 msgid "Removing false matches" -msgstr "Removendo coincidentes falsos" +msgstr "Removendo resultados falsos" #: core/scanner.py:154 msgid "Processed %d/%d matches against the ignore list" -msgstr "%d/%d coincidentes processados em oposição à lista Ignorar" +msgstr "%d/%d resultados processados em oposição à lista Ignorar" #: core/scanner.py:176 msgid "Doing group prioritization" @@ -235,16 +223,16 @@ msgstr "%d/%d fotos analizadas" #: core_pe/matchblock.py:153 msgid "Performed %d/%d chunk matches" -msgstr "%d/%d coincidentes em blocos executados" +msgstr "%d/%d resultados em blocos executados" #: core_pe/matchblock.py:158 msgid "Preparing for matching" -msgstr "Preparando para coincidentes" +msgstr "Preparando para comparação" #: core_pe/matchblock.py:193 msgid "Verified %d/%d matches" -msgstr "%d/%d coincidentes verificados" +msgstr "%d/%d resultados verificados" #: core_pe/matchexif.py:18 msgid "Read EXIF of %d/%d pictures" -msgstr "EXIF de %d/%d fotos lidos" +msgstr "EXIF lido em %d/%d fotos" diff --git a/locale/pt_BR/LC_MESSAGES/ui.po b/locale/pt_BR/LC_MESSAGES/ui.po index 100251be..e310f720 100644 --- a/locale/pt_BR/LC_MESSAGES/ui.po +++ b/locale/pt_BR/LC_MESSAGES/ui.po @@ -6,8 +6,8 @@ msgid "" msgstr "" "Project-Id-Version: dupeGuru\n" -"PO-Revision-Date: 2013-08-03 20:44+0000\n" -"Last-Translator: Vitu \n" +"PO-Revision-Date: 2013-12-07 15:22+0000\n" +"Last-Translator: hsoft \n" "Language-Team: Portuguese (Brazil) (http://www.transifex.com/projects/p/dupeguru/language/pt_BR/)\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: utf-8\n" @@ -22,99 +22,91 @@ msgstr "Removendo faixas sem referência da sua Biblioteca do iTunes" msgid "Scanning the iTunes Library" msgstr "Escaneando Biblioteca do iTunes" -#: cocoa/inter/app_me.py:161 cocoa/inter/app_pe.py:186 +#: cocoa/inter/app_me.py:160 cocoa/inter/app_pe.py:194 msgid "Sending dupes to the Trash" msgstr "Movendo duplicatas para o Lixo" -#: cocoa/inter/app_me.py:163 +#: cocoa/inter/app_me.py:162 msgid "Talking to iTunes. Don't touch it!" msgstr "Comunicando com o iTunes. Não mexa!" -#: cocoa/inter/app_me.py:189 -msgid "" -"There were communication problems with iTunes. The operation couldn't be " -"completed." -msgstr "" -"Ocorreu um erro de comunicação com o iTunes. A operação não pôde ser " -"finalizada." - -#: cocoa/inter/app_me.py:195 +#: cocoa/inter/app_me.py:197 msgid "" "Your iTunes Library contains %d dead tracks ready to be removed. Continue?" msgstr "Remover %d faixas sem referência da Biblioteca do iTunes?" -#: cocoa/inter/app_me.py:199 +#: cocoa/inter/app_me.py:201 msgid "You have no dead tracks in your iTunes Library" msgstr "Você não possui nenhuma faixa sem referência na Biblioteca do iTunes" -#: cocoa/inter/app_me.py:217 +#: cocoa/inter/app_me.py:219 msgid "The iTunes application couldn't be found." msgstr "O aplicativo iTunes não foi encontrado." -#: cocoa/inter/app_pe.py:188 +#: cocoa/inter/app_pe.py:196 msgid "Talking to iPhoto. Don't touch it!" msgstr "Comunicando com o iPhoto. Não mexa!" -#: cocoa/inter/app_pe.py:197 +#: cocoa/inter/app_pe.py:205 msgid "Talking to Aperture. Don't touch it!" msgstr "Comunicando com o Aperture. Não mexa!" -#: cocoa/inter/app_pe.py:270 +#: cocoa/inter/app_pe.py:278 msgid "Deleted Aperture photos were sent to a project called \"dupeGuru Trash\"." msgstr "" "As fotos apagadas do Aperture foram movidas para o projeto \"dupeGuru " "Trash\"." -#: cocoa/inter/app_pe.py:296 +#: cocoa/inter/app_pe.py:304 msgid "The iPhoto application couldn't be found." msgstr "O aplicativo iPhoto não foi encontrado." -#: qt/base/app.py:95 +#: qt/base/app.py:83 msgid "Quit" msgstr "Encerrar" -#: qt/base/app.py:96 qt/base/preferences_dialog.py:123 +#: qt/base/app.py:84 qt/base/preferences_dialog.py:123 msgid "Preferences" msgstr "Preferências" -#: qt/base/app.py:97 qt/base/ignore_list_dialog.py:32 +#: qt/base/app.py:85 qt/base/ignore_list_dialog.py:32 #: cocoa/base/en.lproj/Localizable.strings:0 msgid "Ignore List" msgstr "Lista Ignorar" -#: qt/base/app.py:98 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/app.py:86 cocoa/base/en.lproj/Localizable.strings:0 msgid "dupeGuru Help" msgstr "Ajuda dupeGuru" -#: qt/base/app.py:99 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/app.py:87 cocoa/base/en.lproj/Localizable.strings:0 msgid "About dupeGuru" msgstr "Sobre o dupeGuru" -#: qt/base/app.py:100 +#: qt/base/app.py:88 msgid "Register dupeGuru" -msgstr "Registrar dupeGuru" +msgstr "Registrar o dupeGuru" -#: qt/base/app.py:101 +#: qt/base/app.py:89 msgid "Check for Update" msgstr "Buscar Atualizaçõs" -#: qt/base/app.py:102 +#: qt/base/app.py:90 msgid "Open Debug Log" msgstr "Abrir Registro de Depuração" -#: qt/base/app.py:257 +#: qt/base/app.py:217 msgid "{} file (*.{})" msgstr "Arquivo {} (*.{})" -#: qt/base/deletion_options.py:30 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/deletion_options.py:29 cocoa/base/en.lproj/Localizable.strings:0 msgid "Deletion Options" msgstr "Opções de Apagamento" -#: qt/base/deletion_options.py:35 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/deletion_options.py:34 cocoa/base/en.lproj/Localizable.strings:0 msgid "Link deleted files" msgstr "Criar link dos arquivos apagados" -#: qt/base/deletion_options.py:37 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/deletion_options.py:36 cocoa/base/en.lproj/Localizable.strings:0 msgid "" "After having deleted a duplicate, place a link targeting the reference file " "to replace the deleted file." @@ -122,19 +114,23 @@ msgstr "" "Após apagar uma duplicata, cria um link direcionado ao arquivo original para" " substituir o arquivo apagado." -#: qt/base/deletion_options.py:42 +#: qt/base/deletion_options.py:41 msgid "Hardlink" msgstr "Hardlink" -#: qt/base/deletion_options.py:42 +#: qt/base/deletion_options.py:41 msgid "Symlink" msgstr "Symlink" -#: qt/base/deletion_options.py:48 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/deletion_options.py:46 +msgid " (unsupported)" +msgstr "(incompatível)" + +#: qt/base/deletion_options.py:47 cocoa/base/en.lproj/Localizable.strings:0 msgid "Directly delete files" msgstr "Apagar arquivos imediatamente" -#: qt/base/deletion_options.py:50 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/deletion_options.py:49 cocoa/base/en.lproj/Localizable.strings:0 msgid "" "Instead of sending files to trash, delete them directly. This option is " "usually used as a workaround when the normal deletion method doesn't work." @@ -142,11 +138,11 @@ msgstr "" "Apaga os arquivos imediatamente ao invés de movê-los para o Lixo. Essa opção" " é usada como alternativa para quando o método normal falha." -#: qt/base/deletion_options.py:56 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/deletion_options.py:55 cocoa/base/en.lproj/Localizable.strings:0 msgid "Proceed" msgstr "Continuar" -#: qt/base/deletion_options.py:57 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/deletion_options.py:56 cocoa/base/en.lproj/Localizable.strings:0 msgid "Cancel" msgstr "Cancelar" @@ -195,7 +191,7 @@ msgstr "Carregar Resultados Recentes" #: qt/base/directories_dialog.py:108 cocoa/base/en.lproj/Localizable.strings:0 msgid "Select folders to scan and press \"Scan\"." -msgstr "Selecione as pastas desejadas e clique em \"Escanear\"." +msgstr "Selecione as pastas desejadas e clique em “Escanear”." #: qt/base/directories_dialog.py:132 cocoa/base/en.lproj/Localizable.strings:0 msgid "Load Results" @@ -211,7 +207,7 @@ msgstr "Resultados não salvos" #: qt/base/directories_dialog.py:180 cocoa/base/en.lproj/Localizable.strings:0 msgid "You have unsaved results, do you really want to quit?" -msgstr "Você possui resultados não salvos, deseja encerrar mesmo assim?" +msgstr "Você possui resultados não salvos, deseja encerrar assim mesmo?" #: qt/base/directories_dialog.py:188 cocoa/base/en.lproj/Localizable.strings:0 msgid "Select a folder to add to the scanning list" @@ -318,7 +314,7 @@ msgstr "" #: qt/base/prioritize_dialog.py:71 cocoa/base/en.lproj/Localizable.strings:0 msgid "Re-Prioritize duplicates" -msgstr "Re-Priorizar duplicatas" +msgstr "Repriorizar duplicatas" #: qt/base/prioritize_dialog.py:75 cocoa/base/en.lproj/Localizable.strings:0 msgid "" @@ -326,9 +322,9 @@ msgid "" " the best to these criteria to their respective group's reference position. " "Read the help file for more information." msgstr "" -"Adicione critérios à caixa da direita e clique OK para elevar as duplicatas " -"à posição de referência em seus respectivos grupos, baseado nos critérios " -"escolhidos. Leia a Ajuda para maiores informações." +"Adicione critérios à caixa da direita e clique em OK para elevar as " +"duplicatas à posição de referência em seus respectivos grupos, baseado nos " +"critérios escolhidos. Leia a Ajuda para maiores informações." #: qt/base/problem_dialog.py:31 cocoa/base/en.lproj/Localizable.strings:0 msgid "Problems!" @@ -346,7 +342,7 @@ msgstr "" #: qt/base/problem_dialog.py:52 msgid "Reveal Selected" -msgstr "Mostrar no Finder" +msgstr "Mostrar Seleção" #: qt/base/result_window.py:44 qt/base/result_window.py:170 #: qt/me/details_dialog.py:20 qt/pe/details_dialog.py:25 @@ -386,7 +382,7 @@ msgstr "Remover Marcados dos Resultados" #: qt/base/result_window.py:52 cocoa/base/en.lproj/Localizable.strings:0 msgid "Re-Prioritize Results..." -msgstr "Re-Priorizar Resultados…" +msgstr "Repriorizar Resultados…" #: qt/base/result_window.py:53 cocoa/base/en.lproj/Localizable.strings:0 msgid "Remove Selected from Results" @@ -414,11 +410,11 @@ msgstr "Renomear Seleção" #: qt/base/result_window.py:59 cocoa/base/en.lproj/Localizable.strings:0 msgid "Mark All" -msgstr "Marcar Todos" +msgstr "Marcar Tudo" #: qt/base/result_window.py:60 cocoa/base/en.lproj/Localizable.strings:0 msgid "Mark None" -msgstr "Marcar Nenhum" +msgstr "Desmarcar Tudo" #: qt/base/result_window.py:61 cocoa/base/en.lproj/Localizable.strings:0 msgid "Invert Marking" @@ -569,16 +565,16 @@ msgstr "Timestamp EXIF" msgid "Match pictures of different dimensions" msgstr "Coincidir fotos de dimensões diferentes" -#: qt/pe/result_window.py:19 qt/pe/result_window.py:24 +#: qt/pe/result_window.py:19 qt/pe/result_window.py:25 #: cocoa/base/en.lproj/Localizable.strings:0 msgid "Clear Picture Cache" msgstr "Apagar Cache de Fotos" -#: qt/pe/result_window.py:25 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/pe/result_window.py:26 cocoa/base/en.lproj/Localizable.strings:0 msgid "Do you really want to remove all your cached picture analysis?" msgstr "Deseja remover todo o cache das fotos já analizadas?" -#: qt/pe/result_window.py:28 +#: qt/pe/result_window.py:29 msgid "Picture cache cleared." msgstr "Cache de fotos apagado." @@ -636,7 +632,7 @@ msgstr "Básico" #: cocoa/base/en.lproj/Localizable.strings:0 msgid "Bring All to Front" -msgstr "Trazer Todas Para a Frente" +msgstr "Trazer Todas para Frente" #: cocoa/base/en.lproj/Localizable.strings:0 msgid "Check for update..." @@ -810,6 +806,10 @@ msgstr "Mostrar" msgid "Reveal Selected in Finder" msgstr "Mostrar Seleção no Finder" +#: cocoa/base/en.lproj/Localizable.strings:0 +msgid "Select All" +msgstr "" + #: cocoa/base/en.lproj/Localizable.strings:0 msgid "Send Marked to Trash..." msgstr "Mover Marcados para o Lixo…" @@ -828,7 +828,7 @@ msgstr "Iniciar Escaneamento de Duplicata" #: cocoa/base/en.lproj/Localizable.strings:0 msgid "The name '%@' already exists." -msgstr "O nome '%@' já existe." +msgstr "O nome ‘%@’ já existe." #: cocoa/base/en.lproj/Localizable.strings:0 msgid "Window" @@ -836,8 +836,4 @@ msgstr "Janela" #: cocoa/base/en.lproj/Localizable.strings:0 msgid "Zoom" -msgstr "Reduzir/Ampliar" - -#: qt/base/deletion_options.py:46 -msgid " (unsupported)" -msgstr "(incompatível)" +msgstr "Ampliar/Reduzir" diff --git a/locale/ru/LC_MESSAGES/columns.po b/locale/ru/LC_MESSAGES/columns.po index ce658752..2e05d609 100644 --- a/locale/ru/LC_MESSAGES/columns.po +++ b/locale/ru/LC_MESSAGES/columns.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: dupeGuru\n" -"PO-Revision-Date: 2013-06-02 11:27+0000\n" +"PO-Revision-Date: 2013-11-20 11:53+0000\n" "Last-Translator: Kyrill Detinov \n" "Language-Team: Russian (http://www.transifex.com/projects/p/dupeguru/language/ru/)\n" "Content-Type: text/plain; charset=UTF-8\n" diff --git a/locale/ru/LC_MESSAGES/core.po b/locale/ru/LC_MESSAGES/core.po index 4758dab0..b8b0ca81 100644 --- a/locale/ru/LC_MESSAGES/core.po +++ b/locale/ru/LC_MESSAGES/core.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: dupeGuru\n" -"PO-Revision-Date: 2013-08-03 20:35+0000\n" +"PO-Revision-Date: 2013-12-07 15:22+0000\n" "Last-Translator: hsoft \n" "Language-Team: Russian (http://www.transifex.com/projects/p/dupeguru/language/ru/)\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -11,15 +11,15 @@ msgstr "" "Language: ru\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" -#: core/app.py:40 +#: core/app.py:39 msgid "There are no marked duplicates. Nothing has been done." msgstr "Дубликаты не отмечены. Нечего выполнять." -#: core/app.py:41 +#: core/app.py:40 msgid "There are no selected duplicates. Nothing has been done." msgstr "Дубликаты не выбраны. Нечего выполнять." -#: core/app.py:42 +#: core/app.py:41 msgid "" "You're about to open many files at once. Depending on what those files are " "opened with, doing so can create quite a mess. Continue?" @@ -28,37 +28,31 @@ msgstr "" "файлы будут открыты, это действие может создать настоящий беспорядок. " "Продолжать?" -#: core/app.py:58 +#: core/app.py:57 msgid "Scanning for duplicates" msgstr "Проверка на наличие дубликатов" -#: core/app.py:59 +#: core/app.py:58 msgid "Loading" msgstr "Загрузка" -#: core/app.py:60 +#: core/app.py:59 msgid "Moving" msgstr "Перемещение" -#: core/app.py:61 +#: core/app.py:60 msgid "Copying" msgstr "Копирование" -#: core/app.py:62 +#: core/app.py:61 msgid "Sending to Trash" msgstr "Перемещение в Корзину" -#: core/app.py:65 +#: core/app.py:64 msgid "Sending files to the recycle bin" msgstr "Перемещение файлов в Корзину" -#: core/app.py:110 -msgid "will only be able to delete, move or copy 10 duplicates at once" -msgstr "" -"вы сможете удалить, переместить или скопировать только 10 дубликатов за один" -" раз" - -#: core/app.py:246 +#: core/app.py:295 msgid "" "A previous action is still hanging in there. You can't start a new one yet. " "Wait a few seconds, then try again." @@ -66,39 +60,31 @@ msgstr "" "Предыдущее действие до сих пор выполняется. Вы не можете начать новое. " "Подождите несколько секунд, затем повторите попытку." -#: core/app.py:254 +#: core/app.py:302 msgid "No duplicates found." msgstr "Дубликаты не найдены." -#: core/app.py:267 +#: core/app.py:315 msgid "All marked files were copied successfully." msgstr "Все отмеченные файлы были скопированы успешно." -#: core/app.py:268 +#: core/app.py:316 msgid "All marked files were moved successfully." msgstr "Все отмеченные файлы были перемещены успешно." -#: core/app.py:269 +#: core/app.py:317 msgid "All marked files were successfully sent to Trash." msgstr "Все отмеченные файлы были успешно отправлены в Корзину." -#: core/app.py:296 -msgid "" -"You cannot delete, move or copy more than 10 duplicates at once in demo " -"mode." -msgstr "" -"Вы не можете удалять, перемещать или копировать более 10 дубликатов за один " -"раз в демонстрационном режиме." - -#: core/app.py:307 +#: core/app.py:354 msgid "'{}' already is in the list." msgstr "'{}' уже присутствует в списке." -#: core/app.py:309 +#: core/app.py:356 msgid "'{}' does not exist." msgstr "'{}' не существует." -#: core/app.py:316 +#: core/app.py:365 msgid "" "All selected %d matches are going to be ignored in all subsequent scans. " "Continue?" @@ -106,59 +92,59 @@ msgstr "" "Все выбранные %d совпадений будут игнорироваться при всех последующих " "проверках. Продолжить?" -#: core/app.py:376 +#: core/app.py:431 msgid "copy" msgstr "копирование" -#: core/app.py:376 +#: core/app.py:431 msgid "move" msgstr "перемещение" -#: core/app.py:377 +#: core/app.py:432 msgid "Select a directory to {} marked files to" msgstr "Выберите каталог {} для отмеченных файлов" -#: core/app.py:403 +#: core/app.py:469 msgid "Select a destination for your exported CSV" msgstr "Выберите назначение для экспортируемого " -#: core/app.py:428 +#: core/app.py:494 msgid "You have no custom command set up. Set it up in your preferences." msgstr "Вы не создали пользовательскую команду. Задайте её в настройках." -#: core/app.py:535 core/app.py:546 +#: core/app.py:646 core/app.py:659 msgid "You are about to remove %d files from results. Continue?" msgstr "Вы собираетесь удалить %d файлов из результата поиска. Продолжить?" -#: core/app.py:566 +#: core/app.py:693 msgid "{} duplicate groups were changed by the re-prioritization." msgstr "{} групп дубликатов было изменено при реприоритезации." -#: core/app.py:586 +#: core/app.py:721 msgid "Collecting files to scan" msgstr "Сбор файлов для сканирования" -#: core/app.py:597 +#: core/app.py:732 msgid "The selected directories contain no scannable file." msgstr "Выбранные каталоги не содержат файлов для сканирования." -#: core/app.py:636 +#: core/app.py:773 msgid "%s (%d discarded)" msgstr "%s. (%d отменено)" -#: core/engine.py:178 core/engine.py:215 +#: core/engine.py:220 core/engine.py:265 msgid "0 matches found" msgstr "0 совпадений найдено" -#: core/engine.py:196 core/engine.py:223 +#: core/engine.py:238 core/engine.py:273 msgid "%d matches found" msgstr "%d совпадений найдено" -#: core/engine.py:208 core/scanner.py:79 +#: core/engine.py:258 core/scanner.py:79 msgid "Read size of %d/%d files" msgstr "Подсчитан размер %d/%d файлов" -#: core/engine.py:361 +#: core/engine.py:464 msgid "Grouped %d/%d matches" msgstr "Группировка %d/%d совпадений" @@ -207,11 +193,11 @@ msgstr "Новейший" msgid "Oldest" msgstr "Старейшие" -#: core/results.py:113 +#: core/results.py:126 msgid "%d / %d (%s / %s) duplicates marked." msgstr "%d / %d (%s / %s) дубликатов отмечено." -#: core/results.py:120 +#: core/results.py:133 msgid " filter: %s" msgstr "фильтр: %s" diff --git a/locale/ru/LC_MESSAGES/ui.po b/locale/ru/LC_MESSAGES/ui.po index 4d9a00bb..b39541e5 100644 --- a/locale/ru/LC_MESSAGES/ui.po +++ b/locale/ru/LC_MESSAGES/ui.po @@ -3,8 +3,8 @@ msgid "" msgstr "" "Project-Id-Version: dupeGuru\n" -"PO-Revision-Date: 2013-08-03 23:20+0000\n" -"Last-Translator: Kyrill Detinov \n" +"PO-Revision-Date: 2013-12-07 15:22+0000\n" +"Last-Translator: hsoft \n" "Language-Team: Russian (http://www.transifex.com/projects/p/dupeguru/language/ru/)\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: utf-8\n" @@ -19,98 +19,92 @@ msgstr "Удаление отсутствующих треков из библи msgid "Scanning the iTunes Library" msgstr "Сканирование библиотеки iTunes" -#: cocoa/inter/app_me.py:161 cocoa/inter/app_pe.py:186 +#: cocoa/inter/app_me.py:160 cocoa/inter/app_pe.py:194 msgid "Sending dupes to the Trash" msgstr "Перемещение дубликатов в Корзину" -#: cocoa/inter/app_me.py:163 +#: cocoa/inter/app_me.py:162 msgid "Talking to iTunes. Don't touch it!" msgstr "Соединение с iTunes. Не трогайте!" -#: cocoa/inter/app_me.py:189 -msgid "" -"There were communication problems with iTunes. The operation couldn't be " -"completed." -msgstr "Проблема соединения с iTunes. Операция не может быть завершена." - -#: cocoa/inter/app_me.py:195 +#: cocoa/inter/app_me.py:197 msgid "" "Your iTunes Library contains %d dead tracks ready to be removed. Continue?" msgstr "" "Ваш библиотека iTunes содержит %d отсутствующих треков готовых для удаления." " Продолжить?" -#: cocoa/inter/app_me.py:199 +#: cocoa/inter/app_me.py:201 msgid "You have no dead tracks in your iTunes Library" msgstr "У вас нет отсутствующих треков в библиотеке iTunes" -#: cocoa/inter/app_me.py:217 +#: cocoa/inter/app_me.py:219 msgid "The iTunes application couldn't be found." msgstr "Приложение iTunes не найдено." -#: cocoa/inter/app_pe.py:188 +#: cocoa/inter/app_pe.py:196 msgid "Talking to iPhoto. Don't touch it!" msgstr "В контакте с iPhoto. Не трогайте!" -#: cocoa/inter/app_pe.py:197 +#: cocoa/inter/app_pe.py:205 msgid "Talking to Aperture. Don't touch it!" msgstr "Соединение с Aperture. Не трогайте!" -#: cocoa/inter/app_pe.py:270 +#: cocoa/inter/app_pe.py:278 msgid "Deleted Aperture photos were sent to a project called \"dupeGuru Trash\"." msgstr "" "Удалённые фотографии Aperture были перемещены в проект «dupeGuru Trash»." -#: cocoa/inter/app_pe.py:296 +#: cocoa/inter/app_pe.py:304 msgid "The iPhoto application couldn't be found." msgstr "Приложение iPhoto не найдено." -#: qt/base/app.py:95 +#: qt/base/app.py:83 msgid "Quit" msgstr "Выйти" -#: qt/base/app.py:96 qt/base/preferences_dialog.py:123 +#: qt/base/app.py:84 qt/base/preferences_dialog.py:123 msgid "Preferences" msgstr "Настройки" -#: qt/base/app.py:97 qt/base/ignore_list_dialog.py:32 +#: qt/base/app.py:85 qt/base/ignore_list_dialog.py:32 #: cocoa/base/en.lproj/Localizable.strings:0 msgid "Ignore List" msgstr "Список игнорирования" -#: qt/base/app.py:98 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/app.py:86 cocoa/base/en.lproj/Localizable.strings:0 msgid "dupeGuru Help" msgstr "Справка dupeGuru" -#: qt/base/app.py:99 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/app.py:87 cocoa/base/en.lproj/Localizable.strings:0 msgid "About dupeGuru" msgstr "О dupeGuru" -#: qt/base/app.py:100 +#: qt/base/app.py:88 msgid "Register dupeGuru" msgstr "Регистрация dupeGuru" -#: qt/base/app.py:101 +#: qt/base/app.py:89 msgid "Check for Update" msgstr "Проверить обновления" -#: qt/base/app.py:102 +#: qt/base/app.py:90 msgid "Open Debug Log" msgstr "Открыть журнал отладки" -#: qt/base/app.py:257 +#: qt/base/app.py:217 msgid "{} file (*.{})" msgstr "{} файл (*.{})" -#: qt/base/deletion_options.py:30 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/deletion_options.py:29 cocoa/base/en.lproj/Localizable.strings:0 msgid "Deletion Options" msgstr "Параметры удаления" -#: qt/base/deletion_options.py:35 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/deletion_options.py:34 cocoa/base/en.lproj/Localizable.strings:0 msgid "Link deleted files" msgstr "Создать ссылку вместо удалённого файла" -#: qt/base/deletion_options.py:37 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/deletion_options.py:36 cocoa/base/en.lproj/Localizable.strings:0 msgid "" "After having deleted a duplicate, place a link targeting the reference file " "to replace the deleted file." @@ -118,19 +112,23 @@ msgstr "" "После удаления дубликата создать ссылку на эталонный файл на месте " "удалённого." -#: qt/base/deletion_options.py:42 +#: qt/base/deletion_options.py:41 msgid "Hardlink" msgstr "Жёсткая ссылка" -#: qt/base/deletion_options.py:42 +#: qt/base/deletion_options.py:41 msgid "Symlink" msgstr "Символьная ссылка" -#: qt/base/deletion_options.py:48 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/deletion_options.py:46 +msgid " (unsupported)" +msgstr "(не поддерживается)" + +#: qt/base/deletion_options.py:47 cocoa/base/en.lproj/Localizable.strings:0 msgid "Directly delete files" msgstr "Удалить файл с диска" -#: qt/base/deletion_options.py:50 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/deletion_options.py:49 cocoa/base/en.lproj/Localizable.strings:0 msgid "" "Instead of sending files to trash, delete them directly. This option is " "usually used as a workaround when the normal deletion method doesn't work." @@ -138,11 +136,11 @@ msgstr "" "Вместо отправки файлов в Корзину удалить их с диска. Этот параметр обычно " "используется как обходной путь, когда нормальный метод удаления не работает." -#: qt/base/deletion_options.py:56 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/deletion_options.py:55 cocoa/base/en.lproj/Localizable.strings:0 msgid "Proceed" msgstr "Выполняется" -#: qt/base/deletion_options.py:57 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/deletion_options.py:56 cocoa/base/en.lproj/Localizable.strings:0 msgid "Cancel" msgstr "Отменить" @@ -565,16 +563,16 @@ msgstr "Временная отметка EXIF" msgid "Match pictures of different dimensions" msgstr "Совпадение рисунков разных размеров" -#: qt/pe/result_window.py:19 qt/pe/result_window.py:24 +#: qt/pe/result_window.py:19 qt/pe/result_window.py:25 #: cocoa/base/en.lproj/Localizable.strings:0 msgid "Clear Picture Cache" msgstr "Очистить кэш изображений" -#: qt/pe/result_window.py:25 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/pe/result_window.py:26 cocoa/base/en.lproj/Localizable.strings:0 msgid "Do you really want to remove all your cached picture analysis?" msgstr "Вы действительно хотите удалить все кэшированные данные изображений?" -#: qt/pe/result_window.py:28 +#: qt/pe/result_window.py:29 msgid "Picture cache cleared." msgstr "Кэш изображений очищен." @@ -807,6 +805,10 @@ msgstr "Показать" msgid "Reveal Selected in Finder" msgstr "Показать выбранное в Finder-е" +#: cocoa/base/en.lproj/Localizable.strings:0 +msgid "Select All" +msgstr "" + #: cocoa/base/en.lproj/Localizable.strings:0 msgid "Send Marked to Trash..." msgstr "Переместить отмеченные в Корзину…" @@ -834,7 +836,3 @@ msgstr "Окно" #: cocoa/base/en.lproj/Localizable.strings:0 msgid "Zoom" msgstr "Увеличить" - -#: qt/base/deletion_options.py:46 -msgid " (unsupported)" -msgstr "(не поддерживается)" diff --git a/locale/ui.pot b/locale/ui.pot index a9e527e6..91e18ea3 100644 --- a/locale/ui.pot +++ b/locale/ui.pot @@ -12,123 +12,121 @@ msgstr "" msgid "Scanning the iTunes Library" msgstr "" -#: cocoa/inter/app_me.py:161 cocoa/inter/app_pe.py:186 +#: cocoa/inter/app_me.py:160 cocoa/inter/app_pe.py:194 msgid "Sending dupes to the Trash" msgstr "" -#: cocoa/inter/app_me.py:163 +#: cocoa/inter/app_me.py:162 msgid "Talking to iTunes. Don't touch it!" msgstr "" -#: cocoa/inter/app_me.py:189 -msgid "" -"There were communication problems with iTunes. The operation couldn't be " -"completed." -msgstr "" - -#: cocoa/inter/app_me.py:195 +#: cocoa/inter/app_me.py:197 msgid "" "Your iTunes Library contains %d dead tracks ready to be removed. Continue?" msgstr "" -#: cocoa/inter/app_me.py:199 +#: cocoa/inter/app_me.py:201 msgid "You have no dead tracks in your iTunes Library" msgstr "" -#: cocoa/inter/app_me.py:217 +#: cocoa/inter/app_me.py:219 msgid "The iTunes application couldn't be found." msgstr "" -#: cocoa/inter/app_pe.py:188 +#: cocoa/inter/app_pe.py:196 msgid "Talking to iPhoto. Don't touch it!" msgstr "" -#: cocoa/inter/app_pe.py:197 +#: cocoa/inter/app_pe.py:205 msgid "Talking to Aperture. Don't touch it!" msgstr "" -#: cocoa/inter/app_pe.py:270 +#: cocoa/inter/app_pe.py:278 msgid "Deleted Aperture photos were sent to a project called \"dupeGuru Trash\"." msgstr "" -#: cocoa/inter/app_pe.py:296 +#: cocoa/inter/app_pe.py:304 msgid "The iPhoto application couldn't be found." msgstr "" -#: qt/base/app.py:95 +#: qt/base/app.py:83 msgid "Quit" msgstr "" -#: qt/base/app.py:96 qt/base/preferences_dialog.py:123 +#: qt/base/app.py:84 qt/base/preferences_dialog.py:123 msgid "Preferences" msgstr "" -#: qt/base/app.py:97 qt/base/ignore_list_dialog.py:32 +#: qt/base/app.py:85 qt/base/ignore_list_dialog.py:32 #: cocoa/base/en.lproj/Localizable.strings:0 msgid "Ignore List" msgstr "" -#: qt/base/app.py:98 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/app.py:86 cocoa/base/en.lproj/Localizable.strings:0 msgid "dupeGuru Help" msgstr "" -#: qt/base/app.py:99 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/app.py:87 cocoa/base/en.lproj/Localizable.strings:0 msgid "About dupeGuru" msgstr "" -#: qt/base/app.py:100 +#: qt/base/app.py:88 msgid "Register dupeGuru" msgstr "" -#: qt/base/app.py:101 +#: qt/base/app.py:89 msgid "Check for Update" msgstr "" -#: qt/base/app.py:102 +#: qt/base/app.py:90 msgid "Open Debug Log" msgstr "" -#: qt/base/app.py:257 +#: qt/base/app.py:217 msgid "{} file (*.{})" msgstr "" -#: qt/base/deletion_options.py:30 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/deletion_options.py:29 cocoa/base/en.lproj/Localizable.strings:0 msgid "Deletion Options" msgstr "" -#: qt/base/deletion_options.py:35 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/deletion_options.py:34 cocoa/base/en.lproj/Localizable.strings:0 msgid "Link deleted files" msgstr "" -#: qt/base/deletion_options.py:37 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/deletion_options.py:36 cocoa/base/en.lproj/Localizable.strings:0 msgid "" "After having deleted a duplicate, place a link targeting the reference file " "to replace the deleted file." msgstr "" -#: qt/base/deletion_options.py:42 +#: qt/base/deletion_options.py:41 msgid "Hardlink" msgstr "" -#: qt/base/deletion_options.py:42 +#: qt/base/deletion_options.py:41 msgid "Symlink" msgstr "" -#: qt/base/deletion_options.py:48 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/deletion_options.py:46 +msgid " (unsupported)" +msgstr "" + +#: qt/base/deletion_options.py:47 cocoa/base/en.lproj/Localizable.strings:0 msgid "Directly delete files" msgstr "" -#: qt/base/deletion_options.py:50 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/deletion_options.py:49 cocoa/base/en.lproj/Localizable.strings:0 msgid "" "Instead of sending files to trash, delete them directly. This option is " "usually used as a workaround when the normal deletion method doesn't work." msgstr "" -#: qt/base/deletion_options.py:56 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/deletion_options.py:55 cocoa/base/en.lproj/Localizable.strings:0 msgid "Proceed" msgstr "" -#: qt/base/deletion_options.py:57 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/deletion_options.py:56 cocoa/base/en.lproj/Localizable.strings:0 msgid "Cancel" msgstr "" @@ -543,16 +541,16 @@ msgstr "" msgid "Match pictures of different dimensions" msgstr "" -#: qt/pe/result_window.py:19 qt/pe/result_window.py:24 +#: qt/pe/result_window.py:19 qt/pe/result_window.py:25 #: cocoa/base/en.lproj/Localizable.strings:0 msgid "Clear Picture Cache" msgstr "" -#: qt/pe/result_window.py:25 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/pe/result_window.py:26 cocoa/base/en.lproj/Localizable.strings:0 msgid "Do you really want to remove all your cached picture analysis?" msgstr "" -#: qt/pe/result_window.py:28 +#: qt/pe/result_window.py:29 msgid "Picture cache cleared." msgstr "" @@ -784,6 +782,10 @@ msgstr "" msgid "Reveal Selected in Finder" msgstr "" +#: cocoa/base/en.lproj/Localizable.strings:0 +msgid "Select All" +msgstr "" + #: cocoa/base/en.lproj/Localizable.strings:0 msgid "Send Marked to Trash..." msgstr "" @@ -811,7 +813,3 @@ msgstr "" #: cocoa/base/en.lproj/Localizable.strings:0 msgid "Zoom" msgstr "" - -#: qt/base/deletion_options.py:46 -msgid " (unsupported)" -msgstr "" diff --git a/locale/uk/LC_MESSAGES/columns.po b/locale/uk/LC_MESSAGES/columns.po index aa9d97e9..929a98b3 100755 --- a/locale/uk/LC_MESSAGES/columns.po +++ b/locale/uk/LC_MESSAGES/columns.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: dupeGuru\n" -"PO-Revision-Date: 2012-09-05 15:22+0000\n" +"PO-Revision-Date: 2013-11-20 11:53+0000\n" "Last-Translator: hsoft \n" "Language-Team: Ukrainian (http://www.transifex.com/projects/p/dupeguru/language/uk/)\n" "Content-Type: text/plain; charset=UTF-8\n" diff --git a/locale/uk/LC_MESSAGES/core.po b/locale/uk/LC_MESSAGES/core.po index c0a59bdb..f1db7b21 100755 --- a/locale/uk/LC_MESSAGES/core.po +++ b/locale/uk/LC_MESSAGES/core.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: dupeGuru\n" -"PO-Revision-Date: 2013-08-03 20:35+0000\n" +"PO-Revision-Date: 2013-12-07 15:22+0000\n" "Last-Translator: hsoft \n" "Language-Team: Ukrainian (http://www.transifex.com/projects/p/dupeguru/language/uk/)\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -10,49 +10,45 @@ msgstr "" "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" -#: core/app.py:40 +#: core/app.py:39 msgid "There are no marked duplicates. Nothing has been done." msgstr "Немає позначених дублікатів - нічого робити." -#: core/app.py:41 +#: core/app.py:40 msgid "There are no selected duplicates. Nothing has been done." msgstr "Немає обраних дублікатів - нічого робити." -#: core/app.py:42 +#: core/app.py:41 msgid "" "You're about to open many files at once. Depending on what those files are " "opened with, doing so can create quite a mess. Continue?" msgstr "" -#: core/app.py:58 +#: core/app.py:57 msgid "Scanning for duplicates" msgstr "Пошук дублікатів" -#: core/app.py:59 +#: core/app.py:58 msgid "Loading" msgstr "Завантаження" -#: core/app.py:60 +#: core/app.py:59 msgid "Moving" msgstr "Переміщення" -#: core/app.py:61 +#: core/app.py:60 msgid "Copying" msgstr "Копіювання" -#: core/app.py:62 +#: core/app.py:61 msgid "Sending to Trash" msgstr "Відправка до кошику" -#: core/app.py:65 +#: core/app.py:64 msgid "Sending files to the recycle bin" msgstr "Відправлення файлів до кошика" -#: core/app.py:110 -msgid "will only be able to delete, move or copy 10 duplicates at once" -msgstr "може видаляти, переміщувати або копіювати лише 10 копій відразу" - -#: core/app.py:246 +#: core/app.py:295 msgid "" "A previous action is still hanging in there. You can't start a new one yet. " "Wait a few seconds, then try again." @@ -60,39 +56,31 @@ msgstr "" "Попередню дію ще не закінчено. Ви покищо не можете розпочаті нову. Зачекайте" " кілька секунд, потім повторіть спробу." -#: core/app.py:254 +#: core/app.py:302 msgid "No duplicates found." msgstr "Не знайдено жодного дублікату." -#: core/app.py:267 +#: core/app.py:315 msgid "All marked files were copied successfully." msgstr "Усі позначені файли були скопійовані успішно." -#: core/app.py:268 +#: core/app.py:316 msgid "All marked files were moved successfully." msgstr "Усі позначені файли були переміщені успішно." -#: core/app.py:269 +#: core/app.py:317 msgid "All marked files were successfully sent to Trash." msgstr "Усі позначені файли були успішно відправлені до кошика." -#: core/app.py:296 -msgid "" -"You cannot delete, move or copy more than 10 duplicates at once in demo " -"mode." -msgstr "" -"Ви не можете видаляти, переміщати або копіювати більше 10 дублікатів відразу" -" в демонстраційному режимі." - -#: core/app.py:307 +#: core/app.py:354 msgid "'{}' already is in the list." msgstr "'{}' вже є в списку." -#: core/app.py:309 +#: core/app.py:356 msgid "'{}' does not exist." msgstr "'{}' не існує." -#: core/app.py:316 +#: core/app.py:365 msgid "" "All selected %d matches are going to be ignored in all subsequent scans. " "Continue?" @@ -100,59 +88,59 @@ msgstr "" "Усі обрані %d результатів будуть ігноруватися під час усіх наступних " "пошуків. Продовжити?" -#: core/app.py:376 +#: core/app.py:431 msgid "copy" msgstr "копіювання" -#: core/app.py:376 +#: core/app.py:431 msgid "move" msgstr "переміщення" -#: core/app.py:377 +#: core/app.py:432 msgid "Select a directory to {} marked files to" msgstr "Оберіть цільову папку для {} позначених файлів" -#: core/app.py:403 +#: core/app.py:469 msgid "Select a destination for your exported CSV" msgstr "" -#: core/app.py:428 +#: core/app.py:494 msgid "You have no custom command set up. Set it up in your preferences." msgstr "Власна команда не встановлена. Встановіть її у налаштуваннях." -#: core/app.py:535 core/app.py:546 +#: core/app.py:646 core/app.py:659 msgid "You are about to remove %d files from results. Continue?" msgstr "Ви збираєтеся видалити %d файлів з результату пошуку. Продовжити?" -#: core/app.py:566 +#: core/app.py:693 msgid "{} duplicate groups were changed by the re-prioritization." msgstr "" -#: core/app.py:586 +#: core/app.py:721 msgid "Collecting files to scan" msgstr "Збір файлів для пошуку" -#: core/app.py:597 +#: core/app.py:732 msgid "The selected directories contain no scannable file." msgstr "Обрані папки не містять файлів придатних для пошуку." -#: core/app.py:636 +#: core/app.py:773 msgid "%s (%d discarded)" msgstr "%s (%d відкинуто)" -#: core/engine.py:178 core/engine.py:215 +#: core/engine.py:220 core/engine.py:265 msgid "0 matches found" msgstr "0 результатів знайдено" -#: core/engine.py:196 core/engine.py:223 +#: core/engine.py:238 core/engine.py:273 msgid "%d matches found" msgstr "%d результатів знайдено" -#: core/engine.py:208 core/scanner.py:79 +#: core/engine.py:258 core/scanner.py:79 msgid "Read size of %d/%d files" msgstr "Прочитано розмір %d/%d файлів" -#: core/engine.py:361 +#: core/engine.py:464 msgid "Grouped %d/%d matches" msgstr "Згруповано %d/%d результатів" @@ -200,11 +188,11 @@ msgstr "" msgid "Oldest" msgstr "" -#: core/results.py:113 +#: core/results.py:126 msgid "%d / %d (%s / %s) duplicates marked." msgstr "%d / %d (%s / %s) дублікатів позначено." -#: core/results.py:120 +#: core/results.py:133 msgid " filter: %s" msgstr "фільтр: %s" diff --git a/locale/uk/LC_MESSAGES/ui.po b/locale/uk/LC_MESSAGES/ui.po index c3e1ddae..b9e54302 100755 --- a/locale/uk/LC_MESSAGES/ui.po +++ b/locale/uk/LC_MESSAGES/ui.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: dupeGuru\n" -"PO-Revision-Date: 2013-08-03 20:36+0000\n" +"PO-Revision-Date: 2013-12-07 15:22+0000\n" "Last-Translator: hsoft \n" "Language-Team: Ukrainian (http://www.transifex.com/projects/p/dupeguru/language/uk/)\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -18,125 +18,123 @@ msgstr "Видалення мертвих треків з вашої біблі msgid "Scanning the iTunes Library" msgstr "Сканування бібліотеки iTunes" -#: cocoa/inter/app_me.py:161 cocoa/inter/app_pe.py:186 +#: cocoa/inter/app_me.py:160 cocoa/inter/app_pe.py:194 msgid "Sending dupes to the Trash" msgstr "Відправлення дублікатів до кошика" -#: cocoa/inter/app_me.py:163 +#: cocoa/inter/app_me.py:162 msgid "Talking to iTunes. Don't touch it!" msgstr "Виконується взаємодія з програмою iTunes. Не чіпайте її!" -#: cocoa/inter/app_me.py:189 -msgid "" -"There were communication problems with iTunes. The operation couldn't be " -"completed." -msgstr "Виникла проблема зв'язку з iTunes. Операція не може бути завершена." - -#: cocoa/inter/app_me.py:195 +#: cocoa/inter/app_me.py:197 msgid "" "Your iTunes Library contains %d dead tracks ready to be removed. Continue?" msgstr "" "Ваша бібліотека iTunes містить %d мертвих треків, які готові до видалення. " "Продовжити?" -#: cocoa/inter/app_me.py:199 +#: cocoa/inter/app_me.py:201 msgid "You have no dead tracks in your iTunes Library" msgstr "У вашій бібліотеці iTunes немає мертвих треків " -#: cocoa/inter/app_me.py:217 +#: cocoa/inter/app_me.py:219 msgid "The iTunes application couldn't be found." msgstr "Не можливо знайти програму iTunes" -#: cocoa/inter/app_pe.py:188 +#: cocoa/inter/app_pe.py:196 msgid "Talking to iPhoto. Don't touch it!" msgstr "Виконується взаємодія з програмою iPhoto. Не чіпайте її!" -#: cocoa/inter/app_pe.py:197 +#: cocoa/inter/app_pe.py:205 msgid "Talking to Aperture. Don't touch it!" msgstr "" -#: cocoa/inter/app_pe.py:270 +#: cocoa/inter/app_pe.py:278 msgid "Deleted Aperture photos were sent to a project called \"dupeGuru Trash\"." msgstr "" -#: cocoa/inter/app_pe.py:296 +#: cocoa/inter/app_pe.py:304 msgid "The iPhoto application couldn't be found." msgstr "Не вдалося знайти програму iPhoto." -#: qt/base/app.py:95 +#: qt/base/app.py:83 msgid "Quit" msgstr "Вихід" -#: qt/base/app.py:96 qt/base/preferences_dialog.py:123 +#: qt/base/app.py:84 qt/base/preferences_dialog.py:123 msgid "Preferences" msgstr "Налаштування" -#: qt/base/app.py:97 qt/base/ignore_list_dialog.py:32 +#: qt/base/app.py:85 qt/base/ignore_list_dialog.py:32 #: cocoa/base/en.lproj/Localizable.strings:0 msgid "Ignore List" msgstr "Чорний список" -#: qt/base/app.py:98 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/app.py:86 cocoa/base/en.lproj/Localizable.strings:0 msgid "dupeGuru Help" msgstr "Довідка dupeGuru" -#: qt/base/app.py:99 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/app.py:87 cocoa/base/en.lproj/Localizable.strings:0 msgid "About dupeGuru" msgstr "Про dupeGuru" -#: qt/base/app.py:100 +#: qt/base/app.py:88 msgid "Register dupeGuru" msgstr "Зареєструвати dupeGuru" -#: qt/base/app.py:101 +#: qt/base/app.py:89 msgid "Check for Update" msgstr "Перевірити оновлення" -#: qt/base/app.py:102 +#: qt/base/app.py:90 msgid "Open Debug Log" msgstr "Відкрити журнал налагодження" -#: qt/base/app.py:257 +#: qt/base/app.py:217 msgid "{} file (*.{})" msgstr "" -#: qt/base/deletion_options.py:30 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/deletion_options.py:29 cocoa/base/en.lproj/Localizable.strings:0 msgid "Deletion Options" msgstr "" -#: qt/base/deletion_options.py:35 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/deletion_options.py:34 cocoa/base/en.lproj/Localizable.strings:0 msgid "Link deleted files" msgstr "" -#: qt/base/deletion_options.py:37 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/deletion_options.py:36 cocoa/base/en.lproj/Localizable.strings:0 msgid "" "After having deleted a duplicate, place a link targeting the reference file " "to replace the deleted file." msgstr "" -#: qt/base/deletion_options.py:42 +#: qt/base/deletion_options.py:41 msgid "Hardlink" msgstr "" -#: qt/base/deletion_options.py:42 +#: qt/base/deletion_options.py:41 msgid "Symlink" msgstr "" -#: qt/base/deletion_options.py:48 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/deletion_options.py:46 +msgid " (unsupported)" +msgstr "" + +#: qt/base/deletion_options.py:47 cocoa/base/en.lproj/Localizable.strings:0 msgid "Directly delete files" msgstr "" -#: qt/base/deletion_options.py:50 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/deletion_options.py:49 cocoa/base/en.lproj/Localizable.strings:0 msgid "" "Instead of sending files to trash, delete them directly. This option is " "usually used as a workaround when the normal deletion method doesn't work." msgstr "" -#: qt/base/deletion_options.py:56 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/deletion_options.py:55 cocoa/base/en.lproj/Localizable.strings:0 msgid "Proceed" msgstr "" -#: qt/base/deletion_options.py:57 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/deletion_options.py:56 cocoa/base/en.lproj/Localizable.strings:0 msgid "Cancel" msgstr "Скасувати" @@ -558,16 +556,16 @@ msgstr "Часова мітка EXIF" msgid "Match pictures of different dimensions" msgstr "Порівнювати малюнки різних розмірів" -#: qt/pe/result_window.py:19 qt/pe/result_window.py:24 +#: qt/pe/result_window.py:19 qt/pe/result_window.py:25 #: cocoa/base/en.lproj/Localizable.strings:0 msgid "Clear Picture Cache" msgstr "Очистити кеш зображень" -#: qt/pe/result_window.py:25 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/pe/result_window.py:26 cocoa/base/en.lproj/Localizable.strings:0 msgid "Do you really want to remove all your cached picture analysis?" msgstr "Ви дійсно хочете видалити всі кешовані результати аналізу зображень?" -#: qt/pe/result_window.py:28 +#: qt/pe/result_window.py:29 msgid "Picture cache cleared." msgstr "Кеш зображень очищено." @@ -799,6 +797,10 @@ msgstr "" msgid "Reveal Selected in Finder" msgstr "Показати вибране у Finder" +#: cocoa/base/en.lproj/Localizable.strings:0 +msgid "Select All" +msgstr "" + #: cocoa/base/en.lproj/Localizable.strings:0 msgid "Send Marked to Trash..." msgstr "Надіслати позначене до кошику..." @@ -826,7 +828,3 @@ msgstr "Вікно" #: cocoa/base/en.lproj/Localizable.strings:0 msgid "Zoom" msgstr "Збільшити" - -#: qt/base/deletion_options.py:46 -msgid " (unsupported)" -msgstr "" diff --git a/locale/vi/LC_MESSAGES/columns.po b/locale/vi/LC_MESSAGES/columns.po index 03666e55..bd88a8ac 100644 --- a/locale/vi/LC_MESSAGES/columns.po +++ b/locale/vi/LC_MESSAGES/columns.po @@ -1,10 +1,10 @@ # Translators: -# ppanhh , 2013 +# ppanhh , 2013 msgid "" msgstr "" "Project-Id-Version: dupeGuru\n" -"PO-Revision-Date: 2013-07-07 08:45+0000\n" -"Last-Translator: ppanhh \n" +"PO-Revision-Date: 2013-11-20 11:53+0000\n" +"Last-Translator: ppanhh \n" "Language-Team: Vietnamese (http://www.transifex.com/projects/p/dupeguru/language/vi/)\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: utf-8\n" diff --git a/locale/vi/LC_MESSAGES/core.po b/locale/vi/LC_MESSAGES/core.po index fa8bfb67..2a6a0b9b 100644 --- a/locale/vi/LC_MESSAGES/core.po +++ b/locale/vi/LC_MESSAGES/core.po @@ -1,27 +1,27 @@ # Translators: -# ppanhh , 2013 +# ppanhh , 2013 msgid "" msgstr "" "Project-Id-Version: dupeGuru\n" -"PO-Revision-Date: 2013-08-04 03:40+0000\n" -"Last-Translator: ppanhh \n" +"PO-Revision-Date: 2013-12-07 15:22+0000\n" +"Last-Translator: hsoft \n" "Language-Team: Vietnamese (http://www.transifex.com/projects/p/dupeguru/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" -#: core/app.py:40 +#: core/app.py:39 msgid "There are no marked duplicates. Nothing has been done." msgstr "" "Không có phần đánh dấu nào trùng nhau. Vẫn chưa thực hiện thao tác nào." -#: core/app.py:41 +#: core/app.py:40 msgid "There are no selected duplicates. Nothing has been done." msgstr "" "Không có phần đánh dấu nào trùng nhau. Vẫn chưa thực hiện thao tác nào." -#: core/app.py:42 +#: core/app.py:41 msgid "" "You're about to open many files at once. Depending on what those files are " "opened with, doing so can create quite a mess. Continue?" @@ -29,37 +29,31 @@ msgstr "" "Bạn chuẩn bị mở nhiều tập tin cùng lúc. Dựa trên chương trình các tập tin " "được mở, thao tác này có thể gây ra trạng thái lộn xộn. Vẫn muốn tiếp tục?" -#: core/app.py:58 +#: core/app.py:57 msgid "Scanning for duplicates" msgstr "Quét các phần trùng nhau" -#: core/app.py:59 +#: core/app.py:58 msgid "Loading" msgstr "Đang tải" -#: core/app.py:60 +#: core/app.py:59 msgid "Moving" msgstr "Đang di chuyển" -#: core/app.py:61 +#: core/app.py:60 msgid "Copying" msgstr "Đang sao chép" -#: core/app.py:62 +#: core/app.py:61 msgid "Sending to Trash" msgstr "Đang gửi vào thùng rác" -#: core/app.py:65 +#: core/app.py:64 msgid "Sending files to the recycle bin" msgstr "Đang chuyển các tập tin vào thùng rác trong Windows" -#: core/app.py:110 -msgid "will only be able to delete, move or copy 10 duplicates at once" -msgstr "" -"chỉ được xóa, di chuyển hoặc sao chép 10 thành phần trùng nhau trong cùng " -"một lần" - -#: core/app.py:246 +#: core/app.py:295 msgid "" "A previous action is still hanging in there. You can't start a new one yet. " "Wait a few seconds, then try again." @@ -67,40 +61,32 @@ msgstr "" "Hiện đã có một tiến trình đang được tiến hành. Bạn không thể bắt đầu một " "phần khác. Hãy đợi trong vài giây, và sau đó thử lại lần nữa." -#: core/app.py:254 +#: core/app.py:302 msgid "No duplicates found." msgstr "Không tìm thấy thành phần trùng nhau." -#: core/app.py:267 +#: core/app.py:315 msgid "All marked files were copied successfully." msgstr "Tất cả tập tin được đánh dấu đã được sao chép thành công." -#: core/app.py:268 +#: core/app.py:316 msgid "All marked files were moved successfully." msgstr "Tất cả các tập tin được đánh dấu đã được di chuyển thành công." -#: core/app.py:269 +#: core/app.py:317 msgid "All marked files were successfully sent to Trash." msgstr "" "Tất cả các tập tin được đánh dấu đã được gửi đến Thùng Rác thành công." -#: core/app.py:296 -msgid "" -"You cannot delete, move or copy more than 10 duplicates at once in demo " -"mode." -msgstr "" -"Bạn không thể xóa, di chuyển hoặc sao chép nhiều hơn 10 đối tượng trùng nhau" -" trong cùng một lần ở chế độ xài thử." - -#: core/app.py:307 +#: core/app.py:354 msgid "'{}' already is in the list." msgstr "'{}' đã tồn tại trong danh sách." -#: core/app.py:309 +#: core/app.py:356 msgid "'{}' does not exist." msgstr "'{}' không tồn tại." -#: core/app.py:316 +#: core/app.py:365 msgid "" "All selected %d matches are going to be ignored in all subsequent scans. " "Continue?" @@ -108,61 +94,61 @@ msgstr "" "Các phần được chọn %d khớp với nhau sẽ được bỏ qua trong các lần quét sau. " "Tiếp tục?" -#: core/app.py:376 +#: core/app.py:431 msgid "copy" msgstr "sao chép" -#: core/app.py:376 +#: core/app.py:431 msgid "move" msgstr "di chuyển" -#: core/app.py:377 +#: core/app.py:432 msgid "Select a directory to {} marked files to" msgstr "Chọn một thư mục để {} các tập tin được đánh dấu đến" -#: core/app.py:403 +#: core/app.py:469 msgid "Select a destination for your exported CSV" msgstr "Chọn một điểm xuất dữ liệu dạng CSV" -#: core/app.py:428 +#: core/app.py:494 msgid "You have no custom command set up. Set it up in your preferences." msgstr "" "Bạn vẫn chưa chỉnh sửa phần thiết lập dòng lệnh. Hãy sử dụng tính năng này " "trong phần tùy biến của bạn." -#: core/app.py:535 core/app.py:546 +#: core/app.py:646 core/app.py:659 msgid "You are about to remove %d files from results. Continue?" msgstr "Bạn chuẩn bị loại bỏ %d tập tin từ phần kết quả. Tiếp tục?" -#: core/app.py:566 +#: core/app.py:693 msgid "{} duplicate groups were changed by the re-prioritization." msgstr "{} các nhóm trùng nhau đã được thay đổi bởi thứ tự-tái ưu tiên." -#: core/app.py:586 +#: core/app.py:721 msgid "Collecting files to scan" msgstr "Đang thu thập các tập tin để quét" -#: core/app.py:597 +#: core/app.py:732 msgid "The selected directories contain no scannable file." msgstr "Các thứ mục được chọn chứa các tập tin không thể quét được." -#: core/app.py:636 +#: core/app.py:773 msgid "%s (%d discarded)" msgstr "%s (%d bị bỏ qua)" -#: core/engine.py:178 core/engine.py:215 +#: core/engine.py:220 core/engine.py:265 msgid "0 matches found" msgstr "đã tìm thấy 0 phần khớp nhau" -#: core/engine.py:196 core/engine.py:223 +#: core/engine.py:238 core/engine.py:273 msgid "%d matches found" msgstr "đã tìm thấy %d phần khớp nhau" -#: core/engine.py:208 core/scanner.py:79 +#: core/engine.py:258 core/scanner.py:79 msgid "Read size of %d/%d files" msgstr "Đọc kích thước của các tập tin %d/%d" -#: core/engine.py:361 +#: core/engine.py:464 msgid "Grouped %d/%d matches" msgstr "Đã nhóm %d/%d phần khớp nhau" @@ -210,11 +196,11 @@ msgstr "Mới nhất" msgid "Oldest" msgstr "Cũ nhất" -#: core/results.py:113 +#: core/results.py:126 msgid "%d / %d (%s / %s) duplicates marked." msgstr "%d / %d (%s / %s) phần trùng nhau đã được đánh dấu." -#: core/results.py:120 +#: core/results.py:133 msgid " filter: %s" msgstr " bộ lọc: %s" diff --git a/locale/vi/LC_MESSAGES/ui.po b/locale/vi/LC_MESSAGES/ui.po index cc2e9e76..1a31e479 100644 --- a/locale/vi/LC_MESSAGES/ui.po +++ b/locale/vi/LC_MESSAGES/ui.po @@ -1,9 +1,10 @@ # Translators: -# ppanhh , 2013 +# ppanhh , 2013 +# ppanhh , 2013 msgid "" msgstr "" "Project-Id-Version: dupeGuru\n" -"PO-Revision-Date: 2013-08-03 20:36+0000\n" +"PO-Revision-Date: 2013-12-07 15:22+0000\n" "Last-Translator: hsoft \n" "Language-Team: Vietnamese (http://www.transifex.com/projects/p/dupeguru/language/vi/)\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -19,101 +20,93 @@ msgstr "Đang loại bỏ các track bị lỗi khỏi thư viện iTunes của msgid "Scanning the iTunes Library" msgstr "Đang quét thư viện iTunes" -#: cocoa/inter/app_me.py:161 cocoa/inter/app_pe.py:186 +#: cocoa/inter/app_me.py:160 cocoa/inter/app_pe.py:194 msgid "Sending dupes to the Trash" msgstr "Đang gửi các đối tượng bị lỗi đến Thùng Rác" -#: cocoa/inter/app_me.py:163 +#: cocoa/inter/app_me.py:162 msgid "Talking to iTunes. Don't touch it!" msgstr "Đang giao tiếp với iTunes. Đừng chạm vào!" -#: cocoa/inter/app_me.py:189 -msgid "" -"There were communication problems with iTunes. The operation couldn't be " -"completed." -msgstr "" -"Xảy ra vấn đề khi giao tiếp với iTunes. Tiến trình này không thể được thực " -"hiện." - -#: cocoa/inter/app_me.py:195 +#: cocoa/inter/app_me.py:197 msgid "" "Your iTunes Library contains %d dead tracks ready to be removed. Continue?" msgstr "" "Thư viện iTunes của bạn chứa %d các track bị lỗi và đang chuẩn bị được loại " "bỏ. Tiếp tục?" -#: cocoa/inter/app_me.py:199 +#: cocoa/inter/app_me.py:201 msgid "You have no dead tracks in your iTunes Library" msgstr "Bạn không có track bị lỗi trong thư viện iTunes" -#: cocoa/inter/app_me.py:217 +#: cocoa/inter/app_me.py:219 msgid "The iTunes application couldn't be found." msgstr "Không tìm thấy ứng dụng iTunes." -#: cocoa/inter/app_pe.py:188 +#: cocoa/inter/app_pe.py:196 msgid "Talking to iPhoto. Don't touch it!" msgstr "Đang giao tiếp với iPhoto. Đừng chạm vào!" -#: cocoa/inter/app_pe.py:197 +#: cocoa/inter/app_pe.py:205 msgid "Talking to Aperture. Don't touch it!" msgstr "Đang giao tiếp với Aperture. Đừng chạm vào!" -#: cocoa/inter/app_pe.py:270 +#: cocoa/inter/app_pe.py:278 msgid "Deleted Aperture photos were sent to a project called \"dupeGuru Trash\"." msgstr "" "Đã xóa các hình chụp thuộc Aperture và được gửi đến dự án có tên \"Thùng rác" " dupeGuru\"." -#: cocoa/inter/app_pe.py:296 +#: cocoa/inter/app_pe.py:304 msgid "The iPhoto application couldn't be found." msgstr "Không tìm thấy ứng dụng iPhoto." -#: qt/base/app.py:95 +#: qt/base/app.py:83 msgid "Quit" msgstr "Thoát" -#: qt/base/app.py:96 qt/base/preferences_dialog.py:123 +#: qt/base/app.py:84 qt/base/preferences_dialog.py:123 msgid "Preferences" msgstr "Tùy biến" -#: qt/base/app.py:97 qt/base/ignore_list_dialog.py:32 +#: qt/base/app.py:85 qt/base/ignore_list_dialog.py:32 #: cocoa/base/en.lproj/Localizable.strings:0 msgid "Ignore List" msgstr "Danh sách bỏ qua" -#: qt/base/app.py:98 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/app.py:86 cocoa/base/en.lproj/Localizable.strings:0 msgid "dupeGuru Help" msgstr "Trợ giúp" -#: qt/base/app.py:99 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/app.py:87 cocoa/base/en.lproj/Localizable.strings:0 msgid "About dupeGuru" msgstr "Dịch bởi Phan Anh" -#: qt/base/app.py:100 +#: qt/base/app.py:88 msgid "Register dupeGuru" msgstr "Đăng ký chương trình" -#: qt/base/app.py:101 +#: qt/base/app.py:89 msgid "Check for Update" msgstr "Kiểm tra cập nhật" -#: qt/base/app.py:102 +#: qt/base/app.py:90 msgid "Open Debug Log" msgstr "Mở nhật trình gỡ rối" -#: qt/base/app.py:257 +#: qt/base/app.py:217 msgid "{} file (*.{})" msgstr "{} tập tin (*.{})" -#: qt/base/deletion_options.py:30 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/deletion_options.py:29 cocoa/base/en.lproj/Localizable.strings:0 msgid "Deletion Options" msgstr "Xóa tùy chọn" -#: qt/base/deletion_options.py:35 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/deletion_options.py:34 cocoa/base/en.lproj/Localizable.strings:0 msgid "Link deleted files" msgstr "Liên kết đến các tập tin đã bị xóa" -#: qt/base/deletion_options.py:37 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/deletion_options.py:36 cocoa/base/en.lproj/Localizable.strings:0 msgid "" "After having deleted a duplicate, place a link targeting the reference file " "to replace the deleted file." @@ -121,19 +114,23 @@ msgstr "" "Sau khi đã xóa một đối tượng bị trùng, đặt một liên kết chỉ thẳng nhằm tham " "chiếu đến tập tin để thay thế tập tin đã được xóa." -#: qt/base/deletion_options.py:42 +#: qt/base/deletion_options.py:41 msgid "Hardlink" msgstr "Liên kết cứng" -#: qt/base/deletion_options.py:42 +#: qt/base/deletion_options.py:41 msgid "Symlink" msgstr "Liên kết biểu tượng" -#: qt/base/deletion_options.py:48 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/deletion_options.py:46 +msgid " (unsupported)" +msgstr " (chưa được hỗ trợ)" + +#: qt/base/deletion_options.py:47 cocoa/base/en.lproj/Localizable.strings:0 msgid "Directly delete files" msgstr "Trực tiếp xóa các tập tin" -#: qt/base/deletion_options.py:50 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/deletion_options.py:49 cocoa/base/en.lproj/Localizable.strings:0 msgid "" "Instead of sending files to trash, delete them directly. This option is " "usually used as a workaround when the normal deletion method doesn't work." @@ -142,11 +139,11 @@ msgstr "" " này thường được dùng trong các môi trường làm việc nơi mà các tác dụng xóa " "những tập tin theo cách thông thường không được áp dụng." -#: qt/base/deletion_options.py:56 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/deletion_options.py:55 cocoa/base/en.lproj/Localizable.strings:0 msgid "Proceed" msgstr "Tiếp tục" -#: qt/base/deletion_options.py:57 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/deletion_options.py:56 cocoa/base/en.lproj/Localizable.strings:0 msgid "Cancel" msgstr "Hủy bỏ" @@ -570,18 +567,18 @@ msgstr "EXIF Timestamp" msgid "Match pictures of different dimensions" msgstr "Chỉ các hình ảnh khớp nhau với các chiều khác nhau" -#: qt/pe/result_window.py:19 qt/pe/result_window.py:24 +#: qt/pe/result_window.py:19 qt/pe/result_window.py:25 #: cocoa/base/en.lproj/Localizable.strings:0 msgid "Clear Picture Cache" msgstr "Dọn dẹp bộ nhớ đệm của hình ảnh" -#: qt/pe/result_window.py:25 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/pe/result_window.py:26 cocoa/base/en.lproj/Localizable.strings:0 msgid "Do you really want to remove all your cached picture analysis?" msgstr "" "Bạn có muốn loại bỏ toàn bộ các phân tích trong bộ nhớ đệm về hình ảnh hay " "không?" -#: qt/pe/result_window.py:28 +#: qt/pe/result_window.py:29 msgid "Picture cache cleared." msgstr "Đã dọn dẹp bộ nhớ đệm xử lý hình ảnh." @@ -813,6 +810,10 @@ msgstr "Biểu hiện" msgid "Reveal Selected in Finder" msgstr "Biểu hiện các phần được chọn trong phần Tìm Kiếm" +#: cocoa/base/en.lproj/Localizable.strings:0 +msgid "Select All" +msgstr "" + #: cocoa/base/en.lproj/Localizable.strings:0 msgid "Send Marked to Trash..." msgstr "Gửi phần được đánh dấu vào Thùng Rác..." @@ -840,7 +841,3 @@ msgstr "Cửa sổ" #: cocoa/base/en.lproj/Localizable.strings:0 msgid "Zoom" msgstr "Phóng to" - -#: qt/base/deletion_options.py:46 -msgid " (unsupported)" -msgstr "" diff --git a/locale/zh_CN/LC_MESSAGES/columns.po b/locale/zh_CN/LC_MESSAGES/columns.po index 99f5ac9c..715cfbf8 100644 --- a/locale/zh_CN/LC_MESSAGES/columns.po +++ b/locale/zh_CN/LC_MESSAGES/columns.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: dupeGuru\n" -"PO-Revision-Date: 2012-09-05 15:22+0000\n" +"PO-Revision-Date: 2013-11-20 11:53+0000\n" "Last-Translator: hsoft \n" "Language-Team: Chinese (China) (http://www.transifex.com/projects/p/dupeguru/language/zh_CN/)\n" "Content-Type: text/plain; charset=UTF-8\n" diff --git a/locale/zh_CN/LC_MESSAGES/core.po b/locale/zh_CN/LC_MESSAGES/core.po index 4a6c9190..9f43f699 100644 --- a/locale/zh_CN/LC_MESSAGES/core.po +++ b/locale/zh_CN/LC_MESSAGES/core.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: dupeGuru\n" -"PO-Revision-Date: 2013-08-03 20:35+0000\n" +"PO-Revision-Date: 2013-12-07 15:22+0000\n" "Last-Translator: hsoft \n" "Language-Team: Chinese (China) (http://www.transifex.com/projects/p/dupeguru/language/zh_CN/)\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -10,143 +10,133 @@ msgstr "" "Language: zh_CN\n" "Plural-Forms: nplurals=1; plural=0;\n" -#: core/app.py:40 +#: core/app.py:39 msgid "There are no marked duplicates. Nothing has been done." msgstr "" -#: core/app.py:41 +#: core/app.py:40 msgid "There are no selected duplicates. Nothing has been done." msgstr "" -#: core/app.py:42 +#: core/app.py:41 msgid "" "You're about to open many files at once. Depending on what those files are " "opened with, doing so can create quite a mess. Continue?" msgstr "" -#: core/app.py:58 +#: core/app.py:57 msgid "Scanning for duplicates" msgstr "重复文件扫描中" -#: core/app.py:59 +#: core/app.py:58 msgid "Loading" msgstr "载入中" -#: core/app.py:60 +#: core/app.py:59 msgid "Moving" msgstr "移动中" -#: core/app.py:61 +#: core/app.py:60 msgid "Copying" msgstr "复制中" -#: core/app.py:62 +#: core/app.py:61 msgid "Sending to Trash" msgstr "移到垃圾桶" -#: core/app.py:65 +#: core/app.py:64 msgid "Sending files to the recycle bin" msgstr "将文件移到回收站" -#: core/app.py:110 -msgid "will only be able to delete, move or copy 10 duplicates at once" -msgstr "" - -#: core/app.py:246 +#: core/app.py:295 msgid "" "A previous action is still hanging in there. You can't start a new one yet. " "Wait a few seconds, then try again." msgstr "目前还有任务在执行,新任务无法开启。请等待几秒钟后再重新试一次。" -#: core/app.py:254 +#: core/app.py:302 msgid "No duplicates found." msgstr "没有找到重复文件。" -#: core/app.py:267 +#: core/app.py:315 msgid "All marked files were copied successfully." msgstr "" -#: core/app.py:268 +#: core/app.py:316 msgid "All marked files were moved successfully." msgstr "" -#: core/app.py:269 +#: core/app.py:317 msgid "All marked files were successfully sent to Trash." msgstr "" -#: core/app.py:296 -msgid "" -"You cannot delete, move or copy more than 10 duplicates at once in demo " -"mode." -msgstr "" - -#: core/app.py:307 +#: core/app.py:354 msgid "'{}' already is in the list." msgstr "" -#: core/app.py:309 +#: core/app.py:356 msgid "'{}' does not exist." msgstr "" -#: core/app.py:316 +#: core/app.py:365 msgid "" "All selected %d matches are going to be ignored in all subsequent scans. " "Continue?" msgstr "目前已选的 %d 个匹配项将在后续的扫描中被忽略。继续吗?" -#: core/app.py:376 +#: core/app.py:431 msgid "copy" msgstr "复制" -#: core/app.py:376 +#: core/app.py:431 msgid "move" msgstr "移动" -#: core/app.py:377 +#: core/app.py:432 msgid "Select a directory to {} marked files to" msgstr "选择一个文件夹将标记的 {} 个文件进行..." -#: core/app.py:403 +#: core/app.py:469 msgid "Select a destination for your exported CSV" msgstr "" -#: core/app.py:428 +#: core/app.py:494 msgid "You have no custom command set up. Set it up in your preferences." msgstr "你没有设定自定义命令。请在首选项中进行设定。" -#: core/app.py:535 core/app.py:546 +#: core/app.py:646 core/app.py:659 msgid "You are about to remove %d files from results. Continue?" msgstr "你将从结果中移除 %d 个文件。继续吗?" -#: core/app.py:566 +#: core/app.py:693 msgid "{} duplicate groups were changed by the re-prioritization." msgstr "" -#: core/app.py:586 +#: core/app.py:721 msgid "Collecting files to scan" msgstr "收集文件以备扫描" -#: core/app.py:597 +#: core/app.py:732 msgid "The selected directories contain no scannable file." msgstr "所选文件夹中不包含可供扫描的文件。" -#: core/app.py:636 +#: core/app.py:773 msgid "%s (%d discarded)" msgstr "%s (%d 无效)" -#: core/engine.py:178 core/engine.py:215 +#: core/engine.py:220 core/engine.py:265 msgid "0 matches found" msgstr "未找到匹配项" -#: core/engine.py:196 core/engine.py:223 +#: core/engine.py:238 core/engine.py:273 msgid "%d matches found" msgstr "找到 %d 个匹配项" -#: core/engine.py:208 core/scanner.py:79 +#: core/engine.py:258 core/scanner.py:79 msgid "Read size of %d/%d files" msgstr "读取 %d/%d 文件大小" -#: core/engine.py:361 +#: core/engine.py:464 msgid "Grouped %d/%d matches" msgstr "%d/%d 匹配项组合在一起" @@ -194,11 +184,11 @@ msgstr "" msgid "Oldest" msgstr "" -#: core/results.py:113 +#: core/results.py:126 msgid "%d / %d (%s / %s) duplicates marked." msgstr "已标记 %d / %d (%s / %s) 个重复项。" -#: core/results.py:120 +#: core/results.py:133 msgid " filter: %s" msgstr " 筛选: %s" diff --git a/locale/zh_CN/LC_MESSAGES/ui.po b/locale/zh_CN/LC_MESSAGES/ui.po index c371d1d1..5d625f4d 100644 --- a/locale/zh_CN/LC_MESSAGES/ui.po +++ b/locale/zh_CN/LC_MESSAGES/ui.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: dupeGuru\n" -"PO-Revision-Date: 2013-08-03 20:36+0000\n" +"PO-Revision-Date: 2013-12-07 15:22+0000\n" "Last-Translator: hsoft \n" "Language-Team: Chinese (China) (http://www.transifex.com/projects/p/dupeguru/language/zh_CN/)\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -18,124 +18,122 @@ msgstr "从你的iTunes库中移除无效的音轨" msgid "Scanning the iTunes Library" msgstr "正在扫描iTunes库" -#: cocoa/inter/app_me.py:161 cocoa/inter/app_pe.py:186 +#: cocoa/inter/app_me.py:160 cocoa/inter/app_pe.py:194 msgid "Sending dupes to the Trash" msgstr "将重复文件移到垃圾桶" -#: cocoa/inter/app_me.py:163 +#: cocoa/inter/app_me.py:162 msgid "Talking to iTunes. Don't touch it!" msgstr "" -#: cocoa/inter/app_me.py:189 -msgid "" -"There were communication problems with iTunes. The operation couldn't be " -"completed." -msgstr "" - -#: cocoa/inter/app_me.py:195 +#: cocoa/inter/app_me.py:197 msgid "" "Your iTunes Library contains %d dead tracks ready to be removed. Continue?" msgstr "" "Your iTunes Library contains %d dead tracks ready to be removed. Continue?" -#: cocoa/inter/app_me.py:199 +#: cocoa/inter/app_me.py:201 msgid "You have no dead tracks in your iTunes Library" msgstr "You have no dead tracks in your iTunes Library" -#: cocoa/inter/app_me.py:217 +#: cocoa/inter/app_me.py:219 msgid "The iTunes application couldn't be found." msgstr "" -#: cocoa/inter/app_pe.py:188 +#: cocoa/inter/app_pe.py:196 msgid "Talking to iPhoto. Don't touch it!" msgstr "" -#: cocoa/inter/app_pe.py:197 +#: cocoa/inter/app_pe.py:205 msgid "Talking to Aperture. Don't touch it!" msgstr "" -#: cocoa/inter/app_pe.py:270 +#: cocoa/inter/app_pe.py:278 msgid "Deleted Aperture photos were sent to a project called \"dupeGuru Trash\"." msgstr "" -#: cocoa/inter/app_pe.py:296 +#: cocoa/inter/app_pe.py:304 msgid "The iPhoto application couldn't be found." msgstr "The iPhoto application couldn't be found." -#: qt/base/app.py:95 +#: qt/base/app.py:83 msgid "Quit" msgstr "退出" -#: qt/base/app.py:96 qt/base/preferences_dialog.py:123 +#: qt/base/app.py:84 qt/base/preferences_dialog.py:123 msgid "Preferences" msgstr "首选项" -#: qt/base/app.py:97 qt/base/ignore_list_dialog.py:32 +#: qt/base/app.py:85 qt/base/ignore_list_dialog.py:32 #: cocoa/base/en.lproj/Localizable.strings:0 msgid "Ignore List" msgstr "" -#: qt/base/app.py:98 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/app.py:86 cocoa/base/en.lproj/Localizable.strings:0 msgid "dupeGuru Help" msgstr "dupeGuru帮助" -#: qt/base/app.py:99 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/app.py:87 cocoa/base/en.lproj/Localizable.strings:0 msgid "About dupeGuru" msgstr "关于dupeGuru" -#: qt/base/app.py:100 +#: qt/base/app.py:88 msgid "Register dupeGuru" msgstr "注册dupeGuru" -#: qt/base/app.py:101 +#: qt/base/app.py:89 msgid "Check for Update" msgstr "检查更新" -#: qt/base/app.py:102 +#: qt/base/app.py:90 msgid "Open Debug Log" msgstr "打开调试记录" -#: qt/base/app.py:257 +#: qt/base/app.py:217 msgid "{} file (*.{})" msgstr "" -#: qt/base/deletion_options.py:30 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/deletion_options.py:29 cocoa/base/en.lproj/Localizable.strings:0 msgid "Deletion Options" msgstr "" -#: qt/base/deletion_options.py:35 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/deletion_options.py:34 cocoa/base/en.lproj/Localizable.strings:0 msgid "Link deleted files" msgstr "" -#: qt/base/deletion_options.py:37 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/deletion_options.py:36 cocoa/base/en.lproj/Localizable.strings:0 msgid "" "After having deleted a duplicate, place a link targeting the reference file " "to replace the deleted file." msgstr "" -#: qt/base/deletion_options.py:42 +#: qt/base/deletion_options.py:41 msgid "Hardlink" msgstr "" -#: qt/base/deletion_options.py:42 +#: qt/base/deletion_options.py:41 msgid "Symlink" msgstr "" -#: qt/base/deletion_options.py:48 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/deletion_options.py:46 +msgid " (unsupported)" +msgstr "" + +#: qt/base/deletion_options.py:47 cocoa/base/en.lproj/Localizable.strings:0 msgid "Directly delete files" msgstr "" -#: qt/base/deletion_options.py:50 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/deletion_options.py:49 cocoa/base/en.lproj/Localizable.strings:0 msgid "" "Instead of sending files to trash, delete them directly. This option is " "usually used as a workaround when the normal deletion method doesn't work." msgstr "" -#: qt/base/deletion_options.py:56 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/deletion_options.py:55 cocoa/base/en.lproj/Localizable.strings:0 msgid "Proceed" msgstr "" -#: qt/base/deletion_options.py:57 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/base/deletion_options.py:56 cocoa/base/en.lproj/Localizable.strings:0 msgid "Cancel" msgstr "Cancel" @@ -553,16 +551,16 @@ msgstr "EXIF Timestamp" msgid "Match pictures of different dimensions" msgstr "匹配不同规格的图像" -#: qt/pe/result_window.py:19 qt/pe/result_window.py:24 +#: qt/pe/result_window.py:19 qt/pe/result_window.py:25 #: cocoa/base/en.lproj/Localizable.strings:0 msgid "Clear Picture Cache" msgstr "清空图片缓存" -#: qt/pe/result_window.py:25 cocoa/base/en.lproj/Localizable.strings:0 +#: qt/pe/result_window.py:26 cocoa/base/en.lproj/Localizable.strings:0 msgid "Do you really want to remove all your cached picture analysis?" msgstr "确定要移除所有缓存图片?" -#: qt/pe/result_window.py:28 +#: qt/pe/result_window.py:29 msgid "Picture cache cleared." msgstr "图片缓存已清空。" @@ -794,6 +792,10 @@ msgstr "" msgid "Reveal Selected in Finder" msgstr "Reveal Selected in Finder" +#: cocoa/base/en.lproj/Localizable.strings:0 +msgid "Select All" +msgstr "" + #: cocoa/base/en.lproj/Localizable.strings:0 msgid "Send Marked to Trash..." msgstr "" @@ -821,7 +823,3 @@ msgstr "Window" #: cocoa/base/en.lproj/Localizable.strings:0 msgid "Zoom" msgstr "Zoom" - -#: qt/base/deletion_options.py:46 -msgid " (unsupported)" -msgstr "" diff --git a/qt/base/app.py b/qt/base/app.py index 01de41aa..974e8191 100644 --- a/qt/base/app.py +++ b/qt/base/app.py @@ -7,7 +7,6 @@ # http://www.hardcoded.net/licenses/bsd_license import sys -import os import os.path as op from PyQt4.QtCore import QTimer, QObject, QCoreApplication, QUrl, QProcess, SIGNAL, pyqtSignal @@ -15,11 +14,11 @@ from PyQt4.QtGui import QDesktopServices, QFileDialog, QDialog, QMessageBox, QAp from hscommon.trans import trget from hscommon.plat import ISLINUX +from hscommon import desktop from qtlib.about_box import AboutBox from qtlib.recent import Recent -from qtlib.reg import Registration -from qtlib.util import createActions, getAppData +from qtlib.util import createActions from qtlib.progress_window import ProgressWindow from . import platform @@ -46,7 +45,7 @@ class DupeGuru(QObject): QObject.__init__(self) self.prefs = self.PREFERENCES_CLASS() self.prefs.load() - self.model = self.MODELCLASS(view=self, appdata=getAppData()) + self.model = self.MODELCLASS(view=self) self._setup() self.prefsChanged.emit(self.prefs) @@ -85,7 +84,6 @@ class DupeGuru(QObject): ('actionIgnoreList', '', '', tr("Ignore List"), self.ignoreListTriggered), ('actionShowHelp', 'F1', '', tr("dupeGuru Help"), self.showHelpTriggered), ('actionAbout', '', '', tr("About dupeGuru"), self.showAboutBoxTriggered), - ('actionRegister', '', '', tr("Register dupeGuru"), self.registerTriggered), ('actionCheckForUpdate', '', '', tr("Check for Update"), self.checkForUpdateTriggered), ('actionOpenDebugLog', '', '', tr("Open Debug Log"), self.openDebugLogTriggered), ] @@ -108,10 +106,6 @@ class DupeGuru(QObject): def remove_selected(self): self.model.remove_selected(self) - def askForRegCode(self): - reg = Registration(self.model) - reg.ask_for_code() - def confirm(self, title, msg, default_button=QMessageBox.Yes): active = QApplication.activeWindow() buttons = QMessageBox.Yes | QMessageBox.No @@ -133,7 +127,6 @@ class DupeGuru(QObject): #--- Events def finishedLaunching(self): - self.model.initial_registration_setup() if sys.getfilesystemencoding() == 'ascii': # No need to localize this, it's a debugging message. msg = "Something is wrong with the way your system locale is set. If the files you're "\ @@ -154,7 +147,7 @@ class DupeGuru(QObject): def openDebugLogTriggered(self): debugLogPath = op.join(self.model.appdata, 'debug.log') - self.open_path(debugLogPath) + desktop.open_path(debugLogPath) def preferencesTriggered(self): self.preferences_dialog.load() @@ -168,10 +161,6 @@ class DupeGuru(QObject): def quitTriggered(self): self.directories_dialog.close() - def registerTriggered(self): - reg = Registration(self.model) - reg.ask_for_code() - def showAboutBoxTriggered(self): self.about_box.show() @@ -181,30 +170,12 @@ class DupeGuru(QObject): QDesktopServices.openUrl(url) #--- model --> view - @staticmethod - def open_path(path): - url = QUrl.fromLocalFile(str(path)) - QDesktopServices.openUrl(url) - - @staticmethod - def reveal_path(path): - DupeGuru.open_path(path[:-1]) - def get_default(self, key): return self.prefs.get_value(key) def set_default(self, key, value): self.prefs.set_value(key, value) - def setup_as_registered(self): - self.actionRegister.setVisible(False) - self.about_box.registerButton.hide() - self.about_box.registeredEmailLabel.setText(self.model.registration_email) - - def show_demo_nag(self, prompt): - reg = Registration(self.model) - reg.show_demo_nag(prompt) - def show_message(self, msg): window = QApplication.activeWindow() QMessageBox.information(window, '', msg) @@ -212,10 +183,6 @@ class DupeGuru(QObject): def ask_yes_no(self, prompt): return self.confirm('', prompt) - def open_url(self, url): - url = QUrl(url) - QDesktopServices.openUrl(url) - def show_results_window(self): self.showResultsWindow() diff --git a/qt/base/deletion_options.py b/qt/base/deletion_options.py index e42234d1..1ef204ab 100644 --- a/qt/base/deletion_options.py +++ b/qt/base/deletion_options.py @@ -22,6 +22,7 @@ class DeletionOptions(QDialog): self._setupUi() self.model.view = self + self.linkCheckbox.stateChanged.connect(self.linkCheckboxChanged) self.buttonBox.accepted.connect(self.accept) self.buttonBox.rejected.connect(self.reject) @@ -42,7 +43,6 @@ class DeletionOptions(QDialog): self.verticalLayout.addWidget(self.linkTypeRadio) if not self.model.supports_links(): self.linkCheckbox.setEnabled(False) - self.linkTypeRadio.setEnabled(False) self.linkCheckbox.setText(self.linkCheckbox.text() + tr(" (unsupported)")) self.directCheckbox = QCheckBox(tr("Directly delete files")) self.verticalLayout.addWidget(self.directCheckbox) @@ -56,8 +56,12 @@ class DeletionOptions(QDialog): self.buttonBox.addButton(tr("Cancel"), QDialogButtonBox.RejectRole) self.verticalLayout.addWidget(self.buttonBox) + #--- Signals + def linkCheckboxChanged(self, changed: int): + self.model.link_deleted = bool(changed) + #--- model --> view - def update_msg(self, msg): + def update_msg(self, msg: str): self.msgLabel.setText(msg) def show(self): @@ -70,3 +74,6 @@ class DeletionOptions(QDialog): self.model.direct = self.directCheckbox.isChecked() return result == QDialog.Accepted + def set_hardlink_option_enabled(self, is_enabled: bool): + self.linkTypeRadio.setEnabled(is_enabled) + diff --git a/qt/base/directories_dialog.py b/qt/base/directories_dialog.py index 3e4ea3ba..56918779 100644 --- a/qt/base/directories_dialog.py +++ b/qt/base/directories_dialog.py @@ -81,7 +81,6 @@ class DirectoriesDialog(QMainWindow): self.menuView.addAction(self.actionShowResultsWindow) self.menuView.addAction(self.app.actionIgnoreList) self.menuHelp.addAction(self.app.actionShowHelp) - self.menuHelp.addAction(self.app.actionRegister) self.menuHelp.addAction(self.app.actionCheckForUpdate) self.menuHelp.addAction(self.app.actionOpenDebugLog) self.menuHelp.addAction(self.app.actionAbout) diff --git a/qt/base/result_window.py b/qt/base/result_window.py index 1ee10b25..a7e65f55 100644 --- a/qt/base/result_window.py +++ b/qt/base/result_window.py @@ -111,7 +111,6 @@ class ResultWindow(QMainWindow): self.menuView.addAction(self.app.actionIgnoreList) self.menuView.addAction(self.app.actionPreferences) self.menuHelp.addAction(self.app.actionShowHelp) - self.menuHelp.addAction(self.app.actionRegister) self.menuHelp.addAction(self.app.actionCheckForUpdate) self.menuHelp.addAction(self.app.actionOpenDebugLog) self.menuHelp.addAction(self.app.actionAbout) diff --git a/qt/run_template.py b/qt/run_template.py index 73da88c3..0ec20a27 100644 --- a/qt/run_template.py +++ b/qt/run_template.py @@ -37,7 +37,5 @@ if __name__ == "__main__": from qt.{edition}.app import DupeGuru app.setWindowIcon(QIcon(QPixmap(":/{0}".format(DupeGuru.LOGO_NAME)))) dgapp = DupeGuru() - if not ISWINDOWS: - dgapp.model.registered = True install_excepthook() sys.exit(app.exec_()) diff --git a/qtlib/about_box.py b/qtlib/about_box.py index 00632aad..323faf65 100644 --- a/qtlib/about_box.py +++ b/qtlib/about_box.py @@ -15,17 +15,14 @@ from hscommon.trans import trget tr = trget('qtlib') class AboutBox(QDialog): - def __init__(self, parent, app, withreg=True): + def __init__(self, parent, app): flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint | Qt.MSWindowsFixedSizeDialogHint QDialog.__init__(self, parent, flags) self.app = app - self.withreg = withreg self._setupUi() self.buttonBox.accepted.connect(self.accept) self.buttonBox.rejected.connect(self.reject) - if self.withreg: - self.buttonBox.clicked.connect(self.buttonClicked) def _setupUi(self): self.setWindowTitle(tr("About {}").format(QCoreApplication.instance().applicationName())) @@ -59,23 +56,12 @@ class AboutBox(QDialog): font.setBold(True) self.label.setFont(font) self.verticalLayout.addWidget(self.label) - self.registeredEmailLabel = QLabel(self) - if self.withreg: - self.registeredEmailLabel.setText(tr("UNREGISTERED")) - self.verticalLayout.addWidget(self.registeredEmailLabel) self.buttonBox = QDialogButtonBox(self) self.buttonBox.setOrientation(Qt.Horizontal) self.buttonBox.setStandardButtons(QDialogButtonBox.Ok) - if self.withreg: - self.registerButton = self.buttonBox.addButton(tr("Register"), QDialogButtonBox.ActionRole) self.verticalLayout.addWidget(self.buttonBox) self.horizontalLayout.addLayout(self.verticalLayout) - #--- Events - def buttonClicked(self, button): - if button is self.registerButton: - self.app.askForRegCode() - if __name__ == '__main__': import sys diff --git a/qtlib/locale/cs/LC_MESSAGES/qtlib.po b/qtlib/locale/cs/LC_MESSAGES/qtlib.po index ee24907b..cfda09ac 100644 --- a/qtlib/locale/cs/LC_MESSAGES/qtlib.po +++ b/qtlib/locale/cs/LC_MESSAGES/qtlib.po @@ -9,26 +9,18 @@ msgstr "" "Language: cs\n" "Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" -#: qtlib/about_box.py:31 +#: qtlib/about_box.py:28 msgid "About {}" msgstr "" -#: qtlib/about_box.py:51 +#: qtlib/about_box.py:48 msgid "Version {}" msgstr "" -#: qtlib/about_box.py:55 +#: qtlib/about_box.py:52 msgid "Copyright Hardcoded Software 2013" msgstr "" -#: qtlib/about_box.py:64 -msgid "UNREGISTERED" -msgstr "" - -#: qtlib/about_box.py:70 -msgid "Register" -msgstr "" - #: qtlib/error_report_dialog.py:38 msgid "Error Report" msgstr "" @@ -105,58 +97,10 @@ msgstr "" msgid "Clear List" msgstr "" -#: qtlib/reg_demo_dialog.py:35 -msgid "$appname is Fairware" -msgstr "" - -#: qtlib/reg_demo_dialog.py:49 -msgid "Try" -msgstr "" - -#: qtlib/reg_demo_dialog.py:52 -msgid "Enter Key" -msgstr "" - -#: qtlib/reg_demo_dialog.py:55 -msgid "Buy" -msgstr "" - -#: qtlib/reg_demo_dialog.py:57 -msgid "Fairware?" -msgstr "" - -#: qtlib/reg_submit_dialog.py:31 -msgid "Enter your registration key" -msgstr "" - -#: qtlib/reg_submit_dialog.py:36 -msgid "" -"Type the key you received when you contributed to $appname, as well as the " -"e-mail used as a reference for the purchase." -msgstr "" -"Type the key you received when you contributed to $appname, as well as the " -"e-mail used as a reference for the purchase." - -#: qtlib/reg_submit_dialog.py:48 -msgid "Registration key:" -msgstr "" - -#: qtlib/reg_submit_dialog.py:51 -msgid "Registered e-mail:" -msgstr "" - -#: qtlib/reg_submit_dialog.py:60 -msgid "Contribute" -msgstr "Přispět" - -#: qtlib/reg_submit_dialog.py:71 -msgid "Cancel" -msgstr "Zrušit" - -#: qtlib/reg_submit_dialog.py:80 -msgid "Submit" -msgstr "" - #: qtlib/search_edit.py:41 msgid "Search..." msgstr "" + +#: qtlib/preferences.py:29 +msgid "Vietnamese" +msgstr "" diff --git a/qtlib/locale/de/LC_MESSAGES/qtlib.po b/qtlib/locale/de/LC_MESSAGES/qtlib.po index b0688648..585a65d0 100644 --- a/qtlib/locale/de/LC_MESSAGES/qtlib.po +++ b/qtlib/locale/de/LC_MESSAGES/qtlib.po @@ -9,26 +9,18 @@ msgstr "" "Language: de\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: qtlib/about_box.py:31 +#: qtlib/about_box.py:28 msgid "About {}" msgstr "" -#: qtlib/about_box.py:51 +#: qtlib/about_box.py:48 msgid "Version {}" msgstr "" -#: qtlib/about_box.py:55 +#: qtlib/about_box.py:52 msgid "Copyright Hardcoded Software 2013" msgstr "" -#: qtlib/about_box.py:64 -msgid "UNREGISTERED" -msgstr "" - -#: qtlib/about_box.py:70 -msgid "Register" -msgstr "" - #: qtlib/error_report_dialog.py:38 msgid "Error Report" msgstr "" @@ -105,58 +97,10 @@ msgstr "" msgid "Clear List" msgstr "" -#: qtlib/reg_demo_dialog.py:35 -msgid "$appname is Fairware" -msgstr "$appname ist Fairware" - -#: qtlib/reg_demo_dialog.py:49 -msgid "Try" -msgstr "Spenden" - -#: qtlib/reg_demo_dialog.py:52 -msgid "Enter Key" -msgstr "Registrieren" - -#: qtlib/reg_demo_dialog.py:55 -msgid "Buy" -msgstr "" - -#: qtlib/reg_demo_dialog.py:57 -msgid "Fairware?" -msgstr "" - -#: qtlib/reg_submit_dialog.py:31 -msgid "Enter your registration key" -msgstr "Geben Sie ihren Schlüssel ein" - -#: qtlib/reg_submit_dialog.py:36 -msgid "" -"Type the key you received when you contributed to $appname, as well as the " -"e-mail used as a reference for the purchase." -msgstr "" -"Geben Sie den empfangenen Schlüssel und die E-Mail-Adresse als Referenz für " -"den Kauf an, wenn Sie für $appname gespendet haben." - -#: qtlib/reg_submit_dialog.py:48 -msgid "Registration key:" -msgstr "Registrierungsschlüssel:" - -#: qtlib/reg_submit_dialog.py:51 -msgid "Registered e-mail:" -msgstr "Registrierte E-Mail:" - -#: qtlib/reg_submit_dialog.py:60 -msgid "Contribute" -msgstr "Spenden" - -#: qtlib/reg_submit_dialog.py:71 -msgid "Cancel" -msgstr "Abbrechen" - -#: qtlib/reg_submit_dialog.py:80 -msgid "Submit" -msgstr "Abschicken" - #: qtlib/search_edit.py:41 msgid "Search..." msgstr "" + +#: qtlib/preferences.py:29 +msgid "Vietnamese" +msgstr "" diff --git a/qtlib/locale/es/LC_MESSAGES/qtlib.po b/qtlib/locale/es/LC_MESSAGES/qtlib.po index 9b7d728b..44dcadb8 100644 --- a/qtlib/locale/es/LC_MESSAGES/qtlib.po +++ b/qtlib/locale/es/LC_MESSAGES/qtlib.po @@ -9,26 +9,18 @@ msgstr "" "Language: es\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: qtlib/about_box.py:31 +#: qtlib/about_box.py:28 msgid "About {}" msgstr "Acerca de {}" -#: qtlib/about_box.py:51 +#: qtlib/about_box.py:48 msgid "Version {}" msgstr "Versión {}" -#: qtlib/about_box.py:55 +#: qtlib/about_box.py:52 msgid "Copyright Hardcoded Software 2013" msgstr "Copyright Hardcoded Software 2013" -#: qtlib/about_box.py:64 -msgid "UNREGISTERED" -msgstr "SIN REGISTRAR" - -#: qtlib/about_box.py:70 -msgid "Register" -msgstr "Registrar" - #: qtlib/error_report_dialog.py:38 msgid "Error Report" msgstr "Informe de error" @@ -109,58 +101,10 @@ msgstr "Español" msgid "Clear List" msgstr "Limpiar lista" -#: qtlib/reg_demo_dialog.py:35 -msgid "$appname is Fairware" -msgstr "$appname es Fairware" - -#: qtlib/reg_demo_dialog.py:49 -msgid "Try" -msgstr "Probar" - -#: qtlib/reg_demo_dialog.py:52 -msgid "Enter Key" -msgstr "Introducir clave" - -#: qtlib/reg_demo_dialog.py:55 -msgid "Buy" -msgstr "Comprar" - -#: qtlib/reg_demo_dialog.py:57 -msgid "Fairware?" -msgstr "¿Fairware?" - -#: qtlib/reg_submit_dialog.py:31 -msgid "Enter your registration key" -msgstr "Introduzca su clave de registro" - -#: qtlib/reg_submit_dialog.py:36 -msgid "" -"Type the key you received when you contributed to $appname, as well as the " -"e-mail used as a reference for the purchase." -msgstr "" -"Escriba la clave que recibió al donar a $appname, así como el correo " -"electrónico que usó durante la compra." - -#: qtlib/reg_submit_dialog.py:48 -msgid "Registration key:" -msgstr "Clave de registro" - -#: qtlib/reg_submit_dialog.py:51 -msgid "Registered e-mail:" -msgstr "Correo electrónico de registro:" - -#: qtlib/reg_submit_dialog.py:60 -msgid "Contribute" -msgstr "Donar" - -#: qtlib/reg_submit_dialog.py:71 -msgid "Cancel" -msgstr "Cancelar" - -#: qtlib/reg_submit_dialog.py:80 -msgid "Submit" -msgstr "Enviar" - #: qtlib/search_edit.py:41 msgid "Search..." msgstr "Búsqueda..." + +#: qtlib/preferences.py:29 +msgid "Vietnamese" +msgstr "" diff --git a/qtlib/locale/fr/LC_MESSAGES/qtlib.po b/qtlib/locale/fr/LC_MESSAGES/qtlib.po index 63bff653..c908be3f 100644 --- a/qtlib/locale/fr/LC_MESSAGES/qtlib.po +++ b/qtlib/locale/fr/LC_MESSAGES/qtlib.po @@ -9,26 +9,18 @@ msgstr "" "Language: fr\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" -#: qtlib/about_box.py:31 +#: qtlib/about_box.py:28 msgid "About {}" msgstr "" -#: qtlib/about_box.py:51 +#: qtlib/about_box.py:48 msgid "Version {}" msgstr "" -#: qtlib/about_box.py:55 +#: qtlib/about_box.py:52 msgid "Copyright Hardcoded Software 2013" msgstr "" -#: qtlib/about_box.py:64 -msgid "UNREGISTERED" -msgstr "" - -#: qtlib/about_box.py:70 -msgid "Register" -msgstr "" - #: qtlib/error_report_dialog.py:38 msgid "Error Report" msgstr "" @@ -105,58 +97,10 @@ msgstr "" msgid "Clear List" msgstr "Vider la liste" -#: qtlib/reg_demo_dialog.py:35 -msgid "$appname is Fairware" -msgstr "$appname est Fairware" - -#: qtlib/reg_demo_dialog.py:49 -msgid "Try" -msgstr "Essayer" - -#: qtlib/reg_demo_dialog.py:52 -msgid "Enter Key" -msgstr "Enregistrer" - -#: qtlib/reg_demo_dialog.py:55 -msgid "Buy" -msgstr "Acheter" - -#: qtlib/reg_demo_dialog.py:57 -msgid "Fairware?" -msgstr "" - -#: qtlib/reg_submit_dialog.py:31 -msgid "Enter your registration key" -msgstr "Entrez votre clé" - -#: qtlib/reg_submit_dialog.py:36 -msgid "" -"Type the key you received when you contributed to $appname, as well as the " -"e-mail used as a reference for the purchase." -msgstr "" -"Entrez la clé que vous avez reçue en contribuant à $appname, ainsi que le " -"courriel utilisé pour la contribution." - -#: qtlib/reg_submit_dialog.py:48 -msgid "Registration key:" -msgstr "Clé d'enregistrement:" - -#: qtlib/reg_submit_dialog.py:51 -msgid "Registered e-mail:" -msgstr "Courriel référence:" - -#: qtlib/reg_submit_dialog.py:60 -msgid "Contribute" -msgstr "Contribuer" - -#: qtlib/reg_submit_dialog.py:71 -msgid "Cancel" -msgstr "Annuler" - -#: qtlib/reg_submit_dialog.py:80 -msgid "Submit" -msgstr "Soumettre" - #: qtlib/search_edit.py:41 msgid "Search..." msgstr "Recherche..." + +#: qtlib/preferences.py:29 +msgid "Vietnamese" +msgstr "" diff --git a/qtlib/locale/hy/LC_MESSAGES/qtlib.po b/qtlib/locale/hy/LC_MESSAGES/qtlib.po index 62039158..12186e3d 100755 --- a/qtlib/locale/hy/LC_MESSAGES/qtlib.po +++ b/qtlib/locale/hy/LC_MESSAGES/qtlib.po @@ -9,26 +9,18 @@ msgstr "" "Language: hy\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: qtlib/about_box.py:31 +#: qtlib/about_box.py:28 msgid "About {}" msgstr "" -#: qtlib/about_box.py:51 +#: qtlib/about_box.py:48 msgid "Version {}" msgstr "" -#: qtlib/about_box.py:55 +#: qtlib/about_box.py:52 msgid "Copyright Hardcoded Software 2013" msgstr "Հեղ. իրավունքը Hardcoded Software 2013" -#: qtlib/about_box.py:64 -msgid "UNREGISTERED" -msgstr "ՉԳՐԱՆՑՎԱԾ" - -#: qtlib/about_box.py:70 -msgid "Register" -msgstr "" - #: qtlib/error_report_dialog.py:38 msgid "Error Report" msgstr "Սխալի զեկույցը" @@ -106,58 +98,10 @@ msgstr "" msgid "Clear List" msgstr "Մաքրել ցանկը" -#: qtlib/reg_demo_dialog.py:35 -msgid "$appname is Fairware" -msgstr "$appname-ը Fairware է" - -#: qtlib/reg_demo_dialog.py:49 -msgid "Try" -msgstr "Փորձել" - -#: qtlib/reg_demo_dialog.py:52 -msgid "Enter Key" -msgstr "Գրել բանալին" - -#: qtlib/reg_demo_dialog.py:55 -msgid "Buy" -msgstr "Գնել" - -#: qtlib/reg_demo_dialog.py:57 -msgid "Fairware?" -msgstr "Fairware է՞" - -#: qtlib/reg_submit_dialog.py:31 -msgid "Enter your registration key" -msgstr "Գրեք գրանցման բանալին" - -#: qtlib/reg_submit_dialog.py:36 -msgid "" -"Type the key you received when you contributed to $appname, as well as the " -"e-mail used as a reference for the purchase." -msgstr "" -"Մուտքագրեք այն բանալին, որը ստացել եք $appname-ին աջակցելիս, քանզի Ձեր էլ. " -"հասցեն օգտագործվել է գնման ժամանակ:" - -#: qtlib/reg_submit_dialog.py:48 -msgid "Registration key:" -msgstr "Գրանցման բանալին." - -#: qtlib/reg_submit_dialog.py:51 -msgid "Registered e-mail:" -msgstr "Գրանցված էլ. հասցեն." - -#: qtlib/reg_submit_dialog.py:60 -msgid "Contribute" -msgstr "Մասնակցել" - -#: qtlib/reg_submit_dialog.py:71 -msgid "Cancel" -msgstr "Չեղարկել" - -#: qtlib/reg_submit_dialog.py:80 -msgid "Submit" -msgstr "Հաստատել" - #: qtlib/search_edit.py:41 msgid "Search..." msgstr "" + +#: qtlib/preferences.py:29 +msgid "Vietnamese" +msgstr "" diff --git a/qtlib/locale/it/LC_MESSAGES/qtlib.po b/qtlib/locale/it/LC_MESSAGES/qtlib.po index 3b4eb2cc..ac346b86 100644 --- a/qtlib/locale/it/LC_MESSAGES/qtlib.po +++ b/qtlib/locale/it/LC_MESSAGES/qtlib.po @@ -9,26 +9,18 @@ msgstr "" "Language: it\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: qtlib/about_box.py:31 +#: qtlib/about_box.py:28 msgid "About {}" msgstr "" -#: qtlib/about_box.py:51 +#: qtlib/about_box.py:48 msgid "Version {}" msgstr "" -#: qtlib/about_box.py:55 +#: qtlib/about_box.py:52 msgid "Copyright Hardcoded Software 2013" msgstr "" -#: qtlib/about_box.py:64 -msgid "UNREGISTERED" -msgstr "" - -#: qtlib/about_box.py:70 -msgid "Register" -msgstr "" - #: qtlib/error_report_dialog.py:38 msgid "Error Report" msgstr "" @@ -105,58 +97,10 @@ msgstr "" msgid "Clear List" msgstr "" -#: qtlib/reg_demo_dialog.py:35 -msgid "$appname is Fairware" -msgstr "" - -#: qtlib/reg_demo_dialog.py:49 -msgid "Try" -msgstr "" - -#: qtlib/reg_demo_dialog.py:52 -msgid "Enter Key" -msgstr "" - -#: qtlib/reg_demo_dialog.py:55 -msgid "Buy" -msgstr "" - -#: qtlib/reg_demo_dialog.py:57 -msgid "Fairware?" -msgstr "" - -#: qtlib/reg_submit_dialog.py:31 -msgid "Enter your registration key" -msgstr "" - -#: qtlib/reg_submit_dialog.py:36 -msgid "" -"Type the key you received when you contributed to $appname, as well as the " -"e-mail used as a reference for the purchase." -msgstr "" -"Type the key you received when you contributed to $appname, as well as the " -"e-mail used as a reference for the purchase." - -#: qtlib/reg_submit_dialog.py:48 -msgid "Registration key:" -msgstr "" - -#: qtlib/reg_submit_dialog.py:51 -msgid "Registered e-mail:" -msgstr "" - -#: qtlib/reg_submit_dialog.py:60 -msgid "Contribute" -msgstr "" - -#: qtlib/reg_submit_dialog.py:71 -msgid "Cancel" -msgstr "" - -#: qtlib/reg_submit_dialog.py:80 -msgid "Submit" -msgstr "" - #: qtlib/search_edit.py:41 msgid "Search..." msgstr "" + +#: qtlib/preferences.py:29 +msgid "Vietnamese" +msgstr "" diff --git a/qtlib/locale/nl/LC_MESSAGES/qtlib.po b/qtlib/locale/nl/LC_MESSAGES/qtlib.po index 519e8311..93032983 100644 --- a/qtlib/locale/nl/LC_MESSAGES/qtlib.po +++ b/qtlib/locale/nl/LC_MESSAGES/qtlib.po @@ -9,26 +9,18 @@ msgstr "" "Language: nl\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: qtlib/about_box.py:31 +#: qtlib/about_box.py:28 msgid "About {}" msgstr "" -#: qtlib/about_box.py:51 +#: qtlib/about_box.py:48 msgid "Version {}" msgstr "" -#: qtlib/about_box.py:55 +#: qtlib/about_box.py:52 msgid "Copyright Hardcoded Software 2013" msgstr "" -#: qtlib/about_box.py:64 -msgid "UNREGISTERED" -msgstr "" - -#: qtlib/about_box.py:70 -msgid "Register" -msgstr "" - #: qtlib/error_report_dialog.py:38 msgid "Error Report" msgstr "" @@ -105,56 +97,10 @@ msgstr "" msgid "Clear List" msgstr "" -#: qtlib/reg_demo_dialog.py:35 -msgid "$appname is Fairware" -msgstr "$appname is Fairware" - -#: qtlib/reg_demo_dialog.py:49 -msgid "Try" -msgstr "Probeer" - -#: qtlib/reg_demo_dialog.py:52 -msgid "Enter Key" -msgstr "Registreren" - -#: qtlib/reg_demo_dialog.py:55 -msgid "Buy" -msgstr "Koop" - -#: qtlib/reg_demo_dialog.py:57 -msgid "Fairware?" -msgstr "" - -#: qtlib/reg_submit_dialog.py:31 -msgid "Enter your registration key" -msgstr "Voer uw registratiesleutel in" - -#: qtlib/reg_submit_dialog.py:36 -msgid "" -"Type the key you received when you contributed to $appname, as well as the " -"e-mail used as a reference for the purchase." -msgstr "" - -#: qtlib/reg_submit_dialog.py:48 -msgid "Registration key:" -msgstr "Registratiesleutel:" - -#: qtlib/reg_submit_dialog.py:51 -msgid "Registered e-mail:" -msgstr "Geregistreerde E-Mail:" - -#: qtlib/reg_submit_dialog.py:60 -msgid "Contribute" -msgstr "Bijdragen" - -#: qtlib/reg_submit_dialog.py:71 -msgid "Cancel" -msgstr "Annuleren" - -#: qtlib/reg_submit_dialog.py:80 -msgid "Submit" -msgstr "Doorgeven" - #: qtlib/search_edit.py:41 msgid "Search..." msgstr "" + +#: qtlib/preferences.py:29 +msgid "Vietnamese" +msgstr "" diff --git a/qtlib/locale/pt_BR/LC_MESSAGES/qtlib.po b/qtlib/locale/pt_BR/LC_MESSAGES/qtlib.po index c861b216..6cfb8b7c 100644 --- a/qtlib/locale/pt_BR/LC_MESSAGES/qtlib.po +++ b/qtlib/locale/pt_BR/LC_MESSAGES/qtlib.po @@ -9,26 +9,18 @@ msgstr "" "Language: pt_BR\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" -#: qtlib/about_box.py:31 +#: qtlib/about_box.py:28 msgid "About {}" msgstr "" -#: qtlib/about_box.py:51 +#: qtlib/about_box.py:48 msgid "Version {}" msgstr "" -#: qtlib/about_box.py:55 +#: qtlib/about_box.py:52 msgid "Copyright Hardcoded Software 2013" msgstr "Direitos Autorais Hardcoded Software 2013" -#: qtlib/about_box.py:64 -msgid "UNREGISTERED" -msgstr "NÃO REGISTRADO" - -#: qtlib/about_box.py:70 -msgid "Register" -msgstr "" - #: qtlib/error_report_dialog.py:38 msgid "Error Report" msgstr "Relatório de Erro" @@ -108,58 +100,10 @@ msgstr "" msgid "Clear List" msgstr "Limpar Lista" -#: qtlib/reg_demo_dialog.py:35 -msgid "$appname is Fairware" -msgstr "$appname é Fairware" - -#: qtlib/reg_demo_dialog.py:49 -msgid "Try" -msgstr "Testar" - -#: qtlib/reg_demo_dialog.py:52 -msgid "Enter Key" -msgstr "Entrar Chave" - -#: qtlib/reg_demo_dialog.py:55 -msgid "Buy" -msgstr "Comprar" - -#: qtlib/reg_demo_dialog.py:57 -msgid "Fairware?" -msgstr "Fairware?" - -#: qtlib/reg_submit_dialog.py:31 -msgid "Enter your registration key" -msgstr "Entre sua chave de registro" - -#: qtlib/reg_submit_dialog.py:36 -msgid "" -"Type the key you received when you contributed to $appname, as well as the " -"e-mail used as a reference for the purchase." -msgstr "" -"Digite a chave que você recebeu ao contribuir com o $appname, assim como o " -"e-mail usado para a compra." - -#: qtlib/reg_submit_dialog.py:48 -msgid "Registration key:" -msgstr "Chave de registro:" - -#: qtlib/reg_submit_dialog.py:51 -msgid "Registered e-mail:" -msgstr "e-mail registrado:" - -#: qtlib/reg_submit_dialog.py:60 -msgid "Contribute" -msgstr "Contribuir" - -#: qtlib/reg_submit_dialog.py:71 -msgid "Cancel" -msgstr "Cancelar" - -#: qtlib/reg_submit_dialog.py:80 -msgid "Submit" -msgstr "Enviar" - #: qtlib/search_edit.py:41 msgid "Search..." msgstr "Buscar…" + +#: qtlib/preferences.py:29 +msgid "Vietnamese" +msgstr "" diff --git a/qtlib/locale/qtlib.pot b/qtlib/locale/qtlib.pot index 4b455fbf..ec2a8178 100644 --- a/qtlib/locale/qtlib.pot +++ b/qtlib/locale/qtlib.pot @@ -4,26 +4,18 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: utf-8\n" -#: qtlib/about_box.py:31 +#: qtlib/about_box.py:28 msgid "About {}" msgstr "" -#: qtlib/about_box.py:51 +#: qtlib/about_box.py:48 msgid "Version {}" msgstr "" -#: qtlib/about_box.py:55 +#: qtlib/about_box.py:52 msgid "Copyright Hardcoded Software 2013" msgstr "" -#: qtlib/about_box.py:64 -msgid "UNREGISTERED" -msgstr "" - -#: qtlib/about_box.py:70 -msgid "Register" -msgstr "" - #: qtlib/error_report_dialog.py:38 msgid "Error Report" msgstr "" @@ -92,58 +84,14 @@ msgstr "" msgid "Spanish" msgstr "" +#: qtlib/preferences.py:29 +msgid "Vietnamese" +msgstr "" + #: qtlib/recent.py:53 msgid "Clear List" msgstr "" -#: qtlib/reg_demo_dialog.py:35 -msgid "$appname is Fairware" -msgstr "" - -#: qtlib/reg_demo_dialog.py:49 -msgid "Try" -msgstr "" - -#: qtlib/reg_demo_dialog.py:52 -msgid "Enter Key" -msgstr "" - -#: qtlib/reg_demo_dialog.py:55 -msgid "Buy" -msgstr "" - -#: qtlib/reg_demo_dialog.py:57 -msgid "Fairware?" -msgstr "" - -#: qtlib/reg_submit_dialog.py:31 -msgid "Enter your registration key" -msgstr "" - -#: qtlib/reg_submit_dialog.py:36 -msgid "Type the key you received when you contributed to $appname, as well as the e-mail used as a reference for the purchase." -msgstr "" - -#: qtlib/reg_submit_dialog.py:48 -msgid "Registration key:" -msgstr "" - -#: qtlib/reg_submit_dialog.py:51 -msgid "Registered e-mail:" -msgstr "" - -#: qtlib/reg_submit_dialog.py:60 -msgid "Contribute" -msgstr "" - -#: qtlib/reg_submit_dialog.py:71 -msgid "Cancel" -msgstr "" - -#: qtlib/reg_submit_dialog.py:80 -msgid "Submit" -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 index 1d0081d7..45dfd0e8 100644 --- a/qtlib/locale/ru/LC_MESSAGES/qtlib.po +++ b/qtlib/locale/ru/LC_MESSAGES/qtlib.po @@ -9,26 +9,18 @@ msgstr "" "Language: ru\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:31 +#: qtlib/about_box.py:28 msgid "About {}" msgstr "" -#: qtlib/about_box.py:51 +#: qtlib/about_box.py:48 msgid "Version {}" msgstr "" -#: qtlib/about_box.py:55 +#: qtlib/about_box.py:52 msgid "Copyright Hardcoded Software 2013" msgstr "Copyright Hardcoded Software 2013" -#: qtlib/about_box.py:64 -msgid "UNREGISTERED" -msgstr "НЕЗАРЕГИСТРИРОВАННАЯ" - -#: qtlib/about_box.py:70 -msgid "Register" -msgstr "" - #: qtlib/error_report_dialog.py:38 msgid "Error Report" msgstr "Сообщение об ошибке" @@ -106,58 +98,10 @@ msgstr "" msgid "Clear List" msgstr "Очистить список" -#: qtlib/reg_demo_dialog.py:35 -msgid "$appname is Fairware" -msgstr "$appname является Fairware" - -#: qtlib/reg_demo_dialog.py:49 -msgid "Try" -msgstr "Попробовать" - -#: qtlib/reg_demo_dialog.py:52 -msgid "Enter Key" -msgstr "Ввод ключа" - -#: qtlib/reg_demo_dialog.py:55 -msgid "Buy" -msgstr "Купить" - -#: qtlib/reg_demo_dialog.py:57 -msgid "Fairware?" -msgstr "Fairware?" - -#: qtlib/reg_submit_dialog.py:31 -msgid "Enter your registration key" -msgstr "Введите ваш регистрационный ключ" - -#: qtlib/reg_submit_dialog.py:36 -msgid "" -"Type the key you received when you contributed to $appname, as well as the " -"e-mail used as a reference for the purchase." -msgstr "" -"Введите ключ, который вы получили при вкладе в $appname, а также адрес " -"электронной почты использованный для покупки." - -#: qtlib/reg_submit_dialog.py:48 -msgid "Registration key:" -msgstr "Регистрационный ключ:" - -#: qtlib/reg_submit_dialog.py:51 -msgid "Registered e-mail:" -msgstr "Зарегистрированный e-mail:" - -#: qtlib/reg_submit_dialog.py:60 -msgid "Contribute" -msgstr "Поддержите" - -#: qtlib/reg_submit_dialog.py:71 -msgid "Cancel" -msgstr "Отменить" - -#: qtlib/reg_submit_dialog.py:80 -msgid "Submit" -msgstr "Подтвердить" - #: qtlib/search_edit.py:41 msgid "Search..." msgstr "" + +#: qtlib/preferences.py:29 +msgid "Vietnamese" +msgstr "" diff --git a/qtlib/locale/uk/LC_MESSAGES/qtlib.po b/qtlib/locale/uk/LC_MESSAGES/qtlib.po index 7e636bfd..2e9fc386 100755 --- a/qtlib/locale/uk/LC_MESSAGES/qtlib.po +++ b/qtlib/locale/uk/LC_MESSAGES/qtlib.po @@ -9,26 +9,18 @@ msgstr "" "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:31 +#: qtlib/about_box.py:28 msgid "About {}" msgstr "" -#: qtlib/about_box.py:51 +#: qtlib/about_box.py:48 msgid "Version {}" msgstr "" -#: qtlib/about_box.py:55 +#: qtlib/about_box.py:52 msgid "Copyright Hardcoded Software 2013" msgstr "Авторське право Hardcoded Software 2013" -#: qtlib/about_box.py:64 -msgid "UNREGISTERED" -msgstr "НЕЗАРЕЄСТРОВАНИЙ" - -#: qtlib/about_box.py:70 -msgid "Register" -msgstr "" - #: qtlib/error_report_dialog.py:38 msgid "Error Report" msgstr "Повідомлення про помилки" @@ -108,58 +100,10 @@ msgstr "" msgid "Clear List" msgstr "Очистити список" -#: qtlib/reg_demo_dialog.py:35 -msgid "$appname is Fairware" -msgstr "$appname є Fairware" - -#: qtlib/reg_demo_dialog.py:49 -msgid "Try" -msgstr "Спробувати" - -#: qtlib/reg_demo_dialog.py:52 -msgid "Enter Key" -msgstr "Введіть Ваш ключ" - -#: qtlib/reg_demo_dialog.py:55 -msgid "Buy" -msgstr "Купити" - -#: qtlib/reg_demo_dialog.py:57 -msgid "Fairware?" -msgstr "Fairware?" - -#: qtlib/reg_submit_dialog.py:31 -msgid "Enter your registration key" -msgstr "Введіть Ваш реєстраційний ключ" - -#: qtlib/reg_submit_dialog.py:36 -msgid "" -"Type the key you received when you contributed to $appname, as well as the " -"e-mail used as a reference for the purchase." -msgstr "" -"Введіть ключ, який Ви отримали зробивши внесок за $appname, а також адресу " -"електронної пошти, яка була вказана під час покупки." - -#: qtlib/reg_submit_dialog.py:48 -msgid "Registration key:" -msgstr "Реєстраційний ключ:" - -#: qtlib/reg_submit_dialog.py:51 -msgid "Registered e-mail:" -msgstr "Адреса електронної пошти:" - -#: qtlib/reg_submit_dialog.py:60 -msgid "Contribute" -msgstr "Зробити внесок" - -#: qtlib/reg_submit_dialog.py:71 -msgid "Cancel" -msgstr "Скасувати" - -#: qtlib/reg_submit_dialog.py:80 -msgid "Submit" -msgstr "Надіслати" - #: qtlib/search_edit.py:41 msgid "Search..." msgstr "Шукати..." + +#: qtlib/preferences.py:29 +msgid "Vietnamese" +msgstr "" diff --git a/qtlib/locale/vi/LC_MESSAGES/qtlib.po b/qtlib/locale/vi/LC_MESSAGES/qtlib.po index 011cd8c5..15f21087 100644 --- a/qtlib/locale/vi/LC_MESSAGES/qtlib.po +++ b/qtlib/locale/vi/LC_MESSAGES/qtlib.po @@ -10,26 +10,18 @@ msgstr "" "Language: vi\n" "Plural-Forms: nplurals=1; plural=0;\n" -#: qtlib/about_box.py:31 +#: qtlib/about_box.py:28 msgid "About {}" msgstr "" -#: qtlib/about_box.py:51 +#: qtlib/about_box.py:48 msgid "Version {}" msgstr "" -#: qtlib/about_box.py:55 +#: qtlib/about_box.py:52 msgid "Copyright Hardcoded Software 2013" msgstr "" -#: qtlib/about_box.py:64 -msgid "UNREGISTERED" -msgstr "" - -#: qtlib/about_box.py:70 -msgid "Register" -msgstr "" - #: qtlib/error_report_dialog.py:38 msgid "Error Report" msgstr "" @@ -106,56 +98,10 @@ msgstr "" msgid "Clear List" msgstr "" -#: qtlib/reg_demo_dialog.py:35 -msgid "$appname is Fairware" -msgstr "" - -#: qtlib/reg_demo_dialog.py:49 -msgid "Try" -msgstr "" - -#: qtlib/reg_demo_dialog.py:52 -msgid "Enter Key" -msgstr "" - -#: qtlib/reg_demo_dialog.py:55 -msgid "Buy" -msgstr "" - -#: qtlib/reg_demo_dialog.py:57 -msgid "Fairware?" -msgstr "" - -#: qtlib/reg_submit_dialog.py:31 -msgid "Enter your registration key" -msgstr "" - -#: qtlib/reg_submit_dialog.py:36 -msgid "" -"Type the key you received when you contributed to $appname, as well as the " -"e-mail used as a reference for the purchase." -msgstr "" - -#: qtlib/reg_submit_dialog.py:48 -msgid "Registration key:" -msgstr "" - -#: qtlib/reg_submit_dialog.py:51 -msgid "Registered e-mail:" -msgstr "" - -#: qtlib/reg_submit_dialog.py:60 -msgid "Contribute" -msgstr "" - -#: qtlib/reg_submit_dialog.py:71 -msgid "Cancel" -msgstr "" - -#: qtlib/reg_submit_dialog.py:80 -msgid "Submit" -msgstr "" - #: qtlib/search_edit.py:41 msgid "Search..." msgstr "" + +#: qtlib/preferences.py:29 +msgid "Vietnamese" +msgstr "" diff --git a/qtlib/locale/zh_CN/LC_MESSAGES/qtlib.po b/qtlib/locale/zh_CN/LC_MESSAGES/qtlib.po index 693ed227..81b505f1 100644 --- a/qtlib/locale/zh_CN/LC_MESSAGES/qtlib.po +++ b/qtlib/locale/zh_CN/LC_MESSAGES/qtlib.po @@ -9,26 +9,18 @@ msgstr "" "Language: zh_CN\n" "Plural-Forms: nplurals=1; plural=0;\n" -#: qtlib/about_box.py:31 +#: qtlib/about_box.py:28 msgid "About {}" msgstr "" -#: qtlib/about_box.py:51 +#: qtlib/about_box.py:48 msgid "Version {}" msgstr "" -#: qtlib/about_box.py:55 +#: qtlib/about_box.py:52 msgid "Copyright Hardcoded Software 2013" msgstr "" -#: qtlib/about_box.py:64 -msgid "UNREGISTERED" -msgstr "" - -#: qtlib/about_box.py:70 -msgid "Register" -msgstr "" - #: qtlib/error_report_dialog.py:38 msgid "Error Report" msgstr "" @@ -105,56 +97,10 @@ msgstr "" msgid "Clear List" msgstr "清空列表" -#: qtlib/reg_demo_dialog.py:35 -msgid "$appname is Fairware" -msgstr "$appname 是一款捐助型软件" - -#: qtlib/reg_demo_dialog.py:49 -msgid "Try" -msgstr "捐助" - -#: qtlib/reg_demo_dialog.py:52 -msgid "Enter Key" -msgstr "输入密钥" - -#: qtlib/reg_demo_dialog.py:55 -msgid "Buy" -msgstr "" - -#: qtlib/reg_demo_dialog.py:57 -msgid "Fairware?" -msgstr "" - -#: qtlib/reg_submit_dialog.py:31 -msgid "Enter your registration key" -msgstr "输入密钥" - -#: qtlib/reg_submit_dialog.py:36 -msgid "" -"Type the key you received when you contributed to $appname, as well as the " -"e-mail used as a reference for the purchase." -msgstr "当您捐助 $appname 后,请输入收到的注册密钥以及电子邮件,这将作为购买凭证。" - -#: qtlib/reg_submit_dialog.py:48 -msgid "Registration key:" -msgstr "密钥:" - -#: qtlib/reg_submit_dialog.py:51 -msgid "Registered e-mail:" -msgstr "电子邮箱:" - -#: qtlib/reg_submit_dialog.py:60 -msgid "Contribute" -msgstr "捐助" - -#: qtlib/reg_submit_dialog.py:71 -msgid "Cancel" -msgstr "取消" - -#: qtlib/reg_submit_dialog.py:80 -msgid "Submit" -msgstr "提交" - #: qtlib/search_edit.py:41 msgid "Search..." msgstr "" + +#: qtlib/preferences.py:29 +msgid "Vietnamese" +msgstr "" diff --git a/qtlib/reg.py b/qtlib/reg.py deleted file mode 100644 index c66c578d..00000000 --- a/qtlib/reg.py +++ /dev/null @@ -1,25 +0,0 @@ -# Created By: Virgil Dupras -# Created On: 2009-05-09 -# Copyright 2013 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "BSD" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at -# http://www.hardcoded.net/licenses/bsd_license - -from PyQt4.QtGui import QDialog - -from .reg_submit_dialog import RegSubmitDialog -from .reg_demo_dialog import RegDemoDialog - -class Registration: - def __init__(self, app): - self.app = app - - def ask_for_code(self): - dialog = RegSubmitDialog(None, self) - return dialog.exec_() == QDialog.Accepted - - def show_demo_nag(self, prompt): - dialog = RegDemoDialog(None, self, prompt) - dialog.exec_() - diff --git a/qtlib/reg_demo_dialog.py b/qtlib/reg_demo_dialog.py deleted file mode 100644 index e6675d6a..00000000 --- a/qtlib/reg_demo_dialog.py +++ /dev/null @@ -1,80 +0,0 @@ -# Created By: Virgil Dupras -# Created On: 2009-05-10 -# Copyright 2013 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "BSD" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at -# http://www.hardcoded.net/licenses/bsd_license - -import sys - -from PyQt4.QtCore import Qt, QCoreApplication -from PyQt4.QtGui import (QDialog, QApplication, QVBoxLayout, QHBoxLayout, QLabel, - QFont, QSpacerItem, QSizePolicy, QPushButton) - -from hscommon.plat import ISLINUX -from hscommon.trans import trget - -tr = trget('qtlib') - -class RegDemoDialog(QDialog): - def __init__(self, parent, reg, prompt): - flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint - QDialog.__init__(self, parent, flags) - self.reg = reg - self._setupUi() - self.descLabel.setText(prompt) - - self.enterCodeButton.clicked.connect(self.enterCodeClicked) - self.buyButton.clicked.connect(self.buyClicked) - self.tryButton.clicked.connect(self.accept) - self.moreInfoButton.clicked.connect(self.moreInfoClicked) - - def _setupUi(self): - appname = QCoreApplication.instance().applicationName() - title = tr("$appname is Fairware") - title = title.replace('$appname', appname) - self.setWindowTitle(title) - # Workaround for bug at http://bugreports.qt.nokia.com/browse/QTBUG-8212 - dlg_height = 370 if ISLINUX else 240 - self.resize(400, dlg_height) - self.verticalLayout = QVBoxLayout(self) - self.descLabel = QLabel(self) - self.descLabel.setWordWrap(True) - self.verticalLayout.addWidget(self.descLabel) - spacerItem = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding) - self.verticalLayout.addItem(spacerItem) - self.horizontalLayout = QHBoxLayout() - self.tryButton = QPushButton(self) - self.tryButton.setText(tr("Try")) - self.horizontalLayout.addWidget(self.tryButton) - self.enterCodeButton = QPushButton(self) - self.enterCodeButton.setText(tr("Enter Key")) - self.horizontalLayout.addWidget(self.enterCodeButton) - self.buyButton = QPushButton(self) - self.buyButton.setText(tr("Buy")) - self.horizontalLayout.addWidget(self.buyButton) - self.moreInfoButton = QPushButton(tr("Fairware?")) - self.horizontalLayout.addWidget(self.moreInfoButton) - self.verticalLayout.addLayout(self.horizontalLayout) - - #--- Events - def enterCodeClicked(self): - if self.reg.ask_for_code(): - self.accept() - - def buyClicked(self): - self.reg.app.buy() - - def moreInfoClicked(self): - self.reg.app.about_fairware() - - -if __name__ == '__main__': - app = QApplication([]) - app.unpaid_hours = 42.4 - class FakeReg: - app = app - dialog = RegDemoDialog(None, FakeReg(), "foo bar baz") - dialog.show() - sys.exit(app.exec_()) diff --git a/qtlib/reg_submit_dialog.py b/qtlib/reg_submit_dialog.py deleted file mode 100644 index 9a55a108..00000000 --- a/qtlib/reg_submit_dialog.py +++ /dev/null @@ -1,102 +0,0 @@ -# Created By: Virgil Dupras -# Created On: 2009-05-09 -# Copyright 2013 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "BSD" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at -# http://www.hardcoded.net/licenses/bsd_license - -import sys - -from PyQt4.QtCore import Qt, QCoreApplication -from PyQt4.QtGui import (QDialog, QApplication, QVBoxLayout, QHBoxLayout, QLabel, QFormLayout, - QLayout, QLineEdit, QPushButton, QSpacerItem, QSizePolicy) - -from hscommon.trans import trget - -tr = trget('qtlib') - -class RegSubmitDialog(QDialog): - def __init__(self, parent, reg): - flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint - QDialog.__init__(self, parent, flags) - self._setupUi() - self.reg = reg - - self.submitButton.clicked.connect(self.submitClicked) - self.contributeButton.clicked.connect(self.contributeClicked) - self.cancelButton.clicked.connect(self.reject) - - def _setupUi(self): - self.setWindowTitle(tr("Enter your registration key")) - self.resize(365, 126) - self.verticalLayout = QVBoxLayout(self) - self.promptLabel = QLabel(self) - appname = str(QCoreApplication.instance().applicationName()) - prompt = tr("Type the key you received when you contributed to $appname, as well as the " - "e-mail used as a reference for the purchase.").replace('$appname', appname) - self.promptLabel.setText(prompt) - self.promptLabel.setAlignment(Qt.AlignLeading|Qt.AlignLeft|Qt.AlignTop) - self.promptLabel.setWordWrap(True) - self.verticalLayout.addWidget(self.promptLabel) - self.formLayout = QFormLayout() - self.formLayout.setSizeConstraint(QLayout.SetNoConstraint) - self.formLayout.setFieldGrowthPolicy(QFormLayout.ExpandingFieldsGrow) - self.formLayout.setLabelAlignment(Qt.AlignRight|Qt.AlignTrailing|Qt.AlignVCenter) - self.formLayout.setFormAlignment(Qt.AlignLeading|Qt.AlignLeft|Qt.AlignTop) - self.label2 = QLabel(self) - self.label2.setText(tr("Registration key:")) - self.formLayout.setWidget(0, QFormLayout.LabelRole, self.label2) - self.label3 = QLabel(self) - self.label3.setText(tr("Registered e-mail:")) - self.formLayout.setWidget(1, QFormLayout.LabelRole, self.label3) - self.codeEdit = QLineEdit(self) - self.formLayout.setWidget(0, QFormLayout.FieldRole, self.codeEdit) - self.emailEdit = QLineEdit(self) - self.formLayout.setWidget(1, QFormLayout.FieldRole, self.emailEdit) - self.verticalLayout.addLayout(self.formLayout) - self.horizontalLayout = QHBoxLayout() - self.contributeButton = QPushButton(self) - self.contributeButton.setText(tr("Contribute")) - self.contributeButton.setAutoDefault(False) - self.horizontalLayout.addWidget(self.contributeButton) - spacerItem = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) - self.horizontalLayout.addItem(spacerItem) - self.cancelButton = QPushButton(self) - sizePolicy = QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.cancelButton.sizePolicy().hasHeightForWidth()) - self.cancelButton.setSizePolicy(sizePolicy) - self.cancelButton.setText(tr("Cancel")) - self.cancelButton.setAutoDefault(False) - self.horizontalLayout.addWidget(self.cancelButton) - self.submitButton = QPushButton(self) - sizePolicy = QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.submitButton.sizePolicy().hasHeightForWidth()) - self.submitButton.setSizePolicy(sizePolicy) - self.submitButton.setText(tr("Submit")) - self.submitButton.setAutoDefault(False) - self.submitButton.setDefault(True) - self.horizontalLayout.addWidget(self.submitButton) - self.verticalLayout.addLayout(self.horizontalLayout) - - #--- Events - def contributeClicked(self): - self.reg.app.contribute() - - def submitClicked(self): - code = self.codeEdit.text() - email = self.emailEdit.text() - if self.reg.app.set_registration(code, email, False): - self.accept() - - -if __name__ == '__main__': - app = QApplication([]) - validate = lambda *args: True - dialog = RegSubmitDialog(None, validate) - dialog.show() - sys.exit(app.exec_())