You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

380 lines
12 KiB

  1. /*
  2. Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
  3. This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
  4. which should be included with this package. The terms are also available at
  5. http://www.gnu.org/licenses/gpl-3.0.html
  6. */
  7. #import "AppDelegate.h"
  8. #import "ProgressController.h"
  9. #import "HSPyUtil.h"
  10. #import "Consts.h"
  11. #import "Dialogs.h"
  12. #import "Utils.h"
  13. #import "ValueTransformers.h"
  14. #import "DetailsPanelPicture.h"
  15. @implementation AppDelegate
  16. @synthesize recentResultsMenu;
  17. @synthesize columnsMenu;
  18. + (NSDictionary *)defaultPreferences
  19. {
  20. NSMutableDictionary *d = [NSMutableDictionary dictionary];
  21. [d setObject:i2n(1) forKey:@"scanTypeStandard"];
  22. [d setObject:i2n(3) forKey:@"scanTypeMusic"];
  23. [d setObject:i2n(0) forKey:@"scanTypePicture"];
  24. [d setObject:i2n(95) forKey:@"minMatchPercentage"];
  25. [d setObject:i2n(30) forKey:@"smallFileThreshold"];
  26. [d setObject:b2n(YES) forKey:@"wordWeighting"];
  27. [d setObject:b2n(NO) forKey:@"matchSimilarWords"];
  28. [d setObject:b2n(YES) forKey:@"ignoreSmallFiles"];
  29. [d setObject:b2n(NO) forKey:@"scanTagTrack"];
  30. [d setObject:b2n(YES) forKey:@"scanTagArtist"];
  31. [d setObject:b2n(YES) forKey:@"scanTagAlbum"];
  32. [d setObject:b2n(YES) forKey:@"scanTagTitle"];
  33. [d setObject:b2n(NO) forKey:@"scanTagGenre"];
  34. [d setObject:b2n(NO) forKey:@"scanTagYear"];
  35. [d setObject:b2n(NO) forKey:@"matchScaled"];
  36. [d setObject:i2n(1) forKey:@"recreatePathType"];
  37. [d setObject:i2n(11) forKey:TableFontSize];
  38. [d setObject:b2n(YES) forKey:@"mixFileKind"];
  39. [d setObject:b2n(NO) forKey:@"useRegexpFilter"];
  40. [d setObject:b2n(NO) forKey:@"ignoreHardlinkMatches"];
  41. [d setObject:b2n(NO) forKey:@"removeEmptyFolders"];
  42. [d setObject:b2n(NO) forKey:@"DebugMode"];
  43. [d setObject:@"" forKey:@"CustomCommand"];
  44. [d setObject:[NSArray array] forKey:@"recentDirectories"];
  45. [d setObject:[NSArray array] forKey:@"columnsOrder"];
  46. [d setObject:[NSDictionary dictionary] forKey:@"columnsWidth"];
  47. return d;
  48. }
  49. + (void)initialize
  50. {
  51. HSVTAdd *vt = [[[HSVTAdd alloc] initWithValue:4] autorelease];
  52. [NSValueTransformer setValueTransformer:vt forName:@"vtRowHeightOffset"];
  53. NSDictionary *d = [self defaultPreferences];
  54. [[NSUserDefaultsController sharedUserDefaultsController] setInitialValues:d];
  55. [[NSUserDefaults standardUserDefaults] registerDefaults:d];
  56. }
  57. - (void)awakeFromNib
  58. {
  59. model = [[PyDupeGuru alloc] init];
  60. [model bindCallback:createCallback(@"DupeGuruView", self)];
  61. NSMutableIndexSet *contentsIndexes = [NSMutableIndexSet indexSet];
  62. [contentsIndexes addIndex:1];
  63. [contentsIndexes addIndex:2];
  64. VTIsIntIn *vt = [[[VTIsIntIn alloc] initWithValues:contentsIndexes reverse:YES] autorelease];
  65. [NSValueTransformer setValueTransformer:vt forName:@"vtScanTypeIsNotContent"];
  66. NSMutableIndexSet *i = [NSMutableIndexSet indexSetWithIndex:0];
  67. VTIsIntIn *vtScanTypeIsFuzzy = [[[VTIsIntIn alloc] initWithValues:i reverse:NO] autorelease];
  68. [NSValueTransformer setValueTransformer:vtScanTypeIsFuzzy forName:@"vtScanTypeIsFuzzy"];
  69. i = [NSMutableIndexSet indexSetWithIndex:4];
  70. VTIsIntIn *vtScanTypeIsNotContent = [[[VTIsIntIn alloc] initWithValues:i reverse:YES] autorelease];
  71. [NSValueTransformer setValueTransformer:vtScanTypeIsNotContent forName:@"vtScanTypeMusicIsNotContent"];
  72. VTIsIntIn *vtScanTypeIsTag = [[[VTIsIntIn alloc] initWithValues:[NSIndexSet indexSetWithIndex:3] reverse:NO] autorelease];
  73. [NSValueTransformer setValueTransformer:vtScanTypeIsTag forName:@"vtScanTypeIsTag"];
  74. _recentResults = [[HSRecentFiles alloc] initWithName:@"recentResults" menu:recentResultsMenu];
  75. [_recentResults setDelegate:self];
  76. _directoryPanel = [[DirectoryPanel alloc] initWithParentApp:self];
  77. _ignoreListDialog = [[IgnoreListDialog alloc] initWithPyRef:[model ignoreListDialog]];
  78. _problemDialog = [[ProblemDialog alloc] initWithPyRef:[model problemDialog]];
  79. _deletionOptions = [[DeletionOptions alloc] initWithPyRef:[model deletionOptions]];
  80. _progressWindow = [[HSProgressWindow alloc] initWithPyRef:[[self model] progressWindow] view:nil];
  81. [_progressWindow setParentWindow:[_directoryPanel window]];
  82. // Lazily loaded
  83. _aboutBox = nil;
  84. _preferencesPanel = nil;
  85. _resultWindow = nil;
  86. _detailsPanel = nil;
  87. [[[self directoryPanel] window] makeKeyAndOrderFront:self];
  88. }
  89. /* Virtual */
  90. - (PyDupeGuru *)model
  91. {
  92. return model;
  93. }
  94. - (DetailsPanel *)createDetailsPanel
  95. {
  96. NSInteger appMode = [self getAppMode];
  97. if (appMode == AppModePicture) {
  98. return [[DetailsPanelPicture alloc] initWithApp:model];
  99. }
  100. else {
  101. return [[DetailsPanel alloc] initWithPyRef:[model detailsPanel]];
  102. }
  103. }
  104. - (void)setScanOptions
  105. {
  106. NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
  107. NSString *scanTypeOptionName;
  108. NSInteger appMode = [self getAppMode];
  109. if (appMode == AppModePicture) {
  110. scanTypeOptionName = @"scanTypePicture";
  111. }
  112. else if (appMode == AppModeMusic) {
  113. scanTypeOptionName = @"scanTypeMusic";
  114. }
  115. else {
  116. scanTypeOptionName = @"scanTypeStandard";
  117. }
  118. [model setScanType:n2i([ud objectForKey:scanTypeOptionName])];
  119. [model setMinMatchPercentage:n2i([ud objectForKey:@"minMatchPercentage"])];
  120. [model setWordWeighting:n2b([ud objectForKey:@"wordWeighting"])];
  121. [model setMixFileKind:n2b([ud objectForKey:@"mixFileKind"])];
  122. [model setIgnoreHardlinkMatches:n2b([ud objectForKey:@"ignoreHardlinkMatches"])];
  123. [model setMatchSimilarWords:n2b([ud objectForKey:@"matchSimilarWords"])];
  124. NSInteger smallFileThreshold = [ud integerForKey:@"smallFileThreshold"]; // In KB
  125. NSInteger sizeThreshold = [ud boolForKey:@"ignoreSmallFiles"] ? smallFileThreshold * 1024 : 0; // The py side wants bytes
  126. [model setSizeThreshold:sizeThreshold];
  127. [model enable:n2b([ud objectForKey:@"scanTagTrack"]) scanForTag:@"track"];
  128. [model enable:n2b([ud objectForKey:@"scanTagArtist"]) scanForTag:@"artist"];
  129. [model enable:n2b([ud objectForKey:@"scanTagAlbum"]) scanForTag:@"album"];
  130. [model enable:n2b([ud objectForKey:@"scanTagTitle"]) scanForTag:@"title"];
  131. [model enable:n2b([ud objectForKey:@"scanTagGenre"]) scanForTag:@"genre"];
  132. [model enable:n2b([ud objectForKey:@"scanTagYear"]) scanForTag:@"year"];
  133. [model setMatchScaled:n2b([ud objectForKey:@"matchScaled"])];
  134. }
  135. /* Public */
  136. - (ResultWindow *)resultWindow
  137. {
  138. return _resultWindow;
  139. }
  140. - (DirectoryPanel *)directoryPanel
  141. {
  142. return _directoryPanel;
  143. }
  144. - (DetailsPanel *)detailsPanel
  145. {
  146. return _detailsPanel;
  147. }
  148. - (HSRecentFiles *)recentResults
  149. {
  150. return _recentResults;
  151. }
  152. - (NSInteger)getAppMode
  153. {
  154. return [model getAppMode];
  155. }
  156. - (void)setAppMode:(NSInteger)appMode
  157. {
  158. [model setAppMode:appMode];
  159. if (_preferencesPanel != nil) {
  160. [_preferencesPanel release];
  161. _preferencesPanel = nil;
  162. }
  163. }
  164. /* Actions */
  165. - (IBAction)clearPictureCache:(id)sender
  166. {
  167. NSString *msg = NSLocalizedString(@"Do you really want to remove all your cached picture analysis?", @"");
  168. if ([Dialogs askYesNo:msg] == NSAlertSecondButtonReturn) // NO
  169. return;
  170. [model clearPictureCache];
  171. }
  172. - (IBAction)loadResults:(id)sender
  173. {
  174. NSOpenPanel *op = [NSOpenPanel openPanel];
  175. [op setCanChooseFiles:YES];
  176. [op setCanChooseDirectories:NO];
  177. [op setCanCreateDirectories:NO];
  178. [op setAllowsMultipleSelection:NO];
  179. [op setAllowedFileTypes:[NSArray arrayWithObject:@"dupeguru"]];
  180. [op setTitle:NSLocalizedString(@"Select a results file to load", @"")];
  181. if ([op runModal] == NSModalResponseOK) {
  182. NSString *filename = [[[op URLs] objectAtIndex:0] path];
  183. [model loadResultsFrom:filename];
  184. [[self recentResults] addFile:filename];
  185. }
  186. }
  187. - (IBAction)openWebsite:(id)sender
  188. {
  189. [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"https://www.hardcoded.net/dupeguru/"]];
  190. }
  191. - (IBAction)openHelp:(id)sender
  192. {
  193. NSBundle *b = [NSBundle mainBundle];
  194. NSString *p = [b pathForResource:@"index" ofType:@"html" inDirectory:@"help"];
  195. NSURL *u = [NSURL fileURLWithPath:p];
  196. [[NSWorkspace sharedWorkspace] openURL:u];
  197. }
  198. - (IBAction)showAboutBox:(id)sender
  199. {
  200. if (_aboutBox == nil) {
  201. _aboutBox = [[HSAboutBox alloc] initWithApp:model];
  202. }
  203. [[_aboutBox window] makeKeyAndOrderFront:nil];
  204. }
  205. - (IBAction)showDirectoryWindow:(id)sender
  206. {
  207. [[[self directoryPanel] window] makeKeyAndOrderFront:nil];
  208. }
  209. - (IBAction)showPreferencesPanel:(id)sender
  210. {
  211. if (_preferencesPanel == nil) {
  212. NSString *nibName = @"Preferences";
  213. NSInteger appMode = [model getAppMode];
  214. if (appMode == AppModePicture) {
  215. nibName = @"PreferencesPicture";
  216. }
  217. else if (appMode == AppModeMusic) {
  218. nibName = @"PreferencesMusic";
  219. }
  220. _preferencesPanel = [[NSWindowController alloc] initWithWindowNibName:nibName];
  221. }
  222. [_preferencesPanel showWindow:nil];
  223. }
  224. - (IBAction)showResultWindow:(id)sender
  225. {
  226. [[[self resultWindow] window] makeKeyAndOrderFront:nil];
  227. }
  228. - (IBAction)showIgnoreList:(id)sender
  229. {
  230. [model showIgnoreList];
  231. }
  232. - (IBAction)startScanning:(id)sender
  233. {
  234. [[self directoryPanel] startDuplicateScan:sender];
  235. }
  236. /* Delegate */
  237. - (void)applicationDidFinishLaunching:(NSNotification *)aNotification
  238. {
  239. [model loadSession];
  240. }
  241. - (void)applicationWillBecomeActive:(NSNotification *)aNotification
  242. {
  243. if (![[[self directoryPanel] window] isVisible]) {
  244. [[self directoryPanel] showWindow:NSApp];
  245. }
  246. }
  247. - (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender
  248. {
  249. if ([model resultsAreModified]) {
  250. NSString *msg = NSLocalizedString(@"You have unsaved results, do you really want to quit?", @"");
  251. if ([Dialogs askYesNo:msg] == NSAlertSecondButtonReturn) { // NO
  252. return NSTerminateCancel;
  253. }
  254. }
  255. return NSTerminateNow;
  256. }
  257. - (void)applicationWillTerminate:(NSNotification *)aNotification
  258. {
  259. NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
  260. NSInteger sc = [ud integerForKey:@"sessionCountSinceLastIgnorePurge"];
  261. if (sc >= 10) {
  262. sc = -1;
  263. [model purgeIgnoreList];
  264. }
  265. sc++;
  266. [model saveSession];
  267. [ud setInteger:sc forKey:@"sessionCountSinceLastIgnorePurge"];
  268. // NSApplication does not release nib instances objects, we must do it manually
  269. // Well, it isn't needed because the memory is freed anyway (we are quitting the application
  270. // But I need to release HSRecentFiles so it saves the user defaults
  271. [_directoryPanel release];
  272. [_recentResults release];
  273. }
  274. - (void)recentFileClicked:(NSString *)path
  275. {
  276. [model loadResultsFrom:path];
  277. }
  278. /* model --> view */
  279. - (void)showMessage:(NSString *)msg
  280. {
  281. [Dialogs showMessage:msg];
  282. }
  283. - (BOOL)askYesNoWithPrompt:(NSString *)prompt
  284. {
  285. return [Dialogs askYesNo:prompt] == NSAlertFirstButtonReturn;
  286. }
  287. - (void)createResultsWindow
  288. {
  289. if (_resultWindow != nil) {
  290. [_resultWindow release];
  291. }
  292. if (_detailsPanel != nil) {
  293. [_detailsPanel release];
  294. }
  295. // Warning: creation order is important
  296. // If the details panel is not created first and that there are some results in the model
  297. // (happens if we load results), a dupe selection event triggers a details refresh in the
  298. // core before we have the chance to initialize it, and then we crash.
  299. _detailsPanel = [self createDetailsPanel];
  300. _resultWindow = [[ResultWindow alloc] initWithParentApp:self];
  301. }
  302. - (void)showResultsWindow
  303. {
  304. [[[self resultWindow] window] makeKeyAndOrderFront:nil];
  305. }
  306. - (void)showProblemDialog
  307. {
  308. [_problemDialog showWindow:self];
  309. }
  310. - (NSString *)selectDestFolderWithPrompt:(NSString *)prompt
  311. {
  312. NSOpenPanel *op = [NSOpenPanel openPanel];
  313. [op setCanChooseFiles:NO];
  314. [op setCanChooseDirectories:YES];
  315. [op setCanCreateDirectories:YES];
  316. [op setAllowsMultipleSelection:NO];
  317. [op setTitle:prompt];
  318. if ([op runModal] == NSModalResponseOK) {
  319. return [[[op URLs] objectAtIndex:0] path];
  320. }
  321. else {
  322. return nil;
  323. }
  324. }
  325. - (NSString *)selectDestFileWithPrompt:(NSString *)prompt extension:(NSString *)extension
  326. {
  327. NSSavePanel *sp = [NSSavePanel savePanel];
  328. [sp setCanCreateDirectories:YES];
  329. [sp setAllowedFileTypes:[NSArray arrayWithObject:extension]];
  330. [sp setTitle:prompt];
  331. if ([sp runModal] == NSModalResponseOK) {
  332. return [[sp URL] path];
  333. }
  334. else {
  335. return nil;
  336. }
  337. }
  338. @end