├── .gitignore ├── src ├── private │ ├── common_types.nim │ ├── native_dialogs_macosx.nim │ ├── native_dialogs_gtk.nim │ └── native_dialogs_windows.nim └── native_dialogs.nim ├── native_dialogs.nimble ├── README.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | native_dialogs 2 | nimcache/ 3 | *.exe 4 | -------------------------------------------------------------------------------- /src/private/common_types.nim: -------------------------------------------------------------------------------- 1 | type 2 | WindowToolkitKind* {.pure.} = enum 3 | Windows = 0 4 | Macosx 5 | Gtk 6 | Qt 7 | 8 | DialogButtonInfo* = tuple[title: string, responseType: int] 9 | -------------------------------------------------------------------------------- /native_dialogs.nimble: -------------------------------------------------------------------------------- 1 | # Package `native_dialogs` 2 | 3 | version = "0.2.0" 4 | author = "Rostyslav Dzinko (rostislav.dzinko@gmail.com)" 5 | description = "Implements framework-agnostic native operating system dialogs calls" 6 | license = "MIT" 7 | srcDir = "src" 8 | 9 | # Deps 10 | 11 | requires "nim >= 0.10.0" 12 | requires "gtk2" -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nim-native-dialogs 2 | 3 | [![nimble](https://raw.githubusercontent.com/yglukhov/nimble-tag/master/nimble.png)](https://github.com/yglukhov/nimble-tag) 4 | 5 | Native Operating System Dialogs implementation for Nim-lang via single API. 6 | The library is GUI framework agnostic and supports the next platforms: 7 | 8 | * GNU/Linux + GTK+3 9 | * OSX + Cocoa 10 | * Windows + Win32 API 11 | 12 | Currently the next dialogs are implemented in a single-file mode: 13 | 14 | * Open File Dialog 15 | * Save File Dialog 16 | * Folder Creation Dialog (GTK-only, stubs to save file dialog on other OSes) 17 | * Folder Selection Dialog (GTK-only, stubs to open file dialog on other OSes) 18 | 19 | ## Installation 20 | 21 | ```bash 22 | $ nimble install native_dialogs 23 | ``` 24 | 25 | ## Usage 26 | ```nim 27 | import native_dialogs 28 | 29 | echo callDialogFileOpen("Open File") 30 | echo callDialogFileSave("Save File") 31 | echo callDialogFolderCreate("Create New Folder") 32 | echo callDialogFolderSelect("Open Folder") 33 | ``` 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2023 Rostyslav Dzinko 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /src/native_dialogs.nim: -------------------------------------------------------------------------------- 1 | import private/common_types 2 | 3 | export WindowToolkitKind 4 | 5 | 6 | when defined(windows): 7 | import private/native_dialogs_windows 8 | elif defined(macosx) and not defined(ios): 9 | import private/native_dialogs_macosx 10 | elif defined(linux) and not defined(android) and not defined(emscripten): 11 | import private/native_dialogs_gtk 12 | 13 | 14 | proc getWindowToolkitKind*(): WindowToolkitKind = getWindowToolkitKindImpl() 15 | 16 | proc callDialogFileOpen*(title: string): string = return callDialogFileOpenImpl(title) 17 | proc callDialogFileSave*(title: string): string = return callDialogFileSaveImpl(title) 18 | proc callDialogFolderCreate*(title: string): string = return callDialogFolderCreateImpl(title) 19 | proc callDialogFolderSelect*(title: string): string = return callDialogFolderSelectImpl(title) 20 | 21 | 22 | when isMainModule: 23 | echo "Your window toolkit is: ", getWindowToolkitKind() 24 | echo callDialogFileOpen("Open Simulation Results File") 25 | echo callDialogFileSave("Save Simulation Results File") 26 | echo callDialogFolderCreate("Create Folder For Simulation Results") 27 | echo callDialogFolderSelect("Select Folder For Simulation Results") 28 | -------------------------------------------------------------------------------- /src/private/native_dialogs_macosx.nim: -------------------------------------------------------------------------------- 1 | import common_types 2 | 3 | {.passL: "-framework AppKit".} 4 | 5 | 6 | type NSSavePanel {.importobjc: "NSSavePanel*", header: "", incompleteStruct.} = object 7 | type NSOpenPanel {.importobjc: "NSOpenPanel*", header: "", incompleteStruct.} = object 8 | 9 | proc newOpenPanel: NSOpenPanel {.importobjc: "NSOpenPanel openPanel", nodecl.} 10 | proc newSavePanel: NSSavePanel {.importobjc: "NSSavePanel savePanel", nodecl.} 11 | 12 | 13 | template wrapObjModalCode(body: untyped) = 14 | {.emit: """ 15 | NSAutoreleasePool* pool = [NSAutoreleasePool new]; 16 | NSWindow* keyWindow = [NSApp keyWindow]; 17 | NSOpenGLContext* glCtx = [[NSOpenGLContext currentContext] retain]; 18 | """.} 19 | body 20 | {.emit: """ 21 | [pool drain]; 22 | [glCtx makeCurrentContext]; 23 | [glCtx release]; 24 | [keyWindow makeKeyAndOrderFront: nil]; 25 | """.} 26 | 27 | 28 | proc getWindowToolkitKindImpl*(): WindowToolkitKind = 29 | return WindowToolkitKind.Macosx 30 | 31 | 32 | proc callDialogFileOpenImpl*(title: string): string = 33 | wrapObjModalCode: 34 | let dialog = newOpenPanel() 35 | let ctitle : cstring = title 36 | var cres: cstring 37 | {.emit: """ 38 | [`dialog` setCanChooseFiles:YES]; 39 | `dialog`.title = [NSString stringWithUTF8String: `ctitle`]; 40 | if ([`dialog` runModal] == NSOKButton && `dialog`.URLs.count > 0) { 41 | `cres` = [`dialog`.URLs objectAtIndex: 0].path.UTF8String; 42 | } 43 | """.} 44 | if not cres.isNil: 45 | result = $cres 46 | 47 | proc callDialogFileSaveImpl*(title: string): string = 48 | wrapObjModalCode: 49 | let dialog = newSavePanel() 50 | let ctitle : cstring = title 51 | var cres: cstring 52 | 53 | {.emit: """ 54 | `dialog`.canCreateDirectories = YES; 55 | `dialog`.title = [NSString stringWithUTF8String: `ctitle`]; 56 | if ([`dialog` runModal] == NSOKButton) { 57 | `cres` = `dialog`.URL.path.UTF8String; 58 | } 59 | """.} 60 | if not cres.isNil: 61 | result = $cres 62 | 63 | 64 | proc callDialogFolderCreateImpl*(title: string): string = 65 | return callDialogFileSaveImpl(title) 66 | 67 | 68 | proc callDialogFolderSelectImpl*(title: string): string = 69 | return callDialogFileOpenImpl(title) -------------------------------------------------------------------------------- /src/private/native_dialogs_gtk.nim: -------------------------------------------------------------------------------- 1 | import common_types 2 | 3 | import gtk2 4 | 5 | nim_init() 6 | 7 | 8 | const 9 | dialogFileOpenDefaultButtons*: seq[DialogButtonInfo] = @[ 10 | (title: "Cancel", responseType: RESPONSE_CANCEL.int), 11 | (title: "Open", responseType: RESPONSE_ACCEPT.int) 12 | ] 13 | 14 | dialogFileSaveDefaultButtons*: seq[DialogButtonInfo] = @[ 15 | (title: "Cancel", responseType: RESPONSE_CANCEL.int), 16 | (title: "Save", responseType: RESPONSE_ACCEPT.int) 17 | ] 18 | 19 | dialogFolderCreateDefaultButtons*: seq[DialogButtonInfo] = @[ 20 | (title: "Cancel", responseType: RESPONSE_CANCEL.int), 21 | (title: "Create", responseType: RESPONSE_ACCEPT.int) 22 | ] 23 | 24 | dialogFolderSelectDefaultButtons*: seq[DialogButtonInfo] = @[ 25 | (title: "Cancel", responseType: RESPONSE_CANCEL.int), 26 | (title: "Open", responseType: RESPONSE_ACCEPT.int) 27 | ] 28 | 29 | 30 | proc callDialogFile(action: TFileChooserAction, title: string, buttons: seq[DialogButtonInfo] = @[]): string = 31 | var dialog = file_chooser_dialog_new(title.cstring, nil, action, nil) 32 | 33 | for button in buttons: 34 | discard dialog.add_button(button.title.cstring, button.responseType.cint) 35 | 36 | var res = dialog.run() 37 | 38 | case res: 39 | of RESPONSE_ACCEPT, RESPONSE_YES, RESPONSE_APPLY: 40 | let fileChooser = cast[PFileChooser](pointer(dialog)) 41 | result = $fileChooser.get_filename() 42 | of RESPONSE_REJECT, RESPONSE_NO, RESPONSE_CANCEL, RESPONSE_CLOSE: 43 | result = "" 44 | else: 45 | result = "" 46 | 47 | dialog.destroy() 48 | while events_pending() > 0: 49 | discard main_iteration() 50 | 51 | 52 | proc getWindowToolkitKindImpl*(): WindowToolkitKind = 53 | return WindowToolkitKind.Gtk 54 | 55 | proc callDialogFileOpenImpl*(title: string, buttons: seq[DialogButtonInfo] = dialogFileOpenDefaultButtons): string = 56 | result = callDialogFile(TFileChooserAction.FILE_CHOOSER_ACTION_OPEN, title, buttons) 57 | 58 | proc callDialogFileSaveImpl*(title: string): string = 59 | return callDialogFile(TFileChooserAction.FILE_CHOOSER_ACTION_SAVE, title, dialogFileSaveDefaultButtons) 60 | 61 | proc callDialogFolderCreateImpl*(title: string): string = 62 | return callDialogFile(TFileChooserAction.FILE_CHOOSER_ACTION_CREATE_FOLDER, title, dialogFolderCreateDefaultButtons) 63 | 64 | proc callDialogFolderSelectImpl*(title: string): string = 65 | return callDialogFile(TFileChooserAction.FILE_CHOOSER_ACTION_SELECT_FOLDER, title, dialogFolderSelectDefaultButtons) 66 | -------------------------------------------------------------------------------- /src/private/native_dialogs_windows.nim: -------------------------------------------------------------------------------- 1 | import common_types 2 | 3 | type 4 | BOOL = int32 5 | WORD = uint16 6 | DWORD = uint32 7 | HANDLE = pointer 8 | HWND = HANDLE 9 | HINSTANCE = HANDLE 10 | LPCSTR = cstring 11 | LPSTR = cstring 12 | LPARAM = ByteAddress 13 | WPARAM = ByteAddress 14 | WINUINT = uint32 15 | LPOFNHOOKPROC = proc (para1: HWND, para2: WINUINT, para3: WPARAM, para4: LPARAM): WINUINT {.stdcall.} 16 | LPEDITMENU = pointer 17 | 18 | OpenFileNameA {.final, pure.} = object 19 | lStructSize: DWORD 20 | hwndOwner: HWND 21 | hInstance: HINSTANCE 22 | lpstrFilter: LPCSTR 23 | lpstrCustomFilter: LPSTR 24 | nMaxCustFilter: DWORD 25 | nFilterIndex: DWORD 26 | lpstrFile: LPSTR 27 | nMaxFile: DWORD 28 | lpstrFileTitle: LPSTR 29 | nMaxFileTitle: DWORD 30 | lpstrInitialDir: LPCSTR 31 | lpstrTitle: LPCSTR 32 | flags: DWORD 33 | nFileOffset: WORD 34 | nFileExtension: WORD 35 | lpstrDefExt: LPCSTR 36 | lCustData: LPARAM 37 | lpfnHook: LPOFNHOOKPROC 38 | lpTemplateName: LPCSTR 39 | pvReserved: pointer 40 | dwReserved: DWORD 41 | flagsEx: DWORD 42 | 43 | 44 | const 45 | OFN_FILEMUSTEXIST: DWORD = 0x00001000 46 | OFN_PATHMUSTEXIST: DWORD = 0x00000800 47 | 48 | const 49 | TRUE: BOOL = 1 50 | 51 | 52 | proc GetOpenFileNameA(unnamedParam1: ptr OpenFileNameA): BOOL {.cdecl, stdcall, importc, dynlib: "Comdlg32.dll".} 53 | proc GetSaveFileNameA(unnamedParam1: ptr OpenFileNameA): BOOL {.cdecl, stdcall, importc, dynlib: "Comdlg32.dll".} 54 | proc CommDlgExtendedError(): DWORD {.cdecl, used, importc, stdcall, dynlib: "Comdlg32.dll".} 55 | 56 | 57 | proc getWindowToolkitKindImpl*(): WindowToolkitKind = 58 | return WindowToolkitKind.Windows 59 | 60 | 61 | proc callDialogFileSaveImpl*(title: string): string = 62 | var 63 | fileInfo: ptr OpenFileNameA = cast[ptr OpenFileNameA](alloc0(sizeof(OpenFileNameA))) 64 | buf: cstring = cast[cstring](alloc0(1024)) 65 | 66 | fileInfo.lStructSize = sizeof(OpenFileNameA).DWORD 67 | fileInfo.hWndOwner = cast[HWND](0) 68 | fileInfo.flags = OFN_FILEMUSTEXIST or OFN_PATHMUSTEXIST 69 | fileInfo.lpstrFile = buf 70 | fileInfo.nMaxFile = 1024 71 | fileInfo.lpstrFilter = "All\0*.*\0"; 72 | fileInfo.lpstrFileTitle = title 73 | 74 | let res = GetSaveFileNameA(fileInfo) 75 | 76 | return if res == TRUE : $buf else: "" 77 | 78 | 79 | proc callDialogFileOpenImpl*(title: string): string = 80 | var 81 | fileInfo: ptr OpenFileNameA = cast[ptr OpenFileNameA](alloc0(sizeof(OpenFileNameA))) 82 | buf: cstring = cast[cstring](alloc0(1024)) 83 | 84 | fileInfo.lStructSize = sizeof(OpenFileNameA).DWORD 85 | fileInfo.hWndOwner = cast[HWND](0) 86 | fileInfo.flags = OFN_FILEMUSTEXIST or OFN_PATHMUSTEXIST 87 | fileInfo.lpstrFile = buf 88 | fileInfo.nMaxFile = 1024 89 | fileInfo.lpstrFilter = "All\0*.*\0"; 90 | fileInfo.lpstrFileTitle = title 91 | 92 | let res = GetOpenFileNameA(fileInfo) 93 | 94 | return if res == TRUE: $buf else: "" 95 | 96 | 97 | proc callDialogFolderCreateImpl*(title: string): string = 98 | return callDialogFileSaveImpl(title) 99 | 100 | 101 | proc callDialogFolderSelectImpl*(title: string): string = 102 | return callDialogFileOpenImpl(title) --------------------------------------------------------------------------------