Tk Source Code

View Ticket
Login
Ticket UUID: 46827143b4df57e496893fd68c862174a34bde0
Title: Allow picking all file types on OSX by having panel accessory view option
Type: Patch Version: 8.6.3rc
Submitter: brianhds Created on: 2014-11-07 20:55:55
Subsystem: 37. [tk_get*File] Assigned To: kevin_walzer
Priority: 5 Medium Severity: Important
Status: Open Last Modified: 2014-12-01 02:56:11
Resolution: None Closed By: nobody
    Closed on:
Description:
On OSX, the public.data UTI is not mapped to a a particular filetype but is needed to let users select 'All Files' like they can on other platforms.

Using a checkbox is not done as a "lazy solution" instead of implementation the same functionality as other platforms--OSX has diverged significantly from other platforms in terms on file extension usage and association (see: http://en.wikipedia.org/wiki/Uniform_Type_Identifier) and a mapping from file extension to UTI is no longer deterministic--even for All Files (*.*).  A truly complete solution would be to replace or add to Tk's MacType with UTI's but would require Tk applications to be (re)written extensively for use on OSX.

There is a current feature request for something similar at: http://sourceforge.net/p/tktoolkit/feature-requests/248/

The following patch allows adding an -allbutton option to the arguments for tk_getOpenFile.  The value given to -allbutton should be the text to display for a checkbox to allow selecting any (public.data) file.  A second option, -allbuttontip, allows specifying the tooltip text for the checkbox.

Index: macosx/tkMacOSXDialog.c
==================================================================
--- macosx/tkMacOSXDialog.c
+++ macosx/tkMacOSXDialog.c
@@ -22,16 +22,16 @@
 };
 
 static const char *const openOptionStrings[] = {
     "-defaultextension", "-filetypes", "-initialdir", "-initialfile",
     "-message", "-multiple", "-parent", "-title", "-typevariable",
-    "-command", NULL
+    "-command", "-allbutton", "-allbuttontip", NULL
 };
 enum openOptions {
     OPEN_DEFAULT, OPEN_FILETYPES, OPEN_INITDIR, OPEN_INITFILE,
     OPEN_MESSAGE, OPEN_MULTIPLE, OPEN_PARENT, OPEN_TITLE,
-    OPEN_TYPEVARIABLE, OPEN_COMMAND,
+    OPEN_TYPEVARIABLE, OPEN_COMMAND, OPEN_ALLBUTTON, OPEN_ALLBUTTONTIP
 };
 static const char *const saveOptionStrings[] = {
     "-defaultextension", "-filetypes", "-initialdir", "-initialfile",
     "-message", "-parent", "-title", "-typevariable", "-command",
     "-confirmoverwrite", NULL
@@ -218,10 +218,39 @@
 	Tcl_DecrRefCount(callbackInfo->cmdObj);
 	ckfree(callbackInfo);
     }
 }
 @end
