/***************************************************************************** * Copyright (c) 2014-2018 OpenRCT2 developers * * For a complete list of all authors, please refer to contributors.md * Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2 * * OpenRCT2 is licensed under the GNU General Public License version 3. *****************************************************************************/ #if (defined(__linux__) || defined(__OpenBSD__) || defined(__FreeBSD__) || defined(__EMSCRIPTEN__)) && !defined(__ANDROID__) # include "UiContext.h" # include # include # include # include # include # include # include # include # include namespace OpenRCT2::Ui { enum class DIALOG_TYPE { NONE, KDIALOG, ZENITY, }; class LinuxContext final : public IPlatformUiContext { private: public: LinuxContext() { } void SetWindowIcon(SDL_Window* window) override { } bool IsSteamOverlayAttached() override { # ifdef __linux__ // See http://syprog.blogspot.ru/2011/12/listing-loaded-shared-objects-in-linux.html struct lmap { void* base_address; char* path; void* unused; lmap* next; lmap* prev; }; struct dummy { void* pointers[3]; dummy* ptr; }; bool result = false; void* processHandle = dlopen(nullptr, RTLD_NOW); if (processHandle != nullptr) { dummy* p = ((dummy*)processHandle)->ptr; lmap* pl = (lmap*)p->ptr; while (pl != nullptr) { if (strstr(pl->path, "gameoverlayrenderer.so") != nullptr) { result = true; break; } pl = pl->next; } dlclose(processHandle); } return result; # else return false; // Needed for OpenBSD, likely all other Unixes. # endif } void ShowMessageBox(SDL_Window* window, const std::string& message) override { log_verbose(message.c_str()); std::string executablePath; DIALOG_TYPE dtype = GetDialogApp(&executablePath); switch (dtype) { case DIALOG_TYPE::KDIALOG: { std::string cmd = String::Format( "%s --title \"OpenRCT2\" --msgbox \"%s\"", executablePath.c_str(), message.c_str()); Execute(cmd); break; } case DIALOG_TYPE::ZENITY: { std::string cmd = String::Format( "%s --title=\"OpenRCT2\" --info --text=\"%s\"", executablePath.c_str(), message.c_str()); Execute(cmd); break; } default: SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_WARNING, "OpenRCT2", message.c_str(), window); break; } } void OpenFolder(const std::string& path) override { std::string cmd = String::Format("xdg-open %s", EscapePathForShell(path).c_str()); Execute(cmd); } std::string ShowFileDialog(SDL_Window* window, const FileDialogDesc& desc) override { std::string result; std::string executablePath; DIALOG_TYPE dtype = GetDialogApp(&executablePath); switch (dtype) { case DIALOG_TYPE::KDIALOG: { std::string action = (desc.Type == FILE_DIALOG_TYPE::OPEN) ? "--getopenfilename" : "--getsavefilename"; std::string filter = GetKDialogFilterString(desc.Filters); std::string cmd = String::StdFormat( "%s --title '%s' %s '%s' '%s'", executablePath.c_str(), desc.Title.c_str(), action.c_str(), desc.InitialDirectory.c_str(), filter.c_str()); std::string output; if (Execute(cmd, &output) == 0) { result = output; } break; } case DIALOG_TYPE::ZENITY: { std::string action = "--file-selection"; std::string flags; if (desc.Type == FILE_DIALOG_TYPE::SAVE) { flags = "--confirm-overwrite --save"; } std::string filters = GetZenityFilterString(desc.Filters); std::string cmd = String::StdFormat( "%s %s --filename='%s/' %s --title='%s' / %s", executablePath.c_str(), action.c_str(), desc.InitialDirectory.c_str(), flags.c_str(), desc.Title.c_str(), filters.c_str()); std::string output; if (Execute(cmd, &output) == 0) { if (desc.Type == FILE_DIALOG_TYPE::SAVE) { // The default file extension is taken from the **first** available filter, since // we cannot obtain it from zenity's output. This means that the FileDialogDesc::Filters // array must be carefully populated, at least the first element. std::string pattern = desc.Filters[0].Pattern; std::string defaultExtension = pattern.substr(pattern.find_last_of('.')); const utf8* filename = Path::GetFileName(output.c_str()); // If there is no extension, append the pattern const utf8* extension = Path::GetExtension(filename); result = output; if (extension[0] == '\0' && !defaultExtension.empty()) { result = output.append(defaultExtension); } } else { result = output; } } break; } default: ThrowMissingDialogApp(); break; } if (!result.empty()) { if (desc.Type == FILE_DIALOG_TYPE::OPEN && access(result.c_str(), F_OK) == -1) { std::string msg = String::StdFormat( "\"%s\" not found: %s, please choose another file\n", result.c_str(), strerror(errno)); ShowMessageBox(window, msg); return ShowFileDialog(window, desc); } else if ( desc.Type == FILE_DIALOG_TYPE::SAVE && access(result.c_str(), F_OK) != -1 && dtype == DIALOG_TYPE::KDIALOG) { std::string cmd = String::StdFormat("%s --yesno \"Overwrite %s?\"", executablePath.c_str(), result.c_str()); if (Execute(cmd) != 0) { result = std::string(); } } } return result; } std::string ShowDirectoryDialog(SDL_Window* window, const std::string& title) override { std::string result; std::string executablePath; DIALOG_TYPE dtype = GetDialogApp(&executablePath); switch (dtype) { case DIALOG_TYPE::KDIALOG: { std::string output; std::string cmd = String::Format( "%s --title '%s' --getexistingdirectory /", executablePath.c_str(), title.c_str()); if (Execute(cmd, &output) == 0) { result = output; } break; } case DIALOG_TYPE::ZENITY: { std::string output; std::string cmd = String::Format( "%s --title='%s' --file-selection --directory /", executablePath.c_str(), title.c_str()); if (Execute(cmd, &output) == 0) { result = output; } break; } default: ThrowMissingDialogApp(); break; } return result; } private: static DIALOG_TYPE GetDialogApp(std::string* executablePath) { // Prefer zenity as it offers more required features, e.g., overwrite // confirmation and selecting only existing files. // Silence error output with 2> /dev/null to avoid confusion in the // case where a user does not have zenity and/or kdialog. // OpenRCT2 will fall back to an SDL pop-up if the user has neither. if (Execute("which zenity 2> /dev/null", executablePath) == 0) { return DIALOG_TYPE::ZENITY; } if (Execute("which kdialog 2> /dev/null", executablePath) == 0) { return DIALOG_TYPE::KDIALOG; } return DIALOG_TYPE::NONE; } static int32_t Execute(const std::string& command, std::string* output = nullptr) { # ifndef __EMSCRIPTEN__ log_verbose("executing \"%s\"...\n", command.c_str()); FILE* fpipe = popen(command.c_str(), "r"); if (fpipe == nullptr) { return -1; } if (output != nullptr) { // Read output into buffer std::vector outputBuffer; char buffer[1024]; size_t readBytes; while ((readBytes = fread(buffer, 1, sizeof(buffer), fpipe)) > 0) { outputBuffer.insert(outputBuffer.begin(), buffer, buffer + readBytes); } // Trim line breaks size_t outputLength = outputBuffer.size(); for (size_t i = outputLength - 1; i != SIZE_MAX; i--) { if (outputBuffer[i] == '\n') { outputLength = i; } else { break; } } // Convert to string *output = std::string(outputBuffer.data(), outputLength); } else { fflush(fpipe); } // Return exit code return pclose(fpipe); # else log_warning("Emscripten cannot execute processes. The commandline was '%s'.", command.c_str()); return -1; # endif // __EMSCRIPTEN__ } static std::string GetKDialogFilterString(const std::vector filters) { std::stringstream filtersb; bool first = true; for (const auto& filter : filters) { // KDialog wants filters space-delimited and we don't expect ';' anywhere else std::string pattern = filter.Pattern; for (size_t i = 0; i < pattern.size(); i++) { if (pattern[i] == ';') { pattern[i] = ' '; } } if (first) { filtersb << String::StdFormat("%s | %s", pattern.c_str(), filter.Name.c_str()); first = false; } else { filtersb << String::StdFormat("\\n%s | %s", pattern.c_str(), filter.Name.c_str()); } } return filtersb.str(); } static std::string GetZenityFilterString(const std::vector filters) { // Zenity seems to be case sensitive, while KDialog isn't std::stringstream filtersb; for (const auto& filter : filters) { filtersb << " --file-filter='" << filter.Name << " | "; for (char c : filter.Pattern) { if (c == ';') { filtersb << ' '; } else if (isalpha(c)) { filtersb << '[' << (char)tolower(c) << (char)toupper(c) << ']'; } else { filtersb << c; } } filtersb << "'"; } return filtersb.str(); } static void ThrowMissingDialogApp() { auto uiContext = GetContext()->GetUiContext(); std::string dialogMissingWarning = language_get_string(STR_MISSING_DIALOG_APPLICATION_ERROR); uiContext->ShowMessageBox(dialogMissingWarning); throw std::runtime_error(dialogMissingWarning); } static std::string EscapePathForShell(std::string path) { for (size_t index = 0; (index = path.find('"', index)) != std::string::npos; index += 2) { path.replace(index, 1, "\\\""); } return '"' + path + '"'; } }; IPlatformUiContext* CreatePlatformUiContext() { return new LinuxContext(); } } // namespace OpenRCT2::Ui #endif // __linux__ || __OpenBSD__