diff --git a/CMakeLists.txt b/CMakeLists.txt index 9b9a2376..c45e87f5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -350,6 +350,7 @@ src/gui/font_unifont.cpp src/gui/font_icon.cpp src/gui/fonts.cpp src/gui/debug.cpp +src/gui/fileDialog.cpp src/gui/intConst.cpp src/gui/guiConst.cpp @@ -401,6 +402,9 @@ endif() if (NOT MSVC) set(WARNING_FLAGS -Wall -Wextra -Wno-unused-parameter) + if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + list(APPEND WARNING_FLAGS -Wno-cast-function-type) + endif() if (WARNINGS_ARE_ERRORS) list(APPEND WARNING_FLAGS -Werror) endif() diff --git a/README.md b/README.md index e1d97726..a1b64854 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,8 @@ some people have provided packages for Unix/Unix-like distributions. here's a li # developer info +[![Build furnace](https://github.com/tildearrow/furnace/actions/workflows/build.yml/badge.svg)](https://github.com/tildearrow/furnace/actions/workflows/build.yml) + **NOTE: do not download the project's source as a .zip or .tar.gz as these do not include the project's submodules which are necessary to proceed with building.** ## dependencies diff --git a/extern/igfd/ImGuiFileDialog.cpp b/extern/igfd/ImGuiFileDialog.cpp index 5be45793..3980bcd3 100644 --- a/extern/igfd/ImGuiFileDialog.cpp +++ b/extern/igfd/ImGuiFileDialog.cpp @@ -3877,6 +3877,7 @@ namespace IGFD static ImGuiSelectableFlags selectableFlags = ImGuiSelectableFlags_AllowDoubleClick | ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_SpanAvailWidth; + // TODO BUG?! va_list args; va_start(args, vFmt); vsnprintf(fdi.puVariadicBuffer, MAX_FILE_DIALOG_NAME_BUFFER, vFmt, args); @@ -4074,6 +4075,7 @@ namespace IGFD if (ImGui::TableNextColumn()) // file name { + // TODO BUG?!?!?! needToBreakTheloop = prSelectableItem(i, infos, selected, _str.c_str()); if (needToBreakTheloop==2) escape=true; } diff --git a/extern/pfd-fixed/.gitignore b/extern/pfd-fixed/.gitignore new file mode 100644 index 00000000..ec121a99 --- /dev/null +++ b/extern/pfd-fixed/.gitignore @@ -0,0 +1,4 @@ +CMakeCache.txt +CMakeFiles +Makefile +cmake_install.cmake diff --git a/extern/pfd-fixed/.lgtm.yml b/extern/pfd-fixed/.lgtm.yml new file mode 100644 index 00000000..b4c4985b --- /dev/null +++ b/extern/pfd-fixed/.lgtm.yml @@ -0,0 +1,5 @@ +extraction: + cpp: + index: + build_command: + - make -C examples diff --git a/extern/pfd-fixed/CMakeLists.txt b/extern/pfd-fixed/CMakeLists.txt new file mode 100644 index 00000000..3be61ae5 --- /dev/null +++ b/extern/pfd-fixed/CMakeLists.txt @@ -0,0 +1,6 @@ +cmake_minimum_required(VERSION 3.1.0) + +project(portable_file_dialogs VERSION 1.00 LANGUAGES CXX) + +add_library(${PROJECT_NAME} INTERFACE) +target_include_directories(${PROJECT_NAME} INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}) \ No newline at end of file diff --git a/extern/pfd-fixed/COPYING b/extern/pfd-fixed/COPYING new file mode 100644 index 00000000..8b014d64 --- /dev/null +++ b/extern/pfd-fixed/COPYING @@ -0,0 +1,14 @@ + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + Version 2, December 2004 + + Copyright (C) 2004 Sam Hocevar + + Everyone is permitted to copy and distribute verbatim or modified + copies of this license document, and changing it is allowed as long + as the name is changed. + + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. You just DO WHAT THE FUCK YOU WANT TO. + diff --git a/extern/pfd-fixed/README.md b/extern/pfd-fixed/README.md new file mode 100644 index 00000000..ceb36a40 --- /dev/null +++ b/extern/pfd-fixed/README.md @@ -0,0 +1,64 @@ +# Portable File Dialogs + +A free C++11 file dialog library. + +- works on Windows, Mac OS X, Linux +- **single-header**, no extra library dependencies +- **synchronous *or* asynchronous** (does not block the rest of your program!) +- **cancelable** (kill asynchronous dialogues without user interaction) +- **secure** (immune to shell-quote vulnerabilities) + +[![Codacy Badge](https://app.codacy.com/project/badge/Grade/a25d3fd6959a4333871f630ac70b6e09)](https://www.codacy.com/manual/samhocevar/portable-file-dialogs?utm_source=github.com&utm_medium=referral&utm_content=samhocevar/portable-file-dialogs&utm_campaign=Badge_Grade) + +## Status + +The library is now pretty robust. It is not as feature-complete as +[Tiny File Dialogs](https://sourceforge.net/projects/tinyfiledialogs/), +but has asynchonous dialogs, more maintainable code, and fewer potential +security issues. + +The currently available backends are: + +- Win32 API (all known versions of Windows) +- Mac OS X (using AppleScript) +- GNOME desktop (using [Zenity](https://en.wikipedia.org/wiki/Zenity) or its clones Matedialog and Qarma) +- KDE desktop (using [KDialog](https://github.com/KDE/kdialog)) + +Experimental support for Emscripten is on its way. + +## Documentation + +- [`pfd`](doc/pfd.md) general documentation +- [`pfd::message`](doc/message.md) message box +- [`pfd::notify`](doc/notify.md) notification +- [`pfd::open_file`](doc/open_file.md) file open +- [`pfd::save_file`](doc/save_file.md) file save +- [`pfd::select_folder`](doc/select_folder.md) folder selection + +## History + +- 0.1.0 (July 16, 2020): first public release + +## Screenshots (Windows 10) + +![warning-win32](https://user-images.githubusercontent.com/245089/47136607-76919a00-d2b4-11e8-8f42-e2d62c4f9570.png) +![notify-win32](https://user-images.githubusercontent.com/245089/47142453-2ff76c00-d2c3-11e8-871a-1a110ac91eb2.png) +![open-win32](https://user-images.githubusercontent.com/245089/47155865-0f8cd900-d2e6-11e8-8041-1e20b6f77dee.png) + +## Screenshots (Mac OS X, dark theme) + +![warning-osxdark](https://user-images.githubusercontent.com/245089/56053001-22dba700-5d53-11e9-8233-ca7a2c58188d.png) +![notify-osxdark](https://user-images.githubusercontent.com/245089/56053188-bc0abd80-5d53-11e9-8298-68aa96315c6c.png) +![open-osxdark](https://user-images.githubusercontent.com/245089/56053378-39363280-5d54-11e9-9583-9f1c978fa0db.png) + +## Screenshots (Linux, GNOME desktop) + +![warning-gnome](https://user-images.githubusercontent.com/245089/47136608-772a3080-d2b4-11e8-9e1d-60a7e743e908.png) +![notify-gnome](https://user-images.githubusercontent.com/245089/47142455-30900280-d2c3-11e8-8b76-ea16c7e502d4.png) +![open-gnome](https://user-images.githubusercontent.com/245089/47155867-0f8cd900-d2e6-11e8-93af-275636491ec4.png) + +## Screenshots (Linux, KDE Plasma desktop) + +![warning-kde](https://user-images.githubusercontent.com/245089/47149255-4dcccd00-d2d3-11e8-84c9-f85612784680.png) +![notify-kde](https://user-images.githubusercontent.com/245089/47149206-27a72d00-d2d3-11e8-8f1b-96e462f08c2b.png) +![open-kde](https://user-images.githubusercontent.com/245089/47155866-0f8cd900-d2e6-11e8-8006-f14b948afc55.png) diff --git a/extern/pfd-fixed/doc/message.md b/extern/pfd-fixed/doc/message.md new file mode 100644 index 00000000..e54c4477 --- /dev/null +++ b/extern/pfd-fixed/doc/message.md @@ -0,0 +1,97 @@ +## Message Box API + +Displaying a message box is done using the `pfd::message` class. It can be provided a title, a +message text, a `choice` representing which buttons need to be rendered, and an `icon` for the +message: + +```cpp +pfd::message::message(std::string const &title, + std::string const &text, + pfd::choice choice = pfd::choice::ok_cancel, + pfd::icon icon = pfd::icon::info); + +enum class pfd::choice { ok, ok_cancel, yes_no, yes_no_cancel }; + +enum class pfd::icon { info, warning, error, question }; +``` + +The pressed button is queried using `pfd::message::result()`. If the dialog box is closed by any +other means, the `pfd::button::cancel` is assumed: + +```cpp +pfd::button pfd::message::result(); + +enum class pfd::button { ok, cancel, yes, no }; +``` + +It is possible to ask the dialog box whether the user took action using the `pfd::message::ready()` +method, with an optional `timeout` argument. If the user did not press a button within `timeout` +milliseconds, the function will return `false`: + +```cpp +bool pfd::message::ready(int timeout = pfd::default_wait_timeout); +``` + +## Example 1: simple notification + +The `pfd::message` destructor waits for user action, so this operation will block until the user +closes the message box: + +```cpp +pfd::message("Problem", "An error occurred while doing things", + pfd::choice::ok, pfd::icon::error); +``` + +## Example 2: retrieving the pressed button + +Using `pfd::message::result()` will also wait for user action before returning. This operation will block and return the user choice: + +```cpp +// Ask for user opinion +auto button = pfd::message("Action requested", "Do you want to proceed with things?", + pfd::choice::yes_no, pfd::icon::question).result(); +// Do something with button… +``` + +## Example 3: asynchronous message box + +Using `pfd::message::ready()` allows the application to perform other tasks while waiting for +user input: + +```cpp +// Message box with nice message +auto box = pfd::message("Unsaved Files", "Do you want to save the current " + "document before closing the application?", + pfd::choice::yes_no_cancel, + pfd::icon::warning); + +// Do something while waiting for user input +while (!box.ready(1000)) + std::cout << "Waited 1 second for user input...\n"; + +// Act depending on the selected button +switch (box.result()) +{ + case pfd::button::yes: std::cout << "User agreed.\n"; break; + case pfd::button::no: std::cout << "User disagreed.\n"; break; + case pfd::button::cancel: std::cout << "User freaked out.\n"; break; +} +``` + +## Screenshots + +#### Windows 10 + +![warning-win32](https://user-images.githubusercontent.com/245089/47136607-76919a00-d2b4-11e8-8f42-e2d62c4f9570.png) + +#### Mac OS X + +![warning-osx-dark](https://user-images.githubusercontent.com/245089/56053001-22dba700-5d53-11e9-8233-ca7a2c58188d.png) ![warning-osx-light](https://user-images.githubusercontent.com/245089/56053055-49014700-5d53-11e9-8306-e9a03a25e044.png) + +#### Linux (GNOME desktop) + +![warning-gnome](https://user-images.githubusercontent.com/245089/47140824-8662ab80-d2bf-11e8-9c87-2742dd5b58af.png) + +#### Linux (KDE desktop) + +![warning-kde](https://user-images.githubusercontent.com/245089/47149255-4dcccd00-d2d3-11e8-84c9-f85612784680.png) diff --git a/extern/pfd-fixed/doc/notify.md b/extern/pfd-fixed/doc/notify.md new file mode 100644 index 00000000..b140e261 --- /dev/null +++ b/extern/pfd-fixed/doc/notify.md @@ -0,0 +1,40 @@ +## Notification API + +Displaying a desktop notification is done using the `pfd::notify` class. It can be provided a +title, a message text, and an `icon` for the notification style: + +```cpp +pfd::notify::notify(std::string const &title, + std::string const &text, + pfd::icon icon = pfd::icon::info); + +enum class pfd::icon { info, warning, error }; +``` + +## Example + +Displaying a notification is straightforward. Emoji are supported: + +```cpp +pfd::notify("System event", "Something might be on fire 🔥", + pfd::icon::warning); +``` + +The `pfd::notify` object needs not be kept around, letting the object clean up itself is enough. + +## Screenshots + +Windows 10: +![notify-win32](https://user-images.githubusercontent.com/245089/47142453-2ff76c00-d2c3-11e8-871a-1a110ac91eb2.png) + +Mac OS X (dark theme): +![image](https://user-images.githubusercontent.com/245089/56053188-bc0abd80-5d53-11e9-8298-68aa96315c6c.png) + +Mac OS X (light theme): +![image](https://user-images.githubusercontent.com/245089/56053137-92ea2d00-5d53-11e9-8cf2-049486c45713.png) + +Linux (GNOME desktop): +![notify-gnome](https://user-images.githubusercontent.com/245089/47142455-30900280-d2c3-11e8-8b76-ea16c7e502d4.png) + +Linux (KDE desktop): +![notify-kde](https://user-images.githubusercontent.com/245089/47149206-27a72d00-d2d3-11e8-8f1b-96e462f08c2b.png) diff --git a/extern/pfd-fixed/doc/open_file.md b/extern/pfd-fixed/doc/open_file.md new file mode 100644 index 00000000..db3158ef --- /dev/null +++ b/extern/pfd-fixed/doc/open_file.md @@ -0,0 +1,90 @@ +## File Open API + +The `pfd::open_file` class handles file opening dialogs. It can be provided a title, a starting +directory and/or pre-selected file, an optional filter for recognised file types, and an optional +flag to allow multiple selection: + +```cpp +pfd::open_file::open_file(std::string const &title, + std::string const &initial_path, + std::vector filters = { "All Files", "*" }, + pfd::opt option = pfd::opt::none); +``` + +The `option` parameter can be `pfd::opt::multiselect` to allow selecting multiple files. + +The selected files are queried using `pfd::open_file::result()`. If the user canceled the +operation, the returned list is empty: + +```cpp +std::vector pfd::open_file::result(); +``` + +It is possible to ask the file open dialog whether the user took action using the +`pfd::message::ready()` method, with an optional `timeout` argument. If the user did not validate +the dialog within `timeout` milliseconds, the function will return `false`: + +```cpp +bool pfd::open_file::ready(int timeout = pfd::default_wait_timeout); +``` + +## Example 1: simple file selection + +Using `pfd::open_file::result()` will wait for user action before returning. This operation will +block and return the user choice: + +```cpp +auto selection = pfd::open_file("Select a file").result(); +if (!selection.empty()) + std::cout << "User selected file " << selection[0] << "\n"; +``` + +## Example 2: filters + +The filter list enumerates filter names and corresponded space-separated wildcard lists. It +defaults to `{ "All Files", "*" }`, but here is how to use other options: + +```cpp +auto selection = pfd::open_file("Select a file", ".", + { "Image Files", "*.png *.jpg *.jpeg *.bmp", + "Audio Files", "*.wav *.mp3", + "All Files", "*" }, + pfd::opt::multiselect).result(); +// Do something with selection +for (auto const &filename : dialog.result()) + std::cout << "Selected file: " << filename << "\n"; +``` + +## Example 3: asynchronous file open + +Using `pfd::open_file::ready()` allows the application to perform other tasks while waiting for +user input: + +```cpp +// File open dialog +auto dialog = pfd::open_file("Select file to open"); + +// Do something while waiting for user input +while (!dialog.ready(1000)) + std::cout << "Waited 1 second for user input...\n"; + +// Act depending on the user choice +std::cout << "Number of selected files: " << dialog.result().size() << "\n"; +``` + +## Screenshots + +Windows 10: +![open-win32](https://user-images.githubusercontent.com/245089/47155865-0f8cd900-d2e6-11e8-8041-1e20b6f77dee.png) + +Mac OS X (dark theme): +![image](https://user-images.githubusercontent.com/245089/56053378-39363280-5d54-11e9-9583-9f1c978fa0db.png) + +Mac OS X (light theme): +![image](https://user-images.githubusercontent.com/245089/56053413-4fdc8980-5d54-11e9-85e3-e9e5d0e10772.png) + +Linux (GNOME desktop): +![open-gnome](https://user-images.githubusercontent.com/245089/47155867-0f8cd900-d2e6-11e8-93af-275636491ec4.png) + +Linux (KDE desktop): +![open-kde](https://user-images.githubusercontent.com/245089/47155866-0f8cd900-d2e6-11e8-8006-f14b948afc55.png) diff --git a/extern/pfd-fixed/doc/pfd.md b/extern/pfd-fixed/doc/pfd.md new file mode 100644 index 00000000..f62799ef --- /dev/null +++ b/extern/pfd-fixed/doc/pfd.md @@ -0,0 +1,120 @@ +## Portable File Dialogs documentation + +The library can be used either as a [header-only library](https://en.wikipedia.org/wiki/Header-only), +or as a [single file library](https://github.com/nothings/single_file_libs). + +### Use as header-only library + +Just include the main header file wherever needed: + +```cpp +#include "portable-file-dialogs.h" + +/* ... */ + + pfd::message::message("Hello", "This is a test"); + +/* ... */ +``` + +### Use as a single-file library + +Defining the `PFD_SKIP_IMPLEMENTATION` macro before including `portable-file-dialogs.h` will +skip all the implementation code and reduce compilation times. You still need to include the +header without the macro at least once, typically in a `pfd-impl.cpp` file. + +```cpp +// In pfd-impl.cpp +#include "portable-file-dialogs.h" +``` + +```cpp +// In all other files +#define PFD_SKIP_IMPLEMENTATION 1 +#include "portable-file-dialogs.h" +``` + +### General concepts + +Dialogs inherit from `pfd::dialog` and are created by calling their class constructor. Their +destructor will block until the window is closed by user interaction. So for instance this +will block until the end of the line: + +```cpp +pfd::message::message("Hi", "there"); +``` + +Whereas this will only block until the end of the scope, allowing the program to perform +additional operations while the dialog is open: + +```cpp +{ + auto m = pfd::message::message("Hi", "there"); + + // ... perform asynchronous operations here +} +``` + +It is possible to call `bool pfd::dialog::ready(timeout)` on the dialog in order to query its +status and perform asynchronous operations as long as the user has not interacted: + +```cpp +{ + auto m = pfd::message::message("Hi", "there"); + + while (!m.ready()) + { + // ... perform asynchronous operations here + } +} +``` + +If necessary, a dialog can be forcibly closed using `bool pfd::dialog::kill()`. Note that this +may be confusing to the user and should only be used in very specific situations. It is also not +possible to close a Windows message box that provides no _Cancel_ button. + +```cpp +{ + auto m = pfd::message::message("Hi", "there"); + + while (!m.ready()) + { + // ... perform asynchronous operations here + + if (too_much_time_has_passed()) + m.kill(); + } +} +``` + +Finally, the user response can be retrieved using `pfd::dialog::result()`. The return value of +this function depends on which dialog is being used. See their respective documentation for more +information: + + * [`pfd::message`](message.md) (message box) + * [`pfd::notify`](notify.md) (notification) + * [`pfd::open_file`](open_file.md) (file open) + * [`pfd::save_file`](save_file.md) (file save) + * [`pfd::select_folder`](select_folder.md) (folder selection) + +### Settings + +The library can be queried and configured through the `pfd::settings` class. + +```cpp +bool pfd::settings::available(); +void pfd::settings::verbose(bool value); +void pfd::settings::rescan(); +``` + +The return value of `pfd::settings::available()` indicates whether a suitable dialog backend (such +as Zenity or KDialog on Linux) has been found. If not, the library will not work and all dialog +invocations will be no-ops. The program will not crash but you should account for this situation +and add a fallback mechanism or exit gracefully. + +Calling `pfd::settings::rescan()` will force a rescan of available backends. This may change the +result of `pfd::settings::available()` if a backend was installed on the system in the meantime. +This is probably only useful for debugging purposes. + +Calling `pfd::settings::verbose(true)` may help debug the library. It will output debug information +to `std::cout` about some operations being performed. diff --git a/extern/pfd-fixed/doc/save_file.md b/extern/pfd-fixed/doc/save_file.md new file mode 100644 index 00000000..5c10badb --- /dev/null +++ b/extern/pfd-fixed/doc/save_file.md @@ -0,0 +1,73 @@ +## File Open API + +The `pfd::save_file` class handles file saving dialogs. It can be provided a title, a starting +directory and/or pre-selected file, an optional filter for recognised file types, and an optional +flag to allow multiple selection: + +```cpp +pfd::save_file::save_file(std::string const &title, + std::string const &initial_path, + std::vector filters = { "All Files", "*" }, + pfd::opt option = pfd::opt::none); +``` + +The `option` parameter can be `pfd::opt::force_overwrite` to disable a potential warning when +saving to an existing file. + +The selected file is queried using `pfd::save_file::result()`. If the user canceled the +operation, the returned file name is empty: + +```cpp +std::string pfd::save_file::result(); +``` + +It is possible to ask the file save dialog whether the user took action using the +`pfd::message::ready()` method, with an optional `timeout` argument. If the user did not validate +the dialog within `timeout` milliseconds, the function will return `false`: + +```cpp +bool pfd::save_file::ready(int timeout = pfd::default_wait_timeout); +``` + +## Example 1: simple file selection + +Using `pfd::save_file::result()` will wait for user action before returning. This operation will +block and return the user choice: + +```cpp +auto destination = pfd::save_file("Select a file").result(); +if (!destination.empty()) + std::cout << "User selected file " << destination << "\n"; +``` + +## Example 2: filters + +The filter list enumerates filter names and corresponded space-separated wildcard lists. It +defaults to `{ "All Files", "*" }`, but here is how to use other options: + +```cpp +auto destination = pfd::save_file("Select a file", ".", + { "Image Files", "*.png *.jpg *.jpeg *.bmp", + "Audio Files", "*.wav *.mp3", + "All Files", "*" }, + pfd::opt::force_overwrite).result(); +// Do something with destination +std::cout << "Selected file: " << destination << "\n"; +``` + +## Example 3: asynchronous file save + +Using `pfd::save_file::ready()` allows the application to perform other tasks while waiting for +user input: + +```cpp +// File save dialog +auto dialog = pfd::save_file("Select file to save"); + +// Do something while waiting for user input +while (!dialog.ready(1000)) + std::cout << "Waited 1 second for user input...\n"; + +// Act depending on the user choice +std::cout << "User selected file: " << dialog.result() << "\n"; +``` diff --git a/extern/pfd-fixed/doc/select_folder.md b/extern/pfd-fixed/doc/select_folder.md new file mode 100644 index 00000000..28f5f63f --- /dev/null +++ b/extern/pfd-fixed/doc/select_folder.md @@ -0,0 +1,55 @@ +## Folder Selection API + +The `pfd::select_folder` class handles folder opening dialogs. It can be provided a title, and an +optional starting directory: + +```cpp +pfd::select_folder::select_folder(std::string const &title, + std::string const &default_path = "", + pfd::opt option = pfd::opt::none); +``` + +The `option` parameter can be `pfd::opt::force_path` to force the operating system to use the +provided path. Some systems default to the most recently used path, if applicable. + +The selected folder is queried using `pfd::select_folder::result()`. If the user canceled the +operation, the returned string is empty: + +```cpp +std::string pfd::select_folder::result(); +``` + +It is possible to ask the folder selection dialog whether the user took action using the +`pfd::message::ready()` method, with an optional `timeout` argument. If the user did not validate +the dialog within `timeout` milliseconds, the function will return `false`: + +```cpp +bool pfd::select_folder::ready(int timeout = pfd::default_wait_timeout); +``` + +## Example 1: simple folder selection + +Using `pfd::select_folder::result()` will wait for user action before returning. This operation +will block and return the user choice: + +```cpp +auto selection = pfd::select_folder("Select a folder").result(); +if (!selection.empty()) + std::cout << "User selected folder " << selection << "\n"; +``` + +## Example 2: asynchronous folder open + +Using `pfd::select_folder::ready()` allows the application to perform other tasks while waiting for user input: + +```cpp +// Folder selection dialog +auto dialog = pfd::select_folder("Select folder to open"); + +// Do something while waiting for user input +while (!dialog.ready(1000)) + std::cout << "Waited 1 second for user input...\n"; + +// Act depending on the user choice +std::cout << "Selected folder: " << dialog.result() << "\n"; +``` diff --git a/extern/pfd-fixed/examples/.gitignore b/extern/pfd-fixed/examples/.gitignore new file mode 100644 index 00000000..bda6acf7 --- /dev/null +++ b/extern/pfd-fixed/examples/.gitignore @@ -0,0 +1,11 @@ +example +example.exe +kill +kill.exe + +Debug +Release +*.vcxproj.user + +.idea +cmake-build-* diff --git a/extern/pfd-fixed/examples/example.cpp b/extern/pfd-fixed/examples/example.cpp new file mode 100644 index 00000000..a216ae4b --- /dev/null +++ b/extern/pfd-fixed/examples/example.cpp @@ -0,0 +1,110 @@ +// +// Portable File Dialogs +// +// Copyright © 2018—2020 Sam Hocevar +// +// This program is free software. It comes without any warranty, to +// the extent permitted by applicable law. You can redistribute it +// and/or modify it under the terms of the Do What the Fuck You Want +// to Public License, Version 2, as published by the WTFPL Task Force. +// See http://www.wtfpl.net/ for more details. +// + +#include "portable-file-dialogs.h" + +#include + +#if _WIN32 +#define DEFAULT_PATH "C:\\" +#else +#define DEFAULT_PATH "/tmp" +#endif + +int main() +{ + // Check that a backend is available + if (!pfd::settings::available()) + { + std::cout << "Portable File Dialogs are not available on this platform.\n"; + return 1; + } + + // Set verbosity to true + pfd::settings::verbose(true); + + // Notification + pfd::notify("Important Notification", + "This is ' a message, pay \" attention \\ to it!", + pfd::icon::info); + + // Message box with nice message + auto m = pfd::message("Personal Message", + "You are an amazing person, don’t let anyone make you think otherwise.", + pfd::choice::yes_no_cancel, + pfd::icon::warning); + + // Optional: do something while waiting for user action + for (int i = 0; i < 10 && !m.ready(1000); ++i) + std::cout << "Waited 1 second for user input...\n"; + + // Do something according to the selected button + switch (m.result()) + { + case pfd::button::yes: std::cout << "User agreed.\n"; break; + case pfd::button::no: std::cout << "User disagreed.\n"; break; + case pfd::button::cancel: std::cout << "User freaked out.\n"; break; + default: break; // Should not happen + } + + // Directory selection + auto dir = pfd::select_folder("Select any directory", DEFAULT_PATH).result(); + std::cout << "Selected dir: " << dir << "\n"; + + // File open + auto f = pfd::open_file("Choose files to read", DEFAULT_PATH, + { "Text Files (.txt .text)", "*.txt *.text", + "All Files", "*" }, + pfd::opt::multiselect); + std::cout << "Selected files:"; + for (auto const &name : f.result()) + std::cout << " " + name; + std::cout << "\n"; +} + +// Unused function that just tests the whole API +void api() +{ + // pfd::settings + pfd::settings::verbose(true); + pfd::settings::rescan(); + + // pfd::notify + pfd::notify("", ""); + pfd::notify("", "", pfd::icon::info); + pfd::notify("", "", pfd::icon::warning); + pfd::notify("", "", pfd::icon::error); + pfd::notify("", "", pfd::icon::question); + + pfd::notify a("", ""); + (void)a.ready(); + (void)a.ready(42); + + // pfd::message + pfd::message("", ""); + pfd::message("", "", pfd::choice::ok); + pfd::message("", "", pfd::choice::ok_cancel); + pfd::message("", "", pfd::choice::yes_no); + pfd::message("", "", pfd::choice::yes_no_cancel); + pfd::message("", "", pfd::choice::retry_cancel); + pfd::message("", "", pfd::choice::abort_retry_ignore); + pfd::message("", "", pfd::choice::ok, pfd::icon::info); + pfd::message("", "", pfd::choice::ok, pfd::icon::warning); + pfd::message("", "", pfd::choice::ok, pfd::icon::error); + pfd::message("", "", pfd::choice::ok, pfd::icon::question); + + pfd::message b("", ""); + (void)b.ready(); + (void)b.ready(42); + (void)b.result(); +} + diff --git a/extern/pfd-fixed/examples/example.vcxproj b/extern/pfd-fixed/examples/example.vcxproj new file mode 100644 index 00000000..d7e91492 --- /dev/null +++ b/extern/pfd-fixed/examples/example.vcxproj @@ -0,0 +1,96 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + + + + + + + 15.0 + {10F4364D-27C4-4C74-8079-7C42971E81E7} + Win32Proj + example + 10.0 + + + + Application + v142 + Unicode + + + true + + + false + true + + + + + + + + + + + + true + + + false + + + + .. + NotUsing + Level3 + true + true + + + Console + true + + + + + Disabled + _DEBUG;_CONSOLE;%(PreprocessorDefinitions) + + + + + MaxSpeed + NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + true + + + true + true + + + + + + \ No newline at end of file diff --git a/extern/pfd-fixed/examples/kill.cpp b/extern/pfd-fixed/examples/kill.cpp new file mode 100644 index 00000000..787edbeb --- /dev/null +++ b/extern/pfd-fixed/examples/kill.cpp @@ -0,0 +1,42 @@ +// +// Portable File Dialogs +// +// Copyright © 2018—2020 Sam Hocevar +// +// This program is free software. It comes without any warranty, to +// the extent permitted by applicable law. You can redistribute it +// and/or modify it under the terms of the Do What the Fuck You Want +// to Public License, Version 2, as published by the WTFPL Task Force. +// See http://www.wtfpl.net/ for more details. +// + +#include "portable-file-dialogs.h" + +#include + +int main() +{ + // Set verbosity to true + pfd::settings::verbose(true); + + // Message box with nice message + auto m = pfd::message("Upgrade software?", + "Press OK to upgrade this software.\n" + "\n" + "By default, the software will update itself\n" + "automatically in 10 seconds.", + pfd::choice::ok_cancel, + pfd::icon::warning); + + // Wait for an answer for up to 10 seconds + for (int i = 0; i < 10 && !m.ready(1000); ++i) + ; + + // Upgrade software if user clicked OK, or if user didn’t interact + bool upgrade = m.ready() ? m.result() == pfd::button::ok : m.kill(); + if (upgrade) + std::cout << "Upgrading software!\n"; + else + std::cout << "Not upgrading software.\n"; +} + diff --git a/extern/pfd-fixed/examples/kill.vcxproj b/extern/pfd-fixed/examples/kill.vcxproj new file mode 100644 index 00000000..b1ee3c9c --- /dev/null +++ b/extern/pfd-fixed/examples/kill.vcxproj @@ -0,0 +1,96 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + + + + + + + 15.0 + {B94D26B1-7EF7-43A2-A973-9A96A08E2E17} + Win32Proj + kill + 10.0 + + + + Application + v142 + Unicode + + + true + + + false + true + + + + + + + + + + + + true + + + false + + + + .. + NotUsing + Level3 + true + true + + + Console + true + + + + + Disabled + _DEBUG;_CONSOLE;%(PreprocessorDefinitions) + + + + + MaxSpeed + NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + true + + + true + true + + + + + + diff --git a/extern/pfd-fixed/portable-file-dialogs.h b/extern/pfd-fixed/portable-file-dialogs.h new file mode 100644 index 00000000..41e588ac --- /dev/null +++ b/extern/pfd-fixed/portable-file-dialogs.h @@ -0,0 +1,1731 @@ +// +// Portable File Dialogs +// +// Copyright © 2018—2020 Sam Hocevar +// +// This library is free software. It comes without any warranty, to +// the extent permitted by applicable law. You can redistribute it +// and/or modify it under the terms of the Do What the Fuck You Want +// to Public License, Version 2, as published by the WTFPL Task Force. +// See http://www.wtfpl.net/ for more details. +// + +#pragma once + +#if _WIN32 +#ifndef WIN32_LEAN_AND_MEAN +# define WIN32_LEAN_AND_MEAN 1 +#endif +#include +#include +#include +#include // IFileDialog +#include +#include +#include // std::async + +#elif __EMSCRIPTEN__ +#include + +#else +#ifndef _POSIX_C_SOURCE +# define _POSIX_C_SOURCE 2 // for popen() +#endif +#ifdef __APPLE__ +# ifndef _DARWIN_C_SOURCE +# define _DARWIN_C_SOURCE +# endif +#endif +#include // popen() +#include // std::getenv() +#include // fcntl() +#include // read(), pipe(), dup2() +#include // ::kill, std::signal +#include // waitpid() +#endif + +#include // std::string +#include // std::shared_ptr +#include // std::ostream +#include // std::map +#include // std::set +#include // std::regex +#include // std::mutex, std::this_thread +#include // std::chrono + +// Versions of mingw64 g++ up to 9.3.0 do not have a complete IFileDialog +#ifndef PFD_HAS_IFILEDIALOG +# define PFD_HAS_IFILEDIALOG 1 +# if (defined __MINGW64__ || defined __MINGW32__) && defined __GXX_ABI_VERSION +# if __GXX_ABI_VERSION <= 1013 +# undef PFD_HAS_IFILEDIALOG +# define PFD_HAS_IFILEDIALOG 0 +# endif +# endif +#endif + +namespace pfd +{ + +enum class button +{ + cancel = -1, + ok, + yes, + no, + abort, + retry, + ignore, +}; + +enum class choice +{ + ok = 0, + ok_cancel, + yes_no, + yes_no_cancel, + retry_cancel, + abort_retry_ignore, +}; + +enum class icon +{ + info = 0, + warning, + error, + question, +}; + +// Additional option flags for various dialog constructors +enum class opt : uint8_t +{ + none = 0, + // For file open, allow multiselect. + multiselect = 0x1, + // For file save, force overwrite and disable the confirmation dialog. + force_overwrite = 0x2, + // For folder select, force path to be the provided argument instead + // of the last opened directory, which is the Microsoft-recommended, + // user-friendly behaviour. + force_path = 0x4, +}; + +inline opt operator |(opt a, opt b) { return opt(uint8_t(a) | uint8_t(b)); } +inline bool operator &(opt a, opt b) { return bool(uint8_t(a) & uint8_t(b)); } + +// The settings class, only exposing to the user a way to set verbose mode +// and to force a rescan of installed desktop helpers (zenity, kdialog…). +class settings +{ +public: + static bool available(); + + static void verbose(bool value); + static void rescan(); + +protected: + explicit settings(bool resync = false); + + bool check_program(std::string const &program); + + inline bool is_osascript() const; + inline bool is_zenity() const; + inline bool is_kdialog() const; + + enum class flag + { + is_scanned = 0, + is_verbose, + + has_zenity, + has_matedialog, + has_qarma, + has_kdialog, + is_vista, + + max_flag, + }; + + // Static array of flags for internal state + bool const &flags(flag in_flag) const; + + // Non-const getter for the static array of flags + bool &flags(flag in_flag); +}; + +// Internal classes, not to be used by client applications +namespace internal +{ + +// Process wait timeout, in milliseconds +static int const default_wait_timeout = 20; + +class executor +{ + friend class dialog; + +public: + // High level function to get the result of a command + std::string result(int *exit_code = nullptr); + + // High level function to abort + bool kill(); + +#if _WIN32 + void start_func(std::function const &fun); + static BOOL CALLBACK enum_windows_callback(HWND hwnd, LPARAM lParam); +#elif __EMSCRIPTEN__ + void start(int exit_code); +#else + void start_process(std::vector const &command); +#endif + + ~executor(); + +protected: + bool ready(int timeout = default_wait_timeout); + void stop(); + +private: + bool m_running = false; + std::string m_stdout; + int m_exit_code = -1; +#if _WIN32 + std::future m_future; + std::set m_windows; + std::condition_variable m_cond; + std::mutex m_mutex; + DWORD m_tid; +#elif __EMSCRIPTEN__ || __NX__ + // FIXME: do something +#else + pid_t m_pid = 0; + int m_fd = -1; +#endif +}; + +class platform +{ +protected: +#if _WIN32 + // Helper class around LoadLibraryA() and GetProcAddress() with some safety + class dll + { + public: + dll(std::string const &name); + ~dll(); + + template class proc + { + public: + proc(dll const &lib, std::string const &sym) + : m_proc(reinterpret_cast(::GetProcAddress(lib.handle, sym.c_str()))) + {} + + operator bool() const { return m_proc != nullptr; } + operator T *() const { return m_proc; } + + private: + T *m_proc; + }; + + private: + HMODULE handle; + }; + + // Helper class around CoInitialize() and CoUnInitialize() + class ole32_dll : public dll + { + public: + ole32_dll(); + ~ole32_dll(); + bool is_initialized(); + + private: + HRESULT m_state; + }; + + // Helper class around CreateActCtx() and ActivateActCtx() + class new_style_context + { + public: + new_style_context(); + ~new_style_context(); + + private: + HANDLE create(); + ULONG_PTR m_cookie = 0; + }; +#endif +}; + +class dialog : protected settings, protected platform +{ +public: + bool ready(int timeout = default_wait_timeout) const; + bool kill() const; + +protected: + explicit dialog(); + + std::vector desktop_helper() const; + static std::string buttons_to_name(choice _choice); + static std::string get_icon_name(icon _icon); + + std::string powershell_quote(std::string const &str) const; + std::string osascript_quote(std::string const &str) const; + std::string shell_quote(std::string const &str) const; + + // Keep handle to executing command + std::shared_ptr m_async; +}; + +class file_dialog : public dialog +{ +protected: + enum type + { + open, + save, + folder, + }; + + file_dialog(type in_type, + std::string const &title, + std::string const &default_path = "", + std::vector const &filters = {}, + opt options = opt::none); + +protected: + std::string string_result(); + std::vector vector_result(); + +#if _WIN32 + static int CALLBACK bffcallback(HWND hwnd, UINT uMsg, LPARAM, LPARAM pData); +#if PFD_HAS_IFILEDIALOG + std::string select_folder_vista(IFileDialog *ifd, bool force_path); +#endif + + std::wstring m_wtitle; + std::wstring m_wdefault_path; + + std::vector m_vector_result; +#endif +}; + +} // namespace internal + +// +// The notify widget +// + +class notify : public internal::dialog +{ +public: + notify(std::string const &title, + std::string const &message, + icon _icon = icon::info); +}; + +// +// The message widget +// + +class message : public internal::dialog +{ +public: + message(std::string const &title, + std::string const &text, + choice _choice = choice::ok_cancel, + icon _icon = icon::info); + + button result(); + +private: + // Some extra logic to map the exit code to button number + std::map m_mappings; +}; + +// +// The open_file, save_file, and open_folder widgets +// + +class open_file : public internal::file_dialog +{ +public: + open_file(std::string const &title, + std::string const &default_path = "", + std::vector const &filters = { "All Files", "*" }, + opt options = opt::none); + +#if defined(__has_cpp_attribute) +#if __has_cpp_attribute(deprecated) + // Backwards compatibility + [[deprecated("Use pfd::opt::multiselect instead of allow_multiselect")]] +#endif +#endif + open_file(std::string const &title, + std::string const &default_path, + std::vector const &filters, + bool allow_multiselect); + + std::vector result(); +}; + +class save_file : public internal::file_dialog +{ +public: + save_file(std::string const &title, + std::string const &default_path = "", + std::vector const &filters = { "All Files", "*" }, + opt options = opt::none); + +#if defined(__has_cpp_attribute) +#if __has_cpp_attribute(deprecated) + // Backwards compatibility + [[deprecated("Use pfd::opt::force_overwrite instead of confirm_overwrite")]] +#endif +#endif + save_file(std::string const &title, + std::string const &default_path, + std::vector const &filters, + bool confirm_overwrite); + + std::string result(); +}; + +class select_folder : public internal::file_dialog +{ +public: + select_folder(std::string const &title, + std::string const &default_path = "", + opt options = opt::none); + + std::string result(); +}; + +// +// Below this are all the method implementations. You may choose to define the +// macro PFD_SKIP_IMPLEMENTATION everywhere before including this header except +// in one place. This may reduce compilation times. +// + +#if !defined PFD_SKIP_IMPLEMENTATION + +// internal free functions implementations + +namespace internal +{ + +#if _WIN32 +static inline std::wstring str2wstr(std::string const &str) +{ + int len = MultiByteToWideChar(CP_UTF8, 0, str.c_str(), (int)str.size(), nullptr, 0); + std::wstring ret(len, '\0'); + MultiByteToWideChar(CP_UTF8, 0, str.c_str(), (int)str.size(), (LPWSTR)ret.data(), (int)ret.size()); + return ret; +} + +static inline std::string wstr2str(std::wstring const &str) +{ + int len = WideCharToMultiByte(CP_UTF8, 0, str.c_str(), (int)str.size(), nullptr, 0, nullptr, nullptr); + std::string ret(len, '\0'); + WideCharToMultiByte(CP_UTF8, 0, str.c_str(), (int)str.size(), (LPSTR)ret.data(), (int)ret.size(), nullptr, nullptr); + return ret; +} + +static inline bool is_vista() +{ + OSVERSIONINFOEXW osvi; + memset(&osvi, 0, sizeof(osvi)); + DWORDLONG const mask = VerSetConditionMask( + VerSetConditionMask( + VerSetConditionMask( + 0, VER_MAJORVERSION, VER_GREATER_EQUAL), + VER_MINORVERSION, VER_GREATER_EQUAL), + VER_SERVICEPACKMAJOR, VER_GREATER_EQUAL); + osvi.dwOSVersionInfoSize = sizeof(osvi); + osvi.dwMajorVersion = HIBYTE(_WIN32_WINNT_VISTA); + osvi.dwMinorVersion = LOBYTE(_WIN32_WINNT_VISTA); + osvi.wServicePackMajor = 0; + + return VerifyVersionInfoW(&osvi, VER_MAJORVERSION | VER_MINORVERSION | VER_SERVICEPACKMAJOR, mask) != FALSE; +} +#endif + +// This is necessary until C++20 which will have std::string::ends_with() etc. + +static inline bool ends_with(std::string const &str, std::string const &suffix) +{ + return suffix.size() <= str.size() && + str.compare(str.size() - suffix.size(), suffix.size(), suffix) == 0; +} + +static inline bool starts_with(std::string const &str, std::string const &prefix) +{ + return prefix.size() <= str.size() && + str.compare(0, prefix.size(), prefix) == 0; +} + +} // namespace internal + +// settings implementation + +inline settings::settings(bool resync) +{ + flags(flag::is_scanned) &= !resync; + + if (flags(flag::is_scanned)) + return; + +#if _WIN32 + flags(flag::is_vista) = internal::is_vista(); +#elif !__APPLE__ + flags(flag::has_zenity) = check_program("zenity"); + flags(flag::has_matedialog) = check_program("matedialog"); + flags(flag::has_qarma) = check_program("qarma"); + flags(flag::has_kdialog) = check_program("kdialog"); + + // If multiple helpers are available, try to default to the best one + if (flags(flag::has_zenity) && flags(flag::has_kdialog)) + { + auto desktop_name = std::getenv("XDG_SESSION_DESKTOP"); + if (desktop_name && desktop_name == std::string("gnome")) + flags(flag::has_kdialog) = false; + else if (desktop_name && desktop_name == std::string("KDE")) + flags(flag::has_zenity) = false; + } +#endif + + flags(flag::is_scanned) = true; +} + +inline bool settings::available() +{ +#if _WIN32 + return true; +#elif __APPLE__ + return true; +#else + settings tmp; + return tmp.flags(flag::has_zenity) || + tmp.flags(flag::has_matedialog) || + tmp.flags(flag::has_qarma) || + tmp.flags(flag::has_kdialog); +#endif +} + +inline void settings::verbose(bool value) +{ + settings().flags(flag::is_verbose) = value; +} + +inline void settings::rescan() +{ + settings(/* resync = */ true); +} + +// Check whether a program is present using “which”. +inline bool settings::check_program(std::string const &program) +{ +#if _WIN32 + (void)program; + return false; +#elif __EMSCRIPTEN__ + (void)program; + return false; +#else + int exit_code = -1; + internal::executor async; + async.start_process({"/bin/sh", "-c", "which " + program}); + async.result(&exit_code); + return exit_code == 0; +#endif +} + +inline bool settings::is_osascript() const +{ +#if __APPLE__ + return true; +#else + return false; +#endif +} + +inline bool settings::is_zenity() const +{ + return flags(flag::has_zenity) || + flags(flag::has_matedialog) || + flags(flag::has_qarma); +} + +inline bool settings::is_kdialog() const +{ + return flags(flag::has_kdialog); +} + +inline bool const &settings::flags(flag in_flag) const +{ + static bool flags[size_t(flag::max_flag)]; + return flags[size_t(in_flag)]; +} + +inline bool &settings::flags(flag in_flag) +{ + return const_cast(static_cast(this)->flags(in_flag)); +} + +// executor implementation + +inline std::string internal::executor::result(int *exit_code /* = nullptr */) +{ + stop(); + if (exit_code) + *exit_code = m_exit_code; + return m_stdout; +} + +inline bool internal::executor::kill() +{ +#if _WIN32 + if (m_future.valid()) + { + // Close all windows that weren’t open when we started the future + auto previous_windows = m_windows; + EnumWindows(&enum_windows_callback, (LPARAM)this); + for (auto hwnd : m_windows) + if (previous_windows.find(hwnd) == previous_windows.end()) + SendMessage(hwnd, WM_CLOSE, 0, 0); + } +#elif __EMSCRIPTEN__ || __NX__ + // FIXME: do something + (void)timeout; + return false; // cannot kill +#else + ::kill(m_pid, SIGKILL); +#endif + stop(); + return true; +} + +#if _WIN32 +inline BOOL CALLBACK internal::executor::enum_windows_callback(HWND hwnd, LPARAM lParam) +{ + auto that = (executor *)lParam; + + DWORD pid; + auto tid = GetWindowThreadProcessId(hwnd, &pid); + if (tid == that->m_tid) + that->m_windows.insert(hwnd); + return TRUE; +} +#endif + +#if _WIN32 +inline void internal::executor::start_func(std::function const &fun) +{ + stop(); + + auto trampoline = [fun, this]() + { + // Save our thread id so that the caller can cancel us + m_tid = GetCurrentThreadId(); + EnumWindows(&enum_windows_callback, (LPARAM)this); + m_cond.notify_all(); + return fun(&m_exit_code); + }; + + std::unique_lock lock(m_mutex); + m_future = std::async(std::launch::async, trampoline); + m_cond.wait(lock); + m_running = true; +} + +#elif __EMSCRIPTEN__ +inline void internal::executor::start(int exit_code) +{ + m_exit_code = exit_code; +} + +#else +inline void internal::executor::start_process(std::vector const &command) +{ + stop(); + m_stdout.clear(); + m_exit_code = -1; + + int in[2], out[2]; + if (pipe(in) != 0 || pipe(out) != 0) + return; + + m_pid = fork(); + if (m_pid < 0) + return; + + close(in[m_pid ? 0 : 1]); + close(out[m_pid ? 1 : 0]); + + if (m_pid == 0) + { + dup2(in[0], STDIN_FILENO); + dup2(out[1], STDOUT_FILENO); + + // Ignore stderr so that it doesn’t pollute the console (e.g. GTK+ errors from zenity) + int fd = open("/dev/null", O_WRONLY); + dup2(fd, STDERR_FILENO); + close(fd); + + std::vector args; + std::transform(command.cbegin(), command.cend(), std::back_inserter(args), + [](std::string const &s) { return const_cast(s.c_str()); }); + args.push_back(nullptr); // null-terminate argv[] + + execvp(args[0], args.data()); + exit(1); + } + + close(in[1]); + m_fd = out[0]; + auto flags = fcntl(m_fd, F_GETFL); + fcntl(m_fd, F_SETFL, flags | O_NONBLOCK); + + m_running = true; +} +#endif + +inline internal::executor::~executor() +{ + stop(); +} + +inline bool internal::executor::ready(int timeout /* = default_wait_timeout */) +{ + if (!m_running) + return true; + +#if _WIN32 + if (m_future.valid()) + { + auto status = m_future.wait_for(std::chrono::milliseconds(timeout)); + if (status != std::future_status::ready) + { + // On Windows, we need to run the message pump. If the async + // thread uses a Windows API dialog, it may be attached to the + // main thread and waiting for messages that only we can dispatch. + MSG msg; + while (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE)) + { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + return false; + } + + m_stdout = m_future.get(); + } +#elif __EMSCRIPTEN__ || __NX__ + // FIXME: do something + (void)timeout; +#else + char buf[BUFSIZ]; + ssize_t received = read(m_fd, buf, BUFSIZ); // Flawfinder: ignore + if (received > 0) + { + m_stdout += std::string(buf, received); + return false; + } + + // Reap child process if it is dead. It is possible that the system has already reaped it + // (this happens when the calling application handles or ignores SIG_CHLD) and results in + // waitpid() failing with ECHILD. Otherwise we assume the child is running and we sleep for + // a little while. + int status; + pid_t child = waitpid(m_pid, &status, WNOHANG); + if (child != m_pid && (child >= 0 || errno != ECHILD)) + { + // FIXME: this happens almost always at first iteration + std::this_thread::sleep_for(std::chrono::milliseconds(timeout)); + return false; + } + + close(m_fd); + m_exit_code = WEXITSTATUS(status); +#endif + + m_running = false; + return true; +} + +inline void internal::executor::stop() +{ + // Loop until the user closes the dialog + while (!ready()) + ; +} + +// dll implementation + +#if _WIN32 +inline internal::platform::dll::dll(std::string const &name) + : handle(::LoadLibraryA(name.c_str())) +{} + +inline internal::platform::dll::~dll() +{ + if (handle) + ::FreeLibrary(handle); +} +#endif // _WIN32 + +// ole32_dll implementation + +#if _WIN32 +inline internal::platform::ole32_dll::ole32_dll() + : dll("ole32.dll") +{ + // Use COINIT_MULTITHREADED because COINIT_APARTMENTTHREADED causes crashes. + // See https://github.com/samhocevar/portable-file-dialogs/issues/51 + auto coinit = proc(*this, "CoInitializeEx"); + m_state = coinit(nullptr, COINIT_MULTITHREADED); +} + +inline internal::platform::ole32_dll::~ole32_dll() +{ + if (is_initialized()) + proc(*this, "CoUninitialize")(); +} + +inline bool internal::platform::ole32_dll::is_initialized() +{ + return m_state == S_OK || m_state == S_FALSE; +} +#endif + +// new_style_context implementation + +#if _WIN32 +inline internal::platform::new_style_context::new_style_context() +{ + // Only create one activation context for the whole app lifetime. + static HANDLE hctx = create(); + + if (hctx != INVALID_HANDLE_VALUE) + ActivateActCtx(hctx, &m_cookie); +} + +inline internal::platform::new_style_context::~new_style_context() +{ + DeactivateActCtx(0, m_cookie); +} + +inline HANDLE internal::platform::new_style_context::create() +{ + // This “hack” seems to be necessary for this code to work on windows XP. + // Without it, dialogs do not show and close immediately. GetError() + // returns 0 so I don’t know what causes this. I was not able to reproduce + // this behavior on Windows 7 and 10 but just in case, let it be here for + // those versions too. + // This hack is not required if other dialogs are used (they load comdlg32 + // automatically), only if message boxes are used. + dll comdlg32("comdlg32.dll"); + + // Using approach as shown here: https://stackoverflow.com/a/10444161 + UINT len = ::GetSystemDirectoryA(nullptr, 0); + std::string sys_dir(len, '\0'); + ::GetSystemDirectoryA(&sys_dir[0], len); + + ACTCTXA act_ctx = + { + // Do not set flag ACTCTX_FLAG_SET_PROCESS_DEFAULT, since it causes a + // crash with error “default context is already set”. + sizeof(act_ctx), + ACTCTX_FLAG_RESOURCE_NAME_VALID | ACTCTX_FLAG_ASSEMBLY_DIRECTORY_VALID, + "shell32.dll", 0, 0, sys_dir.c_str(), (LPCSTR)124, NULL, NULL + }; + + return ::CreateActCtxA(&act_ctx); +} +#endif // _WIN32 + +// dialog implementation + +inline bool internal::dialog::ready(int timeout /* = default_wait_timeout */) const +{ + return m_async->ready(timeout); +} + +inline bool internal::dialog::kill() const +{ + return m_async->kill(); +} + +inline internal::dialog::dialog() + : m_async(std::make_shared()) +{ +} + +inline std::vector internal::dialog::desktop_helper() const +{ +#if __APPLE__ + return { "osascript" }; +#else + return { flags(flag::has_zenity) ? "zenity" + : flags(flag::has_matedialog) ? "matedialog" + : flags(flag::has_qarma) ? "qarma" + : flags(flag::has_kdialog) ? "kdialog" + : "echo" }; +#endif +} + +inline std::string internal::dialog::buttons_to_name(choice _choice) +{ + switch (_choice) + { + case choice::ok_cancel: return "okcancel"; + case choice::yes_no: return "yesno"; + case choice::yes_no_cancel: return "yesnocancel"; + case choice::retry_cancel: return "retrycancel"; + case choice::abort_retry_ignore: return "abortretryignore"; + /* case choice::ok: */ default: return "ok"; + } +} + +inline std::string internal::dialog::get_icon_name(icon _icon) +{ + switch (_icon) + { + case icon::warning: return "warning"; + case icon::error: return "error"; + case icon::question: return "question"; + // Zenity wants "information" but WinForms wants "info" + /* case icon::info: */ default: +#if _WIN32 + return "info"; +#else + return "information"; +#endif + } +} + +// THis is only used for debugging purposes +inline std::ostream& operator <<(std::ostream &s, std::vector const &v) +{ + int not_first = 0; + for (auto &e : v) + s << (not_first++ ? " " : "") << e; + return s; +} + +// Properly quote a string for Powershell: replace ' or " with '' or "" +// FIXME: we should probably get rid of newlines! +// FIXME: the \" sequence seems unsafe, too! +// XXX: this is no longer used but I would like to keep it around just in case +inline std::string internal::dialog::powershell_quote(std::string const &str) const +{ + return "'" + std::regex_replace(str, std::regex("['\"]"), "$&$&") + "'"; +} + +// Properly quote a string for osascript: replace \ or " with \\ or \" +// XXX: this also used to replace ' with \' when popen was used, but it would be +// smarter to do shell_quote(osascript_quote(...)) if this is needed again. +inline std::string internal::dialog::osascript_quote(std::string const &str) const +{ + return "\"" + std::regex_replace(str, std::regex("[\\\\\"]"), "\\$&") + "\""; +} + +// Properly quote a string for the shell: just replace ' with '\'' +// XXX: this is no longer used but I would like to keep it around just in case +inline std::string internal::dialog::shell_quote(std::string const &str) const +{ + return "'" + std::regex_replace(str, std::regex("'"), "'\\''") + "'"; +} + +// file_dialog implementation + +inline internal::file_dialog::file_dialog(type in_type, + std::string const &title, + std::string const &default_path /* = "" */, + std::vector const &filters /* = {} */, + opt options /* = opt::none */) +{ +#if _WIN32 + std::string filter_list; + std::regex whitespace(" *"); + for (size_t i = 0; i + 1 < filters.size(); i += 2) + { + filter_list += filters[i] + '\0'; + filter_list += std::regex_replace(filters[i + 1], whitespace, ";") + '\0'; + } + filter_list += '\0'; + + m_async->start_func([this, in_type, title, default_path, filter_list, + options](int *exit_code) -> std::string + { + (void)exit_code; + m_wtitle = internal::str2wstr(title); + m_wdefault_path = internal::str2wstr(default_path); + auto wfilter_list = internal::str2wstr(filter_list); + + // Initialise COM. This is required for the new folder selection window, + // (see https://github.com/samhocevar/portable-file-dialogs/pull/21) + // and to avoid random crashes with GetOpenFileNameW() (see + // https://github.com/samhocevar/portable-file-dialogs/issues/51) + ole32_dll ole32; + + // Folder selection uses a different method + if (in_type == type::folder) + { +#if PFD_HAS_IFILEDIALOG + if (flags(flag::is_vista)) + { + // On Vista and higher we should be able to use IFileDialog for folder selection + IFileDialog *ifd; + HRESULT hr = dll::proc(ole32, "CoCreateInstance") + (CLSID_FileOpenDialog, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&ifd)); + + // In case CoCreateInstance fails (which it should not), try legacy approach + if (SUCCEEDED(hr)) + return select_folder_vista(ifd, options & opt::force_path); + } +#endif + + BROWSEINFOW bi; + memset(&bi, 0, sizeof(bi)); + + bi.lpfn = &bffcallback; + bi.lParam = (LPARAM)this; + + if (flags(flag::is_vista)) + { + if (ole32.is_initialized()) + bi.ulFlags |= BIF_NEWDIALOGSTYLE; + bi.ulFlags |= BIF_EDITBOX; + bi.ulFlags |= BIF_STATUSTEXT; + } + + auto *list = SHBrowseForFolderW(&bi); + std::string ret; + if (list) + { + auto buffer = new wchar_t[MAX_PATH]; + SHGetPathFromIDListW(list, buffer); + dll::proc(ole32, "CoTaskMemFree")(list); + ret = internal::wstr2str(buffer); + delete[] buffer; + } + return ret; + } + + OPENFILENAMEW ofn; + memset(&ofn, 0, sizeof(ofn)); + ofn.lStructSize = sizeof(OPENFILENAMEW); + ofn.hwndOwner = GetActiveWindow(); + + ofn.lpstrFilter = wfilter_list.c_str(); + + auto woutput = std::wstring(MAX_PATH * 256, L'\0'); + ofn.lpstrFile = (LPWSTR)woutput.data(); + ofn.nMaxFile = (DWORD)woutput.size(); + if (!m_wdefault_path.empty()) + { + // If a directory was provided, use it as the initial directory. If + // a valid path was provided, use it as the initial file. Otherwise, + // let the Windows API decide. + auto path_attr = GetFileAttributesW(m_wdefault_path.c_str()); + if (path_attr != INVALID_FILE_ATTRIBUTES && (path_attr & FILE_ATTRIBUTE_DIRECTORY)) + ofn.lpstrInitialDir = m_wdefault_path.c_str(); + else if (m_wdefault_path.size() <= woutput.size()) + //second argument is size of buffer, not length of string + StringCchCopyW(ofn.lpstrFile, MAX_PATH*256+1, m_wdefault_path.c_str()); + else + { + ofn.lpstrFileTitle = (LPWSTR)m_wdefault_path.data(); + ofn.nMaxFileTitle = (DWORD)m_wdefault_path.size(); + } + } + ofn.lpstrTitle = m_wtitle.c_str(); + ofn.Flags = OFN_NOCHANGEDIR | OFN_EXPLORER; + + dll comdlg32("comdlg32.dll"); + + // Apply new visual style (required for windows XP) + new_style_context ctx; + + if (in_type == type::save) + { + if (!(options & opt::force_overwrite)) + ofn.Flags |= OFN_OVERWRITEPROMPT; + + dll::proc get_save_file_name(comdlg32, "GetSaveFileNameW"); + if (get_save_file_name(&ofn) == 0) + return ""; + return internal::wstr2str(woutput.c_str()); + } + else + { + if (options & opt::multiselect) + ofn.Flags |= OFN_ALLOWMULTISELECT; + ofn.Flags |= OFN_PATHMUSTEXIST; + + dll::proc get_open_file_name(comdlg32, "GetOpenFileNameW"); + if (get_open_file_name(&ofn) == 0) + return ""; + } + + std::string prefix; + for (wchar_t const *p = woutput.c_str(); *p; ) + { + auto filename = internal::wstr2str(p); + p += wcslen(p); + // In multiselect mode, we advance p one wchar further and + // check for another filename. If there is one and the + // prefix is empty, it means we just read the prefix. + if ((options & opt::multiselect) && *++p && prefix.empty()) + { + prefix = filename + "/"; + continue; + } + + m_vector_result.push_back(prefix + filename); + } + + return ""; + }); +#else + auto command = desktop_helper(); + + if (is_osascript()) + { + std::string script = "set ret to choose"; + switch (in_type) + { + case type::save: + script += " file name"; + break; + case type::open: default: + script += " file"; + if (options & opt::multiselect) + script += " with multiple selections allowed"; + break; + case type::folder: + script += " folder"; + break; + } + + if (default_path.size()) + script += " default location " + osascript_quote(default_path); + script += " with prompt " + osascript_quote(title); + + if (in_type == type::open) + { + // Concatenate all user-provided filter patterns + std::string patterns; + for (size_t i = 0; i < filters.size() / 2; ++i) + patterns += " " + filters[2 * i + 1]; + + // Split the pattern list to check whether "*" is in there; if it + // is, we have to disable filters because there is no mechanism in + // OS X for the user to override the filter. + std::regex sep("\\s+"); + std::string filter_list; + bool has_filter = true; + std::sregex_token_iterator iter(patterns.begin(), patterns.end(), sep, -1); + std::sregex_token_iterator end; + for ( ; iter != end; ++iter) + { + auto pat = iter->str(); + if (pat == "*" || pat == "*.*") + has_filter = false; + else if (internal::starts_with(pat, "*.")) + filter_list += (filter_list.size() == 0 ? "" : ",") + + osascript_quote(pat.substr(2, pat.size() - 2)); + } + if (has_filter && filter_list.size() > 0) + script += " of type {" + filter_list + "}"; + } + + if (in_type == type::open && (options & opt::multiselect)) + { + script += "\nset s to \"\""; + script += "\nrepeat with i in ret"; + script += "\n set s to s & (POSIX path of i) & \"\\n\""; + script += "\nend repeat"; + script += "\ncopy s to stdout"; + } + else + { + script += "\nPOSIX path of ret"; + } + + command.push_back("-e"); + command.push_back(script); + } + else if (is_zenity()) + { + command.push_back("--file-selection"); + command.push_back("--filename=" + default_path); + command.push_back("--title"); + command.push_back(title); + command.push_back("--separator=\n"); + + for (size_t i = 0; i < filters.size() / 2; ++i) + { + command.push_back("--file-filter"); + command.push_back(filters[2 * i] + "|" + filters[2 * i + 1]); + } + + if (in_type == type::save) + command.push_back("--save"); + if (in_type == type::folder) + command.push_back("--directory"); + if (!(options & opt::force_overwrite)) + command.push_back("--confirm-overwrite"); + if (options & opt::multiselect) + command.push_back("--multiple"); + } + else if (is_kdialog()) + { + switch (in_type) + { + case type::save: command.push_back("--getsavefilename"); break; + case type::open: command.push_back("--getopenfilename"); break; + case type::folder: command.push_back("--getexistingdirectory"); break; + } + if (options & opt::multiselect) + { + command.push_back("--multiple"); + command.push_back("--separate-output"); + } + + command.push_back(default_path); + + std::string filter; + for (size_t i = 0; i < filters.size() / 2; ++i) + filter += (i == 0 ? "" : " | ") + filters[2 * i] + "(" + filters[2 * i + 1] + ")"; + command.push_back(filter); + + command.push_back("--title"); + command.push_back(title); + } + + if (flags(flag::is_verbose)) + std::cerr << "pfd: " << command << std::endl; + + m_async->start_process(command); +#endif +} + +inline std::string internal::file_dialog::string_result() +{ +#if _WIN32 + return m_async->result(); +#else + auto ret = m_async->result(); + // Strip potential trailing newline (zenity). Also strip trailing slash + // added by osascript for consistency with other backends. + while (!ret.empty() && (ret.back() == '\n' || ret.back() == '/')) + ret.pop_back(); + return ret; +#endif +} + +inline std::vector internal::file_dialog::vector_result() +{ +#if _WIN32 + m_async->result(); + return m_vector_result; +#else + std::vector ret; + auto result = m_async->result(); + for (;;) + { + // Split result along newline characters + auto i = result.find('\n'); + if (i == 0 || i == std::string::npos) + break; + ret.push_back(result.substr(0, i)); + result = result.substr(i + 1, result.size()); + } + return ret; +#endif +} + +#if _WIN32 +// Use a static function to pass as BFFCALLBACK for legacy folder select +inline int CALLBACK internal::file_dialog::bffcallback(HWND hwnd, UINT uMsg, + LPARAM, LPARAM pData) +{ + auto inst = (file_dialog *)pData; + switch (uMsg) + { + case BFFM_INITIALIZED: + SendMessage(hwnd, BFFM_SETSELECTIONW, TRUE, (LPARAM)inst->m_wdefault_path.c_str()); + break; + } + return 0; +} + +#if PFD_HAS_IFILEDIALOG +inline std::string internal::file_dialog::select_folder_vista(IFileDialog *ifd, bool force_path) +{ + std::string result; + + IShellItem *folder; + + // Load library at runtime so app doesn't link it at load time (which will fail on windows XP) + dll shell32("shell32.dll"); + dll::proc + create_item(shell32, "SHCreateItemFromParsingName"); + + if (!create_item) + return ""; + + auto hr = create_item(m_wdefault_path.c_str(), + nullptr, + IID_PPV_ARGS(&folder)); + + // Set default folder if found. This only sets the default folder. If + // Windows has any info about the most recently selected folder, it + // will display it instead. Generally, calling SetFolder() to set the + // current directory “is not a good or expected user experience and + // should therefore be avoided”: + // https://docs.microsoft.com/windows/win32/api/shobjidl_core/nf-shobjidl_core-ifiledialog-setfolder + if (SUCCEEDED(hr)) + { + if (force_path) + ifd->SetFolder(folder); + else + ifd->SetDefaultFolder(folder); + folder->Release(); + } + + // Set the dialog title and option to select folders + ifd->SetOptions(FOS_PICKFOLDERS); + ifd->SetTitle(m_wtitle.c_str()); + + hr = ifd->Show(GetActiveWindow()); + if (SUCCEEDED(hr)) + { + IShellItem* item; + hr = ifd->GetResult(&item); + if (SUCCEEDED(hr)) + { + wchar_t* wselected = nullptr; + item->GetDisplayName(SIGDN_FILESYSPATH, &wselected); + item->Release(); + + if (wselected) + { + result = internal::wstr2str(std::wstring(wselected)); + dll::proc(ole32_dll(), "CoTaskMemFree")(wselected); + } + } + } + + ifd->Release(); + + return result; +} +#endif +#endif + +// notify implementation + +inline notify::notify(std::string const &title, + std::string const &message, + icon _icon /* = icon::info */) +{ + if (_icon == icon::question) // Not supported by notifications + _icon = icon::info; + +#if _WIN32 + // Use a static shared pointer for notify_icon so that we can delete + // it whenever we need to display a new one, and we can also wait + // until the program has finished running. + struct notify_icon_data : public NOTIFYICONDATAW + { + ~notify_icon_data() { Shell_NotifyIconW(NIM_DELETE, this); } + }; + + static std::shared_ptr nid; + + // Release the previous notification icon, if any, and allocate a new + // one. Note that std::make_shared() does value initialization, so there + // is no need to memset the structure. + nid = nullptr; + nid = std::make_shared(); + + // For XP support + nid->cbSize = NOTIFYICONDATAW_V2_SIZE; + nid->hWnd = nullptr; + nid->uID = 0; + + // Flag Description: + // - NIF_ICON The hIcon member is valid. + // - NIF_MESSAGE The uCallbackMessage member is valid. + // - NIF_TIP The szTip member is valid. + // - NIF_STATE The dwState and dwStateMask members are valid. + // - NIF_INFO Use a balloon ToolTip instead of a standard ToolTip. The szInfo, uTimeout, szInfoTitle, and dwInfoFlags members are valid. + // - NIF_GUID Reserved. + nid->uFlags = NIF_MESSAGE | NIF_ICON | NIF_INFO; + + // Flag Description + // - NIIF_ERROR An error icon. + // - NIIF_INFO An information icon. + // - NIIF_NONE No icon. + // - NIIF_WARNING A warning icon. + // - NIIF_ICON_MASK Version 6.0. Reserved. + // - NIIF_NOSOUND Version 6.0. Do not play the associated sound. Applies only to balloon ToolTips + switch (_icon) + { + case icon::warning: nid->dwInfoFlags = NIIF_WARNING; break; + case icon::error: nid->dwInfoFlags = NIIF_ERROR; break; + /* case icon::info: */ default: nid->dwInfoFlags = NIIF_INFO; break; + } + + ENUMRESNAMEPROC icon_enum_callback = [](HMODULE, LPCTSTR, LPTSTR lpName, LONG_PTR lParam) -> BOOL + { + ((NOTIFYICONDATAW *)lParam)->hIcon = ::LoadIcon(GetModuleHandle(nullptr), lpName); + return false; + }; + + nid->hIcon = ::LoadIcon(nullptr, IDI_APPLICATION); + ::EnumResourceNames(nullptr, RT_GROUP_ICON, icon_enum_callback, (LONG_PTR)nid.get()); + + nid->uTimeout = 5000; + + StringCchCopyW(nid->szInfoTitle, ARRAYSIZE(nid->szInfoTitle), internal::str2wstr(title).c_str()); + StringCchCopyW(nid->szInfo, ARRAYSIZE(nid->szInfo), internal::str2wstr(message).c_str()); + + // Display the new icon + Shell_NotifyIconW(NIM_ADD, nid.get()); +#else + auto command = desktop_helper(); + + if (is_osascript()) + { + command.push_back("-e"); + command.push_back("display notification " + osascript_quote(message) + + " with title " + osascript_quote(title)); + } + else if (is_zenity()) + { + command.push_back("--notification"); + command.push_back("--window-icon"); + command.push_back(get_icon_name(_icon)); + command.push_back("--text"); + command.push_back(title + "\n" + message); + } + else if (is_kdialog()) + { + command.push_back("--icon"); + command.push_back(get_icon_name(_icon)); + command.push_back("--title"); + command.push_back(title); + command.push_back("--passivepopup"); + command.push_back(message); + command.push_back("5"); + } + + if (flags(flag::is_verbose)) + std::cerr << "pfd: " << command << std::endl; + + m_async->start_process(command); +#endif +} + +// message implementation + +inline message::message(std::string const &title, + std::string const &text, + choice _choice /* = choice::ok_cancel */, + icon _icon /* = icon::info */) +{ +#if _WIN32 + // Use MB_SYSTEMMODAL rather than MB_TOPMOST to ensure the message window is brought + // to front. See https://github.com/samhocevar/portable-file-dialogs/issues/52 + UINT style = MB_SYSTEMMODAL; + switch (_icon) + { + case icon::warning: style |= MB_ICONWARNING; break; + case icon::error: style |= MB_ICONERROR; break; + case icon::question: style |= MB_ICONQUESTION; break; + /* case icon::info: */ default: style |= MB_ICONINFORMATION; break; + } + + switch (_choice) + { + case choice::ok_cancel: style |= MB_OKCANCEL; break; + case choice::yes_no: style |= MB_YESNO; break; + case choice::yes_no_cancel: style |= MB_YESNOCANCEL; break; + case choice::retry_cancel: style |= MB_RETRYCANCEL; break; + case choice::abort_retry_ignore: style |= MB_ABORTRETRYIGNORE; break; + /* case choice::ok: */ default: style |= MB_OK; break; + } + + m_mappings[IDCANCEL] = button::cancel; + m_mappings[IDOK] = button::ok; + m_mappings[IDYES] = button::yes; + m_mappings[IDNO] = button::no; + m_mappings[IDABORT] = button::abort; + m_mappings[IDRETRY] = button::retry; + m_mappings[IDIGNORE] = button::ignore; + + m_async->start_func([text, title, style](int* exit_code) -> std::string + { + auto wtext = internal::str2wstr(text); + auto wtitle = internal::str2wstr(title); + // Apply new visual style (required for all Windows versions) + new_style_context ctx; + *exit_code = MessageBoxW(GetActiveWindow(), wtext.c_str(), wtitle.c_str(), style); + return ""; + }); + +#elif __EMSCRIPTEN__ + std::string full_message; + switch (_icon) + { + case icon::warning: full_message = "⚠️"; break; + case icon::error: full_message = "⛔"; break; + case icon::question: full_message = "❓"; break; + /* case icon::info: */ default: full_message = "ℹ"; break; + } + + full_message += ' ' + title + "\n\n" + text; + + // This does not really start an async task; it just passes the + // EM_ASM_INT return value to a fake start() function. + m_async->start(EM_ASM_INT( + { + if ($1) + return window.confirm(UTF8ToString($0)) ? 0 : -1; + alert(UTF8ToString($0)); + return 0; + }, full_message.c_str(), _choice == choice::ok_cancel)); +#else + auto command = desktop_helper(); + + if (is_osascript()) + { + std::string script = "display dialog " + osascript_quote(text) + + " with title " + osascript_quote(title); + switch (_choice) + { + case choice::ok_cancel: + script += "buttons {\"OK\", \"Cancel\"}" + " default button \"OK\"" + " cancel button \"Cancel\""; + m_mappings[256] = button::cancel; + break; + case choice::yes_no: + script += "buttons {\"Yes\", \"No\"}" + " default button \"Yes\"" + " cancel button \"No\""; + m_mappings[256] = button::no; + break; + case choice::yes_no_cancel: + script += "buttons {\"Yes\", \"No\", \"Cancel\"}" + " default button \"Yes\"" + " cancel button \"Cancel\""; + m_mappings[256] = button::cancel; + break; + case choice::retry_cancel: + script += "buttons {\"Retry\", \"Cancel\"}" + " default button \"Retry\"" + " cancel button \"Cancel\""; + m_mappings[256] = button::cancel; + break; + case choice::abort_retry_ignore: + script += "buttons {\"Abort\", \"Retry\", \"Ignore\"}" + " default button \"Retry\"" + " cancel button \"Retry\""; + m_mappings[256] = button::cancel; + break; + case choice::ok: default: + script += "buttons {\"OK\"}" + " default button \"OK\"" + " cancel button \"OK\""; + m_mappings[256] = button::ok; + break; + } + script += " with icon "; + switch (_icon) + { + #define PFD_OSX_ICON(n) "alias ((path to library folder from system domain) as text " \ + "& \"CoreServices:CoreTypes.bundle:Contents:Resources:" n ".icns\")" + case icon::info: default: script += PFD_OSX_ICON("ToolBarInfo"); break; + case icon::warning: script += "caution"; break; + case icon::error: script += "stop"; break; + case icon::question: script += PFD_OSX_ICON("GenericQuestionMarkIcon"); break; + #undef PFD_OSX_ICON + } + + command.push_back("-e"); + command.push_back(script); + } + else if (is_zenity()) + { + switch (_choice) + { + case choice::ok_cancel: + command.insert(command.end(), { "--question", "--cancel-label=Cancel", "--ok-label=OK" }); break; + case choice::yes_no: + // Do not use standard --question because it causes “No” to return -1, + // which is inconsistent with the “Yes/No/Cancel” mode below. + command.insert(command.end(), { "--question", "--switch", "--extra-button=No", "--extra-button=Yes" }); break; + case choice::yes_no_cancel: + command.insert(command.end(), { "--question", "--switch", "--extra-button=Cancel", "--extra-button=No", "--extra-button=Yes" }); break; + case choice::retry_cancel: + command.insert(command.end(), { "--question", "--switch", "--extra-button=Cancel", "--extra-button=Retry" }); break; + case choice::abort_retry_ignore: + command.insert(command.end(), { "--question", "--switch", "--extra-button=Ignore", "--extra-button=Abort", "--extra-button=Retry" }); break; + case choice::ok: + default: + switch (_icon) + { + case icon::error: command.push_back("--error"); break; + case icon::warning: command.push_back("--warning"); break; + default: command.push_back("--info"); break; + } + } + + command.insert(command.end(), { "--title", title, + "--width=300", "--height=0", // sensible defaults + "--text", text, + "--icon-name=dialog-" + get_icon_name(_icon) }); + } + else if (is_kdialog()) + { + if (_choice == choice::ok) + { + switch (_icon) + { + case icon::error: command.push_back("--error"); break; + case icon::warning: command.push_back("--sorry"); break; + default: command.push_back("--msgbox"); break; + } + } + else + { + std::string flag = "--"; + if (_icon == icon::warning || _icon == icon::error) + flag += "warning"; + flag += "yesno"; + if (_choice == choice::yes_no_cancel) + flag += "cancel"; + command.push_back(flag); + if (_choice == choice::yes_no || _choice == choice::yes_no_cancel) + { + m_mappings[0] = button::yes; + m_mappings[256] = button::no; + } + } + + command.push_back(text); + command.push_back("--title"); + command.push_back(title); + + // Must be after the above part + if (_choice == choice::ok_cancel) + command.insert(command.end(), { "--yes-label", "OK", "--no-label", "Cancel" }); + } + + if (flags(flag::is_verbose)) + std::cerr << "pfd: " << command << std::endl; + + m_async->start_process(command); +#endif +} + +inline button message::result() +{ + int exit_code; + auto ret = m_async->result(&exit_code); + // osascript will say "button returned:Cancel\n" + // and others will just say "Cancel\n" + if (exit_code < 0 || // this means cancel + internal::ends_with(ret, "Cancel\n")) + return button::cancel; + if (internal::ends_with(ret, "OK\n")) + return button::ok; + if (internal::ends_with(ret, "Yes\n")) + return button::yes; + if (internal::ends_with(ret, "No\n")) + return button::no; + if (internal::ends_with(ret, "Abort\n")) + return button::abort; + if (internal::ends_with(ret, "Retry\n")) + return button::retry; + if (internal::ends_with(ret, "Ignore\n")) + return button::ignore; + if (m_mappings.count(exit_code) != 0) + return m_mappings[exit_code]; + return exit_code == 0 ? button::ok : button::cancel; +} + +// open_file implementation + +inline open_file::open_file(std::string const &title, + std::string const &default_path /* = "" */, + std::vector const &filters /* = { "All Files", "*" } */, + opt options /* = opt::none */) + : file_dialog(type::open, title, default_path, filters, options) +{ +} + +inline open_file::open_file(std::string const &title, + std::string const &default_path, + std::vector const &filters, + bool allow_multiselect) + : open_file(title, default_path, filters, + (allow_multiselect ? opt::multiselect : opt::none)) +{ +} + +inline std::vector open_file::result() +{ + return vector_result(); +} + +// save_file implementation + +inline save_file::save_file(std::string const &title, + std::string const &default_path /* = "" */, + std::vector const &filters /* = { "All Files", "*" } */, + opt options /* = opt::none */) + : file_dialog(type::save, title, default_path, filters, options) +{ +} + +inline save_file::save_file(std::string const &title, + std::string const &default_path, + std::vector const &filters, + bool confirm_overwrite) + : save_file(title, default_path, filters, + (confirm_overwrite ? opt::none : opt::force_overwrite)) +{ +} + +inline std::string save_file::result() +{ + return string_result(); +} + +// select_folder implementation + +inline select_folder::select_folder(std::string const &title, + std::string const &default_path /* = "" */, + opt options /* = opt::none */) + : file_dialog(type::folder, title, default_path, {}, options) +{ +} + +inline std::string select_folder::result() +{ + return string_result(); +} + +#endif // PFD_SKIP_IMPLEMENTATION + +} // namespace pfd + diff --git a/papers/doc/7-systems/ay8910.md b/papers/doc/7-systems/ay8910.md index 3dd1049f..4d13a153 100644 --- a/papers/doc/7-systems/ay8910.md +++ b/papers/doc/7-systems/ay8910.md @@ -4,6 +4,8 @@ this chip was used in several home computers (ZX Spectrum, MSX, Amstrad CPC, Ata the chip's powerful sound comes from the envelope... +AY-3-8914 variant was used in Intellivision, it's basically original AY with 4 level envelope volume per channel and different register format. + # effects - `20xx`: set channel mode. `xx` may be one of the following: diff --git a/papers/format.md b/papers/format.md index b04deac7..d5b9c67e 100644 --- a/papers/format.md +++ b/papers/format.md @@ -29,6 +29,7 @@ furthermore, an `or reserved` indicates this field is always present, but is res the format versions are: +- 66: Furnace dev66 - 65: Furnace dev65 - 64: Furnace dev64 - 63: Furnace dev63 @@ -207,7 +208,8 @@ size | description 1 | continuous vibrato (>=62) or reserved 1 | broken DAC mode (>=64) or reserved 1 | one tick cut (>=65) or reserved - 2 | reserved + 1 | instrument change allowed during porta (>=66) or reserved + 1 | reserved 4?? | pointers to instruments 4?? | pointers to wavetables 4?? | pointers to samples diff --git a/scripts/release-win32.sh b/scripts/release-win32.sh index d2c086d2..2257a595 100755 --- a/scripts/release-win32.sh +++ b/scripts/release-win32.sh @@ -1,6 +1,6 @@ #!/bin/bash # make Windows release -# this script shall be run from Linux with MinGW installed! +# this script shall be run from Arch Linux with MinGW installed! if [ ! -e /tmp/furnace ]; then ln -s "$PWD" /tmp/furnace || exit 1 @@ -14,7 +14,8 @@ fi cd win32build -i686-w64-mingw32-cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_FLAGS="-O2" -DCMAKE_CXX_FLAGS="-O2 -Wall -Wextra -Wno-unused-parameter -Werror" -DBUILD_SHARED_LIBS=OFF .. || exit 1 +# TODO: potential Arch-ism? +i686-w64-mingw32-cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_FLAGS="-O2" -DCMAKE_CXX_FLAGS="-O2 -Wall -Wextra -Wno-unused-parameter -Wno-cast-function-type -Werror" -DBUILD_SHARED_LIBS=OFF .. || exit 1 make -j8 || exit 1 i686-w64-mingw32-strip -s furnace.exe || exit 1 @@ -30,3 +31,7 @@ cp -r ../../papers papers || exit 1 cp -r ../../demos demos || exit 1 zip -r furnace.zip LICENSE.txt furnace.exe README.txt papers demos + +furName=$(git describe --tags | sed "s/v0/0/") + +mv furnace.zip furnace-"$furName"-win32.zip diff --git a/scripts/release-win64.sh b/scripts/release-win64.sh index 69bc602a..db580e09 100755 --- a/scripts/release-win64.sh +++ b/scripts/release-win64.sh @@ -1,6 +1,6 @@ #!/bin/bash # make Windows release -# this script shall be run from Linux with MinGW installed! +# this script shall be run from Arch Linux with MinGW installed! if [ ! -e /tmp/furnace ]; then ln -s "$PWD" /tmp/furnace || exit 1 @@ -14,7 +14,8 @@ fi cd winbuild -x86_64-w64-mingw32-cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_FLAGS="-O2" -DCMAKE_CXX_FLAGS="-O2 -Wall -Wextra -Wno-unused-parameter -Werror" .. || exit 1 +# TODO: potential Arch-ism? +x86_64-w64-mingw32-cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_FLAGS="-O2" -DCMAKE_CXX_FLAGS="-O2 -Wall -Wextra -Wno-unused-parameter -Wno-cast-function-type -Werror" .. || exit 1 make -j8 || exit 1 x86_64-w64-mingw32-strip -s furnace.exe || exit 1 @@ -30,3 +31,7 @@ cp -r ../../papers papers || exit 1 cp -r ../../demos demos || exit 1 zip -r furnace.zip LICENSE.txt furnace.exe README.txt papers demos + +furName=$(git describe --tags | sed "s/v0/0/") + +mv furnace.zip furnace-"$furName"-win64.zip diff --git a/src/engine/engine.h b/src/engine/engine.h index fc70c873..b75fc2f0 100644 --- a/src/engine/engine.h +++ b/src/engine/engine.h @@ -37,8 +37,8 @@ warnings+=(String("\n")+x); \ } -#define DIV_VERSION "dev65" -#define DIV_ENGINE_VERSION 65 +#define DIV_VERSION "dev66" +#define DIV_ENGINE_VERSION 66 enum DivStatusView { DIV_STATUS_NOTHING=0, @@ -69,7 +69,7 @@ enum DivHaltPositions { struct DivChannelState { std::vector delayed; - int note, oldNote, pitch, portaSpeed, portaNote; + int note, oldNote, lastIns, pitch, portaSpeed, portaNote; int volume, volSpeed, cut, rowDelay, volMax; int delayOrder, delayRow, retrigSpeed, retrigTick; int vibratoDepth, vibratoRate, vibratoPos, vibratoDir, vibratoFine; @@ -80,6 +80,7 @@ struct DivChannelState { DivChannelState(): note(-1), oldNote(-1), + lastIns(-1), pitch(0), portaSpeed(-1), portaNote(-1), diff --git a/src/engine/fileOps.cpp b/src/engine/fileOps.cpp index 3c1a9d2a..524531f8 100644 --- a/src/engine/fileOps.cpp +++ b/src/engine/fileOps.cpp @@ -142,6 +142,7 @@ bool DivEngine::loadDMF(unsigned char* file, size_t len) { ds.ignoreDuplicateSlides=true; ds.brokenDACMode=true; ds.oneTickCut=false; + ds.newInsTriggersInPorta=true; // 1.1 compat flags if (ds.version>24) { @@ -807,6 +808,9 @@ bool DivEngine::loadFur(unsigned char* file, size_t len) { if (ds.version<65) { ds.oneTickCut=false; } + if (ds.version<66) { + ds.newInsTriggersInPorta=false; + } ds.isDMF=false; reader.readS(); // reserved @@ -993,7 +997,12 @@ bool DivEngine::loadFur(unsigned char* file, size_t len) { } else { reader.readC(); } - for (int i=0; i<2; i++) reader.readC(); + if (ds.version>=66) { + ds.newInsTriggersInPorta=reader.readC(); + } else { + reader.readC(); + } + for (int i=0; i<1; i++) reader.readC(); } else { for (int i=0; i<20; i++) reader.readC(); } @@ -1437,7 +1446,8 @@ SafeWriter* DivEngine::saveFur() { w->writeC(song.continuousVibrato); w->writeC(song.brokenDACMode); w->writeC(song.oneTickCut); - for (int i=0; i<2; i++) { + w->writeC(song.newInsTriggersInPorta); + for (int i=0; i<1; i++) { w->writeC(0); } diff --git a/src/engine/platform/ay.cpp b/src/engine/platform/ay.cpp index 0777fca8..526b3fbd 100644 --- a/src/engine/platform/ay.cpp +++ b/src/engine/platform/ay.cpp @@ -24,7 +24,7 @@ #include #define rWrite(a,v) if (!skipRegisterWrites) {pendingWrites[a]=v;} -#define immWrite(a,v) if (!skipRegisterWrites) {writes.emplace(a,v); if (dumpWrites) {addWrite(a,v);} } +#define immWrite(a,v) if (!skipRegisterWrites) {writes.emplace(regRemap(a),v); if (dumpWrites) {addWrite(regRemap(a),v);} } #define CHIP_DIVIDER 8 @@ -48,8 +48,28 @@ const char* regCheatSheetAY[]={ NULL }; +const char* regCheatSheetAY8914[]={ + "FreqL_A", "0", + "FreqL_B", "1", + "FreqL_C", "2", + "FreqL_Env", "3", + "FreqH_A", "4", + "FreqH_B", "5", + "FreqH_C", "6", + "FreqH_Env", "7", + "Enable", "8", + "FreqNoise", "9", + "Control_Env", "A", + "Volume_A", "B", + "Volume_B", "C", + "Volume_C", "D", + "PortA", "E", + "PortB", "F", + NULL +}; + const char** DivPlatformAY8910::getRegisterSheet() { - return regCheatSheetAY; + return intellivision?regCheatSheetAY8914:regCheatSheetAY; } const char* DivPlatformAY8910::getEffectName(unsigned char effect) { @@ -92,8 +112,13 @@ void DivPlatformAY8910::acquire(short* bufL, short* bufR, size_t start, size_t l } while (!writes.empty()) { QueuedWrite w=writes.front(); - ay->address_w(w.addr); - ay->data_w(w.val); + if (intellivision) { + ay8914_device* ay8914=(ay8914_device*)ay; + ay8914->write(w.addr,w.val); + } else { + ay->address_w(w.addr); + ay->data_w(w.val); + } regPool[w.addr&0x0f]=w.val; writes.pop(); } @@ -125,6 +150,8 @@ void DivPlatformAY8910::tick() { if (chan[i].outVol<0) chan[i].outVol=0; if (isMuted[i]) { rWrite(0x08+i,0); + } else if (intellivision && (chan[i].psgMode&4)) { + rWrite(0x08+i,(chan[i].outVol&0xc)<<2); } else { rWrite(0x08+i,(chan[i].outVol&15)|((chan[i].psgMode&4)<<2)); } @@ -151,6 +178,8 @@ void DivPlatformAY8910::tick() { chan[i].psgMode=(chan[i].std.wave+1)&7; if (isMuted[i]) { rWrite(0x08+i,0); + } else if (intellivision && (chan[i].psgMode&4)) { + rWrite(0x08+i,(chan[i].outVol&0xc)<<2); } else { rWrite(0x08+i,(chan[i].outVol&15)|((chan[i].psgMode&4)<<2)); } @@ -242,6 +271,8 @@ int DivPlatformAY8910::dispatch(DivCommand c) { chan[c.chan].std.init(ins); if (isMuted[c.chan]) { rWrite(0x08+c.chan,0); + } else if (intellivision && (chan[c.chan].psgMode&4)) { + rWrite(0x08+c.chan,(chan[c.chan].vol&0xc)<<2); } else { rWrite(0x08+c.chan,(chan[c.chan].vol&15)|((chan[c.chan].psgMode&4)<<2)); } @@ -264,7 +295,13 @@ int DivPlatformAY8910::dispatch(DivCommand c) { if (isMuted[c.chan]) { rWrite(0x08+c.chan,0); } else { - if (chan[c.chan].active) rWrite(0x08+c.chan,(chan[c.chan].vol&15)|((chan[c.chan].psgMode&4)<<2)); + if (chan[c.chan].active) { + if (intellivision && (chan[c.chan].psgMode&4)) { + rWrite(0x08+c.chan,(chan[c.chan].vol&0xc)<<2); + } else { + rWrite(0x08+c.chan,(chan[c.chan].vol&15)|((chan[c.chan].psgMode&4)<<2)); + } + } } break; } @@ -317,7 +354,11 @@ int DivPlatformAY8910::dispatch(DivCommand c) { if (isMuted[c.chan]) { rWrite(0x08+c.chan,0); } else if (chan[c.chan].active) { - rWrite(0x08+c.chan,(chan[c.chan].outVol&15)|((chan[c.chan].psgMode&4)<<2)); + if (intellivision && (chan[c.chan].psgMode&4)) { + rWrite(0x08+c.chan,(chan[c.chan].outVol&0xc)<<2); + } else { + rWrite(0x08+c.chan,(chan[c.chan].outVol&15)|((chan[c.chan].psgMode&4)<<2)); + } } } break; @@ -334,6 +375,8 @@ int DivPlatformAY8910::dispatch(DivCommand c) { } if (isMuted[c.chan]) { rWrite(0x08+c.chan,0); + } else if (intellivision && (chan[c.chan].psgMode&4)) { + rWrite(0x08+c.chan,(chan[c.chan].vol&0xc)<<2); } else { rWrite(0x08+c.chan,(chan[c.chan].vol&15)|((chan[c.chan].psgMode&4)<<2)); } @@ -383,6 +426,8 @@ void DivPlatformAY8910::muteChannel(int ch, bool mute) { isMuted[ch]=mute; if (isMuted[ch]) { rWrite(0x08+ch,0); + } else if (intellivision && (chan[ch].psgMode&4)) { + rWrite(0x08+ch,(chan[ch].vol&0xc)<<2); } else { rWrite(0x08+ch,(chan[ch].outVol&15)|((chan[ch].psgMode&4)<<2)); } @@ -508,14 +553,22 @@ void DivPlatformAY8910::setFlags(unsigned int flags) { case 1: ay=new ym2149_device(rate); sunsoft=false; + intellivision=false; break; case 2: ay=new sunsoft_5b_sound_device(rate); sunsoft=true; + intellivision=false; + break; + case 3: + ay=new ay8914_device(rate); + sunsoft=false; + intellivision=true; break; default: ay=new ay8910_device(rate); sunsoft=false; + intellivision=false; break; } ay->device_start(); diff --git a/src/engine/platform/ay.h b/src/engine/platform/ay.h index c2292990..41c3c404 100644 --- a/src/engine/platform/ay.h +++ b/src/engine/platform/ay.h @@ -26,6 +26,10 @@ class DivPlatformAY8910: public DivDispatch { protected: + const unsigned char AY8914RegRemap[16]={ + 0,4,1,5,2,6,9,8,11,12,13,3,7,10,14,15 + }; + inline unsigned char regRemap(unsigned char reg) { return intellivision?AY8914RegRemap[reg&0x0f]:reg&0x0f; } struct Channel { unsigned char freqH, freqL; int freq, baseFreq, note, pitch; @@ -60,7 +64,7 @@ class DivPlatformAY8910: public DivDispatch { int delay; bool extMode; - bool stereo, sunsoft; + bool stereo, sunsoft, intellivision; short oldWrites[16]; short pendingWrites[16]; diff --git a/src/engine/platform/genesis.cpp b/src/engine/platform/genesis.cpp index b3096e72..75c54af6 100644 --- a/src/engine/platform/genesis.cpp +++ b/src/engine/platform/genesis.cpp @@ -92,7 +92,7 @@ void DivPlatformGenesis::acquire_nuked(short* bufL, short* bufR, size_t start, s DivSample* s=parent->getSample(dacSample); if (s->samples>0) { if (!isMuted[5]) { - immWrite(0x2a,(unsigned char)s->data8[dacPos]+0x80); + urgentWrite(0x2a,(unsigned char)s->data8[dacPos]+0x80); } if (++dacPos>=s->samples) { if (s->loopStart>=0 && s->loopStart<=(int)s->samples) { @@ -121,7 +121,7 @@ void DivPlatformGenesis::acquire_nuked(short* bufL, short* bufR, size_t start, s //printf("write: %x = %.2x\n",w.addr,w.val); lastBusy=0; regPool[w.addr&0x1ff]=w.val; - writes.pop(); + writes.pop_front(); } else { lastBusy++; if (fm.write_busy==0) { @@ -159,7 +159,7 @@ void DivPlatformGenesis::acquire_ymfm(short* bufL, short* bufR, size_t start, si DivSample* s=parent->getSample(dacSample); if (s->samples>0) { if (!isMuted[5]) { - immWrite(0x2a,(unsigned char)s->data8[dacPos]+0x80); + urgentWrite(0x2a,(unsigned char)s->data8[dacPos]+0x80); } if (++dacPos>=s->samples) { if (s->loopStart>=0 && s->loopStart<=(int)s->samples) { @@ -184,7 +184,7 @@ void DivPlatformGenesis::acquire_ymfm(short* bufL, short* bufR, size_t start, si fm_ymfm->write(0x0+((w.addr>>8)<<1),w.addr); fm_ymfm->write(0x1+((w.addr>>8)<<1),w.val); regPool[w.addr&0x1ff]=w.val; - writes.pop(); + writes.pop_front(); lastBusy=1; } @@ -782,7 +782,7 @@ int DivPlatformGenesis::getRegisterPoolSize() { } void DivPlatformGenesis::reset() { - while (!writes.empty()) writes.pop(); + while (!writes.empty()) writes.pop_front(); memset(regPool,0,512); if (useYMFM) { fm_ymfm->reset(); diff --git a/src/engine/platform/genesis.h b/src/engine/platform/genesis.h index cd7b0396..095744df 100644 --- a/src/engine/platform/genesis.h +++ b/src/engine/platform/genesis.h @@ -20,7 +20,7 @@ #ifndef _GENESIS_H #define _GENESIS_H #include "../dispatch.h" -#include +#include #include "../../../extern/Nuked-OPN2/ym3438.h" #include "sound/ymfm/ymfm_opn.h" @@ -68,7 +68,7 @@ class DivPlatformGenesis: public DivDispatch { bool addrOrVal; QueuedWrite(unsigned short a, unsigned char v): addr(a), val(v), addrOrVal(false) {} }; - std::queue writes; + std::deque writes; ym3438_t fm; int delay; unsigned char lastBusy; diff --git a/src/engine/platform/genesisshared.h b/src/engine/platform/genesisshared.h index d2402a60..01d45f95 100644 --- a/src/engine/platform/genesisshared.h +++ b/src/engine/platform/genesisshared.h @@ -43,6 +43,7 @@ static int orderedOps[4]={ }; #define rWrite(a,v) if (!skipRegisterWrites) {pendingWrites[a]=v;} -#define immWrite(a,v) if (!skipRegisterWrites) {writes.emplace(a,v); if (dumpWrites) {addWrite(a,v);} } +#define immWrite(a,v) if (!skipRegisterWrites) {writes.push_back(QueuedWrite(a,v)); if (dumpWrites) {addWrite(a,v);} } +#define urgentWrite(a,v) if (!skipRegisterWrites) {if (writes.front().addrOrVal) {writes.push_back(QueuedWrite(a,v));} else {writes.push_front(QueuedWrite(a,v));}; if (dumpWrites) {addWrite(a,v);} } #include "fmshared_OPN.h" diff --git a/src/engine/platform/lynx.cpp b/src/engine/platform/lynx.cpp index 872cd99c..dc0df8a6 100644 --- a/src/engine/platform/lynx.cpp +++ b/src/engine/platform/lynx.cpp @@ -235,7 +235,7 @@ int DivPlatformLynx::dispatch(DivCommand c) { } break; case DIV_CMD_PANNING: - chan[c.chan].pan=((c.value&0x0f)<<4)|((c.value&0xf0)>>4); + chan[c.chan].pan=c.value; WRITE_ATTEN(c.chan,chan[c.chan].pan); break; case DIV_CMD_GET_VOLUME: diff --git a/src/engine/platform/nes.cpp b/src/engine/platform/nes.cpp index 897d8e84..06fbc725 100644 --- a/src/engine/platform/nes.cpp +++ b/src/engine/platform/nes.cpp @@ -201,6 +201,7 @@ void DivPlatformNES::tick() { } else { chan[i].freq=parent->calcFreq(chan[i].baseFreq,chan[i].pitch,true)-1; if (chan[i].freq>2047) chan[i].freq=2047; + if (chan[i].freq<0) chan[i].freq=0; } if (chan[i].keyOn) { //rWrite(16+i*5+1,((chan[i].duty&3)<<6)|(63-(ins->gb.soundLen&63))); diff --git a/src/engine/platform/opl.cpp b/src/engine/platform/opl.cpp index d8426553..84e8dd26 100644 --- a/src/engine/platform/opl.cpp +++ b/src/engine/platform/opl.cpp @@ -222,7 +222,7 @@ void DivPlatformOPL::acquire(short* bufL, short* bufR, size_t start, size_t len) } void DivPlatformOPL::tick() { - for (int i=0; i<20; i++) { + for (int i=0; icalcFreq(chan[i].baseFreq,chan[i].pitch,false,octave(chan[i].baseFreq)); if (chan[i].freq>131071) chan[i].freq=131071; @@ -368,12 +380,21 @@ void DivPlatformOPL::tick() { chan[i].freqH=freqt>>8; chan[i].freqL=freqt&0xff; immWrite(chanMap[i]+ADDR_FREQ,chan[i].freqL); + if (chan[i].state.ops==4 && i<6) { + immWrite(chanMap[i+1]+ADDR_FREQ,chan[i].freqL); + } } if (chan[i].keyOn) { immWrite(chanMap[i]+ADDR_FREQH,chan[i].freqH|(0x20)); + if (chan[i].state.ops==4 && i<6) { + immWrite(chanMap[i+1]+ADDR_FREQH,chan[i].freqH|(0x20)); + } chan[i].keyOn=false; } else if (chan[i].freqChanged) { immWrite(chanMap[i]+ADDR_FREQH,chan[i].freqH|(chan[i].active<<5)); + if (chan[i].state.ops==4 && i<6) { + immWrite(chanMap[i+1]+ADDR_FREQH,chan[i].freqH|(chan[i].active<<5)); + } } chan[i].freqChanged=false; } @@ -424,25 +445,42 @@ int DivPlatformOPL::toFreq(int freq) { void DivPlatformOPL::muteChannel(int ch, bool mute) { isMuted[ch]=mute; - /* - for (int j=0; j<4; j++) { - unsigned short baseAddr=chanOffs[ch]|opOffs[j]; - DivInstrumentFM::Operator& op=chan[ch].state.op[j]; + int ops=(slots[3][ch]!=255 && chan[ch].state.ops==4 && oplType==3)?4:2; + chan[ch].fourOp=(ops==4); + update4OpMask=true; + for (int i=0; i>1)&1)|(chan[ch].state.fb<<1)); + } + } else { + rWrite(chanMap[ch]+ADDR_LR_FB_ALG,(chan[ch].state.alg&1)|(chan[ch].state.fb<<1)|((chan[ch].pan&3)<<4)); + if (ops==4) { + rWrite(chanMap[ch+1]+ADDR_LR_FB_ALG,((chan[ch].state.alg>>1)&1)|(chan[ch].state.fb<<1)|((chan[ch].pan&3)<<4)); + } + } } int DivPlatformOPL::dispatch(DivCommand c) { + // TODO: drums mode! + if (c.chan>=melodicChans) return 0; switch (c.cmd) { case DIV_CMD_NOTE_ON: { DivInstrument* ins=parent->getIns(chan[c.chan].ins); @@ -456,7 +494,9 @@ int DivPlatformOPL::dispatch(DivCommand c) { chan[c.chan].outVol=chan[c.chan].vol; } if (chan[c.chan].insChanged) { - int ops=(slots[3][c.chan]!=255 && ins->fm.ops==4 && oplType==3)?4:2; + int ops=(slots[3][c.chan]!=255 && chan[c.chan].state.ops==4 && oplType==3)?4:2; + chan[c.chan].fourOp=(ops==4); + update4OpMask=true; for (int i=0; i0)<<1)|((c.value>>4)>0); + } + int ops=(slots[3][c.chan]!=255 && chan[c.chan].state.ops==4 && oplType==3)?4:2; + if (isMuted[c.chan]) { + rWrite(chanMap[c.chan]+ADDR_LR_FB_ALG,(chan[c.chan].state.alg&1)|(chan[c.chan].state.fb<<1)); + if (ops==4) { + rWrite(chanMap[c.chan+1]+ADDR_LR_FB_ALG,((chan[c.chan].state.alg>>1)&1)|(chan[c.chan].state.fb<<1)); + } + } else { + rWrite(chanMap[c.chan]+ADDR_LR_FB_ALG,(chan[c.chan].state.alg&1)|(chan[c.chan].state.fb<<1)|((chan[c.chan].pan&3)<<4)); + if (ops==4) { + rWrite(chanMap[c.chan+1]+ADDR_LR_FB_ALG,((chan[c.chan].state.alg>>1)&1)|(chan[c.chan].state.fb<<1)|((chan[c.chan].pan&3)<<4)); + } } //rWrite(chanOffs[c.chan]+ADDR_LRAF,(isMuted[c.chan]?0:(chan[c.chan].pan<<6))|(chan[c.chan].state.fms&7)|((chan[c.chan].state.ams&3)<<4)); break; @@ -683,39 +731,46 @@ int DivPlatformOPL::dispatch(DivCommand c) { } void DivPlatformOPL::forceIns() { - /* - for (int i=0; i<20; i++) { - for (int j=0; j<4; j++) { - unsigned short baseAddr=chanOffs[i]|opOffs[j]; - DivInstrumentFM::Operator& op=chan[i].state.op[j]; + for (int i=0; i1) { + rWrite(baseAddr+ADDR_WS,op.ws&((oplType==3)?7:3)); + } } - rWrite(chanOffs[i]+ADDR_FB_ALG,(chan[i].state.alg&7)|(chan[i].state.fb<<3)); - rWrite(chanOffs[i]+ADDR_LRAF,(isMuted[i]?0:(chan[i].pan<<6))|(chan[i].state.fms&7)|((chan[i].state.ams&3)<<4)); - if (chan[i].active) { - chan[i].keyOn=true; - chan[i].freqChanged=true; + + if (isMuted[i]) { + rWrite(chanMap[i]+ADDR_LR_FB_ALG,(chan[i].state.alg&1)|(chan[i].state.fb<<1)); + if (ops==4) { + rWrite(chanMap[i+1]+ADDR_LR_FB_ALG,((chan[i].state.alg>>1)&1)|(chan[i].state.fb<<1)); + } + } else { + rWrite(chanMap[i]+ADDR_LR_FB_ALG,(chan[i].state.alg&1)|(chan[i].state.fb<<1)|((chan[i].pan&3)<<4)); + if (ops==4) { + rWrite(chanMap[i+1]+ADDR_LR_FB_ALG,((chan[i].state.alg>>1)&1)|(chan[i].state.fb<<1)|((chan[i].pan&3)<<4)); + } } } - if (dacMode) { - rWrite(0x2b,0x80); - } - immWrite(0x22,lfoValue); - */ + update4OpMask=true; } void DivPlatformOPL::toggleRegisterDump(bool enable) { @@ -746,7 +801,7 @@ void DivPlatformOPL::reset() { if (dumpWrites) { addWrite(0xffffffff,0); } - for (int i=0; i<20; i++) { + for (int i=0; i> 2; + mAttenuationRight[idx] = ( value & 0x0f ) << 2; + mAttenuationLeft[idx] = ( value & 0xf0 ) >> 2; break; case MPAN: mPan = value; diff --git a/src/engine/playback.cpp b/src/engine/playback.cpp index a30603fc..70af360f 100644 --- a/src/engine/playback.cpp +++ b/src/engine/playback.cpp @@ -663,6 +663,12 @@ void DivEngine::processRow(int i, bool afterDelay) { // instrument if (pat->data[whatRow][2]!=-1) { dispatchCmd(DivCommand(DIV_CMD_INSTRUMENT,i,pat->data[whatRow][2])); + if (chan[i].lastIns!=pat->data[whatRow][2]) { + chan[i].lastIns=pat->data[whatRow][2]; + if (chan[i].inPorta && song.newInsTriggersInPorta) { + dispatchCmd(DivCommand(DIV_CMD_NOTE_ON,i,DIV_NOTE_NULL)); + } + } } // note if (pat->data[whatRow][0]==100) { // note off diff --git a/src/engine/song.h b/src/engine/song.h index c0db8d01..130c9a91 100644 --- a/src/engine/song.h +++ b/src/engine/song.h @@ -298,6 +298,7 @@ struct DivSong { bool continuousVibrato; bool brokenDACMode; bool oneTickCut; + bool newInsTriggersInPorta; DivOrders orders; std::vector ins; @@ -364,7 +365,8 @@ struct DivSong { stopPortaOnNoteOff(false), continuousVibrato(false), brokenDACMode(false), - oneTickCut(false) { + oneTickCut(false), + newInsTriggersInPorta(true) { for (int i=0; i<32; i++) { system[i]=DIV_SYSTEM_NULL; systemVol[i]=64; diff --git a/src/engine/sysDef.cpp b/src/engine/sysDef.cpp index bb743f96..c23343d7 100644 --- a/src/engine/sysDef.cpp +++ b/src/engine/sysDef.cpp @@ -421,10 +421,6 @@ const char* DivEngine::getSongSystemName() { return "Vectrex"; case 5: // AY-3-8910, 1MHz return "Amstrad CPC"; - case 6: // AY-3-8910, 0.somethingMhz - return "Intellivision"; - case 8: // AY-3-8910, 0.somethingMhz - return "Intellivision (PAL)"; case 0x10: // YM2149, 1.79MHz return "MSX"; @@ -434,7 +430,12 @@ const char* DivEngine::getSongSystemName() { return "Sunsoft 5B standalone"; case 0x28: // 5B PAL return "Sunsoft 5B standalone (PAL)"; - + + case 0x30: // AY-3-8914, 1.79MHz + return "Intellivision"; + case 0x33: // AY-3-8914, 2MHz + return "Intellivision (PAL)"; + default: if ((song.systemFlags[0]&0x30)==0x00) { return "AY-3-8910"; @@ -442,6 +443,8 @@ const char* DivEngine::getSongSystemName() { return "Yamaha YM2149"; } else if ((song.systemFlags[0]&0x30)==0x20) { return "Overclocked Sunsoft 5B"; + } else if ((song.systemFlags[0]&0x30)==0x30) { + return "Intellivision"; } } } else if (song.system[0]==DIV_SYSTEM_SMS) { diff --git a/src/gui/fileDialog.cpp b/src/gui/fileDialog.cpp new file mode 100644 index 00000000..79a87f3e --- /dev/null +++ b/src/gui/fileDialog.cpp @@ -0,0 +1,106 @@ +#include "fileDialog.h" +#include "ImGuiFileDialog.h" +#include "../ta-log.h" + +#include "../../extern/pfd-fixed/portable-file-dialogs.h" + +bool FurnaceGUIFileDialog::openLoad(String header, std::vector filter, const char* noSysFilter, String path, double dpiScale) { + if (opened) return false; + saving=false; + curPath=path; + if (sysDialog) { + dialogO=new pfd::open_file(header,path,filter); + } else { + ImGuiFileDialog::Instance()->DpiScale=dpiScale; + ImGuiFileDialog::Instance()->OpenModal("FileDialog",header,noSysFilter,path); + } + opened=true; + return true; +} + +bool FurnaceGUIFileDialog::openSave(String header, std::vector filter, const char* noSysFilter, String path, double dpiScale) { + if (opened) return false; + saving=true; + curPath=path; + if (sysDialog) { + dialogS=new pfd::save_file(header,path,filter); + } else { + ImGuiFileDialog::Instance()->DpiScale=dpiScale; + ImGuiFileDialog::Instance()->OpenModal("FileDialog",header,noSysFilter,path,1,nullptr,ImGuiFileDialogFlags_ConfirmOverwrite); + } + opened=true; + return true; +} + +bool FurnaceGUIFileDialog::accepted() { + if (sysDialog) { + return (fileName!=""); + } else { + return ImGuiFileDialog::Instance()->IsOk(); + } +} + +void FurnaceGUIFileDialog::close() { + if (sysDialog) { + if (saving) { + if (dialogS!=NULL) { + delete dialogS; + dialogS=NULL; + } + } else { + if (dialogO!=NULL) { + delete dialogO; + dialogO=NULL; + } + } + } else { + ImGuiFileDialog::Instance()->Close(); + } + opened=false; +} + +bool FurnaceGUIFileDialog::render(const ImVec2& min, const ImVec2& max) { + if (sysDialog) { + if (saving) { + if (dialogS!=NULL) { + if (dialogS->ready(1)) { + fileName=dialogS->result(); + logD("returning %s\n",fileName.c_str()); + return true; + } + } + } else { + if (dialogO!=NULL) { + if (dialogO->ready(1)) { + if (dialogO->result().empty()) { + fileName=""; + logD("returning nothing\n"); + } else { + fileName=dialogO->result()[0]; + logD("returning %s\n",fileName.c_str()); + } + return true; + } + } + } + return false; + } else { + return ImGuiFileDialog::Instance()->Display("FileDialog",ImGuiWindowFlags_NoCollapse|ImGuiWindowFlags_NoMove,min,max); + } +} + +String FurnaceGUIFileDialog::getPath() { + if (sysDialog) { + return curPath; + } else { + return ImGuiFileDialog::Instance()->GetCurrentPath(); + } +} + +String FurnaceGUIFileDialog::getFileName() { + if (sysDialog) { + return fileName; + } else { + return ImGuiFileDialog::Instance()->GetFilePathName(); + } +} diff --git a/src/gui/fileDialog.h b/src/gui/fileDialog.h new file mode 100644 index 00000000..b7f21abf --- /dev/null +++ b/src/gui/fileDialog.h @@ -0,0 +1,32 @@ +#include "../ta-utils.h" +#include "imgui.h" +#include + +namespace pfd { + class open_file; + class save_file; +} + +class FurnaceGUIFileDialog { + bool sysDialog; + bool opened; + bool saving; + String curPath; + String fileName; + pfd::open_file* dialogO; + pfd::save_file* dialogS; + public: + bool openLoad(String header, std::vector filter, const char* noSysFilter, String path, double dpiScale); + bool openSave(String header, std::vector filter, const char* noSysFilter, String path, double dpiScale); + bool accepted(); + void close(); + bool render(const ImVec2& min, const ImVec2& max); + String getPath(); + String getFileName(); + FurnaceGUIFileDialog(bool system): + sysDialog(system), + opened(false), + saving(false), + dialogO(NULL), + dialogS(NULL) {} +}; diff --git a/src/gui/gui.cpp b/src/gui/gui.cpp index 9c9368f9..2d58615d 100644 --- a/src/gui/gui.cpp +++ b/src/gui/gui.cpp @@ -32,6 +32,7 @@ #include "ImGuiFileDialog.h" #include "IconsFontAwesome4.h" #include "misc/cpp/imgui_stdlib.h" +#include "plot_nolerp.h" #include "guiConst.h" #include "intConst.h" #include @@ -1090,6 +1091,13 @@ void FurnaceGUI::drawInsList() { } ImGui::Separator(); if (ImGui::BeginTable("InsListScroll",1,ImGuiTableFlags_ScrollY)) { + if (settings.unifiedDataView) { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Text(ICON_FA_TASKS " Instruments"); + ImGui::Indent(); + } + for (int i=0; i<(int)e->song.ins.size(); i++) { DivInstrument* ins=e->song.ins[i]; String name; @@ -1214,12 +1222,32 @@ void FurnaceGUI::drawInsList() { } ImGui::PopStyleColor(); if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("%s",(ins->type>DIV_INS_MAX)?"Unknown":insTypes[ins->type]); if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) { insEditOpen=true; nextWindow=GUI_WINDOW_INS_EDIT; } } } + + if (settings.unifiedDataView) { + ImGui::Unindent(); + + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Text(ICON_FA_AREA_CHART " Wavetables"); + ImGui::Indent(); + actualWaveList(); + ImGui::Unindent(); + + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Text(ICON_FA_VOLUME_UP " Samples"); + ImGui::Indent(); + actualSampleList(); + ImGui::Unindent(); + } + ImGui::EndTable(); } } @@ -1231,6 +1259,47 @@ const char* sampleNote[12]={ "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B" }; + +void FurnaceGUI::actualWaveList() { + float wavePreview[256]; + for (int i=0; i<(int)e->song.wave.size(); i++) { + DivWavetable* wave=e->song.wave[i]; + for (int i=0; ilen; i++) { + wavePreview[i]=wave->data[i]; + } + if (wave->len>0) wavePreview[wave->len]=wave->data[wave->len-1]; + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + if (ImGui::Selectable(fmt::sprintf("%d##_WAVE%d\n",i,i).c_str(),curWave==i)) { + curWave=i; + } + if (ImGui::IsItemHovered()) { + if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) { + waveEditOpen=true; + } + } + ImGui::SameLine(); + PlotNoLerp(fmt::sprintf("##_WAVEP%d",i).c_str(),wavePreview,wave->len+1,0,NULL,0,wave->max); + } +} + +void FurnaceGUI::actualSampleList() { + for (int i=0; i<(int)e->song.sample.size(); i++) { + DivSample* sample=e->song.sample[i]; + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + if (ImGui::Selectable(fmt::sprintf("%d: %s##_SAM%d",i,sample->name,i).c_str(),curSample==i)) { + curSample=i; + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Bank %d: %s",i/12,sampleNote[i%12]); + if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) { + sampleEditOpen=true; + } + } + } +} + void FurnaceGUI::drawSampleList() { if (nextWindow==GUI_WINDOW_SAMPLE_LIST) { sampleListOpen=true; @@ -1272,24 +1341,7 @@ void FurnaceGUI::drawSampleList() { } ImGui::Separator(); if (ImGui::BeginTable("SampleListScroll",1,ImGuiTableFlags_ScrollY)) { - for (int i=0; i<(int)e->song.sample.size(); i++) { - DivSample* sample=e->song.sample[i]; - ImGui::TableNextRow(); - ImGui::TableNextColumn(); - if ((i%12)==0) { - if (i>0) ImGui::Unindent(); - ImGui::Text("Bank %d",i/12); - ImGui::Indent(); - } - if (ImGui::Selectable(fmt::sprintf("%s: %s##_SAM%d",sampleNote[i%12],sample->name,i).c_str(),curSample==i)) { - curSample=i; - } - if (ImGui::IsItemHovered()) { - if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) { - sampleEditOpen=true; - } - } - } + actualSampleList(); ImGui::EndTable(); } ImGui::Unindent(); @@ -2164,6 +2216,10 @@ void FurnaceGUI::drawCompatFlags() { if (ImGui::IsItemHovered()) { ImGui::SetTooltip("behavior changed in 0.6"); } + ImGui::Checkbox("Allow instrument change during slides",&e->song.newInsTriggersInPorta); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("behavior changed in 0.6"); + } } if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) curWindow=GUI_WINDOW_COMPAT_FLAGS; ImGui::End(); @@ -3330,6 +3386,31 @@ void FurnaceGUI::doFlip() { finishSelection(); prepareUndo(GUI_UNDO_PATTERN_FLIP); + DivPattern patBuffer; + int iCoarse=selStart.xCoarse; + int iFine=selStart.xFine; + int ord=e->getOrder(); + for (; iCoarse<=selEnd.xCoarse; iCoarse++) { + if (!e->song.chanShow[iCoarse]) continue; + DivPattern* pat=e->song.pat[iCoarse].getPattern(e->song.orders.ord[iCoarse][ord],true); + for (; iFine<3+e->song.pat[iCoarse].effectRows*2 && (iCoarsedata[j][0]; + } + patBuffer.data[j][iFine+1]=pat->data[j][iFine+1]; + } + for (int j=selStart.y; j<=selEnd.y; j++) { + if (iFine==0) { + pat->data[j][0]=patBuffer.data[selEnd.y-j+selStart.y][0]; + } + pat->data[j][iFine+1]=patBuffer.data[selEnd.y-j+selStart.y][iFine+1]; + } + } + iFine=0; + } + makeUndo(GUI_UNDO_PATTERN_FLIP); } @@ -3337,13 +3418,99 @@ void FurnaceGUI::doCollapse(int divider) { finishSelection(); prepareUndo(GUI_UNDO_PATTERN_COLLAPSE); + DivPattern patBuffer; + int iCoarse=selStart.xCoarse; + int iFine=selStart.xFine; + int ord=e->getOrder(); + for (; iCoarse<=selEnd.xCoarse; iCoarse++) { + if (!e->song.chanShow[iCoarse]) continue; + DivPattern* pat=e->song.pat[iCoarse].getPattern(e->song.orders.ord[iCoarse][ord],true); + for (; iFine<3+e->song.pat[iCoarse].effectRows*2 && (iCoarsedata[j][0]; + } + patBuffer.data[j][iFine+1]=pat->data[j][iFine+1]; + } + for (int j=0; j<=selEnd.y-selStart.y; j++) { + if (j*divider>=selEnd.y-selStart.y) { + if (iFine==0) { + pat->data[j+selStart.y][0]=0; + pat->data[j+selStart.y][1]=0; + } else { + pat->data[j+selStart.y][iFine+1]=-1; + } + } else { + if (iFine==0) { + pat->data[j+selStart.y][0]=patBuffer.data[j*divider+selStart.y][0]; + } + pat->data[j+selStart.y][iFine+1]=patBuffer.data[j*divider+selStart.y][iFine+1]; + + if (iFine==0) { + for (int k=1; k=selEnd.y-selStart.y) break; + if (!(pat->data[j+selStart.y][0]==0 && pat->data[j+selStart.y][1]==0)) break; + pat->data[j+selStart.y][0]=patBuffer.data[j*divider+selStart.y+k][0]; + pat->data[j+selStart.y][1]=patBuffer.data[j*divider+selStart.y+k][1]; + } + } else { + for (int k=1; k=selEnd.y-selStart.y) break; + if (pat->data[j+selStart.y][iFine+1]!=-1) break; + pat->data[j+selStart.y][iFine+1]=patBuffer.data[j*divider+selStart.y+k][iFine+1]; + } + } + } + } + } + iFine=0; + } + makeUndo(GUI_UNDO_PATTERN_COLLAPSE); } void FurnaceGUI::doExpand(int multiplier) { + if (multiplier<1) return; + finishSelection(); prepareUndo(GUI_UNDO_PATTERN_EXPAND); + DivPattern patBuffer; + int iCoarse=selStart.xCoarse; + int iFine=selStart.xFine; + int ord=e->getOrder(); + for (; iCoarse<=selEnd.xCoarse; iCoarse++) { + if (!e->song.chanShow[iCoarse]) continue; + DivPattern* pat=e->song.pat[iCoarse].getPattern(e->song.orders.ord[iCoarse][ord],true); + for (; iFine<3+e->song.pat[iCoarse].effectRows*2 && (iCoarsedata[j][0]; + } + patBuffer.data[j][iFine+1]=pat->data[j][iFine+1]; + } + for (int j=0; j<=(selEnd.y-selStart.y)*multiplier; j++) { + if ((j+selStart.y)>=e->song.patLen) break; + if ((j%multiplier)!=0) { + if (iFine==0) { + pat->data[j+selStart.y][0]=0; + pat->data[j+selStart.y][1]=0; + } else { + pat->data[j+selStart.y][iFine+1]=-1; + } + continue; + } + if (iFine==0) { + pat->data[j+selStart.y][0]=patBuffer.data[j/multiplier+selStart.y][0]; + } + pat->data[j+selStart.y][iFine+1]=patBuffer.data[j/multiplier+selStart.y][iFine+1]; + } + } + iFine=0; + } + makeUndo(GUI_UNDO_PATTERN_EXPAND); } @@ -4221,6 +4388,7 @@ void FurnaceGUI::keyDown(SDL_Event& ev) { int num=12*curOctave+key; if (edit) { + // TODO: separate when adding MIDI input. DivPattern* pat=e->song.pat[cursor.xCoarse].getPattern(e->song.orders.ord[cursor.xCoarse][e->getOrder()],true); prepareUndo(GUI_UNDO_PATTERN_EDIT); @@ -4242,7 +4410,17 @@ void FurnaceGUI::keyDown(SDL_Event& ev) { pat->data[cursor.y][1]--; } pat->data[cursor.y][1]=(unsigned char)pat->data[cursor.y][1]; - pat->data[cursor.y][2]=curIns; + if (latchIns==-2) { + pat->data[cursor.y][2]=curIns; + } else if (latchIns!=-1 && !e->song.ins.empty()) { + pat->data[cursor.y][2]=MIN(((int)e->song.ins.size())-1,latchIns); + } + if (latchVol!=-1) { + int maxVol=e->getMaxVolumeChan(cursor.xCoarse); + pat->data[cursor.y][3]=MIN(maxVol,latchVol); + } + if (latchEffect!=-1) pat->data[cursor.y][4]=latchEffect; + if (latchEffectVal!=-1) pat->data[cursor.y][5]=latchEffectVal; previewNote(cursor.xCoarse,num); } makeUndo(GUI_UNDO_PATTERN_EDIT); @@ -4463,73 +4641,168 @@ bool dirExists(String what) { } void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { - ImGuiFileDialog::Instance()->DpiScale=dpiScale; + bool hasOpened=false; switch (type) { case GUI_FILE_OPEN: if (!dirExists(workingDirSong)) workingDirSong=getHomeDir(); - ImGuiFileDialog::Instance()->OpenModal("FileDialog","Open File","compatible files{.fur,.dmf},.*",workingDirSong); + hasOpened=fileDialog->openLoad( + "Open File", + {"compatible files", "*.fur *.dmf", + "all files", ".*"}, + "compatible files{.fur,.dmf},.*", + workingDirSong, + dpiScale + ); break; case GUI_FILE_SAVE: if (!dirExists(workingDirSong)) workingDirSong=getHomeDir(); - ImGuiFileDialog::Instance()->OpenModal("FileDialog","Save File","Furnace song{.fur},DefleMask 1.1 module{.dmf}",workingDirSong,1,nullptr,ImGuiFileDialogFlags_ConfirmOverwrite); + hasOpened=fileDialog->openSave( + "Save File", + {"Furnace song", "*.fur", + "DefleMask 1.1 module", "*.dmf"}, + "Furnace song{.fur},DefleMask 1.1 module{.dmf}", + workingDirSong, + dpiScale + ); break; case GUI_FILE_SAVE_DMF_LEGACY: if (!dirExists(workingDirSong)) workingDirSong=getHomeDir(); - ImGuiFileDialog::Instance()->OpenModal("FileDialog","Save File","DefleMask 1.0/legacy module{.dmf}",workingDirSong,1,nullptr,ImGuiFileDialogFlags_ConfirmOverwrite); + hasOpened=fileDialog->openSave( + "Save File", + {"DefleMask 1.0/legacy module", "*.dmf"}, + "DefleMask 1.0/legacy module{.dmf}", + workingDirSong, + dpiScale + ); break; case GUI_FILE_INS_OPEN: if (!dirExists(workingDirIns)) workingDirIns=getHomeDir(); - ImGuiFileDialog::Instance()->OpenModal("FileDialog","Load Instrument","compatible files{.fui,.dmp,.tfi,.vgi},.*",workingDirIns); + hasOpened=fileDialog->openLoad( + "Load Instrument", + {"compatible files", "*.fui *.dmp *.tfi *.vgi", + "all files", ".*"}, + "compatible files{.fui,.dmp,.tfi,.vgi},.*", + workingDirIns, + dpiScale + ); break; case GUI_FILE_INS_SAVE: if (!dirExists(workingDirIns)) workingDirIns=getHomeDir(); - ImGuiFileDialog::Instance()->OpenModal("FileDialog","Save Instrument","Furnace instrument{.fui}",workingDirIns,1,nullptr,ImGuiFileDialogFlags_ConfirmOverwrite); + hasOpened=fileDialog->openSave( + "Save Instrument", + {"Furnace instrument", "*.fui"}, + "Furnace instrument{.fui}", + workingDirIns, + dpiScale + ); break; case GUI_FILE_WAVE_OPEN: if (!dirExists(workingDirWave)) workingDirWave=getHomeDir(); - ImGuiFileDialog::Instance()->OpenModal("FileDialog","Load Wavetable","compatible files{.fuw,.dmw},.*",workingDirWave); + hasOpened=fileDialog->openLoad( + "Load Wavetable", + {"compatible files", "*.fuw *.dmw", + "all files", ".*"}, + "compatible files{.fuw,.dmw},.*", + workingDirWave, + dpiScale + ); break; case GUI_FILE_WAVE_SAVE: if (!dirExists(workingDirWave)) workingDirWave=getHomeDir(); - ImGuiFileDialog::Instance()->OpenModal("FileDialog","Save Wavetable","Furnace wavetable{.fuw}",workingDirWave,1,nullptr,ImGuiFileDialogFlags_ConfirmOverwrite); + hasOpened=fileDialog->openSave( + "Save Wavetable", + {"Furnace wavetable", ".fuw"}, + "Furnace wavetable{.fuw}", + workingDirWave, + dpiScale + ); break; case GUI_FILE_SAMPLE_OPEN: if (!dirExists(workingDirSample)) workingDirSample=getHomeDir(); - ImGuiFileDialog::Instance()->OpenModal("FileDialog","Load Sample","Wave file{.wav},.*",workingDirSample); + hasOpened=fileDialog->openLoad( + "Load Sample", + {"Wave file", "*.wav", + "all files", ".*"}, + "Wave file{.wav},.*", + workingDirSample, + dpiScale + ); break; case GUI_FILE_SAMPLE_SAVE: if (!dirExists(workingDirSample)) workingDirSample=getHomeDir(); - ImGuiFileDialog::Instance()->OpenModal("FileDialog","Save Sample","Wave file{.wav}",workingDirSample,1,nullptr,ImGuiFileDialogFlags_ConfirmOverwrite); + hasOpened=fileDialog->openSave( + "Save Sample", + {"Wave file", "*.wav"}, + "Wave file{.wav}", + workingDirSample, + dpiScale + ); break; case GUI_FILE_EXPORT_AUDIO_ONE: if (!dirExists(workingDirAudioExport)) workingDirAudioExport=getHomeDir(); - ImGuiFileDialog::Instance()->OpenModal("FileDialog","Export Audio","Wave file{.wav}",workingDirAudioExport,1,nullptr,ImGuiFileDialogFlags_ConfirmOverwrite); + hasOpened=fileDialog->openSave( + "Export Audio", + {"Wave file", "*.wav"}, + "Wave file{.wav}", + workingDirAudioExport, + dpiScale + ); break; case GUI_FILE_EXPORT_AUDIO_PER_SYS: if (!dirExists(workingDirAudioExport)) workingDirAudioExport=getHomeDir(); - ImGuiFileDialog::Instance()->OpenModal("FileDialog","Export Audio","Wave file{.wav}",workingDirAudioExport,1,nullptr,ImGuiFileDialogFlags_ConfirmOverwrite); + hasOpened=fileDialog->openSave( + "Export Audio", + {"Wave file", "*.wav"}, + "Wave file{.wav}", + workingDirAudioExport, + dpiScale + ); break; case GUI_FILE_EXPORT_AUDIO_PER_CHANNEL: if (!dirExists(workingDirAudioExport)) workingDirAudioExport=getHomeDir(); - ImGuiFileDialog::Instance()->OpenModal("FileDialog","Export Audio","Wave file{.wav}",workingDirAudioExport,1,nullptr,ImGuiFileDialogFlags_ConfirmOverwrite); + hasOpened=fileDialog->openSave( + "Export Audio", + {"Wave file", "*.wav"}, + "Wave file{.wav}", + workingDirAudioExport, + dpiScale + ); break; case GUI_FILE_EXPORT_VGM: if (!dirExists(workingDirVGMExport)) workingDirVGMExport=getHomeDir(); - ImGuiFileDialog::Instance()->OpenModal("FileDialog","Export VGM",".vgm",workingDirVGMExport,1,nullptr,ImGuiFileDialogFlags_ConfirmOverwrite); + hasOpened=fileDialog->openSave( + "Export VGM", + {"VGM file", "*.vgm"}, + "VGM file{.vgm}", + workingDirVGMExport, + dpiScale + ); break; case GUI_FILE_EXPORT_ROM: showError("Coming soon!"); break; case GUI_FILE_LOAD_MAIN_FONT: if (!dirExists(workingDirFont)) workingDirFont=getHomeDir(); - ImGuiFileDialog::Instance()->OpenModal("FileDialog","Select Font","compatible files{.ttf,.otf,.ttc}",workingDirFont); + hasOpened=fileDialog->openLoad( + "Select Font", + {"compatible files", "*.ttf *.otf *.ttc"}, + "compatible files{.ttf,.otf,.ttc}", + workingDirFont, + dpiScale + ); break; case GUI_FILE_LOAD_PAT_FONT: if (!dirExists(workingDirFont)) workingDirFont=getHomeDir(); - ImGuiFileDialog::Instance()->OpenModal("FileDialog","Select Font","compatible files{.ttf,.otf,.ttc}",workingDirFont); + hasOpened=fileDialog->openLoad( + "Select Font", + {"compatible files", "*.ttf *.otf *.ttc"}, + "compatible files{.ttf,.otf,.ttc}", + workingDirFont, + dpiScale + ); break; } - curFileDialog=type; + if (hasOpened) curFileDialog=type; //ImGui::GetIO().ConfigFlags|=ImGuiConfigFlags_NavEnableKeyboard; } @@ -4654,7 +4927,7 @@ int FurnaceGUI::load(String path) { } if (len<1) { if (len==0) { - printf("that file is empty!\n"); + logE("that file is empty!\n"); lastError="file is empty"; } else { perror("tell error"); @@ -4810,6 +5083,15 @@ void FurnaceGUI::processDrags(int dragX, int dragY) { fileName+=x; \ } +#define checkExtensionDual(x,y,fallback) \ + String lowerCase=fileName; \ + for (char& i: lowerCase) { \ + if (i>='A' && i<='Z') i+='a'-'A'; \ + } \ + if (lowerCase.size()<4 || (lowerCase.rfind(x)!=lowerCase.size()-4 && lowerCase.rfind(y)!=lowerCase.size()-4)) { \ + fileName+=fallback; \ + } + #define BIND_FOR(x) getKeyName(actionKeys[x],true).c_str() void FurnaceGUI::editOptions(bool topMenu) { @@ -5475,6 +5757,10 @@ bool FurnaceGUI::loop() { e->setSysFlags(i,(flags&(~0x30))|32,restart); updateWindowTitle(); } + if (ImGui::RadioButton("AY-3-8914",(flags&0x30)==48)) { + e->setSysFlags(i,(flags&(~0x30))|48,restart); + updateWindowTitle(); + } } bool stereo=flags&0x40; ImGui::BeginDisabled((flags&0x30)==32); @@ -5819,49 +6105,47 @@ bool FurnaceGUI::loop() { if (patternOpen) nextWindow=GUI_WINDOW_PATTERN; } - if (ImGuiFileDialog::Instance()->Display("FileDialog",ImGuiWindowFlags_NoCollapse|ImGuiWindowFlags_NoMove,ImVec2(600.0f*dpiScale,400.0f*dpiScale),ImVec2(scrW*dpiScale,scrH*dpiScale))) { + if (fileDialog->render(ImVec2(600.0f*dpiScale,400.0f*dpiScale),ImVec2(scrW*dpiScale,scrH*dpiScale))) { //ImGui::GetIO().ConfigFlags&=~ImGuiConfigFlags_NavEnableKeyboard; switch (curFileDialog) { case GUI_FILE_OPEN: case GUI_FILE_SAVE: case GUI_FILE_SAVE_DMF_LEGACY: - workingDirSong=ImGuiFileDialog::Instance()->GetCurrentPath()+DIR_SEPARATOR_STR; + workingDirSong=fileDialog->getPath()+DIR_SEPARATOR_STR; break; case GUI_FILE_INS_OPEN: case GUI_FILE_INS_SAVE: - workingDirIns=ImGuiFileDialog::Instance()->GetCurrentPath()+DIR_SEPARATOR_STR; + workingDirIns=fileDialog->getPath()+DIR_SEPARATOR_STR; break; case GUI_FILE_WAVE_OPEN: case GUI_FILE_WAVE_SAVE: - workingDirWave=ImGuiFileDialog::Instance()->GetCurrentPath()+DIR_SEPARATOR_STR; + workingDirWave=fileDialog->getPath()+DIR_SEPARATOR_STR; break; case GUI_FILE_SAMPLE_OPEN: case GUI_FILE_SAMPLE_SAVE: - workingDirSample=ImGuiFileDialog::Instance()->GetCurrentPath()+DIR_SEPARATOR_STR; + workingDirSample=fileDialog->getPath()+DIR_SEPARATOR_STR; break; case GUI_FILE_EXPORT_AUDIO_ONE: case GUI_FILE_EXPORT_AUDIO_PER_SYS: case GUI_FILE_EXPORT_AUDIO_PER_CHANNEL: - workingDirAudioExport=ImGuiFileDialog::Instance()->GetCurrentPath()+DIR_SEPARATOR_STR; + workingDirAudioExport=fileDialog->getPath()+DIR_SEPARATOR_STR; break; case GUI_FILE_EXPORT_VGM: case GUI_FILE_EXPORT_ROM: - workingDirVGMExport=ImGuiFileDialog::Instance()->GetCurrentPath()+DIR_SEPARATOR_STR; + workingDirVGMExport=fileDialog->getPath()+DIR_SEPARATOR_STR; break; case GUI_FILE_LOAD_MAIN_FONT: case GUI_FILE_LOAD_PAT_FONT: - workingDirFont=ImGuiFileDialog::Instance()->GetCurrentPath()+DIR_SEPARATOR_STR; + workingDirFont=fileDialog->getPath()+DIR_SEPARATOR_STR; break; } - if (ImGuiFileDialog::Instance()->IsOk()) { - fileName=ImGuiFileDialog::Instance()->GetFilePathName(); + if (fileDialog->accepted()) { + fileName=fileDialog->getFileName(); if (fileName!="") { if (curFileDialog==GUI_FILE_SAVE) { - if (ImGuiFileDialog::Instance()->GetCurrentFilter()=="Furnace song") { - checkExtension(".fur"); - } else { - checkExtension(".dmf"); - } + // we can't tell whether the user chose .dmf or .fur in the system file picker + const char* fallbackExt=(settings.sysFileDialog || ImGuiFileDialog::Instance()->GetCurrentFilter()=="Furnace song")?".fur":".dmf"; + checkExtensionDual(".fur",".dmf",fallbackExt); } if (curFileDialog==GUI_FILE_SAVE_DMF_LEGACY) { checkExtension(".dmf"); @@ -5888,9 +6172,13 @@ bool FurnaceGUI::loop() { showError(fmt::sprintf("Error while loading file! (%s)",lastError)); } break; - case GUI_FILE_SAVE: - printf("saving: %s\n",copyOfName.c_str()); - if (ImGuiFileDialog::Instance()->GetCurrentFilter()=="Furnace song") { + case GUI_FILE_SAVE: { + logD("saving: %s\n",copyOfName.c_str()); + String lowerCase=fileName; + for (char& i: lowerCase) { + if (i>='A' && i<='Z') i+='a'-'A'; + } + if ((lowerCase.size()<4 || lowerCase.rfind(".dmf")!=lowerCase.size()-4)) { if (save(copyOfName,0)>0) { showError(fmt::sprintf("Error while saving file! (%s)",lastError)); } @@ -5900,8 +6188,9 @@ bool FurnaceGUI::loop() { } } break; + } case GUI_FILE_SAVE_DMF_LEGACY: - printf("saving: %s\n",copyOfName.c_str()); + logD("saving: %s\n",copyOfName.c_str()); if (save(copyOfName,24)>0) { showError(fmt::sprintf("Error while saving file! (%s)",lastError)); } @@ -5980,7 +6269,7 @@ bool FurnaceGUI::loop() { curFileDialog=GUI_FILE_OPEN; } } - ImGuiFileDialog::Instance()->Close(); + fileDialog->close(); } if (warnQuit) { @@ -6458,6 +6747,9 @@ void FurnaceGUI::applyUISettings() { if ((bigFont=ImGui::GetIO().Fonts->AddFontFromMemoryCompressedTTF(font_plexSans_compressed_data,font_plexSans_compressed_size,40*dpiScale))==NULL) { logE("could not load big UI font!\n"); } + + if (fileDialog!=NULL) delete fileDialog; + fileDialog=new FurnaceGUIFileDialog(settings.sysFileDialog); } bool FurnaceGUI::init() { @@ -6587,6 +6879,7 @@ bool FurnaceGUI::init() { ImGui::GetIO().IniFilename=finalLayoutPath; ImGui::LoadIniSettingsFromDisk(finalLayoutPath); + // TODO: allow changing these colors. ImGuiFileDialog::Instance()->SetFileStyle(IGFD_FileStyleByTypeDir,"",ImVec4(0.0f,1.0f,1.0f,1.0f),ICON_FA_FOLDER_O); ImGuiFileDialog::Instance()->SetFileStyle(IGFD_FileStyleByTypeFile,"",ImVec4(0.7f,0.7f,0.7f,1.0f),ICON_FA_FILE_O); ImGuiFileDialog::Instance()->SetFileStyle(IGFD_FileStyleByExtension,".fur",ImVec4(0.5f,1.0f,0.5f,1.0f),ICON_FA_FILE); @@ -6682,6 +6975,7 @@ FurnaceGUI::FurnaceGUI(): displayNew(false), curFileDialog(GUI_FILE_OPEN), warnAction(GUI_WARN_OPEN), + fileDialog(NULL), scrW(1280), scrH(800), dpiScale(1), @@ -6750,7 +7044,7 @@ FurnaceGUI::FurnaceGUI(): opMaskEffect(true), opMaskEffectVal(true), latchNote(-1), - latchIns(-1), + latchIns(-2), latchVol(-1), latchEffect(-1), latchEffectVal(-1), @@ -7058,7 +7352,7 @@ FurnaceGUI::FurnaceGUI(): )); cat.systems.push_back(FurnaceGUISysDef( "Mattel Intellivision", { - DIV_SYSTEM_AY8910, 64, 0, 6, + DIV_SYSTEM_AY8910, 64, 0, 48, 0 } )); diff --git a/src/gui/gui.h b/src/gui/gui.h index 0aec3256..22e8036e 100644 --- a/src/gui/gui.h +++ b/src/gui/gui.h @@ -27,6 +27,8 @@ #include #include +#include "fileDialog.h" + #define rightClickable if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) ImGui::SetKeyboardFocusHere(-1); #define handleUnimportant if (settings.insFocusesPattern && patternOpen) {nextWindow=GUI_WINDOW_PATTERN;} @@ -476,6 +478,8 @@ class FurnaceGUI { FurnaceGUIFileDialogs curFileDialog; FurnaceGUIWarnings warnAction; + FurnaceGUIFileDialog* fileDialog; + int scrW, scrH; double dpiScale; @@ -534,6 +538,10 @@ class FurnaceGUI { int avoidRaisingPattern; int insFocusesPattern; int stepOnInsert; + // TODO flags + int unifiedDataView; + int sysFileDialog; + // end unsigned int maxUndoSteps; String mainFontPath; String patFontPath; @@ -578,6 +586,8 @@ class FurnaceGUI { avoidRaisingPattern(0), insFocusesPattern(1), stepOnInsert(0), + unifiedDataView(0), + sysFileDialog(1), maxUndoSteps(100), mainFontPath(""), patFontPath(""), @@ -663,6 +673,7 @@ class FurnaceGUI { bool macroDragInitialValueSet; bool macroDragInitialValue; bool macroDragChar; + bool macroDragLineMode; // TODO bool macroDragActive; ImVec2 macroLoopDragStart; @@ -710,6 +721,9 @@ class FurnaceGUI { void patternRow(int i, bool isPlaying, float lineHeight, int chans, int ord); + void actualWaveList(); + void actualSampleList(); + void drawEditControls(); void drawSongInfo(); void drawOrders(); diff --git a/src/gui/guiConst.cpp b/src/gui/guiConst.cpp index c872b05b..83220d25 100644 --- a/src/gui/guiConst.cpp +++ b/src/gui/guiConst.cpp @@ -19,6 +19,7 @@ // guiConst: constants used in the GUI like arrays, strings and other stuff #include "guiConst.h" +#include "../engine/instrument.h" const int opOrder[4]={ 0, 2, 1, 3 @@ -64,3 +65,31 @@ const char* pitchLabel[11]={ "1/6", "1/5", "1/4", "1/3", "1/2", "1x", "2x", "3x", "4x", "5x", "6x" }; +const char* insTypes[DIV_INS_MAX]={ + "Standard", + "FM (4-operator)", + "Game Boy", + "C64", + "Amiga/Sample", + "PC Engine", + "AY-3-8910/SSG", + "AY8930", + "TIA", + "SAA1099", + "VIC", + "PET", + "VRC6", + "FM (OPLL)", + "FM (OPL)", + "FDS", + "Virtual Boy", + "Namco 163", + "Konami SCC", + "FM (OPZ)", + "POKEY", + "PC Beeper", + "WonderSwan", + "Atari Lynx", + "VERA", + "X1-010" +}; \ No newline at end of file diff --git a/src/gui/guiConst.h b/src/gui/guiConst.h index 3d252b53..9f415915 100644 --- a/src/gui/guiConst.h +++ b/src/gui/guiConst.h @@ -23,3 +23,4 @@ extern const int opOrder[4]; extern const char* noteNames[180]; extern const char* noteNamesG[180]; extern const char* pitchLabel[11]; +extern const char* insTypes[]; \ No newline at end of file diff --git a/src/gui/insEdit.cpp b/src/gui/insEdit.cpp index 2c178058..862de6e8 100644 --- a/src/gui/insEdit.cpp +++ b/src/gui/insEdit.cpp @@ -27,35 +27,6 @@ #include #include "plot_nolerp.h" -const char* insTypes[DIV_INS_MAX]={ - "Standard", - "FM (4-operator)", - "Game Boy", - "C64", - "Amiga/Sample", - "PC Engine", - "AY-3-8910/SSG", - "AY8930", - "TIA", - "SAA1099", - "VIC", - "PET", - "VRC6", - "FM (OPLL)", - "FM (OPL)", - "FDS", - "Virtual Boy", - "Namco 163", - "Konami SCC", - "FM (OPZ)", - "POKEY", - "PC Beeper", - "WonderSwan", - "Atari Lynx", - "VERA", - "X1-010" -}; - const char* ssgEnvTypes[8]={ "Down Down Down", "Down.", "Down Up Down Up", "Down UP", "Up Up Up", "Up.", "Up Down Up Down", "Up DOWN" }; @@ -1787,7 +1758,6 @@ void FurnaceGUI::drawWaveList() { nextWindow=GUI_WINDOW_NOTHING; } if (!waveListOpen) return; - float wavePreview[256]; if (ImGui::Begin("Wavetables",&waveListOpen)) { if (ImGui::Button(ICON_FA_PLUS "##WaveAdd")) { doAction(GUI_ACTION_WAVE_LIST_ADD); @@ -1818,25 +1788,7 @@ void FurnaceGUI::drawWaveList() { } ImGui::Separator(); if (ImGui::BeginTable("WaveListScroll",1,ImGuiTableFlags_ScrollY)) { - for (int i=0; i<(int)e->song.wave.size(); i++) { - DivWavetable* wave=e->song.wave[i]; - for (int i=0; ilen; i++) { - wavePreview[i]=wave->data[i]; - } - if (wave->len>0) wavePreview[wave->len]=wave->data[wave->len-1]; - ImGui::TableNextRow(); - ImGui::TableNextColumn(); - if (ImGui::Selectable(fmt::sprintf("%d##_WAVE%d\n",i,i).c_str(),curWave==i)) { - curWave=i; - } - if (ImGui::IsItemHovered()) { - if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) { - waveEditOpen=true; - } - } - ImGui::SameLine(); - PlotNoLerp(fmt::sprintf("##_WAVEP%d",i).c_str(),wavePreview,wave->len+1,0,NULL,0,wave->max); - } + actualWaveList(); ImGui::EndTable(); } } diff --git a/src/gui/settings.cpp b/src/gui/settings.cpp index c8f6763b..a10795fb 100644 --- a/src/gui/settings.cpp +++ b/src/gui/settings.cpp @@ -182,6 +182,11 @@ void FurnaceGUI::drawSettings() { settings.restartOnFlagChange=restartOnFlagChangeB; } + bool sysFileDialogB=settings.sysFileDialog; + if (ImGui::Checkbox("Use system file picker",&sysFileDialogB)) { + settings.sysFileDialog=sysFileDialogB; + } + ImGui::Text("Wrap pattern cursor horizontally:"); if (ImGui::RadioButton("No##wrapH0",settings.wrapHorizontal==0)) { settings.wrapHorizontal=0; @@ -403,6 +408,11 @@ void FurnaceGUI::drawSettings() { settings.macroView=macroViewB; } + bool unifiedDataViewB=settings.unifiedDataView; + if (ImGui::Checkbox("Unified instrument/wavetable/sample list",&unifiedDataViewB)) { + settings.unifiedDataView=unifiedDataViewB; + } + bool chipNamesB=settings.chipNames; if (ImGui::Checkbox("Use chip names instead of system names",&chipNamesB)) { settings.chipNames=chipNamesB; @@ -892,6 +902,8 @@ void FurnaceGUI::syncSettings() { settings.avoidRaisingPattern=e->getConfInt("avoidRaisingPattern",0); settings.insFocusesPattern=e->getConfInt("insFocusesPattern",1); settings.stepOnInsert=e->getConfInt("stepOnInsert",0); + settings.unifiedDataView=e->getConfInt("unifiedDataView",0); + settings.sysFileDialog=e->getConfInt("sysFileDialog",1); clampSetting(settings.mainFontSize,2,96); clampSetting(settings.patFontSize,2,96); @@ -930,6 +942,8 @@ void FurnaceGUI::syncSettings() { clampSetting(settings.avoidRaisingPattern,0,1); clampSetting(settings.insFocusesPattern,0,1); clampSetting(settings.stepOnInsert,0,1); + clampSetting(settings.unifiedDataView,0,1); + clampSetting(settings.sysFileDialog,0,1); // keybinds LOAD_KEYBIND(GUI_ACTION_OPEN,FURKMOD_CMD|SDLK_o); @@ -1129,6 +1143,8 @@ void FurnaceGUI::commitSettings() { e->setConf("avoidRaisingPattern",settings.avoidRaisingPattern); e->setConf("insFocusesPattern",settings.insFocusesPattern); e->setConf("stepOnInsert",settings.stepOnInsert); + e->setConf("unifiedDataView",settings.unifiedDataView); + e->setConf("sysFileDialog",settings.sysFileDialog); PUT_UI_COLOR(GUI_COLOR_BACKGROUND); PUT_UI_COLOR(GUI_COLOR_FRAME_BACKGROUND);