Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ jobs:
odin check win32/game_of_life -target:windows_amd64 $FLAGS
odin check win32/open_window -target:windows_amd64 $FLAGS
odin check win32/embedded_manifest -target:windows_amd64 $FLAGS
odin check win32/common_dialogs -target:windows_amd64 $FLAGS

odin check nanovg/example.odin -file $FLAGS
odin check nanovg/fbo.odin -file $FLAGS
Expand Down
70 changes: 70 additions & 0 deletions win32/common_dialogs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Common dialog examples

Demonstrates the usage of the common Win32 dialog boxes in `comdlg32.dll`:

* [Color chooser](#color-chooser-dialog)
* [Font chooser](#font-chooser-dialog)
* [Open/Save file](#opensave-file-dialog)
* [Print](#print-dialog)
* [Page setup](#page-setup-dialog)
* [Find/Replace text](#findreplace-text-dialog)

See also: https://learn.microsoft.com/en-us/windows/win32/dlgbox/using-common-dialog-boxes

## Color chooser dialog

See: `color_chooser.odin`

![Color chooser](doc/color_chooser.png)

## Font chooser dialog

See: `font_chooser.odin`

![Font chooser](doc/font_chooser.png)

## Open/Save file dialog

See: `file_dialog.odin`

The example contains a convenience wrapper which converts to and from the Odin `string` type,
and handles construction of the file type filter string.

The example code does not implement the multi-select option.

**TODO**: Implement convenience feature for `file_dialog` with `ALLOW_MULTI_SELECT` that takes
output of the form `"path\u0000\file1u\0000file2"` and turns it into `[]string` with the path + file pre-concatenated for the user.

![Open file](doc/open_file.png)

![Save file](doc/save_file.png)

## Print dialog

See: `print.odin`

The print dialog examples reproduce the examples provided on MSDN without further modifications.

![Print](doc/print.png)

## Page setup dialog

See: `page_setup.odin`

The example also demonstrates how the displayed preview page can be customized.

![Page setup](doc/page_setup.png)

## Find/Replace text dialog

See `find_replace_dialog.odin`

Unlike the other dialogs, this one requires additional interaction with the application's
main window.

The application is responsible for actually performing the find/replace action
in `handle_find_replace_message`.

![Find](doc/find.png)

![Replace](doc/replace.png)
14 changes: 14 additions & 0 deletions win32/common_dialogs/app.manifest
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<dependency>
<dependentAssembly>
<assemblyIdentity
type="win32"
name="Microsoft.Windows.Common-Controls"
version="6.0.0.0"
processorArchitecture="*"
publicKeyToken="6595b64144ccf1df"
language="*"/>
</dependentAssembly>
</dependency>
</assembly>
39 changes: 39 additions & 0 deletions win32/common_dialogs/color_chooser.odin
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package comdlg32_example

import win "core:sys/windows"

// Stores the persistent custom color palette of the color chooser dialog.
g_color_chooser_custom_colors := default_custom_colors()

default_custom_colors :: proc "contextless" () -> [16]win.COLORREF {
c: [16]win.COLORREF
for i in 0 ..< len(c) {
c[i] = win.RGB(255, 255, 255)
}
return c
}

choose_color :: proc(
owner: win.HWND,
initial_color: win.COLORREF = 0x00ffffff,
) -> (
dialog_confirmed: bool,
new_color: win.COLORREF,
) {
cc := win.CHOOSECOLORW {
lStructSize = size_of(win.CHOOSECOLORW),
hwndOwner = owner,
hInstance = nil,
rgbResult = initial_color,
lpCustColors = raw_data(g_color_chooser_custom_colors[:]),
Flags = win.CC_RGBINIT | win.CC_FULLOPEN,
}

confirmed := win.ChooseColorW(&cc)

if confirmed {
return true, cc.rgbResult
} else {
return false, 0
}
}
Binary file added win32/common_dialogs/doc/color_chooser.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added win32/common_dialogs/doc/find.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added win32/common_dialogs/doc/font_chooser.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added win32/common_dialogs/doc/open_file.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added win32/common_dialogs/doc/page_setup.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added win32/common_dialogs/doc/print.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added win32/common_dialogs/doc/replace.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added win32/common_dialogs/doc/save_file.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
151 changes: 151 additions & 0 deletions win32/common_dialogs/file_dialog.odin
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package comdlg32_example

import runtime "base:runtime"
import strings "core:strings"
import log "core:log"
import win "core:sys/windows"

File_Dialog_Mode :: enum {
open,
save,
}

File_Dialog_Filter :: struct {
// The full line of text displayed to the user in the selection box.
//
// Examples:
// - `"Text files (*.txt)"`
// - `"All files (*.*)"`
display_text: string,

// The internal search pattern used to filter the files.
//
// Examples:
// - `"*.txt"`
// - `"*.*"` // NOTE: This includes files without an extension.
filter: string,
}

file_dialog :: proc(
owner: win.HWND,
mode: File_Dialog_Mode,
initial_file: Maybe(string) = nil,
filters: []File_Dialog_Filter = nil,
#any_int selected_filter_index: u32 = 0,
dialog_caption: Maybe(string) = nil,
initial_directory: Maybe(string) = nil,
allocator: runtime.Allocator = context.allocator,
) -> (
dialog_confirmed: bool,
selected_file: string,
) {
file_buf := make([]u16, win.MAX_PATH_WIDE, context.temp_allocator)
file_buf[0] = 0

if initial_file != nil {
converted := win.utf8_to_wstring(file_buf, initial_file.?)
if converted == nil {
// Conversion error.
log.warnf("Unable to convert initial file name string.")
file_buf[0] = 0
}
}

ofn := win.OPENFILENAMEW {
lStructSize = size_of(win.OPENFILENAMEW),
hwndOwner = owner,
lpstrFilter = file_dialog_build_filter_string(filters),
nFilterIndex = selected_filter_index + 1,
lpstrFile = raw_data(file_buf[:]),
nMaxFile = win.MAX_PATH_WIDE,
}

if dialog_caption != nil {
ofn.lpstrTitle = win.utf8_to_wstring(dialog_caption.?, context.temp_allocator)
}
if initial_directory != nil {
ofn.lpstrInitialDir = win.utf8_to_wstring(initial_directory.?, context.temp_allocator)
}

confirmed: win.BOOL

switch mode {
case .open:
ofn.Flags = win.OFN_PATHMUSTEXIST | win.OFN_FILEMUSTEXIST | win.OFN_EXPLORER
confirmed = win.GetOpenFileNameW(&ofn)
case .save:
ofn.Flags = win.OFN_OVERWRITEPROMPT | win.OFN_EXPLORER
confirmed = win.GetSaveFileNameW(&ofn)
case:
unreachable()
}

if confirmed {
// Convert result to string.
path := cast(cstring16)ofn.lpstrFile
path_str, err1 := win.wstring_to_utf8(path, allocator = allocator)
if err1 != nil {
log.warnf("File dialog confirmed - string conversion error: %v", err1)
return false, ""
}
return true, path_str
} else {
return false, ""
}
}

// Converts the `filters` slice to the string format required by `OPENFILENAMEW.lpstrFilter`.
//
// Returns `nil` if `filters` is an empty slice.
file_dialog_build_filter_string :: proc(filters: []File_Dialog_Filter, allocator: runtime.Allocator = context.temp_allocator) -> win.LPCWSTR {

if len(filters) == 0 {
return nil
}

needed_capacity := 0
for f in filters {
needed_capacity += len(f.display_text)
needed_capacity += 1
needed_capacity += len(f.filter)
needed_capacity += 1
}
needed_capacity += 1

sb: strings.Builder
strings.builder_init_len_cap(&sb, 0, needed_capacity, context.temp_allocator)

for f in filters {
strings.write_string(&sb, f.display_text)
strings.write_string(&sb, "\x00")
strings.write_string(&sb, f.filter)
strings.write_string(&sb, "\x00")
}
strings.write_string(&sb, "\x00")

return win.utf8_to_wstring(strings.to_string(sb), allocator)
}

// Converts a file path or file name to the display name that the file explorer would
// display to the user. This takes into account system settings regarding file extensions.
get_file_display_name :: proc(file_path: string, allocator := context.temp_allocator) -> (result: string, ok: bool) #optional_ok {
file_path_wstr := win.utf8_to_wstring(file_path, context.temp_allocator)
needed := win.GetFileTitleW(file_path_wstr, nil, 0)
if needed <= 0 {
return "", false
}
buf := make([]u16, needed, context.temp_allocator)
conversion_result := win.GetFileTitleW(file_path_wstr, raw_data(buf[:]), auto_cast len(buf))
if conversion_result == 0 {
// Success.
r, err := win.wstring_to_utf8(cast(win.wstring)raw_data(buf[:]), allocator = allocator)
if err != nil {
return "", false
} else {
return r, true
}
} else {
// Conversion failure.
return "", false
}
}
Loading
Loading