aboutsummaryrefslogtreecommitdiffstats
path: root/Source
diff options
context:
space:
mode:
Diffstat (limited to 'Source')
-rw-r--r--Source/SPAboutController.m3
-rw-r--r--Source/SPAppController.m14
-rw-r--r--Source/SPBundleEditorController.m3
-rw-r--r--Source/SPBundleHTMLOutputController.m2
-rw-r--r--Source/SPCategoryAdditions.h1
-rw-r--r--Source/SPCompatibility.h151
-rw-r--r--Source/SPConnectionController.m16
-rw-r--r--Source/SPConstants.h26
-rw-r--r--Source/SPCopyTable.m37
-rw-r--r--Source/SPCustomQuery.h10
-rw-r--r--Source/SPCustomQuery.m66
-rw-r--r--Source/SPDataAdditions.h1
-rw-r--r--Source/SPDataAdditions.m142
-rw-r--r--Source/SPDataImport.m62
-rw-r--r--Source/SPDataStorage.h7
-rw-r--r--Source/SPDataStorage.m171
-rw-r--r--Source/SPDatabaseDocument.m94
-rw-r--r--Source/SPEditorPreferencePane.m2
-rw-r--r--Source/SPExportInitializer.m4
-rw-r--r--Source/SPExtendedTableInfo.m5
-rw-r--r--Source/SPFieldEditorController.m7
-rw-r--r--Source/SPFieldMapperController.m2
-rw-r--r--Source/SPFileHandle.h4
-rw-r--r--Source/SPFileHandle.m66
-rw-r--r--Source/SPFunctions.m3
-rw-r--r--Source/SPIndexesController.m163
-rw-r--r--Source/SPKeychain.m6
-rw-r--r--Source/SPMenuAdditions.m46
-rw-r--r--Source/SPOSInfo.m21
-rw-r--r--Source/SPProcessListController.m47
-rw-r--r--Source/SPQueryConsoleDataSource.m11
-rw-r--r--Source/SPQueryController.h17
-rw-r--r--Source/SPQueryController.m85
-rw-r--r--Source/SPQueryControllerInitializer.m3
-rw-r--r--Source/SPTableContent.h1
-rw-r--r--Source/SPTableContent.m91
-rw-r--r--Source/SPTableContentDataSource.h2
-rw-r--r--Source/SPTableContentDataSource.m24
-rw-r--r--Source/SPTableContentDelegate.m47
-rw-r--r--Source/SPTableData.m212
-rw-r--r--Source/SPTableRelations.h6
-rw-r--r--Source/SPTableRelations.m56
-rw-r--r--Source/SPTableRelationsDelegate.h (renamed from Source/SPMenuAdditions.h)11
-rw-r--r--Source/SPTableRelationsDelegate.m87
-rw-r--r--Source/SPTablesList.m7
-rw-r--r--Source/SPTooltip.m6
-rw-r--r--Source/SPWindow.m8
-rw-r--r--Source/SPWindowController.m16
-rw-r--r--Source/SPWindowControllerDelegate.m16
-rw-r--r--Source/Sequel-Pro.pch4
50 files changed, 1178 insertions, 714 deletions
diff --git a/Source/SPAboutController.m b/Source/SPAboutController.m
index 0fa5a06c..712f311c 100644
--- a/Source/SPAboutController.m
+++ b/Source/SPAboutController.m
@@ -58,7 +58,8 @@ static NSString *SPShortVersionHashKey = @"SPVersionShortHash";
NSString *version = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"];
// If the version string has a prefix of 'Nightly' then this is obviously a nighly build.
- BOOL isSnapshotBuild = [version containsString:SPSnapshotBuildIndicator];
+ NSRange matchRange = [version rangeOfString:SPSnapshotBuildIndicator];
+ BOOL isSnapshotBuild = (matchRange.location != NSNotFound);
// Set the application name, but only include the major version if this is not a nightly build.
[appNameVersionTextField setStringValue:isSnapshotBuild ? @"Sequel Pro" : [NSString stringWithFormat:@"Sequel Pro %@", version]];
diff --git a/Source/SPAppController.m b/Source/SPAppController.m
index c7412419..afdfd16f 100644
--- a/Source/SPAppController.m
+++ b/Source/SPAppController.m
@@ -50,6 +50,7 @@
#import "SPWindowManagement.h"
#import "SPCopyTable.h"
#import "SPSyntaxParser.h"
+#import "SPOSInfo.h"
#import <PSMTabBar/PSMTabBarControl.h>
#import <Sparkle/Sparkle.h>
@@ -769,7 +770,7 @@
}
else {
NSBeep();
- NSLog(@"Error in sequelpro URL scheme");
+ NSLog(@"Error in sequelpro URL scheme for URL <%@>",url);
}
}
@@ -825,7 +826,14 @@
// remove percent encoding
NSMutableArray *decodedPathComponents = [NSMutableArray arrayWithCapacity:pathComponents.count];
for (NSString *component in pathComponents) {
- [decodedPathComponents addObject:component.stringByRemovingPercentEncoding];
+ NSString *decoded;
+ if([SPOSInfo isOSVersionAtLeastMajor:10 minor:9 patch:0]) {
+ decoded = [component stringByRemovingPercentEncoding];
+ }
+ else {
+ decoded = [component stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
+ }
+ [decodedPathComponents addObject:decoded];
}
pathComponents = decodedPathComponents.copy;
@@ -1599,7 +1607,7 @@
NSMenu *menu = [[[NSApp mainMenu] itemWithTag:SPMainMenuBundles] submenu];
// Clean menu
- [menu compatibleRemoveAllItems];
+ [menu removeAllItems];
// Set up the bundle search paths
// First process all in Application Support folder installed ones then Default ones
diff --git a/Source/SPBundleEditorController.m b/Source/SPBundleEditorController.m
index e95c5d8c..ddac2b9e 100644
--- a/Source/SPBundleEditorController.m
+++ b/Source/SPBundleEditorController.m
@@ -29,7 +29,6 @@
// More info at <https://github.com/sequelpro/sequelpro>
#import "SPBundleEditorController.h"
-#import "SPMenuAdditions.h"
#import "SPBundleCommandRunner.h"
#import "SPOutlineView.h"
#import "SPBundleCommandTextView.h"
@@ -245,7 +244,7 @@
}
NSMenuItem *anItem;
- [inputGeneralScopePopUpMenu compatibleRemoveAllItems];
+ [inputGeneralScopePopUpMenu removeAllItems];
anItem = [[NSMenuItem alloc] initWithTitle:SP_BUNDLEEDITOR_SCOPE_GENERAL_STRING action:@selector(scopeButtonChanged:) keyEquivalent:@""];
[anItem setTag:kGeneralScopeArrayIndex];
[inputGeneralScopePopUpMenu addItem:anItem];
diff --git a/Source/SPBundleHTMLOutputController.m b/Source/SPBundleHTMLOutputController.m
index ecf16faa..6d9f70be 100644
--- a/Source/SPBundleHTMLOutputController.m
+++ b/Source/SPBundleHTMLOutputController.m
@@ -73,9 +73,7 @@ static NSString *SPSaveDocumentAction = @"SPSaveDocument";
[webView setDrawsBackground:YES];
[webView setEditable:NO];
[webView setShouldCloseWithWindow:YES];
-#if __MAC_OS_X_VERSION_MIN_REQUIRED >= 1060
[webView setShouldUpdateWhileOffscreen:NO];
-#endif
suppressExceptionAlerting = NO;
}
diff --git a/Source/SPCategoryAdditions.h b/Source/SPCategoryAdditions.h
index 991cb60c..f2e08f53 100644
--- a/Source/SPCategoryAdditions.h
+++ b/Source/SPCategoryAdditions.h
@@ -42,7 +42,6 @@
#import "SPWindowAdditions.h"
#import "SPDataAdditions.h"
#import "SPDataBase64EncodingAdditions.h"
-#import "SPMenuAdditions.h"
#import "SPNotLoaded.h"
#import "SPMainThreadTrampoline.h"
#import "SPColorAdditions.h"
diff --git a/Source/SPCompatibility.h b/Source/SPCompatibility.h
new file mode 100644
index 00000000..0154bb2a
--- /dev/null
+++ b/Source/SPCompatibility.h
@@ -0,0 +1,151 @@
+//
+// SPCompatibility.h
+// sequel-pro
+//
+// Created by Max Lohrmann on 31.03.17.
+// Copyright (c) 2017 Max Lohrmann. All rights reserved.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the "Software"), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+// More info at <https://github.com/sequelpro/sequelpro>
+
+/**
+ * This file contains declarations for backward compatibility to
+ * older XCode versions / SDKs.
+ *
+ * The current minimum required SDK is 10.8!
+ */
+
+#ifndef SPCompatibility
+#define SPCompatibility
+
+#pragma mark - 10.8 Mountain Lion
+
+#ifndef __MAC_10_8
+#define __MAC_10_8 1080
+#endif
+
+#if __MAC_OS_X_VERSION_MAX_ALLOWED < __MAC_10_8
+#error You need to have at least SDK 10.8!
+#endif
+
+#pragma mark - 10.9 Mavericks
+
+#ifndef __MAC_10_9
+#define __MAC_10_9 1090
+#endif
+
+#if __MAC_OS_X_VERSION_MAX_ALLOWED < __MAC_10_9
+
+@interface NSString (Mavericks)
+
+@property (readonly, copy) NSString *stringByRemovingPercentEncoding;
+
+@end
+
+#endif
+
+#pragma mark - 10.10 Yosemite
+
+#ifndef __MAC_10_10
+#define __MAC_10_10 101000
+#endif
+
+#if __MAC_OS_X_VERSION_MAX_ALLOWED < __MAC_10_10
+
+// This enum is available since 10.5 but only got a "name" in 10.10
+typedef NSUInteger NSCellHitResult;
+
+@compatibility_alias NSTitlebarAccessoryViewController NSViewController;
+
+@interface NSViewController (NSTitlebarAccessoryViewController)
+
+@property NSLayoutAttribute layoutAttribute;
+
+@end
+
+@interface NSWindow (Yosemite)
+
+- (NSArray *)titlebarAccessoryViewControllers;
+- (void)removeTitlebarAccessoryViewControllerAtIndex:(NSInteger)index;
+- (void)addTitlebarAccessoryViewController:(NSTitlebarAccessoryViewController *)controller;
+
+@end
+
+// This code is available since 10.8 but public only since 10.10
+typedef struct {
+ NSInteger majorVersion;
+ NSInteger minorVersion;
+ NSInteger patchVersion;
+} NSOperatingSystemVersion;
+
+@interface NSProcessInfo ()
+- (NSOperatingSystemVersion)operatingSystemVersion;
+- (BOOL)isOperatingSystemAtLeastVersion:(NSOperatingSystemVersion)version;
+@end
+
+#endif
+
+#pragma mark - 10.11 El Capitan
+
+#ifndef __MAC_10_11
+#define __MAC_10_11 101100
+#endif
+
+#if !__has_feature(objc_kindof)
+#define __kindof
+#endif
+
+#if __MAC_OS_X_VERSION_MAX_ALLOWED < __MAC_10_11
+
+// formal protocol since 10.11, NSObject category before
+@protocol WebFrameLoadDelegate <NSObject>
+@end
+
+@interface NSOpenPanel (ElCapitan)
+
+@property (getter=isAccessoryViewDisclosed) BOOL accessoryViewDisclosed;
+
+@end
+
+#endif
+
+#pragma mark - 10.12 Sierra
+
+#ifndef __MAC_10_12
+#define __MAC_10_12 101200
+#endif
+
+#if __MAC_OS_X_VERSION_MAX_ALLOWED < __MAC_10_12
+
+//those enums got renamed in 10.12, probably for consistency
+#define NSAlertStyleInformational NSInformationalAlertStyle
+#define NSAlertStyleWarning NSWarningAlertStyle
+#define NSAlertStyleCritical NSCriticalAlertStyle
+
+@interface NSWindow (Sierra)
++ (void)setAllowsAutomaticWindowTabbing:(BOOL)arg;
+@end
+
+#endif
+
+#endif
diff --git a/Source/SPConnectionController.m b/Source/SPConnectionController.m
index f1968af6..1ccde091 100644
--- a/Source/SPConnectionController.m
+++ b/Source/SPConnectionController.m
@@ -63,22 +63,6 @@ static NSString *SPRemoveNode = @"RemoveNode";
static NSString *SPExportFavoritesFilename = @"SequelProFavorites.plist";
#endif
-#if __MAC_OS_X_VERSION_MAX_ALLOWED < __MAC_10_6
-@interface NSSavePanel (NSSavePanel_unpublishedUntilSnowLeopardAPI)
-
-- (void)setShowsHiddenFiles:(BOOL)flag;
-
-@end
-#endif
-
-#if __MAC_OS_X_VERSION_MAX_ALLOWED < __MAC_10_11
-@interface NSOpenPanel (NSOpenPanel_ElCaptian)
-
-@property (getter=isAccessoryViewDisclosed) BOOL accessoryViewDisclosed;
-
-@end
-#endif
-
/**
* This is a utility function to validate SSL key/certificate files
* @param fileData The contents of the file
diff --git a/Source/SPConstants.h b/Source/SPConstants.h
index 71b0c49b..1419ec9a 100644
--- a/Source/SPConstants.h
+++ b/Source/SPConstants.h
@@ -665,32 +665,6 @@ typedef NS_ENUM(NSInteger,SPErrorCode) { // error codes in SPErrorDomain
void _SPClear(id *addr);
#define SPClear(x) _SPClear(&x)
-// Backwards compatibility
-#ifndef __MAC_10_7
- #define __MAC_10_7 1070
-#endif
-
-#ifndef __MAC_10_8
- #define __MAC_10_8 1080
-#endif
-
-#ifndef __MAC_10_10
- #define __MAC_10_10 101000
-#endif
-
-#ifndef __MAC_10_11
- #define __MAC_10_11 101100
-#endif
-
-#ifndef __MAC_10_12
- #define __MAC_10_12 101200
-#endif
-
-// This enum is available since 10.5 but only got a "name" in 10.10
-#if __MAC_OS_X_VERSION_MAX_ALLOWED < __MAC_10_10
-typedef NSUInteger NSCellHitResult;
-#endif
-
// Stolen from Stack Overflow: http://stackoverflow.com/questions/969130
#define SPLog(fmt, ...) NSLog((@"%s:%d: " fmt), __PRETTY_FUNCTION__, __LINE__, ##__VA_ARGS__)
diff --git a/Source/SPCopyTable.m b/Source/SPCopyTable.m
index ed7b1d71..aa77ffbc 100644
--- a/Source/SPCopyTable.m
+++ b/Source/SPCopyTable.m
@@ -184,6 +184,7 @@ static const NSInteger kBlobAsImageFile = 4;
[fm createDirectoryAtPath:tmpBlobFileDirectory withIntermediateDirectories:YES attributes:nil error:nil];
}
+ BOOL hexBlobs = [prefs boolForKey:SPDisplayBinaryDataAsHex];
[selectedRows enumerateIndexesUsingBlock:^(NSUInteger rowIndex, BOOL * _Nonnull stop) {
for (NSUInteger c = 0; c < numColumns; c++ ) {
id cellData = SPDataStorageObjectAtRowAndColumn(tableStorage, rowIndex, columnMappings[c]);
@@ -197,8 +198,12 @@ static const NSInteger kBlobAsImageFile = 4;
[result appendFormat:@"%@\t", NSLocalizedString(@"(not loaded)", @"value shown for hidden blob and text fields")];
else if ([cellData isKindOfClass:[NSData class]]) {
if(withBlobHandling == kBlobInclude) {
- NSString *displayString = [[NSString alloc] initWithData:cellData encoding:[mySQLConnection stringEncoding]];
- if (!displayString) displayString = [[NSString alloc] initWithData:cellData encoding:NSASCIIStringEncoding];
+ NSString *displayString;
+ if (hexBlobs)
+ displayString = [[NSString alloc] initWithFormat:@"0x%@", [cellData dataToHexString]];
+ else
+ displayString = [[NSString alloc] initWithData:cellData encoding:[mySQLConnection stringEncoding]];
+ if (!displayString) displayString = [[NSString alloc] initWithData:cellData encoding:NSISOLatin1StringEncoding];
if (displayString) {
[result appendFormat:@"%@\t", displayString];
[displayString release];
@@ -315,6 +320,7 @@ static const NSInteger kBlobAsImageFile = 4;
[fm createDirectoryAtPath:tmpBlobFileDirectory withIntermediateDirectories:YES attributes:nil error:nil];
}
+ BOOL hexBlobs = [prefs boolForKey:SPDisplayBinaryDataAsHex];
[selectedRows enumerateIndexesUsingBlock:^(NSUInteger rowIndex, BOOL * _Nonnull stop) {
for (NSUInteger c = 0; c < numColumns; c++ ) {
id cellData = SPDataStorageObjectAtRowAndColumn(tableStorage, rowIndex, columnMappings[c]);
@@ -328,8 +334,12 @@ static const NSInteger kBlobAsImageFile = 4;
[result appendFormat:@"\"%@\",", NSLocalizedString(@"(not loaded)", @"value shown for hidden blob and text fields")];
else if ([cellData isKindOfClass:[NSData class]]) {
if(withBlobHandling == kBlobInclude) {
- NSString *displayString = [[NSString alloc] initWithData:cellData encoding:[mySQLConnection stringEncoding]];
- if (!displayString) displayString = [[NSString alloc] initWithData:cellData encoding:NSASCIIStringEncoding];
+ NSString *displayString;
+ if (hexBlobs)
+ displayString = [[NSString alloc] initWithFormat:@"0x%@", [cellData dataToHexString]];
+ else
+ displayString = [[NSString alloc] initWithData:cellData encoding:[mySQLConnection stringEncoding]];
+ if (!displayString) displayString = [[NSString alloc] initWithData:cellData encoding:NSISOLatin1StringEncoding];
if (displayString) {
[result appendFormat:@"\"%@\",", displayString];
[displayString release];
@@ -620,6 +630,7 @@ static const NSInteger kBlobAsImageFile = 4;
Class nsDataClass = [NSData class];
Class spmysqlGeometryData = [SPMySQLGeometryData class];
NSStringEncoding connectionEncoding = [mySQLConnection stringEncoding];
+ BOOL hexBlobs = [prefs boolForKey:SPDisplayBinaryDataAsHex];
[selectedRows enumerateIndexesUsingBlock:^(NSUInteger rowIndex, BOOL * _Nonnull stop) {
for (NSUInteger c = 0; c < numColumns; c++ ) {
id cellData = SPDataStorageObjectAtRowAndColumn(tableStorage, rowIndex, columnMappings[c]);
@@ -632,10 +643,15 @@ static const NSInteger kBlobAsImageFile = 4;
else if ([cellData isSPNotLoaded])
[result appendFormat:@"%@\t", NSLocalizedString(@"(not loaded)", @"value shown for hidden blob and text fields")];
else if ([cellData isKindOfClass:nsDataClass]) {
- NSString *displayString = [[NSString alloc] initWithData:cellData encoding:connectionEncoding];
- if (!displayString) displayString = [[NSString alloc] initWithData:cellData encoding:NSASCIIStringEncoding];
+ NSString *displayString;
+ if (hexBlobs)
+ displayString = [[NSString alloc] initWithFormat:@"0x%@", [cellData dataToHexString]];
+ else
+ displayString = [[NSString alloc] initWithData:cellData encoding:connectionEncoding];
+ if (!displayString) displayString = [[NSString alloc] initWithData:cellData encoding:NSISOLatin1StringEncoding];
if (displayString) {
[result appendString:displayString];
+ [result appendString:@"\t"];
[displayString release];
}
}
@@ -749,7 +765,6 @@ static const NSInteger kBlobAsImageFile = 4;
- (NSUInteger)autodetectWidthForColumnDefinition:(NSDictionary *)columnDefinition maxRows:(NSUInteger)rowsToCheck
{
CGFloat columnBaseWidth;
- id contentString;
NSUInteger cellWidth, maxCellWidth, i;
NSRange linebreakRange;
double rowStep;
@@ -762,6 +777,7 @@ static const NSInteger kBlobAsImageFile = 4;
NSUInteger columnIndex = (NSUInteger)[[columnDefinition objectForKey:@"datacolumnindex"] integerValue];
NSDictionary *stringAttributes = @{NSFontAttributeName : tableFont};
Class spmysqlGeometryData = [SPMySQLGeometryData class];
+ BOOL hexBlobs = [prefs boolForKey:SPDisplayBinaryDataAsHex];
// Check the number of rows available to check, sampling every n rows
if ([tableStorage count] < rowsToCheck)
@@ -779,7 +795,7 @@ static const NSInteger kBlobAsImageFile = 4;
for (i = 0; i < rowsToCheck; i += rowStep) {
// Retrieve part of the cell's content to get widths, topping out at a maximum length
- contentString = SPDataStoragePreviewAtRowAndColumn(tableStorage, i, columnIndex, 500);
+ id contentString = SPDataStoragePreviewAtRowAndColumn(tableStorage, i, columnIndex, 500);
// If the cell hasn't loaded yet, skip processing
if (!contentString)
@@ -801,7 +817,10 @@ static const NSInteger kBlobAsImageFile = 4;
// Otherwise, ensure the cell is represented as a short string
if ([contentString isKindOfClass:[NSData class]]) {
- contentString = [contentString shortStringRepresentationUsingEncoding:[mySQLConnection stringEncoding]];
+ if (hexBlobs)
+ contentString = [[NSString alloc] initWithFormat:@"0x%@", [(NSData *)contentString dataToHexString]];
+ else
+ contentString = [contentString shortStringRepresentationUsingEncoding:[mySQLConnection stringEncoding]];
} else if ([(NSString *)contentString length] > 500) {
contentString = [contentString substringToIndex:500];
}
diff --git a/Source/SPCustomQuery.h b/Source/SPCustomQuery.h
index e976f716..7491b304 100644
--- a/Source/SPCustomQuery.h
+++ b/Source/SPCustomQuery.h
@@ -57,16 +57,14 @@
@class SPMySQLConnection;
@class SPMySQLStreamingResultStore;
@class SPTextView;
-
-#ifdef SP_CODA
@class SPDatabaseDocument;
@class SPTablesList;
-#endif
+
@interface SPCustomQuery : NSObject <NSTableViewDataSource, NSWindowDelegate, NSTableViewDelegate, SPDatabaseContentViewDelegate>
{
- IBOutlet id tableDocumentInstance;
- IBOutlet id tablesListInstance;
+ IBOutlet SPDatabaseDocument *tableDocumentInstance;
+ IBOutlet SPTablesList *tablesListInstance;
#ifndef SP_CODA
IBOutlet id queryFavoritesButton;
@@ -160,8 +158,6 @@
SPDataStorage *resultData;
pthread_mutex_t resultDataLock;
- NSCondition *resultLoadingCondition;
- NSInteger resultDataCount;
NSArray *cqColumnDefinition;
NSString *lastExecutedQuery;
NSInteger editedRow;
diff --git a/Source/SPCustomQuery.m b/Source/SPCustomQuery.m
index 6f79948d..c14c275d 100644
--- a/Source/SPCustomQuery.m
+++ b/Source/SPCustomQuery.m
@@ -61,6 +61,7 @@
#import "SPAppController.h"
#import "SPBundleHTMLOutputController.h"
#endif
+#import "SPFunctions.h"
#import <pthread.h>
#import <SPMySQL/SPMySQL.h>
@@ -69,6 +70,7 @@
- (id)_resultDataItemAtRow:(NSInteger)row columnIndex:(NSUInteger)column preserveNULLs:(BOOL)preserveNULLs asPreview:(BOOL)asPreview;
+ (NSString *)linkToHelpTopic:(NSString *)aTopic;
+- (void)documentWillClose:(NSNotification *)notification;
@end
@@ -802,7 +804,7 @@
// Reload table list if at least one query began with drop, alter, rename, or create
if(tableListNeedsReload || databaseWasChanged) {
// Build database pulldown menu
- [tableDocumentInstance setDatabases:self];
+ [[tableDocumentInstance onMainThread] setDatabases:self];
if (databaseWasChanged)
// Reset the current database
@@ -884,7 +886,7 @@
(long)totalAffectedRows
];
}
- if(resultDataCount) {
+ if([resultData count]) {
// we were running a query that returns a result set (ie. SELECT).
// TODO: mysql_query() returns as soon as the first result row is found (which might be pretty soon when using indexes / not doing aggregations)
// and that makes our query time measurement pretty useless (see #264)
@@ -905,7 +907,7 @@
#endif
// If no results were returned, redraw the empty table and post notifications before returning.
- if ( !resultDataCount ) {
+ if ( ![resultData count] ) {
[customQueryView performSelectorOnMainThread:@selector(reloadData) withObject:nil waitUntilDone:YES];
// Notify any listeners that the query has completed
@@ -931,8 +933,6 @@
return;
}
- [[customQueryView onMainThread] reloadData];
-
// Restore the result view origin if appropriate
if (!NSEqualRects(selectionViewportToRestore, NSZeroRect)) {
@@ -976,12 +976,12 @@
*/
- (void)updateResultStore:(SPMySQLStreamingResultStore *)theResultStore
{
-
- // Remove all items from the table
- resultDataCount = 0;
- [customQueryView performSelectorOnMainThread:@selector(noteNumberOfRowsChanged) withObject:nil waitUntilDone:YES];
pthread_mutex_lock(&resultDataLock);
- [resultData removeAllRows];
+ // Remove all items from the table
+ SPMainQSync(^{
+ [resultData removeAllRows];
+ [customQueryView noteNumberOfRowsChanged];
+ });
// Add the new store
[resultData setDataStorage:theResultStore updatingExisting:NO];
@@ -993,16 +993,9 @@
// Set up the table updates timer and wait for it to notify this thread about completion
[[self onMainThread] initQueryLoadTimer];
- [resultLoadingCondition lock];
- while (![resultData dataDownloaded]) {
- [resultLoadingCondition waitUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.05]];
- }
- [resultLoadingCondition unlock];
-
- // If the final column autoresize wasn't performed, perform it
- if (queryLoadLastRowCount < 200) [[self onMainThread] autosizeColumns];
-
- [customQueryView performSelectorOnMainThread:@selector(noteNumberOfRowsChanged) withObject:nil waitUntilDone:NO];
+ [resultData awaitDataDownloaded];
+
+ // Any further UI updates are the responsibility of the timer callback
}
/**
@@ -1492,23 +1485,20 @@
*/
- (void) queryLoadUpdate:(NSTimer *)theTimer
{
- resultDataCount = [resultData count];
-
+ NSUInteger resultDataCount = [resultData count];
+
if (queryLoadTimerTicksSinceLastUpdate < queryLoadInterfaceUpdateInterval) {
queryLoadTimerTicksSinceLastUpdate++;
return;
}
if ([resultData dataDownloaded]) {
- [resultLoadingCondition lock];
- [resultLoadingCondition signal];
[self clearQueryLoadTimer];
- [resultLoadingCondition unlock];
}
// Check whether a table update is required, based on whether new rows are
// available to display.
- if (resultDataCount == (NSInteger)queryLoadLastRowCount) {
+ if (resultDataCount == queryLoadLastRowCount) {
return;
}
@@ -1555,7 +1545,7 @@
*/
- (NSUInteger)currentResultRowCount
{
- return resultDataCount;
+ return [resultData count];
}
/**
@@ -2084,7 +2074,7 @@
*/
- (NSInteger)numberOfRowsInTableView:(NSTableView *)aTableView
{
- return (aTableView == customQueryView) ? (resultData == nil) ? 0 : resultDataCount : 0;
+ return (aTableView == customQueryView) ? (resultData == nil) ? 0 : [resultData count] : 0;
}
/**
@@ -2109,7 +2099,7 @@
if (isWorking) {
pthread_mutex_lock(&resultDataLock);
- if (rowIndex < resultDataCount && columnIndex < [resultData columnCount]) {
+ if (SPIntS2U(rowIndex) < [resultData count] && columnIndex < [resultData columnCount]) {
showCellAsGray = [resultData cellIsNullOrUnloadedAtRow:rowIndex column:columnIndex];
} else {
showCellAsGray = YES;
@@ -2410,7 +2400,7 @@
// cases.
if (isWorking) {
pthread_mutex_lock(&resultDataLock);
- if (row < resultDataCount && (NSUInteger)[[aTableColumn identifier] integerValue] < [resultData columnCount]) {
+ if (SPIntS2U(row) < [resultData count] && (NSUInteger)[[aTableColumn identifier] integerValue] < [resultData columnCount]) {
theValue = [[SPDataStorageObjectAtRowAndColumn(resultData, row, [[aTableColumn identifier] integerValue]) copy] autorelease];
}
pthread_mutex_unlock(&resultDataLock);
@@ -3776,7 +3766,6 @@
#endif
// init tableView's data source
- resultDataCount = 0;
resultData = [[SPDataStorage alloc] init];
editedRow = -1;
@@ -3787,7 +3776,6 @@
runPrimaryActionButtonAsSelection = nil;
queryLoadTimer = nil;
- resultLoadingCondition = [NSCondition new];
prefs = [NSUserDefaults standardUserDefaults];
@@ -3999,6 +3987,10 @@
selector:@selector(endDocumentTaskForTab:)
name:SPDocumentTaskEndNotification
object:tableDocumentInstance];
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(documentWillClose:)
+ name:SPDocumentWillCloseNotification
+ object:tableDocumentInstance];
#ifndef SP_CODA
[prefs addObserver:self forKeyPath:SPGlobalResultTableFont options:NSKeyValueObservingOptionNew context:NULL];
@@ -4030,7 +4022,7 @@
if (isWorking) {
pthread_mutex_lock(&resultDataLock);
- if (row < resultDataCount && column < [resultData columnCount]) {
+ if (SPIntS2U(row) < [resultData count] && column < [resultData columnCount]) {
value = SPDataStoragePreviewAtRowAndColumn(resultData, row, column, 150);
}
@@ -4059,6 +4051,13 @@
return value;
}
+//this method is called right before the UI objects are deallocated
+- (void)documentWillClose:(NSNotification *)notification
+{
+ // if a result load is in progress we must stop the timer or it may try to call invalid IBOutlets
+ [self clearQueryLoadTimer];
+}
+
#pragma mark -
- (void)dealloc
@@ -4070,7 +4069,6 @@
[NSObject cancelPreviousPerformRequestsWithTarget:customQueryView];
[self clearQueryLoadTimer];
- SPClear(resultLoadingCondition);
SPClear(usedQuery);
SPClear(lastExecutedQuery);
SPClear(resultData);
diff --git a/Source/SPDataAdditions.h b/Source/SPDataAdditions.h
index cd8374f6..a446e315 100644
--- a/Source/SPDataAdditions.h
+++ b/Source/SPDataAdditions.h
@@ -43,6 +43,7 @@ typedef NS_OPTIONS(NSUInteger, SPLineTerminator) {
- (NSData *)dataEncryptedWithKey:(NSData *)aesKey IV:(NSData *)iv;
- (NSData *)dataDecryptedWithPassword:(NSString *)password;
- (NSData *)dataDecryptedWithKey:(NSData *)key;
++ (NSData *)dataWithHexString:(NSString *)hex;
- (NSData *)compress;
- (NSData *)decompress;
diff --git a/Source/SPDataAdditions.m b/Source/SPDataAdditions.m
index 65605577..19539cd0 100644
--- a/Source/SPDataAdditions.m
+++ b/Source/SPDataAdditions.m
@@ -149,6 +149,7 @@ uint32_t LimitUInt32(NSUInteger i);
unsigned char *lenPtr = paddedBytes + (paddedLength - 4);
memcpy(lenPtr, &bigIntDataLength, 4);
+ size_t bytesWritten;
CCCryptorStatus res = CCCrypt(
kCCEncrypt, // operation mode
kCCAlgorithmAES128, // algorithm
@@ -160,7 +161,7 @@ uint32_t LimitUInt32(NSUInteger i);
paddedLength, // length of raw data
paddedBytes, // output buffer. overwriting input is OK
paddedLength, // output buffer size
- NULL // number of bytes written. not relevant here
+ &bytesWritten // number of bytes written. not relevant here, but 10.6 fails if omitted
);
if(res != kCCSuccess)
@@ -168,7 +169,7 @@ uint32_t LimitUInt32(NSUInteger i);
reason:[NSString stringWithFormat:@"CCCrypt() failed! (CCCryptorStatus=%d)",res]
userInfo:@{@"cryptorStatus":@(res)}];
- // the return code of CCCrypt() is not always reliable, better check it again
+ // CVE-2016-4711: the return code of CCCrypt() is not always reliable, better check it again
if(memcmp(lenPtr, &bigIntDataLength, 4) == 0)
@throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Encrypted data is same as plaintext data!" userInfo:nil];
@@ -203,6 +204,7 @@ uint32_t LimitUInt32(NSUInteger i);
// Decrypt the data
unsigned char *decryptedBytes = calloc(1,encryptedLength);
+ size_t bytesRead;
CCCryptorStatus res = CCCrypt(
kCCDecrypt, // operation mode
kCCAlgorithmAES128, // algorithm
@@ -214,7 +216,7 @@ uint32_t LimitUInt32(NSUInteger i);
encryptedLength, // length of raw data
decryptedBytes, // output buffer. overwriting input is OK
encryptedLength, // output buffer size
- NULL // number of bytes written. not relevant here
+ &bytesRead // number of bytes decrypted. not relevant here, but 10.6 fails if omitted
);
if(res != kCCSuccess) {
@@ -342,6 +344,140 @@ uint32_t LimitUInt32(NSUInteger i);
}
/**
+ * Returns the integer value for a single hex-encoded nibble or -1 for invalid values.
+ * Supported characters: 0-9,a-f,A-F
+ *
+ * Note: You usually would call this method like ((hexchar2nibble(highByte) << 4) + hexchar2nibble(lowByte)) to decode a single hex-encoded byte.
+ */
+static int hexchar2nibble(char c)
+{
+ if (c >= '0' && c <= '9') return c - '0';
+ if (c >= 'a' && c <= 'f') return c - 'a' + 10;
+ if (c >= 'A' && c <= 'F') return c - 'A' + 10;
+ return -1;
+}
+
+/**
+ * Decodes a sequence of hex digits to raw byte values.
+ * This function is very strict about the allowed inputs and must only be used for validated inputs!
+ *
+ * - If numRawBytes != 0 and inBuffer == NULL or outBuffer == NULL, this will crash
+ * - The hex sequence must ONLY contain chars 0-9,a-f,A-F or the result will be undefined
+ * - The sequence must be padded to have an even length. numRawBytes is the number of bytes AFTER decoding, so inBuffer must be exactly 2x as large
+ * - inBuffer and outBuffer may be the same pointer
+ */
+static void decodeValidHexSequence(const char *inBuffer,uint8_t *outBuffer, NSUInteger numRawBytes)
+{
+ NSUInteger outIndex = 0;
+ NSUInteger srcIndex = 0;
+ while (outIndex < numRawBytes) {
+ uint8_t v = (hexchar2nibble(inBuffer[srcIndex]) << 4) + hexchar2nibble(inBuffer[srcIndex+1]);
+ outBuffer[outIndex++] = v;
+ srcIndex += 2;
+ }
+}
+
+/**
+ * Interpret a string of hex digits in 'hex' as hex data, and return
+ * an NSData representation of the data. Spaces are permitted within
+ * the string and an initial '0x' will be ignored. If bad input
+ * is detected, nil is returned.
+ *
+ * Alternatively the MySQL-style X'val' syntax is also supported,
+ * with the same restrictions as in MySQL:
+ * - val must always be an even number of characters
+ * - val cannot contain whitespace (whitespace before/after is ok)
+ * - The leading x is case-INsensitive
+ */
++ (NSData *)dataWithHexString:(NSString *)hex
+{
+ if(!hex) return nil; // no string
+ const char *sourceBytes = [hex UTF8String];
+
+ size_t length = strlen(sourceBytes); // keep in mind that [hex length] is the number of Unicode characters, not the number of bytes
+ if (length < 1) return [NSData data]; // empty string
+
+ NSUInteger srcIndex = 0;
+ NSData *data = nil;
+ NSUInteger nbytes;
+
+ //skip leading whitespace (in order to properly check for leading "0x")
+ while(srcIndex < length && (sourceBytes[srcIndex] == ' ' || sourceBytes[srcIndex] == '\t')) srcIndex++;
+
+ // bypass initial 0x
+ if(srcIndex+1 < length && sourceBytes[srcIndex] == '0' && sourceBytes[srcIndex+1] == 'x' ) {
+ srcIndex += 2;
+ }
+ //check for mysql syntax
+ else if(srcIndex+2 < length && (sourceBytes[srcIndex] == 'x' || sourceBytes[srcIndex] == 'X') && sourceBytes[srcIndex+1] == '\'') {
+ srcIndex += 2;
+ //look for the terminating quote
+ NSUInteger startIndex = srcIndex;
+ NSUInteger endIndex = startIndex; //startIndex points to the first character inside the quotes, which may already be the terminating quote
+ while(endIndex < length) {
+ char c = sourceBytes[endIndex];
+ //if we've hit the terminator, verify that only whitespace follows and stop reading
+ if(c == '\'') {
+ NSUInteger afterIndex = endIndex+1;
+ while (afterIndex < length) {
+ c = sourceBytes[afterIndex++];
+ if(c != ' ' && c != '\t') return nil;
+ }
+ break;
+ }
+ endIndex++;
+ // Check for non-hex characters
+ if (hexchar2nibble(c) < 0) return nil;
+ }
+ // Check for unterminated sequence and uneven number of bytes
+ NSUInteger n = endIndex - startIndex;
+ if(endIndex == length || ((n % 2) != 0)) return nil;
+ // shortcut
+ if(n == 0) return [NSData data];
+ //looks good, create the output buffer and decode
+ nbytes = n / 2;
+ unsigned char *outBuf = malloc(nbytes);
+ decodeValidHexSequence(&sourceBytes[startIndex], outBuf, nbytes);
+ return [NSData dataWithBytesNoCopy:outBuf length:nbytes freeWhenDone:YES];
+ }
+
+ // Copy input while removing spaces and tabs.
+ char *trimmedFull = (char *)malloc(length + 1);
+ char *trimmed = (trimmedFull + 1); //we'll use the first byte in case we have to fill in a leading '0'
+ NSUInteger trimIndex = 0;
+ NSUInteger n = 0; // n = # of hex digits
+ while(srcIndex < length) {
+ char c = sourceBytes[srcIndex++];
+ if(c == ' ' || c == '\t') continue;
+ trimmed[trimIndex++] = c;
+ if(!c) break;
+ n++;
+ // Check for non-hex characters
+ if (hexchar2nibble(c) < 0) goto fail_cleanup;
+ }
+ //shortcut
+ if(n == 0) {
+ data = [NSData data];
+ goto fail_cleanup;
+ }
+
+ BOOL isEven = ((n % 2) == 0);
+ nbytes = !isEven ? (n + 1) / 2 : n / 2; //adjust for cases where "0aff" is written as "aff" (e.g.)
+ if(!isEven) {
+ trimmed--;
+ trimmed[0] = '0';
+ }
+
+ //we'll just decode the data in-place since the raw values have to be shorter by definition, anyway
+ decodeValidHexSequence(trimmed, (uint8_t *)trimmedFull, nbytes);
+ return [NSData dataWithBytesNoCopy:trimmedFull length:nbytes freeWhenDone:YES];
+
+fail_cleanup:
+ free(trimmedFull);
+ return data;
+}
+
+/**
* Returns the hex representation of the given data.
*/
- (NSString *)dataToFormattedHexString
diff --git a/Source/SPDataImport.m b/Source/SPDataImport.m
index 03b83c6c..4c680801 100644
--- a/Source/SPDataImport.m
+++ b/Source/SPDataImport.m
@@ -162,6 +162,7 @@
{
SPMainQSync(^{
[NSApp endSheet:singleProgressSheet];
+ [singleProgressBar setIndeterminate:YES];
[singleProgressSheet orderOut:nil];
[singleProgressBar stopAnimation:self];
[singleProgressBar setMaxValue:100];
@@ -397,16 +398,12 @@
fileTotalLength = (NSUInteger)[[[[NSFileManager defaultManager] attributesOfItemAtPath:filename error:NULL] objectForKey:NSFileSize] longLongValue];
if (!fileTotalLength) fileTotalLength = 1;
- // If importing a bzipped file, use indeterminate progress bars as no progress is available
- BOOL useIndeterminate = NO;
- if ([sqlFileHandle compressionFormat] == SPBzip2Compression) useIndeterminate = YES;
-
SPMainQSync(^{
// Reset progress interface
[errorsView setString:@""];
[singleProgressTitle setStringValue:NSLocalizedString(@"Importing SQL", @"text showing that the application is importing SQL")];
[singleProgressText setStringValue:NSLocalizedString(@"Reading...", @"text showing that app is reading dump")];
- [singleProgressBar setIndeterminate:useIndeterminate];
+ [singleProgressBar setIndeterminate:NO];
[singleProgressBar setMaxValue:fileTotalLength];
[singleProgressBar setUsesThreadedAnimation:YES];
[singleProgressBar startAnimation:self];
@@ -690,10 +687,10 @@
}
// Update available databases
- [tableDocumentInstance setDatabases:self];
+ [[tableDocumentInstance onMainThread] setDatabases:self];
// Update current selected database
- [[tableDocumentInstance onMainThread] refreshCurrentDatabase];
+ [tableDocumentInstance refreshCurrentDatabase];
// Update current database tables
[tablesListInstance updateTables:self];
@@ -787,16 +784,12 @@
if (!fileTotalLength) fileTotalLength = 1;
fileIsCompressed = ([csvFileHandle compressionFormat] != SPNoCompression);
- // If importing a bzipped file, use indeterminate progress bars as no progress is available
- BOOL useIndeterminate = NO;
- if ([csvFileHandle compressionFormat] == SPBzip2Compression) useIndeterminate = YES;
-
// Reset progress interface
SPMainQSync(^{
[errorsView setString:@""];
[singleProgressTitle setStringValue:NSLocalizedString(@"Importing CSV", @"text showing that the application is importing CSV")];
[singleProgressText setStringValue:NSLocalizedString(@"Reading...", @"text showing that app is reading dump")];
- [singleProgressBar setIndeterminate:YES];
+ [singleProgressBar setIndeterminate:NO];
[singleProgressBar setUsesThreadedAnimation:YES];
[singleProgressBar startAnimation:self];
@@ -971,8 +964,8 @@
// Reset progress interface and open the progress sheet
SPMainQSync(^{
- [singleProgressBar setIndeterminate:useIndeterminate];
[singleProgressBar setMaxValue:fileTotalLength];
+ [singleProgressBar setIndeterminate:NO];
[singleProgressBar startAnimation:self];
[NSApp beginSheet:singleProgressSheet modalForWindow:[tableDocumentInstance parentWindow] modalDelegate:self didEndSelector:nil contextInfo:nil];
[singleProgressSheet makeKeyWindow];
@@ -1213,29 +1206,30 @@
document:tableDocumentInstance
notificationName:@"Import Finished"];
+ SPMainQSync(^{
+ if(importIntoNewTable) {
- if(importIntoNewTable) {
-
- // Select the new table
-
- // Update current database tables
- [[tablesListInstance onMainThread] updateTables:self];
-
- // Re-query the structure of all databases in the background
- [[tableDocumentInstance databaseStructureRetrieval] queryDbStructureInBackgroundWithUserInfo:@{@"forceUpdate" : @YES}];
-
- // Select the new table
- [tablesListInstance selectItemWithName:selectedTableTarget];
-
- } else {
-
- // If import was done into a new table or the table selected for import is also selected in the content view,
- // update the content view - on the main thread to avoid crashes.
- if ([tablesListInstance tableName] && [selectedTableTarget isEqualToString:[tablesListInstance tableName]]) {
- [tableDocumentInstance setContentRequiresReload:YES];
+ // Select the new table
+
+ // Update current database tables
+ [tablesListInstance updateTables:self];
+
+ // Re-query the structure of all databases in the background
+ [[tableDocumentInstance databaseStructureRetrieval] queryDbStructureInBackgroundWithUserInfo:@{@"forceUpdate" : @YES}];
+
+ // Select the new table
+ [tablesListInstance selectItemWithName:selectedTableTarget];
+
+ } else {
+
+ // If import was done into a new table or the table selected for import is also selected in the content view,
+ // update the content view - on the main thread to avoid crashes.
+ if ([tablesListInstance tableName] && [selectedTableTarget isEqualToString:[tablesListInstance tableName]]) {
+ [tableDocumentInstance setContentRequiresReload:YES];
+ }
+
}
-
- }
+ });
}
diff --git a/Source/SPDataStorage.h b/Source/SPDataStorage.h
index bb45f71b..23c7e124 100644
--- a/Source/SPDataStorage.h
+++ b/Source/SPDataStorage.h
@@ -44,6 +44,7 @@
SPMySQLStreamingResultStore *dataStorage;
NSPointerArray *editedRows;
BOOL *unloadedColumns;
+ NSCondition *dataDownloadedLock;
NSUInteger numberOfColumns;
NSUInteger editedRowCount;
@@ -75,6 +76,12 @@
- (NSUInteger) columnCount;
- (BOOL) dataDownloaded;
+/**
+ * This method will block the caller until -dataDownloaded returns YES.
+ * Multiple parallel calls from different threads are possible.
+ */
+- (void) awaitDataDownloaded;
+
/* Delegate callback methods */
- (void)resultStoreDidFinishLoadingData:(SPMySQLStreamingResultStore *)resultStore;
diff --git a/Source/SPDataStorage.m b/Source/SPDataStorage.m
index fca33c0a..5db56b1e 100644
--- a/Source/SPDataStorage.m
+++ b/Source/SPDataStorage.m
@@ -37,6 +37,7 @@
@interface SPDataStorage (Private_API)
- (void) _checkNewRow:(NSMutableArray *)aRow;
+- (void) _addRowUnsafeUnchecked:(NSMutableArray *)aRow;
@end
@@ -58,33 +59,38 @@ static inline NSMutableArray* SPDataStorageGetEditedRow(NSPointerArray* rowStore
*/
- (void) setDataStorage:(SPMySQLStreamingResultStore *)newDataStorage updatingExisting:(BOOL)updateExistingStore
{
- SPMySQLStreamingResultStore *oldDataStorage = dataStorage;
+ BOOL *oldUnloadedColumns;
+ NSPointerArray *oldEditedRows;
+ SPMySQLStreamingResultStore *oldDataStorage;
+
+ @synchronized(self) {
+ oldDataStorage = dataStorage;
- if (oldDataStorage) {
- // If the table is reloading data, link to the current data store for smoother loads
- if (updateExistingStore) {
- [newDataStorage replaceExistingResultStore:oldDataStorage];
+ if (oldDataStorage) {
+ // If the table is reloading data, link to the current data store for smoother loads
+ if (updateExistingStore) {
+ [newDataStorage replaceExistingResultStore:oldDataStorage];
+ }
}
- }
- [newDataStorage retain];
+ [newDataStorage retain];
- NSPointerArray *newEditedRows = [[NSPointerArray alloc] init];
- NSUInteger newNumberOfColumns = [newDataStorage numberOfFields];
- BOOL *newUnloadedColumns = calloc(newNumberOfColumns, sizeof(BOOL));
- for (NSUInteger i = 0; i < newNumberOfColumns; i++) {
- newUnloadedColumns[i] = NO;
- }
-
- BOOL *oldUnloadedColumns = unloadedColumns;
- NSPointerArray *oldEditedRows = editedRows;
- @synchronized(self) {
+ NSPointerArray *newEditedRows = [[NSPointerArray alloc] init];
+ NSUInteger newNumberOfColumns = [newDataStorage numberOfFields];
+ BOOL *newUnloadedColumns = calloc(newNumberOfColumns, sizeof(BOOL));
+ for (NSUInteger i = 0; i < newNumberOfColumns; i++) {
+ newUnloadedColumns[i] = NO;
+ }
+
+ oldUnloadedColumns = unloadedColumns;
+ oldEditedRows = editedRows;
dataStorage = newDataStorage;
numberOfColumns = newNumberOfColumns;
unloadedColumns = newUnloadedColumns;
editedRowCount = 0;
editedRows = newEditedRows;
}
+
free(oldUnloadedColumns);
[oldEditedRows release];
[oldDataStorage release];
@@ -107,6 +113,7 @@ static inline NSMutableArray* SPDataStorageGetEditedRow(NSPointerArray* rowStore
/**
* Return a mutable array containing the data for a specified row.
+ * The returned array will be a shallow copy of the internal row object.
*/
- (NSMutableArray *) rowContentsAtIndex:(NSUInteger)anIndex
{
@@ -117,12 +124,12 @@ static inline NSMutableArray* SPDataStorageGetEditedRow(NSPointerArray* rowStore
NSMutableArray *editedRow = SPDataStorageGetEditedRow(editedRows, anIndex);
if (editedRow != NULL) {
- return editedRow;
+ return [NSMutableArray arrayWithArray:editedRow]; //make a copy to not give away control of our internal state
}
}
// Otherwise, prepare to return the underlying storage row
- NSMutableArray *dataArray = SPMySQLResultStoreGetRow(dataStorage, anIndex);
+ NSMutableArray *dataArray = SPMySQLResultStoreGetRow(dataStorage, anIndex); //returned array is already a copy
// Modify unloaded cells as appropriate
for (NSUInteger i = 0; i < numberOfColumns; i++) {
@@ -252,11 +259,14 @@ static inline NSMutableArray* SPDataStorageGetEditedRow(NSPointerArray* rowStore
// If an edited row exists for the supplied index, use that; otherwise use the underlying
// storage row
if (state->state < editedRowCount) {
- targetRow = SPDataStorageGetEditedRow(editedRows, state->state);
+ NSMutableArray *internalRow = SPDataStorageGetEditedRow(editedRows, state->state);
+ if(internalRow != NULL) {
+ targetRow = [NSMutableArray arrayWithArray:internalRow]; //make a copy to not give away control of our internal state
+ }
}
if (targetRow == nil) {
- targetRow = SPMySQLResultStoreGetRow(dataStorage, state->state);
+ targetRow = SPMySQLResultStoreGetRow(dataStorage, state->state); //returned array is already a copy
// Modify unloaded cells as appropriate
for (NSUInteger i = 0; i < numberOfColumns; i++) {
@@ -287,16 +297,18 @@ static inline NSMutableArray* SPDataStorageGetEditedRow(NSPointerArray* rowStore
*/
- (void) addRowWithContents:(NSMutableArray *)aRow
{
- @synchronized(self) {
- // Verify the row is of the correct length
- [self _checkNewRow:aRow];
-
- // Add the new row to the editable store
- [editedRows addPointer:aRow];
- editedRowCount++;
-
- // Update the underlying store as well to keep counts correct
- [dataStorage addDummyRow];
+ // we can't just store the passed in array as that would give an outsider too much control of our internal state
+ // (e.g. they could change the bounds after adding it, defeating the check below), so let's make a shallow copy.
+ NSMutableArray *newArray = [[NSMutableArray alloc] initWithArray:aRow];
+ @try {
+ @synchronized(self) {
+ // Verify the row is of the correct length
+ [self _checkNewRow:newArray];
+ [self _addRowUnsafeUnchecked:newArray];
+ }
+ }
+ @finally {
+ [newArray release];
}
}
@@ -307,39 +319,58 @@ static inline NSMutableArray* SPDataStorageGetEditedRow(NSPointerArray* rowStore
*/
- (void) insertRowContents:(NSMutableArray *)aRow atIndex:(NSUInteger)anIndex
{
- @synchronized(self) {
- unsigned long long numberOfRows = SPMySQLResultStoreGetRowCount(dataStorage);
-
- // Verify the row is of the correct length
- [self _checkNewRow:aRow];
-
- // Throw an exception if the index is out of bounds
- if (anIndex > numberOfRows) {
- [NSException raise:NSRangeException format:@"Requested storage index (%llu) beyond bounds (%llu)", (unsigned long long)anIndex, numberOfRows];
- }
-
- // If "inserting" at the end of the array just add a row
- if (anIndex == numberOfRows) {
- return [self addRowWithContents:aRow];
+ // we can't just store the passed in array as that would give an outsider too much control of our internal state
+ // (e.g. they could change the bounds after adding it, defeating the check below), so let's make a shallow copy.
+ NSMutableArray *newArray = [[NSMutableArray alloc] initWithArray:aRow];
+ @try {
+ @synchronized(self) {
+ unsigned long long numberOfRows = SPMySQLResultStoreGetRowCount(dataStorage);
+
+ // Verify the row is of the correct length
+ [self _checkNewRow:newArray];
+
+ // Throw an exception if the index is out of bounds
+ if (anIndex > numberOfRows) {
+ [NSException raise:NSRangeException format:@"Requested storage index (%llu) beyond bounds (%llu)", (unsigned long long)anIndex, numberOfRows];
+ }
+
+ // If "inserting" at the end of the array just add a row
+ if (anIndex == numberOfRows) {
+ [self _addRowUnsafeUnchecked:newArray];
+ return;
+ }
+
+ // Add the new row to the editable store
+ [editedRows insertPointer:newArray atIndex:anIndex];
+ editedRowCount++;
+
+ // Update the underlying store to keep counts and indices correct
+ [dataStorage insertDummyRowAtIndex:anIndex];
}
-
- // Add the new row to the editable store
- [editedRows insertPointer:aRow atIndex:anIndex];
- editedRowCount++;
-
- // Update the underlying store to keep counts and indices correct
- [dataStorage insertDummyRowAtIndex:anIndex];
+ }
+ @finally {
+ [newArray release];
}
}
/**
* Replace a row with contents of the supplied NSArray.
+ *
+ * Note that the supplied objects within the array are retained as a reference rather than copied.
*/
- (void) replaceRowAtIndex:(NSUInteger)anIndex withRowContents:(NSMutableArray *)aRow
{
- @synchronized(self) {
- [self _checkNewRow:aRow];
- [editedRows replacePointerAtIndex:anIndex withPointer:aRow];
+ // we can't just store the passed in array as that would give an outsider too much control of our internal state
+ // (e.g. they could change the bounds after adding it, defeating the check below), so let's make a shallow copy.
+ NSMutableArray *newArray = [[NSMutableArray alloc] initWithArray:aRow];
+ @try {
+ @synchronized(self) {
+ [self _checkNewRow:newArray];
+ [editedRows replacePointerAtIndex:anIndex withPointer:newArray];
+ }
+ }
+ @finally {
+ [newArray release];
}
}
@@ -357,7 +388,7 @@ static inline NSMutableArray* SPDataStorageGetEditedRow(NSPointerArray* rowStore
// Make sure that the row in question is editable
if (editableRow == nil) {
- editableRow = [self rowContentsAtIndex:rowIndex];
+ editableRow = [self rowContentsAtIndex:rowIndex]; //already returns a copy, so we don't have to go via -replaceRowAtIndex:withRowContents:
[editedRows replacePointerAtIndex:rowIndex withPointer:editableRow];
}
}
@@ -468,6 +499,13 @@ static inline NSMutableArray* SPDataStorageGetEditedRow(NSPointerArray* rowStore
}
}
+- (void) awaitDataDownloaded
+{
+ [dataDownloadedLock lock];
+ while(![self dataDownloaded]) [dataDownloadedLock wait];
+ [dataDownloadedLock unlock];
+}
+
#pragma mark - Delegate callback methods
/**
@@ -476,9 +514,16 @@ static inline NSMutableArray* SPDataStorageGetEditedRow(NSPointerArray* rowStore
- (void)resultStoreDidFinishLoadingData:(SPMySQLStreamingResultStore *)resultStore
{
@synchronized(self) {
+ if(resultStore != dataStorage) {
+ NSLog(@"%s: received delegate callback from an unknown result store %p (expected: %p). Ignored!", __PRETTY_FUNCTION__, resultStore, dataStorage);
+ return;
+ }
[editedRows setCount:(NSUInteger)[resultStore numberOfRows]];
editedRowCount = [editedRows count];
}
+ [dataDownloadedLock lock];
+ [dataDownloadedLock broadcast];
+ [dataDownloadedLock unlock];
}
/**
@@ -492,6 +537,7 @@ static inline NSMutableArray* SPDataStorageGetEditedRow(NSPointerArray* rowStore
dataStorage = nil;
editedRows = nil;
unloadedColumns = NULL;
+ dataDownloadedLock = [NSCondition new];
numberOfColumns = 0;
editedRowCount = 0;
@@ -504,6 +550,7 @@ static inline NSMutableArray* SPDataStorageGetEditedRow(NSPointerArray* rowStore
@synchronized(self) {
SPClear(dataStorage);
SPClear(editedRows);
+ SPClear(dataDownloadedLock);
if (unloadedColumns) {
free(unloadedColumns), unloadedColumns = NULL;
}
@@ -524,4 +571,16 @@ static inline NSMutableArray* SPDataStorageGetEditedRow(NSPointerArray* rowStore
}
}
+// DO NOT CALL THIS METHOD UNLESS YOU CURRENTLY HAVE A LOCK ON SELF!!!
+// DO NOT CALL THIS METHOD UNLESS YOU HAVE CALLED _checkNewRow: FIRST!
+- (void)_addRowUnsafeUnchecked:(NSMutableArray *)aRow
+{
+ // Add the new row to the editable store
+ [editedRows addPointer:aRow];
+ editedRowCount++;
+
+ // Update the underlying store as well to keep counts correct
+ [dataStorage addDummyRow];
+}
+
@end
diff --git a/Source/SPDatabaseDocument.m b/Source/SPDatabaseDocument.m
index 08b2737d..2581967c 100644
--- a/Source/SPDatabaseDocument.m
+++ b/Source/SPDatabaseDocument.m
@@ -29,13 +29,6 @@
//
// More info at <https://github.com/sequelpro/sequelpro>
-// Forward-declare for 10.7 compatibility
-#if !defined(MAC_OS_X_VERSION_10_7) || MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_7
-enum {
- NSFullScreenWindowMask = 1 << 14
-};
-#endif
-
#import "SPDatabaseDocument.h"
#import "SPConnectionController.h"
#import "SPConnectionHandler.h"
@@ -410,6 +403,10 @@ static int64_t SPDatabaseDocumentInstanceCounter = 0;
#pragma mark -
#pragma mark Connection callback and methods
+/**
+ *
+ * This method *MUST* be called from the UI thread!
+ */
- (void)setConnection:(SPMySQLConnection *)theConnection
{
if ([theConnection userTriggeredDisconnect]) {
@@ -427,7 +424,10 @@ static int64_t SPDatabaseDocumentInstanceCounter = 0;
#ifndef SP_CODA
// Set the fileURL and init the preferences (query favs, filters, and history) if available for that URL
- [self setFileURL:[[SPQueryController sharedQueryController] registerDocumentWithFileURL:[self fileURL] andContextInfo:spfPreferences]];
+ NSURL *newURL = [[SPQueryController sharedQueryController] registerDocumentWithFileURL:[self fileURL] andContextInfo:spfPreferences];
+#warning debug code for #2266
+ if(!newURL) NSLog(@"#2266: Trying to set nil fileURL in %s from queryController=%@ oldFileURL=%@ contextInfo=%@", __func__, [SPQueryController sharedQueryController], [self fileURL], spfPreferences);
+ [self setFileURL:newURL];
// ...but hide the icon while the document is temporary
if ([self isUntitled]) [[parentWindow standardWindowButton:NSWindowDocumentIconButton] setImage:nil];
@@ -614,6 +614,8 @@ static int64_t SPDatabaseDocumentInstanceCounter = 0;
/**
* sets up the database select toolbar item
+ *
+ * This method *MUST* be called from the UI thread!
*/
- (IBAction)setDatabases:(id)sender;
{
@@ -1168,6 +1170,8 @@ static int64_t SPDatabaseDocumentInstanceCounter = 0;
/**
* Reset the current selected database name
+ *
+ * This method MAY be called from UI and background threads!
*/
- (void)refreshCurrentDatabase
{
@@ -1185,23 +1189,21 @@ static int64_t SPDatabaseDocumentInstanceCounter = 0;
dbName = NSArrayObjectAtIndex(eachRow, 0);
}
- // TODO: there have been crash reports because dbName == nil at this point. When could that happen?
- if(dbName && ![dbName isNSNull]) {
- if(![dbName isEqualToString:selectedDatabase]) {
+ SPMainQSync(^{
+ // TODO: there have been crash reports because dbName == nil at this point. When could that happen?
+ if(dbName && ![dbName isNSNull]) {
+ if(![dbName isEqualToString:selectedDatabase]) {
+ if (selectedDatabase) SPClear(selectedDatabase);
+ selectedDatabase = [[NSString alloc] initWithString:dbName];
+ [chooseDatabaseButton selectItemWithTitle:selectedDatabase];
+ [self updateWindowTitle:self];
+ }
+ } else {
if (selectedDatabase) SPClear(selectedDatabase);
- selectedDatabase = [[NSString alloc] initWithString:dbName];
- [chooseDatabaseButton selectItemWithTitle:selectedDatabase];
-#ifndef SP_CODA /* [self updateWindowTitle:self] */
+ [chooseDatabaseButton selectItemAtIndex:0];
[self updateWindowTitle:self];
-#endif
}
- } else {
- if (selectedDatabase) SPClear(selectedDatabase);
- [chooseDatabaseButton selectItemAtIndex:0];
-#ifndef SP_CODA /* [self updateWindowTitle:self] */
- [self updateWindowTitle:self];
-#endif
- }
+ });
}
//query finished
@@ -3534,7 +3536,10 @@ static int64_t SPDatabaseDocumentInstanceCounter = 0;
[preferences setObject:[spfStructure objectForKey:SPContentFilters] forKey:SPContentFilters];
[[SPQueryController sharedQueryController] registerDocumentWithFileURL:[NSURL fileURLWithPath:fileName] andContextInfo:preferences];
- [self setFileURL:[NSURL fileURLWithPath:fileName]];
+ NSURL *newURL = [NSURL fileURLWithPath:fileName];
+#warning debug code for #2266
+ if(!newURL) NSLog(@"#2266: Trying to set nil fileURL in %s from fileName=%@", __func__, fileName);
+ [self setFileURL:newURL];
[[NSDocumentController sharedDocumentController] noteNewRecentDocumentURL:[NSURL fileURLWithPath:fileName]];
[self updateWindowTitle:self];
@@ -3978,10 +3983,11 @@ static int64_t SPDatabaseDocumentInstanceCounter = 0;
if (newIsVisible == windowTitleStatusViewIsVisible) return;
if (newIsVisible) {
- if (NSClassFromString(@"NSTitlebarAccessoryViewController")) { // OS X 10.11 and later
+ Class controllerClass;
+ if ((controllerClass = NSClassFromString(@"NSTitlebarAccessoryViewController"))) { // OS X 10.11 and later
[titleAccessoryView setFrame:NSMakeRect(0, 0, titleAccessoryView.frame.size.width, 120)]; // make it really tall, so that it's on the top right of the title/toolbar area, instead of the bottom right (AppKit will not prevent it from going behind the toolbar)
- NSTitlebarAccessoryViewController *accessoryViewController = [[[NSTitlebarAccessoryViewController alloc] init] autorelease];
+ NSTitlebarAccessoryViewController *accessoryViewController = [[[controllerClass alloc] init] autorelease];
accessoryViewController.view = titleAccessoryView;
accessoryViewController.layoutAttribute = NSLayoutAttributeRight;
[parentWindow addTitlebarAccessoryViewController:accessoryViewController];
@@ -4462,6 +4468,7 @@ static int64_t SPDatabaseDocumentInstanceCounter = 0;
{
// Coax the main split view into actually checking its constraints
[contentViewSplitter setPosition:[[[contentViewSplitter subviews] objectAtIndex:0] bounds].size.width ofDividerAtIndex:0];
+
// If the task interface is visible, and this tab is frontmost, re-center the task child window
if (_isWorkingLevel && [parentWindowController selectedTableDocument] == self) [self centerTaskWindow];
}
@@ -4470,17 +4477,19 @@ static int64_t SPDatabaseDocumentInstanceCounter = 0;
/**
* Set the parent window
*/
-- (void)setParentWindow:(NSWindow *)aWindow
+- (void)setParentWindow:(NSWindow *)window
{
-#ifndef SP_CODA
+ NSWindow *favoritesOutlineViewWindow = [(NSView *)[connectionController favoritesOutlineView] window];
+
// If the window is being set for the first time - connection controller is visible - update focus
- if (!parentWindow && !mySQLConnection) {
- [aWindow makeFirstResponder:(NSResponder *)[connectionController favoritesOutlineView]];
+ if (!parentWindow && !mySQLConnection && window == favoritesOutlineViewWindow) {
+ [window makeFirstResponder:(NSResponder *)[connectionController favoritesOutlineView]];
}
-#endif
- parentWindow = aWindow;
+ parentWindow = window;
+
SPSSHTunnel *currentTunnel = [connectionController valueForKeyPath:@"sshTunnel"];
+
if (currentTunnel) [currentTunnel setParentWindow:parentWindow];
}
@@ -5088,7 +5097,10 @@ static int64_t SPDatabaseDocumentInstanceCounter = 0;
}
if (![self isSaveInBundle]) {
- [self setFileURL:[NSURL fileURLWithPath:path]];
+ NSURL *newURL = [NSURL fileURLWithPath:path];
+#warning debug code for #2266
+ if(!newURL) NSLog(@"#2266: Trying to set nil fileURL in %s from path=%@", __func__, path);
+ [self setFileURL:newURL];
}
[spfDocData setObject:[NSNumber numberWithBool:([[data objectForKey:@"connection"] objectForKey:@"password"]) ? YES : NO] forKey:@"save_password"];
@@ -6062,6 +6074,10 @@ static int64_t SPDatabaseDocumentInstanceCounter = 0;
#ifndef SP_CODA /* whole database operations */
+/**
+ *
+ * This method *MUST* be called from the UI thread!
+ */
- (void)_copyDatabase
{
if ([[databaseCopyNameField stringValue] isEqualToString:@""]) {
@@ -6094,6 +6110,10 @@ static int64_t SPDatabaseDocumentInstanceCounter = 0;
}
#endif
+/**
+ *
+ * This method *MUST* be called from the UI thread!
+ */
- (void)_renameDatabase
{
NSString *newDatabaseName = [databaseRenameNameField stringValue];
@@ -6138,6 +6158,8 @@ static int64_t SPDatabaseDocumentInstanceCounter = 0;
/**
* Adds a new database.
+ *
+ * This method *MUST* be called from the UI thread!
*/
- (void)_addDatabase
{
@@ -6208,6 +6230,8 @@ static int64_t SPDatabaseDocumentInstanceCounter = 0;
/**
* Removes the current database.
+ *
+ * This method *MUST* be called from the UI thread!
*/
- (void)_removeDatabase
{
@@ -6479,6 +6503,9 @@ static int64_t SPDatabaseDocumentInstanceCounter = 0;
- (void)dealloc
{
NSAssert([NSThread isMainThread], @"Calling %s from a background thread is not supported!", __func__);
+
+ // Tell listeners that this database document is being closed - fixes retain cycles and allows cleanup
+ [[NSNotificationCenter defaultCenter] postNotificationName:SPDocumentWillCloseNotification object:self];
// Unregister observers
[self _removePreferenceObservers];
@@ -6493,9 +6520,6 @@ static int64_t SPDatabaseDocumentInstanceCounter = 0;
for (id retainedObject in nibObjectsToRelease) [retainedObject release];
SPClear(nibObjectsToRelease);
-
- // Tell listeners that this database document is being closed - fixes retain cycles and allows cleanup
- [[NSNotificationCenter defaultCenter] postNotificationName:SPDocumentWillCloseNotification object:self];
SPClear(databaseStructureRetrieval);
diff --git a/Source/SPEditorPreferencePane.m b/Source/SPEditorPreferencePane.m
index 0428984a..7fc810ea 100644
--- a/Source/SPEditorPreferencePane.m
+++ b/Source/SPEditorPreferencePane.m
@@ -351,7 +351,7 @@ static NSString *SPCustomColorSchemeNameLC = @"user-defined";
[defaultItem setTarget:self];
// Build theme selection submenu
- [themeSelectionMenu compatibleRemoveAllItems];
+ [themeSelectionMenu removeAllItems];
[themeSelectionMenu addItem:defaultItem];
[themeSelectionMenu addItem:[NSMenuItem separatorItem]];
diff --git a/Source/SPExportInitializer.m b/Source/SPExportInitializer.m
index 3238ea5c..9269cac8 100644
--- a/Source/SPExportInitializer.m
+++ b/Source/SPExportInitializer.m
@@ -520,7 +520,7 @@
BOOL tableNameInTokens = NO;
NSArray *representedObjects = [exportCustomFilenameTokenField objectValue];
for (id representedObject in representedObjects) {
- if ([representedObject isKindOfClass:[SPExportFileNameTokenObject class]] && [[representedObject tokenId] isEqualToString:NSLocalizedString(@"table", @"table")]) tableNameInTokens = YES;
+ if ([representedObject isKindOfClass:[SPExportFileNameTokenObject class]] && [[representedObject tokenId] isEqualToString:SPFileNameTableTokenName]) tableNameInTokens = YES;
}
[exportFilename setString:(tableNameInTokens ? exportFilename : [exportFilename stringByAppendingFormat:@"_%@", table])];
}
@@ -582,7 +582,7 @@
BOOL tableNameInTokens = NO;
NSArray *representedObjects = [exportCustomFilenameTokenField objectValue];
for (id representedObject in representedObjects) {
- if ([representedObject isKindOfClass:[SPExportFileNameTokenObject class]] && [[representedObject tokenId] isEqualToString:NSLocalizedString(@"table", @"table")]) tableNameInTokens = YES;
+ if ([representedObject isKindOfClass:[SPExportFileNameTokenObject class]] && [[representedObject tokenId] isEqualToString:SPFileNameTableTokenName]) tableNameInTokens = YES;
}
[exportFilename setString:(tableNameInTokens ? exportFilename : [exportFilename stringByAppendingFormat:@"_%@", table])];
}
diff --git a/Source/SPExtendedTableInfo.m b/Source/SPExtendedTableInfo.m
index df084bf3..dc311bdf 100644
--- a/Source/SPExtendedTableInfo.m
+++ b/Source/SPExtendedTableInfo.m
@@ -404,7 +404,8 @@ static NSString *SPMySQLCommentField = @"Comment";
[tableSizeFree setStringValue:[self _formatValueWithKey:SPMySQLDataFreeField inDictionary:statusFields]];
// Set comments
- NSString *commentText = [statusFields objectForKey:SPMySQLCommentField];
+ // Note: On MySQL the comment column is marked as NOT NULL, but we still received crash reports because it was NULL!? (#2791)
+ NSString *commentText = [[statusFields objectForKey:SPMySQLCommentField] unboxNull];
if (!commentText) commentText = @"";
@@ -541,7 +542,7 @@ static NSString *SPMySQLCommentField = @"Comment";
if ((object == tableCommentsTextView) && ([object isEditable]) && ([selectedTable length] > 0)) {
- NSString *currentComment = [[tableDataInstance statusValueForKey:@"Comment"] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
+ NSString *currentComment = [[[tableDataInstance statusValueForKey:SPMySQLCommentField] unboxNull] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
NSString *newComment = [[tableCommentsTextView string] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
// Check that the user actually changed the tables comment
diff --git a/Source/SPFieldEditorController.m b/Source/SPFieldEditorController.m
index 0298973d..b79355b5 100644
--- a/Source/SPFieldEditorController.m
+++ b/Source/SPFieldEditorController.m
@@ -41,13 +41,6 @@
#import <SPMySQL/SPMySQL.h>
-#if __MAC_OS_X_VERSION_MAX_ALLOWED < __MAC_10_7
-@interface NSTextView (LionPlus)
-- (void)setUsesFindBar:(BOOL)value;
-- (BOOL)usesFindBar;
-@end
-#endif
-
typedef enum {
TextSegment = 0,
ImageSegment,
diff --git a/Source/SPFieldMapperController.m b/Source/SPFieldMapperController.m
index ca40d71a..ee80174a 100644
--- a/Source/SPFieldMapperController.m
+++ b/Source/SPFieldMapperController.m
@@ -2040,7 +2040,7 @@ static NSUInteger SPSourceColumnTypeInteger = 1;
#endif
// Re-init recent menu
- [recentGlobalValueMenu compatibleRemoveAllItems];
+ [recentGlobalValueMenu removeAllItems];
for(id item in recents)
[recentGlobalValueMenu addItemWithTitle:item action:@selector(insertRecentGlobalValue:) keyEquivalent:@""];
diff --git a/Source/SPFileHandle.h b/Source/SPFileHandle.h
index 5622b1b6..2b605f59 100644
--- a/Source/SPFileHandle.h
+++ b/Source/SPFileHandle.h
@@ -28,7 +28,7 @@
//
// More info at <https://github.com/sequelpro/sequelpro>
-union SPSomeFileHandle;
+struct SPRawFileHandles;
/**
* @class SPFileHandle SPFileHandle.h
*
@@ -40,7 +40,7 @@ union SPSomeFileHandle;
*/
@interface SPFileHandle : NSObject
{
- union SPSomeFileHandle *wrappedFile;
+ struct SPRawFileHandles *wrappedFile;
char *wrappedFilePath;
NSMutableData *buffer;
diff --git a/Source/SPFileHandle.m b/Source/SPFileHandle.m
index 7da3d100..af6f413f 100644
--- a/Source/SPFileHandle.m
+++ b/Source/SPFileHandle.m
@@ -37,7 +37,7 @@
// waits until some has been written out. This can affect speed and memory usage.
#define SPFH_MAX_WRITE_BUFFER_SIZE 1048576
-union SPSomeFileHandle {
+struct SPRawFileHandles {
FILE *file;
BZFILE *bzfile;
gzFile *gzfile;
@@ -46,6 +46,7 @@ union SPSomeFileHandle {
@interface SPFileHandle ()
- (void)_writeBufferToData;
+- (void)_closeFileHandles;
@end
@@ -132,11 +133,11 @@ union SPSomeFileHandle {
if (isBzip2) {
compressionFormat = SPBzip2Compression;
- wrappedFile->bzfile = BZ2_bzopen(path, "rb");
+ wrappedFile->bzfile = BZ2_bzReadOpen(NULL, theFile, 0, 0, NULL, 0);
}
}
- // Default to plain
- if(compressionFormat == SPNoCompression) {
+ // We need to save the file handle both in plain and BZ2 format
+ if(compressionFormat == SPNoCompression || compressionFormat == SPBzip2Compression) {
wrappedFile->file = theFile;
}
else {
@@ -231,7 +232,7 @@ union SPSomeFileHandle {
}
/**
- * Returns the on-disk (raw/uncompressed) length of data read so far.
+ * Returns the on-disk (raw/compressed) length of data read so far.
* This includes any compression headers within the data, and can be used
* for progress bars when processing files.
*/
@@ -243,7 +244,7 @@ union SPSomeFileHandle {
return gzoffset(wrappedFile->gzfile);
}
else if(compressionFormat == SPBzip2Compression) {
- return 0;
+ return ftell(wrappedFile->file);
}
else {
return ftell(wrappedFile->file);
@@ -263,15 +264,7 @@ union SPSomeFileHandle {
if (compressionFormat == useCompressionFormat) return;
// Regardless of the supplied argument, close the current file according to how it was previously opened
- if (compressionFormat == SPGzipCompression) {
- gzclose(wrappedFile->gzfile);
- }
- else if (compressionFormat == SPBzip2Compression) {
- BZ2_bzclose(wrappedFile->bzfile);
- }
- else {
- fclose(wrappedFile->file);
- }
+ [self _closeFileHandles];
if (dataWritten) [NSException raise:NSInternalInconsistencyException format:@"Cannot change compression settings when data has already been written."];
@@ -282,7 +275,8 @@ union SPSomeFileHandle {
gzbuffer(wrappedFile->gzfile, 131072);
}
else if (compressionFormat == SPBzip2Compression) {
- wrappedFile->bzfile = BZ2_bzopen(wrappedFilePath, "wb");
+ wrappedFile->file = fopen(wrappedFilePath, "wb");
+ wrappedFile->bzfile = BZ2_bzWriteOpen(NULL, wrappedFile->file, 9, 0, 0);
}
else {
wrappedFile->file = fopen(wrappedFilePath, "wb");
@@ -343,16 +337,7 @@ union SPSomeFileHandle {
{
if (!fileIsClosed) {
[self synchronizeFile];
-
- if (compressionFormat == SPGzipCompression) {
- gzclose(wrappedFile->gzfile);
- }
- else if (compressionFormat == SPBzip2Compression) {
- BZ2_bzclose(wrappedFile->bzfile);
- }
- else {
- fclose(wrappedFile->file);
- }
+ [self _closeFileHandles];
if (processingThread) {
if ([processingThread isExecuting]) {
@@ -442,6 +427,35 @@ union SPSomeFileHandle {
[writePool drain];
}
+/**
+ * Close any open file handles
+ */
+- (void)_closeFileHandles
+{
+ if (compressionFormat == SPGzipCompression) {
+ gzclose(wrappedFile->gzfile);
+ wrappedFile->gzfile = NULL;
+ }
+ else if (compressionFormat == SPBzip2Compression) {
+ if (fileMode == O_RDONLY) {
+ BZ2_bzReadClose(NULL, wrappedFile->bzfile);
+ }
+ else if (fileMode == O_WRONLY) {
+ BZ2_bzWriteClose(NULL, wrappedFile->bzfile, 0, NULL, NULL);
+ }
+ else {
+ [NSException raise:NSInvalidArgumentException format:@"SPFileHandle only supports read-only and write-only file modes"];
+ }
+ fclose(wrappedFile->file);
+ wrappedFile->bzfile = NULL;
+ wrappedFile->file = NULL;
+ }
+ else {
+ fclose(wrappedFile->file);
+ wrappedFile->file = NULL;
+ }
+}
+
#pragma mark -
/**
diff --git a/Source/SPFunctions.m b/Source/SPFunctions.m
index 93008059..461304e0 100644
--- a/Source/SPFunctions.m
+++ b/Source/SPFunctions.m
@@ -44,11 +44,10 @@ void SPMainQSync(void (^block)(void))
int SPBetterRandomBytes(uint8_t *buf, size_t count)
{
-#if MAC_OS_X_VERSION_MAX_ALLOWED >= __MAC_10_7
if([SPOSInfo isOSVersionAtLeastMajor:10 minor:7 patch:0]) {
return SecRandomCopyBytes(kSecRandomDefault, count, buf);
}
-#endif
+
// Version for 10.6
// https://developer.apple.com/library/prerelease/mac/documentation/Security/Conceptual/cryptoservices/RandomNumberGenerationAPIs/RandomNumberGenerationAPIs.html#//apple_ref/doc/uid/TP40011172-CH12-SW1
FILE *fp = fopen("/dev/random", "r");
diff --git a/Source/SPIndexesController.m b/Source/SPIndexesController.m
index debeaf30..dcf01ccb 100644
--- a/Source/SPIndexesController.m
+++ b/Source/SPIndexesController.m
@@ -40,6 +40,7 @@
#import "SPTableStructure.h"
#import "SPTableStructureLoading.h"
#import "SPThreadAdditions.h"
+#import "SPFunctions.h"
#import <SPMySQL/SPMySQL.h>
@@ -245,31 +246,15 @@ static const NSString *SPNewIndexKeyBlockSize = @"IndexKeyBlockSize";
if ((index == -1) || (index > ((NSInteger)[indexes count] - 1))) return;
- NSString *keyName = [[indexes objectAtIndex:index] objectForKey:@"Key_name"];
- NSString *columnName = [[indexes objectAtIndex:index] objectForKey:@"Column_name"];
-
- BOOL hasForeignKey = NO;
- NSString *constraintName = @"";
-
- // Check to see whether the user is attempting to remove an index that a foreign key constraint depends on
- // thus would result in an error if not dropped before removing the index.
- for (NSDictionary *constraint in [tableData getConstraints])
- {
- for (NSString *column in [constraint objectForKey:@"columns"])
- {
- if ([column isEqualToString:columnName]) {
- hasForeignKey = YES;
- constraintName = [constraint objectForKey:@"name"];
- break;
- }
- }
- }
+ NSString *keyName = [[indexes objectAtIndex:index] objectForKey:@"Key_name"];
+
+ if(![keyName length]) return; //safeguard for the contextInfo array creation below
NSAlert *alert = [NSAlert alertWithMessageText:[NSString stringWithFormat:NSLocalizedString(@"Delete index '%@'?", @"delete index message"), keyName]
defaultButton:NSLocalizedString(@"Delete", @"delete button")
alternateButton:NSLocalizedString(@"Cancel", @"cancel button")
otherButton:nil
- informativeTextWithFormat:hasForeignKey ? NSLocalizedString(@"The foreign key relationship '%@' has a dependency on this index. This relationship must be removed before the index can be deleted.\n\nAre you sure you want to continue to delete the relationship and the index? This action cannot be undone.", @"delete index and foreign key informative message"), constraintName : NSLocalizedString(@"Are you sure you want to delete the index '%@'? This action cannot be undone.", @"delete index informative message"), keyName];
+ informativeTextWithFormat:NSLocalizedString(@"Are you sure you want to delete the index '%@'? This action cannot be undone.", @"delete index informative message"), keyName];
[alert setAlertStyle:NSCriticalAlertStyle];
@@ -283,7 +268,7 @@ static const NSString *SPNewIndexKeyBlockSize = @"IndexKeyBlockSize";
[alert beginSheetModalForWindow:[dbDocument parentWindow]
modalDelegate:self
didEndSelector:@selector(removeIndexSheetDidEnd:returnCode:contextInfo:)
- contextInfo:(hasForeignKey) ? @"removeIndexAndForeignKey" : @"removeIndex"];
+ contextInfo:[@{@"Key_name" : keyName} retain]]; // contextInfo is NOT retained by Cocoa!
}
/**
@@ -636,22 +621,19 @@ static const NSString *SPNewIndexKeyBlockSize = @"IndexKeyBlockSize";
{
// Order out current sheet to suppress overlapping of sheets
[[alert window] orderOut:nil];
+
+ NSDictionary *info = [(id)contextInfo autorelease]; //we explicitly retained it beforehand, because Cocoa does NOT!
if (returnCode == NSAlertDefaultReturn) {
[dbDocument startTaskWithDescription:NSLocalizedString(@"Removing index...", @"removing index task status message")];
- NSMutableDictionary *indexDetails = [NSMutableDictionary dictionary];
-
- [indexDetails setObject:[indexes objectAtIndex:[indexesTableView selectedRow]] forKey:@"Index"];
- [indexDetails setObject:[NSNumber numberWithBool:[(NSString *)contextInfo hasSuffix:@"AndForeignKey"]] forKey:@"RemoveForeignKey"];
-
if ([NSThread isMainThread]) {
- [NSThread detachNewThreadWithName:SPCtxt(@"SPIndexesController index removal thread", dbDocument) target:self selector:@selector(_removeIndexUsingDetails:) object:indexDetails];
+ [NSThread detachNewThreadWithName:SPCtxt(@"SPIndexesController index removal thread", dbDocument) target:self selector:@selector(_removeIndexUsingDetails:) object:info];
[dbDocument enableTaskCancellationWithTitle:NSLocalizedString(@"Cancel", @"cancel button") callbackObject:self callbackFunction:NULL];
}
else {
- [self _removeIndexUsingDetails:indexDetails];
+ [self _removeIndexUsingDetails:info];
}
}
}
@@ -911,58 +893,48 @@ static const NSString *SPNewIndexKeyBlockSize = @"IndexKeyBlockSize";
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
- NSDictionary *index = [indexDetails objectForKey:@"Index"];
- BOOL removeForeignKey = [[indexDetails objectForKey:@"RemoveForeignKey"] boolValue];
+ NSString *index = [indexDetails objectForKey:@"Key_name"];
+ NSString *fkName = [indexDetails objectForKey:@"ForeignKey"];
// Remove the foreign key dependency before the index if required
- if (removeForeignKey) {
-
- NSString *columnName = [index objectForKey:@"Column_name"];
-
- NSString *constraintName = @"";
-
- // Check to see whether the user is attempting to remove an index that a foreign key constraint depends on
- // thus would result in an error if not dropped before removing the index.
- for (NSDictionary *constraint in [tableData getConstraints])
- {
- for (NSString *column in [constraint objectForKey:@"columns"])
- {
- if ([column isEqualToString:columnName]) {
- constraintName = [constraint objectForKey:@"name"];
- break;
- }
- }
- }
+ if ([fkName length]) {
- [connection queryString:[NSString stringWithFormat:@"ALTER TABLE %@ DROP FOREIGN KEY %@", [table backtickQuotedString], [constraintName backtickQuotedString]]];
+ [connection queryString:[NSString stringWithFormat:@"ALTER TABLE %@ DROP FOREIGN KEY %@", [table backtickQuotedString], [fkName backtickQuotedString]]];
// Check for errors, but only if the query wasn't cancelled
if ([connection queryErrored] && ![connection lastQueryWasCancelled]) {
NSMutableDictionary *errorDictionary = [NSMutableDictionary dictionary];
[errorDictionary setObject:NSLocalizedString(@"Unable to delete relation", @"error deleting relation message") forKey:@"title"];
- [errorDictionary setObject:[NSString stringWithFormat:NSLocalizedString(@"An error occurred while trying to delete the relation '%@'.\n\nMySQL said: %@", @"error deleting relation informative message"), constraintName, [connection lastErrorMessage]] forKey:@"message"];
+ [errorDictionary setObject:[NSString stringWithFormat:NSLocalizedString(@"An error occurred while trying to delete the relation '%@'.\n\nMySQL said: %@", @"error deleting relation informative message"), fkName, [connection lastErrorMessage]] forKey:@"message"];
[(SPTableStructure*)[tableStructure onMainThread] showErrorSheetWith:errorDictionary];
}
}
- if ([[index objectForKey:@"Key_name"] isEqualToString:@"PRIMARY"]) {
+ if ([index isEqualToString:@"PRIMARY"]) {
[connection queryString:[NSString stringWithFormat:@"ALTER TABLE %@ DROP PRIMARY KEY", [table backtickQuotedString]]];
}
else {
[connection queryString:[NSString stringWithFormat:@"ALTER TABLE %@ DROP INDEX %@",
- [table backtickQuotedString], [[index objectForKey:@"Key_name"] backtickQuotedString]]];
+ [table backtickQuotedString], [index backtickQuotedString]]];
}
// Check for errors, but only if the query wasn't cancelled
if ([connection queryErrored] && ![connection lastQueryWasCancelled]) {
- NSMutableDictionary *errorDictionary = [NSMutableDictionary dictionary];
-
- [errorDictionary setObject:NSLocalizedString(@"Unable to delete index", @"error deleting index message") forKey:@"title"];
- [errorDictionary setObject:[NSString stringWithFormat:NSLocalizedString(@"An error occured while trying to delete the index.\n\nMySQL said: %@", @"error deleting index informative message"), [connection lastErrorMessage]] forKey:@"message"];
-
- [(SPTableStructure*)[tableStructure onMainThread] showErrorSheetWith:errorDictionary];
+ //if the last error was 1553 and we did not already try to remove a FK beforehand, we have to request to remove the foreign key before we can remove the index
+ if([connection lastErrorID] == 1553 /* ER_DROP_INDEX_FK */ && ![fkName length]) {
+ NSDictionary *details = @{@"Key_name": index, @"error": SPBoxNil([connection lastErrorMessage])};
+ [self performSelectorOnMainThread:@selector(_removingIndexFailedWithForeignKeyError:) withObject:details waitUntilDone:NO];
+ }
+ else {
+ NSMutableDictionary *errorDictionary = [NSMutableDictionary dictionary];
+
+ [errorDictionary setObject:NSLocalizedString(@"Unable to delete index", @"error deleting index message") forKey:@"title"];
+ [errorDictionary setObject:[NSString stringWithFormat:NSLocalizedString(@"An error occured while trying to delete the index.\n\nMySQL said: %@", @"error deleting index informative message"), [connection lastErrorMessage]] forKey:@"message"];
+
+ [(SPTableStructure*)[tableStructure onMainThread] showErrorSheetWith:errorDictionary];
+ }
}
else {
[tableData resetAllData];
@@ -977,6 +949,81 @@ static const NSString *SPNewIndexKeyBlockSize = @"IndexKeyBlockSize";
}
/**
+ * If removing an index failed, because an FK depends on it (mysql error 1553) this
+ * will ask the user to confirm deleting the FK, too (if it is found).
+ *
+ * MUST be called on the UI thread!
+ */
+- (void)_removingIndexFailedWithForeignKeyError:(NSDictionary *)info
+{
+ NSString *keyName = [info objectForKey:@"Key_name"];
+
+ //we have to find out which fk uses this index (and need to watch out for compound indexes)
+ NSString *constraintName = nil;
+
+ NSMutableArray *myColumns = [NSMutableArray array];
+
+ for (NSDictionary *indexPart in indexes) {
+ if ([[indexPart objectForKey:@"Key_name"] isEqualToString:keyName]) {
+ [myColumns addObject:[indexPart objectForKey:@"Column_name"]];
+ }
+ }
+
+ //if the index has no columns, something's fucky
+ if(![myColumns count]) {
+ SPOnewayAlertSheet(
+ [NSString stringWithFormat:NSLocalizedString(@"Failed to remove index '%@'", @"table structure : indexes : delete index : no columns error : title"),keyName],
+ [dbDocument parentWindow],
+ NSLocalizedString(@"Sequel Pro could not find any columns belonging to this index. Maybe it has been removed already?", @"table structure : indexes : delete index : no columns error : description")
+ );
+ return;
+ }
+
+ [myColumns sortUsingSelector:@selector(compare:)];
+
+ //now let's find a matching fk (ie. one that has the same columns as the index)
+ for (NSDictionary *fkInfo in [tableData getConstraints]) {
+ NSArray *fkColumns = [[fkInfo objectForKey:@"columns"] sortedArrayUsingSelector:@selector(compare:)];
+ if(![myColumns isEqualToArray:fkColumns]) continue;
+ if(constraintName != nil) {
+ goto no_or_multiple_matches; //we already found a matching FK, but there is another one!? -> abort
+ }
+ constraintName = [fkInfo objectForKey:@"name"];
+ }
+
+ if(!constraintName) goto no_or_multiple_matches; //we found no matching FK
+
+ NSAlert *alert = [NSAlert alertWithMessageText:NSLocalizedString(@"A foreign key needs this index", @"table structure : indexes : delete index : error 1553 : title")
+ defaultButton:NSLocalizedString(@"Delete Both", @"table structure : indexes : delete index : error 1553 : delete index and FK button")
+ alternateButton:NSLocalizedString(@"Cancel", @"cancel button")
+ otherButton:nil
+ informativeTextWithFormat:NSLocalizedString(@"The foreign key relationship '%@' has a dependency on index '%@'. This relationship must be removed before the index can be deleted.\n\nAre you sure you want to continue to delete the relationship and the index? This action cannot be undone.", @"table structure : indexes : delete index : error 1553 : description"), constraintName, keyName];
+
+ [alert setAlertStyle:NSCriticalAlertStyle];
+
+ NSArray *buttons = [alert buttons];
+
+ // Change the alert's cancel button to have the key equivalent of return
+ [[buttons objectAtIndex:0] setKeyEquivalent:@"d"];
+ [[buttons objectAtIndex:0] setKeyEquivalentModifierMask:NSCommandKeyMask];
+ [[buttons objectAtIndex:1] setKeyEquivalent:@"\r"];
+
+ [alert beginSheetModalForWindow:[dbDocument parentWindow]
+ modalDelegate:self
+ didEndSelector:@selector(removeIndexSheetDidEnd:returnCode:contextInfo:)
+ contextInfo:[@{@"Key_name" : keyName, @"ForeignKey": constraintName} retain]]; // contextInfo is NOT retained by Cocoa!
+
+ return;
+
+no_or_multiple_matches:
+ SPOnewayAlertSheet(
+ NSLocalizedString(@"A foreign key needs this index", @"table structure : indexes : delete index : error 1553, no FK found : title"),
+ [dbDocument parentWindow],
+ [NSString stringWithFormat:NSLocalizedString(@"This index cannot be deleted, because it is used by an existing foreign key relationship.\n\nPlease remove the relationship, before trying to remove this index.\n\nMySQL said: %@", @"table structure : indexes : delete index : error 1553, no FK found : description"), [info objectForKey:@"error"]]
+ );
+}
+
+/**
* Resizes the new index sheet's height by the supplied delta, while retaining the position of
* all interface controls to accommodate the advanced options view.
*
diff --git a/Source/SPKeychain.m b/Source/SPKeychain.m
index 49e2de69..94b561c5 100644
--- a/Source/SPKeychain.m
+++ b/Source/SPKeychain.m
@@ -230,10 +230,8 @@
*/
- (BOOL)passwordExistsForName:(NSString *)name account:(NSString *)account
{
-#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 1070
// "kSecClassGenericPassword" was introduced with the 10.7 SDK.
// It won't work on 10.6 either (meaning this code never matches properly there).
- // (That's why there are compile time and runtime checks here)
if([SPOSInfo isOSVersionAtLeastMajor:10 minor:7 patch:0]) {
NSMutableDictionary *query = [NSMutableDictionary dictionary];
@@ -248,7 +246,9 @@
return SecItemCopyMatching((CFDictionaryRef)query, (CFTypeRef *)&result) == errSecSuccess;
}
-#endif
+
+ //Version for 10.6
+
SecKeychainItemRef item;
SecKeychainSearchRef search = NULL;
NSInteger numberOfItemsFound = 0;
diff --git a/Source/SPMenuAdditions.m b/Source/SPMenuAdditions.m
deleted file mode 100644
index f4544808..00000000
--- a/Source/SPMenuAdditions.m
+++ /dev/null
@@ -1,46 +0,0 @@
-//
-// SPMenuAdditions.m
-// sequel-pro
-//
-// Created by Rowan Beentje on November 27, 2010.
-// Copyright (c) 2010 Rowan Beentje. All rights reserved.
-//
-// Permission is hereby granted, free of charge, to any person
-// obtaining a copy of this software and associated documentation
-// files (the "Software"), to deal in the Software without
-// restriction, including without limitation the rights to use,
-// copy, modify, merge, publish, distribute, sublicense, and/or sell
-// copies of the Software, and to permit persons to whom the
-// Software is furnished to do so, subject to the following
-// conditions:
-//
-// The above copyright notice and this permission notice shall be
-// included in all copies or substantial portions of the Software.
-//
-// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
-// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
-// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
-// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
-// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
-// OTHER DEALINGS IN THE SOFTWARE.
-//
-// More info at <https://github.com/sequelpro/sequelpro>
-
-#import "SPMenuAdditions.h"
-
-@implementation NSMenu (SPMenuAdditions)
-
-// Add a 10.5-compatible removeAllItems
-- (void)compatibleRemoveAllItems
-{
- if ([self respondsToSelector:@selector(removeAllItems)]) {
- [(id)self removeAllItems];
- }
- else {
- while ([self numberOfItems]) [self removeItemAtIndex:0];
- }
-}
-
-@end
diff --git a/Source/SPOSInfo.m b/Source/SPOSInfo.m
index b862e898..412b634a 100644
--- a/Source/SPOSInfo.m
+++ b/Source/SPOSInfo.m
@@ -29,26 +29,7 @@
// More info at <https://github.com/sequelpro/sequelpro>
#import "SPOSInfo.h"
-
-// Needed because this class is also compiled with SequelProTunnelAssistant which can't access SPConstants.h
-#ifndef __MAC_10_10
-#define __MAC_10_10 101000
-#endif
-
-#if __MAC_OS_X_VERSION_MAX_ALLOWED < __MAC_10_10
-// This code is available since 10.8 but public only since 10.10
-typedef struct {
- NSInteger majorVersion;
- NSInteger minorVersion;
- NSInteger patchVersion;
-} NSOperatingSystemVersion;
-
-@interface NSProcessInfo ()
-- (NSOperatingSystemVersion)operatingSystemVersion;
-- (BOOL)isOperatingSystemAtLeastVersion:(NSOperatingSystemVersion)version;
-@end
-
-#endif
+#import "SPCompatibility.h" // Needed because this class is also compiled with SequelProTunnelAssistant
int SPOSVersionCompare(SPOSVersion left, SPOSVersion right)
{
diff --git a/Source/SPProcessListController.m b/Source/SPProcessListController.m
index 8290b5d9..eb22d484 100644
--- a/Source/SPProcessListController.m
+++ b/Source/SPProcessListController.m
@@ -42,6 +42,9 @@ static NSString *SPKillProcessQueryMode = @"SPKillProcessQueryMode";
static NSString *SPKillProcessConnectionMode = @"SPKillProcessConnectionMode";
static NSString *SPTableViewIDColumnIdentifier = @"Id";
+static NSString * const SPKillModeKey = @"SPKillMode";
+static NSString * const SPKillIdKey = @"SPKillId";
+
@interface SPProcessListController (PrivateAPI)
- (void)_processListRefreshed;
@@ -283,7 +286,14 @@ static NSString *SPTableViewIDColumnIdentifier = @"Id";
[alert setAlertStyle:NSCriticalAlertStyle];
- [alert beginSheetModalForWindow:[self window] modalDelegate:self didEndSelector:@selector(sheetDidEnd:returnCode:contextInfo:) contextInfo:SPKillProcessQueryMode];
+ // while the alert is displayed, the results may be updated and the selectedRow may point to a different
+ // row or has disappeared (= -1) by the time the didEndSelector is invoked,
+ // so we must remember the ACTUAL processId we prompt the user to kill.
+ NSDictionary *userInfo = @{SPKillModeKey: SPKillProcessQueryMode, SPKillIdKey: @(processId)};
+ [alert beginSheetModalForWindow:[self window]
+ modalDelegate:self
+ didEndSelector:@selector(sheetDidEnd:returnCode:contextInfo:)
+ contextInfo:[userInfo retain]]; //keep in mind contextInfo is a void * and not an id => no memory management here
}
/**
@@ -311,7 +321,14 @@ static NSString *SPTableViewIDColumnIdentifier = @"Id";
[alert setAlertStyle:NSCriticalAlertStyle];
- [alert beginSheetModalForWindow:[self window] modalDelegate:self didEndSelector:@selector(sheetDidEnd:returnCode:contextInfo:) contextInfo:SPKillProcessConnectionMode];
+ // while the alert is displayed, the results may be updated and the selectedRow may point to a different
+ // row or has disappeared (= -1) by the time the didEndSelector is invoked,
+ // so we must remember the ACTUAL processId we prompt the user to kill.
+ NSDictionary *userInfo = @{SPKillModeKey: SPKillProcessConnectionMode, SPKillIdKey: @(processId)};
+ [alert beginSheetModalForWindow:[self window]
+ modalDelegate:self
+ didEndSelector:@selector(sheetDidEnd:returnCode:contextInfo:)
+ contextInfo:[userInfo retain]]; //keep in mind contextInfo is a void * and not an id => no memory management here
}
/**
@@ -364,7 +381,7 @@ static NSString *SPTableViewIDColumnIdentifier = @"Id";
modalForWindow:[self window]
modalDelegate:self
didEndSelector:@selector(sheetDidEnd:returnCode:contextInfo:)
- contextInfo:nil];
+ contextInfo:NULL];
}
#pragma mark -
@@ -386,7 +403,7 @@ static NSString *SPTableViewIDColumnIdentifier = @"Id";
/**
* Invoked when the kill alerts are dismissed. Decide what to do based on the user's decision.
*/
-- (void)sheetDidEnd:(id)sheet returnCode:(NSInteger)returnCode contextInfo:(NSString *)contextInfo
+- (void)sheetDidEnd:(id)sheet returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo
{
// Order out current sheet to suppress overlapping of sheets
if ([sheet respondsToSelector:@selector(orderOut:)]) {
@@ -396,20 +413,24 @@ static NSString *SPTableViewIDColumnIdentifier = @"Id";
[[sheet window] orderOut:nil];
}
- if (returnCode == NSAlertDefaultReturn) {
-
- if (sheet == customIntervalWindow) {
- [self _startAutoRefreshTimerWithInterval:[customIntervalTextField integerValue]];
- }
- else {
- long long processId = [[[processesFiltered objectAtIndex:[processListTableView selectedRow]] valueForKey:@"Id"] longLongValue];
+ if (sheet == customIntervalWindow) {
+ if (returnCode == NSAlertDefaultReturn) [self _startAutoRefreshTimerWithInterval:[customIntervalTextField integerValue]];
+ }
+ else {
+ NSDictionary *userInfo = [(NSDictionary *)contextInfo autorelease]; //we retained it during the beginSheet… call because Cocoa does not do memory management on void *.
+ if (returnCode == NSAlertDefaultReturn) {
+ long long processId = [[userInfo objectForKey:SPKillIdKey] longLongValue];
- if ([contextInfo isEqualToString:SPKillProcessQueryMode]) {
+ NSString *mode = [userInfo objectForKey:SPKillModeKey];
+ if ([mode isEqualToString:SPKillProcessQueryMode]) {
[self _killProcessQueryWithId:processId];
}
- else if ([contextInfo isEqualToString:SPKillProcessConnectionMode]) {
+ else if ([mode isEqualToString:SPKillProcessConnectionMode]) {
[self _killProcessConnectionWithId:processId];
}
+ else {
+ [NSException raise:NSInternalInconsistencyException format:@"%s: Unhandled branch for mode=%@", __PRETTY_FUNCTION__, mode];
+ }
}
}
}
diff --git a/Source/SPQueryConsoleDataSource.m b/Source/SPQueryConsoleDataSource.m
index eaac2151..eb669051 100644
--- a/Source/SPQueryConsoleDataSource.m
+++ b/Source/SPQueryConsoleDataSource.m
@@ -97,4 +97,15 @@ static NSUInteger SPMessageTruncateCharacterLength = 256;
#endif
}
+- (BOOL)tableView:(NSTableView *)aTableView writeRowsWithIndexes:(NSIndexSet *)rowIndexes toPasteboard:(NSPasteboard *)pboard
+{
+ NSString *string = [self sqlStringForRowIndexes:rowIndexes];
+ if([string length]) {
+ [pboard declareTypes:@[NSStringPboardType] owner:self];
+ return [pboard setString:string forType:NSStringPboardType];
+ }
+
+ return NO;
+}
+
@end
diff --git a/Source/SPQueryController.h b/Source/SPQueryController.h
index 8d128fe3..12ff6d7b 100644
--- a/Source/SPQueryController.h
+++ b/Source/SPQueryController.h
@@ -84,6 +84,10 @@ extern NSString *SPTableViewDatabaseColumnID;
+ (SPQueryController *)sharedQueryController;
+/**
+ * Calls -sqlStringForForRowIndexes: with the current selection and
+ * puts the output into the general Pasteboard (only if non-empty)
+ */
- (IBAction)copy:(id)sender;
- (IBAction)clearConsole:(id)sender;
- (IBAction)saveConsoleAs:(id)sender;
@@ -103,4 +107,17 @@ extern NSString *SPTableViewDatabaseColumnID;
- (NSUInteger)consoleMessageCount;
+/**
+ * Returns the console messages specified by indexes as a string, each message separated by "\n".
+ * @param indexes The indexes of rows to be returned.
+ * Invalid indexes will be skipped silently.
+ * nil is treated as an empty set.
+ *
+ * If no (valid) indexes are given, @"" will be returned.
+ * The output may include other info like timestamp, host, etc. if shown in the table view, as part of a comment.
+ *
+ * THIS METHOD IS NOT THREAD-SAFE!
+ */
+- (NSString *)sqlStringForRowIndexes:(NSIndexSet *)indexes;
+
@end
diff --git a/Source/SPQueryController.m b/Source/SPQueryController.m
index dbdc3d44..41d71154 100644
--- a/Source/SPQueryController.m
+++ b/Source/SPQueryController.m
@@ -150,49 +150,13 @@ static SPQueryController *sharedQueryController = nil;
*/
- (void)copy:(id)sender
{
-#ifndef SP_CODA
NSResponder *firstResponder = [[self window] firstResponder];
if ((firstResponder == consoleTableView) && ([consoleTableView numberOfSelectedRows] > 0)) {
-
- NSMutableString *string = [NSMutableString string];
+
NSIndexSet *rows = [consoleTableView selectedRowIndexes];
- BOOL includeTimestamps = ![[consoleTableView tableColumnWithIdentifier:SPTableViewDateColumnID] isHidden];
- BOOL includeConnections = ![[consoleTableView tableColumnWithIdentifier:SPTableViewConnectionColumnID] isHidden];
- BOOL includeDatabases = ![[consoleTableView tableColumnWithIdentifier:SPTableViewDatabaseColumnID] isHidden];
-
- [string setString:@""];
-
- [rows enumerateIndexesUsingBlock:^(NSUInteger i, BOOL * _Nonnull stop) {
- if (i < [messagesVisibleSet count]) {
- SPConsoleMessage *message = NSArrayObjectAtIndex(messagesVisibleSet, i);
-
- if (includeTimestamps || includeConnections || includeDatabases) [string appendString:@"/* "];
-
- NSDate *date = [message messageDate];
- if (includeTimestamps && date) {
- [string appendString:[dateFormatter stringFromDate:date]];
- [string appendString:@" "];
- }
-
- NSString *connection = [message messageConnection];
- if (includeConnections && connection) {
- [string appendString:connection];
- [string appendString:@" "];
- }
-
- NSString *database = [message messageDatabase];
- if (includeDatabases && database) {
- [string appendString:database];
- [string appendString:@" "];
- }
-
- if (includeTimestamps || includeConnections || includeDatabases) [string appendString:@"*/ "];
-
- [string appendFormat:@"%@\n", [message message]];
- }
- }];
+ NSString *string = [self sqlStringForRowIndexes:rows];
NSPasteboard *pasteBoard = [NSPasteboard generalPasteboard];
@@ -200,7 +164,50 @@ static SPQueryController *sharedQueryController = nil;
[pasteBoard declareTypes:@[NSStringPboardType] owner:nil];
[pasteBoard setString:string forType:NSStringPboardType];
}
-#endif
+}
+
+- (NSString *)sqlStringForRowIndexes:(NSIndexSet *)rows
+{
+ if(![rows count]) return @"";
+
+ NSMutableString *string = [[NSMutableString alloc] init];
+
+ BOOL includeTimestamps = ![[consoleTableView tableColumnWithIdentifier:SPTableViewDateColumnID] isHidden];
+ BOOL includeConnections = ![[consoleTableView tableColumnWithIdentifier:SPTableViewConnectionColumnID] isHidden];
+ BOOL includeDatabases = ![[consoleTableView tableColumnWithIdentifier:SPTableViewDatabaseColumnID] isHidden];
+
+ [rows enumerateIndexesUsingBlock:^(NSUInteger i, BOOL * _Nonnull stop) {
+ if (i < [messagesVisibleSet count]) {
+ SPConsoleMessage *message = NSArrayObjectAtIndex(messagesVisibleSet, i);
+
+ if (includeTimestamps || includeConnections || includeDatabases) [string appendString:@"/* "];
+
+ NSDate *date = [message messageDate];
+ if (includeTimestamps && date) {
+ [string appendString:[dateFormatter stringFromDate:date]];
+ [string appendString:@" "];
+ }
+
+ NSString *connection = [message messageConnection];
+ if (includeConnections && connection) {
+ [string appendString:connection];
+ [string appendString:@" "];
+ }
+
+ NSString *database = [message messageDatabase];
+ if (includeDatabases && database) {
+ [string appendString:database];
+ [string appendString:@" "];
+ }
+
+ if (includeTimestamps || includeConnections || includeDatabases) [string appendString:@"*/ "];
+
+ [string appendString:[message message]];
+ [string appendString:@"\n"];
+ }
+ }];
+
+ return [string autorelease];
}
/**
diff --git a/Source/SPQueryControllerInitializer.m b/Source/SPQueryControllerInitializer.m
index fc03d107..f8292b37 100644
--- a/Source/SPQueryControllerInitializer.m
+++ b/Source/SPQueryControllerInitializer.m
@@ -85,6 +85,9 @@ static NSString *SPCompletionTokensSnippetsKey = @"function_argument_snippets";
{
[[column dataCell] setFont:(useMonospacedFont) ? [NSFont fontWithName:SPDefaultMonospacedFontName size:monospacedFontSize] : [NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
}
+
+ //allow drag-out copying of selected rows
+ [consoleTableView setDraggingSourceOperationMask:NSDragOperationCopy forLocal:NO];
#endif
}
diff --git a/Source/SPTableContent.h b/Source/SPTableContent.h
index ac720d01..74329e20 100644
--- a/Source/SPTableContent.h
+++ b/Source/SPTableContent.h
@@ -125,7 +125,6 @@
BOOL _mainNibLoaded;
BOOL isWorking;
pthread_mutex_t tableValuesLock;
- NSCondition *tableLoadingCondition;
#ifndef SP_CODA
NSMutableArray *nibObjectsToRelease;
#endif
diff --git a/Source/SPTableContent.m b/Source/SPTableContent.m
index 416b7cd8..48871c11 100644
--- a/Source/SPTableContent.m
+++ b/Source/SPTableContent.m
@@ -31,6 +31,7 @@
#import "SPTableContent.h"
#import "SPTableContentFilter.h"
+#import "SPTableContentDataSource.h"
#import "SPDatabaseDocument.h"
#import "SPTableStructure.h"
#import "SPTableInfo.h"
@@ -81,6 +82,7 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOper
@interface SPTableContent ()
- (BOOL)cancelRowEditing;
+- (void)documentWillClose:(NSNotification *)notification;
@end
@@ -172,7 +174,6 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOper
usedQuery = [[NSString alloc] initWithString:@""];
tableLoadTimer = nil;
- tableLoadingCondition = [NSCondition new];
blackColor = [NSColor blackColor];
lightGrayColor = [NSColor lightGrayColor];
@@ -243,20 +244,16 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOper
[nibLoader release];
-#if __MAC_OS_X_VERSION_MAX_ALLOWED >= __MAC_10_7
//let's see if we can use the NSPopover (10.7+) or have to make do with our legacy clone.
//this is using reflection right now, as our SDK is 10.8 but our minimum supported version is 10.6
Class popOverClass = NSClassFromString(@"NSPopover");
- if(popOverClass)
- {
+ if(popOverClass) {
paginationPopover = [[popOverClass alloc] init];
[paginationPopover setDelegate:(SPTableContent<NSPopoverDelegate> *)self];
[paginationPopover setContentViewController:paginationViewController];
[paginationPopover setBehavior:NSPopoverBehaviorTransient];
}
- else
-#endif
- {
+ else {
[paginationBox setContentView:[paginationViewController view]];
// Add the pagination view to the content area
@@ -297,6 +294,10 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOper
selector:@selector(endDocumentTaskForTab:)
name:SPDocumentTaskEndNotification
object:tableDocumentInstance];
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(documentWillClose:)
+ name:SPDocumentWillCloseNotification
+ object:tableDocumentInstance];
}
#pragma mark -
@@ -1060,11 +1061,8 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOper
// Set up the table updates timer and wait for it to notify this thread about completion
[[self onMainThread] initTableLoadTimer];
- [tableLoadingCondition lock];
- while (![tableValues dataDownloaded]) {
- [tableLoadingCondition waitUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.05]];
- }
- [tableLoadingCondition unlock];
+ [tableValues awaitDataDownloaded];
+
tableRowsCount = [tableValues count];
// If the final column autoresize wasn't performed, perform it
@@ -1269,10 +1267,7 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOper
}
if ([tableValues dataDownloaded]) {
- [tableLoadingCondition lock];
- [tableLoadingCondition signal];
[self clearTableLoadTimer];
- [tableLoadingCondition unlock];
}
// Check whether a table update is required, based on whether new rows are
@@ -1328,20 +1323,21 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOper
{
NSAutoreleasePool *reloadPool = [[NSAutoreleasePool alloc] init];
- // Check whether a save of the current row is required.
- if (![[self onMainThread] saveRowOnDeselect]) return;
+ // Check whether a save of the current row is required, abort if pending changes couldn't be saved.
+ if ([[self onMainThread] saveRowOnDeselect]) {
- // Save view details to restore safely if possible (except viewport, which will be
- // preserved automatically, and can then be scrolled as the table loads)
- [self storeCurrentDetailsForRestoration];
- [self setViewportToRestore:NSZeroRect];
+ // Save view details to restore safely if possible (except viewport, which will be
+ // preserved automatically, and can then be scrolled as the table loads)
+ [self storeCurrentDetailsForRestoration];
+ [self setViewportToRestore:NSZeroRect];
- // Clear the table data column cache and status (including counts)
- [tableDataInstance resetColumnData];
- [tableDataInstance resetStatusData];
+ // Clear the table data column cache and status (including counts)
+ [tableDataInstance resetColumnData];
+ [tableDataInstance resetStatusData];
- // Load the table's data
- [self loadTable:[tablesListInstance tableName]];
+ // Load the table's data
+ [self loadTable:[tablesListInstance tableName]];
+ }
[tableDocumentInstance endTask];
@@ -1638,13 +1634,11 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOper
}
#endif
-#if __MAC_OS_X_VERSION_MAX_ALLOWED >= __MAC_10_7
- (void)popoverDidClose:(NSNotification *)notification
{
//not to hide the view, but to change the paginationButton
[self setPaginationViewVisibility:NO];
}
-#endif
/**
* Show or hide the pagination layer, also changing the first responder as appropriate.
@@ -1672,7 +1666,6 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOper
}
}
-#if __MAC_OS_X_VERSION_MAX_ALLOWED >= __MAC_10_7
if(paginationPopover) {
if(makeVisible) {
[paginationPopover showRelativeToRect:[paginationButton bounds] ofView:paginationButton preferredEdge:NSMinYEdge];
@@ -1684,7 +1677,6 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOper
}
return;
}
-#endif
if (makeVisible) {
if (paginationViewFrame.size.height == paginationViewHeight) return;
@@ -2399,7 +2391,8 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOper
for (NSTableColumn *aTableColumn in tableColumns)
{
- id o = SPDataStorageObjectAtRowAndColumn(tableValues, i, [[aTableColumn identifier] integerValue]);
+ NSUInteger columnIndex = [[aTableColumn identifier] integerValue];
+ id o = SPDataStorageObjectAtRowAndColumn(tableValues, i, columnIndex);
if ([o isNSNull]) {
[tempRow addObject:includeNULLs ? [NSNull null] : [prefs objectForKey:SPNullValue]];
@@ -2450,7 +2443,17 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOper
[[image TIFFRepresentationUsingCompression:NSTIFFCompressionJPEG factor:0.01f] base64Encoding]]];
}
else {
- [tempRow addObject:hide ? @"&lt;BLOB&gt;" : [o stringRepresentationUsingEncoding:[mySQLConnection stringEncoding]]];
+ NSString *str;
+ if (hide) {
+ str = @"&lt;BLOB&gt;";
+ }
+ else if ([self cellValueIsDisplayedAsHexForColumn:columnIndex]) {
+ str = [NSString stringWithFormat:@"0x%@", [o dataToHexString]];
+ }
+ else {
+ str = [o stringRepresentationUsingEncoding:[mySQLConnection stringEncoding]];
+ }
+ [tempRow addObject:str];
}
if(image) [image release];
@@ -2549,13 +2552,15 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOper
@"filterValue": targetFilterValue,
@"filterComparison": SPBoxNil(filterComparison)
};
- [self setFiltersToRestore:filterSettings];
-
- // Attempt to switch to the target table
- if (![tablesListInstance selectItemWithName:[refDictionary objectForKey:@"table"]]) {
- NSBeep();
- [self setFiltersToRestore:nil];
- }
+ SPMainQSync(^{
+ [self setFiltersToRestore:filterSettings];
+
+ // Attempt to switch to the target table
+ if (![tablesListInstance selectItemWithName:[refDictionary objectForKey:@"table"]]) {
+ NSBeep();
+ [self setFiltersToRestore:nil];
+ }
+ });
}
#ifndef SP_CODA
@@ -4148,6 +4153,13 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOper
tableRowsSelectable = YES;
}
+//this method is called right before the UI objects are deallocated
+- (void)documentWillClose:(NSNotification *)notification
+{
+ // if a result load is in progress we must stop the timer or it may try to call invalid IBOutlets
+ [self clearTableLoadTimer];
+}
+
#pragma mark -
#pragma mark KVO methods
@@ -4232,7 +4244,6 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOper
if(fieldEditor) SPClear(fieldEditor);
[self clearTableLoadTimer];
- SPClear(tableLoadingCondition);
SPClear(tableValues);
pthread_mutex_destroy(&tableValuesLock);
SPClear(dataColumns);
diff --git a/Source/SPTableContentDataSource.h b/Source/SPTableContentDataSource.h
index f257dce7..19864a80 100644
--- a/Source/SPTableContentDataSource.h
+++ b/Source/SPTableContentDataSource.h
@@ -32,4 +32,6 @@
@interface SPTableContent (SPTableContentDataSource)
+- (BOOL)cellValueIsDisplayedAsHexForColumn:(NSUInteger)columnIndex;
+
@end
diff --git a/Source/SPTableContentDataSource.m b/Source/SPTableContentDataSource.m
index 56e8df71..a623f83b 100644
--- a/Source/SPTableContentDataSource.m
+++ b/Source/SPTableContentDataSource.m
@@ -33,6 +33,7 @@
#import "SPDataStorage.h"
#import "SPCopyTable.h"
#import "SPTablesList.h"
+#import "SPAlertSheets.h"
#import <pthread.h>
#import <SPMySQL/SPMySQL.h>
@@ -119,7 +120,7 @@
if ([self cellValueIsDisplayedAsHexForColumn:columnIndex]) {
if ([(NSData *)value length] > 255) {
- return [NSString stringWithFormat:@"0x%@...", [[(NSData *)value subdataWithRange:NSMakeRange(0, 255)] dataToHexString]];
+ return [NSString stringWithFormat:@"0x%@…", [[(NSData *)value subdataWithRange:NSMakeRange(0, 255)] dataToHexString]];
}
return [NSString stringWithFormat:@"0x%@", [(NSData *)value dataToHexString]];
}
@@ -164,10 +165,10 @@
}
#endif
if (tableView == tableContentView) {
-
+ NSInteger columnIndex = [[tableColumn identifier] integerValue];
// If the current cell should have been edited in a sheet, do nothing - field closing will have already
// updated the field.
- if ([tableContentView shouldUseFieldEditorForRow:rowIndex column:[[tableColumn identifier] integerValue] checkWithLock:NULL]) {
+ if ([tableContentView shouldUseFieldEditorForRow:rowIndex column:columnIndex checkWithLock:NULL]) {
return;
}
@@ -190,18 +191,29 @@
currentlyEditingRow = rowIndex;
}
- NSDictionary *column = NSArrayObjectAtIndex(dataColumns, [[tableColumn identifier] integerValue]);
+ NSDictionary *column = NSArrayObjectAtIndex(dataColumns, columnIndex);
if (object) {
// Restore NULLs if necessary
if ([object isEqualToString:[prefs objectForKey:SPNullValue]] && [[column objectForKey:@"null"] boolValue]) {
object = [NSNull null];
}
+ else if ([self cellValueIsDisplayedAsHexForColumn:columnIndex]) {
+ // This is a binary object being edited as a hex string.
+ // Convert the string back to binary.
+ // Error checking is done in -control:textShouldEndEditing:
+ NSData *data = [NSData dataWithHexString:object];
+ if (!data) {
+ NSBeep();
+ return;
+ }
+ object = data;
+ }
- [tableValues replaceObjectInRow:rowIndex column:[[tableColumn identifier] integerValue] withObject:object];
+ [tableValues replaceObjectInRow:rowIndex column:columnIndex withObject:object];
}
else {
- [tableValues replaceObjectInRow:rowIndex column:[[tableColumn identifier] integerValue] withObject:@""];
+ [tableValues replaceObjectInRow:rowIndex column:columnIndex withObject:@""];
}
}
}
diff --git a/Source/SPTableContentDelegate.m b/Source/SPTableContentDelegate.m
index a510108b..0a80b602 100644
--- a/Source/SPTableContentDelegate.m
+++ b/Source/SPTableContentDelegate.m
@@ -30,6 +30,7 @@
#import "SPTableContentDelegate.h"
#import "SPTableContentFilter.h"
+#import "SPTableContentDataSource.h"
#ifndef SP_CODA /* headers */
#import "SPAppController.h"
#endif
@@ -54,7 +55,6 @@
@interface SPTableContent (SPDeclaredAPI)
- (BOOL)cancelRowEditing;
-- (BOOL)cellValueIsDisplayedAsHexForColumn:(NSUInteger)columnIndex;
@end
@@ -273,13 +273,6 @@
// Retrieve the column definition
NSDictionary *columnDefinition = [cqColumnDefinition objectAtIndex:[[tableColumn identifier] integerValue]];
- // TODO: Fix editing of "Display as Hex" columns and remove this (also see above)
- if ([self cellValueIsDisplayedAsHexForColumn:[[tableColumn identifier] integerValue]]) {
- NSBeep();
- [SPTooltip showWithObject:NSLocalizedString(@"Disable \"Display Binary Data as Hex\" in the View menu to edit this field.",@"Temporary : Tooltip shown when trying to edit a binary field in table content view while it is displayed using HEX conversion")];
- return NO;
- }
-
// Open the editing sheet if required
if ([tableContentView shouldUseFieldEditorForRow:rowIndex column:[[tableColumn identifier] integerValue] checkWithLock:NULL]) {
@@ -518,12 +511,10 @@
}
else {
[cell setTextColor:blackColor];
- }
-
- NSDictionary *columnDefinition = [[(id <SPDatabaseContentViewDelegate>)[tableContentView delegate] dataColumnDefinitions] objectAtIndex:columnIndex];
-
- if ([self cellValueIsDisplayedAsHexForColumn:[[tableColumn identifier] integerValue]]) {
- [cell setTextColor:rowIndex == [tableContentView selectedRow] ? whiteColor : blueColor];
+
+ if ([self cellValueIsDisplayedAsHexForColumn:[[tableColumn identifier] integerValue]]) {
+ [cell setTextColor:rowIndex == [tableContentView selectedRow] ? whiteColor : blueColor];
+ }
}
// Disable link arrows for the currently editing row and for any NULL or unloaded cells
@@ -689,6 +680,34 @@
#pragma mark -
#pragma mark Control delegate methods
+- (BOOL)control:(NSControl *)control textShouldEndEditing:(NSText *)editor
+{
+ // Validate hex input
+ // We do this here because the textfield will still be selected with the pending changes if we bail out here
+ if(control == tableContentView) {
+ NSInteger columnIndex = [tableContentView editedColumn];
+ if ([self cellValueIsDisplayedAsHexForColumn:columnIndex]) {
+ // special case: the "NULL" string
+ NSDictionary *column = NSArrayObjectAtIndex(dataColumns, columnIndex);
+ if ([[editor string] isEqualToString:[prefs objectForKey:SPNullValue]] && [[column objectForKey:@"null"] boolValue]) {
+ return YES;
+ }
+ // This is a binary object being edited as a hex string.
+ // Convert the string back to binary, checking for errors.
+ NSData *data = [NSData dataWithHexString:[editor string]];
+ if (!data) {
+ SPOnewayAlertSheet(
+ NSLocalizedString(@"Invalid hexadecimal value", @"table content : editing : error message title when parsing as hex string failed"),
+ [tableDocumentInstance parentWindow],
+ NSLocalizedString(@"A valid hex string may only contain the numbers 0-9 and letters A-F (a-f). It can optionally begin with „0x“ and spaces will be ignored.\nAlternatively the syntax X'val' is supported, too.", @"table content : editing : error message description when parsing as hex string failed")
+ );
+ return NO;
+ }
+ }
+ }
+ return YES;
+}
+
- (void)controlTextDidChange:(NSNotification *)notification
{
#ifndef SP_CODA
diff --git a/Source/SPTableData.m b/Source/SPTableData.m
index 8033d5a0..a0bfa7ff 100644
--- a/Source/SPTableData.m
+++ b/Source/SPTableData.m
@@ -524,7 +524,7 @@
}
/**
- * Analyse a CREATE TABLE tring to extract the field details, primary key, unique keys, and table encoding.
+ * Analyse a CREATE TABLE string to extract the field details, primary key, unique keys, and table encoding.
* @param tableDef @"CREATE TABLE ..."
* @param tableType Can either be Table or View. Value is copied to the result and not used otherwise
* @return A dict containing info about the table's structure
@@ -532,6 +532,9 @@
* In future this could also be used to retrieve the majority of index information
* assuming information like cardinality isn't needed.
* This function is rather long due to the painful parsing required, but is fast.
+ *
+ * *WARNING* This method is only designed to handle the output of a "SHOW CREATE ..." query.
+ * DO NOT try to use it with user-defined input. The code does not handle the full possible syntax!
*/
- (NSDictionary *)parseCreateStatement:(NSString *)tableDef ofType:(NSString *)tableType
{
@@ -579,11 +582,12 @@
if(fieldName == nil || [fieldName length] == 0) {
NSBeep();
SPOnewayAlertSheetWithStyle(
- NSLocalizedString(@"Error while parsing CREATE TABLE syntax",@"error while parsing CREATE TABLE syntax"),
- nil,
- nil,
- [NSString stringWithFormat:NSLocalizedString(@"“%@” couldn't be parsed. You can edit the column setup but the column will not be shown in the Content view; please report this issue to the Sequel Pro team using the Help menu item.", @"“%@” couldn't be parsed. You can edit the column setup but the column will not be shown in the Content view; please report this issue to the Sequel Pro team using the Help menu item."), fieldsParser],
- NSCriticalAlertStyle);
+ NSLocalizedString(@"Error while parsing CREATE TABLE syntax",@"error while parsing CREATE TABLE syntax"),
+ nil,
+ nil,
+ [NSString stringWithFormat:NSLocalizedString(@"“%@” couldn't be parsed. You can edit the column setup but the column will not be shown in the Content view; please report this issue to the Sequel Pro team using the Help menu item.", @"“%@” couldn't be parsed. You can edit the column setup but the column will not be shown in the Content view; please report this issue to the Sequel Pro team using the Help menu item."), fieldsParser],
+ NSCriticalAlertStyle
+ );
continue;
}
//if the next character is again a backtick, we stumbled across an escaped backtick. we have to continue parsing.
@@ -622,108 +626,116 @@
// Constraints
if ([[parts objectAtIndex:0] hasPrefix:@"CONSTRAINT"]) {
NSMutableDictionary *constraintDetails = [[NSMutableDictionary alloc] init];
-
- // Extract the relevant details from the constraint string
- [fieldsParser setString:[[parts objectAtIndex:1] stringByTrimmingCharactersInSet:bracketSet]];
- [constraintDetails setObject:[fieldsParser unquotedString] forKey:@"name"];
-
- NSMutableArray *keyColumns = [NSMutableArray array];
- NSArray *keyColumnStrings = [[[parts objectAtIndex:4] stringByTrimmingCharactersInSet:bracketSet] componentsSeparatedByString:@","];
-
- for (NSString *keyColumn in keyColumnStrings)
- {
- [fieldsParser setString:[[keyColumn stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]] stringByTrimmingCharactersInSet:bracketSet]];
- [keyColumns addObject:[fieldsParser unquotedString]];
- }
-
- [constraintDetails setObject:keyColumns forKey:@"columns"];
-
- NSString *part = [[parts objectAtIndex:6] stringByTrimmingCharactersInSet:bracketSet];
-
- NSArray *reference = [part captureComponentsMatchedByRegex:@"^`([\\w_.]+)`\\.`([\\w_.]+)`$" options:RKLCaseless range:NSMakeRange(0, [part length]) error:nil];
- if ([reference count]) {
- [constraintDetails setObject:[reference objectAtIndex:1] forKey:@"ref_database"];
- [constraintDetails setObject:[reference objectAtIndex:2] forKey:@"ref_table"];
- }
- else {
- [fieldsParser setString:part];
- [constraintDetails setObject:[fieldsParser unquotedString] forKey:@"ref_table"];
- }
-
- NSMutableArray *refKeyColumns = [NSMutableArray array];
- NSArray *refKeyColumnStrings = [[[parts objectAtIndex:7] stringByTrimmingCharactersInSet:bracketSet] componentsSeparatedByString:@","];
-
- for (NSString *keyColumn in refKeyColumnStrings)
- {
- [fieldsParser setString:[[keyColumn stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]] stringByTrimmingCharactersInSet:bracketSet]];
- [refKeyColumns addObject:[fieldsParser unquotedString]];
- }
-
- [constraintDetails setObject:refKeyColumns forKey:@"ref_columns"];
-
- NSUInteger nextOffs = 12;
-
- if ([parts count] > 8) {
- // NOTE: this won't get SET NULL | NO ACTION | RESTRICT
- if ([[parts objectAtIndex:9] hasPrefix:@"UPDATE"]) {
- if( [NSArrayObjectAtIndex(parts, 10) hasPrefix:@"SET"] ) {
- [constraintDetails setObject:@"SET NULL"
- forKey:@"update"];
- nextOffs = 13;
- } else if( [NSArrayObjectAtIndex(parts, 10) hasPrefix:@"NO"] ) {
- [constraintDetails setObject:@"NO ACTION"
- forKey:@"update"];
- nextOffs = 13;
- } else {
- [constraintDetails setObject:NSArrayObjectAtIndex(parts, 10)
- forKey:@"update"];
- }
+ if([[parts objectAtIndex:2] hasPrefix:@"FOREIGN"] && [[parts objectAtIndex:3] hasPrefix:@"KEY"]) {
+ // Extract the relevant details from the constraint string
+ [fieldsParser setString:[[parts objectAtIndex:1] stringByTrimmingCharactersInSet:bracketSet]];
+ [constraintDetails setObject:[fieldsParser unquotedString] forKey:@"name"];
+
+ NSMutableArray *keyColumns = [NSMutableArray array];
+ NSArray *keyColumnStrings = [[[parts objectAtIndex:4] stringByTrimmingCharactersInSet:bracketSet] componentsSeparatedByString:@","];
+
+ for (NSString *keyColumn in keyColumnStrings)
+ {
+ [fieldsParser setString:[[keyColumn stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]] stringByTrimmingCharactersInSet:bracketSet]];
+ [keyColumns addObject:[fieldsParser unquotedString]];
}
- else if ([NSArrayObjectAtIndex(parts, 9) hasPrefix:@"DELETE"]) {
- if ([NSArrayObjectAtIndex(parts, 10) hasPrefix:@"SET"]) {
- [constraintDetails setObject:@"SET NULL"
- forKey:@"delete"];
- nextOffs = 13;
- } else if( [NSArrayObjectAtIndex(parts, 10) hasPrefix:@"NO"] ) {
- [constraintDetails setObject:@"NO ACTION"
- forKey:@"delete"];
- nextOffs = 13;
- } else {
- [constraintDetails setObject:NSArrayObjectAtIndex(parts, 10)
- forKey:@"delete"];
- }
+
+ [constraintDetails setObject:keyColumns forKey:@"columns"];
+
+ NSString *part = [[parts objectAtIndex:6] stringByTrimmingCharactersInSet:bracketSet];
+
+ NSArray *reference = [part captureComponentsMatchedByRegex:@"^`([\\w_.]+)`\\.`([\\w_.]+)`$" options:RKLCaseless range:NSMakeRange(0, [part length]) error:nil];
+
+ if ([reference count]) {
+ [constraintDetails setObject:[reference objectAtIndex:1] forKey:@"ref_database"];
+ [constraintDetails setObject:[reference objectAtIndex:2] forKey:@"ref_table"];
}
- }
-
- if ([parts count] > nextOffs - 1) {
- if( [NSArrayObjectAtIndex(parts, nextOffs) hasPrefix:@"UPDATE"] ) {
- if( [NSArrayObjectAtIndex(parts, nextOffs+1) hasPrefix:@"SET"] ) {
- [constraintDetails setObject:@"SET NULL"
- forKey:@"update"];
- } else if( [NSArrayObjectAtIndex(parts, nextOffs+1) hasPrefix:@"NO"] ) {
- [constraintDetails setObject:@"NO ACTION"
- forKey:@"update"];
- } else {
- [constraintDetails setObject:NSArrayObjectAtIndex(parts, nextOffs+1)
- forKey:@"update"];
+ else {
+ [fieldsParser setString:part];
+ [constraintDetails setObject:[fieldsParser unquotedString] forKey:@"ref_table"];
+ }
+
+ NSMutableArray *refKeyColumns = [NSMutableArray array];
+ NSArray *refKeyColumnStrings = [[[parts objectAtIndex:7] stringByTrimmingCharactersInSet:bracketSet] componentsSeparatedByString:@","];
+
+ for (NSString *keyColumn in refKeyColumnStrings)
+ {
+ [fieldsParser setString:[[keyColumn stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]] stringByTrimmingCharactersInSet:bracketSet]];
+ [refKeyColumns addObject:[fieldsParser unquotedString]];
+ }
+
+ [constraintDetails setObject:refKeyColumns forKey:@"ref_columns"];
+
+ NSUInteger nextOffs = 12;
+
+ if ([parts count] > 8) {
+ // NOTE: this won't get SET NULL | NO ACTION | RESTRICT
+ if ([[parts objectAtIndex:9] hasPrefix:@"UPDATE"]) {
+ if( [NSArrayObjectAtIndex(parts, 10) hasPrefix:@"SET"] ) {
+ [constraintDetails setObject:@"SET NULL"
+ forKey:@"update"];
+ nextOffs = 13;
+ } else if( [NSArrayObjectAtIndex(parts, 10) hasPrefix:@"NO"] ) {
+ [constraintDetails setObject:@"NO ACTION"
+ forKey:@"update"];
+ nextOffs = 13;
+ } else {
+ [constraintDetails setObject:NSArrayObjectAtIndex(parts, 10)
+ forKey:@"update"];
+ }
+ }
+ else if ([NSArrayObjectAtIndex(parts, 9) hasPrefix:@"DELETE"]) {
+ if ([NSArrayObjectAtIndex(parts, 10) hasPrefix:@"SET"]) {
+ [constraintDetails setObject:@"SET NULL"
+ forKey:@"delete"];
+ nextOffs = 13;
+ } else if( [NSArrayObjectAtIndex(parts, 10) hasPrefix:@"NO"] ) {
+ [constraintDetails setObject:@"NO ACTION"
+ forKey:@"delete"];
+ nextOffs = 13;
+ } else {
+ [constraintDetails setObject:NSArrayObjectAtIndex(parts, 10)
+ forKey:@"delete"];
+ }
}
}
- else if( [NSArrayObjectAtIndex(parts, nextOffs) hasPrefix:@"DELETE"] ) {
- if( [NSArrayObjectAtIndex(parts, nextOffs+1) hasPrefix:@"SET"] ) {
- [constraintDetails setObject:@"SET NULL"
- forKey:@"delete"];
- } else if( [NSArrayObjectAtIndex(parts, nextOffs+1) hasPrefix:@"NO"] ) {
- [constraintDetails setObject:@"NO ACTION"
- forKey:@"delete"];
- } else {
- [constraintDetails setObject:NSArrayObjectAtIndex(parts, nextOffs+1)
- forKey:@"delete"];
+
+ if ([parts count] > nextOffs - 1) {
+ if( [NSArrayObjectAtIndex(parts, nextOffs) hasPrefix:@"UPDATE"] ) {
+ if( [NSArrayObjectAtIndex(parts, nextOffs+1) hasPrefix:@"SET"] ) {
+ [constraintDetails setObject:@"SET NULL"
+ forKey:@"update"];
+ } else if( [NSArrayObjectAtIndex(parts, nextOffs+1) hasPrefix:@"NO"] ) {
+ [constraintDetails setObject:@"NO ACTION"
+ forKey:@"update"];
+ } else {
+ [constraintDetails setObject:NSArrayObjectAtIndex(parts, nextOffs+1)
+ forKey:@"update"];
+ }
+ }
+ else if( [NSArrayObjectAtIndex(parts, nextOffs) hasPrefix:@"DELETE"] ) {
+ if( [NSArrayObjectAtIndex(parts, nextOffs+1) hasPrefix:@"SET"] ) {
+ [constraintDetails setObject:@"SET NULL"
+ forKey:@"delete"];
+ } else if( [NSArrayObjectAtIndex(parts, nextOffs+1) hasPrefix:@"NO"] ) {
+ [constraintDetails setObject:@"NO ACTION"
+ forKey:@"delete"];
+ } else {
+ [constraintDetails setObject:NSArrayObjectAtIndex(parts, nextOffs+1)
+ forKey:@"delete"];
+ }
}
}
+
+ [constraints addObject:constraintDetails];
+ }
+ else {
+ //TODO: MariaDB 10.2.1+ (not Mysql) supports syntax:
+ // CONSTRAINT [constraint_name] CHECK (expression)
+ SPLog(@"Skipping unrecognized CONSTRAINT in CREATE stmt: %@", fieldsParser);
}
- [constraints addObject:constraintDetails];
[constraintDetails release];
}
@@ -785,7 +797,7 @@
NSUInteger stringStart = NSMaxRange(charsetDefinitionRange);
NSUInteger i;
for (i = stringStart; i < [createTableParser length]; i++) {
- if ([createTableParser characterAtIndex:i] == ' ') break;
+ if ([whitespaceAndNewlineSet characterIsMember:[createTableParser characterAtIndex:i]]) break;
}
// Catch the "default" character encoding:
diff --git a/Source/SPTableRelations.h b/Source/SPTableRelations.h
index 1ab7f1b7..6fa2bd57 100644
--- a/Source/SPTableRelations.h
+++ b/Source/SPTableRelations.h
@@ -34,7 +34,7 @@
@class SPTablesList;
@class SPTableData;
-@interface SPTableRelations : NSObject
+@interface SPTableRelations : NSObject <NSTableViewDelegate, NSTableViewDataSource>
{
IBOutlet SPDatabaseDocument *tableDocumentInstance;
IBOutlet SPTablesList *tablesListInstance;
@@ -85,8 +85,8 @@
- (void)tableSelectionChanged:(NSNotification *)notification;
// Task interaction
-- (void)startDocumentTaskForTab:(NSNotification *)aNotification;
-- (void)endDocumentTaskForTab:(NSNotification *)aNotification;
+- (void)startDocumentTaskForTab:(NSNotification *)notification;
+- (void)endDocumentTaskForTab:(NSNotification *)notification;
// Other
- (NSArray *)relationDataForPrinting;
diff --git a/Source/SPTableRelations.m b/Source/SPTableRelations.m
index 64a7b98b..20059fbb 100644
--- a/Source/SPTableRelations.m
+++ b/Source/SPTableRelations.m
@@ -373,27 +373,6 @@ static NSString *SPRelationOnDeleteKey = @"on_delete";
}
#pragma mark -
-#pragma mark TextField delegate methods
-
-- (void)controlTextDidChange:(NSNotification *)notification
-{
- // Make sure the user does not enter a taken name, using the quickly-generated incomplete list
- if ([notification object] == constraintName) {
- NSString *userValue = [[constraintName stringValue] lowercaseString];
-
- // Make field red and disable add button
- if ([takenConstraintNames containsObject:userValue]) {
- [constraintName setTextColor:[NSColor redColor]];
- [confirmAddRelationButton setEnabled:NO];
- }
- else {
- [constraintName setTextColor:[NSColor controlTextColor]];
- [confirmAddRelationButton setEnabled:YES];
- }
- }
-}
-
-#pragma mark -
#pragma mark Tableview datasource methods
- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView
@@ -413,44 +392,13 @@ static NSString *SPRelationOnDeleteKey = @"on_delete";
}
#pragma mark -
-#pragma mark Tableview delegate methods
-
-/**
- * Called whenever the relations table view selection changes.
- */
-- (void)tableViewSelectionDidChange:(NSNotification *)notification
-{
- [removeRelationButton setEnabled:([relationsTableView numberOfSelectedRows] > 0)];
-}
-
-/*
- * Double-click action on table cells - for the time being, return
- * NO to disable editing.
- */
-- (BOOL)tableView:(NSTableView *)tableView shouldEditTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)rowIndex
-{
- if ([tableDocumentInstance isWorking]) return NO;
-
- return NO;
-}
-
-/**
- * Disable row selection while the document is working.
- */
-- (BOOL)tableView:(NSTableView *)tableView shouldSelectRow:(NSInteger)rowIndex
-{
- return ![tableDocumentInstance isWorking];
-}
-
-#pragma mark -
#pragma mark Task interaction
/**
* Disable all content interactive elements during an ongoing task.
*/
-- (void)startDocumentTaskForTab:(NSNotification *)aNotification
+- (void)startDocumentTaskForTab:(NSNotification *)notification
{
-
// Only proceed if this view is selected.
if (![[tableDocumentInstance selectedToolbarItemIdentifier] isEqualToString:SPMainToolbarTableRelations]) return;
@@ -462,7 +410,7 @@ static NSString *SPRelationOnDeleteKey = @"on_delete";
/**
* Enable all content interactive elements after an ongoing task.
*/
-- (void)endDocumentTaskForTab:(NSNotification *)aNotification
+- (void)endDocumentTaskForTab:(NSNotification *)notification
{
// Only proceed if this view is selected.
if (![[tableDocumentInstance selectedToolbarItemIdentifier] isEqualToString:SPMainToolbarTableRelations]) return;
diff --git a/Source/SPMenuAdditions.h b/Source/SPTableRelationsDelegate.h
index 6c1070b2..79b1e914 100644
--- a/Source/SPMenuAdditions.h
+++ b/Source/SPTableRelationsDelegate.h
@@ -1,9 +1,9 @@
//
-// SPMenuAdditions.h
+// SPTableRelationsDelegate.h
// sequel-pro
//
-// Created by Rowan Beentje on November 27, 2010.
-// Copyright (c) 2010 Rowan Beentje. All rights reserved.
+// Created by Stuart Connolly (stuconnolly.com) on March 28, 2017.
+// Copyright (c) 2017 Stuart Connolly. All rights reserved.
//
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
@@ -28,9 +28,8 @@
//
// More info at <https://github.com/sequelpro/sequelpro>
-@interface NSMenu (SPMenuAdditions)
+#import "SPTableRelations.h"
-// Add a 10.5-compatible removeAllItems
-- (void)compatibleRemoveAllItems;
+@interface SPTableRelations (SPTableRelationsDelegate)
@end
diff --git a/Source/SPTableRelationsDelegate.m b/Source/SPTableRelationsDelegate.m
new file mode 100644
index 00000000..bf083318
--- /dev/null
+++ b/Source/SPTableRelationsDelegate.m
@@ -0,0 +1,87 @@
+//
+// SPTableRelationsDelegate.m
+// sequel-pro
+//
+// Created by Stuart Connolly (stuconnolly.com) on March 28, 2017.
+// Copyright (c) 2017 Stuart Connolly. All rights reserved.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the "Software"), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+// More info at <https://github.com/sequelpro/sequelpro>
+
+#import "SPTableRelationsDelegate.h"
+#import "SPDatabaseDocument.h"
+
+@implementation SPTableRelations (SPTableRelationsDelegate)
+
+#pragma mark -
+#pragma mark TextField delegate methods
+
+- (void)controlTextDidChange:(NSNotification *)notification
+{
+ // Make sure the user does not enter a taken name, using the quickly-generated incomplete list
+ if ([notification object] == constraintName) {
+ NSString *userValue = [[constraintName stringValue] lowercaseString];
+
+ // Make field red and disable add button
+ if ([takenConstraintNames containsObject:userValue]) {
+ [constraintName setTextColor:[NSColor redColor]];
+ [confirmAddRelationButton setEnabled:NO];
+ }
+ else {
+ [constraintName setTextColor:[NSColor controlTextColor]];
+ [confirmAddRelationButton setEnabled:YES];
+ }
+ }
+}
+
+#pragma mark -
+#pragma mark Tableview delegate methods
+
+/**
+ * Called whenever the relations table view selection changes.
+ */
+- (void)tableViewSelectionDidChange:(NSNotification *)notification
+{
+ [removeRelationButton setEnabled:([relationsTableView numberOfSelectedRows] > 0)];
+}
+
+/*
+ * Double-click action on table cells - for the time being, return
+ * NO to disable editing.
+ */
+- (BOOL)tableView:(NSTableView *)tableView shouldEditTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)rowIndex
+{
+ if ([tableDocumentInstance isWorking]) return NO;
+
+ return NO;
+}
+
+/**
+ * Disable row selection while the document is working.
+ */
+- (BOOL)tableView:(NSTableView *)tableView shouldSelectRow:(NSInteger)rowIndex
+{
+ return ![tableDocumentInstance isWorking];
+}
+
+@end
diff --git a/Source/SPTablesList.m b/Source/SPTablesList.m
index 2fa26229..c75198b7 100644
--- a/Source/SPTablesList.m
+++ b/Source/SPTablesList.m
@@ -232,7 +232,8 @@ static NSString *SPDuplicateTable = @"SPDuplicateTable";
NSString *pQuery = [NSString stringWithFormat:@"SELECT * FROM information_schema.routines WHERE routine_schema = %@ ORDER BY routine_name", [[tableDocumentInstance database] tickQuotedString]];
theResult = [mySQLConnection queryString:pQuery];
[theResult setDefaultRowReturnType:SPMySQLResultRowAsArray];
-
+ [theResult setReturnDataAsStrings:YES]; //see tables above
+
// Check for mysql errors - if information_schema is not accessible for some reasons
// omit adding procedures and functions
if(![mySQLConnection queryErrored] && theResult != nil && [theResult numberOfRows] && [theResult numberOfFields] > 3) {
@@ -1370,6 +1371,8 @@ static NSString *SPDuplicateTable = @"SPDuplicateTable";
/**
* Select an item using the provided name; returns YES if the
* supplied name could be selected, or NO if not.
+ *
+ * MUST BE CALLED ON THE UI THREAD!
*/
- (BOOL)selectItemWithName:(NSString *)theName
{
@@ -1418,7 +1421,7 @@ static NSString *SPDuplicateTable = @"SPDuplicateTable";
}
}
- [[tablesListView onMainThread] scrollRowToVisible:[tablesListView selectedRow]];
+ [tablesListView scrollRowToVisible:[tablesListView selectedRow]];
#endif
return YES;
diff --git a/Source/SPTooltip.m b/Source/SPTooltip.m
index 54a997f6..ff68fdbe 100644
--- a/Source/SPTooltip.m
+++ b/Source/SPTooltip.m
@@ -83,12 +83,6 @@ static CGFloat slow_in_out (CGFloat t)
@end
-@interface WebView (LeopardOnly)
-
-- (void)setDrawsBackground:(BOOL)drawsBackground;
-
-@end
-
@implementation SPTooltip
// ==================
diff --git a/Source/SPWindow.m b/Source/SPWindow.m
index 383b7fcd..18c4fae3 100644
--- a/Source/SPWindow.m
+++ b/Source/SPWindow.m
@@ -31,12 +31,6 @@
#import "SPWindow.h"
#import "SPWindowController.h"
-@interface NSWindow (LionPlusMethods)
-
-- (void)toggleFullScreen:(id)sender;
-
-@end
-
@implementation SPWindow
@synthesize isSheetWhichCanBecomeMain;
@@ -45,12 +39,10 @@
+ (void)initialize
{
-#if __MAC_OS_X_VERSION_MAX_ALLOWED >= __MAC_10_12
// Disable automatic window tabbing on 10.12+
if ([NSWindow respondsToSelector:@selector(setAllowsAutomaticWindowTabbing:)]) {
[NSWindow setAllowsAutomaticWindowTabbing:NO];
}
-#endif
}
#pragma mark -
diff --git a/Source/SPWindowController.m b/Source/SPWindowController.m
index cddf6b09..9755cd0d 100644
--- a/Source/SPWindowController.m
+++ b/Source/SPWindowController.m
@@ -39,15 +39,6 @@
#import <PSMTabBar/PSMTabBarControl.h>
#import <PSMTabBar/PSMTabStyle.h>
-// Forward-declare for 10.7 compatibility
-#if !defined(MAC_OS_X_VERSION_10_7) || MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_7
-enum {
- NSWindowCollectionBehaviorFullScreenPrimary = 1 << 7,
- NSWindowCollectionBehaviorFullScreenAuxiliary = 1 << 8,
- NSFullScreenWindowMask = 1 << 14
-};
-#endif
-
@interface SPWindowController ()
- (void)_setUpTabBar;
@@ -169,15 +160,14 @@ enum {
*/
- (IBAction)closeTab:(id)sender
{
- // Return if the selected tab shouldn't be closed
- if (![selectedTableDocument parentTabShouldClose]) return;
-
// If there are multiple tabs, close the front tab.
if ([tabView numberOfTabViewItems] > 1) {
+ // Return if the selected tab shouldn't be closed
+ if (![selectedTableDocument parentTabShouldClose]) return;
[tabView removeTabViewItem:[tabView selectedTabViewItem]];
-
}
else {
+ //trying to close the window will itself call parentTabShouldClose for all tabs in windowShouldClose:
[[self window] performClose:self];
}
}
diff --git a/Source/SPWindowControllerDelegate.m b/Source/SPWindowControllerDelegate.m
index 79b1e2f1..009dc0a4 100644
--- a/Source/SPWindowControllerDelegate.m
+++ b/Source/SPWindowControllerDelegate.m
@@ -56,15 +56,11 @@
*/
- (BOOL)windowShouldClose:(id)sender
{
- // Iterate through all tabs if more than one tab is opened only otherwise
- // [... parentTabShouldClose] will be called twice [see self closeTab:(id)sender]
- if ([[tabView tabViewItems] count] > 1) {
- for (NSTabViewItem *eachItem in [tabView tabViewItems])
- {
- SPDatabaseDocument *eachDocument = [eachItem identifier];
-
- if (![eachDocument parentTabShouldClose]) return NO;
- }
+ for (NSTabViewItem *eachItem in [tabView tabViewItems])
+ {
+ SPDatabaseDocument *eachDocument = [eachItem identifier];
+
+ if (![eachDocument parentTabShouldClose]) return NO;
}
// Remove global session data if the last window of a session will be closed
@@ -190,6 +186,8 @@
/**
* Called to determine whether a tab view item can be closed
+ *
+ * Note: This is ONLY called when using the "X" button on the tab itself.
*/
- (BOOL)tabView:(NSTabView *)aTabView shouldCloseTabViewItem:(NSTabViewItem *)tabViewItem
{
diff --git a/Source/Sequel-Pro.pch b/Source/Sequel-Pro.pch
index f3b2fd16..dc6bbf9b 100644
--- a/Source/Sequel-Pro.pch
+++ b/Source/Sequel-Pro.pch
@@ -32,7 +32,9 @@
// Nearly every class uses constants
#import "SPConstants.h"
-
+
+ #import "SPCompatibility.h"
+
// Make all our custom additions available
#import "SPCategoryAdditions.h"
#endif