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