JBR-7194: extension-based filters in native file dialogs

(cherry picked from commit cf5d136b3e)
This commit is contained in:
Roman Shevchenko
2024-05-30 18:28:14 +02:00
committed by jbrbot
parent 8f5d7a9c6a
commit e03dda3e23
9 changed files with 197 additions and 68 deletions

View File

@@ -443,6 +443,7 @@ ifeq ($(call isTargetOs, macosx), true)
-framework Metal \
-framework OpenGL \
-framework QuartzCore \
-framework UniformTypeIdentifiers \
-framework Security, \
STATIC_LIB_EXCLUDE_OBJS := $(LIBAWT_LWAWT_STATIC_EXCLUDE_OBJS), \
))

View File

@@ -105,6 +105,7 @@ final class CFileDialog implements FileDialogPeer {
chooseFiles,
createDirectories,
target.getFilenameFilter() != null,
jbrDialog.fileFilterExtensions,
target.getDirectory(),
target.getFile());
@@ -198,7 +199,7 @@ final class CFileDialog implements FileDialogPeer {
private native String[] nativeRunFileDialog(long ownerPtr, String title, int mode,
boolean multipleMode, boolean shouldNavigateApps,
boolean canChooseDirectories, boolean canChooseFiles,
boolean canCreateDirectories, boolean hasFilenameFilter,
boolean canCreateDirectories, boolean hasFilenameFilter, String[] allowedFileTypes,
String directory, String file);
@Override

View File

@@ -31,6 +31,9 @@
// Should we query back to Java for a file filter?
jboolean fHasFileFilter;
// Allowed file types
NSArray *fFileTypes;
// sun.awt.CFileDialog
jobject fFileDialog;
@@ -68,7 +71,8 @@
// Allocator
- (id) initWithOwner:(NSWindow*) owner
filter:(jboolean)inHasFilter
filter:(jboolean)inHasFilter
fileTypes:(NSArray *)inFileTypes
fileDialog:(jobject)inDialog
title:(NSString *)inTitle
directory:(NSString *)inPath

View File

@@ -25,6 +25,7 @@
#import <sys/stat.h>
#import <Cocoa/Cocoa.h>
#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
#import "ThreadUtilities.h"
#import "JNIUtilities.h"
@@ -40,6 +41,7 @@
- (id)initWithOwner:(NSWindow*)owner
filter:(jboolean)inHasFilter
fileTypes:(NSArray *)inFileTypes
fileDialog:(jobject)inDialog
title:(NSString *)inTitle
directory:(NSString *)inPath
@@ -56,6 +58,8 @@ canCreateDirectories:(BOOL)inCreateDirectories
fOwner = owner;
[fOwner retain];
fHasFileFilter = inHasFilter;
fFileTypes = inFileTypes;
[fFileTypes retain];
fFileDialog = (*env)->NewGlobalRef(env, inDialog);
fDirectory = inPath;
[fDirectory retain];
@@ -84,6 +88,9 @@ canCreateDirectories:(BOOL)inCreateDirectories
}
-(void) dealloc {
[fFileTypes release];
fFileTypes = nil;
[fDirectory release];
fDirectory = nil;
@@ -123,6 +130,23 @@ canCreateDirectories:(BOOL)inCreateDirectories
if (thePanel != nil) {
[thePanel setTitle:fTitle];
if (fFileTypes != nil) {
if (@available(macOS 11, *)) {
int nTypes = (int)[fFileTypes count];
NSMutableArray *contentTypes = [NSMutableArray arrayWithCapacity:nTypes];
for (int i = 0; i < nTypes; ++i) {
NSString *fileType = (NSString *)[fFileTypes objectAtIndex:i];
UTType *contentType = [UTType typeWithFilenameExtension:fileType conformingToType:UTTypeData];
if (contentType != nil) {
[contentTypes addObject:contentType];
}
}
[thePanel setAllowedContentTypes:contentTypes];
} else {
[thePanel setAllowedFileTypes:fFileTypes];
}
}
if (fNavigateApps) {
[thePanel setTreatsFilePackagesAsDirectories:YES];
}
@@ -304,7 +328,7 @@ JNIEXPORT jobjectArray JNICALL
Java_sun_lwawt_macosx_CFileDialog_nativeRunFileDialog
(JNIEnv *env, jobject peer, jlong ownerPtr, jstring title, jint mode, jboolean multipleMode,
jboolean navigateApps, jboolean chooseDirectories, jboolean chooseFiles, jboolean createDirectories,
jboolean hasFilter, jstring directory, jstring file)
jboolean hasFilter, jobjectArray allowedFileTypes, jstring directory, jstring file)
{
jobjectArray returnValue = NULL;
@@ -314,8 +338,22 @@ JNI_COCOA_ENTER(env);
dialogTitle = @" ";
}
NSMutableArray *fileTypes = nil;
if (allowedFileTypes != nil) {
int nTypes = (*env)->GetArrayLength(env, allowedFileTypes);
if (nTypes > 0) {
fileTypes = [NSMutableArray arrayWithCapacity:nTypes];
for (int i = 0; i < nTypes; i++) {
jstring fileType = (jstring)(*env)->GetObjectArrayElement(env, allowedFileTypes, i);
[fileTypes addObject:JavaStringToNSString(env, fileType)];
(*env)->DeleteLocalRef(env, fileType);
}
}
}
CFileDialog *dialogDelegate = [[CFileDialog alloc] initWithOwner:(NSWindow *)jlong_to_ptr(ownerPtr)
filter:hasFilter
fileTypes:fileTypes
fileDialog:peer
title:dialogTitle
directory:JavaStringToNSString(env, directory)

View File

@@ -6,6 +6,7 @@ import java.lang.annotation.Native;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
import java.awt.*;
import java.util.Objects;
public class JBRFileDialog implements Serializable {
@@ -25,28 +26,19 @@ public class JBRFileDialog implements Serializable {
return (JBRFileDialog) getter.get(dialog);
}
/**
* Whether to select files, directories or both (used when common file dialogs are enabled on Windows, or on macOS)
*/
@Native public static final int SELECT_FILES_HINT = 1, SELECT_DIRECTORIES_HINT = 2;
/**
* Whether to allow creating directories or not (used on macOS)
*/
@Native public static final int CREATE_DIRECTORIES_HINT = 4;
public static final String OPEN_FILE_BUTTON_KEY = "jbrFileDialogOpenFile";
public static final String OPEN_DIRECTORY_BUTTON_KEY = "jbrFileDialogSelectDir";
public static final String ALL_FILES_COMBO_KEY = "jbrFileDialogAllFiles";
public int hints = CREATE_DIRECTORIES_HINT;
/**
* Text for "Open" button (used when common file dialogs are enabled on
* Windows).
*/
public String openButtonText;
/**
* Text for "Select Folder" button (used when common file dialogs are
* enabled on Windows).
*/
public String selectFolderButtonText;
public String allFilesFilterDescription;
public String fileFilterDescription;
public String[] fileFilterExtensions;
public void setHints(int hints) {
this.hints = hints;
@@ -55,9 +47,24 @@ public class JBRFileDialog implements Serializable {
return hints;
}
public void setLocalizationStrings(String openButtonText, String selectFolderButtonText) {
this.openButtonText = openButtonText;
this.selectFolderButtonText = selectFolderButtonText;
public void setLocalizationString(String key, String text) {
Objects.requireNonNull(key);
switch (key) {
case OPEN_FILE_BUTTON_KEY -> openButtonText = text;
case OPEN_DIRECTORY_BUTTON_KEY -> selectFolderButtonText = text;
case ALL_FILES_COMBO_KEY -> allFilesFilterDescription = text;
default -> throw new IllegalArgumentException("unrecognized key: " + key);
}
}
@Deprecated(forRemoval = true)
public void setLocalizationStrings(String openButtonText, String selectFolderButtonText) {
setLocalizationString(OPEN_FILE_BUTTON_KEY, openButtonText);
setLocalizationString(OPEN_DIRECTORY_BUTTON_KEY, selectFolderButtonText);
}
public void setFileFilterExtensions(String fileFilterDescription, String[] fileFilterExtensions) {
this.fileFilterDescription = fileFilterDescription;
this.fileFilterExtensions = fileFilterExtensions;
}
}

View File

@@ -57,6 +57,9 @@ jfieldID AwtFileDialog::filterID;
jfieldID AwtFileDialog::jbrDialogID;
jfieldID AwtFileDialog::openButtonTextID;
jfieldID AwtFileDialog::selectFolderButtonTextID;
jfieldID AwtFileDialog::allFilesFilterDescriptionID;
jfieldID AwtFileDialog::fileFilterDescriptionID;
jfieldID AwtFileDialog::fileFilterExtensionsID;
jfieldID AwtFileDialog::hintsID;
class CoTaskStringHolder {
@@ -156,8 +159,6 @@ class SmartHolder<T[]> : public SmartHolderBase<T> {
static TCHAR s_fileFilterString[MAX_FILTER_STRING];
/* Non-localized suffix of the filter string */
static const TCHAR s_additionalString[] = TEXT(" (*.*)\0*.*\0");
static SmartHolder<COMDLG_FILTERSPEC> s_fileFilterSpec;
static UINT s_fileFilterCount;
// Default limit of the output buffer.
#define SINGLE_MODE_BUFFER_LIMIT MAX_PATH+1
@@ -175,36 +176,6 @@ _COM_SMARTPTR_TYPEDEF(IOleWindowPtr, __uuidof(IOleWindowPtr));
/***********************************************************************/
COMDLG_FILTERSPEC *CreateFilterSpec(UINT *count) {
UINT filterCount = 0;
for (UINT index = 0; index < MAX_FILTER_STRING - 1; index++) {
if (s_fileFilterString[index] == _T('\0')) {
filterCount++;
if (s_fileFilterString[index + 1] == _T('\0'))
break;
}
}
filterCount /= 2;
COMDLG_FILTERSPEC *filterSpec = new COMDLG_FILTERSPEC[filterCount];
UINT currentIndex = 0;
TCHAR *currentStart = s_fileFilterString;
for (UINT index = 0; index < MAX_FILTER_STRING - 1; index++) {
if (s_fileFilterString[index] == _T('\0')) {
if (currentIndex & 1) {
filterSpec[currentIndex / 2].pszSpec = currentStart;
} else {
filterSpec[currentIndex / 2].pszName = currentStart;
}
currentStart = s_fileFilterString + index + 1;
currentIndex++;
if (s_fileFilterString[index + 1] == _T('\0'))
break;
}
}
*count = filterCount;
return filterSpec;
}
void
AwtFileDialog::Initialize(JNIEnv *env, jstring filterDescription)
{
@@ -224,7 +195,6 @@ AwtFileDialog::Initialize(JNIEnv *env, jstring filterDescription)
}
DASSERT(s + sizeof(s_additionalString) < s_fileFilterString + MAX_FILTER_STRING);
memcpy(s, s_additionalString, sizeof(s_additionalString));
s_fileFilterSpec.Attach(CreateFilterSpec(&s_fileFilterCount));
}
LRESULT CALLBACK FileDialogWndProc(HWND hWnd, UINT message,
@@ -789,6 +759,9 @@ AwtFileDialog::Show(void *p)
jobject parent = NULL;
AwtComponent* awtParent = NULL;
jboolean multipleMode = JNI_FALSE;
wchar_t *allFilesFilterDescrBuf = NULL;
wchar_t *fileFilterDescrBuf = NULL;
wchar_t *fileFilterSpecBuf = NULL;
OLE_DECL
OLEHolder _ole_;
@@ -941,8 +914,67 @@ AwtFileDialog::Show(void *p)
OLE_HRT(pfd->SetTitle(titleBuffer));
if (!folderPickerMode) {
OLE_HRT(pfd->SetFileTypes(s_fileFilterCount, s_fileFilterSpec));
auto allFilesDescrStr = reinterpret_cast<jstring>(env->GetObjectField(jbrDialog, allFilesFilterDescriptionID));
if (allFilesDescrStr != nullptr) {
int descriptionLen = env->GetStringLength(allFilesDescrStr);
allFilesFilterDescrBuf = new wchar_t[descriptionLen + 1];
auto descriptionChars = JNU_GetStringPlatformChars(env, allFilesDescrStr, nullptr);
wcsncpy(allFilesFilterDescrBuf, descriptionChars, descriptionLen);
JNU_ReleaseStringPlatformChars(env, allFilesDescrStr, descriptionChars);
allFilesFilterDescrBuf[descriptionLen] = L'\0';
}
auto allFilesFilterDescrPtr = allFilesFilterDescrBuf != nullptr ? allFilesFilterDescrBuf : L"All Files";
auto descriptionStr = reinterpret_cast<jstring>(env->GetObjectField(jbrDialog, fileFilterDescriptionID));
auto extensionArray = reinterpret_cast<jobjectArray>(env->GetObjectField(jbrDialog, fileFilterExtensionsID));
if (descriptionStr != nullptr && extensionArray != nullptr) {
int extensionArrayLen = env->GetArrayLength(extensionArray);
int filterSpecLen = extensionArrayLen * 3;
for (int i = 0; i < extensionArrayLen; ++i) {
auto extensionStr = reinterpret_cast<jstring>(env->GetObjectArrayElement(extensionArray, i));
filterSpecLen += env->GetStringLength(extensionStr);
env->DeleteLocalRef(extensionStr);
}
fileFilterSpecBuf = new wchar_t[filterSpecLen];
for (int i = 0, fsp = 0; i < extensionArrayLen; ++i) {
if (i > 0) {
fileFilterSpecBuf[fsp++] = L';';
}
fileFilterSpecBuf[fsp++] = L'*';
fileFilterSpecBuf[fsp++] = L'.';
auto extensionStr = reinterpret_cast<jstring>(env->GetObjectArrayElement(extensionArray, i));
auto extensionChars = JNU_GetStringPlatformChars(env, extensionStr, nullptr);
int extensionStrLen = env->GetStringLength(extensionStr);
wcsncpy(fileFilterSpecBuf + fsp, extensionChars, extensionStrLen);
fsp += extensionStrLen;
JNU_ReleaseStringPlatformChars(env, extensionStr, extensionChars);
env->DeleteLocalRef(extensionStr);
}
fileFilterSpecBuf[filterSpecLen - 1] = L'\0';
int descriptionLen = env->GetStringLength(descriptionStr);
fileFilterDescrBuf = new wchar_t[descriptionLen + 1];
auto descriptionChars = JNU_GetStringPlatformChars(env, descriptionStr, nullptr);
wcsncpy(fileFilterDescrBuf, descriptionChars, descriptionLen);
JNU_ReleaseStringPlatformChars(env, descriptionStr, descriptionChars);
fileFilterDescrBuf[descriptionLen] = L'\0';
COMDLG_FILTERSPEC rgSpec[] = {
{fileFilterDescrBuf, fileFilterSpecBuf},
{allFilesFilterDescrPtr, L"*.*"}
};
OLE_HRT(pfd->SetFileTypes(2, rgSpec));
} else {
COMDLG_FILTERSPEC rgSpec[] = {
{allFilesFilterDescrPtr, L"*.*"}
};
OLE_HRT(pfd->SetFileTypes(1, rgSpec));
}
OLE_HRT(pfd->SetFileTypeIndex(1));
env->DeleteLocalRef(allFilesDescrStr);
env->DeleteLocalRef(descriptionStr);
env->DeleteLocalRef(extensionArray);
}
{
@@ -1046,6 +1078,9 @@ AwtFileDialog::Show(void *p)
}
DASSERT(!safe_ExceptionOccurred(env));
} catch (...) {
delete[] allFilesFilterDescrBuf;
delete[] fileFilterDescrBuf;
delete[] fileFilterSpecBuf;
if (useCommonItemDialog) {
if (pfd && dwCookie != OLE_BAD_COOKIE) {
@@ -1229,6 +1264,21 @@ Java_sun_awt_windows_WFileDialogPeer_initIDs(JNIEnv *env, jclass cls)
DASSERT(AwtFileDialog::selectFolderButtonTextID != NULL);
CHECK_NULL(AwtFileDialog::selectFolderButtonTextID);
AwtFileDialog::allFilesFilterDescriptionID =
env->GetFieldID(cls, "allFilesFilterDescription", "Ljava/lang/String;");
DASSERT(AwtFileDialog::allFilesFilterDescriptionID != NULL);
CHECK_NULL(AwtFileDialog::allFilesFilterDescriptionID);
AwtFileDialog::fileFilterDescriptionID =
env->GetFieldID(cls, "fileFilterDescription", "Ljava/lang/String;");
DASSERT(AwtFileDialog::fileFilterDescriptionID != NULL);
CHECK_NULL(AwtFileDialog::fileFilterDescriptionID);
AwtFileDialog::fileFilterExtensionsID =
env->GetFieldID(cls, "fileFilterExtensions", "[Ljava/lang/String;");
DASSERT(AwtFileDialog::fileFilterExtensionsID != NULL);
CHECK_NULL(AwtFileDialog::fileFilterExtensionsID);
AwtFileDialog::hintsID = env->GetFieldID(cls, "hints", "I");
DASSERT(AwtFileDialog::hintsID != NULL);
CHECK_NULL(AwtFileDialog::hintsID);

View File

@@ -61,6 +61,9 @@ public:
/* com.jetbrains.desktop.JBRFileDialog field and method ids */
static jfieldID openButtonTextID;
static jfieldID selectFolderButtonTextID;
static jfieldID allFilesFilterDescriptionID;
static jfieldID fileFilterDescriptionID;
static jfieldID fileFilterExtensionsID;
static jfieldID hintsID;
static void Initialize(JNIEnv *env, jstring filterDescription);

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2000-2023 JetBrains s.r.o.
* Copyright 2000-2024 JetBrains s.r.o.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -27,6 +27,11 @@ package com.jetbrains;
import java.awt.*;
/**
* Extensions to the AWT {@code FileDialog} that allow clients fully use a native file chooser
* on supported platforms (currently macOS and Windows; the latter requires setting
* {@code sun.awt.windows.useCommonItemDialog} property to {@code true}).
*/
public interface JBRFileDialog {
/*CONST com.jetbrains.desktop.JBRFileDialog.*_HINT*/
@@ -37,12 +42,11 @@ public interface JBRFileDialog {
}
/**
* Set JBR-specific file dialog hints:
* Set file dialog hints:
* <ul>
* <li>SELECT_FILES_HINT, SELECT_DIRECTORIES_HINT - Whether to select files, directories or both
* (used when common file dialogs are enabled on Windows, or on macOS),
* both unset bits are treated as a default platform-specific behavior</li>
* <li>CREATE_DIRECTORIES_HINT - Whether to allow creating directories or not (used on macOS)</li>
* <li>SELECT_FILES_HINT, SELECT_DIRECTORIES_HINT - whether to select files, directories, or both;
* if neither of the two is set, the behavior is platform-specific</li>
* <li>CREATE_DIRECTORIES_HINT - whether to allow creating directories or not (macOS)</li>
* </ul>
*/
void setHints(int hints);
@@ -52,7 +56,28 @@ public interface JBRFileDialog {
*/
int getHints();
/*CONST com.jetbrains.desktop.JBRFileDialog.*_KEY*/
/**
* Change text of UI elements (Windows).
* Supported keys:
* <ul>
* <li>OPEN_FILE_BUTTON_KEY - "open" button when a file is selected in the list</li>
* <li>OPEN_DIRECTORY_BUTTON_KEY - "open" button when a directory is selected in the list</li>
* <li>ALL_FILES_COMBO_KEY - "all files" item in the file filter combo box</li>
* </ul>
*/
void setLocalizationString(String key, String text);
/** @deprecated use {@link #setLocalizationString} */
@Deprecated(forRemoval = true)
void setLocalizationStrings(String openButtonText, String selectFolderButtonText);
/**
* Set file filter - a set of file extensions for files to be visible (Windows)
* or not greyed out (macOS), and a name for the file filter combo box (Windows).
*/
void setFileFilterExtensions(String fileFilterDescription, String[] fileFilterExtensions);
}
interface JBRFileDialogService {

View File

@@ -2,13 +2,13 @@
# Version has the following format: MAJOR.MINOR.PATCH
#
# How to increment JBR API version?
# 1. For small changes if no public API was changed (e.g. only javadoc changes) - increment PATCH
# 2. When only new API is added, or some existing API was @Deprecated - increment MINOR, reset PATCH to 0
# 3. For major backwards incompatible API changes - increment MAJOR, reset MINOR and PATCH to 0
# 1. For small changes if no public API was changed (e.g., only javadoc changes) - increment PATCH.
# 2. When only new API is added, or some existing API was @Deprecated - increment MINOR, reset PATCH to 0.
# 3. For major backwards incompatible API changes - increment MAJOR, reset MINOR and PATCH to 0.
VERSION = 0.0.17
VERSION = 0.0.18
# Hash is used to track changes to jetbrains.api, so you would not forget to update version when needed.
# Hash is used to track changes to jetbrains.api, so you would not forget to update the version when needed.
# When you make any changes, "make jbr-api" will fail and ask you to update hash and version number here.
HASH = 40633C1FDDD2A6D1FBA8F1A36E84FB8
HASH = C3233ED7B0A05816D1B0DB6139AE5F2F