|
|
#include "tooltips.hpp" |
|
|
#include "config.hpp" |
|
|
#include "filesystem.hpp" |
|
|
#include "info.hpp" |
|
|
#include "notebook.hpp" |
|
|
#include "selection_dialog.hpp" |
|
|
#include "utility.hpp" |
|
|
#include <algorithm> |
|
|
#include <limits> |
|
|
#include <regex> |
|
|
|
|
|
std::set<Tooltip *> Tooltips::shown_tooltips; |
|
|
Gdk::Rectangle Tooltips::drawn_tooltips_rectangle = Gdk::Rectangle(); |
|
|
|
|
|
Tooltip::Tooltip(Gsv::View *view, const Gtk::TextIter &start_iter, const Gtk::TextIter &end_iter, std::function<void(Tooltip &toolip)> set_buffer_) |
|
|
: start_mark(start_iter), end_mark(end_iter), view(view), set_buffer(std::move(set_buffer_)) {} |
|
|
|
|
|
Tooltip::Tooltip(std::function<void(Tooltip &)> set_buffer_) |
|
|
: view(nullptr), set_buffer(std::move(set_buffer_)) {} |
|
|
|
|
|
Tooltip::~Tooltip() { |
|
|
Tooltips::shown_tooltips.erase(this); |
|
|
} |
|
|
|
|
|
void Tooltip::update() { |
|
|
if(view) { |
|
|
auto iter = start_mark->get_iter(); |
|
|
auto end_iter = end_mark->get_iter(); |
|
|
view->get_iter_location(iter, activation_rectangle); |
|
|
if(iter.get_offset() < end_iter.get_offset()) { |
|
|
while(iter.forward_char() && iter != end_iter) { |
|
|
Gdk::Rectangle rectangle; |
|
|
view->get_iter_location(iter, rectangle); |
|
|
activation_rectangle.join(rectangle); |
|
|
} |
|
|
} |
|
|
int location_window_x, location_window_y; |
|
|
view->buffer_to_window_coords(Gtk::TextWindowType::TEXT_WINDOW_TEXT, activation_rectangle.get_x(), activation_rectangle.get_y(), location_window_x, location_window_y); |
|
|
activation_rectangle.set_x(location_window_x); |
|
|
activation_rectangle.set_y(location_window_y); |
|
|
} |
|
|
} |
|
|
|
|
|
void Tooltip::show(bool disregard_drawn, const std::function<void()> &on_motion) { |
|
|
Tooltips::shown_tooltips.emplace(this); |
|
|
|
|
|
if(!window) { |
|
|
//init window |
|
|
window = std::make_unique<Gtk::Window>(Gtk::WindowType::WINDOW_POPUP); |
|
|
|
|
|
auto g_application = g_application_get_default(); |
|
|
auto gio_application = Glib::wrap(g_application, true); |
|
|
auto application = Glib::RefPtr<Gtk::Application>::cast_static(gio_application); |
|
|
if(auto active_window = application->get_active_window()) |
|
|
window->set_transient_for(*active_window); |
|
|
|
|
|
window->set_type_hint(Gdk::WindowTypeHint::WINDOW_TYPE_HINT_TOOLTIP); |
|
|
|
|
|
window->set_events(Gdk::POINTER_MOTION_MASK); |
|
|
window->property_decorated() = false; |
|
|
window->set_accept_focus(false); |
|
|
window->set_skip_taskbar_hint(true); |
|
|
window->set_default_size(0, 0); |
|
|
|
|
|
window->signal_motion_notify_event().connect([on_motion](GdkEventMotion *event) { |
|
|
if(on_motion) |
|
|
on_motion(); |
|
|
return false; |
|
|
}); |
|
|
|
|
|
window->get_style_context()->add_class("juci_tooltip_window"); |
|
|
auto visual = window->get_screen()->get_rgba_visual(); |
|
|
if(visual) |
|
|
gtk_widget_set_visual(reinterpret_cast<GtkWidget *>(window->gobj()), visual->gobj()); |
|
|
|
|
|
auto box = Gtk::manage(new Gtk::Box(Gtk::Orientation::ORIENTATION_VERTICAL)); |
|
|
box->get_style_context()->add_class("juci_tooltip_box"); |
|
|
window->add(*box); |
|
|
|
|
|
buffer = Gtk::TextBuffer::create(); |
|
|
|
|
|
if(view) { |
|
|
auto create_tag_from_style = [this](const std::string &style_name) { |
|
|
if(auto style = view->get_source_buffer()->get_style_scheme()->get_style(style_name)) { |
|
|
auto tag = buffer->create_tag(style_name); |
|
|
if(style->property_foreground_set()) |
|
|
tag->property_foreground() = style->property_foreground(); |
|
|
if(style->property_background_set()) |
|
|
tag->property_background() = style->property_background(); |
|
|
} |
|
|
}; |
|
|
create_tag_from_style("def:warning"); |
|
|
create_tag_from_style("def:error"); |
|
|
} |
|
|
link_tag = buffer->create_tag("link"); |
|
|
link_tag->property_underline() = Pango::Underline::UNDERLINE_SINGLE; |
|
|
link_tag->property_foreground_rgba() = window->get_style_context()->get_color(Gtk::StateFlags::STATE_FLAG_LINK); |
|
|
|
|
|
set_buffer(*this); |
|
|
|
|
|
remove_trailing_newlines(); |
|
|
|
|
|
wrap_lines(); |
|
|
|
|
|
auto tooltip_text_view = Gtk::manage(new Gtk::TextView(buffer)); |
|
|
tooltip_text_view->get_style_context()->add_class("juci_tooltip_text_view"); |
|
|
tooltip_text_view->set_editable(false); |
|
|
|
|
|
static auto link_mouse_cursor = Gdk::Cursor::create(Gdk::CursorType::HAND1); |
|
|
static auto default_mouse_cursor = Gdk::Cursor::create(Gdk::CursorType::XTERM); |
|
|
tooltip_text_view->signal_motion_notify_event().connect([this, tooltip_text_view](GdkEventMotion *event) { |
|
|
Gtk::TextIter iter; |
|
|
int location_x, location_y; |
|
|
tooltip_text_view->window_to_buffer_coords(Gtk::TextWindowType::TEXT_WINDOW_TEXT, event->x, event->y, location_x, location_y); |
|
|
tooltip_text_view->get_iter_at_location(iter, location_x, location_y); |
|
|
if(iter.has_tag(link_tag)) |
|
|
tooltip_text_view->get_window(Gtk::TextWindowType::TEXT_WINDOW_TEXT)->set_cursor(link_mouse_cursor); |
|
|
else |
|
|
tooltip_text_view->get_window(Gtk::TextWindowType::TEXT_WINDOW_TEXT)->set_cursor(default_mouse_cursor); |
|
|
return false; |
|
|
}); |
|
|
tooltip_text_view->signal_button_press_event().connect([this, tooltip_text_view](GdkEventButton *event) { |
|
|
if(event->type == GDK_BUTTON_PRESS && event->button == GDK_BUTTON_PRIMARY) { |
|
|
Gtk::TextIter iter; |
|
|
int location_x, location_y; |
|
|
tooltip_text_view->window_to_buffer_coords(Gtk::TextWindowType::TEXT_WINDOW_TEXT, event->x, event->y, location_x, location_y); |
|
|
tooltip_text_view->get_iter_at_location(iter, location_x, location_y); |
|
|
if(iter.has_tag(link_tag)) { |
|
|
auto start = iter; |
|
|
if(!start.begins_tag(link_tag)) |
|
|
start.backward_to_tag_toggle(link_tag); |
|
|
auto end = iter; |
|
|
end.forward_to_tag_toggle(link_tag); |
|
|
std::string text = tooltip_text_view->get_buffer()->get_text(start, end); |
|
|
std::string link; |
|
|
for(auto &tag : start.get_tags()) { |
|
|
auto it = links.find(tag); |
|
|
if(it != links.end()) { |
|
|
link = it->second; |
|
|
break; |
|
|
} |
|
|
it = reference_links.find(tag); |
|
|
if(it != reference_links.end()) { |
|
|
auto reference_it = references.find(it->second); |
|
|
if(reference_it != references.end()) |
|
|
link = reference_it->second; |
|
|
break; |
|
|
} |
|
|
} |
|
|
if(link.empty()) |
|
|
link = text; |
|
|
|
|
|
if(starts_with(link, "http://") || starts_with(link, "https://")) { |
|
|
Notebook::get().open_uri(link); |
|
|
return true; |
|
|
} |
|
|
|
|
|
const static std::regex regex("^([^:]+):([^:]+)(:[^:]+)?$", std::regex::optimize); |
|
|
std::smatch sm; |
|
|
if(std::regex_match(link, sm, regex)) { |
|
|
auto path = boost::filesystem::path(sm[1].str()); |
|
|
if(path.is_relative()) { |
|
|
if(auto source_view = dynamic_cast<Source::View *>(view)) |
|
|
path = filesystem::get_normal_path(source_view->file_path.parent_path() / path); |
|
|
} |
|
|
|
|
|
boost::system::error_code ec; |
|
|
if(boost::filesystem::is_regular_file(path, ec)) { |
|
|
if(Notebook::get().open(path)) { |
|
|
try { |
|
|
auto line = std::stoi(sm[2].str()) - 1; |
|
|
auto offset = sm.length(3) ? std::stoi(sm[3].str().substr(1)) - 1 : 0; |
|
|
auto view = Notebook::get().get_current_view(); |
|
|
view->place_cursor_at_line_offset(line, offset); |
|
|
view->scroll_to_cursor_delayed(true, false); |
|
|
} |
|
|
catch(...) { |
|
|
} |
|
|
} |
|
|
return true; |
|
|
} |
|
|
} |
|
|
|
|
|
auto path = boost::filesystem::path(link); |
|
|
if(auto source_view = dynamic_cast<Source::View *>(view)) |
|
|
path = filesystem::get_normal_path(source_view->file_path.parent_path() / path); |
|
|
boost::system::error_code ec; |
|
|
if(boost::filesystem::is_regular_file(path, ec) && Notebook::get().open(path)) |
|
|
return true; |
|
|
Info::get().print("Could not open: " + link); |
|
|
} |
|
|
} |
|
|
return false; |
|
|
}); |
|
|
|
|
|
Gdk::Rectangle rect; |
|
|
auto iter = buffer->begin(); |
|
|
tooltip_text_view->get_iter_location(iter, rect); |
|
|
int min_x = rect.get_x(); |
|
|
int max_x = rect.get_x() + rect.get_width(); |
|
|
int height = 0; |
|
|
while(true) { |
|
|
if(iter.ends_line()) { |
|
|
tooltip_text_view->get_iter_location(iter, rect); |
|
|
max_x = std::max(max_x, rect.get_x() + rect.get_width()); |
|
|
height += rect.get_height(); |
|
|
} |
|
|
if(iter.is_end()) |
|
|
break; |
|
|
iter.forward_char(); |
|
|
} |
|
|
size = {max_x - min_x, height}; |
|
|
size.first += 6; // 2xpadding |
|
|
size.second += 8; // 2xpadding + 2 |
|
|
|
|
|
// Add ScrolledWindow if needed |
|
|
Gtk::Widget *widget = tooltip_text_view; |
|
|
auto screen_width = Gdk::Screen::get_default()->get_width(); |
|
|
auto screen_height = Gdk::Screen::get_default()->get_height(); |
|
|
if(size.first > screen_width - 6 /* 2xpadding */ || size.second > screen_height - 6 /* 2xpadding */) { |
|
|
auto scrolled_window = Gtk::manage(new Gtk::ScrolledWindow()); |
|
|
scrolled_window->property_hscrollbar_policy() = size.first > screen_width - 6 ? Gtk::PolicyType::POLICY_AUTOMATIC : Gtk::PolicyType::POLICY_NEVER; |
|
|
scrolled_window->property_vscrollbar_policy() = size.second > screen_height - 6 ? Gtk::PolicyType::POLICY_AUTOMATIC : Gtk::PolicyType::POLICY_NEVER; |
|
|
scrolled_window->add(*tooltip_text_view); |
|
|
scrolled_window->set_size_request(size.first > screen_width - 6 ? screen_width - 6 : -1, size.second > screen_height - 6 ? screen_height - 6 : -1); |
|
|
widget = scrolled_window; |
|
|
} |
|
|
|
|
|
#if GTK_VERSION_GE(3, 20) |
|
|
box->add(*widget); |
|
|
#else |
|
|
auto box2 = Gtk::manage(new Gtk::Box()); |
|
|
box2->pack_start(*widget, true, true, 3); |
|
|
box->pack_start(*box2, true, true, 3); |
|
|
#endif |
|
|
|
|
|
window->signal_realize().connect([this] { |
|
|
if(!view) { |
|
|
auto &dialog = SelectionDialog::get(); |
|
|
if(dialog && dialog->is_visible()) { |
|
|
int root_x, root_y; |
|
|
dialog->get_position(root_x, root_y); |
|
|
root_x -= 3; // -1xpadding |
|
|
rectangle.set_x(root_x); |
|
|
rectangle.set_y(root_y - size.second); |
|
|
if(rectangle.get_y() < 0) |
|
|
rectangle.set_y(0); |
|
|
} |
|
|
} |
|
|
window->move(rectangle.get_x(), rectangle.get_y()); |
|
|
}); |
|
|
} |
|
|
|
|
|
if(buffer->size() == 0) |
|
|
return; // Do not show empty tooltips |
|
|
|
|
|
int root_x = 0, root_y = 0; |
|
|
if(view) { |
|
|
Gdk::Rectangle visible_rect; |
|
|
view->get_visible_rect(visible_rect); |
|
|
int visible_window_x, visible_window_y; |
|
|
view->buffer_to_window_coords(Gtk::TextWindowType::TEXT_WINDOW_TEXT, visible_rect.get_x(), visible_rect.get_y(), visible_window_x, visible_window_y); |
|
|
|
|
|
auto window_x = std::max(activation_rectangle.get_x(), visible_window_x); // Adjust tooltip right if it is left of view |
|
|
auto window_y = activation_rectangle.get_y(); |
|
|
view->get_window(Gtk::TextWindowType::TEXT_WINDOW_TEXT)->get_root_coords(window_x, window_y, root_x, root_y); |
|
|
root_x -= 3; // -1xpadding |
|
|
|
|
|
if(root_y < size.second) |
|
|
root_x += visible_rect.get_width() * 0.1; // Adjust tooltip right if it might be above cursor |
|
|
rectangle.set_y(std::max(0, root_y - size.second)); // Move tooptip down if it is above screen |
|
|
|
|
|
// Move tooltip left if it is right of screen |
|
|
auto screen_width = Gdk::Screen::get_default()->get_width(); |
|
|
rectangle.set_x(root_x + size.first > screen_width ? std::max(0, screen_width - size.first) /* Move tooptip right if it is left of screen */ : root_x); |
|
|
} |
|
|
|
|
|
rectangle.set_width(size.first); |
|
|
rectangle.set_height(size.second); |
|
|
|
|
|
if(!disregard_drawn) { |
|
|
if(Tooltips::drawn_tooltips_rectangle.get_width() != 0) { |
|
|
if(rectangle.intersects(Tooltips::drawn_tooltips_rectangle)) { // Put tooltip above other tooltips |
|
|
int new_y = Tooltips::drawn_tooltips_rectangle.get_y() - size.second; |
|
|
if(new_y >= 0) |
|
|
rectangle.set_y(new_y); |
|
|
else { // Put tooltip to the right if the tooltip would be placed above the screen |
|
|
int new_x = Tooltips::drawn_tooltips_rectangle.get_x() + Tooltips::drawn_tooltips_rectangle.get_width() + 2; |
|
|
if(new_x + size.first < Gdk::Screen::get_default()->get_width()) |
|
|
rectangle.set_x(new_x); |
|
|
else { // Put tooltip below if the tooltip would be placed outside of the screen on the right side |
|
|
new_y = Tooltips::drawn_tooltips_rectangle.get_y() + Tooltips::drawn_tooltips_rectangle.get_height(); |
|
|
int root_x, root_y; |
|
|
if(view) { |
|
|
view->get_window(Gtk::TextWindowType::TEXT_WINDOW_TEXT)->get_root_coords(activation_rectangle.get_x(), activation_rectangle.get_y(), root_x, root_y); |
|
|
if(new_y < root_y + activation_rectangle.get_height()) |
|
|
new_y = root_y + activation_rectangle.get_height() + 2; |
|
|
} |
|
|
rectangle.set_y(new_y); |
|
|
} |
|
|
} |
|
|
} |
|
|
Tooltips::drawn_tooltips_rectangle.join(rectangle); |
|
|
} |
|
|
else |
|
|
Tooltips::drawn_tooltips_rectangle = rectangle; |
|
|
} |
|
|
|
|
|
if(window->get_realized()) |
|
|
window->move(rectangle.get_x(), rectangle.get_y()); |
|
|
window->show_all(); |
|
|
shown = true; |
|
|
} |
|
|
|
|
|
void Tooltip::hide(const boost::optional<std::pair<int, int>> &last_mouse_pos, const boost::optional<std::pair<int, int>> &mouse_pos) { |
|
|
// Keep tooltip if mouse is moving towards it |
|
|
// Calculated using dot product between the mouse_pos vector and the corners of the tooltip window |
|
|
if(view && window && shown && last_mouse_pos && mouse_pos) { |
|
|
static int root_x, root_y; |
|
|
view->get_window(Gtk::TextWindowType::TEXT_WINDOW_TEXT)->get_root_coords(last_mouse_pos->first, last_mouse_pos->second, root_x, root_y); |
|
|
int diff_x = mouse_pos->first - last_mouse_pos->first; |
|
|
int diff_y = mouse_pos->second - last_mouse_pos->second; |
|
|
class Corner { |
|
|
public: |
|
|
Corner(int x, int y) : x(x - root_x), y(y - root_y) {} |
|
|
int x, y; |
|
|
}; |
|
|
std::vector<Corner> corners; |
|
|
corners.emplace_back(rectangle.get_x(), rectangle.get_y()); |
|
|
corners.emplace_back(rectangle.get_x() + rectangle.get_width(), rectangle.get_y()); |
|
|
corners.emplace_back(rectangle.get_x(), rectangle.get_y() + rectangle.get_height()); |
|
|
corners.emplace_back(rectangle.get_x() + rectangle.get_width(), rectangle.get_y() + rectangle.get_height()); |
|
|
for(auto &corner : corners) { |
|
|
if(diff_x * corner.x + diff_y * corner.y >= 0) |
|
|
return; |
|
|
} |
|
|
} |
|
|
Tooltips::shown_tooltips.erase(this); |
|
|
if(window) |
|
|
window->hide(); |
|
|
shown = false; |
|
|
} |
|
|
|
|
|
void Tooltip::wrap_lines() { |
|
|
auto iter = buffer->begin(); |
|
|
if(!iter) |
|
|
return; |
|
|
|
|
|
while(true) { |
|
|
auto last_space = buffer->end(); |
|
|
bool long_line = true; |
|
|
for(int c = 0; c <= max_columns; c++) { |
|
|
if(*iter == ' ') |
|
|
last_space = iter; |
|
|
if(iter.ends_line()) { |
|
|
long_line = false; |
|
|
break; |
|
|
} |
|
|
if(!iter.forward_char()) |
|
|
return; |
|
|
} |
|
|
if(long_line) { |
|
|
if(!last_space) { // If word is longer than max_columns |
|
|
while(true) { |
|
|
if(iter.ends_line()) |
|
|
break; |
|
|
if(*iter == ' ') { |
|
|
last_space = iter; |
|
|
break; |
|
|
} |
|
|
if(!iter.forward_char()) |
|
|
return; |
|
|
} |
|
|
} |
|
|
if(last_space) { |
|
|
auto mark = buffer->create_mark(last_space); |
|
|
|
|
|
auto next = last_space; |
|
|
next.forward_char(); |
|
|
buffer->erase(last_space, next); |
|
|
buffer->insert(mark->get_iter(), "\n"); |
|
|
|
|
|
iter = mark->get_iter(); |
|
|
buffer->delete_mark(mark); |
|
|
} |
|
|
} |
|
|
if(!iter.forward_char()) |
|
|
return; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
void Tooltip::create_tags() { |
|
|
if(!h1_tag) { |
|
|
h1_tag = buffer->create_tag(); |
|
|
h1_tag->property_weight() = Pango::WEIGHT_ULTRAHEAVY; |
|
|
h1_tag->property_underline() = Pango::UNDERLINE_SINGLE; |
|
|
|
|
|
h2_tag = buffer->create_tag(); |
|
|
h2_tag->property_weight() = Pango::WEIGHT_ULTRAHEAVY; |
|
|
|
|
|
h3_tag = buffer->create_tag(); |
|
|
h3_tag->property_style() = Pango::Style::STYLE_ITALIC; |
|
|
|
|
|
auto source_font_family = Pango::FontDescription(Config::get().source.font).get_family(); |
|
|
|
|
|
code_tag = buffer->create_tag(); |
|
|
code_tag->property_family() = source_font_family; |
|
|
auto background_rgba = Gdk::RGBA(); |
|
|
auto normal_color = window->get_style_context()->get_color(Gtk::StateFlags::STATE_FLAG_NORMAL); |
|
|
auto light_theme = (normal_color.get_red() + normal_color.get_green() + normal_color.get_blue()) / 3 < 0.5; |
|
|
if(light_theme) |
|
|
background_rgba.set_rgba(1.0, 1.0, 1.0, 0.4); |
|
|
else |
|
|
background_rgba.set_rgba(0.0, 0.0, 0.0, 0.2); |
|
|
code_tag->property_background_rgba() = background_rgba; |
|
|
|
|
|
code_block_tag = buffer->create_tag(); |
|
|
code_block_tag->property_family() = source_font_family; |
|
|
code_block_tag->property_paragraph_background_rgba() = background_rgba; |
|
|
|
|
|
bold_tag = buffer->create_tag(); |
|
|
bold_tag->property_weight() = Pango::WEIGHT_ULTRAHEAVY; |
|
|
|
|
|
italic_tag = buffer->create_tag(); |
|
|
italic_tag->property_style() = Pango::Style::STYLE_ITALIC; |
|
|
|
|
|
strikethrough_tag = buffer->create_tag(); |
|
|
strikethrough_tag->property_strikethrough() = true; |
|
|
} |
|
|
} |
|
|
|
|
|
void Tooltip::insert_with_links_tagged(const std::string &text) { |
|
|
if(text.empty()) |
|
|
return; |
|
|
const static std::regex http_regex("https?://[\\w\\-.~:/?#%\\[\\]@!$&'()*+,;=]+[\\w\\-~/#@$*+;=]", std::regex::optimize); |
|
|
std::smatch sm; |
|
|
std::sregex_iterator it(text.begin(), text.end(), http_regex); |
|
|
std::sregex_iterator end; |
|
|
size_t start_pos = 0; |
|
|
for(; it != end; ++it) { |
|
|
buffer->insert(buffer->get_insert()->get_iter(), &text[start_pos], &text[it->position()]); |
|
|
buffer->insert_with_tag(buffer->get_insert()->get_iter(), &text[it->position()], text.c_str() + it->position() + it->length(), link_tag); |
|
|
start_pos = it->position() + it->length(); |
|
|
} |
|
|
if(start_pos < text.size()) |
|
|
buffer->insert(buffer->get_insert()->get_iter(), &text[start_pos], text.c_str() + text.size()); |
|
|
} |
|
|
|
|
|
void Tooltip::insert_markdown(const std::string &input) { |
|
|
create_tags(); |
|
|
|
|
|
size_t i = 0; |
|
|
|
|
|
auto forward_to = [&](const std::vector<char> chars) { |
|
|
for(; i < input.size(); i++) { |
|
|
if(std::any_of(chars.begin(), chars.end(), [&input, &i](char chr) { return input[i] == chr; })) |
|
|
return true; |
|
|
} |
|
|
return false; |
|
|
}; |
|
|
|
|
|
auto forward_passed = [&](const std::vector<char> chars) { |
|
|
for(; i < input.size(); i++) { |
|
|
if(std::none_of(chars.begin(), chars.end(), [&input, &i](char chr) { return input[i] == chr; })) |
|
|
return true; |
|
|
} |
|
|
return false; |
|
|
}; |
|
|
|
|
|
auto unescape = [&](size_t &i) { |
|
|
if(input[i] == '\\') { |
|
|
if(i + 1 < input.size()) |
|
|
i++; |
|
|
return true; |
|
|
} |
|
|
return false; |
|
|
}; |
|
|
|
|
|
std::function<void(size_t, size_t)> insert_text = [&](size_t from, size_t to) { |
|
|
if(from == to) |
|
|
return; |
|
|
|
|
|
auto i = from; |
|
|
|
|
|
std::string partial; |
|
|
|
|
|
auto is_whitespace_character = [&](size_t i) { |
|
|
return input[i] == ' ' || input[i] == '\t' || input[i] == '\n' || input[i] == '\r' || input[i] == '\f'; |
|
|
}; |
|
|
|
|
|
auto is_punctuation_character = [&](size_t i) { |
|
|
static std::set<char> punctuation_characters = {'!', '"', '#', '$', '%', '&', '\'', '(', ')', '*', '+', ',', '-', '.', '/', ':', ';', '<', '=', '>', '?', '@', '[', '\\', ']', '^', '_', '`', '{', '|', '}', '~'}; |
|
|
return punctuation_characters.find(input[i]) != punctuation_characters.end(); |
|
|
}; |
|
|
|
|
|
auto left_flanking_delimiter_run = [&](size_t i, size_t n) { |
|
|
return !(i + n >= to || is_whitespace_character(i + n)) && |
|
|
(!is_punctuation_character(i + n) || |
|
|
(i == from || is_whitespace_character(i - 1) || is_punctuation_character(i - 1))); |
|
|
}; |
|
|
auto right_flanking_delimiter_run = [&](size_t i, size_t n) { |
|
|
return !(i == from || is_whitespace_character(i - 1)) && |
|
|
(!is_punctuation_character(i - 1) || |
|
|
(i + n >= to || is_whitespace_character(i + n) || is_punctuation_character(i + n))); |
|
|
}; |
|
|
|
|
|
/// Using rules in https://spec.commonmark.org/0.29/#emphasis-and-strong-emphasis |
|
|
auto insert_emphasis = [&] { |
|
|
auto i_saved = i; |
|
|
std::string prefix; |
|
|
if(input[i] == '*' || input[i] == '_') { |
|
|
prefix += input[i]; |
|
|
++i; |
|
|
for(; prefix.size() < 3 && input[i] == prefix.back(); ++i) |
|
|
prefix += input[i]; |
|
|
} |
|
|
if(prefix.empty()) |
|
|
return false; |
|
|
if(prefix.back() == '*') { // Rule 1 and 5 |
|
|
if(!left_flanking_delimiter_run(i - prefix.size(), prefix.size())) { |
|
|
i = i_saved; |
|
|
return false; |
|
|
} |
|
|
} |
|
|
else { // Rule 2 and 6 |
|
|
if(!(left_flanking_delimiter_run(i - prefix.size(), prefix.size()) && |
|
|
(!right_flanking_delimiter_run(i - prefix.size(), prefix.size()) || i - prefix.size() == from || is_punctuation_character(i - prefix.size() - 1)))) { |
|
|
i = i_saved; |
|
|
return false; |
|
|
} |
|
|
} |
|
|
auto start = i; |
|
|
for(; i < to - (prefix.size() - 1); i++) { |
|
|
if(!unescape(i)) { |
|
|
if(starts_with(input, i, prefix)) { |
|
|
if(prefix.back() == '*') { // Rule 3 and 7 |
|
|
if(right_flanking_delimiter_run(i, prefix.size())) |
|
|
break; |
|
|
else if(left_flanking_delimiter_run(i, prefix.size())) { |
|
|
i = i_saved; |
|
|
return false; |
|
|
} |
|
|
} |
|
|
else { // Rule 4 and 8 |
|
|
if(right_flanking_delimiter_run(i, prefix.size()) && |
|
|
(!left_flanking_delimiter_run(i, prefix.size()) || i + prefix.size() == to || is_punctuation_character(i + prefix.size()))) |
|
|
break; |
|
|
else if(left_flanking_delimiter_run(i, prefix.size()) && |
|
|
(!right_flanking_delimiter_run(i, prefix.size()) || i == from || is_punctuation_character(i - 1))) { |
|
|
i = i_saved; |
|
|
return false; |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
if(i >= to - (prefix.size() - 1)) { |
|
|
i = i_saved; |
|
|
return false; |
|
|
} |
|
|
insert_with_links_tagged(partial); |
|
|
partial.clear(); |
|
|
auto start_offset = buffer->get_insert()->get_iter().get_offset(); |
|
|
insert_text(start, i); |
|
|
if(prefix.size() == 1) |
|
|
buffer->apply_tag(italic_tag, buffer->get_iter_at_offset(start_offset), buffer->get_insert()->get_iter()); |
|
|
else if(prefix.size() == 2) |
|
|
buffer->apply_tag(bold_tag, buffer->get_iter_at_offset(start_offset), buffer->get_insert()->get_iter()); |
|
|
else { |
|
|
buffer->apply_tag(italic_tag, buffer->get_iter_at_offset(start_offset), buffer->get_insert()->get_iter()); |
|
|
buffer->apply_tag(bold_tag, buffer->get_iter_at_offset(start_offset), buffer->get_insert()->get_iter()); |
|
|
} |
|
|
i += prefix.size() - 1; |
|
|
return true; |
|
|
}; |
|
|
|
|
|
auto insert_strikethrough = [&] { |
|
|
if(starts_with(input, i, "~~")) { |
|
|
auto i_saved = i; |
|
|
i += 2; |
|
|
if(i < to) { |
|
|
auto start = i; |
|
|
for(; i < to; i++) { |
|
|
if(!unescape(i) && starts_with(input, i, "~~")) |
|
|
break; |
|
|
} |
|
|
if(i == to) { |
|
|
i = i_saved; |
|
|
return false; |
|
|
} |
|
|
insert_with_links_tagged(partial); |
|
|
partial.clear(); |
|
|
auto start_offset = buffer->get_insert()->get_iter().get_offset(); |
|
|
insert_text(start, i); |
|
|
buffer->apply_tag(strikethrough_tag, buffer->get_iter_at_offset(start_offset), buffer->get_insert()->get_iter()); |
|
|
i++; |
|
|
return true; |
|
|
} |
|
|
i = i_saved; |
|
|
} |
|
|
return false; |
|
|
}; |
|
|
|
|
|
auto insert_code = [&] { |
|
|
if(input[i] == '`') { |
|
|
auto i_saved = i; |
|
|
i++; |
|
|
if(i < to) { |
|
|
bool two_backticks = false; |
|
|
if(input[i] == '`') { |
|
|
two_backticks = true; |
|
|
i++; |
|
|
} |
|
|
if(i < to) { |
|
|
auto start = i; |
|
|
for(; i < to; i++) { |
|
|
if(input[i] == '`') { |
|
|
if(!two_backticks) |
|
|
break; |
|
|
if(i + 1 < to && input[i + 1] == '`') |
|
|
break; |
|
|
} |
|
|
} |
|
|
if(i == to) { |
|
|
i = i_saved; |
|
|
return false; |
|
|
} |
|
|
insert_with_links_tagged(partial); |
|
|
partial.clear(); |
|
|
buffer->insert_with_tag(buffer->get_insert()->get_iter(), input.substr(start, i - start), code_tag); |
|
|
if(two_backticks) |
|
|
i++; |
|
|
return true; |
|
|
} |
|
|
} |
|
|
i = i_saved; |
|
|
} |
|
|
return false; |
|
|
}; |
|
|
|
|
|
auto insert_link = [&] { |
|
|
if(input[i] == '[') { |
|
|
auto i_saved = i; |
|
|
i++; |
|
|
if(i < to) { |
|
|
auto text_start = i; |
|
|
for(; i < to; i++) { |
|
|
if(!unescape(i) && input[i] == ']') |
|
|
break; |
|
|
} |
|
|
if(i == to) { |
|
|
i = i_saved; |
|
|
return false; |
|
|
} |
|
|
auto text_end = i; |
|
|
i++; |
|
|
if(i < to && input[i] == '(') { |
|
|
i++; |
|
|
auto link_start = i; |
|
|
for(; i < to; i++) { |
|
|
if(!unescape(i) && input[i] == ')') |
|
|
break; |
|
|
} |
|
|
if(i == to) { |
|
|
i = i_saved; |
|
|
return false; |
|
|
} |
|
|
insert_with_links_tagged(partial); |
|
|
partial.clear(); |
|
|
auto start_offset = buffer->get_insert()->get_iter().get_offset(); |
|
|
insert_text(text_start, text_end); |
|
|
auto start = buffer->get_iter_at_offset(start_offset); |
|
|
auto end = buffer->get_insert()->get_iter(); |
|
|
buffer->apply_tag(link_tag, start, end); |
|
|
auto tag = buffer->create_tag(); |
|
|
buffer->apply_tag(tag, start, end); |
|
|
links.emplace(tag, input.substr(link_start, i - link_start)); |
|
|
return true; |
|
|
} |
|
|
else if(i < to && input[i] == '[') { |
|
|
i++; |
|
|
auto link_start = i; |
|
|
for(; i < to; i++) { |
|
|
if(!unescape(i) && input[i] == ']') |
|
|
break; |
|
|
} |
|
|
if(i == to) { |
|
|
i = i_saved; |
|
|
return false; |
|
|
} |
|
|
insert_with_links_tagged(partial); |
|
|
partial.clear(); |
|
|
auto start_offset = buffer->get_insert()->get_iter().get_offset(); |
|
|
insert_text(text_start, text_end); |
|
|
auto start = buffer->get_iter_at_offset(start_offset); |
|
|
auto end = buffer->get_insert()->get_iter(); |
|
|
buffer->apply_tag(link_tag, start, end); |
|
|
auto tag = buffer->create_tag(); |
|
|
buffer->apply_tag(tag, start, end); |
|
|
reference_links.emplace(tag, input.substr(link_start, i - link_start)); |
|
|
return true; |
|
|
} |
|
|
else if(text_start != text_end) { |
|
|
insert_with_links_tagged(partial); |
|
|
partial.clear(); |
|
|
auto start_offset = buffer->get_insert()->get_iter().get_offset(); |
|
|
insert_text(text_start, text_end); |
|
|
auto start = buffer->get_iter_at_offset(start_offset); |
|
|
auto end = buffer->get_insert()->get_iter(); |
|
|
buffer->apply_tag(link_tag, start, end); |
|
|
auto tag = buffer->create_tag(); |
|
|
buffer->apply_tag(tag, start, end); |
|
|
reference_links.emplace(tag, buffer->get_text(start, end)); |
|
|
i = text_end; |
|
|
return true; |
|
|
} |
|
|
} |
|
|
i = i_saved; |
|
|
} |
|
|
return false; |
|
|
}; |
|
|
|
|
|
for(; i < to; i++) { |
|
|
if(!unescape(i) && (insert_code() || insert_emphasis() || insert_strikethrough() || insert_link())) |
|
|
continue; |
|
|
if(input[i] == '\n' && i + 1 < to) |
|
|
partial += ' '; |
|
|
else if(input[i] == ' ' && ((i > from && (input[i - 1] == '\n' || input[i - 1] == ' ')))) |
|
|
continue; |
|
|
else |
|
|
partial += input[i]; |
|
|
} |
|
|
insert_with_links_tagged(partial); |
|
|
}; |
|
|
|
|
|
auto insert_header = [&] { |
|
|
size_t end_next_line = std::string::npos; |
|
|
int header = 0; |
|
|
for(; i < input.size() && header < 6 && input[i] == '#'; i++) |
|
|
header++; |
|
|
if(header == 0) { |
|
|
auto i_saved = i; |
|
|
forward_to({'\n'}); |
|
|
i++; |
|
|
if(i < input.size() && (input[i] == '=' || input[i] == '-')) { |
|
|
forward_passed({input[i]}); |
|
|
if(i == input.size() || input[i] == '\n') { |
|
|
if(input[i - 1] == '=') |
|
|
header = 1; |
|
|
else |
|
|
header = 2; |
|
|
end_next_line = i; |
|
|
} |
|
|
} |
|
|
i = i_saved; |
|
|
} |
|
|
if(header == 0) |
|
|
return false; |
|
|
forward_passed({' '}); |
|
|
auto start = i; |
|
|
forward_to({'\n'}); |
|
|
auto end = buffer->end(); |
|
|
if(end.backward_char() && !end.starts_line()) |
|
|
buffer->insert_at_cursor("\n"); |
|
|
auto start_offset = buffer->get_insert()->get_iter().get_offset(); |
|
|
insert_text(start, i); |
|
|
if(header == 1) |
|
|
buffer->apply_tag(h1_tag, buffer->get_iter_at_offset(start_offset), buffer->get_insert()->get_iter()); |
|
|
else if(header == 2) |
|
|
buffer->apply_tag(h2_tag, buffer->get_iter_at_offset(start_offset), buffer->get_insert()->get_iter()); |
|
|
else if(header == 3) |
|
|
buffer->apply_tag(h3_tag, buffer->get_iter_at_offset(start_offset), buffer->get_insert()->get_iter()); |
|
|
buffer->insert_at_cursor("\n\n"); |
|
|
if(end_next_line != std::string::npos) |
|
|
i = end_next_line; |
|
|
return true; |
|
|
}; |
|
|
|
|
|
auto is_empty_line = [&] { |
|
|
auto i_saved = i; |
|
|
if(forward_passed({' ', '\t'}) && input[i] == '\n') |
|
|
return true; |
|
|
i = i_saved; |
|
|
return false; |
|
|
}; |
|
|
|
|
|
auto insert_code_block = [&] { |
|
|
if(starts_with(input, i, "```")) { |
|
|
auto i_saved = i; |
|
|
if(forward_to({'\n'})) { |
|
|
auto language = input.substr(i_saved + 3, i - (i_saved + 3)); |
|
|
i++; |
|
|
if(i < input.size()) { |
|
|
auto start = i; |
|
|
while(i < input.size() && !(input[i - 1] == '\n' && starts_with(input, i, "```"))) |
|
|
i++; |
|
|
if(i == input.size()) { |
|
|
i = i_saved; |
|
|
return false; |
|
|
} |
|
|
insert_code(input.substr(start, i - start), language, true); |
|
|
i += 4; |
|
|
if(is_empty_line()) |
|
|
buffer->insert_at_cursor("\n"); |
|
|
return true; |
|
|
} |
|
|
} |
|
|
i = i_saved; |
|
|
} |
|
|
return false; |
|
|
}; |
|
|
|
|
|
auto insert_reference = [&] { |
|
|
if(input[i] == '[') { |
|
|
auto i_saved = i; |
|
|
i++; |
|
|
if(i < input.size()) { |
|
|
auto reference_start = i; |
|
|
for(; i < input.size(); i++) { |
|
|
if(!unescape(i) && input[i] == ']') |
|
|
break; |
|
|
} |
|
|
if(i == input.size()) { |
|
|
i = i_saved; |
|
|
return false; |
|
|
} |
|
|
auto reference_end = i; |
|
|
i++; |
|
|
if(forward_passed({' ', '\n'}) && input[i] == ':') { |
|
|
i++; |
|
|
if(forward_passed({' ', '\n'})) { |
|
|
auto link_start = i; |
|
|
forward_to({' ', '\n'}); |
|
|
auto start_offset = buffer->get_insert()->get_iter().get_offset(); |
|
|
insert_text(reference_start, reference_end); |
|
|
auto start = buffer->get_iter_at_offset(start_offset); |
|
|
auto end = buffer->get_insert()->get_iter(); |
|
|
references.emplace(buffer->get_text(start, end), input.substr(link_start, i - link_start)); |
|
|
buffer->erase(start, end); |
|
|
return true; |
|
|
} |
|
|
} |
|
|
} |
|
|
i = i_saved; |
|
|
} |
|
|
return false; |
|
|
}; |
|
|
|
|
|
auto is_number = [&] { |
|
|
return input[i] >= '0' && input[i] <= '9'; |
|
|
}; |
|
|
|
|
|
auto forward_passed_number = [&] { |
|
|
while(i < input.size() && is_number()) |
|
|
++i; |
|
|
return i < input.size(); |
|
|
}; |
|
|
|
|
|
std::function<bool()> insert_list = [&] { |
|
|
auto i_saved = i; |
|
|
if(starts_with(input, i, "- ") || starts_with(input, i, "+ ") || starts_with(input, i, "* ") || (is_number() && forward_passed_number() && starts_with(input, i, ". "))) { |
|
|
auto start = i_saved; |
|
|
while(true) { |
|
|
forward_to({'\n'}); |
|
|
++i; |
|
|
if(i >= input.size()) { |
|
|
insert_text(start, i - 1); |
|
|
break; |
|
|
} |
|
|
if(is_empty_line()) { |
|
|
insert_text(start, i - 1); |
|
|
buffer->insert_at_cursor("\n\n"); |
|
|
break; |
|
|
} |
|
|
auto i_saved = i; |
|
|
// TODO: Fix check: starts_with(input, i, "1. ") |
|
|
if(forward_passed({' ', '\t'})) { |
|
|
auto i_saved2 = i; |
|
|
if(starts_with(input, i, "- ") || starts_with(input, i, "+ ") || starts_with(input, i, "* ") || (is_number() && forward_passed_number() && starts_with(input, i, ". "))) { |
|
|
i = i_saved2; |
|
|
insert_text(start, i_saved - 1); |
|
|
buffer->insert_at_cursor('\n' + input.substr(i_saved, i - i_saved)); |
|
|
insert_list(); |
|
|
break; |
|
|
} |
|
|
} |
|
|
i = i_saved; |
|
|
} |
|
|
return true; |
|
|
} |
|
|
i = i_saved; |
|
|
return false; |
|
|
}; |
|
|
|
|
|
auto forward_passed_empty_line = [&] { |
|
|
while(is_empty_line()) |
|
|
++i; |
|
|
return i < input.size(); |
|
|
}; |
|
|
|
|
|
while(forward_passed_empty_line()) { |
|
|
if(insert_header() || insert_code_block() || insert_reference() || insert_list()) |
|
|
continue; |
|
|
// Insert paragraph: |
|
|
auto start = i; |
|
|
bool ends_with_empty_line = false; |
|
|
for(; forward_to({'\n'}); i++) { |
|
|
++i; |
|
|
ScopeGuard guard{[&i] { --i; }}; |
|
|
if(is_empty_line()) { |
|
|
ends_with_empty_line = true; |
|
|
break; |
|
|
} |
|
|
if(i < input.size() && (input[i] == '#' || |
|
|
starts_with(input, i, "```") || starts_with(input, i, "- ") || |
|
|
starts_with(input, i, "+ ") || starts_with(input, i, "* ") || |
|
|
starts_with(input, i, "1. "))) { |
|
|
break; |
|
|
} |
|
|
} |
|
|
while(start < i && (input[start] == ' ' || input[start] == '\t' || input[start] == '\n')) |
|
|
++start; |
|
|
insert_text(start, i); |
|
|
buffer->insert_at_cursor(ends_with_empty_line ? "\n\n" : "\n"); |
|
|
} |
|
|
|
|
|
// Remove invalid reference links |
|
|
for(auto link_it = reference_links.begin(); link_it != reference_links.end();) { |
|
|
auto reference_it = references.find(link_it->second); |
|
|
if(reference_it == references.end()) { |
|
|
auto it = buffer->begin(); |
|
|
if(it.begins_tag(link_it->first) || it.forward_to_tag_toggle(link_it->first)) { |
|
|
auto start = it; |
|
|
it.forward_to_tag_toggle(link_it->first); |
|
|
buffer->remove_tag(link_tag, start, it); |
|
|
buffer->remove_tag(link_it->first, buffer->begin(), buffer->end()); |
|
|
auto start_mark = Source::Mark(start); |
|
|
buffer->insert(it, "]"); |
|
|
buffer->insert(start_mark->get_iter(), "["); |
|
|
link_it = reference_links.erase(link_it); |
|
|
continue; |
|
|
} |
|
|
} |
|
|
++link_it; |
|
|
} |
|
|
|
|
|
remove_trailing_newlines(); |
|
|
} |
|
|
|
|
|
void Tooltip::insert_code(const std::string &code, boost::variant<std::string, Glib::RefPtr<Gsv::Language>> language_variant, bool block) { |
|
|
create_tags(); |
|
|
|
|
|
auto insert_iter = buffer->get_insert()->get_iter(); |
|
|
Source::Mark start_mark(insert_iter); |
|
|
|
|
|
bool style_format_type_description = false; |
|
|
if(!block) { |
|
|
auto pos = code.find("\n"); |
|
|
if(pos != std::string::npos) |
|
|
block = true; |
|
|
else if(insert_iter == buffer->begin() && utf8_character_count(code) > static_cast<size_t>(max_columns)) { |
|
|
block = true; |
|
|
style_format_type_description = true; |
|
|
} |
|
|
} |
|
|
|
|
|
buffer->insert_with_tag(insert_iter, code, block ? code_block_tag : code_tag); |
|
|
|
|
|
if(view) { |
|
|
Glib::RefPtr<Gsv::Language> language; |
|
|
if(auto language_ptr = boost::get<Glib::RefPtr<Gsv::Language>>(&language_variant)) |
|
|
language = *language_ptr; |
|
|
else if(auto language_identifier = boost::get<std::string>(&language_variant)) { |
|
|
if(!language_identifier->empty()) { |
|
|
language = Source::LanguageManager::get_default()->get_language(*language_identifier); |
|
|
if(!language) |
|
|
language = Source::guess_language('.' + *language_identifier); |
|
|
if(!language) { |
|
|
if(auto source_view = dynamic_cast<Source::View *>(view)) |
|
|
language = Source::LanguageManager::get_default()->get_language(source_view->language_id); |
|
|
} |
|
|
} |
|
|
} |
|
|
if(language) { |
|
|
Gsv::View tmp_view; |
|
|
tmp_view.get_buffer()->set_text(code); |
|
|
auto scheme = view->get_source_buffer()->get_style_scheme(); |
|
|
tmp_view.get_source_buffer()->set_style_scheme(scheme); |
|
|
tmp_view.get_source_buffer()->set_language(language); |
|
|
tmp_view.get_source_buffer()->set_highlight_syntax(true); |
|
|
tmp_view.get_source_buffer()->ensure_highlight(tmp_view.get_buffer()->begin(), tmp_view.get_buffer()->end()); |
|
|
|
|
|
auto start_iter = start_mark->get_iter(); |
|
|
tmp_view.get_buffer()->get_tag_table()->foreach([this, &tmp_view, &start_iter](const Glib::RefPtr<Gtk::TextTag> &tmp_tag) { |
|
|
if(tmp_tag->property_foreground_set()) { |
|
|
auto tag = buffer->create_tag(); |
|
|
tag->property_foreground_rgba() = tmp_tag->property_foreground_rgba().get_value(); |
|
|
|
|
|
auto tmp_iter = tmp_view.get_source_buffer()->begin(); |
|
|
Gtk::TextIter tmp_start; |
|
|
if(tmp_iter.begins_tag(tmp_tag)) |
|
|
tmp_start = tmp_iter; |
|
|
while(tmp_iter.forward_to_tag_toggle(tmp_tag)) { |
|
|
if(tmp_iter.ends_tag(tmp_tag)) { |
|
|
auto start = start_iter; |
|
|
start.forward_chars(tmp_start.get_offset()); |
|
|
auto end = start_iter; |
|
|
end.forward_chars(tmp_iter.get_offset()); |
|
|
buffer->apply_tag(tag, start, end); |
|
|
} |
|
|
else |
|
|
tmp_start = tmp_iter; |
|
|
} |
|
|
} |
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
// Make long type descriptions readable |
|
|
if(style_format_type_description) { |
|
|
int initial_max_columns = max_columns; |
|
|
std::list<Source::Mark> open_brackets; |
|
|
for(auto iter = start_mark->get_iter(); iter; iter.forward_char()) { |
|
|
if(*iter == '(' || *iter == '[' || *iter == '{' || *iter == '<') |
|
|
open_brackets.emplace_back(iter); |
|
|
else if(*iter == ')' || *iter == ']' || *iter == '}' || *iter == '>') { |
|
|
if(!open_brackets.empty()) |
|
|
open_brackets.pop_back(); |
|
|
} |
|
|
else if(*iter == ',' && !open_brackets.empty()) { |
|
|
auto line_offset = open_brackets.back()->get_iter().get_line_offset(); |
|
|
if(iter.forward_char()) { |
|
|
Source::Mark mark(iter); |
|
|
auto previous = iter; |
|
|
if(*iter == ' ' && iter.forward_char()) { |
|
|
buffer->erase(previous, iter); |
|
|
iter = mark->get_iter(); |
|
|
} |
|
|
buffer->insert(mark->get_iter(), '\n' + std::string(line_offset + 1, ' ')); |
|
|
iter = mark->get_iter(); |
|
|
} |
|
|
} |
|
|
else if(*iter == ' ' && open_brackets.empty() && iter.get_line_offset() + (buffer->end().get_offset() - iter.get_offset()) > initial_max_columns) { // Add new line between return value, parameters and specifiers |
|
|
auto previous = iter; |
|
|
if(iter.forward_char()) { |
|
|
Source::Mark mark(iter); |
|
|
buffer->erase(previous, iter); |
|
|
buffer->insert(mark->get_iter(), "\n"); |
|
|
iter = mark->get_iter(); |
|
|
} |
|
|
} |
|
|
max_columns = std::max(max_columns, iter.get_line_offset() + 1); |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
void Tooltip::insert_doxygen(const std::string &input_, bool remove_delimiters) { |
|
|
create_tags(); |
|
|
|
|
|
auto get_text = [&] { |
|
|
auto &input = input_; |
|
|
size_t i = 0; |
|
|
auto forward_passed = [&](const std::vector<char> chars) { |
|
|
for(; i < input.size(); i++) { |
|
|
if(std::none_of(chars.begin(), chars.end(), [&](char chr) { return input[i] == chr; })) |
|
|
return true; |
|
|
} |
|
|
return false; |
|
|
}; |
|
|
|
|
|
size_t min_indent = std::numeric_limits<size_t>::max(); |
|
|
auto forward_to_text = [&](bool first_line) -> bool { |
|
|
auto line_start = i; |
|
|
auto after_delimiter = i; |
|
|
forward_passed({' ', '\t'}); |
|
|
if(i < input.size() && (input[i] == '/' || input[i] == '*')) { |
|
|
forward_passed({'/'}); |
|
|
if(i < input.size() && input[i] == '*' && forward_passed({'*'})) { |
|
|
if(input[i] == '/') |
|
|
return false; |
|
|
} |
|
|
forward_passed({'!'}); |
|
|
after_delimiter = i; |
|
|
} |
|
|
forward_passed({' ', '\t'}); |
|
|
if(!first_line && i < input.size() && input[i] != '\n') { |
|
|
min_indent = std::min(min_indent, i - line_start); |
|
|
if(i - line_start > min_indent && line_start + min_indent >= after_delimiter) |
|
|
i = line_start + min_indent; |
|
|
} |
|
|
return i < input.size(); |
|
|
}; |
|
|
|
|
|
std::string text; |
|
|
text.reserve(input.size()); |
|
|
bool first_line = true; |
|
|
for(; i < input.size(); ++i) { |
|
|
if(!forward_to_text(first_line)) |
|
|
break; |
|
|
if(!(first_line && input[i] == '\n')) { |
|
|
for(; i < input.size(); ++i) { |
|
|
text += input[i]; |
|
|
if(input[i] == '\n') |
|
|
break; |
|
|
} |
|
|
} |
|
|
first_line = false; |
|
|
} |
|
|
if(text.size() >= 2 && text[text.size() - 2] == '*' && text[text.size() - 1] == '/') { |
|
|
text.erase(text.size() - 2, 2); |
|
|
while(!text.empty() && text.back() == ' ') |
|
|
text.pop_back(); |
|
|
} |
|
|
return text; |
|
|
}; |
|
|
|
|
|
const auto &input = remove_delimiters ? get_text() : input_; |
|
|
size_t i = 0; |
|
|
auto forward_passed = [&](const std::vector<char> chars) { |
|
|
for(; i < input.size(); i++) { |
|
|
if(std::none_of(chars.begin(), chars.end(), [&](char chr) { return input[i] == chr; })) |
|
|
return true; |
|
|
} |
|
|
return false; |
|
|
}; |
|
|
|
|
|
auto get_token = [&] { |
|
|
auto start = i; |
|
|
while(i < input.size() && ((input[i] >= 'a' && input[i] <= 'z') || (input[i] >= 'A' && input[i] <= 'Z') || (input[i] >= '0' && input[i] <= '9') || input[i] == '_')) |
|
|
++i; |
|
|
return input.substr(start, i - start); |
|
|
}; |
|
|
|
|
|
auto is_escape_character = [&] { |
|
|
static std::set<char> escape_characters{'\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '#', '+', '-', '.', '!', '|'}; |
|
|
return escape_characters.find(input[i]) != escape_characters.end(); |
|
|
}; |
|
|
|
|
|
auto get_word = [&](bool escape) { |
|
|
std::string word; |
|
|
for(; i < input.size() && input[i] != ' ' && input[i] != '\t' && input[i] != '\n'; ++i) { |
|
|
if((input[i] == '.' || input[i] == ',' || input[i] == ':' || input[i] == ';' || input[i] == '!' || input[i] == '?') && |
|
|
(i + 1 >= input.size() || input[i + 1] == ' ' || input[i + 1] == '\t' || input[i + 1] == '\n')) { |
|
|
break; |
|
|
} |
|
|
if(escape && is_escape_character()) |
|
|
word += "\\"; |
|
|
word += input[i]; |
|
|
} |
|
|
return word; |
|
|
}; |
|
|
|
|
|
std::string markdown; |
|
|
markdown.reserve(input.size()); |
|
|
bool first_parameter = true; |
|
|
|
|
|
while(i < input.size()) { |
|
|
if(input[i] == '\\' || input[i] == '@') { |
|
|
auto i_saved = i; |
|
|
++i; |
|
|
auto token = get_token(); |
|
|
if(token.empty()) { |
|
|
if(i < input.size()) { |
|
|
if(input[i] == '\\') |
|
|
markdown += "\\\\"; |
|
|
else |
|
|
markdown += input[i]; |
|
|
} |
|
|
++i; |
|
|
} |
|
|
else if(token == "a" || token == "e" || token == "em") { |
|
|
forward_passed({' ', '\t', '\n'}); |
|
|
markdown += '*' + get_word(true) + '*'; |
|
|
} |
|
|
else if(token == "b") { |
|
|
forward_passed({' ', '\t', '\n'}); |
|
|
markdown += "**" + get_word(true) + "**"; |
|
|
} |
|
|
else if(token == "c" || token == "p") { |
|
|
forward_passed({' ', '\t', '\n'}); |
|
|
markdown += '`' + get_word(false) + '`'; |
|
|
} |
|
|
else if(token == "brief") |
|
|
forward_passed({' ', '\t', '\n'}); |
|
|
else if(token == "param") { |
|
|
std::string direction; |
|
|
if(input[i] == '[') { |
|
|
++i; |
|
|
if(i >= input.size()) |
|
|
break; |
|
|
auto pos = input.find(']', i); |
|
|
if(pos != std::string::npos) { |
|
|
direction = input.substr(i, pos - i); |
|
|
i = pos + 1; |
|
|
} |
|
|
} |
|
|
forward_passed({' ', '\t', '\n'}); |
|
|
if(first_parameter) { |
|
|
markdown += "\n\n**Parameters:**\n"; |
|
|
first_parameter = false; |
|
|
} |
|
|
markdown += "- `" + get_word(false) + '`'; |
|
|
if(!direction.empty()) |
|
|
markdown += " (" + direction + ')'; |
|
|
} |
|
|
else if(token == "return" || token == "returns" || token == "result") { |
|
|
forward_passed({' ', '\t', '\n'}); |
|
|
markdown += "\n\n**Returns** "; |
|
|
} |
|
|
else if(token == "sa" || token == "see") { |
|
|
forward_passed({' ', '\t', '\n'}); |
|
|
markdown += "\n\nSee also "; |
|
|
} |
|
|
else if(token == "throw" || token == "throws" || token == "exception") { |
|
|
forward_passed({' ', '\t', '\n'}); |
|
|
markdown += "\n\n**Throws** "; |
|
|
} |
|
|
else if(token == "deprecated") { |
|
|
forward_passed({' ', '\t', '\n'}); |
|
|
markdown += "\n\n**Deprecated** "; |
|
|
} |
|
|
else if(token == "note") { |
|
|
forward_passed({' ', '\t', '\n'}); |
|
|
markdown += "\n\n**Note** "; |
|
|
} |
|
|
else if(token == "code") { |
|
|
std::string language_id; |
|
|
if(input[i] == '{') { |
|
|
++i; |
|
|
if(i >= input.size()) |
|
|
break; |
|
|
if(input[i] == '.') { |
|
|
auto pos = input.find('}', i + 1); |
|
|
if(pos != std::string::npos) { |
|
|
language_id = input.substr(i + 1, pos - (i + 1)); |
|
|
i = pos + 1; |
|
|
} |
|
|
} |
|
|
} |
|
|
auto start = i + 1; |
|
|
for(; i < input.size(); ++i) { |
|
|
if(input[i] == '\n') { |
|
|
++i; |
|
|
if(i >= input.size()) |
|
|
break; |
|
|
if(input[i] == '\\' || input[i] == '@') { |
|
|
auto end = i - 1; |
|
|
++i; |
|
|
auto token = get_token(); |
|
|
if(token == "endcode") { |
|
|
if(language_id.empty() && view) { |
|
|
if(auto source_view = dynamic_cast<Source::View *>(view)) |
|
|
language_id = source_view->language_id; |
|
|
} |
|
|
markdown += "```" + language_id + '\n' + input.substr(start, end - start) + "\n```\n"; |
|
|
++i; |
|
|
break; |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
else if(token == "dot") { |
|
|
for(; i < input.size(); ++i) { |
|
|
if(input[i] == '\n') { |
|
|
++i; |
|
|
if(i >= input.size()) |
|
|
break; |
|
|
if(input[i] == '\\') { |
|
|
++i; |
|
|
auto token = get_token(); |
|
|
if(token == "enddot") { |
|
|
markdown += "\n\n"; |
|
|
++i; |
|
|
break; |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
else if(token == "verbatim") { |
|
|
auto start = i + 1; |
|
|
for(; i < input.size(); ++i) { |
|
|
if(input[i] == '\n') { |
|
|
++i; |
|
|
if(i >= input.size()) |
|
|
break; |
|
|
if(input[i] == '\\') { |
|
|
auto end = i - 1; |
|
|
++i; |
|
|
auto token = get_token(); |
|
|
if(token == "endverbatim") { |
|
|
markdown += "```\n" + input.substr(start, end - start) + "\n```\n"; |
|
|
++i; |
|
|
break; |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
else { |
|
|
auto i2 = i_saved; |
|
|
--i2; |
|
|
while(i2 > 0 && (input[i2] == ' ' || input[i2] == '\t')) |
|
|
--i2; |
|
|
if(input[i2] == '\n' || i2 == 0) |
|
|
markdown += "\n\n"; |
|
|
if(input[i_saved] == '\\') |
|
|
markdown += '\\'; |
|
|
markdown += input[i_saved]; |
|
|
markdown += token; |
|
|
} |
|
|
} |
|
|
else if(input[i] == '%') |
|
|
++i; |
|
|
else if(starts_with(input, i, "<tt>")) { |
|
|
i += 4; |
|
|
auto pos = input.find("</tt>", i); |
|
|
if(pos != std::string::npos) { |
|
|
markdown += '`' + input.substr(i, pos - i) + '`'; |
|
|
i = pos + 5; |
|
|
} |
|
|
} |
|
|
else if(starts_with(input, i, "<code>")) { |
|
|
i += 6; |
|
|
auto pos = input.find("</code>", i); |
|
|
if(pos != std::string::npos) { |
|
|
markdown += '`' + input.substr(i, pos - i) + '`'; |
|
|
i = pos + 7; |
|
|
} |
|
|
} |
|
|
else if(starts_with(input, i, "<em>")) { |
|
|
i += 4; |
|
|
markdown += '*'; |
|
|
for(; i < input.size(); ++i) { |
|
|
if(input[i] == '\\' && i + 1 < input.size()) { |
|
|
markdown += input[i]; |
|
|
markdown += input[i + 1]; |
|
|
++i; |
|
|
} |
|
|
else { |
|
|
if(starts_with(input, i, "</em>")) { |
|
|
i += 5; |
|
|
break; |
|
|
} |
|
|
if(is_escape_character()) |
|
|
markdown += '\\'; |
|
|
markdown += input[i]; |
|
|
} |
|
|
} |
|
|
markdown += '*'; |
|
|
} |
|
|
else if(starts_with(input, i, "<b>")) { |
|
|
i += 3; |
|
|
markdown += "**"; |
|
|
for(; i < input.size(); ++i) { |
|
|
if(input[i] == '\\' && i + 1 < input.size()) { |
|
|
markdown += input[i]; |
|
|
markdown += input[i + 1]; |
|
|
++i; |
|
|
} |
|
|
else { |
|
|
if(starts_with(input, i, "</b>")) { |
|
|
i += 4; |
|
|
break; |
|
|
} |
|
|
if(is_escape_character()) |
|
|
markdown += '\\'; |
|
|
markdown += input[i]; |
|
|
} |
|
|
} |
|
|
markdown += "**"; |
|
|
} |
|
|
else if(input[i] == '`') { |
|
|
auto i_saved = i; |
|
|
std::string graves; |
|
|
do { |
|
|
graves += input[i]; |
|
|
++i; |
|
|
} while(i < input.size() && input[i] == '`'); |
|
|
auto pos = input.find(graves, i); |
|
|
if(pos != std::string::npos) { |
|
|
markdown += input.substr(i_saved, pos + graves.size() - i_saved); |
|
|
i = pos + graves.size(); |
|
|
} |
|
|
} |
|
|
else if(input[i] == '"') { |
|
|
markdown += "\""; |
|
|
++i; |
|
|
for(; i < input.size(); ++i) { |
|
|
if(input[i] == '\\' && i + 1 < input.size()) { |
|
|
markdown += input[i]; |
|
|
markdown += input[i + 1]; |
|
|
++i; |
|
|
} |
|
|
else { |
|
|
if(is_escape_character()) |
|
|
markdown += '\\'; |
|
|
markdown += input[i]; |
|
|
if(input[i] == '"') |
|
|
break; |
|
|
} |
|
|
} |
|
|
++i; |
|
|
} |
|
|
else { |
|
|
markdown += input[i]; |
|
|
++i; |
|
|
} |
|
|
} |
|
|
|
|
|
if(!markdown.empty()) |
|
|
insert_markdown(markdown); |
|
|
} |
|
|
|
|
|
void Tooltip::insert_docstring(const std::string &input_) { |
|
|
create_tags(); |
|
|
|
|
|
// Workaround for python-language-server that returns unnecessary function signatures |
|
|
const static std::regex regex("^([a-zA-Z0-9_]+\\([^\n]*\\)( -> [^\n]+)?(\n|$))+(\n|$)", std::regex::extended | std::regex::optimize); |
|
|
std::smatch sm; |
|
|
const std::string &input = std::regex_search(input_, sm, regex) ? sm.suffix() : input_; |
|
|
|
|
|
std::string partial; |
|
|
partial.reserve(input.size()); |
|
|
size_t i = 0; |
|
|
|
|
|
auto unescape = [&](size_t &i) { |
|
|
if(input[i] == '\\') { |
|
|
if(i + 1 < input.size()) |
|
|
i++; |
|
|
return true; |
|
|
} |
|
|
return false; |
|
|
}; |
|
|
|
|
|
auto forward_to = [&](const std::vector<char> chars) { |
|
|
for(; i < input.size(); i++) { |
|
|
if(std::any_of(chars.begin(), chars.end(), [&input, &i](char chr) { return input[i] == chr; })) |
|
|
return true; |
|
|
} |
|
|
return false; |
|
|
}; |
|
|
|
|
|
auto forward_passed = [&](const std::vector<char> chars) { |
|
|
for(; i < input.size(); i++) { |
|
|
if(std::none_of(chars.begin(), chars.end(), [&input, &i](char chr) { return input[i] == chr; })) |
|
|
return true; |
|
|
} |
|
|
return false; |
|
|
}; |
|
|
|
|
|
auto is_whitespace_character = [&](size_t i) { |
|
|
return input[i] == ' ' || input[i] == '\t' || input[i] == '\n' || input[i] == '\r' || input[i] == '\f'; |
|
|
}; |
|
|
|
|
|
auto insert_header = [&] { |
|
|
if(is_whitespace_character(i)) |
|
|
return false; |
|
|
auto i_saved = i; |
|
|
forward_to({'\n'}); |
|
|
auto end_header = i; |
|
|
i++; |
|
|
int header = 0; |
|
|
if(i < input.size() && (input[i] == '=' || input[i] == '-')) { |
|
|
forward_passed({input[i]}); |
|
|
if(i == input.size() || input[i] == '\n') { |
|
|
if(input[i - 1] == '=') |
|
|
header = 1; |
|
|
else |
|
|
header = 2; |
|
|
} |
|
|
} |
|
|
if(header == 0) { |
|
|
i = i_saved; |
|
|
return false; |
|
|
} |
|
|
insert_with_links_tagged(partial); |
|
|
partial.clear(); |
|
|
auto start_offset = buffer->get_insert()->get_iter().get_offset(); |
|
|
insert_with_links_tagged(input.substr(i_saved, end_header - i_saved)); |
|
|
if(header == 1) |
|
|
buffer->apply_tag(h1_tag, buffer->get_iter_at_offset(start_offset), buffer->get_insert()->get_iter()); |
|
|
else if(header == 2) |
|
|
buffer->apply_tag(h2_tag, buffer->get_iter_at_offset(start_offset), buffer->get_insert()->get_iter()); |
|
|
buffer->insert_at_cursor("\n"); |
|
|
return true; |
|
|
}; |
|
|
|
|
|
auto is_punctuation_character = [&](size_t i) { |
|
|
static std::set<char> punctuation_characters = {'!', '"', '#', '$', '%', '&', '\'', '(', ')', '*', '+', ',', '-', '.', '/', ':', ';', '<', '=', '>', '?', '@', '[', '\\', ']', '^', '_', '`', '{', '|', '}', '~'}; |
|
|
return punctuation_characters.find(input[i]) != punctuation_characters.end(); |
|
|
}; |
|
|
|
|
|
auto left_flanking_delimiter_run = [&](size_t i, size_t n) { |
|
|
return !(i + n >= input.size() || is_whitespace_character(i + n)) && |
|
|
(!is_punctuation_character(i + n) || |
|
|
(i == 0 || is_whitespace_character(i - 1) || is_punctuation_character(i - 1))); |
|
|
}; |
|
|
auto right_flanking_delimiter_run = [&](size_t i, size_t n) { |
|
|
return !(i == 0 || is_whitespace_character(i - 1)) && |
|
|
(!is_punctuation_character(i - 1) || |
|
|
(i + n >= input.size() || is_whitespace_character(i + n) || is_punctuation_character(i + n))); |
|
|
}; |
|
|
|
|
|
/// Using rules in https://spec.commonmark.org/0.29/#emphasis-and-strong-emphasis |
|
|
auto insert_emphasis = [&] { |
|
|
auto i_saved = i; |
|
|
std::string prefix; |
|
|
if(input[i] == '*') { |
|
|
prefix += input[i]; |
|
|
++i; |
|
|
for(; prefix.size() < 2 && input[i] == prefix.back(); ++i) |
|
|
prefix += input[i]; |
|
|
} |
|
|
if(prefix.empty()) |
|
|
return false; |
|
|
// Rule 1 and 5 |
|
|
if(!left_flanking_delimiter_run(i - prefix.size(), prefix.size())) { |
|
|
i = i_saved; |
|
|
return false; |
|
|
} |
|
|
auto start = i; |
|
|
for(; i < input.size() - (prefix.size() - 1); i++) { |
|
|
if(!unescape(i)) { |
|
|
if(starts_with(input, i, prefix)) { |
|
|
// Rule 3 and 7 |
|
|
if(right_flanking_delimiter_run(i, prefix.size())) |
|
|
break; |
|
|
else if(left_flanking_delimiter_run(i, prefix.size())) { |
|
|
i = i_saved; |
|
|
return false; |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
if(i >= input.size() - (prefix.size() - 1)) { |
|
|
i = i_saved; |
|
|
return false; |
|
|
} |
|
|
insert_with_links_tagged(partial); |
|
|
partial.clear(); |
|
|
auto start_offset = buffer->get_insert()->get_iter().get_offset(); |
|
|
insert_with_links_tagged(input.substr(start, i - start)); |
|
|
if(prefix.size() == 1) |
|
|
buffer->apply_tag(italic_tag, buffer->get_iter_at_offset(start_offset), buffer->get_insert()->get_iter()); |
|
|
else if(prefix.size() == 2) |
|
|
buffer->apply_tag(bold_tag, buffer->get_iter_at_offset(start_offset), buffer->get_insert()->get_iter()); |
|
|
i += prefix.size() - 1; |
|
|
return true; |
|
|
}; |
|
|
|
|
|
auto insert_code_or_link = [&] { |
|
|
if(input[i] == '`') { |
|
|
auto i_saved = i; |
|
|
i++; |
|
|
if(i < input.size()) { |
|
|
bool two_backticks = false; |
|
|
if(input[i] == '`') { |
|
|
two_backticks = true; |
|
|
i++; |
|
|
} |
|
|
if(i < input.size()) { |
|
|
auto start = i; |
|
|
for(; i < input.size(); i++) { |
|
|
if(input[i] == '`') { |
|
|
if(!two_backticks) |
|
|
break; |
|
|
if(i + 1 < input.size() && input[i + 1] == '`') |
|
|
break; |
|
|
} |
|
|
} |
|
|
if(i == input.size()) { |
|
|
i = i_saved; |
|
|
return false; |
|
|
} |
|
|
insert_with_links_tagged(partial); |
|
|
partial.clear(); |
|
|
if(!two_backticks && i + 1 < input.size() && input[i + 1] == '_') { // Insert link |
|
|
const static std::regex regex("^([^<]*?) ?<([^>]+)>$"); |
|
|
std::smatch sm; |
|
|
if(std::regex_match(input.begin() + start, input.begin() + i, sm, regex)) { |
|
|
if(sm[1].length() == 0) |
|
|
insert_with_links_tagged(sm[2].str()); |
|
|
else { |
|
|
auto start_offset = buffer->get_insert()->get_iter().get_offset(); |
|
|
buffer->insert_at_cursor(sm[1].str()); |
|
|
auto start = buffer->get_iter_at_offset(start_offset); |
|
|
auto end = buffer->get_insert()->get_iter(); |
|
|
buffer->apply_tag(link_tag, start, end); |
|
|
auto tag = buffer->create_tag(); |
|
|
buffer->apply_tag(tag, start, end); |
|
|
links.emplace(tag, sm[2].str()); |
|
|
} |
|
|
} |
|
|
else |
|
|
insert_with_links_tagged(input.substr(start, i - start)); |
|
|
++i; |
|
|
} |
|
|
else { |
|
|
buffer->insert_with_tag(buffer->get_insert()->get_iter(), input.substr(start, i - start), code_tag); |
|
|
if(two_backticks) |
|
|
i++; |
|
|
} |
|
|
return true; |
|
|
} |
|
|
} |
|
|
i = i_saved; |
|
|
} |
|
|
return false; |
|
|
}; |
|
|
|
|
|
auto insert_code_block = [&] { |
|
|
auto i_saved = i; |
|
|
static std::string utf8_space = " "; |
|
|
while(starts_with(input, i, utf8_space)) |
|
|
i += utf8_space.size(); |
|
|
while(i < input.size() && (input[i] == ' ' || input[i] == '\t')) |
|
|
++i; |
|
|
if(starts_with(input, i, ">>>")) { |
|
|
insert_with_links_tagged(partial); |
|
|
partial.clear(); |
|
|
auto pos = input.find("\n\n", i + 3); |
|
|
insert_code(input.substr(i_saved, pos != std::string::npos ? pos - i_saved : pos), {}, true); |
|
|
buffer->insert_at_cursor("\n"); |
|
|
i = pos != std::string::npos ? pos : input.size() - 1; |
|
|
return true; |
|
|
} |
|
|
i = i_saved; |
|
|
return false; |
|
|
}; |
|
|
|
|
|
for(; i < input.size(); ++i) { |
|
|
if(i == 0 || input[i - 1] == '\n') { |
|
|
if(insert_code_block() || insert_header()) |
|
|
continue; |
|
|
} |
|
|
if(!unescape(i) && (insert_code_or_link() || insert_emphasis())) |
|
|
continue; |
|
|
partial += input[i]; |
|
|
} |
|
|
if(!partial.empty()) |
|
|
insert_with_links_tagged(partial); |
|
|
} |
|
|
|
|
|
void Tooltip::remove_trailing_newlines() { |
|
|
auto end = buffer->end(); |
|
|
while(end.starts_line() && end.backward_char()) { |
|
|
} |
|
|
buffer->erase(end, buffer->end()); |
|
|
} |
|
|
|
|
|
void Tooltips::show(const Gdk::Rectangle &rectangle, bool disregard_drawn) { |
|
|
for(auto &tooltip : tooltip_list) { |
|
|
tooltip.update(); |
|
|
if(rectangle.intersects(tooltip.activation_rectangle)) |
|
|
tooltip.show(disregard_drawn, on_motion); |
|
|
else |
|
|
tooltip.hide({}, {}); |
|
|
} |
|
|
} |
|
|
|
|
|
void Tooltips::show(bool disregard_drawn) { |
|
|
for(auto &tooltip : tooltip_list) { |
|
|
tooltip.update(); |
|
|
tooltip.show(disregard_drawn, on_motion); |
|
|
} |
|
|
} |
|
|
|
|
|
void Tooltips::hide(const boost::optional<std::pair<int, int>> &last_mouse_pos, const boost::optional<std::pair<int, int>> &mouse_pos) { |
|
|
for(auto &tooltip : tooltip_list) |
|
|
tooltip.hide(last_mouse_pos, mouse_pos); |
|
|
}
|
|
|
|