|
|
@@ -1,9 +1,73 @@
|
|
|
#include "common.h"
|
|
|
+#include "AvnString.h"
|
|
|
#include "INSWindowHolder.h"
|
|
|
#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
|
|
|
|
|
|
+const int kFileTypePopupTag = 10975;
|
|
|
+
|
|
|
+// Target for NSPopupButton control in file dialog's accessory view.
|
|
|
+// ExtensionDropdownHandler is copied from Chromium MIT code of select_file_dialog_bridge
|
|
|
+@interface ExtensionDropdownHandler : NSObject {
|
|
|
+ @private
|
|
|
+ // The file dialog to which this target object corresponds. Weak reference
|
|
|
+ // since the dialog_ will stay alive longer than this object.
|
|
|
+ NSSavePanel* _dialog;
|
|
|
+
|
|
|
+ // Two ivars serving the same purpose. While `_fileTypeLists` is for pre-macOS
|
|
|
+ // 11, and contains NSStrings with UTType identifiers, `_fileUTTypeLists` is
|
|
|
+ // for macOS 11 and later, and contains UTTypes.
|
|
|
+ NSArray<NSArray<NSString*>*>* __strong _fileTypeLists;
|
|
|
+ NSArray<NSArray<UTType*>*>* __strong _fileUTTypeLists
|
|
|
+ API_AVAILABLE(macos(11.0));
|
|
|
+}
|
|
|
+
|
|
|
+- (instancetype)initWithDialog:(NSSavePanel*)dialog
|
|
|
+ fileTypeLists:(NSArray<NSArray<NSString*>*>*)fileTypeLists;
|
|
|
+
|
|
|
+- (instancetype)initWithDialog:(NSSavePanel*)dialog
|
|
|
+ fileUTTypeLists:(NSArray<NSArray<UTType*>*>*)fileUTTypeLists
|
|
|
+ API_AVAILABLE(macos(11.0));
|
|
|
+
|
|
|
+- (void)popupAction:(id)sender;
|
|
|
+@end
|
|
|
+
|
|
|
+
|
|
|
+@implementation ExtensionDropdownHandler
|
|
|
+
|
|
|
+- (instancetype)initWithDialog:(NSSavePanel*)dialog
|
|
|
+ fileTypeLists:(NSArray<NSArray<NSString*>*>*)fileTypeLists {
|
|
|
+ if ((self = [super init])) {
|
|
|
+ _dialog = dialog;
|
|
|
+ _fileTypeLists = fileTypeLists;
|
|
|
+ }
|
|
|
+ return self;
|
|
|
+}
|
|
|
+
|
|
|
+- (instancetype)initWithDialog:(NSSavePanel*)dialog
|
|
|
+ fileUTTypeLists:(NSArray<NSArray<UTType*>*>*)fileUTTypeLists
|
|
|
+ API_AVAILABLE(macos(11.0)) {
|
|
|
+ if ((self = [super init])) {
|
|
|
+ _dialog = dialog;
|
|
|
+ _fileUTTypeLists = fileUTTypeLists;
|
|
|
+ }
|
|
|
+ return self;
|
|
|
+}
|
|
|
+
|
|
|
+- (void)popupAction:(id)sender {
|
|
|
+ NSUInteger index = [sender indexOfSelectedItem];
|
|
|
+ if (@available(macOS 11, *)) {
|
|
|
+ _dialog.allowedContentTypes = [_fileUTTypeLists objectAtIndex:index];
|
|
|
+ } else {
|
|
|
+ _dialog.allowedFileTypes = [_fileTypeLists objectAtIndex:index];
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+@end
|
|
|
+
|
|
|
class SystemDialogs : public ComSingleObject<IAvnSystemDialogs, &IID_IAvnSystemDialogs>
|
|
|
{
|
|
|
+ ExtensionDropdownHandler* __strong _extension_dropdown_handler;
|
|
|
+
|
|
|
public:
|
|
|
FORWARD_IUNKNOWN()
|
|
|
virtual void SelectFolderDialog (IAvnWindow* parentWindowHandle,
|
|
|
@@ -88,7 +152,7 @@ public:
|
|
|
const char* title,
|
|
|
const char* initialDirectory,
|
|
|
const char* initialFile,
|
|
|
- const char* filters) override
|
|
|
+ IAvnFilePickerFileTypes* filters) override
|
|
|
{
|
|
|
@autoreleasepool
|
|
|
{
|
|
|
@@ -113,25 +177,7 @@ public:
|
|
|
panel.nameFieldStringValue = [NSString stringWithUTF8String:initialFile];
|
|
|
}
|
|
|
|
|
|
- if(filters != nullptr)
|
|
|
- {
|
|
|
- auto filtersString = [NSString stringWithUTF8String:filters];
|
|
|
-
|
|
|
- if(filtersString.length > 0)
|
|
|
- {
|
|
|
- auto allowedTypes = [filtersString componentsSeparatedByString:@";"];
|
|
|
-
|
|
|
- // Prefer allowedContentTypes if available
|
|
|
- if (@available(macOS 11.0, *))
|
|
|
- {
|
|
|
- panel.allowedContentTypes = ConvertToUTType(allowedTypes);
|
|
|
- }
|
|
|
- else
|
|
|
- {
|
|
|
- panel.allowedFileTypes = allowedTypes;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
+ SetAccessoryView(panel, filters, false);
|
|
|
|
|
|
auto handler = ^(NSModalResponse result) {
|
|
|
if(result == NSFileHandlingPanelOKButton)
|
|
|
@@ -187,7 +233,7 @@ public:
|
|
|
const char* title,
|
|
|
const char* initialDirectory,
|
|
|
const char* initialFile,
|
|
|
- const char* filters) override
|
|
|
+ IAvnFilePickerFileTypes* filters) override
|
|
|
{
|
|
|
@autoreleasepool
|
|
|
{
|
|
|
@@ -210,28 +256,7 @@ public:
|
|
|
panel.nameFieldStringValue = [NSString stringWithUTF8String:initialFile];
|
|
|
}
|
|
|
|
|
|
- if(filters != nullptr)
|
|
|
- {
|
|
|
- auto filtersString = [NSString stringWithUTF8String:filters];
|
|
|
-
|
|
|
- if(filtersString.length > 0)
|
|
|
- {
|
|
|
- auto allowedTypes = [filtersString componentsSeparatedByString:@";"];
|
|
|
-
|
|
|
- // Prefer allowedContentTypes if available
|
|
|
- if (@available(macOS 11.0, *))
|
|
|
- {
|
|
|
- panel.allowedContentTypes = ConvertToUTType(allowedTypes);
|
|
|
- }
|
|
|
- else
|
|
|
- {
|
|
|
- panel.allowedFileTypes = allowedTypes;
|
|
|
- }
|
|
|
-
|
|
|
- panel.allowsOtherFileTypes = false;
|
|
|
- panel.extensionHidden = false;
|
|
|
- }
|
|
|
- }
|
|
|
+ SetAccessoryView(panel, filters, true);
|
|
|
|
|
|
auto handler = ^(NSModalResponse result) {
|
|
|
if(result == NSFileHandlingPanelOKButton)
|
|
|
@@ -240,9 +265,9 @@ public:
|
|
|
|
|
|
auto url = [panel URL];
|
|
|
|
|
|
- auto string = [url path];
|
|
|
+ auto string = [url path];
|
|
|
strings[0] = (void*)[string UTF8String];
|
|
|
-
|
|
|
+
|
|
|
events->OnCompleted(1, &strings[0]);
|
|
|
|
|
|
[panel orderOut:panel];
|
|
|
@@ -274,31 +299,221 @@ public:
|
|
|
}
|
|
|
|
|
|
private:
|
|
|
- NSMutableArray* ConvertToUTType(NSArray<NSString*>* allowedTypes)
|
|
|
+ NSView* CreateAccessoryView() {
|
|
|
+ // The label. Add attributes per-OS to match the labels that macOS uses.
|
|
|
+ NSTextField* label = [NSTextField labelWithString:@"File format"];
|
|
|
+ label.translatesAutoresizingMaskIntoConstraints = NO;
|
|
|
+ label.textColor = NSColor.secondaryLabelColor;
|
|
|
+ if (@available(macOS 11.0, *)) {
|
|
|
+ label.font = [NSFont systemFontOfSize:[NSFont smallSystemFontSize]];
|
|
|
+ }
|
|
|
+
|
|
|
+ // The popup.
|
|
|
+ NSPopUpButton* popup = [[NSPopUpButton alloc] initWithFrame:NSZeroRect
|
|
|
+ pullsDown:NO];
|
|
|
+ popup.translatesAutoresizingMaskIntoConstraints = NO;
|
|
|
+ popup.tag = kFileTypePopupTag;
|
|
|
+ [popup setAutoenablesItems:NO];
|
|
|
+
|
|
|
+ // A view to group the label and popup together. The top-level view used as
|
|
|
+ // the accessory view will be stretched horizontally to match the width of
|
|
|
+ // the dialog, and the label and popup need to be grouped together as one
|
|
|
+ // view to do centering within it, so use a view to group the label and
|
|
|
+ // popup.
|
|
|
+ NSView* group = [[NSView alloc] initWithFrame:NSZeroRect];
|
|
|
+ group.translatesAutoresizingMaskIntoConstraints = NO;
|
|
|
+ [group addSubview:label];
|
|
|
+ [group addSubview:popup];
|
|
|
+
|
|
|
+ // This top-level view will be forced by the system to have the width of the
|
|
|
+ // save dialog.
|
|
|
+ NSView* view = [[NSView alloc] initWithFrame:NSZeroRect];
|
|
|
+ view.translatesAutoresizingMaskIntoConstraints = NO;
|
|
|
+ [view addSubview:group];
|
|
|
+
|
|
|
+ NSMutableArray* constraints = [NSMutableArray array];
|
|
|
+
|
|
|
+ // The required constraints for the group, instantiated top-to-bottom:
|
|
|
+ // ┌───────────────────┐
|
|
|
+ // │ ↕︎ │
|
|
|
+ // │ ↔︎ label ↔︎ popup ↔︎ │
|
|
|
+ // │ ↕︎ │
|
|
|
+ // └───────────────────┘
|
|
|
+
|
|
|
+ // Top.
|
|
|
+ [constraints
|
|
|
+ addObject:[popup.topAnchor constraintEqualToAnchor:group.topAnchor
|
|
|
+ constant:10]];
|
|
|
+
|
|
|
+ // Leading.
|
|
|
+ [constraints
|
|
|
+ addObject:[label.leadingAnchor constraintEqualToAnchor:group.leadingAnchor
|
|
|
+ constant:10]];
|
|
|
+
|
|
|
+ // Horizontal and vertical baseline between the label and popup.
|
|
|
+ CGFloat labelPopupPadding;
|
|
|
+ if (@available(macOS 11.0, *)) {
|
|
|
+ labelPopupPadding = 8;
|
|
|
+ } else {
|
|
|
+ labelPopupPadding = 5;
|
|
|
+ }
|
|
|
+ [constraints addObject:[popup.leadingAnchor
|
|
|
+ constraintEqualToAnchor:label.trailingAnchor
|
|
|
+ constant:labelPopupPadding]];
|
|
|
+ [constraints
|
|
|
+ addObject:[popup.firstBaselineAnchor
|
|
|
+ constraintEqualToAnchor:label.firstBaselineAnchor]];
|
|
|
+
|
|
|
+ // Trailing.
|
|
|
+ [constraints addObject:[group.trailingAnchor
|
|
|
+ constraintEqualToAnchor:popup.trailingAnchor
|
|
|
+ constant:10]];
|
|
|
+
|
|
|
+ // Bottom.
|
|
|
+ [constraints
|
|
|
+ addObject:[group.bottomAnchor constraintEqualToAnchor:popup.bottomAnchor
|
|
|
+ constant:10]];
|
|
|
+
|
|
|
+ // Then the constraints centering the group in the accessory view. Vertical
|
|
|
+ // spacing is fully specified, but as the horizontal size of the accessory
|
|
|
+ // view will be forced to conform to the save dialog, only specify horizontal
|
|
|
+ // centering.
|
|
|
+ // ┌──────────────┐
|
|
|
+ // │ ↕︎ │
|
|
|
+ // │ ↔group↔︎ │
|
|
|
+ // │ ↕︎ │
|
|
|
+ // └──────────────┘
|
|
|
+
|
|
|
+ // Top.
|
|
|
+ [constraints
|
|
|
+ addObject:[group.topAnchor constraintEqualToAnchor:view.topAnchor]];
|
|
|
+
|
|
|
+ // Centering.
|
|
|
+ [constraints addObject:[group.centerXAnchor
|
|
|
+ constraintEqualToAnchor:view.centerXAnchor]];
|
|
|
+
|
|
|
+ // Bottom.
|
|
|
+ [constraints
|
|
|
+ addObject:[view.bottomAnchor constraintEqualToAnchor:group.bottomAnchor]];
|
|
|
+
|
|
|
+ [NSLayoutConstraint activateConstraints:constraints];
|
|
|
+
|
|
|
+ return view;
|
|
|
+ }
|
|
|
+
|
|
|
+ void SetAccessoryView(NSSavePanel* panel,
|
|
|
+ IAvnFilePickerFileTypes* filters,
|
|
|
+ bool is_save_panel)
|
|
|
{
|
|
|
- auto originalCount = [allowedTypes count];
|
|
|
- auto mapped = [[NSMutableArray alloc] init];
|
|
|
+ NSView* accessory_view = CreateAccessoryView();
|
|
|
+
|
|
|
+ NSPopUpButton* popup = [accessory_view viewWithTag:kFileTypePopupTag];
|
|
|
|
|
|
- if (@available(macOS 11.0, *))
|
|
|
+ NSMutableArray<NSArray<NSString*>*>* file_type_lists = [NSMutableArray array];
|
|
|
+ NSMutableArray* file_uttype_lists = [NSMutableArray array];
|
|
|
+ int default_extension_index = -1;
|
|
|
+
|
|
|
+ for (int i = 0; i < filters->GetCount(); i++)
|
|
|
{
|
|
|
- for (int i = 0; i < originalCount; i++)
|
|
|
- {
|
|
|
- auto utTypeStr = allowedTypes[i];
|
|
|
- auto utType = [UTType typeWithIdentifier:utTypeStr];
|
|
|
- if (utType == nil)
|
|
|
- {
|
|
|
- utType = [UTType typeWithMIMEType:utTypeStr];
|
|
|
+ NSString* type_description = GetNSStringAndRelease(filters->GetName(i));
|
|
|
+ [popup addItemWithTitle:type_description];
|
|
|
+
|
|
|
+ // If any type is included, enable allowsOtherFileTypes, and skip this filter on save panel.
|
|
|
+ if (filters->IsAnyType(i)) {
|
|
|
+ panel.allowsOtherFileTypes = YES;
|
|
|
+ }
|
|
|
+ // If default extension is specified, auto select it later.
|
|
|
+ if (filters->IsDefaultType(i)) {
|
|
|
+ default_extension_index = i;
|
|
|
+ }
|
|
|
+
|
|
|
+ IAvnStringArray* array;
|
|
|
+
|
|
|
+ // Prefer types priority of: file ext -> apple type id -> mime.
|
|
|
+ // On macOS 10 we only support file extensions.
|
|
|
+ if (@available(macOS 11, *)) {
|
|
|
+ NSMutableArray* file_uttype_array = [NSMutableArray array];
|
|
|
+ bool typeCompleted = false;
|
|
|
+
|
|
|
+ if (filters->IsAnyType(i)) {
|
|
|
+ UTType* type = [UTType typeWithIdentifier:@"public.item"];
|
|
|
+ [file_uttype_array addObject:type];
|
|
|
+ typeCompleted = true;
|
|
|
}
|
|
|
- if (utType != nil)
|
|
|
- {
|
|
|
- [mapped addObject:utType];
|
|
|
+ if (!typeCompleted && filters->GetExtensions(i, &array) == 0) {
|
|
|
+ for (NSString* ext in GetNSArrayOfStringsAndRelease(array))
|
|
|
+ {
|
|
|
+ UTType* type = [UTType typeWithFilenameExtension:ext];
|
|
|
+ if (type && ![file_uttype_array containsObject:type]) {
|
|
|
+ [file_uttype_array addObject:type];
|
|
|
+ typeCompleted = true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (!typeCompleted && filters->GetAppleUniformTypeIdentifiers(i, &array) == 0) {
|
|
|
+ for (NSString* ext in GetNSArrayOfStringsAndRelease(array))
|
|
|
+ {
|
|
|
+ UTType* type = [UTType typeWithIdentifier:ext];
|
|
|
+ if (type && ![file_uttype_array containsObject:type]) {
|
|
|
+ [file_uttype_array addObject:type];
|
|
|
+ typeCompleted = true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (!typeCompleted && filters->GetMimeTypes(i, &array) == 0) {
|
|
|
+ for (NSString* ext in GetNSArrayOfStringsAndRelease(array))
|
|
|
+ {
|
|
|
+ UTType* type = [UTType typeWithMIMEType:ext];
|
|
|
+ if (type && ![file_uttype_array containsObject:type]) {
|
|
|
+ [file_uttype_array addObject:type];
|
|
|
+ typeCompleted = true;
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
+
|
|
|
+ [file_uttype_lists addObject:file_uttype_array];
|
|
|
+ } else {
|
|
|
+ NSMutableArray<NSString*>* file_type_array = [NSMutableArray array];
|
|
|
+ if (filters->IsAnyType(i)) {
|
|
|
+ [file_type_array addObject:@"*.*"];
|
|
|
+ }
|
|
|
+ else if (filters->GetExtensions(i, &array) == 0) {
|
|
|
+ for (NSString* ext in GetNSArrayOfStringsAndRelease(array))
|
|
|
+ {
|
|
|
+ if (![file_type_array containsObject:ext]) {
|
|
|
+ [file_type_array addObject:ext];
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ [file_type_lists addObject:file_type_array];
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ if ([file_uttype_lists count] == 0 && [file_type_lists count] == 0)
|
|
|
+ return;
|
|
|
|
|
|
- return mapped;
|
|
|
- }
|
|
|
-
|
|
|
+ if (@available(macOS 11, *))
|
|
|
+ _extension_dropdown_handler = [[ExtensionDropdownHandler alloc] initWithDialog:panel
|
|
|
+ fileUTTypeLists:file_uttype_lists];
|
|
|
+ else
|
|
|
+ _extension_dropdown_handler = [[ExtensionDropdownHandler alloc] initWithDialog:panel
|
|
|
+ fileTypeLists:file_type_lists];
|
|
|
+
|
|
|
+ [popup setTarget: _extension_dropdown_handler];
|
|
|
+ [popup setAction: @selector(popupAction:)];
|
|
|
+
|
|
|
+ if (default_extension_index != -1) {
|
|
|
+ [popup selectItemAtIndex:default_extension_index];
|
|
|
+ } else {
|
|
|
+ // Select the first item.
|
|
|
+ [popup selectItemAtIndex:0];
|
|
|
+ }
|
|
|
+ [_extension_dropdown_handler popupAction:popup];
|
|
|
+
|
|
|
+ if (popup.numberOfItems > 0) {
|
|
|
+ panel.accessoryView = accessory_view;
|
|
|
+ }
|
|
|
+ };
|
|
|
};
|
|
|
|
|
|
extern IAvnSystemDialogs* CreateSystemDialogs()
|