1337 lines
37 KiB
C++
1337 lines
37 KiB
C++
#include "draw.h"
|
|
#include "node.h"
|
|
#include "syntax_extension.h"
|
|
#include "strikethrough.h"
|
|
#include "mainwindow.h"
|
|
#include <gdk/gdkthreads.h>
|
|
#include <gdk/gdkselection.h>
|
|
#include <gtkmm/textiter.h>
|
|
#include <gdkmm/window.h>
|
|
#include <iostream>
|
|
#include <stdexcept>
|
|
#include <regex>
|
|
|
|
#define PANGO_SCALE_XXX_LARGE ((double)1.98)
|
|
|
|
struct DispatchData
|
|
{
|
|
GtkTextBuffer *buffer;
|
|
// For inserting text
|
|
std::string text;
|
|
std::string url;
|
|
// For removing text
|
|
int charsTruncated;
|
|
// Optional URL formatting
|
|
std::string urlFont;
|
|
};
|
|
|
|
Draw::Draw(MainWindow &mainWindow)
|
|
: mainWindow(mainWindow),
|
|
buffer(Glib::unwrap(this->get_buffer())),
|
|
addViewSourceMenuItem(true),
|
|
fontSize(10 * PANGO_SCALE),
|
|
fontFamily("Ubuntu Monospace"),
|
|
headingLevel(0),
|
|
listLevel(0),
|
|
isBold(false),
|
|
isItalic(false),
|
|
isStrikethrough(false),
|
|
isHighlight(false),
|
|
isSuperscript(false),
|
|
isSubscript(false),
|
|
isQuote(false),
|
|
bulletListLevel(0),
|
|
orderedListLevel(0),
|
|
isOrderedList(false),
|
|
isLink(false),
|
|
hovingOverLink(false),
|
|
defaultFont(fontFamily),
|
|
isUserAction(false)
|
|
{
|
|
this->disableEdit();
|
|
set_indent(15);
|
|
set_left_margin(10);
|
|
set_right_margin(10);
|
|
set_top_margin(12);
|
|
set_bottom_margin(0);
|
|
set_monospace(true);
|
|
set_app_paintable(true);
|
|
set_pixels_above_lines(1);
|
|
set_pixels_below_lines(2);
|
|
set_pixels_inside_wrap(2);
|
|
set_wrap_mode(Gtk::WrapMode::WRAP_WORD_CHAR);
|
|
|
|
// Set cursors
|
|
auto display = get_display();
|
|
normalCursor = Gdk::Cursor::create(display, "default");
|
|
linkCursor = Gdk::Cursor::create(display, "grab");
|
|
textCursor = Gdk::Cursor::create(display, "text");
|
|
|
|
// Connect Signals
|
|
signal_event_after().connect(sigc::mem_fun(this, &Draw::event_after));
|
|
signal_motion_notify_event().connect(sigc::mem_fun(this, &Draw::motion_notify_event));
|
|
signal_populate_popup().connect(sigc::mem_fun(this, &Draw::populate_popup));
|
|
}
|
|
|
|
/**
|
|
* Links can be activated by clicking or touching the screen.
|
|
*/
|
|
void Draw::event_after(GdkEvent *ev)
|
|
{
|
|
gdouble ex, ey;
|
|
Gtk::TextBuffer::iterator iter;
|
|
int x, y;
|
|
|
|
if (ev->type == GDK_BUTTON_RELEASE)
|
|
{
|
|
GdkEventButton *event;
|
|
event = (GdkEventButton *)ev;
|
|
if (event->button != GDK_BUTTON_PRIMARY)
|
|
return;
|
|
ex = event->x;
|
|
ey = event->y;
|
|
}
|
|
else if (ev->type == GDK_TOUCH_END)
|
|
{
|
|
GdkEventTouch *event;
|
|
event = (GdkEventTouch *)ev;
|
|
ex = event->x;
|
|
ey = event->y;
|
|
}
|
|
else
|
|
return;
|
|
|
|
// Get the textview coordinates and retrieve an iterator
|
|
window_to_buffer_coords(Gtk::TextWindowType::TEXT_WINDOW_WIDGET, ex, ey, x, y);
|
|
get_iter_at_location(iter, x, y);
|
|
// Find the links
|
|
followLink(iter);
|
|
}
|
|
|
|
/**
|
|
* Update the cursor whenever there is a link
|
|
*/
|
|
bool Draw::motion_notify_event(GdkEventMotion *motion_event)
|
|
{
|
|
int x, y;
|
|
window_to_buffer_coords(Gtk::TextWindowType::TEXT_WINDOW_WIDGET, motion_event->x, motion_event->y, x, y);
|
|
this->changeCursor(x, y);
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Adapt right-click menu in textview
|
|
*/
|
|
void Draw::populate_popup(Gtk::Menu *menu)
|
|
{
|
|
auto items = menu->get_children();
|
|
for (auto *item : items)
|
|
{
|
|
Gtk::MenuItem *menuItem = static_cast<Gtk::MenuItem *>(item);
|
|
std::string name = menuItem->get_label();
|
|
if (name.compare("Cu_t") == 0)
|
|
{
|
|
menuItem->set_label("Cu_t (Ctrl+X)");
|
|
}
|
|
else if (name.compare("_Copy") == 0)
|
|
{
|
|
menuItem->set_label("_Copy (Ctrl+C)");
|
|
}
|
|
else if (name.compare("_Paste") == 0)
|
|
{
|
|
menuItem->set_label("_Paste (Ctrl+V)");
|
|
}
|
|
else if (name.compare("_Delete") == 0)
|
|
{
|
|
menuItem->set_label("_Delete (Del)");
|
|
}
|
|
else if (name.compare("Select _All") == 0)
|
|
{
|
|
menuItem->set_label("Select _All (Ctrl+A)");
|
|
}
|
|
else if (name.compare("Insert _Emoji") == 0)
|
|
{
|
|
item->hide();
|
|
}
|
|
}
|
|
if (this->addViewSourceMenuItem)
|
|
{
|
|
Gtk::MenuItem *sourceCodeMenuItem = Gtk::manage(new Gtk::MenuItem("View Source", true));
|
|
sourceCodeMenuItem->signal_activate().connect(source_code);
|
|
sourceCodeMenuItem->show();
|
|
menu->append(*sourceCodeMenuItem);
|
|
}
|
|
}
|
|
|
|
void Draw::showMessage(const std::string &message, const std::string &detailed_info)
|
|
{
|
|
if (get_editable())
|
|
this->disableEdit();
|
|
this->clearOnThread();
|
|
|
|
this->headingLevel = 1;
|
|
this->insertText(message);
|
|
this->headingLevel = 0;
|
|
this->insertMarkupTextOnThread("\n\n");
|
|
this->insertText(detailed_info);
|
|
}
|
|
|
|
void Draw::showStartPage()
|
|
{
|
|
if (get_editable())
|
|
this->disableEdit();
|
|
this->clearOnThread();
|
|
|
|
this->headingLevel = 1;
|
|
this->insertText("🚀🌍 Welcome to the Decentralized Web (DWeb)");
|
|
this->headingLevel = 0;
|
|
this->insertMarkupTextOnThread("\n\n");
|
|
this->insertText("You can surf the web as intended via LibreWeb, by using IPFS as a decentralized solution. This is also the fastest browser in the world.\n\n\
|
|
The content is fully written in markdown format, allowing you to easily publish your own site, blog article or e-book.\n\
|
|
This browser has even a built-in editor. Check it out in the menu: File->New Document!\n\n");
|
|
this->insertText("See an example page hosted on IPFS: ");
|
|
this->insertLink("Click here for the example page", "ipfs://QmcuUv9QXR86vXvSAQPkRJfi5kJwtTtu1NftNaWWVNftSd", defaultFont.to_string());
|
|
}
|
|
|
|
/**
|
|
* Process AST document (markdown format) and draw the text in the GTK TextView
|
|
*/
|
|
void Draw::processDocument(cmark_node *root_node)
|
|
{
|
|
if (get_editable())
|
|
this->disableEdit();
|
|
this->clearOnThread();
|
|
|
|
// Loop over AST nodes
|
|
cmark_event_type ev_type;
|
|
cmark_iter *iter = cmark_iter_new(root_node);
|
|
while ((ev_type = cmark_iter_next(iter)) != CMARK_EVENT_DONE)
|
|
{
|
|
cmark_node *cur = cmark_iter_get_node(iter);
|
|
try
|
|
{
|
|
processNode(cur, ev_type);
|
|
}
|
|
catch (const std::runtime_error &error)
|
|
{
|
|
std::cerr << "Error: Processing node failed, with message: " << error.what() << std::endl;
|
|
// Continue nevertheless
|
|
}
|
|
}
|
|
}
|
|
|
|
void Draw::setViewSourceMenuItem(bool isEnabled)
|
|
{
|
|
this->addViewSourceMenuItem = isEnabled;
|
|
}
|
|
|
|
void Draw::newDocument()
|
|
{
|
|
this->undoPool.clear();
|
|
this->redoPool.clear();
|
|
this->clearText();
|
|
|
|
enableEdit();
|
|
grab_focus(); // Claim focus on text view
|
|
}
|
|
|
|
std::string Draw::getText()
|
|
{
|
|
return get_buffer().get()->get_text();
|
|
}
|
|
|
|
void Draw::clearText()
|
|
{
|
|
auto buffer = get_buffer();
|
|
buffer->erase(buffer->begin(), buffer->end());
|
|
}
|
|
|
|
void Draw::enableEdit()
|
|
{
|
|
set_editable(true);
|
|
set_cursor_visible(true);
|
|
auto buffer = get_buffer();
|
|
this->beginUserActionSignalHandler = buffer->signal_begin_user_action().connect(sigc::mem_fun(this, &Draw::begin_user_action), false);
|
|
this->endUserActionSignalHandler = buffer->signal_end_user_action().connect(sigc::mem_fun(this, &Draw::end_user_action), false);
|
|
this->insertTextSignalHandler = buffer->signal_insert().connect(sigc::mem_fun(this, &Draw::on_insert), false);
|
|
this->deleteTextSignalHandler = buffer->signal_erase().connect(sigc::mem_fun(this, &Draw::on_delete), false);
|
|
}
|
|
|
|
void Draw::disableEdit()
|
|
{
|
|
set_editable(false);
|
|
set_cursor_visible(false);
|
|
// Disconnect signal handles
|
|
this->beginUserActionSignalHandler.disconnect();
|
|
this->endUserActionSignalHandler.disconnect();
|
|
this->insertTextSignalHandler.disconnect();
|
|
this->deleteTextSignalHandler.disconnect();
|
|
}
|
|
|
|
/**
|
|
* Search for links
|
|
*/
|
|
void Draw::followLink(Gtk::TextBuffer::iterator &iter)
|
|
{
|
|
auto tags = iter.get_tags();
|
|
for (auto const &tag : tags)
|
|
{
|
|
char *url = static_cast<char *>(tag->get_data("url"));
|
|
if (url != 0 && (strlen(url) > 0))
|
|
{
|
|
// Get the URL
|
|
mainWindow.doRequest(url, true);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Undo action (Ctrl + Z)
|
|
*/
|
|
void Draw::undo()
|
|
{
|
|
if (get_editable() && (undoPool.size() > 0))
|
|
{
|
|
auto undoAction = undoPool.at(undoPool.size() - 1);
|
|
auto buffer = get_buffer();
|
|
undoPool.pop_back();
|
|
if (undoAction.isInsert)
|
|
{
|
|
Gtk::TextBuffer::iterator startIter = buffer->get_iter_at_offset(undoAction.beginOffset);
|
|
Gtk::TextBuffer::iterator endIter = buffer->get_iter_at_offset(undoAction.endOffset);
|
|
buffer->erase(startIter, endIter);
|
|
buffer->place_cursor(buffer->get_iter_at_offset(undoAction.beginOffset));
|
|
}
|
|
else
|
|
{
|
|
Gtk::TextBuffer::iterator startIter = buffer->get_iter_at_offset(undoAction.beginOffset);
|
|
buffer->insert(startIter, undoAction.text);
|
|
buffer->place_cursor(buffer->get_iter_at_offset(undoAction.endOffset));
|
|
}
|
|
redoPool.push_back(undoAction);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Redo action (Ctrl + Y)
|
|
*/
|
|
void Draw::redo()
|
|
{
|
|
if (get_editable() && (redoPool.size() > 0))
|
|
{
|
|
auto redoAction = redoPool.at(redoPool.size() - 1);
|
|
auto buffer = get_buffer();
|
|
redoPool.pop_back();
|
|
if (redoAction.isInsert)
|
|
{
|
|
Gtk::TextBuffer::iterator startIter = buffer->get_iter_at_offset(redoAction.beginOffset);
|
|
buffer->insert(startIter, redoAction.text);
|
|
buffer->place_cursor(buffer->get_iter_at_offset(redoAction.endOffset));
|
|
}
|
|
else
|
|
{
|
|
Gtk::TextBuffer::iterator startIter = buffer->get_iter_at_offset(redoAction.beginOffset);
|
|
Gtk::TextBuffer::iterator endIter = buffer->get_iter_at_offset(redoAction.endOffset);
|
|
buffer->erase(startIter, endIter);
|
|
buffer->place_cursor(buffer->get_iter_at_offset(redoAction.beginOffset));
|
|
}
|
|
undoPool.push_back(redoAction);
|
|
}
|
|
}
|
|
|
|
void Draw::cut()
|
|
{
|
|
if (get_editable())
|
|
{
|
|
auto clipboard = get_clipboard("CLIPBOARD");
|
|
get_buffer()->cut_clipboard(clipboard);
|
|
}
|
|
else
|
|
{
|
|
auto clipboard = get_clipboard("CLIPBOARD");
|
|
get_buffer()->copy_clipboard(clipboard);
|
|
}
|
|
}
|
|
|
|
void Draw::copy()
|
|
{
|
|
auto clipboard = get_clipboard("CLIPBOARD");
|
|
get_buffer()->copy_clipboard(clipboard);
|
|
}
|
|
|
|
void Draw::paste()
|
|
{
|
|
if (get_editable())
|
|
{
|
|
auto clipboard = get_clipboard("CLIPBOARD");
|
|
get_buffer()->paste_clipboard(clipboard);
|
|
}
|
|
}
|
|
|
|
void Draw::del()
|
|
{
|
|
if (get_editable())
|
|
{
|
|
auto buffer = get_buffer();
|
|
buffer->begin_user_action();
|
|
Gtk::TextBuffer::iterator begin, end;
|
|
if (buffer->get_selection_bounds(begin, end))
|
|
{
|
|
buffer->erase(begin, end);
|
|
}
|
|
else
|
|
{
|
|
++end;
|
|
buffer->erase(begin, end);
|
|
}
|
|
buffer->end_user_action();
|
|
}
|
|
}
|
|
|
|
void Draw::selectAll()
|
|
{
|
|
auto buffer = get_buffer();
|
|
buffer->select_range(buffer->begin(), buffer->end());
|
|
}
|
|
|
|
/*************************************************************
|
|
* Editor signals calls
|
|
*************************************************************/
|
|
|
|
void Draw::make_heading(int headingLevel)
|
|
{
|
|
Gtk::TextBuffer::iterator start, end;
|
|
auto buffer = get_buffer();
|
|
buffer->begin_user_action();
|
|
std::string heading = std::string(headingLevel, '#');
|
|
if (buffer->get_selection_bounds(start, end))
|
|
{
|
|
std::string text = buffer->get_text(start, end);
|
|
buffer->erase_selection();
|
|
buffer->insert_at_cursor(heading + " " + text);
|
|
}
|
|
else
|
|
{
|
|
int insertOffset = buffer->get_insert()->get_iter().get_offset();
|
|
buffer->insert_at_cursor(heading + " \n");
|
|
auto newCursorPos = buffer->get_iter_at_offset(insertOffset + headingLevel + 1);
|
|
buffer->place_cursor(newCursorPos);
|
|
}
|
|
buffer->end_user_action();
|
|
}
|
|
|
|
void Draw::make_bold()
|
|
{
|
|
Gtk::TextBuffer::iterator start, end;
|
|
auto buffer = get_buffer();
|
|
buffer->begin_user_action();
|
|
if (buffer->get_selection_bounds(start, end))
|
|
{
|
|
std::string text = buffer->get_text(start, end);
|
|
buffer->erase_selection();
|
|
buffer->insert_at_cursor("**" + text + "**");
|
|
}
|
|
else
|
|
{
|
|
int insertOffset = buffer->get_insert()->get_iter().get_offset();
|
|
buffer->insert_at_cursor("****");
|
|
auto newCursorPos = buffer->get_iter_at_offset(insertOffset + 2);
|
|
buffer->place_cursor(newCursorPos);
|
|
}
|
|
buffer->end_user_action();
|
|
}
|
|
|
|
void Draw::make_italic()
|
|
{
|
|
Gtk::TextBuffer::iterator start, end;
|
|
auto buffer = get_buffer();
|
|
buffer->begin_user_action();
|
|
if (buffer->get_selection_bounds(start, end))
|
|
{
|
|
std::string text = buffer->get_text(start, end);
|
|
buffer->erase_selection();
|
|
buffer->insert_at_cursor("*" + text + "*");
|
|
}
|
|
else
|
|
{
|
|
int insertOffset = buffer->get_insert()->get_iter().get_offset();
|
|
buffer->insert_at_cursor("**");
|
|
auto newCursorPos = buffer->get_iter_at_offset(insertOffset + 1);
|
|
buffer->place_cursor(newCursorPos);
|
|
}
|
|
buffer->end_user_action();
|
|
}
|
|
|
|
void Draw::make_strikethrough()
|
|
{
|
|
Gtk::TextBuffer::iterator start, end;
|
|
auto buffer = get_buffer();
|
|
buffer->begin_user_action();
|
|
if (buffer->get_selection_bounds(start, end))
|
|
{
|
|
std::string text = buffer->get_text(start, end);
|
|
buffer->erase_selection();
|
|
buffer->insert_at_cursor("~~" + text + "~~");
|
|
}
|
|
else
|
|
{
|
|
int insertOffset = buffer->get_insert()->get_iter().get_offset();
|
|
buffer->insert_at_cursor("~~~~");
|
|
auto newCursorPos = buffer->get_iter_at_offset(insertOffset + 2);
|
|
buffer->place_cursor(newCursorPos);
|
|
}
|
|
buffer->end_user_action();
|
|
}
|
|
|
|
void Draw::make_super()
|
|
{
|
|
Gtk::TextBuffer::iterator start, end;
|
|
auto buffer = get_buffer();
|
|
buffer->begin_user_action();
|
|
if (buffer->get_selection_bounds(start, end))
|
|
{
|
|
std::string text = buffer->get_text(start, end);
|
|
buffer->erase_selection();
|
|
buffer->insert_at_cursor("^" + text + "^");
|
|
}
|
|
else
|
|
{
|
|
int insertOffset = buffer->get_insert()->get_iter().get_offset();
|
|
buffer->insert_at_cursor("^^");
|
|
auto newCursorPos = buffer->get_iter_at_offset(insertOffset + 1);
|
|
buffer->place_cursor(newCursorPos);
|
|
}
|
|
buffer->end_user_action();
|
|
}
|
|
|
|
void Draw::make_sub()
|
|
{
|
|
Gtk::TextBuffer::iterator start, end;
|
|
auto buffer = get_buffer();
|
|
buffer->begin_user_action();
|
|
if (buffer->get_selection_bounds(start, end))
|
|
{
|
|
std::string text = buffer->get_text(start, end);
|
|
buffer->erase_selection();
|
|
buffer->insert_at_cursor("%" + text + "%");
|
|
}
|
|
else
|
|
{
|
|
int insertOffset = buffer->get_insert()->get_iter().get_offset();
|
|
buffer->insert_at_cursor("%%");
|
|
auto newCursorPos = buffer->get_iter_at_offset(insertOffset + 1);
|
|
buffer->place_cursor(newCursorPos);
|
|
}
|
|
buffer->end_user_action();
|
|
}
|
|
|
|
void Draw::make_quote()
|
|
{
|
|
Gtk::TextBuffer::iterator start, end;
|
|
auto buffer = get_buffer();
|
|
buffer->begin_user_action();
|
|
if (buffer->get_selection_bounds(start, end))
|
|
{
|
|
std::string text = buffer->get_text(start, end);
|
|
buffer->erase_selection();
|
|
std::istringstream iss(text);
|
|
std::string line;
|
|
while (std::getline(iss, line))
|
|
{
|
|
buffer->insert_at_cursor("> " + line + "\n");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
buffer->insert_at_cursor("\n> text"); // TODO: only insert new line if there is non before
|
|
}
|
|
buffer->end_user_action();
|
|
}
|
|
|
|
void Draw::insert_link()
|
|
{
|
|
Gtk::TextBuffer::iterator start, end;
|
|
auto buffer = get_buffer();
|
|
buffer->begin_user_action();
|
|
if (buffer->get_selection_bounds(start, end))
|
|
{
|
|
int insertOffset = buffer->get_insert()->get_iter().get_offset();
|
|
std::string text = buffer->get_text(start, end);
|
|
buffer->erase_selection();
|
|
buffer->insert_at_cursor("[" + text + "](ipfs://url)");
|
|
auto beginCursorPos = buffer->get_iter_at_offset(insertOffset + text.length() + 10);
|
|
auto endCursorPos = buffer->get_iter_at_offset(insertOffset + text.length() + 13);
|
|
buffer->select_range(beginCursorPos, endCursorPos);
|
|
}
|
|
else
|
|
{
|
|
int insertOffset = buffer->get_insert()->get_iter().get_offset();
|
|
buffer->insert_at_cursor("[link](ipfs://url)");
|
|
auto beginCursorPos = buffer->get_iter_at_offset(insertOffset + 14);
|
|
auto endCursorPos = buffer->get_iter_at_offset(insertOffset + 17);
|
|
buffer->select_range(beginCursorPos, endCursorPos);
|
|
}
|
|
buffer->end_user_action();
|
|
}
|
|
|
|
void Draw::insert_image()
|
|
{
|
|
Gtk::TextBuffer::iterator start, end;
|
|
auto buffer = get_buffer();
|
|
buffer->begin_user_action();
|
|
if (buffer->get_selection_bounds(start, end))
|
|
{
|
|
std::string text = buffer->get_text(start, end);
|
|
buffer->erase_selection();
|
|
buffer->insert_at_cursor("![](" + text + "]");
|
|
}
|
|
else
|
|
{
|
|
int insertOffset = buffer->get_insert()->get_iter().get_offset();
|
|
buffer->insert_at_cursor("![](ipfs://image.jpg)");
|
|
auto beginCursorPos = buffer->get_iter_at_offset(insertOffset + 11);
|
|
auto endCursorPos = buffer->get_iter_at_offset(insertOffset + 20);
|
|
buffer->select_range(beginCursorPos, endCursorPos);
|
|
}
|
|
buffer->end_user_action();
|
|
}
|
|
|
|
void Draw::make_code()
|
|
{
|
|
Gtk::TextBuffer::iterator start, end;
|
|
auto buffer = get_buffer();
|
|
buffer->begin_user_action();
|
|
if (buffer->get_selection_bounds(start, end))
|
|
{
|
|
std::string text = buffer->get_text(start, end);
|
|
buffer->erase_selection();
|
|
// Strip begin & end line breaks
|
|
if (text.starts_with('\n'))
|
|
{
|
|
text.erase(0, 1);
|
|
}
|
|
if (text.ends_with('\n'))
|
|
{
|
|
text.erase(text.size() - 1);
|
|
}
|
|
if (text.find('\n') != std::string::npos)
|
|
{
|
|
// Insert code block
|
|
buffer->insert_at_cursor("```\n" + text + "\n```\n");
|
|
}
|
|
else
|
|
{
|
|
// Insert inline code
|
|
buffer->insert_at_cursor("`" + text + "`");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
int insertOffset = buffer->get_insert()->get_iter().get_offset();
|
|
buffer->insert_at_cursor("``");
|
|
auto newCursorPos = buffer->get_iter_at_offset(insertOffset + 1);
|
|
buffer->place_cursor(newCursorPos);
|
|
}
|
|
buffer->end_user_action();
|
|
}
|
|
|
|
void Draw::insert_bullet_list()
|
|
{
|
|
Gtk::TextBuffer::iterator start, end;
|
|
auto buffer = get_buffer();
|
|
buffer->begin_user_action();
|
|
if (buffer->get_selection_bounds(start, end))
|
|
{
|
|
std::string text = buffer->get_text(start, end);
|
|
buffer->erase_selection();
|
|
std::istringstream iss(text);
|
|
std::string line;
|
|
while (std::getline(iss, line))
|
|
{
|
|
buffer->insert_at_cursor("* " + line + "\n");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
buffer->insert_at_cursor("\n* ");
|
|
}
|
|
buffer->end_user_action();
|
|
}
|
|
|
|
void Draw::insert_numbered_list()
|
|
{
|
|
Gtk::TextBuffer::iterator start, end;
|
|
auto buffer = get_buffer();
|
|
buffer->begin_user_action();
|
|
if (buffer->get_selection_bounds(start, end))
|
|
{
|
|
std::string text = buffer->get_text(start, end);
|
|
buffer->erase_selection();
|
|
std::istringstream iss(text);
|
|
std::string line;
|
|
int counter = 1;
|
|
while (std::getline(iss, line))
|
|
{
|
|
buffer->insert_at_cursor(std::to_string(counter) + ". " + line + "\n");
|
|
counter++;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
buffer->insert_at_cursor("\n1. ");
|
|
}
|
|
buffer->end_user_action();
|
|
}
|
|
|
|
void Draw::make_highlight()
|
|
{
|
|
Gtk::TextBuffer::iterator start, end;
|
|
auto buffer = get_buffer();
|
|
buffer->begin_user_action();
|
|
if (buffer->get_selection_bounds(start, end))
|
|
{
|
|
std::string text = buffer->get_text(start, end);
|
|
buffer->erase_selection();
|
|
buffer->insert_at_cursor("==" + text + "==");
|
|
}
|
|
else
|
|
{
|
|
int insertOffset = buffer->get_insert()->get_iter().get_offset();
|
|
buffer->insert_at_cursor("====");
|
|
auto newCursorPos = buffer->get_iter_at_offset(insertOffset + 2);
|
|
buffer->place_cursor(newCursorPos);
|
|
}
|
|
buffer->end_user_action();
|
|
}
|
|
|
|
void Draw::begin_user_action()
|
|
{
|
|
this->isUserAction = true;
|
|
}
|
|
|
|
void Draw::end_user_action()
|
|
{
|
|
this->isUserAction = false;
|
|
}
|
|
|
|
/**
|
|
* Triggered when text gets inserted
|
|
*/
|
|
void Draw::on_insert(const Gtk::TextBuffer::iterator &pos, const Glib::ustring &text, int bytes __attribute__((unused)))
|
|
{
|
|
if (this->isUserAction)
|
|
{
|
|
UndoRedoData undoData = {};
|
|
undoData.isInsert = true;
|
|
undoData.beginOffset = pos.get_offset();
|
|
undoData.endOffset = pos.get_offset() + text.size();
|
|
undoData.text = text;
|
|
this->undoPool.push_back(undoData);
|
|
this->redoPool.clear();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Triggered when text gets deleted/removed
|
|
*/
|
|
void Draw::on_delete(const Gtk::TextBuffer::iterator &range_start, const Gtk::TextBuffer::iterator &range_end)
|
|
{
|
|
if (this->isUserAction)
|
|
{
|
|
auto text = get_buffer()->get_text(range_start, range_end);
|
|
UndoRedoData undoData = {};
|
|
undoData.isInsert = false;
|
|
undoData.beginOffset = range_start.get_offset();
|
|
undoData.endOffset = range_end.get_offset();
|
|
undoData.text = text;
|
|
this->undoPool.push_back(undoData);
|
|
}
|
|
}
|
|
|
|
/************************************************
|
|
* Private methods
|
|
************************************************/
|
|
|
|
/**
|
|
* Process and parse each node in the AST
|
|
*/
|
|
void Draw::processNode(cmark_node *node, cmark_event_type ev_type)
|
|
{
|
|
bool entering = (ev_type == CMARK_EVENT_ENTER);
|
|
|
|
// Take care of the markdown extensions
|
|
if (node->extension)
|
|
{
|
|
if (strcmp(node->extension->name, "strikethrough") == 0)
|
|
{
|
|
isStrikethrough = entering;
|
|
return;
|
|
}
|
|
else if (strcmp(node->extension->name, "highlight") == 0)
|
|
{
|
|
isHighlight = entering;
|
|
return;
|
|
}
|
|
else if (strcmp(node->extension->name, "superscript") == 0)
|
|
{
|
|
isSuperscript = entering;
|
|
return;
|
|
}
|
|
else if (strcmp(node->extension->name, "subscript") == 0)
|
|
{
|
|
isSubscript = entering;
|
|
return;
|
|
}
|
|
}
|
|
|
|
switch (node->type)
|
|
{
|
|
case CMARK_NODE_DOCUMENT:
|
|
if (entering)
|
|
{
|
|
// Reset all (better safe than sorry)
|
|
headingLevel = 0;
|
|
bulletListLevel = 0;
|
|
orderedListLevel = 0;
|
|
listLevel = 0;
|
|
isOrderedList = false;
|
|
isBold = false;
|
|
isItalic = false;
|
|
isStrikethrough = false;
|
|
isHighlight = false;
|
|
isSuperscript = false;
|
|
isSubscript = false;
|
|
isQuote = false;
|
|
}
|
|
break;
|
|
|
|
case CMARK_NODE_BLOCK_QUOTE:
|
|
isQuote = entering;
|
|
if (!entering)
|
|
{
|
|
// Replace last quote '|'-sign with a normal blank line
|
|
this->truncateText(2);
|
|
this->insertMarkupTextOnThread("\n");
|
|
}
|
|
break;
|
|
|
|
case CMARK_NODE_LIST:
|
|
{
|
|
cmark_list_type listType = node->as.list.list_type;
|
|
|
|
if (entering)
|
|
{
|
|
listLevel++;
|
|
}
|
|
else
|
|
{
|
|
listLevel--;
|
|
}
|
|
if (listLevel == 0)
|
|
{
|
|
// Reset bullet/ordered levels
|
|
bulletListLevel = 0;
|
|
orderedListLevel = 0;
|
|
isOrderedList = false;
|
|
if (!entering)
|
|
this->insertMarkupTextOnThread("\n");
|
|
}
|
|
else if (listLevel > 0)
|
|
{
|
|
if (entering)
|
|
{
|
|
if (listType == cmark_list_type::CMARK_BULLET_LIST)
|
|
{
|
|
bulletListLevel++;
|
|
}
|
|
else if (listType == cmark_list_type::CMARK_ORDERED_LIST)
|
|
{
|
|
orderedListLevel++;
|
|
// Create the counter (and reset to zero)
|
|
orderedListCounters[orderedListLevel] = 0;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Un-indent list level again
|
|
if (listType == cmark_list_type::CMARK_BULLET_LIST)
|
|
{
|
|
bulletListLevel--;
|
|
}
|
|
else if (listType == cmark_list_type::CMARK_ORDERED_LIST)
|
|
{
|
|
orderedListLevel--;
|
|
}
|
|
}
|
|
|
|
isOrderedList = (orderedListLevel > 0) && (bulletListLevel <= 0);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case CMARK_NODE_ITEM:
|
|
if (entering)
|
|
{
|
|
if (isOrderedList)
|
|
{
|
|
// Increasement ordered list counter
|
|
orderedListCounters[orderedListLevel]++;
|
|
}
|
|
|
|
// Insert tabs & bullet/number
|
|
if (bulletListLevel > 0)
|
|
{
|
|
if (bulletListLevel % 2 == 0)
|
|
{
|
|
this->insertText(std::string(bulletListLevel, '\u0009') + "\u25e6 ");
|
|
}
|
|
else
|
|
{
|
|
this->insertText(std::string(bulletListLevel, '\u0009') + "\u2022 ");
|
|
}
|
|
}
|
|
else if (orderedListLevel > 0)
|
|
{
|
|
std::string number;
|
|
if (orderedListLevel % 2 == 0)
|
|
{
|
|
number = Draw::intToRoman(orderedListCounters[orderedListLevel]) + " ";
|
|
}
|
|
else
|
|
{
|
|
number = std::to_string(orderedListCounters[orderedListLevel]) + ". ";
|
|
}
|
|
this->insertText(std::string(orderedListLevel, '\u0009') + number);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case CMARK_NODE_HEADING:
|
|
if (entering)
|
|
{
|
|
headingLevel = node->as.heading.level;
|
|
}
|
|
else
|
|
{
|
|
// Insert line break after heading
|
|
this->insertMarkupTextOnThread("\n\n");
|
|
headingLevel = 0; // reset
|
|
}
|
|
break;
|
|
|
|
case CMARK_NODE_CODE_BLOCK:
|
|
{
|
|
std::string code = cmark_node_get_literal(node);
|
|
std::string newline = (isQuote) ? "" : "\n";
|
|
this->insertText(code + newline, "", CodeTypeEnum::CODE_BLOCK);
|
|
}
|
|
break;
|
|
|
|
case CMARK_NODE_HTML_BLOCK:
|
|
break;
|
|
|
|
case CMARK_NODE_CUSTOM_BLOCK:
|
|
break;
|
|
|
|
case CMARK_NODE_THEMATIC_BREAK:
|
|
{
|
|
this->isBold = true;
|
|
this->insertText("\u2015\u2015\u2015\u2015\u2015\u2015\u2015\u2015\u2015\u2015\u2015\u2015\u2015\u2015\u2015\u2015\u2015\u2015\u2015\u2015\u2015\u2015\u2015\u2015\n\n");
|
|
this->isBold = false;
|
|
}
|
|
break;
|
|
|
|
case CMARK_NODE_PARAGRAPH:
|
|
// For listings only insert a single new line
|
|
if (!entering && (listLevel > 0))
|
|
{
|
|
this->insertMarkupTextOnThread("\n");
|
|
}
|
|
// Dealing with paragraphs in quotes
|
|
else if (entering && isQuote)
|
|
{
|
|
this->insertText("\uFF5C ");
|
|
}
|
|
else if (!entering && isQuote)
|
|
{
|
|
this->insertText("\n\uFF5C\n");
|
|
}
|
|
// Normal paragraph, just blank line
|
|
else if (!entering)
|
|
{
|
|
this->insertMarkupTextOnThread("\n\n");
|
|
}
|
|
break;
|
|
|
|
case CMARK_NODE_TEXT:
|
|
{
|
|
std::string text = cmark_node_get_literal(node);
|
|
// URL
|
|
if (isLink)
|
|
{
|
|
this->insertText(text, linkURL);
|
|
linkURL = "";
|
|
}
|
|
// Text (with optional inline formatting)
|
|
else
|
|
{
|
|
this->insertText(text);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case CMARK_NODE_LINEBREAK:
|
|
// Hard brake
|
|
this->insertMarkupTextOnThread("\n");
|
|
break;
|
|
|
|
case CMARK_NODE_SOFTBREAK:
|
|
// only insert space
|
|
this->insertMarkupTextOnThread(" ");
|
|
break;
|
|
|
|
case CMARK_NODE_CODE:
|
|
{
|
|
std::string code = cmark_node_get_literal(node);
|
|
this->insertText(code, "", CodeTypeEnum::INLINE_CODE);
|
|
}
|
|
break;
|
|
|
|
case CMARK_NODE_HTML_INLINE:
|
|
break;
|
|
|
|
case CMARK_NODE_CUSTOM_INLINE:
|
|
break;
|
|
|
|
case CMARK_NODE_STRONG:
|
|
isBold = entering;
|
|
break;
|
|
|
|
case CMARK_NODE_EMPH:
|
|
isItalic = entering;
|
|
break;
|
|
|
|
case CMARK_NODE_LINK:
|
|
isLink = entering;
|
|
if (entering)
|
|
{
|
|
linkURL = cmark_node_get_url(node);
|
|
}
|
|
break;
|
|
|
|
case CMARK_NODE_IMAGE:
|
|
break;
|
|
|
|
case CMARK_NODE_FOOTNOTE_REFERENCE:
|
|
break;
|
|
|
|
case CMARK_NODE_FOOTNOTE_DEFINITION:
|
|
break;
|
|
default:
|
|
throw std::runtime_error("Node type '" + std::string(cmark_node_get_type_string(node)) + "' not found.");
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Insert markup text - thread safe
|
|
*/
|
|
void Draw::insertText(std::string text, const std::string &url, CodeTypeEnum codeType)
|
|
{
|
|
auto font = defaultFont;
|
|
std::string span;
|
|
span.reserve(80);
|
|
std::string foreground;
|
|
std::string background;
|
|
|
|
// Use by reference to replace the string
|
|
this->encodeText(text);
|
|
|
|
if (isStrikethrough)
|
|
{
|
|
span.append("strikethrough=\"true\" ");
|
|
}
|
|
if (isSuperscript)
|
|
{
|
|
font.set_size(8000);
|
|
span.append("rise=\"6000\" ");
|
|
}
|
|
// You can not have superscript & subscript applied together
|
|
else if (isSubscript)
|
|
{
|
|
font.set_size(8000);
|
|
span.append("rise=\"-6000\" ");
|
|
}
|
|
if (isBold)
|
|
{
|
|
font.set_weight(Pango::WEIGHT_BOLD);
|
|
}
|
|
if (isItalic)
|
|
{
|
|
font.set_style(Pango::STYLE_ITALIC);
|
|
}
|
|
if (isHighlight)
|
|
{
|
|
foreground = "black";
|
|
background = "#FFFF00";
|
|
}
|
|
if (codeType != Draw::CodeTypeEnum::NONE)
|
|
{
|
|
foreground = "#323232";
|
|
background = "#e0e0e0";
|
|
}
|
|
if (headingLevel > 0)
|
|
{
|
|
font.set_weight(Pango::WEIGHT_BOLD);
|
|
switch (headingLevel)
|
|
{
|
|
case 1:
|
|
font.set_size(fontSize * PANGO_SCALE_XXX_LARGE);
|
|
break;
|
|
case 2:
|
|
font.set_size(fontSize * PANGO_SCALE_XX_LARGE);
|
|
break;
|
|
case 3:
|
|
font.set_size(fontSize * PANGO_SCALE_X_LARGE);
|
|
break;
|
|
case 4:
|
|
font.set_size(fontSize * PANGO_SCALE_LARGE);
|
|
break;
|
|
case 5:
|
|
font.set_size(fontSize * PANGO_SCALE_MEDIUM);
|
|
break;
|
|
case 6:
|
|
font.set_size(fontSize * PANGO_SCALE_MEDIUM);
|
|
foreground = "gray";
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
if (isQuote)
|
|
{
|
|
foreground = "blue";
|
|
}
|
|
if (!foreground.empty())
|
|
{
|
|
span.append("foreground=\"" + foreground + "\" ");
|
|
}
|
|
if (!background.empty())
|
|
{
|
|
span.append("background=\"" + background + "\" ");
|
|
}
|
|
span.append("font_desc=\"" + font.to_string() + "\"");
|
|
|
|
// Insert URL
|
|
if (!url.empty())
|
|
{
|
|
this->insertLink(text, url, font.to_string());
|
|
}
|
|
// Insert text/heading
|
|
else
|
|
{
|
|
// Special case for code blocks within quote
|
|
if ((codeType == Draw::CodeTypeEnum::CODE_BLOCK) && isQuote)
|
|
{
|
|
std::istringstream iss(text);
|
|
std::string line;
|
|
// Add a quote for each new code line
|
|
while (getline(iss, line))
|
|
{
|
|
insertMarkupTextOnThread("<span font_desc=\"" + defaultFont.to_string() + "\" foreground=\"blue\">\uFF5C </span><span " + span + ">" + line + "</span>\n");
|
|
}
|
|
insertMarkupTextOnThread("<span font_desc=\"" + defaultFont.to_string() + "\" foreground=\"blue\">\uFF5C\n</span>");
|
|
}
|
|
// Special case for heading within quote
|
|
else if ((headingLevel > 0) && isQuote)
|
|
{
|
|
insertMarkupTextOnThread("<span font_desc=\"" + defaultFont.to_string() + "\" foreground=\"blue\">\uFF5C </span><span " + span + ">" + text + "</span><span font_desc=\"" + defaultFont.to_string() + "\" foreground=\"blue\">\n\uFF5C\n</span>");
|
|
}
|
|
// Just insert text/heading the normal way
|
|
else
|
|
{
|
|
insertMarkupTextOnThread("<span " + span + ">" + text + "</span>");
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Insert url link - thread safe
|
|
*/
|
|
void Draw::insertLink(const std::string &text, const std::string &url, const std::string &urlFont)
|
|
{
|
|
DispatchData *data = g_new0(struct DispatchData, 1);
|
|
data->buffer = buffer;
|
|
data->text = text;
|
|
data->url = url;
|
|
data->urlFont = urlFont;
|
|
gdk_threads_add_idle((GSourceFunc)insertLinkIdle, data);
|
|
}
|
|
|
|
/**
|
|
* Remove nr. chars from the end of the text buffer - thread safe
|
|
*/
|
|
void Draw::truncateText(int charsTruncated)
|
|
{
|
|
DispatchData *data = g_new0(struct DispatchData, 1);
|
|
data->buffer = buffer;
|
|
data->charsTruncated = charsTruncated;
|
|
gdk_threads_add_idle((GSourceFunc)truncateTextIdle, data);
|
|
}
|
|
|
|
/**
|
|
* Encode text string (eg. ampersand-character)
|
|
* @param[in/out] string
|
|
*/
|
|
void Draw::encodeText(std::string &string)
|
|
{
|
|
std::string buffer;
|
|
buffer.reserve(string.size() + 5);
|
|
for (size_t pos = 0; pos != string.size(); ++pos)
|
|
{
|
|
switch (string[pos])
|
|
{
|
|
case '&':
|
|
buffer.append("&");
|
|
break;
|
|
default:
|
|
buffer.append(&string[pos], 1);
|
|
break;
|
|
}
|
|
}
|
|
string.swap(buffer);
|
|
}
|
|
|
|
/******************************************************
|
|
* Helper functions below
|
|
*****************************************************/
|
|
|
|
/**
|
|
* Insert markup pango text - thread safe
|
|
*/
|
|
void Draw::insertMarkupTextOnThread(const std::string &text)
|
|
{
|
|
DispatchData *data = g_new0(struct DispatchData, 1);
|
|
data->buffer = buffer;
|
|
data->text = text;
|
|
gdk_threads_add_idle((GSourceFunc)insertTextIdle, data);
|
|
}
|
|
|
|
/**
|
|
* Clear buffer - thread-safe
|
|
*/
|
|
void Draw::clearOnThread()
|
|
{
|
|
gdk_threads_add_idle((GSourceFunc)clearBufferIdle, buffer);
|
|
}
|
|
|
|
/* Looks at all tags covering the position (x, y) in the text view,
|
|
* and if one of them is a link, change the cursor to the "hands" cursor
|
|
* typically used by web browsers.
|
|
*/
|
|
void Draw::changeCursor(int x, int y)
|
|
{
|
|
Gtk::TextBuffer::iterator iter;
|
|
bool hovering = false;
|
|
|
|
get_iter_at_location(iter, x, y);
|
|
auto tags = iter.get_tags();
|
|
for (auto &tag : tags)
|
|
{
|
|
char *url = static_cast<char *>(tag->get_data("url"));
|
|
if (url != 0 && (strlen(url) > 0))
|
|
{
|
|
// Link
|
|
hovering = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (hovering != hovingOverLink)
|
|
{
|
|
hovingOverLink = hovering;
|
|
auto window = get_window(Gtk::TextWindowType::TEXT_WINDOW_TEXT);
|
|
if (hovingOverLink)
|
|
window->set_cursor(linkCursor);
|
|
else
|
|
window->set_cursor(normalCursor);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Insert text on Idle Call function
|
|
*/
|
|
gboolean Draw::insertTextIdle(struct DispatchData *data)
|
|
{
|
|
GtkTextIter end_iter;
|
|
gtk_text_buffer_get_end_iter(data->buffer, &end_iter);
|
|
gtk_text_buffer_insert_markup(data->buffer, &end_iter, data->text.c_str(), -1);
|
|
g_free(data);
|
|
return FALSE;
|
|
}
|
|
|
|
/**
|
|
* Insert link url on Idle Call function
|
|
*/
|
|
gboolean Draw::insertLinkIdle(struct DispatchData *data)
|
|
{
|
|
GtkTextIter end_iter;
|
|
GtkTextTag *tag;
|
|
gtk_text_buffer_get_end_iter(data->buffer, &end_iter);
|
|
tag = gtk_text_buffer_create_tag(data->buffer, NULL,
|
|
"font", data->urlFont.c_str(),
|
|
"foreground", "#569cd6",
|
|
"underline", PANGO_UNDERLINE_SINGLE,
|
|
NULL);
|
|
g_object_set_data(G_OBJECT(tag), "url", g_strdup(data->url.c_str()));
|
|
gtk_text_buffer_insert_with_tags(data->buffer, &end_iter, data->text.c_str(), -1, tag, NULL);
|
|
g_free(data);
|
|
return FALSE;
|
|
}
|
|
|
|
/**
|
|
* Truncate text from the end of the buffer
|
|
*/
|
|
gboolean Draw::truncateTextIdle(struct DispatchData *data)
|
|
{
|
|
GtkTextIter end_iter;
|
|
gtk_text_buffer_get_end_iter(data->buffer, &end_iter);
|
|
GtkTextIter begin_iter = end_iter;
|
|
gtk_text_iter_backward_chars(&begin_iter, data->charsTruncated);
|
|
gtk_text_buffer_delete(data->buffer, &begin_iter, &end_iter);
|
|
g_free(data);
|
|
return FALSE;
|
|
}
|
|
|
|
/**
|
|
* clearOnThread Text on Idle Call function
|
|
*/
|
|
gboolean Draw::clearBufferIdle(GtkTextBuffer *textBuffer)
|
|
{
|
|
GtkTextIter start_iter, end_iter;
|
|
gtk_text_buffer_get_start_iter(textBuffer, &start_iter);
|
|
gtk_text_buffer_get_end_iter(textBuffer, &end_iter);
|
|
gtk_text_buffer_delete(textBuffer, &start_iter, &end_iter);
|
|
return FALSE;
|
|
}
|
|
|
|
/**
|
|
* Convert number to roman numerals
|
|
*/
|
|
std::string const Draw::intToRoman(int num)
|
|
{
|
|
static const int values[] = {1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1};
|
|
static const std::string numerals[] = {"M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"};
|
|
std::string res;
|
|
for (int i = 0; i < 13; ++i)
|
|
{
|
|
while (num >= values[i])
|
|
{
|
|
num -= values[i];
|
|
res += numerals[i];
|
|
}
|
|
}
|
|
return res;
|
|
}
|