+
+@interface TKOpenDialog : NSControl 
+@property(assign) NSOpenPanel *openPanel;
+@property(assign) NSMutableArray *openFileTypes;
+- (void)openUnrecognizedFiles:(id)sender;
+@end
+
+@implementation TKOpenDialog
+- (id)init {
+    self = [super init];
+    if (self) {
+	_openPanel = nil;
+	_openFileTypes = nil;
+    }
+    return self;
+}
+
+- (void)openUnrecognizedFiles:(id)sender
+{
+    if ([sender state]) {
+	self.openPanel.allowedFileTypes = [NSArray arrayWithObjects:@"public.data", nil];
+    } else {
+	self.openPanel.allowedFileTypes = self.openFileTypes;
+    }
+    [self.openPanel setDirectoryURL:self.openPanel.directoryURL];
+    [self.openPanel validateVisibleColumns];
+}
+
+@end
 
 #pragma mark -
 
 /*
  *----------------------------------------------------------------------
@@ -364,15 +393,18 @@
     Tcl_Obj *cmdObj = NULL, *typeVariablePtr = NULL;
     FilePanelCallbackInfo callbackInfoStruct;
     FilePanelCallbackInfo *callbackInfo = &callbackInfoStruct;
     NSString *directory = nil, *filename = nil;
     NSString *message, *title, *type;
+    NSString *allbutton, *allbuttontip;
     NSWindow *parent;
     NSMutableArray *fileTypes = nil;
     NSOpenPanel *panel = [NSOpenPanel openPanel];
     NSInteger returnCode = NSAlertErrorReturn;
 
+    allbutton = nil;
+    allbuttontip = nil;
     TkInitFileFilters(&fl);
     for (i = 1; i < objc; i += 2) {
 	if (Tcl_GetIndexFromObjStruct(interp, objv[i], openOptionStrings,
 		sizeof(char *), "option", TCL_EXACT, &index) != TCL_OK) {
 	    goto end;
@@ -435,10 +467,18 @@
 	    typeVariablePtr = objv[i + 1];
 	    break;
 	case OPEN_COMMAND:
 	    cmdObj = objv[i+1];
 	    break;
+	case OPEN_ALLBUTTON:
+	    allbutton = [[NSString alloc] initWithUTF8String:
+		    Tcl_GetString(objv[i + 1])];
+	    break;
+	case OPEN_ALLBUTTONTIP:
+	    allbuttontip = [[NSString alloc] initWithUTF8String:
+		    Tcl_GetString(objv[i + 1])];
+	    break;
 	}
     }
     if (fl.filters) {
 	fileTypes = [NSMutableArray array];
 	for (FileFilter *filterPtr = fl.filters; filterPtr;
@@ -470,10 +510,28 @@
 		}
 	    }
 	}
     }
     [panel setAllowsMultipleSelection:multiple];
+
+    if (allbutton) {
+	TKOpenDialog *tkDialog = [TKOpenDialog alloc];
+	tkDialog.openPanel = panel;
+	tkDialog.openFileTypes = fileTypes;
+	
+	NSButton *openPanelAccessoryView = [[[NSButton alloc] initWithFrame:NSMakeRect(0.0, 0.0, 224.0, 22.0)] autorelease];
+	if (allbuttontip) {
+	    [openPanelAccessoryView setToolTip:allbuttontip];
+	}
+	[openPanelAccessoryView setButtonType:NSSwitchButton];
+	[openPanelAccessoryView setBezelStyle:0];
+	[openPanelAccessoryView setTitle:allbutton];
+	[openPanelAccessoryView setAction:@selector(openUnrecognizedFiles:)];
+	[openPanelAccessoryView setTarget:tkDialog];
+	[panel setAccessoryView:openPanelAccessoryView];
+    }
+
     if (cmdObj) {
 	callbackInfo = ckalloc(sizeof(FilePanelCallbackInfo));
 	if (Tcl_IsShared(cmdObj)) {
 	    cmdObj = Tcl_DuplicateObj(cmdObj);
 	}
User Comments: kevin_walzer added on 2014-12-01 02:56:11:
Does this second patch allow a user to select an arbitrary file without the file having a formal UTI under "public.*" specified? i.e., no special monkeying with the file type, it simply exposes any file to the selection dialog if the Tcl/Tk app passes "all files" wildcard? If the dialog should support this per the docs and currently doesn't, then this would fall under a bug fix, and certainly wouldn't need work in a development branch, a TIP, or whatnot. 

I haven't tested the new patch, but if you confirm my understanding, I will review it soon. Thanks.

brianhds added on 2014-11-11 15:41:58:
Besides the fact that the OSX version of tk_getOpenFile currently does nothing at all with the All Files *.* wildcard specification except ignore it, the more fundamental problem is that OSX no longer has a deterministic mapping from file extensions to the ability to filter files for selection.  An application on OSX can create a UTI of com.foo.bar.txt which is under the UTI hierarchy public.data -> public.text -> com.foo -> com.foo.bar -> com.foo.bar.txt and is not selectable unless public.data or public.text or one of the com.foo* variants is specified as the UTI.  Note that Tcl/Tk applications would have to specify that the file extension was '*.public.data', '.public.data' or '*.public.text', '.public.text' or explicitly specify one or all of the com.foo variants for this selection to be allowed with the current Tk code--this definitely makes Tcl/Tk programming for OSX very different from other platforms.

An alternative patch, which does not have any different options added to tk_getOpenFiles, just allows the user to have the option of picking any file if an all files wildcard is specified in the filetypes list is:

--- tkMacOSXDialog.c
+++ tkMacOSXDialog.c
@@ -218,10 +218,39 @@
 	Tcl_DecrRefCount(callbackInfo->cmdObj);
 	ckfree(callbackInfo);
     }
 }
 @end
+
+@interface TKOpenDialog : NSControl 
+@property(assign) NSOpenPanel *openPanel;
+@property(assign) NSMutableArray *openFileTypes;
+- (void)openUnrecognizedFiles:(id)sender;
+@end
+
+@implementation TKOpenDialog
+- (id)init {
+    self = [super init];
+    if (self) {
+	_openPanel = nil;
+	_openFileTypes = nil;
+    }
+    return self;
+}
+
+- (void)openUnrecognizedFiles:(id)sender
+{
+    if ([sender state]) {
+	self.openPanel.allowedFileTypes = [NSArray arrayWithObjects:@"public.data", nil];
+    } else {
+	self.openPanel.allowedFileTypes = self.openFileTypes;
+    }
+    [self.openPanel setDirectoryURL:self.openPanel.directoryURL];
+    [self.openPanel validateVisibleColumns];
+}
+
+@end
 
 #pragma mark -
 
 /*
  *----------------------------------------------------------------------
@@ -364,15 +393,18 @@
     Tcl_Obj *cmdObj = NULL, *typeVariablePtr = NULL;
     FilePanelCallbackInfo callbackInfoStruct;
     FilePanelCallbackInfo *callbackInfo = &callbackInfoStruct;
     NSString *directory = nil, *filename = nil;
     NSString *message, *title, *type;
+    NSString *allbutton, *allbuttontip;
     NSWindow *parent;
     NSMutableArray *fileTypes = nil;
     NSOpenPanel *panel = [NSOpenPanel openPanel];
     NSInteger returnCode = NSAlertErrorReturn;
 
+    allbutton = nil;
+    allbuttontip = nil;
     TkInitFileFilters(&fl);
     for (i = 1; i < objc; i += 2) {
 	if (Tcl_GetIndexFromObjStruct(interp, objv[i], openOptionStrings,
 		sizeof(char *), "option", TCL_EXACT, &index) != TCL_OK) {
 	    goto end;
@@ -455,10 +487,14 @@
 			type = [[NSString alloc] initWithUTF8String:str];
 			if (![fileTypes containsObject:type]) {
 			    [fileTypes addObject:type];
 			}
 			[type release];
+		    } else {
+			/* If empty, then it must have been All Files: *.* */
+			allbutton = @"Select any file";
+			allbuttontip = @"Allow any type of file to be selected--files may remain greyed out until the directory is changed";
 		    }
 		}
 		for (MacFileType *mfPtr = clausePtr->macTypes; mfPtr;
 			mfPtr = mfPtr->next) {
 		    if (mfPtr->type) {
@@ -470,10 +506,28 @@
 		}
 	    }
 	}
     }
     [panel setAllowsMultipleSelection:multiple];
