1 #include "notebook.h"
2
3 #include "notebook_clipboard.hpp"
4 #include "notebook_highlight.hpp"
5 #include "imagewidgets.h"
6 #include "notebook_widgets.hpp"
7
8 #include <unordered_set>
9
10 // #define _DEBUG_MOTION_
11
12 #ifdef _DEBUG_MOTION_
13 #define DMPRINTF(...) printf(__VA_ARGS__);
14 #else
15 #define DMPRINTF(...)
16 #endif
17
18 /* list of Gsv syntax highlighting classes that can be rendered as widgets */
19 std::unordered_map<std::string, std::function<Gtk::Widget *(CNotebook*, Gtk::TextBuffer::iterator, Gtk::TextBuffer::iterator)> >
20 CNotebook::widget_classes {
21 { "checkbox", widgets::RenderCheckbox },
22 { "image", widgets::RenderImage },
23 #if defined (HAVE_LASEM) || defined (HAVE_CLATEXMATH)
24 { "latex", widgets::RenderLatex },
25 #endif
26 };
27
CNotebook()28 CNotebook::CNotebook()
29 {
30 last_device=NULL;
31 }
32
33 /* DIRTY HACK: If the layout changes a lot, sometimes GtkTextView will
34 * get events without it being valid (event race?). This can lead
35 * to crashes. Therefore, we'll manually check for layout validity
36 * at the beginning of events. */
37 extern "C" bool gtk_text_layout_is_valid(void*);
38
on_event(GdkEvent *)39 bool CNotebook::on_event(GdkEvent*)
40 {
41 if(!gtk_text_layout_is_valid(*(void**)GTK_TEXT_VIEW(gobj())->priv)) {
42 //printf("event blocked\n");
43 return true;
44 }
45 return false;
46 }
47
48 /* Initialise notebook widget, loading style files etc. from data_path. */
Init(std::string data_path,bool use_highlight_proxy)49 void CNotebook::Init(std::string data_path, bool use_highlight_proxy)
50 {
51 sbuffer = get_source_buffer();
52
53 /* load syntax highlighting scheme */
54 Glib::RefPtr<Gsv::StyleSchemeManager> styleman = Gsv::StyleSchemeManager::create();
55 styleman->set_search_path({data_path+"/sourceview/"});
56 Glib::RefPtr<Gsv::StyleScheme> the_scheme;
57 the_scheme = styleman->get_scheme("notekit");
58 sbuffer->set_style_scheme(the_scheme);
59
60 langman = Gsv::LanguageManager::create();
61
62 /* make our custom markdown definition override the system gtksourceview defaults,
63 * but retain access to them for other language defs */
64 std::vector<std::string> paths = langman->get_search_path();
65 paths.insert(paths.begin(),data_path+"/sourceview/");
66 if(use_highlight_proxy) paths.insert(paths.begin(),GetHighlightProxyDir());
67 langman->set_search_path(paths);
68
69 sbuffer->set_language(langman->get_language("markdown"));
70
71 /* register our serialisation formats */
72 sbuffer->register_serialize_format("text/notekit-markdown", sigc::bind( sigc::mem_fun(this,&CNotebook::on_serialize), false ));
73 sbuffer->register_deserialize_format("text/notekit-markdown",sigc::mem_fun(this,&CNotebook::on_deserialize));
74
75 sbuffer->register_serialize_format("text/plain", sigc::bind( sigc::mem_fun(this,&CNotebook::on_serialize), true ));
76
77 Glib::RefPtr<Gtk::TargetList> l = sbuffer->get_copy_target_list();
78
79 /* create drawing overlay */
80 add_child_in_window(overlay,Gtk::TEXT_WINDOW_WIDGET,0,0);
81
82 /* define actions that the toolbar will hook up to */
83 actions = Gio::SimpleActionGroup::create();
84 #define ACTION(name,param1,param2) actions->add_action(name, sigc::bind( sigc::mem_fun(this,&CNotebook::on_action), std::string(name), param1, param2 ) )
85 ACTION("cmode-text",NB_ACTION_CMODE,NB_MODE_TEXT);
86 ACTION("cmode-draw",NB_ACTION_CMODE,NB_MODE_DRAW);
87 ACTION("cmode-erase",NB_ACTION_CMODE,NB_MODE_ERASE);
88 ACTION("cmode-select",NB_ACTION_CMODE,NB_MODE_SELECT);
89 ACTION("stroke1",NB_ACTION_STROKE,1);
90 ACTION("stroke2",NB_ACTION_STROKE,2);
91 ACTION("stroke3",NB_ACTION_STROKE,3);
92
93 /* connect signals */
94 signal_size_allocate().connect(sigc::mem_fun(this,&CNotebook::on_allocate));
95 overlay.signal_draw().connect(sigc::mem_fun(this,&CNotebook::on_redraw_overlay));
96 signal_button_press_event().connect(sigc::mem_fun(this,&CNotebook::on_button_press),false);
97 signal_button_release_event().connect(sigc::mem_fun(this,&CNotebook::on_button_release),false);
98 signal_motion_notify_event().connect(sigc::mem_fun(this,&CNotebook::on_motion_notify),false);
99 sbuffer->signal_insert().connect(sigc::mem_fun(this,&CNotebook::on_insert),true);
100 sbuffer->signal_changed().connect(sigc::mem_fun(this,&CNotebook::on_changed),true);
101 sbuffer->signal_highlight_updated().connect(sigc::mem_fun(this,&CNotebook::on_highlight_updated),true);
102 sbuffer->property_cursor_position().signal_changed().connect(sigc::mem_fun(this,&CNotebook::on_move_cursor));
103
104 /* overwrite clipboard signals with our custom impls */
105 GtkTextViewClass *k = GTK_TEXT_VIEW_GET_CLASS(gobj());
106 k->copy_clipboard = notebook_copy_clipboard;
107 k->cut_clipboard = notebook_cut_clipboard;
108 k->paste_clipboard = notebook_paste_clipboard;
109 /* save a pointer to the C++ object so we can call back from the clipboard functions */
110 g_object_set_data((GObject*)gobj(),"cppobj",this);
111 /* DIRTY HACK: GtkSourceView's cursor movement methods loop forever in the presence of invisible text sometimes */
112 GtkWidget *text_view = gtk_text_view_new();
113 GtkTextViewClass *plain = GTK_TEXT_VIEW_GET_CLASS(text_view);
114 k->extend_selection = plain->extend_selection;
115 k->move_cursor = plain->move_cursor;
116 gtk_widget_destroy(text_view);
117
118
119 //printf("%08lX %08lX",k->extend_selection,GTK_TEXT_VIEW_CLASS(&k->parent_class)->extend_selection);
120
121 //k->extend_selection = GTK_TEXT_VIEW_CLASS(&k->parent_class)->extend_selection;
122 //g_signal_connect(gobj(),"copy-clipboard",G_CALLBACK(notebook_copy_clipboard),NULL);
123
124 active_state=NB_STATE_NOTHING;
125 update_cursor=false;
126 attention_ewma=0.0;
127
128 /* load cursor. Why don't we actually do all the cursor handling here? */
129 pointer_cursor = Gdk::Cursor::create(Gdk::Display::get_default(),"default");
130
131 /* create tags for style aspects that the syntax highlighter doesn't handle */
132 tag_extra_space = sbuffer->create_tag();
133 tag_extra_space->property_pixels_below_lines().set_value(8);
134 tag_extra_space->property_pixels_above_lines().set_value(8);
135
136 tag_blockquote = sbuffer->create_tag();
137 tag_blockquote->property_left_margin().set_value(16);
138 tag_blockquote->property_indent().set_value(-16);
139 tag_blockquote->property_accumulative_margin().set_value(true);
140 Gdk::RGBA fg = get_style_context()->get_color();
141 fg.set_alpha(0.75);
142 tag_blockquote->property_foreground_rgba().set_value(fg);
143
144 tag_invisible = sbuffer->create_tag();
145 tag_invisible->property_foreground_rgba().set_value(Gdk::RGBA("rgba(0,0,0,0)"));
146
147 tag_hidden = sbuffer->create_tag();
148 tag_hidden->property_invisible().set_value(true);
149
150 tag_mono = sbuffer->create_tag();
151 tag_mono->property_family().set_value("monospace");
152 tag_mono->property_scale().set_value(0.8);
153
154 tag_is_anchor=sbuffer->create_tag();
155
156 /* remember position for proximity widgets */
157 last_position=sbuffer->create_mark("last_position", sbuffer->begin(),true);
158 tag_proximity=sbuffer->create_tag();
159
160 //use for debug
161 //tag_proximity->property_background_rgba().set_value(Gdk::RGBA("rgb(255,128,128)"));
162
163 set_wrap_mode(Gtk::WRAP_WORD_CHAR);
164 }
165
DisableProximityRendering()166 void CNotebook::DisableProximityRendering()
167 {
168 sbuffer->set_language(langman->get_language("markdown-basic"));
169 Gtk::TextIter begin = sbuffer->begin(), end = sbuffer->end();
170 CleanUpSpan(begin,end);
171 }
172
EnableProximityRendering()173 void CNotebook::EnableProximityRendering()
174 {
175 sbuffer->set_language(langman->get_language("markdown"));
176 }
177
SetCursor(Glib::RefPtr<Gdk::Cursor> c)178 void CNotebook::SetCursor(Glib::RefPtr<Gdk::Cursor> c)
179 {
180 if(auto w=get_window(Gtk::TEXT_WINDOW_TEXT)) w->set_cursor(c);
181 }
182
on_action(std::string name,int type,int param)183 void CNotebook::on_action(std::string name,int type,int param)
184 {
185 switch(type) {
186 case NB_ACTION_CMODE:
187 devicemodes[last_device]=param;
188 update_cursor=true;
189 break;
190 case NB_ACTION_STROKE:
191 stroke_width=param;
192 break;
193 }
194 }
195
on_allocate(Gtk::Allocation & a)196 void CNotebook::on_allocate(Gtk::Allocation &a)
197 {
198 //printf("alloc %d %d\n",a.get_width(),a.get_height());
199 overlay.size_allocate(a);
200 if(overlay.get_window()) {
201 /* if we don't do this, some mystery subset of signals gets eaten */
202 overlay.get_window()->set_pass_through(true);
203
204 // size changed; need to recreate Cairo surface
205 Cairo::RefPtr<Cairo::Surface> newptr = overlay.get_window()->create_similar_surface(Cairo::CONTENT_COLOR_ALPHA,a.get_width(),a.get_height());
206 overlay_ctx = Cairo::Context::create(newptr);
207 // copy old surface
208 if(overlay_image) {
209 overlay_ctx->save();
210 overlay_ctx->set_source(overlay_image,0,0);
211 overlay_ctx->rectangle(0,0,a.get_width(),a.get_height());
212 overlay_ctx->set_operator(Cairo::OPERATOR_SOURCE);
213 overlay_ctx->fill();
214 overlay_ctx->restore();
215 }
216 // replace surface
217 overlay_image = newptr;
218 }
219 }
220
Render(const Cairo::RefPtr<Cairo::Context> & ctx,float basex,float basey,int start_index)221 void CStroke::Render(const Cairo::RefPtr<Cairo::Context> &ctx, float basex, float basey, int start_index)
222 {
223 ctx->translate(-basex,-basey);
224 ctx->set_source_rgba(r,g,b,a);
225 ctx->set_line_cap(Cairo::LINE_CAP_ROUND);
226 for(unsigned int i=start_index;i<xcoords.size();++i) {
227 ctx->move_to(xcoords[i-1],ycoords[i-1]);
228 ctx->line_to(xcoords[i],ycoords[i]);
229 ctx->set_line_width(pcoords[i-1]);
230 ctx->stroke();
231 }
232 ctx->translate(basex,basey);
233 }
234
RenderSelectionGlow(const Cairo::RefPtr<Cairo::Context> & ctx,float basex,float basey)235 void CStroke::RenderSelectionGlow(const Cairo::RefPtr<Cairo::Context> &ctx, float basex, float basey)
236 {
237 if(!selected.size()) return;
238
239 ctx->translate(-basex,-basey);
240 ctx->set_source_rgba(r,g,b,a);
241 ctx->set_line_cap(Cairo::LINE_CAP_ROUND);
242 for(unsigned int i=1;i<xcoords.size();++i) {
243 if(selected[i]) {
244 ctx->move_to(xcoords[i-1],ycoords[i-1]);
245 ctx->line_to(xcoords[i],ycoords[i]);
246 ctx->set_line_width(pcoords[i-1]+6);
247 ctx->stroke();
248 }
249 }
250 ctx->translate(basex,basey);
251 }
252
253 #include <gdk/gdkkeysyms.h>
254
on_key_press_event(GdkEventKey * k)255 bool CNotebook::on_key_press_event(GdkEventKey *k)
256 {
257 modifier_keys = k->state & (GDK_MODIFIER_MASK);
258 latest_keyval = k->keyval;
259 return Gsv::View::on_key_press_event(k);
260 }
261
on_changed()262 void CNotebook::on_changed()
263 {
264 last_modified = g_get_real_time();
265 }
266
on_insert(const Gtk::TextBuffer::iterator & iter,const Glib::ustring & str,int len)267 void CNotebook::on_insert(const Gtk::TextBuffer::iterator &iter,const Glib::ustring& str,int len)
268 {
269 if(str=="\n") {
270 if(modifier_keys & GDK_SHIFT_MASK) return;
271 if(!iter.ends_line()) return;
272
273 /* extract previous line's first word and indentation preceding it */
274 Gtk::TextBuffer::iterator start = iter; start.backward_line();
275 Gtk::TextBuffer::iterator end = start;
276 if(end.get_char()==' ' || end.get_char()=='\t') end.forward_find_char([] (char c) { return (c!=' ' && c!='\t') || c=='\n'; });
277 end.forward_find_char([] (char c) { return c==' ' || c=='\t' || c=='\n'; });
278 end.forward_char();
279 Glib::ustring str = sbuffer->get_text(start,end,true);
280 end.forward_char();
281
282 int num=0,pad=0,len=0;
283 //printf("word: %s\n",str.c_str());
284
285 /* count indentation spaces, then eat them */
286 sscanf(str.c_str()," %n",&pad);
287 str=str.substr(pad);
288
289 //printf("word: %s\n",str.c_str());
290
291 /* try to see if we have any valid markdown enumeration we could extend */
292 sscanf(str.c_str(),"%d.%*1[ \t]%n",&num,&len);
293 char buf[512];
294 if(len==str.length() && num>0) {
295 if(end >= iter) {
296 sbuffer->erase(start,iter);
297 return;
298 }
299 sprintf(buf,"%*s%d. ",pad,"",num+1);
300 sbuffer->insert(iter,buf);
301 } else if(str=="* ") {
302 if(end >= iter) {
303 sbuffer->erase(start,iter);
304 return;
305 }
306 sprintf(buf,"%*s* ",pad,"");
307 sbuffer->insert(iter,buf);
308 } else if(str=="- ") {
309 if(end >= iter) {
310 sbuffer->erase(start,iter);
311 return;
312 }
313 sprintf(buf,"%*s- ",pad,"");
314 sbuffer->insert(iter,buf);
315 } else if(str=="> ") {
316 if(end == iter) {
317 sbuffer->erase(start,iter);
318 return;
319 }
320 sprintf(buf,"%*s> ",pad,"");
321 sbuffer->insert(iter,buf);
322 }
323 }
324 /* TODO: we really should fix up the iterator, but this version of gtkmm erroneously sets iter to const */
325 }
326
327 /* redraw cairo overlay: active stroke, special widgets like lines, etc. */
on_redraw_overlay(const Cairo::RefPtr<Cairo::Context> & ctx)328 bool CNotebook::on_redraw_overlay(const Cairo::RefPtr<Cairo::Context> &ctx)
329 {
330 /* it seems that this can theoretically be called before the first on_allocate;
331 * since we don't know our document view's size yet, we have no choice but to bail */
332 if(!overlay_image) {
333 return true;
334 }
335 //active.Render(ctx,bx,by);
336 ctx->set_source(overlay_image,0,0);
337 ctx->paint();
338 //ctx->rectangle(0,0,get_width(),get_height());
339 //ctx->fill();
340
341 /* draw horizontal rules */
342 Gdk::Rectangle rect;
343 get_visible_rect(rect);
344
345 Gsv::Buffer::iterator i, end;
346 get_iter_at_location(i,rect.get_x(),rect.get_y());
347 get_iter_at_location(end,rect.get_x()+rect.get_width(),rect.get_y()+rect.get_height());
348
349 do{
350 if(sbuffer->iter_has_context_class(i, "hline")) {
351 int y, height;
352 get_line_yrange(i,y,height);
353 int linex0,liney0;
354 buffer_to_window_coords(Gtk::TEXT_WINDOW_WIDGET,0,y+height,linex0,liney0);
355
356 ctx->set_line_width(1.0);
357 ctx->set_source_rgba(.627,.659,.75,1);
358 ctx->move_to(linex0+margin_x,liney0-height/2);
359 ctx->line_to(linex0+rect.get_width()-margin_x,liney0-height/2);
360 ctx->stroke();
361 }
362 }while(sbuffer->iter_forward_to_context_class_toggle(i, "hline") && i<end);
363
364 /* draw selection rect, if there is any */
365 if(active_state == NB_MODE_SELECT) {
366 int bx, by;
367 window_to_buffer_coords(Gtk::TEXT_WINDOW_WIDGET,0,0,bx,by);
368
369 ctx->save();
370 ctx->set_line_width(2.0);
371 ctx->set_source_rgba(.627,.659,.75,1);
372 ctx->rectangle(sel_x0-bx,sel_y0-by,sel_x1-sel_x0,sel_y1-sel_y0);
373 ctx->set_dash(std::valarray<double>({2.0,2.0}),0.0);
374 ctx->stroke();
375 ctx->restore();
376 }
377
378 return true;
379 }
380
SetDocumentPath(std::string newpath)381 void CNotebook::SetDocumentPath(std::string newpath)
382 {
383 document_path = newpath;
384 // todo?: rerender all image widgets.
385 // We can get away without doing that for now, since the document path will only be set before loading.
386 }
387
DepositImage(GdkPixbuf * pixbuf)388 std::string CNotebook::DepositImage(GdkPixbuf *pixbuf)
389 {
390 guchar *buf = gdk_pixbuf_get_pixels(pixbuf);
391
392 /* walk through the pixbuf buffer, hashing the image pixels. */
393 /* this convoluted way is necessary because sometimes stride>3*width, and the padding bytes can contain random noise, breaking deduplication. */
394 int stride = gdk_pixbuf_get_rowstride(pixbuf);
395 int height = gdk_pixbuf_get_height(pixbuf);
396 int width = gdk_pixbuf_get_width(pixbuf);
397 GChecksum *cs = g_checksum_new(G_CHECKSUM_SHA1);
398 for(int y=0;y<height;++y)
399 g_checksum_update(cs,buf+y*stride,width);
400 const gchar *checksum = g_checksum_get_string(cs);
401
402 // gchar *checksum = g_compute_checksum_for_data(G_CHECKSUM_SHA1,buf,buf_length);
403
404 std::string filename = ".images/"+std::string(checksum,16)+".png";
405 // workaround for canonicalize_filename not being supported in glibmm<2.64
406 Glib::RefPtr<Gio::File> file = Glib::wrap(g_file_new_for_commandline_arg_and_cwd(filename.c_str(), document_path.c_str()));
407 std::string real_path = file->get_path();
408
409 Glib::RefPtr<Gio::File> f = Gio::File::create_for_path(document_path+"/.images");
410 try {
411 f->make_directory();
412 } catch(Gio::Error &e) {
413 // already exists
414 }
415
416 GError *err = NULL;
417
418 gdk_pixbuf_save(pixbuf,real_path.c_str(),"png",&err,NULL);
419
420 g_checksum_free(cs);
421
422 return "![]("+filename+") ";
423 }
424
on_highlight_updated(Gtk::TextBuffer::iterator & start,Gtk::TextBuffer::iterator & end)425 void CNotebook::on_highlight_updated(Gtk::TextBuffer::iterator &start, Gtk::TextBuffer::iterator &end)
426 {
427 //printf("relight %d %d\n",start.get_offset(),end.get_offset());
428 std::pair<Glib::ustring,Glib::RefPtr<Gtk::TextTag> > extratags[]
429 = { {"extra-space", tag_extra_space},
430 {"blockquote-text", tag_blockquote},
431 {"hline", tag_invisible},
432 {"mono", tag_mono} };
433 for(auto &[cclass,ttag] : extratags) {
434 Gtk::TextBuffer::iterator i = start;
435 do {
436 Gtk::TextBuffer::iterator next = i;
437 if(!sbuffer->iter_forward_to_context_class_toggle(next, cclass)) next=end;
438 if(sbuffer->iter_has_context_class(i, cclass)) {
439 sbuffer->apply_tag(ttag, i, next);
440 } else {
441 sbuffer->remove_tag(ttag, i, next);
442 }
443 i=next;
444 } while(i<end);
445 }
446
447 /* TODO: this makes some widgets flicker when they shouldn't. */
448 /* We really shouldn't clean up a span unless it changed,
449 * but need to solve the *asdf*ghjk* middle * being part of
450 * both prox tags problem */
451 /* probably:
452 * (1) walk the whole region, clear prox tags that no longer
453 * are overlapped by an exactly matching prox context;
454 * (2) walk the whole region, instantiate all prox contexts
455 * that are not already overlapped by an exactly matching
456 * prox tag.
457 * This ignores the case that a prox region has been
458 * EXACTLY replaced by an incompatible one, but whatevs. */
459 CleanUpSpan(start,end);
460
461 Gtk::TextBuffer::iterator i = start;
462 do {
463 Gtk::TextBuffer::iterator next = i;
464 if(!sbuffer->iter_forward_to_context_class_toggle(next, "prox")) next=end;
465
466 if(sbuffer->iter_has_context_class(i, "prox")) {
467 if(!i.has_tag(tag_proximity)) {
468 /* initialise tag by leaving if necessary */
469 Gtk::TextIter old_iter = sbuffer->get_iter_at_mark(last_position);
470 if(old_iter.compare(i)<0 || old_iter.compare(next)>0) {
471 PushIter(start);
472 PushIter(end);
473 on_leave_region(i,next);
474 end=PopIter();
475 start=PopIter();
476 }
477 }
478 sbuffer->apply_tag(tag_proximity, i, next);
479 } else {
480 /* nothing to do \o/ */
481 }
482 i=next;
483 } while(i<end);
484 }
485
PushIter(Gtk::TextIter i)486 Glib::RefPtr<Gtk::TextMark> CNotebook::PushIter(Gtk::TextIter i)
487 {
488 iter_stack.push(sbuffer->create_mark(i,true));
489 return iter_stack.top();
490 }
491
PopIter()492 Gtk::TextIter CNotebook::PopIter()
493 {
494 Glib::RefPtr<Gtk::TextMark> m = iter_stack.top();
495 iter_stack.pop();
496 Gtk::TextIter ret = sbuffer->get_iter_at_mark(m);
497 sbuffer->delete_mark(m);
498 return ret;
499 }
500
DebugTags(Gtk::TextBuffer::iterator & start,Gtk::TextBuffer::iterator & end)501 void CNotebook::DebugTags(Gtk::TextBuffer::iterator &start, Gtk::TextBuffer::iterator &end)
502 {
503 std::map<GtkTextTag*,char> known_tags;
504 std::map<GtkTextTag*,Glib::ustring> tag_names;
505 char lastid='A';
506 for(auto i=start;i<end;++i) {
507 std::string taglist;
508 std::vector<Glib::RefPtr<Gtk::TextTag> > tags = i.get_tags();
509 for(auto &t : tags) {
510 if(known_tags.count(t->gobj())) {
511 taglist += known_tags[t->gobj()];
512 } else {
513 taglist += lastid;
514 known_tags[t->gobj()] = lastid++;
515 tag_names[t->gobj()] = t->property_name().get_value();
516 }
517 }
518 printf("'%lc' %04X %s\n",i.get_char(),i.get_char(),taglist.c_str());
519 }
520 }
521
522 /* Queue creation of a new child anchor at the position indicated by the mark.
523 * The deferral may be necessary because insertion invalidates all iterators. */
QueueChildAnchor(Glib::RefPtr<Gtk::TextMark> mstart)524 void CNotebook::QueueChildAnchor(Glib::RefPtr<Gtk::TextMark> mstart)
525 {
526 /* once the anchor is created, syntax highlighting will recalculate, eventually
527 * calling this function again from the start */
528 auto s = Glib::IdleSource::create();
529 s->connect(sigc::slot<bool>( [mstart,this]() {
530 Gtk::TextIter start = sbuffer->get_iter_at_mark(mstart);
531
532 if(!start.get_child_anchor()) {
533 sbuffer->create_child_anchor(start);
534 }
535
536 /* We may have entered the region between the issuance of the original command
537 * and the IdleSource being processed, which would result in the second pass
538 * that is supposed to actually attach the widget not happening.
539 *
540 * To prevent this resulting in a displayed [X], start with the anchor hidden. */
541 start = sbuffer->get_iter_at_mark(mstart);
542 auto j = start; ++j;
543 sbuffer->apply_tag(tag_hidden,start,j);
544 sbuffer->apply_tag(tag_is_anchor,start,j);
545
546 sbuffer->delete_mark(mstart);
547 return false;
548 }));
549
550 s->attach();
551 }
552
RenderToWidget(Glib::ustring wtype,Gtk::TextBuffer::iterator & start,Gtk::TextBuffer::iterator & end)553 void CNotebook::RenderToWidget(Glib::ustring wtype, Gtk::TextBuffer::iterator &start, Gtk::TextBuffer::iterator &end)
554 {
555 // Debug output if necessary
556 DMPRINTF("rtw %s: %d, %d \n",wtype.c_str(),start.get_line_offset(),end.get_line_offset());
557 #ifdef _DEBUG_MOTION_
558 Gtk::TextBuffer::iterator s1=start, e1=end;
559 s1.backward_line(); s1.forward_line();
560 e1.forward_line(); e1.backward_char();
561 DebugTags(s1,e1);
562 #endif
563
564 Glib::RefPtr<Gtk::TextBuffer::ChildAnchor> anch;
565 if(!(anch=start.get_child_anchor())) {
566 /* we haven't set up a child anchor yet, so we need to queue the creation of one */
567 Glib::RefPtr<Gtk::TextMark> mstart = sbuffer->create_mark(start,true);
568 QueueChildAnchor(mstart);
569 } else {
570 auto j = start; ++j;
571 sbuffer->remove_tag(tag_hidden,start,j);
572
573 Gtk::Widget *b = widget_classes[wtype](this, start, end);
574
575 Gtk::manage(b);
576 add_child_at_anchor(*b,anch);
577 b->show();
578 }
579 }
580
GetBaselineTag(int baseline)581 Glib::RefPtr<Gtk::TextTag> CNotebook::GetBaselineTag(int baseline)
582 {
583 if(!baseline_tags.count(baseline)) {
584 auto t = sbuffer->create_tag();
585 t->property_rise().set_value(-PANGO_SCALE*baseline);
586 baseline_tags[baseline]=t;
587 }
588 return baseline_tags[baseline];
589 }
590
UnrenderWidgets(Gtk::TextBuffer::iterator & start,Gtk::TextBuffer::iterator & end)591 void CNotebook::UnrenderWidgets(Gtk::TextBuffer::iterator &start, Gtk::TextBuffer::iterator &end)
592 {
593 if(start.get_child_anchor()) {
594 auto ws = start.get_child_anchor()->get_widgets();
595 for(Gtk::Widget *w : ws) {
596 delete w;
597 }
598 auto j = start; ++j;
599 sbuffer->apply_tag(tag_hidden,start,j);
600
601 /*PushIter(end);
602 Gtk::TextBuffer::iterator start_old = start;
603 ++start;
604 start=sbuffer->erase(start_old,start);
605 end=PopIter();*/
606 }
607 }
608
609 /* Remove widget child anchors and proximity-related tags in span. */
CleanUpSpan(Gtk::TextBuffer::iterator & start,Gtk::TextBuffer::iterator & end)610 void CNotebook::CleanUpSpan(Gtk::TextBuffer::iterator &start, Gtk::TextBuffer::iterator &end)
611 {
612 DMPRINTF("start cleaning %d %d\n",start.get_offset(),end.get_offset());
613
614 /* PASS 1: Clean any tag_proximity regions that disappeared. */
615 PushIter(start);
616 Gtk::TextBuffer::iterator i = start;
617
618 do {
619 Gtk::TextBuffer::iterator next = i;
620 if(!next.forward_to_tag_toggle(tag_proximity) || next>end) next=end;
621
622 Gtk::TextIter ic=next, nextc=i;
623 sbuffer->iter_backward_to_context_class_toggle(ic,"prox");
624 sbuffer->iter_forward_to_context_class_toggle(nextc,"prox");
625
626 if(i.has_tag(tag_proximity) && (ic!=i || nextc!=next)) {
627 DMPRINTF("clean old prox region %d %d\n",i.get_offset(),next.get_offset());
628 std::pair<Glib::ustring,Glib::RefPtr<Gtk::TextTag> > volatile_tags[]
629 = { {"invisible", tag_hidden} };
630
631 for(auto &[cclass,ttag] : volatile_tags) {
632 sbuffer->remove_tag(ttag, i, next);
633 }
634 sbuffer->remove_tag(tag_proximity,i,next);
635
636 if(i.get_child_anchor()) {
637 PushIter(end);
638 PushIter(next);
639 next=i;
640 ++next;
641 start=sbuffer->erase(i,next);
642 next=PopIter();
643 end=PopIter();
644 }
645 }
646 i=next;
647 } while(i<end);
648 start=PopIter();
649
650 /* PASS 2: Clean any leftover internal child anchors from tag_proximity regions that got merged. */
651 PushIter(start);
652 i = start;
653
654 do {
655 Gtk::TextBuffer::iterator next = i;
656 if(!next.forward_to_tag_toggle(tag_is_anchor) || next>end) next=end;
657
658 Gtk::TextIter ic=next;
659 sbuffer->iter_backward_to_context_class_toggle(ic,"prox");
660
661 if(i.has_tag(tag_is_anchor) && (ic!=i)) {
662 DMPRINTF("clean straggler tag %d %d\n",i.get_offset(),next.get_offset());
663 if(i.get_child_anchor()) {
664 PushIter(end);
665 PushIter(next);
666 next=i;
667 ++next;
668 start=sbuffer->erase(i,next);
669 next=PopIter();
670 end=PopIter();
671 }
672 }
673 i=next;
674 } while(i<end);
675 start=PopIter();
676 }
677
on_enter_region(Gtk::TextBuffer::iterator & start,Gtk::TextBuffer::iterator & end)678 void CNotebook::on_enter_region(Gtk::TextBuffer::iterator &start, Gtk::TextBuffer::iterator &end)
679 {
680 DMPRINTF("enter region: %d %d\n",start.get_offset(),end.get_offset());
681
682 std::pair<Glib::ustring,Glib::RefPtr<Gtk::TextTag> > volatile_tags[]
683 = { {"invisible", tag_hidden} };
684
685 for(auto &[cclass,ttag] : volatile_tags) {
686 sbuffer->remove_tag(ttag, start, end);
687 }
688
689 for(auto &[s,f] : widget_classes) {
690 if(sbuffer->iter_has_context_class(start, s)) {
691 UnrenderWidgets(start,end);
692 }
693 }
694 }
695
on_leave_region(Gtk::TextBuffer::iterator & start,Gtk::TextBuffer::iterator & end)696 void CNotebook::on_leave_region(Gtk::TextBuffer::iterator &start, Gtk::TextBuffer::iterator &end)
697 {
698 DMPRINTF("leave region: %d %d\n",start.get_offset(),end.get_offset());
699
700 std::pair<Glib::ustring,Glib::RefPtr<Gtk::TextTag> > volatile_tags[]
701 = { {"invisible", tag_hidden} };
702
703 for(auto &[cclass,ttag] : volatile_tags) {
704 Gtk::TextBuffer::iterator i = start;
705 do {
706 Gtk::TextBuffer::iterator next = i;
707 if(!sbuffer->iter_forward_to_context_class_toggle(next, cclass)) next=end;
708 if(sbuffer->iter_has_context_class(i, cclass)) {
709 sbuffer->apply_tag(ttag, i, next);
710 } else {
711 sbuffer->remove_tag(ttag, i, next);
712 }
713 i=next;
714 } while(i<end);
715 }
716
717 for(auto &[s,f] : widget_classes) {
718 if(sbuffer->iter_has_context_class(start, s)) {
719 UnrenderWidgets(start,end); // just in case
720 RenderToWidget(s,start,end);
721 }
722 }
723 }
724
725
GetTagExtents(Gtk::TextIter t,Glib::RefPtr<Gtk::TextTag> tag,Gtk::TextIter & left,Gtk::TextIter & right)726 bool GetTagExtents(Gtk::TextIter t, Glib::RefPtr<Gtk::TextTag> tag, Gtk::TextIter &left, Gtk::TextIter &right)
727 {
728 if(t.starts_tag(tag)) {
729 left=t;
730 t.forward_to_tag_toggle(tag);
731 right=t;
732 return true;
733 }
734 if(t.ends_tag(tag)) {
735 right=t;
736 t.backward_to_tag_toggle(tag);
737 left=t;
738 return true;
739 }
740 if(t.has_tag(tag)) {
741 left=t;
742 left.backward_to_tag_toggle(tag);
743 right=t;
744 right.forward_to_tag_toggle(tag);
745 return true;
746 }
747 return false;
748 }
749
on_move_cursor()750 void CNotebook::on_move_cursor()
751 {
752 /* selecting text is mostly frustrating if the text layout
753 * changes due to hiding/unhiding while doing it */
754 if(sbuffer->get_has_selection()) return;
755
756 auto s = Glib::IdleSource::create();
757 s->connect(sigc::slot<bool>( [this]() {
758 Gtk::TextIter new_iter = sbuffer->get_iter_at_mark(sbuffer->get_insert());
759 Gtk::TextIter old_iter = sbuffer->get_iter_at_mark(last_position);
760 //printf("old: %d, new: %d\n", old_iter.get_offset(), new_iter.get_offset());
761 /*auto v = new_iter.get_marks();
762 printf("%d marks here: ",v.size());
763 for(auto &m : v) printf("%s ",m->get_name().c_str());
764 printf("\n");*/
765
766 Gtk::TextIter old_left, old_right, new_left, new_right;
767
768 bool old_valid = GetTagExtents(old_iter,tag_proximity,old_left,old_right);
769 bool new_valid = GetTagExtents(new_iter,tag_proximity,new_left,new_right);
770 bool from_right = new_valid && new_iter.compare(new_left)>0//old_iter.compare(new_iter)>0
771 && (!latest_keyval || latest_keyval == GDK_KEY_Left ||
772 latest_keyval == GDK_KEY_Up || latest_keyval == GDK_KEY_Down);
773 Glib::RefPtr<Gtk::TextMark> move_to;
774
775 if(old_valid) {
776 if(new_valid) {
777 if(new_iter.compare(old_left)<0 || new_iter.compare(old_right)>0) {
778 if(from_right) {
779 if(new_iter.has_tag(tag_hidden)) {
780 new_iter.forward_to_tag_toggle(tag_hidden);
781 move_to = sbuffer->create_mark(new_iter,false);
782 }
783 }
784 /* moved into a different prox region. signal both. */
785 PushIter(new_left);
786 PushIter(new_right);
787 on_leave_region(old_left,old_right);
788 new_right=PopIter();
789 new_left=PopIter();
790 on_enter_region(new_left,new_right);
791 } else if(new_iter==old_iter) {
792 /* we may have deleted right into a prox region's boundary. signal to be safe. */
793 DMPRINTF("deletion enter\n");
794 on_enter_region(new_left,new_right);
795 }
796 /* otherwise, we stayed in the same one. no signals needed */
797 } else {
798 on_leave_region(old_left,old_right);
799 }
800 } else if(new_valid) {
801 if(from_right) {
802 if(new_iter.has_tag(tag_hidden)) {
803 new_iter.forward_to_tag_toggle(tag_hidden);
804 move_to = sbuffer->create_mark(new_iter,false);
805 }
806 }
807 on_enter_region(new_left,new_right);
808 }
809
810 if(move_to) {
811 //printf("move right: %d\n",latest_keyval);
812 Gtk::TextIter t = sbuffer->get_iter_at_mark(move_to);
813 Gtk::TextIter ins = sbuffer->get_iter_at_mark(sbuffer->get_insert());
814 Gtk::TextIter sb = sbuffer->get_iter_at_mark(sbuffer->get_mark("selection_bound"));
815 sbuffer->move_mark_by_name("insert",t);
816 if(sb==ins) sbuffer->move_mark_by_name("selection_bound",t);
817 sbuffer->delete_mark(move_to);
818 /*auto s = Glib::IdleSource::create();
819 s->connect(sigc::slot<bool>( [move_to,this]() {
820 Gtk::TextIter t = sbuffer->get_iter_at_mark(move_to);
821 Gtk::TextIter ins = sbuffer->get_iter_at_mark(sbuffer->get_insert());
822 Gtk::TextIter sb = sbuffer->get_iter_at_mark(sbuffer->get_mark("selection_bound"));
823 sbuffer->move_mark_by_name("insert",t);
824 if(sb==ins) sbuffer->move_mark_by_name("selection_bound",t);
825 sbuffer->delete_mark(move_to);
826 return false;
827 }));
828 s->attach();*/
829 }
830
831 sbuffer->move_mark(last_position, sbuffer->get_iter_at_mark(sbuffer->get_insert()));//new_iter);
832
833 return false;
834 }));
835 s->attach();
836 }
837
Widget2Doc(double in_x,double in_y,double & out_x,double & out_y)838 void CNotebook::Widget2Doc(double in_x, double in_y, double &out_x, double &out_y)
839 {
840 int bx, by;
841 window_to_buffer_coords(Gtk::TEXT_WINDOW_WIDGET,0,0,bx,by);
842
843 out_x = in_x+bx;
844 out_y = in_y+by;
845 }
846
on_button_press(GdkEventButton * e)847 bool CNotebook::on_button_press(GdkEventButton *e)
848 {
849 latest_keyval=0;
850
851 GdkDevice *d = gdk_event_get_source_device((GdkEvent*)e);
852 if(!devicemodes.count(d)) {
853 if(gdk_device_get_n_axes(d)>4) {
854 // assume it's a pen
855 devicemodes[d] = NB_MODE_DRAW;
856 } else {
857 devicemodes[d] = NB_MODE_TEXT;
858 }
859 }
860 switch(devicemodes[d]) {
861 case NB_MODE_TEXT:
862 return false;
863 case NB_MODE_DRAW: {
864 double x,y;
865 Widget2Doc(e->x,e->y,x,y);
866
867 //printf("sig: %f %f %f %f\n",e->x, e->y, x,y);
868
869 active.Reset();
870 //active.r=active.g=active.b=0; active.a=1;
871 active.Append(x,y, stroke_width*ReadPressure((GdkEvent*)e));
872
873 active_state = NB_STATE_DRAW;
874
875 gdk_window_set_event_compression(e->window,false);
876
877 grab_focus();
878
879 return true;
880 }
881 case NB_MODE_ERASE: {
882 active_state = NB_STATE_ERASE;
883
884 grab_focus();
885
886 return true;
887 }
888 case NB_MODE_SELECT: {
889 double x,y;
890 Widget2Doc(e->x,e->y,x,y);
891
892 active_state = NB_STATE_SELECT;
893
894 grab_focus();
895
896 sel_x0=x;
897 sel_y0=y;
898
899 return true;
900 }
901 default: return false;
902 }
903
904 }
905
on_button_release(GdkEventButton * e)906 bool CNotebook::on_button_release(GdkEventButton *e)
907 {
908 gdk_window_set_event_compression(e->window,true);
909
910 if(active_state!=NB_STATE_NOTHING) {
911 if(active_state==NB_STATE_DRAW) {
912 sbuffer->begin_not_undoable_action();
913 active.Simplify();
914 CommitStroke();
915 sbuffer->end_not_undoable_action();
916 } else if(active_state==NB_STATE_SELECT) {
917 double x,y;
918 Widget2Doc(e->x,e->y,x,y);
919
920 if(sel_x0>x) std::swap(sel_x0,x);
921 if(sel_y0>y) std::swap(sel_y0,y);
922
923 SelectBox(sel_x0,x,sel_y0,y);
924
925 queue_draw();
926 }
927
928 active_state=NB_STATE_NOTHING;
929
930 return true;
931 }
932 return false;
933 }
934
on_motion_notify(GdkEventMotion * e)935 bool CNotebook::on_motion_notify(GdkEventMotion *e)
936 {
937 if(active_state==NB_STATE_DRAW) {
938 double x,y,p;
939 Widget2Doc(e->x,e->y,x,y);
940 p = stroke_width*ReadPressure((GdkEvent*)e);
941
942 /* dynamically toggle event compression */
943 float xprev = 0.0f;
944 float yprev = 0.0f;
945 active.GetHead(xprev,yprev);
946 float curve = abs(active.GetHeadCurvatureWrt(x,y));
947 float delta = sqrt ( (xprev-x)*(xprev-x)+(yprev-y)*(yprev-y) );
948
949 if(delta<0.6) {
950 /* reject if we are too close to previous head */
951 gdk_window_set_event_compression(e->window,true);
952 return false;
953 }
954
955 attention_ewma = sqrt(curve)+(delta>4?0.2:0);
956 if(attention_ewma<0.32) {
957 //printf("compression on\n");
958 //p=1;
959 gdk_window_set_event_compression(e->window,true);
960 } else if(attention_ewma>0.32){
961 //printf("compression off\n");
962 //%%p=3;
963 gdk_window_set_event_compression(e->window,false);
964 }
965 //p=attention_ewma;
966
967 active.Append(x,y,p);
968
969 int bx, by;
970 window_to_buffer_coords(Gtk::TEXT_WINDOW_WIDGET,0,0,bx,by);
971 active.Render(overlay_ctx,bx,by,active.xcoords.size()-1);
972
973 overlay.queue_draw_area(e->x-delta-p,e->y-delta-p,2*delta+4*p,2*delta+4*p);
974 } else if (active_state==NB_STATE_ERASE) {
975 double x,y;
976 Widget2Doc(e->x,e->y,x,y);
977
978 EraseAtPosition(x,y);
979 } else if (active_state==NB_STATE_SELECT) {
980 double x,y;
981 Widget2Doc(e->x,e->y,x,y);
982
983 int bx, by;
984 window_to_buffer_coords(Gtk::TEXT_WINDOW_WIDGET,0,0,bx,by);
985
986 int rex0 = std::min({sel_x0,sel_x1,x})-2-bx;
987 int rex1 = std::max({sel_x0,sel_x1,x})+2-bx;
988 int rey0 = std::min({sel_y0,sel_y1,y})-2-by;
989 int rey1 = std::max({sel_y0,sel_y1,y})+2-by;
990
991 overlay.queue_draw_area(rex0,rey0,rex1-rex0,rey1-rey0);
992
993 sel_x1=x; sel_y1=y;
994 }
995 return false;
996 }
997
ReadPressure(GdkEvent * e)998 float CNotebook::ReadPressure(GdkEvent *e)
999 {
1000 gdouble r;
1001 if(gdk_event_get_axis(e,GDK_AXIS_PRESSURE,&r))
1002 return r;
1003 else return 1.0;
1004 }
1005
1006 /* x, y are in buffer coordinates */
EraseAtPosition(float x,float y)1007 void CNotebook::EraseAtPosition(float x, float y)
1008 {
1009 Gtk::TextBuffer::iterator i;
1010 int trailing;
1011 get_iter_at_position(i,trailing,x,y);
1012
1013 if(i.get_child_anchor()) {
1014 auto w = i.get_child_anchor()->get_widgets();
1015 if(w.size()) {
1016 if(CBoundDrawing* d = CBoundDrawing::TryUpcast(w[0])) {
1017 /* found a region to erase from; we can adjust for its real location,
1018 * but that's relative to the text body, so need to fixup */
1019 int bx, by;
1020 window_to_buffer_coords(Gtk::TEXT_WINDOW_TEXT,0,0,bx,by);
1021 d->EraseAt(x-(d->get_allocation().get_x())-bx,y-(d->get_allocation().get_y())-by,stroke_width,true);
1022
1023 on_changed();
1024 }
1025 }
1026 }
1027 }
1028
1029 /* all parameters are in buffer coordinates */
SelectBox(float x0,float x1,float y0,float y1)1030 void CNotebook::SelectBox(float x0, float x1, float y0, float y1)
1031 {
1032 Gtk::TextBuffer::iterator i,j;
1033 int trailing;
1034 get_iter_at_position(i,trailing,x0,y0);
1035 get_iter_at_position(j,trailing,x1,y1);
1036
1037 int bx, by;
1038 window_to_buffer_coords(Gtk::TEXT_WINDOW_TEXT,0,0,bx,by);
1039
1040 std::unordered_set<CBoundDrawing*> seen;
1041
1042 do {
1043 if(i.get_child_anchor()) {
1044 auto w = i.get_child_anchor()->get_widgets();
1045 if(w.size()) {
1046 if(CBoundDrawing* d = CBoundDrawing::TryUpcast(w[0])) {
1047 float ax = -(d->get_allocation().get_x())-bx;
1048 float ay = -(d->get_allocation().get_y())-by;
1049 d->Select(x0+ax, x1+ax, y0+ay, y1+ay);
1050 seen.insert(d);
1051 }
1052 }
1053 }
1054
1055 if(i==j) break;
1056 ++i;
1057 } while(i!=sbuffer->end());
1058
1059 for(Gtk::Widget *w : get_children()) {
1060 if(CBoundDrawing *d = CBoundDrawing::TryUpcast(w)) {
1061 if(!seen.count(d)) {
1062 printf("unsel %p\n",d);
1063 d->Unselect();
1064 }
1065 }
1066 }
1067 }
1068
CommitStroke()1069 void CNotebook::CommitStroke()
1070 {
1071 float x0,y0,x1,y1;
1072 active.GetBBox(x0,x1,y0,y1);
1073
1074 CBoundDrawing *d = NULL;
1075
1076 /* clear buffer */
1077 overlay_ctx->save();
1078 overlay_ctx->set_source_rgba(0,0,0,0);
1079 overlay_ctx->rectangle(0,0,get_width(),get_height());
1080 overlay_ctx->set_operator(Cairo::OPERATOR_SOURCE);
1081 overlay_ctx->fill();
1082 overlay_ctx->restore();
1083
1084 /* search for preceding regions we can merge with */
1085 Gtk::TextBuffer::iterator i,k;
1086 get_iter_at_location(i,x0,y0);
1087 k=i; // for later
1088 do {
1089 if(i.get_child_anchor()) {
1090 auto w = i.get_child_anchor()->get_widgets();
1091 if(w.size()) {
1092 //printf("match?\n");
1093 d = CBoundDrawing::TryUpcast(w[0]);
1094 break;
1095 }
1096 }
1097 if(i==sbuffer->begin()) break; // nothing more to the left
1098 Gtk::TextBuffer::iterator j = i;
1099 --j;
1100 gunichar prev_char = j.get_char();
1101 if(!g_unichar_isspace(prev_char) && prev_char!=0xFFFC) {
1102 break;
1103 }
1104 i=j;
1105 } while(true);
1106
1107 if(d!=NULL) {
1108 /* found a region to merge with above; we can adjust for its real location,
1109 * but that's relative to the text body, so need to fixup */
1110 int bx, by;
1111 window_to_buffer_coords(Gtk::TEXT_WINDOW_TEXT,0,0,bx,by);
1112 //printf("fix: %d %d\n",bx,by);
1113 d->AddStroke(active,-(d->get_allocation().get_x())-bx,-(d->get_allocation().get_y())-by);
1114 } else {
1115 /* can't merge up, but maybe there's a region further down that can be expanded? */
1116 do {
1117 if(k.get_child_anchor()) {
1118 auto w = k.get_child_anchor()->get_widgets();
1119 if(w.size()) {
1120 //printf("match below?\n");
1121 d = CBoundDrawing::TryUpcast(w[0]);
1122 break;
1123 }
1124 }
1125 gunichar next_char = k.get_char();
1126 if(!g_unichar_isspace(next_char) && next_char!=0xFFFC) {
1127 break;
1128 }
1129 ++k;
1130 } while(k!=sbuffer->end());
1131
1132 if(d!=NULL) {
1133 /* found a region to merge with below; we need to expand it before adding this stroke */
1134
1135 /* find where the drawing should start */
1136 get_iter_at_location(i,x0,y0);
1137 Gdk::Rectangle r;
1138 get_iter_location(i,r);
1139
1140 //printf("get rect: %d %d %d %d\n",r.get_x(),r.get_y(),r.get_width(),r.get_height());
1141
1142 /* once again, need to correct allocation to be in buffer coords */
1143 int bx, by;
1144 window_to_buffer_coords(Gtk::TEXT_WINDOW_TEXT,0,0,bx,by);
1145
1146 int dx = -d->get_allocation().get_x()-bx + r.get_x(),
1147 dy = -d->get_allocation().get_y()-by + r.get_y();
1148
1149 d->UpdateSize(d->w, d->h, dx, dy);
1150
1151 d->AddStroke(active,-(r.get_x()),-(r.get_y()));
1152
1153 /* eat intermittent spaces */
1154 sbuffer->erase(i,k);
1155 } else {
1156 /* couldn't find a region to merge with; create a new image */
1157 d = new CBoundDrawing(get_window(Gtk::TEXT_WINDOW_TEXT));
1158 Gtk::manage(d);
1159 get_iter_at_location(i,x0,y0);
1160 if(i.has_tag(tag_hidden)) {
1161 i.forward_to_tag_toggle(tag_hidden);
1162 ++i; // TODO: Is this safe?
1163 }
1164
1165 /* figure out where it got anchored in the text, so we can translate the stroke correctly */
1166 Gdk::Rectangle r;
1167 get_iter_location(i,r);
1168
1169 if(r.get_x() > x0) {
1170 /* end of document, line too far to the right; add a new line */
1171 /* TODO: check this logic */
1172 sbuffer->insert(i,"\n");
1173 get_iter_at_location(i,x0,y0);
1174 get_iter_location(i,r);
1175 }
1176
1177 auto anch = sbuffer->create_child_anchor(i);
1178 add_child_at_anchor(*d,anch);
1179
1180 //printf("anchoring: %f %f -> %d %d\n",x0,y0,r.get_x(),r.get_y());
1181
1182 d->AddStroke(active,-(r.get_x()),-(r.get_y()));
1183 }
1184 }
1185
1186 d->show();
1187
1188 floats.insert(d);
1189
1190 active.Reset();
1191
1192 on_changed(); // update changed flag
1193 }
1194
on_serialize(const Glib::RefPtr<Gtk::TextBuffer> & content_buffer,const Gtk::TextBuffer::iterator & start,const Gtk::TextBuffer::iterator & end,gsize & length,bool render_images)1195 guint8* CNotebook::on_serialize(const Glib::RefPtr<Gtk::TextBuffer>& content_buffer, const Gtk::TextBuffer::iterator& start, const Gtk::TextBuffer::iterator& end, gsize& length, bool render_images)
1196 {
1197 Glib::ustring buf;
1198 Gtk::TextBuffer::iterator pos0 = start, pos1 = start;
1199 while(pos0!=end) {
1200 while(!pos1.get_child_anchor() && pos1!=end) ++pos1;
1201 buf += pos0.get_text(pos1);
1202 pos0=pos1;
1203 if(pos0!=end && pos0.get_child_anchor()) {
1204 auto w = pos0.get_child_anchor()->get_widgets();
1205 if(w.size()) {
1206 if(CBoundDrawing *d = CBoundDrawing::TryUpcast(w[0])) {
1207 if(render_images)
1208 buf += d->SerializePNG();
1209 else
1210 buf += d->Serialize();
1211 }
1212 }
1213 ++pos0; ++pos1;
1214 }
1215 }
1216
1217 guint8* ret = (guint8*)g_malloc(buf.bytes());
1218 memcpy(ret,buf.data(),buf.bytes());
1219 length = buf.bytes();
1220 return ret;
1221 }
1222
on_deserialize(const Glib::RefPtr<Gtk::TextBuffer> & content_buffer,Gtk::TextBuffer::iterator & iter,const guint8 * data,gsize length,bool create_tags)1223 bool CNotebook::on_deserialize(const Glib::RefPtr<Gtk::TextBuffer>& content_buffer, Gtk::TextBuffer::iterator& iter, const guint8* data, gsize length, bool create_tags)
1224 {
1225 std::string str((const char*)data,length);
1226
1227 int pos=0,pos0=0;
1228 bool drawing=false;
1229 while(pos!=str.length()) {
1230 if(!drawing) {
1231 pos=str.find("![](nk:",pos);
1232 if(pos==std::string::npos) pos=str.length();
1233 else {
1234 drawing=true;
1235 }
1236 iter=content_buffer->insert(iter,str.c_str()+pos0, str.c_str()+pos);
1237 pos0=pos;
1238 } else {
1239 pos+=7; pos0=pos;
1240 pos=str.find(')',pos);
1241
1242 CBoundDrawing *d = new CBoundDrawing(get_window(Gtk::TEXT_WINDOW_TEXT));
1243 Gtk::manage(d);
1244
1245 /* store iterator through mark creation */
1246 //printf("before: %d\n",iter.get_offset());
1247 auto mk = content_buffer->create_mark(iter,false);
1248 auto anch = content_buffer->create_child_anchor(iter);
1249 iter = content_buffer->get_iter_at_mark(mk);
1250 //printf("after: %d\n",iter.get_offset());
1251 content_buffer->delete_mark(mk);
1252
1253
1254 add_child_at_anchor(*d,anch);
1255
1256 d->Deserialize(str.substr(pos0,pos-pos0));
1257 //d->DumpForDebugging();
1258 d->show();
1259
1260 floats.insert(d);
1261
1262 pos0=pos=pos+1;
1263
1264 drawing=false;
1265 }
1266 }
1267
1268 return true;
1269 }
1270
1271