backend/wbprivate/sqlide/wb_sql_editor_panel.cpp (999 lines of code) (raw):
/*
* Copyright (c) 2014, 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
*/
#include "wb_sql_editor_form.h"
#include "wb_sql_editor_panel.h"
#include "wb_sql_editor_result_panel.h"
#include "sqlide/sql_editor_be.h"
#include "sqlide/recordset_cdbc_storage.h"
#include "grtpp_notifications.h"
#include "base/log.h"
#include "base/file_functions.h"
#include "base/util_functions.h"
#include "mforms/toolbar.h"
#include "mforms/menubar.h"
#include "mforms/code_editor.h"
#include "mforms/find_panel.h"
#include "mforms/filechooser.h"
#include "workbench/wb_command_ui.h"
#include "base/boost_smart_ptr_helpers.h"
#include "objimpl/wrapper/mforms_ObjectReference_impl.h"
#include "objimpl/db.query/db_query_Resultset.h"
#include "grtui/file_charset_dialog.h"
#include "grtsqlparser/mysql_parser_services.h"
#include "mysql/MySQLRecognizerCommon.h"
#include <fstream>
#include <sstream>
// 20 MB max file size for auto-restoring
#define MAX_FILE_SIZE_FOR_AUTO_RESTORE 20000000
DEFAULT_LOG_DOMAIN("SqlEditorPanel");
using namespace bec;
using namespace base;
//--------------------------------------------------------------------------------------------------
SqlEditorPanel::SqlEditorPanel(SqlEditorForm *owner, bool is_scratch, bool start_collapsed)
: mforms::AppView(false, "Query Buffer", "db.query.QueryBuffer", false),
_form(owner),
_editor_box(false),
_splitter(false, false),
#ifdef __APPLE__
_lower_tabview(mforms::TabViewEditorBottomPinnable), // TODO: Windows, Linux
#else
_lower_tabview(mforms::TabViewEditorBottom),
#endif
_lower_dock_delegate(&_lower_tabview, db_query_QueryEditor::static_class_name()),
_lower_dock(&_lower_dock_delegate, false),
_tab_action_box(true),
_tab_action_apply(mforms::SmallButton),
_tab_action_revert(mforms::SmallButton),
_tab_action_info("Read Only"),
_rs_sequence(0),
_busy(false),
_is_scratch(is_scratch) {
db_query_QueryEditorRef grtobj(grt::Initialized);
grtobj->resultDockingPoint(mforms_to_grt(&_lower_dock));
_autosave_file_suffix = grtobj.id();
// In opposition to the object editors, each individual sql editor gets an own parser context
// (and hence an own parser), to allow concurrent and multi threaded work.
parsers::MySQLParserServices::Ref services = parsers::MySQLParserServices::get();
parsers::MySQLParserContext::Ref context = services->createParserContext(
owner->rdbms()->characterSets(), owner->rdbms_version(), owner->sql_mode(), owner->lower_case_table_names() != 0);
parsers::SymbolTable *functionSymbols = parsers::functionSymbolsForVersion(bec::versionToEnum(owner->rdbms_version()));
_editor = MySQLEditor::create(context, owner->work_parser_context(), { functionSymbols, owner->databaseSymbols() }, grtobj);
_editor->set_sql_mode(owner->sql_mode());
_editor->set_current_schema(owner->active_schema());
UIForm::scoped_connect(_editor->text_change_signal(), std::bind(&SqlEditorPanel::update_title, this));
add(&_splitter, true, true);
mforms::CodeEditor *code_editor = editor_be()->get_editor_control();
code_editor->set_name("Code Editor");
code_editor->setInternalName("code editor");
_editor_box.add(setup_editor_toolbar(), false, true);
_editor_box.add_end(code_editor, true, true);
code_editor->set_font(
grt::StringRef::cast_from(bec::GRTManager::get()->get_app_option("workbench.general.Editor:Font")));
code_editor->set_status_text("");
code_editor->set_show_find_panel_callback(
std::bind(&SqlEditorPanel::show_find_panel, this, std::placeholders::_1, std::placeholders::_2));
if (start_collapsed)
_editor->get_editor_control()->set_size(-1, 25);
_splitter.add(&_editor_box, 150);
_splitter.add(&_lower_tabview, 150);
_editor_box.set_name("Editor Area");
_editor_box.setInternalName("Editor area");
_lower_tabview.set_name("Resultset Placeholder");
_lower_tabview.setInternalName("Resultset placeholder");
UIForm::scoped_connect(_splitter.signal_position_changed(), std::bind(&SqlEditorPanel::splitter_resized, this));
_tab_action_box.set_spacing(4);
_tab_action_box.add_end(&_tab_action_info, false, true);
_tab_action_box.add_end(&_tab_action_icon, false, true);
_tab_action_box.add_end(&_tab_action_revert, false, true);
_tab_action_box.add_end(&_tab_action_apply, false, true);
_tab_action_icon.set_image(mforms::App::get()->get_resource_path("mini_notice.png"));
_tab_action_icon.show(false);
_tab_action_info.show(false);
_tab_action_apply.enable_internal_padding(true);
_tab_action_apply.set_text("Apply");
_tab_action_apply.signal_clicked()->connect(std::bind(&SqlEditorPanel::apply_clicked, this));
_tab_action_revert.enable_internal_padding(true);
_tab_action_revert.set_text("Revert");
_tab_action_revert.signal_clicked()->connect(std::bind(&SqlEditorPanel::revert_clicked, this));
#ifdef _MSC_VER
// 19 is the size of the tabs in the bottom tabview.
_tab_action_apply.set_size(-1, 19);
_tab_action_revert.set_size(-1, 19);
_tab_action_box.set_size(-1, 19);
_tab_action_box.set_back_color(Color::getApplicationColorAsString(AppColorTabUnselected, false));
#endif
_lower_tabview.set_aux_view(&_tab_action_box);
_lower_tabview.set_allows_reordering(true);
_lower_tabview.signal_tab_reordered()->connect(std::bind(
&SqlEditorPanel::lower_tab_reordered, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
_lower_tabview.signal_tab_changed()->connect(std::bind(&SqlEditorPanel::lower_tab_switched, this));
_lower_tabview.signal_tab_closing()->connect(
std::bind(&SqlEditorPanel::lower_tab_closing, this, std::placeholders::_1));
_lower_tabview.signal_tab_closed()->connect(
std::bind(&SqlEditorPanel::lower_tab_closed, this, std::placeholders::_1, std::placeholders::_2));
_lower_tabview.signal_tab_pin_changed()->connect(
std::bind(&SqlEditorPanel::tab_pinned, this, std::placeholders::_1, std::placeholders::_2));
_lower_tabview.is_pinned = std::bind(&SqlEditorPanel::is_pinned, this, std::placeholders::_1);
_lower_tabview.set_tab_menu(&_lower_tab_menu);
_splitter.set_expanded(false, false);
set_on_close(std::bind(&SqlEditorPanel::on_close_by_user, this));
_lower_tab_menu.signal_will_show()->connect(std::bind(&SqlEditorPanel::tab_menu_will_show, this));
_lower_tab_menu.add_item_with_title("Rename Tab", std::bind(&SqlEditorPanel::rename_tab_clicked, this), "Rename Tab", "rename");
_lower_tab_menu.add_check_item_with_title("Pin Tab", std::bind(&SqlEditorPanel::pin_tab_clicked, this), "Pin Tab", "pin");
_lower_tab_menu.add_separator();
_lower_tab_menu.add_item_with_title("Close Tab", std::bind(&SqlEditorPanel::close_tab_clicked, this), "Close Tab", "close");
_lower_tab_menu.add_item_with_title("Close Other Tabs", std::bind(&SqlEditorPanel::close_other_tabs_clicked, this),
"Close Other Tabs", "close_others");
}
//--------------------------------------------------------------------------------------------------
SqlEditorPanel::~SqlEditorPanel() {
_editor->stop_processing();
_editor->cancel_auto_completion();
}
//--------------------------------------------------------------------------------------------------
db_query_QueryEditorRef SqlEditorPanel::grtobj() {
return db_query_QueryEditorRef::cast_from(_editor->grtobj());
}
//--------------------------------------------------------------------------------------------------
bool SqlEditorPanel::on_close_by_user() {
// this can also get closed when close_all_view() is called when the connection is closed
if (_form->is_closing() || can_close()) {
// do not call close, since that would undock ourselves and the caller will also undock this
// we just need to be sure that the d-tor is called in all closing methods (close the tab itself from the X and
// through kbd,
// close the connection, close WB)
// close();
return true;
}
return false;
}
//--------------------------------------------------------------------------------------------------
bool SqlEditorPanel::can_close() {
if (_busy)
return false;
bool check_editors = true;
// if Save of workspace on close is enabled, we don't need to check whether there are unsaved scratch
// SQL editors but other stuff should be checked.
grt::ValueRef option(bec::GRTManager::get()->get_app_option("workbench:SaveSQLWorkspaceOnClose"));
if (option.is_valid() && *grt::IntegerRef::cast_from(option))
check_editors = false;
// don't need to check for unsaved changes when closing the whole form
// if save-workspace is enabled, since they'll get autosaved anyway
// otoh, when closing the file itself, it should check
if (!_form->is_closing())
check_editors = true;
if (!_is_scratch && check_editors) {
if (is_dirty()) {
int result = mforms::Utilities::show_warning(_("Close SQL Tab"),
strfmt(_("SQL script %s has unsaved changes.\n"
"Would you like to Save these changes before closing?"),
get_title().c_str()),
_("Save"), _("Cancel"), _("Don't Save"));
if (result == mforms::ResultCancel)
return false;
else if (result == mforms::ResultOk) {
if (!save())
return false;
} else
_editor->get_editor_control()->reset_dirty();
}
}
// check if there are unsaved recordset changes
int edited_recordsets = 0;
for (int c = _lower_tabview.page_count(), i = 0; i < c; i++) {
SqlEditorResult *result = dynamic_cast<SqlEditorResult *>(_lower_tabview.get_page(i));
if (result && result->has_pending_changes())
edited_recordsets++;
}
int r = -999;
if (edited_recordsets == 1)
r = mforms::Utilities::show_warning(
_("Close SQL Tab"), strfmt(_("An edited recordset has unsaved changes in %s.\n"
"Would you like to save these changes, discard them or cancel closing the page?"),
get_title().c_str()),
_("Save Changes"), _("Cancel"), _("Don't Save"));
else if (edited_recordsets > 0)
r = mforms::Utilities::show_warning(
_("Close SQL Tab"),
strfmt(_("There are %i recordsets with unsaved changes in %s.\n"
"Would you like to save these changes, discard them or cancel closing to review them manually?"),
edited_recordsets, get_title().c_str()),
_("Save All"), _("Cancel"), _("Don't Save"));
bool success = true;
if (r != -999) {
if (r == mforms::ResultCancel)
success = false;
else {
for (int c = _lower_tabview.page_count(), i = 0; i < c; i++) {
SqlEditorResult *result = dynamic_cast<SqlEditorResult *>(_lower_tabview.get_page(i));
if (result && result->has_pending_changes()) {
try {
if (r == mforms::ResultOk)
result->apply_changes();
else
result->discard_changes();
} catch (const std::exception &exc) {
if (mforms::Utilities::show_error(
_("Save Changes"), strfmt(_("An error occurred while saving changes to the recordset %s\n%s"),
result->recordset()->caption().c_str(), exc.what()),
_("Ignore"), _("Cancel"), "") == mforms::ResultCancel) {
success = false;
break;
}
}
}
}
}
}
// close everything now (recordsets should already have their dirty flag cleared)
if (success && !_lower_dock.close_all_views())
return false;
return success;
}
//--------------------------------------------------------------------------------------------------
void SqlEditorPanel::apply_clicked() {
SqlEditorResult *result = active_result_panel();
if (result)
result->apply_changes();
}
//--------------------------------------------------------------------------------------------------
void SqlEditorPanel::revert_clicked() {
SqlEditorResult *result = active_result_panel();
if (result)
result->discard_changes();
}
//--------------------------------------------------------------------------------------------------
void SqlEditorPanel::resultset_edited() {
SqlEditorResult *result = active_result_panel();
Recordset::Ref rset;
if (result && (rset = result->recordset())) {
bool edited = rset->has_pending_changes();
_tab_action_apply.set_enabled(edited);
_tab_action_revert.set_enabled(edited);
_form->get_menubar()->set_item_enabled("query.save_edits", edited);
_form->get_menubar()->set_item_enabled("query.discard_edits", edited);
}
}
//--------------------------------------------------------------------------------------------------
void SqlEditorPanel::splitter_resized() {
if (_lower_tabview.page_count() > 0) {
bec::GRTManager::get()->set_app_option("DbSqlEditor:ResultSplitterPosition",
grt::IntegerRef(_splitter.get_divider_position()));
}
}
//--------------------------------------------------------------------------------------------------
static bool check_if_file_too_big_to_restore(const std::string &path, const std::string &file_caption,
bool allow_save_as = false) {
std::int64_t length;
if ((length = get_file_size(path.c_str())) > MAX_FILE_SIZE_FOR_AUTO_RESTORE) {
again:
std::string size = sizefmt(length, false);
int rc = mforms::Utilities::show_warning(
"Restore Workspace", strfmt("The file %s has a size of %s. Are you sure you want to restore this file?",
file_caption.c_str(), size.c_str()),
"Restore", "Skip", allow_save_as ? "Save As..." : "");
if (rc == mforms::ResultCancel)
return false;
if (rc == mforms::ResultOther) {
mforms::FileChooser fchooser(mforms::SaveFile);
fchooser.set_title(_("Save File As..."));
if (fchooser.run_modal()) {
if (!base::copyFile(path.c_str(), fchooser.get_path().c_str())) {
if (mforms::Utilities::show_error("Save File",
strfmt("File %s could not be saved.", fchooser.get_path().c_str()),
_("Retry"), _("Cancel"), "") == mforms::ResultOk)
goto again;
}
} else
goto again;
return false;
}
}
return true;
}
#define EDITOR_TEXT_LIMIT 100 * 1024 * 1024
SqlEditorPanel::AutoSaveInfo::AutoSaveInfo(const std::string &info_file) : word_wrap(false), show_special(false) {
wchar_t buffer[4098] = {0};
std::wifstream f = openTextInputStream(info_file);
while (f.getline(buffer, sizeof(buffer))) {
std::string key, value;
base::partition(base::wstring_to_string(buffer), "=", key, value);
if (key == "orig_encoding")
orig_encoding = value;
else if (key == "type")
type = value;
else if (key == "filename")
filename = value;
else if (key == "title")
title = value;
else if (key == "word_wrap")
word_wrap = value == "1";
else if (key == "show_special")
show_special = value == "1";
else if (key == "first_visible_line")
first_visible_line = base::atoi<int>(value, 0);
else if (key == "caret_pos")
caret_pos = base::atoi<int>(value, 0);
}
}
SqlEditorPanel::AutoSaveInfo SqlEditorPanel::AutoSaveInfo::old_scratch(const std::string &scratch_file) {
AutoSaveInfo info;
info.title = base::strip_extension(base::basename(scratch_file));
if (base::is_number(info.title))
info.title = base::strfmt("Query %i", 1 + base::atoi<int>(info.title, 0));
info.type = "scratch";
return info;
}
SqlEditorPanel::AutoSaveInfo SqlEditorPanel::AutoSaveInfo::old_autosave(const std::string &autosave_file) {
char buffer[4098];
AutoSaveInfo info;
info.title = base::strip_extension(base::basename(autosave_file));
info.type = "file";
std::ifstream f(base::strip_extension(autosave_file).c_str());
if (f.getline(buffer, sizeof(buffer)))
info.filename = buffer;
if (f.getline(buffer, sizeof(buffer)))
info.orig_encoding = buffer;
return info;
}
bool SqlEditorPanel::load_autosave(const AutoSaveInfo &info, const std::string &text_file) {
_orig_encoding = info.orig_encoding;
_file_timestamp = 0;
_is_scratch = (info.type == "scratch");
// there's no autosave
if (text_file.empty() || !base::file_exists(text_file)) {
if (!info.filename.empty() &&
!check_if_file_too_big_to_restore(info.filename, strfmt("Saved editor '%s'", info.title.c_str()))) {
return false;
}
// if this was a file, try to load it
if (!info.filename.empty() && load_from(info.filename, info.orig_encoding, false) != Loaded)
return false;
} else {
// check if autosave too big
if (!check_if_file_too_big_to_restore(text_file, strfmt("Saved editor '%s'", info.title.c_str()))) {
return false;
}
// load the autosave
if (load_from(text_file, info.orig_encoding, true) != Loaded)
return false;
}
_filename = info.filename;
if (!_filename.empty())
base::file_mtime(_filename, _file_timestamp);
set_title(info.title);
mforms::ToolBarItem *item = get_toolbar()->find_item("query.toggleInvisible");
item->set_checked(info.show_special);
(*item->signal_activated())(item);
item = get_toolbar()->find_item("query.toggleWordWrap");
item->set_checked(info.word_wrap);
(*item->signal_activated())(item);
_editor->get_editor_control()->set_caret_pos(info.caret_pos);
_editor->get_editor_control()->send_editor(SCI_SETFIRSTVISIBLELINE, info.first_visible_line, 0);
return true;
}
//--------------------------------------------------------------------------------------------------
SqlEditorPanel::LoadResult SqlEditorPanel::load_from(const std::string &file, const std::string &encoding,
bool keep_dirty) {
GError *error = NULL;
gchar *data;
gsize length;
gsize file_size = base_get_file_size(file.c_str());
if (file_size > EDITOR_TEXT_LIMIT) {
// File is larger than 100 MB. Tell the user we are going to switch off code folding and
// auto completion.
int result = mforms::Utilities::show_warning(
_("Large File"), strfmt(_("The file \"%s\" has a size "
"of %.2f MB. Are you sure you want to open this large file?\n\nNote: code folding "
"will be disabled for this file.\n\nClick Run SQL Script... to just execute the file."),
file.c_str(), file_size / 1024.0 / 1024.0),
_("Open"), _("Cancel"), _("Run SQL Script..."));
if (result == mforms::ResultCancel)
return Cancelled;
else if (result == mforms::ResultOther)
return RunInstead;
}
_orig_encoding = encoding;
if (!g_file_get_contents(file.c_str(), &data, &length, &error)) {
logError("Could not read file %s: %s\n", file.c_str(), error->message);
std::string what = error->message;
g_error_free(error);
throw std::runtime_error(what);
}
char *utf8_data;
std::string original_encoding;
FileCharsetDialog::Result result =
FileCharsetDialog::ensure_filedata_utf8(data, length, encoding, file, utf8_data, &original_encoding);
if (result == FileCharsetDialog::Cancelled) {
g_free(data);
return Cancelled;
} else if (result == FileCharsetDialog::RunInstead) {
g_free(data);
return RunInstead;
}
// if original data was in utf8, utf8_data comes back NULL
if (!utf8_data)
utf8_data = data;
else
g_free(data);
_editor->set_refresh_enabled(true);
_editor->sql(utf8_data ? utf8_data : "");
g_free(utf8_data);
if (!keep_dirty) {
_editor->get_editor_control()->reset_dirty();
_filename = file;
_orig_encoding = original_encoding;
set_title(strip_extension(basename(file)));
}
if (!file_mtime(file, _file_timestamp)) {
logWarning("Can't get timestamp for %s\n", file.c_str());
_file_timestamp = 0;
}
return Loaded;
}
//--------------------------------------------------------------------------------------------------
void SqlEditorPanel::close() {
_form->remove_sql_editor(this);
}
//--------------------------------------------------------------------------------------------------
bool SqlEditorPanel::save_as(const std::string &path) {
if (path.empty()) {
mforms::FileChooser dlg(mforms::SaveFile);
dlg.set_title(_("Save SQL Script"));
dlg.set_extensions("SQL Files (*.sql)|*.sql", "sql");
if (!_filename.empty())
dlg.set_path(_filename);
if (!dlg.run_modal())
return false;
_filename = dlg.get_path();
}
if (save()) {
set_title(strip_extension(basename(_filename)));
{
NotificationInfo info;
info["opener"] = "SqlEditorForm";
info["path"] = _filename;
NotificationCenter::get()->send("GNDocumentOpened", this, info);
}
return true;
}
return false;
}
//--------------------------------------------------------------------------------------------------
bool SqlEditorPanel::save() {
if (_filename.empty())
return save_as("");
GError *error = NULL;
// File extension check is already done in FileChooser.
bec::GRTManager::get()->replace_status_text(strfmt(_("Saving SQL script to '%s'..."), _filename.c_str()));
std::pair<const char *, size_t> text = text_data();
if (!g_file_set_contents(_filename.c_str(), text.first, text.second, &error)) {
logError("Could not save script %s: %s\n", _filename.c_str(), error->message);
bec::GRTManager::get()->replace_status_text(strfmt(_("Error saving SQL script to '%s'."), _filename.c_str()));
mforms::Utilities::show_error(strfmt(_("Error writing file %s"), _filename.c_str()), error->message, _("OK"));
g_error_free(error);
return false;
}
// reset dirty marker, but not the undo stack
_editor->get_editor_control()->reset_dirty();
_is_scratch = false; // saving a file makes it not a scratch buffer anymore
file_mtime(_filename, _file_timestamp);
bec::GRTManager::get()->replace_status_text(strfmt(_("SQL script saved to '%s'"), _filename.c_str()));
// update autosave state
_form->auto_save();
update_title();
return true;
}
//--------------------------------------------------------------------------------------------------
void SqlEditorPanel::revert_to_saved() {
_editor->sql("");
if (load_from(_filename, _orig_encoding) == Loaded) {
{
NotificationInfo info;
info["opener"] = "SqlEditorForm";
info["path"] = _filename;
NotificationCenter::get()->send("GNDocumentOpened", this, info);
}
_form->auto_save();
bec::GRTManager::get()->replace_status_text(strfmt(_("Reverted to saved '%s'"), _filename.c_str()));
}
}
//--------------------------------------------------------------------------------------------------
std::string SqlEditorPanel::autosave_file_suffix() {
return _autosave_file_suffix;
}
//--------------------------------------------------------------------------------------------------
void SqlEditorPanel::auto_save(const std::string &path) {
// save info about the file
{
std::wofstream f = openTextOutputStream(base::makePath(path, _autosave_file_suffix + ".info"));
std::string content;
if (_is_scratch)
content += "type=scratch\n";
else
content += "type=file\n";
if (!_is_scratch && !_filename.empty()) {
content += "filename=" + _filename + "\n";
}
content += "orig_encoding=" + _orig_encoding + "\n";
content += "title=" + _title + "\n";
if (get_toolbar()->get_item_checked("query.toggleInvisible"))
content += "show_special=1\n";
else
content += "show_special=0\n";
if (get_toolbar()->get_item_checked("query.toggleWordWrap"))
content += "word_wrap=1\n";
else
content += "word_wrap=0\n";
size_t caret_pos = _editor->get_editor_control()->get_caret_pos();
content += "caret_pos=" + std::to_string(caret_pos) + "\n";
size_t first_line = _editor->get_editor_control()->send_editor(SCI_GETFIRSTVISIBLELINE, 0, 0);
content += "first_visible_line=" + std::to_string(first_line) + "\n";
if (f.good())
f << base::string_to_wstring(content);
f.close();
}
std::string fn = base::makePath(path, _autosave_file_suffix + ".scratch");
// only save editor contents for scratch areas and unsaved editors
if (_is_scratch || _filename.empty() || (!_filename.empty() && is_dirty())) {
// We don't need to lock the editor as we are in the main thread here
// and directly set the file content without detouring to anything that could change the text.
GError *error = 0;
std::pair<const char *, size_t> text = text_data();
if (!g_file_set_contents(fn.c_str(), text.first, text.second, &error)) {
logError("Could not save snapshot of editor contents to %s: %s\n", fn.c_str(), error->message);
std::string msg(strfmt("Could not save snapshot of editor contents to %s: %s", fn.c_str(), error->message));
g_error_free(error);
throw std::runtime_error(msg);
}
} else {
// delete the autosave file if the file was saved
try {
base::remove(fn);
} catch (std::exception &e) {
logWarning("Error deleting autosave file %s: %s\n", fn.c_str(), e.what());
}
}
}
//--------------------------------------------------------------------------------------------------
void SqlEditorPanel::delete_auto_save(const std::string &path) {
// delete the autosave related files
try {
base::remove(base::makePath(path, _autosave_file_suffix + ".autosave"));
} catch (std::exception &exc) {
logWarning("Could not delete auto-save file: %s\n", exc.what());
}
try {
base::remove(base::makePath(path, _autosave_file_suffix + ".info"));
} catch (std::exception &exc) {
logWarning("Could not delete auto-save file: %s\n", exc.what());
}
}
//--------------------------------------------------------------------------------------------------
// Toolbar handling.
void SqlEditorPanel::show_find_panel(mforms::CodeEditor *editor, bool show) {
mforms::FindPanel *panel = editor->get_find_panel();
if (show && !panel->get_parent())
_editor_box.add(panel, false, true);
panel->show(show);
}
//--------------------------------------------------------------------------------------------------
static void toggle_continue_on_error(SqlEditorForm *sql_editor_form) {
sql_editor_form->continue_on_error(!sql_editor_form->continue_on_error());
}
//--------------------------------------------------------------------------------------------------
mforms::ToolBar *SqlEditorPanel::setup_editor_toolbar() {
mforms::ToolBar *tbar(mforms::manage(new mforms::ToolBar(mforms::OptionsToolBar)));
tbar->set_name("Editor Toolbar");
#ifdef _MSC_VER
tbar->set_size(-1, 27);
#endif
mforms::ToolBarItem *item;
item = mforms::manage(new mforms::ToolBarItem(mforms::ActionItem));
item->set_name("Open File");
item->setInternalName("query.openFile");
item->set_icon(IconManager::get_instance()->get_icon_path("qe_sql-editor-tb-icon_open.png"));
item->set_tooltip(_("Open a script file in this editor"));
bec::UIForm::scoped_connect(
item->signal_activated(),
std::bind((void (SqlEditorForm::*)(const std::string &, bool, bool)) & SqlEditorForm::open_file, _form, "", false,
true));
tbar->add_item(item);
item = mforms::manage(new mforms::ToolBarItem(mforms::ActionItem));
item->set_name("Save File");
item->setInternalName("query.saveFile");
item->set_icon(IconManager::get_instance()->get_icon_path("qe_sql-editor-tb-icon_save.png"));
item->set_tooltip(_("Save the script to a file."));
bec::UIForm::scoped_connect(item->signal_activated(), std::bind(&SqlEditorPanel::save, this));
tbar->add_item(item);
tbar->add_item(mforms::manage(new mforms::ToolBarItem(mforms::SeparatorItem)));
item = mforms::manage(new mforms::ToolBarItem(mforms::ActionItem));
item->set_name("Execute");
item->setInternalName("query.execute");
item->set_icon(IconManager::get_instance()->get_icon_path("qe_sql-editor-tb-icon_execute.png"));
item->set_tooltip(_("Execute the selected portion of the script or everything, if there is no selection"));
bec::UIForm::scoped_connect(item->signal_activated(), std::bind(&SqlEditorForm::run_editor_contents, _form, false));
tbar->add_item(item);
item = mforms::manage(new mforms::ToolBarItem(mforms::ActionItem));
item->set_name("Execute Current Statement");
item->setInternalName("query.execute_current_statement");
item->set_icon(IconManager::get_instance()->get_icon_path("qe_sql-editor-tb-icon_execute-current.png"));
item->set_tooltip(_("Execute the statement under the keyboard cursor"));
bec::UIForm::scoped_connect(item->signal_activated(), std::bind(&SqlEditorForm::run_editor_contents, _form, true));
tbar->add_item(item);
item = mforms::manage(new mforms::ToolBarItem(mforms::ActionItem));
item->set_name("Explain Current Statement");
item->setInternalName("query.explain_current_statement");
item->set_icon(IconManager::get_instance()->get_icon_path("qe_sql-editor-tb-icon_explain.png"));
item->set_tooltip(_("Execute the EXPLAIN command on the statement under the cursor"));
bec::UIForm::scoped_connect(item->signal_activated(), std::bind(&SqlEditorForm::explain_current_statement, _form));
tbar->add_item(item);
item = mforms::manage(new mforms::ToolBarItem(mforms::ActionItem));
item->set_name("Cancel");
item->setInternalName("query.cancel");
item->set_icon(IconManager::get_instance()->get_icon_path("qe_sql-editor-tb-icon_stop.png"));
item->set_tooltip(
_("Stop the query being executed (the connection to the DB server will not be restarted and any open transactions "
"will remain open)"));
bec::UIForm::scoped_connect(item->signal_activated(), std::bind(&SqlEditorForm::cancel_query, _form));
tbar->add_item(item);
tbar->add_item(mforms::manage(new mforms::ToolBarItem(mforms::SeparatorItem)));
item = mforms::manage(new mforms::ToolBarItem(mforms::ToggleItem));
item->set_name("Continue On Error");
item->setInternalName("query.continueOnError");
item->set_alt_icon(IconManager::get_instance()->get_icon_path("qe_sql-editor-tb-icon_stop-on-error-on.png"));
item->set_icon(IconManager::get_instance()->get_icon_path("qe_sql-editor-tb-icon_stop-on-error-off.png"));
item->set_tooltip(_("Toggle whether execution of SQL script should continue after failed statements"));
bec::UIForm::scoped_connect(item->signal_activated(), std::bind(toggle_continue_on_error, _form));
tbar->add_item(item);
tbar->add_item(mforms::manage(new mforms::ToolBarItem(mforms::SeparatorItem)));
item = mforms::manage(new mforms::ToolBarItem(mforms::ActionItem));
item->set_name("Commit");
item->setInternalName("query.commit");
item->set_icon(IconManager::get_instance()->get_icon_path("qe_sql-editor-tb-icon_commit.png"));
item->set_tooltip(
_("Commit the current transaction.\nNOTE: all query tabs in the same connection share the same transaction. To "
"have independent transactions, you must open a new connection."));
bec::UIForm::scoped_connect(item->signal_activated(), std::bind(&SqlEditorForm::commit, _form));
tbar->add_item(item);
item = mforms::manage(new mforms::ToolBarItem(mforms::ActionItem));
item->set_name("Rollback");
item->setInternalName("query.rollback");
item->set_icon(IconManager::get_instance()->get_icon_path("qe_sql-editor-tb-icon_rollback.png"));
item->set_tooltip(
_("Rollback the current transaction.\nNOTE: all query tabs in the same connection share the same transaction. To "
"have independent transactions, you must open a new connection."));
bec::UIForm::scoped_connect(item->signal_activated(), std::bind(&SqlEditorForm::rollback, _form));
tbar->add_item(item);
item = mforms::manage(new mforms::ToolBarItem(mforms::ToggleItem));
item->set_name("Auto Commit");
item->setInternalName("query.autocommit");
item->set_alt_icon(IconManager::get_instance()->get_icon_path("qe_sql-editor-tb-icon_autocommit-on.png"));
item->set_icon(IconManager::get_instance()->get_icon_path("qe_sql-editor-tb-icon_autocommit-off.png"));
item->set_tooltip(_(
"Toggle autocommit mode. When enabled, each statement will be committed immediately.\nNOTE: all query tabs in the "
"same connection share the same transaction. To have independent transactions, you must open a new connection."));
bec::UIForm::scoped_connect(item->signal_activated(), std::bind(&SqlEditorForm::toggle_autocommit, _form));
tbar->add_item(item);
tbar->add_separator_item();
item = mforms::manage(new mforms::ToolBarItem(mforms::SelectorItem));
item->set_name("Limit Rows");
item->setInternalName("limit_rows");
item->set_tooltip(
_("Set limit for number of rows returned by queries.\nWorkbench will automatically add the LIMIT clause with the "
"configured number of rows to SELECT queries."));
bec::UIForm::scoped_connect(item->signal_activated(), std::bind(&SqlEditorPanel::limit_rows, this, item));
tbar->add_item(item);
tbar->add_separator_item();
item = mforms::manage(new mforms::ToolBarItem(mforms::ActionItem));
item->set_name("Add Snippet");
item->setInternalName("add_snippet");
item->set_icon(IconManager::get_instance()->get_icon_path("snippet_add.png"));
item->set_tooltip(_("Save current statement or selection to the snippet list."));
bec::UIForm::scoped_connect(item->signal_activated(), std::bind(&SqlEditorForm::save_snippet, _form));
tbar->add_item(item);
tbar->add_separator_item();
// adds generic SQL editor toolbar buttons
_editor->set_base_toolbar(tbar);
update_limit_rows();
return tbar;
}
//--------------------------------------------------------------------------------------------------
void SqlEditorPanel::limit_rows(mforms::ToolBarItem *item) {
_form->limit_rows(item->get_text());
}
//--------------------------------------------------------------------------------------------------
void SqlEditorPanel::update_limit_rows() {
mforms::MenuItem *mitem = _form->get_menubar()->find_item("limit_rows");
std::string selected;
std::vector<std::string> items;
for (int i = 0; i < mitem->item_count(); i++) {
if (!mitem->get_item(i)->get_title().empty()) {
items.push_back(mitem->get_item(i)->get_title());
if (mitem->get_item(i)->get_checked())
selected = items.back();
}
}
mforms::ToolBarItem *item = get_toolbar()->find_item("limit_rows");
item->set_selector_items(items);
item->set_text(selected);
}
//--------------------------------------------------------------------------------------------------
mforms::ToolBar *SqlEditorPanel::get_toolbar() {
return _editor->get_toolbar();
}
//--------------------------------------------------------------------------------------------------
bool SqlEditorPanel::is_dirty() const {
return _editor->get_editor_control()->is_dirty();
}
//--------------------------------------------------------------------------------------------------
void SqlEditorPanel::check_external_file_changes() {
time_t ts;
if (!_filename.empty() && file_mtime(_filename, ts)) {
if (ts > _file_timestamp) {
// File was changed externally. For now we ignore local changes if the user chooses to reload.
std::string connection_description =
_form->connection_descriptor().is_valid()
? strfmt("(from connection to %s) ", _form->connection_descriptor()->name().c_str())
: "";
if (mforms::Utilities::show_warning("File Changed",
strfmt(_("File %s %swas changed from outside MySQL Workbench.\nWould you "
"like to discard your changes and reload it?"),
_filename.c_str(), connection_description.c_str()),
"Reload File", "Ignore", "") == mforms::ResultOk) {
revert_to_saved();
} else {
_file_timestamp = ts;
}
}
}
}
//--------------------------------------------------------------------------------------------------
std::pair<const char *, std::size_t> SqlEditorPanel::text_data() const {
return _editor->text_ptr();
}
//--------------------------------------------------------------------------------------------------
void SqlEditorPanel::set_title(const std::string &title) {
_title = title;
grtobj()->name(_title);
mforms::AppView::set_title(title);
}
//--------------------------------------------------------------------------------------------------
void SqlEditorPanel::set_filename(const std::string &filename) {
_filename = filename;
if (!filename.empty())
set_title(strip_extension(basename(filename)));
}
//--------------------------------------------------------------------------------------------------
void SqlEditorPanel::update_title() {
if (!_is_scratch)
mforms::AppView::set_title(_title + (is_dirty() ? "*" : ""));
}
//--------------------------------------------------------------------------------------------------
/**
* Starts the auto completion list in the currently active editor. The content of this list is
* determined from various sources + the current query context.
*/
void SqlEditorPanel::list_members() {
if (owner()->work_parser_context() != NULL)
editor_be()->show_auto_completion(false);
}
//--------------------------------------------------------------------------------------------------
void SqlEditorPanel::jump_to_placeholder() {
_editor->get_editor_control()->jump_to_next_placeholder();
}
//--------------------------------------------------------------------------------------------------
void SqlEditorPanel::query_started(bool retain_old_recordsets) {
_busy = true;
logDebug("Preparing UI for query run\n");
_form->set_busy_tab(_form->sql_editor_panel_index(this));
// disable tabview reordering because we can get new tabs added at odd times if a query is running
_lower_tabview.set_allows_reordering(false);
// if we're already running the query, it's obvious there's no more need to autocomplete what we typed
_editor->cancel_auto_completion();
if (!retain_old_recordsets) {
logDebug("Releasing old recordset(s) (if possible)\n");
// close recordsets that were opened previously (unless they're pinned or something)
for (int i = _lower_tabview.page_count() - 1; i >= 0; --i) {
SqlEditorResult *result = dynamic_cast<SqlEditorResult *>(_lower_tabview.get_page(i));
if (result) {
if (result->pinned())
continue;
if (result->has_pending_changes())
continue;
// make sure that the result is docked here
int i = _lower_tabview.get_page_index(result);
if (i >= 0) {
result->close();
result_removed();
}
}
}
} else {
logDebug("Retaining old recordset(s)\n");
}
_was_empty = (_lower_tabview.page_count() == 0);
}
//--------------------------------------------------------------------------------------------------
void SqlEditorPanel::query_finished() {
logDebug2("Query successfully finished in editor %s\n", get_title().c_str());
_busy = false;
_form->set_busy_tab(-1);
_lower_tabview.set_allows_reordering(true);
_form->post_query_slot();
}
//--------------------------------------------------------------------------------------------------
void SqlEditorPanel::query_failed(const std::string &message) {
logError("Query execution failed in editor: %s. Error during query: %s\n", get_title().c_str(), message.c_str());
_busy = false;
_form->set_busy_tab(-1);
_lower_tabview.set_allows_reordering(true);
_form->post_query_slot();
}
//--------------------------------------------------------------------------------------------------
// Resultset management.
SqlEditorResult *SqlEditorPanel::active_result_panel() {
return result_panel(_lower_tabview.get_active_tab());
}
//--------------------------------------------------------------------------------------------------
void SqlEditorPanel::lower_tab_switched() {
_lower_dock.view_switched();
db_query_QueryEditorRef qeditor(grtobj());
SqlEditorResult *result = active_result_panel();
Recordset::Ref rset;
if (result && (rset = result->recordset())) {
bool found = false;
for (size_t c = qeditor->resultPanels().count(), i = 0; i < c; i++) {
if (mforms_from_grt(qeditor->resultPanels()[i]->dockingPoint()) == result->dock()) {
found = true;
qeditor->activeResultPanel(qeditor->resultPanels()[i]);
break;
}
}
if (!found)
qeditor->activeResultPanel(db_query_ResultPanelRef());
bool readonly = rset->is_readonly();
_tab_action_apply.show(!readonly);
_tab_action_revert.show(!readonly);
_tab_action_icon.show(readonly);
_tab_action_info.show(readonly);
bool edited = result->has_pending_changes();
_tab_action_apply.set_enabled(edited);
_tab_action_revert.set_enabled(edited);
if (readonly) {
_tab_action_info.set_tooltip(rset->readonly_reason());
_tab_action_icon.set_tooltip(rset->readonly_reason());
}
} else {
qeditor->activeResultPanel(db_query_ResultPanelRef());
_tab_action_apply.show(true);
_tab_action_revert.show(true);
_tab_action_icon.show(false);
_tab_action_info.show(false);
_tab_action_apply.set_enabled(false);
_tab_action_revert.set_enabled(false);
}
#ifdef _MSC_VER
_editor->focus();
#endif
mforms::MenuBar *menu;
if ((menu = _form->get_menubar())) {
Recordset::Ref rset(result ? result->recordset() : Recordset::Ref());
menu->set_item_enabled("query.save_edits", rset && rset->has_pending_changes());
menu->set_item_enabled("query.discard_edits", rset && rset->has_pending_changes());
menu->set_item_enabled("query.export", (bool)rset);
}
// if a lower tab view selection has changed, we make sure it's visible
if (!_busy && _lower_tabview.page_count() > 0) // if we're running a query, then let dock_result handle this
{
int position = (int)bec::GRTManager::get()->get_app_option_int("DbSqlEditor:ResultSplitterPosition", 200);
if (position > _splitter.get_height() - 100)
position = _splitter.get_height() - 100;
_splitter.set_divider_position(position);
}
}
//--------------------------------------------------------------------------------------------------
void SqlEditorPanel::on_recordset_context_menu_show(Recordset::Ptr rs_ptr) {
Recordset::Ref rs(rs_ptr.lock());
if (rs) {
grt::DictRef info(true);
std::vector<int> selection(rs->selected_rows());
grt::IntegerListRef rows(grt::Initialized);
for (std::vector<int>::const_iterator i = selection.begin(); i != selection.end(); ++i)
rows.insert(*i);
info.set("selected-rows", rows);
info.gset("selected-column", rs->selected_column());
info.set("menu", mforms_to_grt(rs->get_context_menu()));
db_query_QueryBufferRef qbuffer(grtobj());
if (qbuffer.is_valid() && db_query_QueryEditorRef::can_wrap(qbuffer)) {
db_query_QueryEditorRef qeditor(db_query_QueryEditorRef::cast_from(qbuffer));
for (size_t c = qeditor->resultPanels().count(), i = 0; i < c; i++) {
db_query_ResultsetRef rset(qeditor->resultPanels()[i]->resultset());
if (rset.is_valid() && dynamic_cast<WBRecordsetResultset *>(rset->get_data())->recordset == rs) {
grt::GRTNotificationCenter::get()->send_grt("GRNSQLResultsetMenuWillShow", rset, info);
break;
}
}
}
}
}
//--------------------------------------------------------------------------------------------------
/**
* Returns the number of all docked result panels in the editor panel.
*/
size_t SqlEditorPanel::result_panel_count() {
return _lower_tabview.page_count();
}
//--------------------------------------------------------------------------------------------------
/**
* Returns the number of all docked resultset panels in the editor panel.
* That excludes all the explain, spatial etc. panels.
*/
size_t SqlEditorPanel::resultset_count() {
return grtobj()->resultPanels().count();
}
//--------------------------------------------------------------------------------------------------
SqlEditorResult *SqlEditorPanel::result_panel(int i) {
if (i >= 0 && i < _lower_tabview.page_count())
return dynamic_cast<SqlEditorResult *>(_lower_tabview.get_page(i));
return NULL;
}
//--------------------------------------------------------------------------------------------------
void SqlEditorPanel::add_panel_for_recordset_from_main(Recordset::Ref rset) {
if (bec::GRTManager::get()->in_main_thread()) {
SqlEditorForm::RecordsetData *rdata = dynamic_cast<SqlEditorForm::RecordsetData *>(rset->client_data());
rdata->result_panel = add_panel_for_recordset(rset);
} else
bec::GRTManager::get()->run_once_when_idle(
dynamic_cast<bec::UIForm *>(this), std::bind(&SqlEditorPanel::add_panel_for_recordset_from_main, this, rset));
}
//--------------------------------------------------------------------------------------------------
SqlEditorResult *SqlEditorPanel::add_panel_for_recordset(Recordset::Ref rset) {
SqlEditorResult *result = mforms::manage(new SqlEditorResult(this));
if (rset)
result->set_recordset(rset);
dock_result_panel(result);
return result;
}
//--------------------------------------------------------------------------------------------------
void SqlEditorPanel::dock_result_panel(SqlEditorResult *result) {
result->grtobj()->owner(grtobj());
grtobj()->resultPanels().insert(result->grtobj());
if (Recordset::Ref rset = result->recordset())
result->set_title(rset->caption());
_lower_dock.dock_view(result);
_lower_dock.select_view(result);
_splitter.set_expanded(false, true);
if (_was_empty) {
int position = (int)bec::GRTManager::get()->get_app_option_int("DbSqlEditor:ResultSplitterPosition", 200);
if (position > _splitter.get_height() - 100)
position = _splitter.get_height() - 100;
_splitter.set_divider_position(position);
// scroll the editor to make the cursor visible and keep the selection, if available
std::size_t selection_start = 0;
std::size_t selection_length = 0;
_editor->get_editor_control()->get_selection(selection_start, selection_length);
_editor->get_editor_control()->set_caret_pos(_editor->get_editor_control()->get_caret_pos());
_editor->get_editor_control()->set_selection(selection_start, selection_length);
}
}
//--------------------------------------------------------------------------------------------------
void SqlEditorPanel::lower_tab_reordered(mforms::View *view, int from, int to) {
if (from == to || dynamic_cast<SqlEditorResult *>(view) == NULL)
return;
// not all tabs will have a SqlEditorResult
// so the reordering gets more complicated, because actual reordering only happens if the relative
// position between the result objects changes...
// relative result object order changes always mean that a tab was reordered, but the other way around is
// not always true
size_t from_index = grtobj()->resultPanels().get_index(dynamic_cast<SqlEditorResult *>(view)->grtobj());
if (from_index == grt::BaseListRef::npos) {
logFatal("Result panel is not in resultPanels() list\n");
return;
}
// first build an array of result panel objects, in the same order as the tabview
std::vector<std::pair<db_query_ResultPanelRef, int> > panels;
for (int result_order = 0, i = 0; i < _lower_tabview.page_count(); i++) {
SqlEditorResult *p = result_panel(i);
if (p)
panels.push_back(std::make_pair(p->grtobj(), result_order++));
else
panels.push_back(std::make_pair(db_query_ResultPanelRef(), 0));
}
int to_index = -1;
// now find out where we have to move to
if (from < to) {
for (int i = to; i > from; i--) {
if (panels[i].first.is_valid()) {
to_index = panels[i].second;
break;
}
}
} else {
for (int i = to; i < from; i++) {
if (panels[i].first.is_valid()) {
to_index = panels[i].second;
break;
}
}
}
if (to_index < 0) {
to_index = panels.back().second;
}
grtobj()->resultPanels()->reorder(from_index, to_index);
}
//--------------------------------------------------------------------------------------------------
bool SqlEditorPanel::lower_tab_closing(int tab) {
mforms::AppView *view = _lower_dock.view_at_index(tab);
if (view) {
if (view->on_close()) {
view->close();
result_removed();
return true;
}
return false;
}
return true;
}
//--------------------------------------------------------------------------------------------------
void SqlEditorPanel::lower_tab_closed(mforms::View *page, int tab) {
SqlEditorResult *rpage = dynamic_cast<SqlEditorResult *>(page);
if (rpage) {
db_query_ResultPanelRef closed_panel(rpage->grtobj());
grtobj()->resultPanels().remove_value(closed_panel);
if (closed_panel->resultset().is_valid())
closed_panel->resultset()->reset_references();
closed_panel->reset_references();
}
}
//--------------------------------------------------------------------------------------------------
void SqlEditorPanel::result_removed() {
if (_lower_tabview.page_count() == 0)
_splitter.set_expanded(false, false);
lower_tab_switched();
}
//--------------------------------------------------------------------------------------------------
std::list<SqlEditorResult *> SqlEditorPanel::dirty_result_panels() {
std::list<SqlEditorResult *> results;
for (int c = _lower_tabview.page_count(), i = 0; i < c; i++) {
SqlEditorResult *result = result_panel(i);
if (result && result->has_pending_changes())
results.push_back(result);
}
return results;
}
//--------------------------------------------------------------------------------------------------
void SqlEditorPanel::tab_menu_will_show() {
SqlEditorResult *result(result_panel(_lower_tabview.get_menu_tab()));
_lower_tab_menu.set_item_enabled("rename", result != NULL);
_lower_tab_menu.set_item_enabled("pin", result != NULL);
_lower_tab_menu.set_item_checked("pin", result && result->pinned());
if (_lower_tabview.page_count() > 1)
_lower_tab_menu.set_item_enabled("close_others", true); // close others
else
_lower_tab_menu.set_item_enabled("close_others", false); // close others
}
//--------------------------------------------------------------------------------------------------
void SqlEditorPanel::rename_tab_clicked() {
int tab = _lower_tabview.get_menu_tab();
SqlEditorResult *result = result_panel(tab);
if (result) {
std::string title;
if (mforms::Utilities::request_input(_("Rename Result Tab"), "Enter a new name for the result tab:",
result->caption().c_str(), title))
_lower_tabview.set_tab_title(tab, title);
}
}
//--------------------------------------------------------------------------------------------------
void SqlEditorPanel::pin_tab_clicked() {
int tab = _lower_tabview.get_menu_tab();
SqlEditorResult *result = result_panel(tab);
if (result)
result->set_pinned(!result->pinned());
}
//--------------------------------------------------------------------------------------------------
bool SqlEditorPanel::is_pinned(int tab) {
SqlEditorResult *result = result_panel(tab);
if (result)
return result->pinned();
return false;
}
//--------------------------------------------------------------------------------------------------
void SqlEditorPanel::tab_pinned(int tab, bool flag) {
SqlEditorResult *result = result_panel(tab);
if (result)
result->set_pinned(flag);
}
//--------------------------------------------------------------------------------------------------
void SqlEditorPanel::close_tab_clicked() {
lower_tab_closing(_lower_tabview.get_menu_tab());
}
//--------------------------------------------------------------------------------------------------
void SqlEditorPanel::close_other_tabs_clicked() {
int tab = _lower_tabview.get_menu_tab();
for (int i = _lower_tabview.page_count() - 1; i >= 0; --i) {
if (i != tab)
lower_tab_closing(i);
}
}
//--------------------------------------------------------------------------------------------------