#include "common.h"
#include "FrnString.h"
#include "INSWindowHolder.h"
#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
const int kFileTypePopupTag = 10975;
@interface ExtensionDropdownHandler : NSObject {
@private
NSSavePanel* _dialog;
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 StorageProvider : public ComSingleObject<IFrnStorageProvider, &IID_IFrnStorageProvider>
{
ExtensionDropdownHandler* __strong _extension_dropdown_handler;
public:
FORWARD_IUNKNOWN()
virtual HRESULT SaveBookmarkToBytes (
IFrnString* fileUriStr,
void** err,
IFrnString** ppv
) override
{
@autoreleasepool
{
if(ppv == nullptr)
return E_POINTER;
*ppv = nullptr;
NSError* error = nil;
auto fileUri = [NSURL URLWithString: GetNSStringAndRelease(fileUriStr)];
auto bookmarkData = [fileUri bookmarkDataWithOptions:NSURLBookmarkCreationWithSecurityScope includingResourceValuesForKeys:nil relativeToURL:nil error:&error];
if (bookmarkData)
{
*ppv = CreateByteArray((void*)bookmarkData.bytes, (int)bookmarkData.length);
}
else if (error != nil && err != nullptr)
{
*err = CreateFrnString([error localizedDescription]);
}
return S_OK;
}
}
virtual HRESULT ReadBookmarkFromBytes (
void* ptr,
int len,
IFrnString** ppv
) override {
@autoreleasepool
{
if(ppv == nullptr)
return E_POINTER;
auto bookmarkData = [[NSData alloc] initWithBytes:ptr length:len];
auto fileUri = [NSURL URLByResolvingBookmarkData: bookmarkData
options:NSURLBookmarkResolutionWithSecurityScope|NSURLBookmarkResolutionWithoutUI
relativeToURL:nil
bookmarkDataIsStale:nil
error:nil];
if (fileUri)
{
*ppv = CreateFrnString([fileUri absoluteString]);
}
return S_OK;
}
}
virtual void ReleaseBookmark (
IFrnString* fileUriStr
) override {
}
virtual bool OpenSecurityScope (
IFrnString* fileUriStr
) override {
@autoreleasepool
{
auto fileUri = [NSURL URLWithString: GetNSStringAndRelease(fileUriStr)];
auto success = [fileUri startAccessingSecurityScopedResource];
return success;
}
}
virtual void CloseSecurityScope (
IFrnString* fileUriStr
) override {
@autoreleasepool
{
auto fileUri = [NSURL URLWithString: GetNSStringAndRelease(fileUriStr)];
[fileUri stopAccessingSecurityScopedResource];
}
}
static NSWindow* GetEffectiveNSWindow(IFrnTopLevel* topLevel)
{
auto windowHolder = dynamic_cast<INSWindowHolder*>(topLevel);
if (windowHolder != nullptr)
return windowHolder->GetNSWindow();
auto viewHolder = dynamic_cast<INSViewHolder*>(topLevel);
if (viewHolder != nullptr) {
auto view = (NSView*)viewHolder->GetNSView();
return [view window];
}
return nullptr;
}
static int GetSelectedFilterIndex(NSSavePanel* _Nonnull panel)
{
if (panel.accessoryView != nil)
{
auto popup = [panel.accessoryView viewWithTag:kFileTypePopupTag];
if ([popup isKindOfClass:[NSPopUpButton class]])
return (int)[(NSPopUpButton*)popup indexOfSelectedItem];
}
return -1;
}
virtual void SelectFolderDialog (IFrnTopLevel* parentTopLevel,
IFrnSystemDialogEvents* events,
bool allowMultiple,
const char* title,
const char* initialDirectory) override
{
@autoreleasepool
{
ComPtr<IFrnSystemDialogEvents> ownedEvents(events); auto panel = [NSOpenPanel openPanel];
panel.allowsMultipleSelection = allowMultiple;
panel.canChooseDirectories = true;
panel.canCreateDirectories = true;
panel.canChooseFiles = false;
panel.treatsFilePackagesAsDirectories = true;
if(title != nullptr)
{
panel.message = [NSString stringWithUTF8String:title];
panel.title = [NSString stringWithUTF8String:title];
}
if(initialDirectory != nullptr)
{
auto directoryString = [NSString stringWithUTF8String:initialDirectory];
panel.directoryURL = [NSURL URLWithString:directoryString];
}
auto parentWindow = GetEffectiveNSWindow(parentTopLevel);
auto handler = ^(NSModalResponse result) {
if(result == NSFileHandlingPanelOKButton)
{
auto urls = [panel URLs];
if(urls.count > 0)
{
auto uriStrings = CreateFrnStringArray(urls);
ownedEvents->OnCompleted(uriStrings);
[panel orderOut:panel];
if (parentWindow != nullptr)
{
[parentWindow makeKeyAndOrderFront:parentWindow];
}
return;
}
}
ownedEvents->OnCompleted(nullptr);
};
if (parentWindow != nullptr)
{
[panel beginSheetModalForWindow:parentWindow completionHandler:handler];
}
else
{
[panel beginWithCompletionHandler: handler];
}
}
}
virtual void OpenFileDialog (IFrnTopLevel* parentTopLevel,
IFrnSystemDialogEvents* events,
bool allowMultiple,
const char* title,
const char* initialDirectory,
const char* initialFile,
IFrnFilePickerFileTypes* filters) override
{
@autoreleasepool
{
ComPtr<IFrnSystemDialogEvents> ownedEvents(events); auto panel = [NSOpenPanel openPanel];
panel.allowsMultipleSelection = allowMultiple;
if(title != nullptr)
{
panel.message = [NSString stringWithUTF8String:title];
panel.title = [NSString stringWithUTF8String:title];
}
if(initialFile != nullptr)
{
panel.nameFieldStringValue = [NSString stringWithUTF8String:initialFile];
}
SetAccessoryView(panel, filters, false);
if(initialDirectory != nullptr)
{
auto directoryString = [NSString stringWithUTF8String:initialDirectory];
panel.directoryURL = [NSURL URLWithString:directoryString];
}
auto parentWindow = GetEffectiveNSWindow(parentTopLevel);
auto handler = ^(NSModalResponse result) {
auto selectedIndex = GetSelectedFilterIndex(panel);
if(result == NSFileHandlingPanelOKButton)
{
auto urls = [panel URLs];
if(urls.count > 0)
{
auto uriStrings = CreateFrnStringArray(urls);
ownedEvents->OnCompletedWithFilter(uriStrings, selectedIndex);
[panel orderOut:panel];
if (parentWindow != nullptr)
{
[parentWindow makeKeyAndOrderFront:parentWindow];
}
return;
}
}
ownedEvents->OnCompletedWithFilter(nullptr, selectedIndex);
};
if (parentWindow != nullptr)
{
[panel beginSheetModalForWindow:parentWindow completionHandler:handler];
}
else
{
[panel beginWithCompletionHandler: handler];
}
}
}
virtual void SaveFileDialog (IFrnTopLevel* parentTopLevel,
IFrnSystemDialogEvents* events,
const char* title,
const char* initialDirectory,
const char* initialFile,
IFrnFilePickerFileTypes* filters) override
{
@autoreleasepool
{
ComPtr<IFrnSystemDialogEvents> ownedEvents(events); auto panel = [NSSavePanel savePanel];
if(title != nullptr)
{
panel.message = [NSString stringWithUTF8String:title];
panel.title = [NSString stringWithUTF8String:title];
}
if(initialFile != nullptr)
{
panel.nameFieldStringValue = [NSString stringWithUTF8String:initialFile];
}
SetAccessoryView(panel, filters, true);
if(initialDirectory != nullptr)
{
auto directoryString = [NSString stringWithUTF8String:initialDirectory];
panel.directoryURL = [NSURL URLWithString:directoryString];
}
auto parentWindow = GetEffectiveNSWindow(parentTopLevel);
auto handler = ^(NSModalResponse result) {
auto selectedIndex = GetSelectedFilterIndex(panel);
if(result == NSFileHandlingPanelOKButton)
{
auto url = [panel URL];
auto urls = [NSArray<NSURL*> arrayWithObject:url];
auto uriStrings = CreateFrnStringArray(urls);
ownedEvents->OnCompletedWithFilter(uriStrings, selectedIndex);
[panel orderOut:panel];
if (parentWindow != nullptr)
{
[parentWindow makeKeyAndOrderFront:parentWindow];
}
return;
}
ownedEvents->OnCompletedWithFilter(nullptr, selectedIndex);
};
if (parentWindow != nullptr)
{
[panel beginSheetModalForWindow:parentWindow completionHandler:handler];
}
else
{
[panel beginWithCompletionHandler: handler];
}
}
}
virtual HRESULT TryResolveFileReferenceUri(IFrnString* fileUriStr, IFrnString** ret) override {
if (ret == nullptr)
return E_POINTER;
if (fileUriStr == nullptr)
{
*ret = nullptr;
return S_OK;
}
auto fileUri = [NSURL URLWithString:GetNSStringAndRelease(fileUriStr)];
if (fileUri == nil)
{
*ret = nullptr;
return S_OK;
}
auto filePathUri = [fileUri filePathURL];
if (filePathUri == nil)
{
*ret = nullptr;
return S_OK;
}
*ret = CreateFrnString([filePathUri absoluteString]);
return S_OK;
}
private:
NSView* CreateAccessoryView() {
NSTextField* label = [NSTextField labelWithString:@"File format"];
label.translatesAutoresizingMaskIntoConstraints = NO;
label.textColor = NSColor.secondaryLabelColor;
if (@available(macOS 11.0, *)) {
label.font = [NSFont systemFontOfSize:[NSFont smallSystemFontSize]];
}
NSPopUpButton* popup = [[NSPopUpButton alloc] initWithFrame:NSZeroRect
pullsDown:NO];
popup.translatesAutoresizingMaskIntoConstraints = NO;
popup.tag = kFileTypePopupTag;
[popup setAutoenablesItems:NO];
NSView* group = [[NSView alloc] initWithFrame:NSZeroRect];
group.translatesAutoresizingMaskIntoConstraints = NO;
[group addSubview:label];
[group addSubview:popup];
NSView* view = [[NSView alloc] initWithFrame:NSZeroRect];
view.translatesAutoresizingMaskIntoConstraints = NO;
[view addSubview:group];
NSMutableArray* constraints = [NSMutableArray array];
[constraints
addObject:[popup.topAnchor constraintEqualToAnchor:group.topAnchor
constant:10]];
[constraints
addObject:[label.leadingAnchor constraintEqualToAnchor:group.leadingAnchor
constant:10]];
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]];
[constraints addObject:[group.trailingAnchor
constraintEqualToAnchor:popup.trailingAnchor
constant:10]];
[constraints
addObject:[group.bottomAnchor constraintEqualToAnchor:popup.bottomAnchor
constant:10]];
[constraints
addObject:[group.topAnchor constraintEqualToAnchor:view.topAnchor]];
[constraints addObject:[group.centerXAnchor
constraintEqualToAnchor:view.centerXAnchor]];
[constraints
addObject:[view.bottomAnchor constraintEqualToAnchor:group.bottomAnchor]];
[NSLayoutConstraint activateConstraints:constraints];
return view;
}
void SetAccessoryView(NSSavePanel* panel,
IFrnFilePickerFileTypes* filters,
bool is_save_panel)
{
NSView* accessory_view = CreateAccessoryView();
NSPopUpButton* popup = [accessory_view viewWithTag:kFileTypePopupTag];
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++)
{
NSString* type_description = GetNSStringAndRelease(filters->GetName(i));
[popup addItemWithTitle:type_description];
if (filters->IsAnyType(i)) {
panel.allowsOtherFileTypes = YES;
}
if (filters->IsDefaultType(i)) {
default_extension_index = i;
}
IFrnStringArray* array;
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 (!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;
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 {
[popup selectItemAtIndex:0];
}
[_extension_dropdown_handler popupAction:popup];
if (popup.numberOfItems > 0) {
panel.accessoryView = accessory_view;
}
};
};
extern IFrnStorageProvider* CreateStorageProvider()
{
return new StorageProvider();
}