library/forms/utilities.cpp (605 lines of code) (raw):
/*
* Copyright (c) 2009, 2019, Oracle and/or its affiliates. All rights reserved.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License, version 2.0,
* as published by the Free Software Foundation.
*
* This program is designed to work with certain software (including
* but not limited to OpenSSL) that is licensed under separate terms, as
* designated in a particular file or component or in included license
* documentation. The authors of MySQL hereby grant you an additional
* permission to link the program and your derivative works with the
* separately licensed software that they have either included with
* the program or referenced in the documentation.
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
* the GNU General Public License, version 2.0, for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
/**
* Utilities is a support class to trigger message boxes and the like in the front end.
*/
#include "base/string_utilities.h"
#include "base/file_functions.h"
#include "base/log.h"
#include "base/threading.h"
#include "mforms/mforms.h"
#include "mforms/password_cache.h"
#include "mdc_image.h"
DEFAULT_LOG_DOMAIN(DOMAIN_MFORMS_BE);
using namespace mforms;
using namespace base;
GThread *_mforms_main_thread = NULL;
static std::map<std::string, int> remembered_message_answers;
static std::string remembered_message_answer_file;
std::function<void()> mforms::Utilities::_driver_shutdown_cb;
//--------------------------------------------------------------------------------------------------
void Utilities::beep() {
ControlFactory::get_instance()->_utilities_impl.beep();
}
//--------------------------------------------------------------------------------------------------
static void *_show_dialog(const DialogType type, const std::string &title, const std::string &text,
const std::string &ok, const std::string &cancel, const std::string &other) {
int *ret = new int;
switch (type) {
case DialogMessage:
*ret = ControlFactory::get_instance()->_utilities_impl.show_message(title, text, ok, cancel, other);
break;
case DialogWarning:
*ret = ControlFactory::get_instance()->_utilities_impl.show_warning(title, text, ok, cancel, other);
break;
case DialogError:
*ret = ControlFactory::get_instance()->_utilities_impl.show_error(title, text, ok, cancel, other);
break;
default:
*ret = mforms::ResultUnknown;
}
return (void *)ret;
}
static int void_to_int(void *val) {
int *ret = (int *)val;
int ret_val = *ret;
delete ret;
return ret_val;
}
int Utilities::show_message(const std::string &title, const std::string &text, const std::string &ok,
const std::string &cancel, const std::string &other) {
if (Utilities::in_main_thread())
return void_to_int(_show_dialog(DialogMessage, title, text, ok, cancel, other));
else
return void_to_int(
Utilities::perform_from_main_thread(std::bind(&_show_dialog, DialogMessage, title, text, ok, cancel, other)));
}
//--------------------------------------------------------------------------------------------------
int Utilities::show_error(const std::string &title, const std::string &text, const std::string &ok,
const std::string &cancel, const std::string &other) {
if (Utilities::in_main_thread())
return void_to_int(_show_dialog(DialogError, title, text, ok, cancel, other));
else
return void_to_int(
Utilities::perform_from_main_thread(std::bind(&_show_dialog, DialogError, title, text, ok, cancel, other)));
}
//--------------------------------------------------------------------------------------------------
int Utilities::show_warning(const std::string &title, const std::string &text, const std::string &ok,
const std::string &cancel, const std::string &other) {
if (Utilities::in_main_thread())
return void_to_int(_show_dialog(DialogWarning, title, text, ok, cancel, other));
else
return void_to_int(
Utilities::perform_from_main_thread(std::bind(&_show_dialog, DialogWarning, title, text, ok, cancel, other)));
}
//--------------------------------------------------------------------------------------------------
int Utilities::show_message_and_remember(const std::string &title, const std::string &text, const std::string &ok,
const std::string &cancel, const std::string &other,
const std::string &answer_id, const std::string &checkbox_text) {
if (remembered_message_answers.find(answer_id) != remembered_message_answers.end())
return remembered_message_answers[answer_id];
if (!ControlFactory::get_instance()->_utilities_impl.show_message_with_checkbox)
return show_message(title, text, ok, cancel, other);
bool remember = false;
int rc = ControlFactory::get_instance()->_utilities_impl.show_message_with_checkbox(title, text, ok, cancel, other,
checkbox_text, remember);
if (remember) {
remembered_message_answers[answer_id] = rc;
save_message_answers();
}
return rc;
}
void Utilities::set_message_answers_storage_path(const std::string &path) {
remembered_message_answer_file = path;
FILE *f = base_fopen(remembered_message_answer_file.c_str(), "r");
if (f) {
char line[1024];
while (fgets(line, sizeof(line), f)) {
char *ptr = strrchr(line, '=');
if (ptr) {
*ptr = 0;
remembered_message_answers[line] = base::atoi<int>(ptr + 1, 0);
}
}
fclose(f);
}
}
void Utilities::save_message_answers() {
if (!remembered_message_answer_file.empty()) {
FILE *f = base_fopen(remembered_message_answer_file.c_str(), "w+");
for (std::map<std::string, int>::const_iterator iter = remembered_message_answers.begin();
iter != remembered_message_answers.end(); ++iter)
fprintf(f, "%s=%i\n", iter->first.c_str(), iter->second);
fclose(f);
}
}
void Utilities::forget_message_answers() {
remembered_message_answers.clear();
save_message_answers();
}
//--------------------------------------------------------------------------------------------------
void Utilities::show_wait_message(const std::string &title, const std::string &text) {
// The wait message is a special window, so there's no need to hide the splash screen.
ControlFactory::get_instance()->_utilities_impl.show_wait_message(title, text);
}
//--------------------------------------------------------------------------------------------------
bool Utilities::hide_wait_message() {
return ControlFactory::get_instance()->_utilities_impl.hide_wait_message();
}
//--------------------------------------------------------------------------------------------------
class CancellableTaskData {
public:
std::function<void *()> task;
bool finished;
std::shared_ptr<void *> result_ptr;
int ref_count;
base::Semaphore semaphore;
CancellableTaskData() : finished(false), ref_count(1), semaphore(0) {
}
};
// To ensure the shared thread data is not freed too early regardless what finishes first
// (the thread or run_cancelable_task()) we store this data here in a private structure, ref counted.
static base::Mutex thread_data_mutex;
static std::map<void *, CancellableTaskData *> thread_data;
static void *cancellable_task_thread(void *) {
CancellableTaskData *data = NULL;
{
base::MutexLock lock(thread_data_mutex);
data = thread_data[g_thread_self()];
if (data != NULL)
data->ref_count++; // Increment ref count while the data is still protected.
}
if (data != NULL) // Should never be NULL but...
{
void *ptr = NULL;
try {
ptr = data->task();
} catch (std::exception &exc) {
logError("Cancellable task threw uncaught exception: %s", exc.what());
}
data->semaphore.wait(); // Wait for the main thread to signal it is ready.
*data->result_ptr = ptr;
data->finished = true;
ControlFactory::get_instance()->_utilities_impl.stop_cancelable_wait_message();
{
base::MutexLock lock(thread_data_mutex);
data->ref_count--;
if (data->ref_count == 0) {
thread_data.erase(g_thread_self());
delete data;
}
}
}
return NULL;
}
bool Utilities::run_cancelable_task(const std::string &title, const std::string &text,
const std::function<void *()> &task, const std::function<bool()> &cancel_task,
void *&task_result) {
std::shared_ptr<void *> result(new void *((void *)-1));
CancellableTaskData *data = NULL;
GThread *thread = NULL;
// Start a thread that will run the task. Store thread data so it can be accessed by the thread
// safely.
{
base::MutexLock lock(thread_data_mutex);
data = new CancellableTaskData(); // Ref count is 1.
GError *error = NULL;
thread = base::create_thread((void *(*)(void *))cancellable_task_thread, NULL, &error);
if (thread == NULL) {
std::string msg("Error creating thread: ");
msg.append(error->message);
g_error_free(error);
delete data;
throw std::runtime_error(msg);
}
// result_ptr is used by the task thread to pass back the result from the task callback.
// We cannot directly pass a pointer to task_result, because if the task is canceled,
// this function will return and the caller may free the task_result object before the
// canceled task actually finishes executing, which would invalidate any ptr to task_result.
data->result_ptr = result;
// The thread will wait on the mutex as we still lock it, so it's safe to work on the data.
thread_data[thread] = data;
data->task = task;
}
// Callback for the frontend to signal the worker thread that it's ready.
std::function<void()> signal_ready = std::bind(&base::Semaphore::post, &data->semaphore);
bool function_result = false;
retry:
if (ControlFactory::get_instance()->_utilities_impl.run_cancelable_wait_message(title, text, signal_ready,
cancel_task)) {
// Sometimes, in the mac, there's a race and/or bug that causes the event loop
// from the wait panel dialog to be exited when a nested modal dialog is closed.
// Clicking OK for the nested dialog, exits the wait panel, which would cause it
// to return prematurely. Just execute the modal panel again if that's the case.
// Additional note: sometimes the wait panel must be closed on purpose, so this is necessary anyway.
if (!data->finished)
goto retry;
// Task completed. We can directly access the data here without lock as we still have
// the increased ref count here.
task_result = *result;
function_result = true;
} else {
// Task canceled by user.
logDebug2("run_cancelable_wait_message returned false\n");
}
{
base::MutexLock lock(thread_data_mutex);
data->ref_count--;
if (data->ref_count == 0) {
thread_data.erase(thread);
delete data;
}
}
return function_result;
}
//--------------------------------------------------------------------------------------------------
void Utilities::set_clipboard_text(const std::string &text) {
ControlFactory::get_instance()->_utilities_impl.set_clipboard_text(text);
}
//--------------------------------------------------------------------------------------------------
std::string Utilities::get_clipboard_text() {
return ControlFactory::get_instance()->_utilities_impl.get_clipboard_text();
}
//--------------------------------------------------------------------------------------------------
std::string Utilities::get_special_folder(FolderType type) {
return ControlFactory::get_instance()->_utilities_impl.get_special_folder(type);
}
//--------------------------------------------------------------------------------------------------
void Utilities::open_url(const std::string &url) {
return ControlFactory::get_instance()->_utilities_impl.open_url(url);
}
//--------------------------------------------------------------------------------------------------
bool Utilities::move_to_trash(const std::string &path) {
if (ControlFactory::get_instance()->_utilities_impl.move_to_trash)
return ControlFactory::get_instance()->_utilities_impl.move_to_trash(path);
else {
if (g_file_test(path.c_str(), G_FILE_TEST_IS_DIR)) {
if (base_rmdir_recursively(path.c_str()) < 0)
return false;
} else {
if (!base::remove(path))
return false;
}
return true;
}
}
//--------------------------------------------------------------------------------------------------
void Utilities::reveal_file(const std::string &path) {
ControlFactory::get_instance()->_utilities_impl.reveal_file(path);
}
//--------------------------------------------------------------------------------------------------
TimeoutHandle Utilities::add_timeout(float interval, const std::function<bool()> &callback) {
return ControlFactory::get_instance()->_utilities_impl.add_timeout(interval, callback);
}
//--------------------------------------------------------------------------------------------------
void Utilities::cancel_timeout(TimeoutHandle handle) {
ControlFactory::get_instance()->_utilities_impl.cancel_timeout(handle);
}
//--------------------------------------------------------------------------------------------------
void Utilities::add_end_ok_cancel_buttons(mforms::Box *box, mforms::Button *ok, mforms::Button *cancel) {
#ifdef __APPLE__
box->add_end(ok, false, true);
box->add_end(cancel, false, true);
#else
box->add_end(cancel, false, true);
box->add_end(ok, false, true);
#endif
}
//--------------------------------------------------------------------------------------------------
static void on_request_action(mforms::TextEntryAction action, mforms::Button *btn) {
if (action == mforms::EntryActivate)
btn->signal_clicked()->operator()();
}
//--------------------------------------------------------------------------------------------------
static void *_request_input_main(const std::string &title, const std::string &description,
const std::string &default_value, std::string *ret_value) {
// In order to avoid trouble with window z-ordering we explicitly ask to hide any wait window
// that could get in the way. Same for the splash screen.
Utilities::hide_wait_message();
mforms::Form input_form(NULL, (FormFlag)(FormDialogFrame | FormStayOnTop));
mforms::Table content;
mforms::ImageBox icon;
mforms::Label description_label("");
mforms::TextEntry edit;
mforms::Box button_box(true);
mforms::Button ok_button;
mforms::Button cancel_button;
input_form.set_title(title.empty() ? _("Enter a value") : title);
content.set_padding(12);
content.set_row_count(2);
content.set_row_spacing(10);
content.set_column_count(3);
content.set_column_spacing(4);
icon.set_image("message_edit.png");
content.add(&icon, 0, 1, 0, 2, HFillFlag | VFillFlag);
description_label.set_text(description);
description_label.set_style(BoldStyle);
edit.set_size(150, -1);
edit.set_value(default_value);
edit.signal_action()->connect(std::bind(&on_request_action, std::placeholders::_1, &ok_button));
content.add(&description_label, 1, 2, 0, 1, HFillFlag | VFillFlag);
content.add(&edit, 2, 3, 0, 1, HFillFlag | VFillFlag);
button_box.set_spacing(8);
ok_button.set_text(_("OK"));
ok_button.set_size(75, -1);
cancel_button.set_text(_("Cancel"));
cancel_button.set_size(75, -1);
Utilities::add_end_ok_cancel_buttons(&button_box, &ok_button, &cancel_button);
content.add(&button_box, 1, 3, 1, 2, HFillFlag | VFillFlag);
input_form.set_content(&content);
input_form.center();
edit.focus();
bool result = input_form.run_modal(&ok_button, &cancel_button);
if (result)
*ret_value = edit.get_string_value();
return (void *)result;
}
//--------------------------------------------------------------------------------------------------
static bool _request_input(const std::string &title, const std::string &description, const std::string &default_value,
std::string &ret_value) {
if (Utilities::in_main_thread())
return _request_input_main(title, description, default_value, &ret_value) != nullptr;
else
return Utilities::perform_from_main_thread(
std::bind(&_request_input_main, title, description, default_value, &ret_value)) != nullptr;
}
//--------------------------------------------------------------------------------------------------
bool Utilities::request_input(const std::string &title, const std::string &description,
const std::string &default_value, std::string &ret_value /*out*/) {
return _request_input(title, description, default_value, ret_value);
}
//--------------------------------------------------------------------------------------------------
void Utilities::store_password(const std::string &service, const std::string &account, const std::string &password) {
// in-memory cache
PasswordCache::get()->add_password(service, account, password.c_str());
// OS storage
logDebug("Storing password for '%s'@'%s'\n", account.c_str(), service.c_str());
ControlFactory::get_instance()->_utilities_impl.store_password(service, account, password);
}
//--------------------------------------------------------------------------------------------------
bool Utilities::find_password(const std::string &service, const std::string &account, std::string &password) {
const bool ret = ControlFactory::get_instance()->_utilities_impl.find_password(service, account, password);
logDebug("Looking up password for '%s'@'%s' has %s\n", account.c_str(), service.c_str(),
ret ? "succeeded" : "failed");
if (ret)
PasswordCache::get()->add_password(service, account, password.c_str());
return ret;
}
//--------------------------------------------------------------------------------------------------
bool Utilities::find_cached_password(const std::string &service, const std::string &account, std::string &password) {
return PasswordCache::get()->get_password(service, account, password);
}
//--------------------------------------------------------------------------------------------------
void Utilities::forget_cached_password(const std::string &service, const std::string &account) {
logDebug2("Forgetting cached password for '%s'@'%s'\n", account.c_str(), service.c_str());
PasswordCache::get()->remove_password(service, account);
}
//--------------------------------------------------------------------------------------------------
void Utilities::forget_password(const std::string &service, const std::string &account) {
Utilities::forget_cached_password(service, account);
logDebug("Forgetting password for '%s'@'%s'\n", account.c_str(), service.c_str());
ControlFactory::get_instance()->_utilities_impl.forget_password(service, account);
}
//-------------------------------------------------------------------------------
void *Utilities::perform_from_main_thread(const std::function<void *()> &slot, bool wait_done) {
return ControlFactory::get_instance()->_utilities_impl.perform_from_main_thread(slot, wait_done);
}
//--------------------------------------------------------------------------------------------------
static void *_ask_for_password_main(const std::string &title, const std::string &service,
std::string *username /*in/out*/, bool prompt_storage,
std::string *ret_password /*out*/, bool *ret_store /*out*/) {
logDebug("Creating and showing password dialog\n");
Utilities::hide_wait_message();
mforms::Form password_form(NULL, (FormFlag)(FormDialogFrame | FormStayOnTop));
mforms::Table content;
mforms::ImageBox icon;
mforms::Label description("");
mforms::Label service_description_label("");
mforms::Label service_label("");
mforms::Label user_description_label("");
mforms::Label pw_description_label("");
mforms::TextEntry password_edit(PasswordEntry);
mforms::CheckBox save_password_box;
mforms::Box button_box(true);
mforms::Button ok_button;
mforms::Button cancel_button;
// Since we cannot simply change the function's signature (I only say: python) we have to use
// a different way to pass an additional value in. The description used for the login details
// request is passed in the title parameter as well, separated by the pipe symbol.
std::vector<std::string> title_parts = base::split(title, "|", 2);
std::string caption;
if (title_parts.size() == 0 || title_parts[0].empty())
caption = _("MySQL Workbench Authentication");
else
caption = title_parts[0];
password_form.set_title(caption);
password_form.set_name(caption);
content.set_padding(12);
content.set_row_count(6);
content.set_row_spacing(prompt_storage ? 8 : 7);
content.set_column_count(3);
content.set_column_spacing(4);
icon.set_image("message_wb_lock.png");
content.add(&icon, 0, 1, 0, 6, HFillFlag | VFillFlag);
if (title_parts.size() < 2 || title_parts[1].empty())
description.set_text(_("Please enter password for the following service:"));
else
description.set_text(title_parts[1]);
description.set_wrap_text(true);
description.set_style(BigBoldStyle);
description.set_size(300, -1);
content.add(&description, 1, 3, 0, 1, HFillFlag | HExpandFlag | VFillFlag);
service_description_label.set_text(_("Service:"));
service_description_label.set_text_align(MiddleRight);
service_description_label.set_style(BoldStyle);
service_label.set_text(service);
content.add(&service_description_label, 1, 2, 1, 2, HFillFlag | VFillFlag);
content.add(&service_label, 2, 3, 1, 2, HFillFlag | VFillFlag);
user_description_label.set_text(_("User:"));
user_description_label.set_text_align(MiddleRight);
user_description_label.set_style(BoldStyle);
// Create an edit box for the user name if the given one is not set, otherwise just display the name.
mforms::TextEntry *user_edit = NULL;
if (username->empty()) {
user_edit = mforms::manage(new mforms::TextEntry());
user_edit->set_value(_("<user name>"));
content.add(&user_description_label, 1, 2, 2, 3, HFillFlag | VFillFlag);
content.add(user_edit, 2, 3, 2, 3, HFillFlag | VFillFlag);
} else {
mforms::Label *user_label = mforms::manage(new mforms::Label(*username));
content.add(&user_description_label, 1, 2, 2, 3, HFillFlag | VFillFlag);
content.add(user_label, 2, 3, 2, 3, HFillFlag | VFillFlag);
}
pw_description_label.set_text(_("Password:"));
pw_description_label.set_text_align(MiddleRight);
pw_description_label.set_style(BoldStyle);
password_edit.set_name("Password");
content.add(&pw_description_label, 1, 2, 3, 4, HFillFlag | VFillFlag);
content.add(&password_edit, 2, 3, 3, 4, HFillFlag | HExpandFlag);
if (prompt_storage) {
#ifdef _MSC_VER
save_password_box.set_text(_("Save password in vault"));
#else
save_password_box.set_text(_("Save password in keychain"));
#endif
content.add(&save_password_box, 2, 3, 4, 5, HExpandFlag | HFillFlag);
}
button_box.set_spacing(8);
button_box.set_name("Button Bar");
ok_button.set_text(_("OK"));
// ok_button.set_size(75, -1);
cancel_button.set_text(_("Cancel"));
// cancel_button.set_size(75, -1);
Utilities::add_end_ok_cancel_buttons(&button_box, &ok_button, &cancel_button);
if (prompt_storage)
content.add(&button_box, 1, 3, 5, 6, HFillFlag | VFillFlag);
else
content.add(&button_box, 1, 3, 4, 5, HFillFlag | VFillFlag);
password_form.set_content(&content);
password_form.center();
password_edit.focus();
password_edit.signal_action()->connect(std::bind(&on_request_action, std::placeholders::_1, &ok_button));
bool result = password_form.run_modal(&ok_button, &cancel_button);
if (result) {
*ret_password = password_edit.get_string_value();
*ret_store = save_password_box.get_active();
if (user_edit != NULL)
*username = user_edit->get_string_value();
// always store in cache
PasswordCache::get()->add_password(service, *username, ret_password->c_str());
}
return (void *)result;
}
static bool _ask_for_password(const std::string &title, const std::string &service, std::string &username /*in/out*/,
bool prompt_storage, std::string &ret_password /*out*/, bool &ret_store /*out*/) {
if (Utilities::in_main_thread())
return _ask_for_password_main(title, service, &username, prompt_storage, &ret_password, &ret_store) != NULL;
else
return Utilities::perform_from_main_thread(std::bind(&_ask_for_password_main, title, service, &username,
prompt_storage, &ret_password, &ret_store)) != NULL;
}
//--------------------------------------------------------------------------------------------------
bool Utilities::ask_for_password(const std::string &title, const std::string &service, const std::string &username,
std::string &ret_password /*out*/) {
std::string ret_username = username;
bool dummy = false;
return _ask_for_password(title, service, ret_username, false, ret_password, dummy);
}
//--------------------------------------------------------------------------------------------------
/**
* Shows a dialog to request the password for the given service and user name.
*
* @param title Optional title describing the reason for the password request (eg. "Connect to MySQL Server")
* @param service A description for what the password is required (e.g. "MySQL Server 5.1 on Thunder").
* @param username [in/out] The name of the user for which to request the password.
* @param password [out] Contains the password on return.
*
* @return True if the user pressed OK, otherwise false.
*/
bool Utilities::ask_for_password_check_store(const std::string &title, const std::string &service,
std::string &username, std::string &password, bool &store) {
return _ask_for_password(title, service, username, true, password, store);
}
//--------------------------------------------------------------------------------------------------
/**
* Shows a dialog to request the password for the given service and user name.
* If requested by the user the password will be saved using either system facilities like macOS Keychain or
* Gnome Keyring, or an encrypted file.
*
* @param title Optional title describing the reason for the password request (eg. "Connect to MySQL Server")
* @param service A description for what the password is required (e.g. "MySQL Server 5.1 on Thunder").
* @param username The name of the user for which to request the password. If empty on enter then the user name can
* also be edited.
* @param reset_password Delete stored password and ask for a new one.
* @param password [out] Contains the password on return.
*
* @return True if the user pressed OK, otherwise false.
*/
bool Utilities::credentials_for_service(const std::string &title, const std::string &service,
std::string &username /*in/out*/, bool reset_password,
std::string &password /*out*/) {
if (!reset_password && find_password(service, username, password))
return true;
if (reset_password)
forget_password(service, username);
bool should_store_password_out = false;
if (ask_for_password_check_store(title, service, username, password, should_store_password_out)) {
if (should_store_password_out) {
try {
store_password(service, username, password);
} catch (std::exception &exc) {
logWarning("Could not store password vault: %s\n", exc.what());
show_warning(title.empty() ? _("Error Storing Password") : title,
std::string("There was an error storing the password:\n") + exc.what(), "OK");
}
}
return true;
}
return false;
}
//--------------------------------------------------------------------------------------------------
#ifdef _MSC_VER
static int modal_loops = 0;
void Utilities::enter_modal_loop() {
modal_loops++;
}
void Utilities::leave_modal_loop() {
modal_loops--;
}
bool Utilities::in_modal_loop() {
return modal_loops > 0;
}
#endif
//--------------------------------------------------------------------------------------------------
static cairo_user_data_key_t hidpi_icon_key;
/**
* Helper function to simplify icon loading. Returns NULL if the icon could not be found or
* something wrong happened while loading.
*/
cairo_surface_t *Utilities::load_icon(const std::string &name, bool allow_hidpi) {
if (name.empty())
return NULL;
#ifdef __APPLE__
allow_hidpi = true; // For OSX we always want hires images.
#endif
if (allow_hidpi && mforms::App::get()->backing_scale_factor() > 1.0) {
std::string hidpi_name = base::strip_extension(name) + "@2x" + base::extension(name);
std::string icon_path = App::get()->get_resource_path(hidpi_name);
cairo_surface_t *tmp = mdc::surface_from_png_image(icon_path);
if (tmp) {
// Mark the surface as being a hi-res variant of a standard icon.
cairo_surface_set_user_data(tmp, &hidpi_icon_key, (void *)1, NULL);
return tmp;
}
}
std::string icon_path = App::get()->get_resource_path(name);
return mdc::surface_from_png_image(icon_path);
}
//--------------------------------------------------------------------------------------------------
bool Utilities::is_hidpi_icon(cairo_surface_t *s) {
return cairo_surface_get_user_data(s, &hidpi_icon_key) == (void *)1;
}
//--------------------------------------------------------------------------------------------------
bool Utilities::icon_needs_reload(cairo_surface_t *s) {
float scale = s && mforms::Utilities::is_hidpi_icon(s) ? 2.0f : 1.0f;
return mforms::App::get()->backing_scale_factor() != scale;
}
//--------------------------------------------------------------------------------------------------
void Utilities::paint_icon(cairo_t *cr, cairo_surface_t *image, double x, double y, float alpha) {
if (cr == nullptr || image == nullptr)
return;
float backing_scale_factor = mforms::App::get()->backing_scale_factor();
if (backing_scale_factor > 1 && mforms::Utilities::is_hidpi_icon(image)) {
cairo_save(cr);
cairo_scale(cr, 1 / backing_scale_factor, 1 / backing_scale_factor);
cairo_set_source_surface(cr, image, x * backing_scale_factor, y * backing_scale_factor);
if (alpha == 1.0)
cairo_paint(cr);
else
cairo_paint_with_alpha(cr, alpha);
cairo_restore(cr);
} else if (backing_scale_factor == 1 && mforms::Utilities::is_hidpi_icon(image)) {
// Special case where the icon is for hidpi but the screen is not.
// This happens when the icon was cached while the window was
// on a hidpi screen but is then dragged to a low dpi screen.
// Ideally this would trigger a reload of the icon.
cairo_save(cr);
cairo_scale(cr, 0.5, 0.5);
cairo_set_source_surface(cr, image, x * 2, y * 2);
if (alpha == 1.0)
cairo_paint(cr);
else
cairo_paint_with_alpha(cr, alpha);
cairo_restore(cr);
logDebug3("Icon is for hidpi screen but the screen is not.\n");
} else {
cairo_set_source_surface(cr, image, x, y);
if (alpha == 1.0)
cairo_paint(cr);
else
cairo_paint_with_alpha(cr, alpha);
}
}
//--------------------------------------------------------------------------------------------------
base::Size Utilities::getImageSize(cairo_surface_t *icon) {
base::Size result(cairo_image_surface_get_width(icon), cairo_image_surface_get_height(icon));
if (mforms::Utilities::is_hidpi_icon(icon)) {
result.width /= 2;
result.height /= 2;
}
return result;
}
//--------------------------------------------------------------------------------------------------
/**
* Shortens the string so that it fits into the given width. If there is already enough room then
* the input is simply returned. Otherwise letters are removed (via binary search) and ellipses
* are added so that the entire result fits into that width.
*/
std::string Utilities::shorten_string(cairo_t *cr, const std::string &text, double width) {
int ellipsis_width = 0;
size_t length;
size_t l, h, n, w;
cairo_text_extents_t extents;
// If the text fits already, return the input.
cairo_text_extents(cr, text.c_str(), &extents);
if (extents.width <= width)
return text;
length = g_utf8_strlen(text.data(), (gssize)text.size());
if (length == 0 || width <= 0)
return "";
else {
cairo_text_extents(cr, "...", &extents);
ellipsis_width = (int)ceil(extents.width);
}
const gchar *head = text.c_str();
if (width <= ellipsis_width)
return "";
else {
// Do a binary search for the optimal string length which fits into the given width.
l = 0;
h = length - 1;
while (l < h) {
n = (l + h) / 2;
// Skip to the nth position, which needs the following loop as we don't have direct
// access to a char in an utf-8 buffer (one of the limitations of that transformation format).
const gchar *tail = head;
for (size_t i = 0; i < n; i++)
tail = g_utf8_next_char(tail);
gchar *part = g_strndup(head, (gsize)(tail - head));
cairo_text_extents(cr, part, &extents);
g_free(part);
w = (int)ceil(extents.width) + ellipsis_width;
if (w <= width)
l = n + 1;
else
h = n;
}
const gchar *begin = g_utf8_offset_to_pointer(text.data(), 0);
const gchar *end = g_utf8_offset_to_pointer(begin, (glong)(l - 1));
std::string temp = std::string(text.data(), end - begin) + "...";
return temp;
}
return "";
}
//--------------------------------------------------------------------------------------------------
double Utilities::get_text_width(const std::string &text, const std::string &font) {
return ControlFactory::get_instance()->_utilities_impl.get_text_width(text, font);
}
//--------------------------------------------------------------------------------------------------
bool Utilities::in_main_thread() {
return g_thread_self() == _mforms_main_thread;
}
void Utilities::set_thread_name(const std::string &name) {
if (ControlFactory::get_instance()->_utilities_impl.set_thread_name)
ControlFactory::get_instance()->_utilities_impl.set_thread_name(name);
}
void Utilities::driver_shutdown() {
if (Utilities::_driver_shutdown_cb)
Utilities::_driver_shutdown_cb();
}
void Utilities::add_driver_shutdown_callback(const std::function<void()> &slot) {
Utilities::_driver_shutdown_cb = slot;
}