+
+    if (allbutton) {
+	TKOpenDialog *tkDialog = [TKOpenDialog alloc];
+	tkDialog.openPanel = panel;
+	tkDialog.openFileTypes = fileTypes;
+	
+	NSButton *openPanelAccessoryView = [[[NSButton alloc] initWithFrame:NSMakeRect(0.0, 0.0, 224.0, 22.0)] autorelease];
+	if (allbuttontip) {
+	    [openPanelAccessoryView setToolTip:allbuttontip];
+	}
+	[openPanelAccessoryView setButtonType:NSSwitchButton];
+	[openPanelAccessoryView setBezelStyle:0];
+	[openPanelAccessoryView setTitle:allbutton];
+	[openPanelAccessoryView setAction:@selector(openUnrecognizedFiles:)];
+	[openPanelAccessoryView setTarget:tkDialog];
+	[panel setAccessoryView:openPanelAccessoryView];
+    }
+
     if (cmdObj) {
 	callbackInfo = ckalloc(sizeof(FilePanelCallbackInfo));
 	if (Tcl_IsShared(cmdObj)) {
 	    cmdObj = Tcl_DuplicateObj(cmdObj);
 	}

kevin_walzer added on 2014-11-11 00:41:50:
Brian,

When you add a comment, please do so in the "append remark" field rather than editing your original comment. You appear to have done so to disagree with dkf's characterization of your patch. 

The difficulty I'm having is that I don't quite understand how this is handled on other platforms. I see the "filter" in the pure-Tk dialog (though I forget the command to produce it). How is this handled on Windows? 

I am not opposed to having a Mac-specific solution, especially if Windows does its own thing using native API's. Sometimes that's the right call, and sometimes it can't be avoided. But I think preserving a cross-platform API is the best way to proceed if possible. 

--Kevin

dkf added on 2014-11-08 23:17:52:

It ought to be committed to a development branch. Then we can hack it further from there.


anonymous (claiming to be auriocus) added on 2014-11-08 20:10:11:
It works as advertised, but it is the wrong solution (or too lazy solution). On other platforms, the accesoryview is a menubutton which allows to select the filter from the patterns that are provided in -filetypes; if one of the filters is *.*, the user can select any file. Furthermore, the -typevariable option declares a variable which is set to the selection of that menubutton (and vice versa on start of the dialog box). Such a menubutton is also used in tk_getSaveFile. Even Apples programs (like Pages) use this in the save dialog. No new options are needed, if this will be implemented in a compatible way to the other platforms. Unfortunately, I do not know enough of Cocoa to do it myself. This patch still goes some way in the right direction to demonstrate, how the accessoryview can be done and the filter be changed, but the bug should be fixed by implementing the same solution as on the other platforms. Anoher odd thing is the absolute pixel size for the accessoryview.

kevin_walzer added on 2014-11-08 19:09:47:
Wondering, however, if it needs a TIP since it introduces extra parameters to the tk_getOpenFile command on the Mac that do not appear to exist on other platforms.

kevin_walzer added on 2014-11-08 03:41:07:
This seems to work well and is a nice addition to the toolkit.