/* This file is part of Bolixo. Bolixo is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Bolixo is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Bolixo. If not, see . */ /* White board drawing program. The goal of this program is to create drawings very quickly. It may contain text graph Anything we usually draw to explain an idea. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include "bolixo.h" #include "proto/bod_client.protodef" #include "documentd.h" #include "documentd_menu.h" #include "bolixo.m" using namespace std; enum WHITEBOARD_SELMODE{ SEL_NONE, SEL_SELECTED, // This element is selected so we can act on it // (move it, enlarge it). SEL_STAR, // This element is selected so we can attach // more elements to it. SEL_IMBED, // This element is selected so we can insert // more elements into it. }; enum WHITEBOARD_TEXTPOS{ TEXTPOS_INSIDE, TEXTPOS_TOP, TEXTPOS_BOTTOM, TEXTPOS_LEFT, TEXTPOS_RIGHT, TEXTPOS_LAST }; enum ARROW_TYPE{ ARROW_NONE, // No line ARROW_END, // A line with an arrow at tne end ARROW_START, // A line with an arrow at the start ARROW_BOTH, // A line with an arrow at the start and the end ARROW_RETURN, // A line with an arrow at the end and another line, curved, with an arrow at the start ARROW_LINE, // Just a line ARROW_DASH, // Just a dash line ARROW_DOT, // Just a dot line ARROW_LAST, // Last item in the list + 1 }; enum BOX_TYPE{ BOX_SOLID, // Solid line BOX_DASH, // Dash line BOX_DOT, // Dot line BOX_HIDDEN, // No box BOX_LAST, // Last item in the list + 1 }; #define documentd_whiteboard_header_NOTNEED #define documentd_whiteboard_header2_NOTNEED #define documentd_whiteboard_elements_NOTNEED #define documentd_whiteboard_elements2_NOTNEED #include "proto/documentd_whiteboard.protoch" #include "proto/documentd_whiteboard.protoh" enum WHITEBOARD_ELMTYPE { // Type of element TYPE_ELLIPSE, TYPE_RECT, TYPE_LINE, // One line or arrow TYPE_HANDLINE, // line drawn by hand TYPE_TEXT, // Draw text with a bullet }; static const char *tbcolor[]={"black","red","yellow","green"}; // This describe an arrow from a parent to a child struct SUBELM{ unsigned id = 0; ARROW_TYPE arrow; SUBELM(unsigned _id, ARROW_TYPE _arrow) :id(_id), arrow(_arrow){ } }; // This is used to sort the various arrows going to and from an element. // All this is computed struct ARROW_ANGLE{ unsigned id = 0; ARROW_TYPE arrow; int angle=0; // Angle between parent and child // The angle is in 1/10 degre // See WHITEBOARD::sortsubelms() ARROW_ANGLE(unsigned _id, ARROW_TYPE _arrow, int _angle) :id(_id), arrow(_arrow), angle(_angle){ } bool operator == (const ARROW_ANGLE &n) const { return id == n.id && arrow == n.arrow && angle == n.angle; } }; /* Make sure size do not underflow */ inline void check_limit (unsigned short &size, int val) { if (val > 0 || -val < size){ size += val; } } static int snap2grid(int x, int grid){ if (grid != 0){ int modx = x % grid; if (modx < grid/2){ return x - modx; }else{ return x + (grid - modx); } } return x; } struct WHITEBOARD_ELM{ WHITEBOARD_SELMODE selmode = SEL_NONE; // Make the element more visible // for other actions BOX_TYPE box_type = BOX_SOLID; unsigned id = 0; unsigned short x; // Coordinates unsigned short y; unsigned short x1=0; // end coordinates unsigned short y1=0; unsigned short width=0; unsigned short height=0; short textsize=0; std::string label; // Useful to reference the element in scripts ? WHITEBOARD_ELMTYPE type; std::string txt; // Caption WHITEBOARD_TEXTPOS textpos = TEXTPOS_INSIDE; vector insides; // insides are drawn inside the element. vector subelms; // subelms are related elements. vector arrows[4]; // All lines going to and from this element with angle, for the four sides DOCUMENT_EMBED embed; string imageurl; void clear_imbed(){ embed.document.clear(); embed.region.clear(); embed.docnum = 0; } void clear_image(){ imageurl.clear(); } bool is_imbed() const { return embed.document.size() > 0; } const char *get_document() const{ return embed.document.c_str(); } const char *get_region() const{ return embed.region.c_str(); } unsigned get_docnum() const{ return embed.docnum; } bool is_image() const { return imageurl.size() > 0; } const char *get_image() const{ return imageurl.c_str(); } void resize(int val, unsigned gridcell, bool horizontal_only, bool vertical_only){ if (type == TYPE_TEXT){ // Resizing a bullet text simply change its indentation check_limit(width,val); }else{ if (horizontal_only){ check_limit(width,val); width = snap2grid (width,gridcell); }else if (vertical_only){ check_limit(height,val); height = snap2grid (height,gridcell); }else if (width == height || width == 0 || height == 0){ check_limit (width,val); check_limit (height,val); width = snap2grid (width,gridcell); height = snap2grid (height,gridcell); }else{ // Here we should compute the aspect ratio of the object and preserve it. double ratio = (double)width/height; check_limit(width,ratio*val); check_limit(height,val/ratio); width = snap2grid (width,gridcell); height = snap2grid (height,gridcell); } } if (width < 8) width = 8; if (height < 8) height = 8; } void move(map &elements, int movex, int movey, set &ids){ ids.insert (id); x += movex; y += movey; if (type == TYPE_LINE){ x1 += movex; y1 += movey; } for (auto &e:insides){ elements[e].move(elements,movex,movey,ids); } } // Compute where should go new items (duplicate) to the right and below pair compute_next_item(unsigned browser_fontsize, unsigned textsize){ unsigned fontsize = compute_fontsize(browser_fontsize,textsize); unsigned textx,texty,textlen; gettextpos(textx,texty,textlen,fontsize); unsigned nextx = x; unsigned nexty = y; { // Compute the item to the right unsigned w_2 = width/2; nextx += w_2; unsigned endtext = textx + textlen; if (endtext > nextx){ // Text is on the right nextx = endtext; }else if (textx < nextx-width){ // Text is on the left nextx += textlen; } nextx += w_2 + 10; } { // Compute the item below unsigned h_2 = height/2; nexty += h_2; if (texty > nexty) nexty = texty; nexty += h_2 + fontsize; } return pair(nextx,nexty); } void gettextpos(unsigned &textx, unsigned &texty, unsigned &textlen, unsigned fontsize) const{ textx=0; texty=0; unsigned w_2 = width/2; unsigned h_2 = height/2; textlen = documentd_displaylen (txt.c_str(),fontsize,1); unsigned centerx=0; unsigned centery=y+fontsize/2-3; if (textlen >= width){ // Text too large, put it at the start of the object centerx = x-w_2; }else{ // Center the text centerx = x-w_2+(width-textlen)/2; } switch(textpos){ case TEXTPOS_LAST: case TEXTPOS_INSIDE: textx = centerx; texty = centery; break; case TEXTPOS_TOP: textx = centerx; texty = y-h_2-3; break; case TEXTPOS_BOTTOM: textx = centerx; texty = y+h_2+fontsize; break; case TEXTPOS_LEFT: textx = x-w_2-3-textlen; texty = centery; break; case TEXTPOS_RIGHT: textx = x+w_2+3; texty = centery; break; } } void gettextpos(unsigned &textx, unsigned &texty, unsigned fontsize){ unsigned textlen; gettextpos(textx,texty,textlen,fontsize); } // Compute the effective font size of this element // by combining the default browser fontsize and this document default textsize unsigned compute_fontsize(unsigned browser_fontsize, unsigned default_textsize) const{ int size = default_textsize + textsize; if (size < 1) size = 1; float size_factor = size / 10.0; return browser_fontsize * size_factor; } // Return the X position of an element. // For text bullet, the position is affected by the width. unsigned getadjx() const { return type == TYPE_TEXT ? x + (width-8)/2 : x; } void redraw(VARVAL &var, unsigned browser_fontsize, unsigned default_textsize){ const char *stroke_color = tbcolor[selmode]; auto tmp_box_type = box_type; if (tmp_box_type == BOX_HIDDEN){ tmp_box_type = BOX_SOLID; if (selmode == SEL_NONE) stroke_color = "none"; } unsigned w_2 = width/2; unsigned h_2 = height/2; if (type == TYPE_ELLIPSE){ var.val += string_f("replace_ellipse('%u',%u,%u,%u,%u,'%s',%u);\n",id,x,y,w_2,h_2,stroke_color,tmp_box_type); }else if (type == TYPE_RECT){ var.val += string_f("replace_rect('%u',%d,%d,%u,%u,'%s',%u);\n",id,(int)x-w_2,(int)y-h_2,width,height,stroke_color,tmp_box_type); }else if (type == TYPE_LINE){ var.val += string_f("replace_typeline('e%u',%u,%u,%u,%u,'%s',%u);\n",id,x,y,x1,y1,stroke_color,tmp_box_type); }else if (type == TYPE_TEXT){ var.val += string_f("replace_typetext('e%u',%u,%u,'%s');\n",id,getadjx(),y,stroke_color); }else{ } unsigned fontsize = compute_fontsize(browser_fontsize,default_textsize); unsigned textx,texty; gettextpos(textx,texty,fontsize); var.val += string_f("replace_text('t%u',%u,%u,'%u','%s','%s');\n" ,id,textx,texty,fontsize,"black",documentd_escape(txt).c_str()); } void changetextpos(){ textpos = (WHITEBOARD_TEXTPOS)(textpos+1); if (textpos == TEXTPOS_LAST){ if (type == TYPE_TEXT){ textpos = (WHITEBOARD_TEXTPOS)(TEXTPOS_INSIDE+1); }else{ textpos = TEXTPOS_INSIDE; } } } void changetextsize(int incr){ textsize += incr; } bool is_inside(unsigned posx, unsigned posy, unsigned browser_fontsize, unsigned textsize) const; }; // Return true if a coordinate posx,posy lies inside the element. bool WHITEBOARD_ELM::is_inside(unsigned posx, unsigned posy, unsigned browser_fontsize, unsigned textsize) const { bool ret = false; auto w_2 = width/2; auto h_2 = height/2; switch(type){ case TYPE_TEXT: case TYPE_ELLIPSE: case TYPE_RECT: // tlmp_warning ("x=%u x0=%d x1=%d y=%u y0=%d y1=%u",x,elm.x-elm.len,elm.x+elm.len,y,elm.y-elm.len,elm.y+elm.len); if ((int)posx > x-w_2 && (int)posx < x+w_2 && (int)posy > y-h_2 && (int)posy < y+h_2){ ret = true; } break; case TYPE_LINE: /* Selecting a line is harder for the user. Here is the logic 1. We compute the angle of the line: angle 2. We compute the len of the line: len 3. We compute the angle of the line made from the start of the line to the pointer position x,y: angle2 4. We compute the len of this new line: len2 5. We compute the angle of a triangle rectangle made this way. We use len2 as the base and 5 as the side. This angle represent the how far away the user can click off the line. This is max_angle_diff. */ { int diffx = x1 - x; int diffy = y1 - y; double angle = atan2(diffy,diffx); //tlmp_warning ("diffy=%d diffx=%d angle=%lf",diffy,diffx,angle); double len = sqrt(diffx*diffx+diffy*diffy); diffx = posx - x; diffy = posy - y; double angle2 = atan2(diffy,diffx); double len2 = sqrt(diffx*diffx+diffy*diffy); //tlmp_warning ("diffy=%d diffx=%d angle2=%lf",diffy,diffx,angle2); //tlmp_warning ("angle=%lf angle2=%lf fabs=%lf len=%lf len2=%lf",angle,angle2,fabs(angle2-angle),len,len2); double max_angle_diff = atan2 (5,len2); //tlmp_warning ("max_angle_diff=%lf",max_angle_diff); if (len2 <= len && fabs(angle2-angle) < max_angle_diff){ //tlmp_warning ("found line"); ret = true; } } break; case TYPE_HANDLINE: break; } // We can click on the text of an element if (!ret && txt.size() > 0){ unsigned textx1,texty2,lentxt; unsigned fontsize = compute_fontsize(browser_fontsize,textsize); gettextpos(textx1,texty2,lentxt,fontsize); unsigned textx2 = textx1 + lentxt; unsigned texty1 = texty2 - fontsize; if (posx >= textx1 && posx <= textx2 && posy >= texty1 && posy <= texty2){ ret = true; } } return ret; } struct WHITEBOARD_PREF{ bool selecting = true; bool starmode = false; // We are attaching more items to another. bool imbedmode = false; // We are inserting items inside another. MOD_KBD mod; unsigned lastx = 0; // Last mouse position, converted in SVG viewbox coordinates unsigned lasty = 0; unsigned viewbox_x = 0; unsigned viewbox_y = 0; }; #define DEFAULT_TEXT_SIZE 15; class WHITEBOARD: public GAME{ map elms; vector baseelms; map prefs; // Per session state unsigned alloc_id=0; unsigned textsize=DEFAULT_TEXT_SIZE; // default textsize x 10 (so 10 is 1em). unsigned gridcell=0; // Grid unit (0 = no grid) void resetsel(WHITEBOARD_SELMODE, set &ids); void redraw(const set &ids, VARVAL &var, unsigned fontsize); void alloc_elm(WHITEBOARD_ELM &elm, WHITEBOARD_ELMTYPE type, unsigned x0, unsigned y0){ elm.selmode = SEL_SELECTED; elm.id = alloc_id++; elm.type = type; elm.x = x0; elm.y = y0; } void add_element(const vector &star_elms, const vector &imbed_elms, WHITEBOARD_ELM &newelm){ if (star_elms.size() > 0){ for (auto p:star_elms) p->subelms.emplace_back(newelm.id,ARROW_END); }else if (imbed_elms.size() > 0){ for (auto p:imbed_elms) p->insides.push_back(newelm.id); }else{ baseelms.push_back(newelm.id); } elms[newelm.id] = move(newelm); } unsigned add_ellipse (const vector &star_elms, const vector &imbed_elms, unsigned x, unsigned y, unsigned x_ray, unsigned y_ray){ WHITEBOARD_ELM elm; alloc_elm(elm,TYPE_ELLIPSE,x,y); elm.width = x_ray*2; elm.height = y_ray*2; elm.selmode = SEL_SELECTED; unsigned ret = elm.id; add_element(star_elms,imbed_elms,elm); return ret; } unsigned add_rect (const vector &star_elms, const vector &imbed_elms, unsigned x, unsigned y, unsigned width, unsigned height){ WHITEBOARD_ELM elm; alloc_elm(elm,TYPE_RECT,x,y); elm.width = width; elm.height = height; unsigned ret = elm.id; add_element(star_elms,imbed_elms,elm); return ret; } unsigned add_line (unsigned x0, unsigned y0, unsigned x1, unsigned y1){ WHITEBOARD_ELM elm; alloc_elm(elm,TYPE_LINE,x0,y0); elm.x1 = x1; elm.y1 = y1; unsigned ret = elm.id; elms[ret] = move(elm); baseelms.push_back(ret); return ret; } unsigned add_handline (unsigned x0, unsigned y0, unsigned x1, unsigned y1){ WHITEBOARD_ELM elm; alloc_elm(elm,TYPE_HANDLINE,x0,y0); elm.x1 = x1; elm.y1 = y1; unsigned ret = elm.id; elms[ret] = move(elm); baseelms.push_back(ret); return ret; } unsigned add_text (const vector &imbed_elms, unsigned x0, unsigned y0, unsigned width){ WHITEBOARD_ELM elm; alloc_elm(elm,TYPE_TEXT,x0,y0); elm.width = width; elm.height = 8; elm.textpos = TEXTPOS_RIGHT; unsigned ret = elm.id; vector empty; add_element(empty,imbed_elms,elm); return ret; } WHITEBOARD_ELM *locate (unsigned x, unsigned y, unsigned fontsize); vector findselected (WHITEBOARD_SELMODE sel); void execstep (const char *var, const char *val, const DOC_CONTEXT &ctx, const DOC_UI_SPECS_receive &sp, VARVAL &script_var, VARVAL ¬ify_var, set ¬ify_ids, std::vector &res); void changeline (const vector &parents, const vector &elms, int direction, ARROW_TYPE arrow, VARVAL ¬ify_var, string &error); void changeline(int direction, string &error, VARVAL ¬ify_var, set ¬ify_ids); void delete_elm(unsigned id, VARVAL ¬ify_var); unsigned place_newelm(WHITEBOARD_ELMTYPE type, unsigned x, unsigned y, unsigned width, unsigned height, vector &star_elms, vector &imbed_elms, VARVAL ¬ify_var, set ¬ify_ids); void sortsubelms(set ¬ify_ids); void setfocus(VARVAL &var); std::string draw_board (unsigned vx, unsigned vy, unsigned svg_height, unsigned board_width, unsigned board_height, unsigned fontsize, unsigned docnum, bool editmode, string &script); std::string define_functions(const DOC_CONTEXT &ctx, const WHITEBOARD_PREF &pref, unsigned board_width, unsigned board_height); std::string define_styles(const DOC_CONTEXT &ctx, const DOC_UI_SPECS_receive &sp); bool label_oper(PARAM_STRING label, set ¬ify_ids, function); void remove_foreign (WHITEBOARD_ELM &elm, VARVAL ¬ify_var); void remove_assign(const vector &selects, set ¬ify_ids, VARVAL ¬ify_var); public: friend void walkboard(class _F_walkboard &c, WHITEBOARD *board); void save(DOC_WRITER &w, bool); void load(DOC_READER &r, std::string &msg); void resetgame(); WHITEBOARD(); const char *getclass() const; void exec (const char *var, const char *val, const DOC_CONTEXT &ctx, const DOC_UI_SPECS_receive &sp, std::vector &res, std::vector &unotifies); void manyexec (const vector &steps, const DOC_CONTEXT &ctx, const DOC_UI_SPECS_receive &sp, vector &res, std::vector &unotifies); void remove_session (const char *session); void set_embed_options (const DOCUMENT_EMBED &embed, bool not_square); vector get_embed_specs() const; void move_lower_bullets(WHITEBOARD_ELM &be_del, unsigned fontsize, set ¬ify_ids); }; vector WHITEBOARD::get_embed_specs() const { vector ret; for (auto &e:elms){ auto &ee = e.second; if (ee.is_imbed()){ ret.push_back(ee.embed); } } return ret; } void WHITEBOARD::set_embed_options (const DOCUMENT_EMBED &embed, bool not_square) { for (auto &e:elms){ auto &ee = e.second; if (ee.is_imbed() && ee.embed == embed){ ee.embed.not_square = not_square; } } } GAME_P make_WHITEBOARD() { return make_shared(); } WHITEBOARD::WHITEBOARD() { } void WHITEBOARD::save(DOC_WRITER &w, bool) { documentd_whiteboard_header3(&w,revision,alloc_id,textsize,gridcell); vector elements; for (auto &m:elms){ WHITEBOARD_ELEMENT3 elm; WHITEBOARD_ELM &e = m.second; elm.id = e.id; elm.selmode = e.selmode; elm.box_type = e.box_type; elm.x = e.x; elm.y = e.y; elm.x1 = e.x1; elm.y1 = e.y1; elm.width = e.width; elm.height = e.height; elm.type = e.type; elm.label = e.label; elm.txt = e.txt; elm.textpos = e.textpos; elm.insides = e.insides; for (auto &s:e.subelms){ WHITEBOARD_SUBELM selm; selm.id = s.id; selm.arrow = s.arrow; elm.subelms.emplace_back(move(selm)); } elm.document = e.embed.document; elm.region = e.embed.region; elm.docnum = e.embed.docnum; elm.image = e.imageurl; elm.textsize = e.textsize; elements.emplace_back(move(elm)); } documentd_whiteboard_elements3 (&w,elements); documentd_whiteboard_baseelms (&w,baseelms); vector wprefs; for (auto &p:prefs){ WHIPREF wp; wp.session = p.first; wp.selecting = p.second.selecting; wp.starmode = p.second.starmode; wp.imbedmode = p.second.imbedmode; wp.mod_shift = p.second.mod.shift; wp.mod_ctrl = p.second.mod.ctrl; wp.mod_alt = p.second.mod.alt; wp.lastx = p.second.lastx; wp.lasty = p.second.lasty; wp.viewbox_x = p.second.viewbox_x; wp.viewbox_y = p.second.viewbox_y; wprefs.emplace_back(move(wp)); } documentd_whiteboard_prefs(&w,wprefs); vector schat; documentd_copychat (schat,chat); documentd_whiteboard_chat(&w,schat); } void WHITEBOARD::load(DOC_READER &r, std::string &msg) { resetgame(); glocal alloc_id; glocal revision; glocal textsize; glocal gridcell; glocal elms; glocal baseelms; glocal chat; glocal prefs; chat.clear(); prefs.clear(); (&r); glocal.revision = revision; glocal.alloc_id = alloc_id; glocal.revision = revision; glocal.alloc_id = alloc_id; glocal.textsize = textsize; glocal.revision = revision; glocal.alloc_id = alloc_id; glocal.textsize = textsize; glocal.gridcell = gridcell; for (auto &e:elements){ WHITEBOARD_ELM elm; elm.id = e.id; elm.selmode = e.selmode; elm.box_type = e.box_type; elm.x = e.x; elm.y = e.y; elm.x1 = e.x1; elm.y1 = e.y1; elm.width = e.width; elm.height = e.height; elm.type = (WHITEBOARD_ELMTYPE)e.type; elm.label = e.label; elm.txt = e.txt; elm.textpos = e.textpos; elm.insides = e.insides; for (auto &s:e.subelms) elm.subelms.emplace_back(s.id,s.arrow); glocal.elms[elm.id] = move(elm); } for (auto &e:elements){ WHITEBOARD_ELM elm; elm.id = e.id; elm.selmode = e.selmode; elm.box_type = e.box_type; elm.x = e.x; elm.y = e.y; elm.x1 = e.x1; elm.y1 = e.y1; elm.width = e.width; elm.height = e.height; elm.type = (WHITEBOARD_ELMTYPE)e.type; elm.label = e.label; elm.txt = e.txt; elm.textpos = e.textpos; elm.insides = e.insides; for (auto &s:e.subelms) elm.subelms.emplace_back(s.id,s.arrow); elm.embed.document = e.document; elm.embed.region = e.region; elm.embed.docnum = e.docnum; elm.imageurl = e.image; glocal.elms[elm.id] = move(elm); } for (auto &e:elements){ WHITEBOARD_ELM elm; elm.id = e.id; elm.selmode = e.selmode; elm.box_type = e.box_type; elm.x = e.x; elm.y = e.y; elm.x1 = e.x1; elm.y1 = e.y1; elm.width = e.width; elm.height = e.height; elm.type = (WHITEBOARD_ELMTYPE)e.type; elm.label = e.label; elm.txt = e.txt; elm.textpos = e.textpos; elm.insides = e.insides; for (auto &s:e.subelms) elm.subelms.emplace_back(s.id,s.arrow); elm.embed.document = e.document; elm.embed.region = e.region; elm.embed.docnum = e.docnum; elm.imageurl = e.image; elm.textsize = e.textsize; glocal.elms[elm.id] = move(elm); } glocal.baseelms = baseelms; for (auto &p:prefs){ WHITEBOARD_PREF wp; wp.selecting = p.selecting; wp.starmode = p.starmode; wp.imbedmode = p.imbedmode; wp.mod.shift = p.mod_shift; wp.mod.ctrl = p.mod_ctrl; wp.mod.alt = p.mod_alt; wp.lastx = p.lastx; wp.lasty = p.lasty; wp.viewbox_x = p.viewbox_x; wp.viewbox_y = p.viewbox_y; glocal.prefs[p.session] = wp; } for (auto l:lines) glocal.chat.emplace_back(l.time,l.line); tlmp_error ("Invalid record while reading whiteboard file: %s",msg); set notify_ids; sortsubelms(notify_ids); } enum WHITEBOARD_QUARTER{ QUARTER_RIGHT, QUARTER_TOP, QUARTER_LEFT, QUARTER_BOTTOM }; #define _TLMP_walkboard struct _F_walkboard{ #define _F_walkboard_element(x) void x element(WHITEBOARD_ELM &elm, WHITEBOARD_ELM *parent, ARROW_TYPE arrow, bool firstseen, \ WHITEBOARD_QUARTER quarter, unsigned nosubelm, unsigned nbsubelm, bool &end) virtual _F_walkboard_element( )=0; }; void walkboard(_F_walkboard &c, WHITEBOARD *board) { set seen; bool end = false; for (auto &e:board->elms){ auto &elm = e.second; { bool firstseen = seen.insert(elm.id).second; c.element(elm,nullptr,ARROW_NONE,firstseen,QUARTER_RIGHT,0,0,end); } WHITEBOARD_QUARTER quarter = QUARTER_RIGHT; for (auto &arrows:elm.arrows){ for (unsigned pos = 0; pos < arrows.size(); pos++){ auto &subelm = arrows[pos]; if (subelm.arrow != ARROW_NONE){ // arrows contain entries for elm -> child and for parent -> elm bool firstseen = seen.insert(subelm.id).second; c.element(board->elms[subelm.id],&elm,subelm.arrow,firstseen,quarter,pos,arrows.size(),end); } } quarter = (WHITEBOARD_QUARTER)(quarter+1); } } } /* Unselect the selected elements. ids will contain the id of selected elements. */ void WHITEBOARD::resetsel(WHITEBOARD_SELMODE selmode, set &ids) { glocal ids; glocal selmode; (this); if (elm.selmode == glocal.selmode){ elm.selmode = SEL_NONE; glocal.ids.insert(elm.id); } } struct ARROW_NUM{ unsigned pos; // Position number of the arrow unsigned num; // Number of arrows }; static ARROW_NUM arrow_pos (WHITEBOARD_ELM &elm, WHITEBOARD_QUARTER quarter, unsigned id) { ARROW_NUM ret; auto &arrows = elm.arrows[quarter]; ret.num = arrows.size()+1; for (ret.pos = 0; ret.pos < ret.num; ret.pos++){ if (arrows[ret.pos].id == id){ ret.pos++; return ret; } } tlmp_warning ("arrow_pos not found elm.id=%u",elm.id); ret.pos++; return ret; } struct LINE_SPEC{ unsigned x; unsigned y; unsigned len; double angle; LINE_SPEC (WHITEBOARD_ELM &parent, WHITEBOARD_ELM &elm, WHITEBOARD_QUARTER quarter, unsigned nosubelm, unsigned nbsubelm){ int elm_x=0; int elm_y=0; x = y = 0; // tlmp_warning ("quarter=%d nosubelm=%u nbsubelm=%u",quarter,nosubelm,nbsubelm); if (quarter == QUARTER_RIGHT){ x = parent.x + parent.width/2; y = parent.y + parent.height/2 - parent.height/(nbsubelm+1)*(nosubelm+1); auto pos = arrow_pos(elm,QUARTER_LEFT,parent.id); elm_x = elm.x - elm.width/2; elm_y = elm.y - elm.height/2 + pos.pos*elm.height/pos.num; }else if (quarter == QUARTER_TOP){ x = parent.x + parent.width/2 - parent.width/(nbsubelm+1)*(nosubelm+1); y = parent.y - parent.height/2; auto pos = arrow_pos(elm,QUARTER_BOTTOM,parent.id); elm_x = elm.x - elm.width/2 + pos.pos*elm.width/pos.num; elm_y = elm.y + elm.height/2; }else if (quarter == QUARTER_BOTTOM){ x = parent.x - parent.width/2 + parent.width/(nbsubelm+1)*(nosubelm+1); y = parent.y + parent.height/2; auto pos = arrow_pos(elm,QUARTER_TOP,parent.id); elm_x = elm.x + elm.width/2 - pos.pos*elm.width/pos.num; elm_y = elm.y - elm.height/2; }else if (quarter == QUARTER_LEFT){ x = parent.x - parent.width/2; y = parent.y - parent.height/2 + parent.height/(nbsubelm+1)*(nosubelm+1); auto pos = arrow_pos(elm,QUARTER_RIGHT,parent.id); elm_x = elm.x + elm.width/2; elm_y = elm.y + elm.height/2 - pos.pos*elm.height/pos.num; } int diff_y = elm_y - y; int diff_x = elm_x - x; angle = atan2(diff_y,diff_x)/M_PI*180; len = sqrt(diff_y*diff_y+diff_x*diff_x); } }; /* Sort the sub-elements by angle. The goal is to organized in walkboard object into four quadrants (right,top,left,bottom). The angle computed here starts at -angle_side degres. So values from 0 to 90 actually represent sub-elements to the right. */ void WHITEBOARD::sortsubelms(set ¬ify_ids) { struct ARROWS{ vector arrows[4]; }; map olds; for (auto &e:elms){ auto &old = olds[e.first]; for (unsigned i=0; i<4; i++) old.arrows[i] = e.second.arrows[i]; for (auto &a:e.second.arrows) a.clear(); } for (auto &e:elms){ auto &parent = e.second; auto &subelms = parent.subelms; if (subelms.size() > 0){ int angle_ratio = atan2(parent.height,parent.width)/M_PI*1800; int angle_side = angle_ratio*2; int angle_top = (3600-2*angle_side)/2; int angle_left = angle_side + angle_top; int angle_bottom = angle_left + angle_side; // tlmp_warning ("angle_side=%d top=%d left=%d bottom=%d",angle_side,angle_top,angle_left,angle_bottom); for (auto &s:subelms){ auto &child = elms[s.id]; int diffy = child.y - parent.y; int diffx = child.x - parent.x; int angle = atan2(-diffy,diffx)/M_PI*1800+3600+angle_ratio; angle %= 3600; WHITEBOARD_QUARTER quarter = QUARTER_RIGHT; WHITEBOARD_QUARTER child_quarter = QUARTER_LEFT; if (angle >= angle_bottom){ quarter = QUARTER_BOTTOM; child_quarter = QUARTER_TOP; }else if (angle >= angle_left){ quarter = QUARTER_LEFT; child_quarter = QUARTER_RIGHT; }else if (angle >= angle_side){ quarter = QUARTER_TOP; child_quarter = QUARTER_BOTTOM; } parent.arrows[quarter].emplace_back(child.id,s.arrow,angle); child.arrows[child_quarter].emplace_back(parent.id,ARROW_NONE,(angle+1800)%3600); } } } for (auto &e:elms){ for (auto &a:e.second.arrows) sort (a.begin(),a.end(),[](auto &s1, auto &s2){ return s1.angle < s2.angle;}); } // Check if there are any changes for (auto &e:elms){ auto &old = olds[e.first]; for (unsigned i=0; i<4; i++){ if (old.arrows[i] != e.second.arrows[i]){ notify_ids.insert(e.first); break; } } } } /* Redraw all elments which have changed */ void WHITEBOARD::redraw(const set &ids, VARVAL &var, unsigned fontsize) { glocal ids; glocal var; glocal fontsize; glocal textsize; if (ids.size() > 0){ (this); if (glocal.ids.count(elm.id) > 0){ elm.redraw(glocal.var,glocal.fontsize,glocal.textsize); if (parent != nullptr){ LINE_SPEC spec(*parent,elm,quarter,nosubelm,nbsubelm); glocal.var.val += string_f("replace_line('l%u,%u',%u,%u,%lf,%u,'black',%u);\n" ,parent->id,elm.id,spec.x,spec.y,spec.angle,spec.len,arrow); } }else if (parent != nullptr && glocal.ids.count(parent->id)>0){ // The parent was moved, so we must redraw the line to each child. LINE_SPEC spec(*parent,elm,quarter,nosubelm,nbsubelm); glocal.var.val += string_f("replace_line('l%u,%u',%u,%u,%lf,%u,'black',%u);\n" ,parent->id,elm.id,spec.x,spec.y,spec.angle,spec.len,arrow); } } } /* Find all selected items (matching selmode) */ vectorWHITEBOARD::findselected(WHITEBOARD_SELMODE selmode) { vector ret; for (auto &e:elms){ if (e.second.selmode == selmode) ret.push_back(&e.second); } return ret; } void WHITEBOARD::resetgame() { elms.clear(); baseelms.clear(); alloc_id = 0; textsize = DEFAULT_TEXT_SIZE; } const char *WHITEBOARD::getclass() const { return "WHIT"; } /* Locate an element based on coordinates */ WHITEBOARD_ELM *WHITEBOARD::locate (unsigned x, unsigned y, unsigned fontsize) { glocal WHITEBOARD_ELM *ret = nullptr; glocal unsigned center_distance=(unsigned)-1; glocal x; glocal y; glocal fontsize; glocal textsize; (this); bool found = elm.is_inside(glocal.x,glocal.y,glocal.fontsize,glocal.textsize); if (found){ // An element may be on top of another and larger, so we won't be able to select it. // We note the distance from the center of the object to the point selected by the user. // If the distance is smaller, we pick this object. // We do not execute the square root, since we do not care about the exact distance. unsigned diffx = glocal.x-elm.x; unsigned diffy = glocal.y-elm.y; unsigned distance = diffx*diffx+diffy*diffy; if (distance < glocal.center_distance){ glocal.center_distance = distance; glocal.ret = &elm; } } // The following line is removed. We want the function to go deep so it locate objects inside objects. //if (glocal.ret != nullptr) end = true; return glocal.ret; } static void addsvgelement( VARVAL &var, const string &gameid, const char *elmtype, const char *color, unsigned stroke_width, PARAM_STRING id, WHITEBOARD_ELM *parent) { var.val += "whi_loop_board(function(svg){\n"; var.val += string_f("\tvar newElement = document.createElementNS('http://www.w3.org/2000/svg', '%s');\n",elmtype); var.val += string_f("\tnewElement.id='%s';\n",id.ptr); if (color != nullptr) var.val += string_f("\tnewElement.style.stroke = '%s';\n",color); if (strcmp(elmtype,"text")==0){ var.val += "\tnewElement.classList.add('whitext');\n"; }else if (stroke_width != 0){ var.val += "\tnewElement.style.fill = 'none';\n"; var.val += string_f("\tnewElement.style.strokeWidth = '%u';\n",stroke_width); } if (parent != nullptr){ var.val += string_f("\tnewElement.setAttribute('x',%u);\n",parent->x); var.val += string_f("\tnewElement.setAttribute('y',%u);\n",parent->y); var.val += string_f("\tnewElement.setAttribute('width',%u);\n",parent->width); var.val += string_f("\tnewElement.setAttribute('height',%u);\n",parent->height); } var.val += "\tsvg.appendChild(newElement);\n"; var.val += "\t});\n"; } /* Change the arrow type between parents and child (elms). In interactive mode, we cycle through the arrow type. The parameter direction is either -1 or 1 to control the direction of the cycling. When direction == 0, the arrow parameter force a specific arrow type. Note that ARROW_NONE is never really applied. The relation between parents and elms are removed. */ void WHITEBOARD::changeline ( const vector &parents, const vector &selects, int direction, ARROW_TYPE arrow, VARVAL ¬ify_var, string &error) { set has_parent; // All elements who are in parent.subelms[] vector lost_parent; // This element was disconnected from a parent // so it it does not has a parent, it // most be added to baseelms; for (auto &e:elms){ auto &elm = e.second; if (find(parents.begin(),parents.end(),&elm)!=parents.end()){ for (auto child:selects){ auto sub = find_if(elm.subelms.begin(),elm.subelms.end(),[child](auto &s){return s.id == child->id;}); if (sub == elm.subelms.end()){ if (direction != 0 || arrow != ARROW_NONE){ // This child is not related to the parent, so we add it // Before we check the parent is not already connected as a child of this child auto badsub = find_if(child->subelms.begin(),child->subelms.end(),[&elm](auto &s){return s.id == elm.id;}); if (badsub != child->subelms.end()){ error = MSG_U(E_INVALIDCONNECT,"Can't connect these elements because they are already connected: child is parent"); }else{ ARROW_TYPE type = arrow; if (direction != 0) type = direction==1 ? ARROW_END : ARROW_LINE; elm.subelms.emplace_back(child->id,type); // Remove it from baseelms if there auto base = find(baseelms.begin(),baseelms.end(),child->id); if (base != baseelms.end()) baseelms.erase(base); has_parent.insert(child->id); addsvgelement(notify_var,gameid,"g","black",2,string_f("l%u,%u",elm.id,child->id),nullptr); } } }else{ if (direction != 0){ sub->arrow = (ARROW_TYPE)((unsigned)sub->arrow+direction); }else{ sub->arrow = arrow; } if (is_any_of(sub->arrow,ARROW_NONE,ARROW_LAST)){ // We loop back to ARROW_NONE. // We just remove the entry elm.subelms.erase(sub); notify_var.val += string_f("removeelm('l%u,%u','g');\n",elm.id,child->id); // Is it owned by another elements ? If not we have to add it to baseelms. lost_parent.push_back(child->id); }else{ has_parent.insert(child->id); } } } } } for (auto id:lost_parent){ if (has_parent.count(id)==0){ // Add it to baseelms if not there auto base = find (baseelms.begin(),baseelms.end(),id); if (base == baseelms.end()) baseelms.push_back(id); } } } void WHITEBOARD::changeline(int direction, string &error, VARVAL ¬ify_var, set ¬ify_ids) { auto parents = findselected(SEL_STAR); auto selects = findselected(SEL_SELECTED); if (parents.size() == 0){ error = MSG_U(E_NOPARENT,"No parent node selected"); }else if (selects.size() == 0){ error = MSG_U(E_NONODE,"No node selected"); }else{ changeline (parents,selects,direction,ARROW_NONE,notify_var,error); for (auto s:selects) notify_ids.insert(s->id); } } /* Generate javascript to remove all traces of image or embed document associated with one whiteboard element. */ void WHITEBOARD::remove_foreign (WHITEBOARD_ELM &elm, VARVAL ¬ify_var) { notify_var.val += string_f("removeelm('f%u','foreignObject');\n",elm.id); if (elm.is_imbed()){ auto &embed = elm.embed; notify_var.val += string_f("removetemplate('temp%u');\n",embed.docnum); string sub_gameid = documentd_rel2abs(gameid,embed.document); notify_var.val += string_f("whi_remove('%s',%d);\n",sub_gameid.c_str(),embed.docnum); } } /* Delete one element. Remove all arrows related to this element (from and to). subelms becoming orphans (not connect to any parent) are added to baseelms. */ void WHITEBOARD::delete_elm (unsigned id, VARVAL ¬ify_var) { glocal id; glocal notify_var; glocal bool subelm_done = false; glocal set has_parent; // While delete this id // we collect the list of all id having a parent (this); if (elm.id == glocal.id){ if (!glocal.subelm_done){ glocal.subelm_done = true; for (auto &s:elm.subelms){ glocal.notify_var.val += string_f("removeelm('l%u,%u','g');\n",elm.id,s.id); } // The subelms become orphan or are owned by another elm.subelms.clear(); } if (parent != nullptr){ glocal.notify_var.val += string_f("removeelm('l%u,%u','g');\n",parent->id,elm.id); auto pt = find_if (parent->subelms.begin(),parent->subelms.end(),[&elm](auto &selm){return elm.id==selm.id;}); // tlmp_warning ("delete_elm pt=%d\n",pt != parent->subelms.end()); if (pt != parent->subelms.end()) parent->subelms.erase(pt); } } for (auto s:elm.subelms){ // tlmp_warning ("has_parent delete id=%d elm.id=%d -> s.id=%d\n",glocal.id,elm.id,s.id); glocal.has_parent.insert(s.id); } auto pt = elms.find(id); if (pt != elms.end()){ static const char *tbtag[]={"ellipse","rect","line","path","circle"}; notify_var.val += string_f("removeelm('e%u','%s');\n",id,tbtag[pt->second.type]); notify_var.val += string_f("removeelm('t%u','text');\n",id); if (pt->second.is_imbed() || pt->second.is_image()){ remove_foreign (pt->second,notify_var); } elms.erase(pt); // Remove from baseelms if there auto pt = find(baseelms.begin(),baseelms.end(),id); if (pt != baseelms.end()) baseelms.erase(pt); } // We have to add the orphan sub-elements into baseelms for (auto &e:elms){ if (glocal.has_parent.count(e.first)==0 && find(baseelms.begin(),baseelms.end(),e.first)==baseelms.end()){ baseelms.push_back(e.first); } } set tmp; sortsubelms (tmp); } static void whiteboard_update_button (string &script, bool oldstate, bool newstate, unsigned button_id) { if (newstate != oldstate){ script += string_f("var button = document.getElementById('button%u');\n",button_id); script += "if (button != null){\n"; script += string_f("\tbutton.style.background='%s';\n",newstate ? "lightblue" : "lightgray"); script += "}\n"; } } void WHITEBOARD::exec ( const char *var, const char *val, const DOC_CONTEXT &ctx, const DOC_UI_SPECS_receive &sp, std::vector &res, std::vector &unotifies) { tlmp_error ("WHITEBOARD::exec called"); } unsigned WHITEBOARD::place_newelm( WHITEBOARD_ELMTYPE type, unsigned x, unsigned y, unsigned width, unsigned height, vector &star_elms, vector &imbed_elms, VARVAL ¬ify_var, set ¬ify_ids) { const char *svgtype = ""; unsigned id = 0; bool addline = false; unsigned width_2 = width/2; unsigned height_2 = height/2; unsigned stroke_width = 2; if (type == TYPE_ELLIPSE){ id = add_ellipse(star_elms,imbed_elms,x,y,width_2,height_2); svgtype = "ellipse"; addline = star_elms.size() > 0; }else if (type == TYPE_RECT){ id = add_rect(star_elms,imbed_elms,x,y,width,height); svgtype = "rect"; addline = star_elms.size() > 0; }else if (type == TYPE_LINE){ id = add_line(x,y,x+width,y+height); svgtype = "line"; }else if (type == TYPE_HANDLINE){ stroke_width = 4; id = add_handline(x,y,x+width,y+height); svgtype = "path"; }else if (type == TYPE_TEXT){ id = add_text(imbed_elms,x,y,width); svgtype = "circle"; } addsvgelement(notify_var,gameid,svgtype,"red",stroke_width,string_f("e%u",id),nullptr); addsvgelement(notify_var,gameid,"text","red",1,string_f("t%u",id),nullptr); if (addline){ for (auto p:star_elms) addsvgelement(notify_var,gameid,"g","black",stroke_width,string_f("l%u,%u",p->id,id),nullptr); } notify_ids.insert(id); return id; } /* Limit move in the board grid */ static void limit_scroll (unsigned &pos, int move, unsigned board_size, unsigned boards) { const unsigned max_pos = (boards-1)*board_size; if (move < 0){ if ((int)pos > -move){ pos += move; }else{ pos = 0; } }else if (pos + move < max_pos){ pos += move; }else{ pos = max_pos; } } void WHITEBOARD::setfocus(VARVAL &var) { documentd_setfocus(var,string_f("text-%s",gameid.c_str())); } static string whiteboard_converturl(PARAM_STRING gameid, PARAM_STRING url) { string ret = url.ptr; if (!is_start_any_ofnc(url.ptr,NONEED,"http://","https://")){ string abspath = documentd_rel2abs(gameid,url); ret = documentd_escape(string_f("/index.hc?webstep=5&image=%s",abspath.c_str())); } return ret; } string WHITEBOARD::draw_board ( unsigned vx, // Viewbox specs unsigned vy, unsigned svg_height, unsigned board_width, unsigned board_height, unsigned fontsize, unsigned docnum, bool editmode, // Enable onXXXX function string &script) { glocal fontsize; glocal textsize; glocal string val; glocal script; glocal gameid; script += string_f("doc_cur_gameid='%s';\n",gameid.c_str()); // Generate JS objects script += string_f( "var whi_%u={\n" "\tgameid:'%s',docnum:%u,vboxx:%u,vboxy:%u\n" "};\n" "whi_%u.gameselect=function(event){\n" "\twhi_gameselect(this.gameid,this.docnum,this.vboxx,this.vboxy,%u,%u,event);\n" "};\n" "whi_%u.gamemove=function(event){\n" "\twhi_gamemove(this.gameid,this.docnum,this.vboxx,this.vboxy,%u,%u,event);\n" "};\n" "whi_%u.setviewbox=function(vx,vy){\n" "\tthis.vboxx=vx;\n" "\tthis.vboxy=vy;\n" "\twhi_setviewbox(vx,vy);\n" "};\n" ,docnum ,gameid.c_str(),docnum,vx,vy ,docnum ,board_width,board_height ,docnum ,board_width,board_height ,docnum); script += string_f("doc_lst.push(whi_%u);\n",docnum); string onfuncs; if (editmode){ onfuncs = string_f(" onmousedown='whi_%u.gameselect(event);'" " onmouseup='whi_gamemouseup(event); return false;'" " onwheel='whi_gamewheel(event); return false;'" " onmousemove='whi_%u.gamemove(event); return false;'" ,docnum,docnum); } // Same computation as define_function() unsigned svg_width = board_width*svg_height/board_height; // tlmp_warning ("draw %u %u %u %u\n",vx,vy,width,height); glocal.val += string_f("\n" ,gameid.c_str(),docnum,vx,vy,svg_width,svg_height ,onfuncs.c_str()); // Draw the grid. The grid is drawn as 2 SVG paths. // By default, the lines are white. When we turn the two paths on, we get the finest grid // When we turn only one on, we get the coarse grid. { const char *tbcol[2]={"none","none"}; if (gridcell == 20){ tbcol[0] = "lightgray"; tbcol[1] = "lightgray"; }else if (gridcell == 40){ tbcol[0] = "lightgray"; } for (unsigned i=0; i<2; i++){ string p = string_f("\n"; glocal.val += p; } } (this); if (firstseen){ const char *color = tbcolor[elm.selmode]; auto box_type = elm.box_type; if (box_type == BOX_HIDDEN){ box_type = BOX_SOLID; if (elm.selmode == SEL_NONE) color = "none"; } static const char *tbdash[]={ ""," stroke-dasharray='5,5'"," stroke-dasharray='2,5'" }; const char *dasharray = tbdash[box_type]; switch(elm.type){ case TYPE_ELLIPSE: glocal.val += string_f("\n" ,elm.id,elm.x,elm.y,elm.width/2,elm.height/2,color,dasharray); if (elm.is_image()){ glocal.val += string_f("\n" ,elm.id,elm.x-elm.width/2,elm.y-elm.height/2,elm.width,elm.height); string paramurl = whiteboard_converturl (glocal.gameid,elm.imageurl); glocal.val += string_f("\n",paramurl.c_str()); glocal.val += "\n"; } break; case TYPE_RECT: glocal.val += string_f("\n" ,elm.id,(int)elm.x-elm.width/2,(int)elm.y-elm.height/2,elm.width,elm.height,color,dasharray); if (elm.is_imbed() || elm.is_image()){ unsigned docnum = elm.get_docnum(); glocal.val += string_f("\n" ,elm.id,elm.x-elm.width/2,elm.y-elm.height/2,elm.width,elm.height); if (elm.is_image()){ string paramurl = whiteboard_converturl (glocal.gameid,elm.imageurl); glocal.val += string_f("\n",paramurl.c_str()); } glocal.val += "\n"; if (elm.is_imbed()) glocal.script += string_f("whi_show_template(document,'temp%u','f%u');\n",docnum,elm.id); } break; case TYPE_LINE: glocal.val += string_f("\n" ,elm.id,elm.x,elm.y,elm.x1,elm.y1,color,dasharray); break; case TYPE_HANDLINE: glocal.val += string_f("\n" ,elm.id,color,elm.x,elm.y,elm.x1,elm.y1); break; case TYPE_TEXT: // The width is used to position the text, and the bullet, but the bullet is always 8 large glocal.val += string_f("\n" ,elm.id,color,color,elm.getadjx(),elm.y,8/2); } unsigned textx,texty; unsigned fontsize = elm.compute_fontsize(glocal.fontsize,glocal.textsize); elm.gettextpos(textx,texty,fontsize); glocal.val += string_f("%s\n" ,elm.id,textx,texty,"black","black",fontsize,documentd_escape(elm.txt).c_str()); } if (parent != nullptr){ LINE_SPEC spec(*parent,elm,quarter,nosubelm,nbsubelm); glocal.val += string_f("\n",parent->id,elm.id); glocal.script += string_f("replace_line('l%u,%u',%u,%u,%lf,%u,'black',%u);\n" ,parent->id,elm.id,spec.x,spec.y,spec.angle,spec.len,arrow); } glocal.val += "\n"; return glocal.val; } const unsigned SVG_BOARD_WIDTH=1000; // SVG viewbox specs const unsigned SVG_BOARD_HEIGHT=1000; /* Create the javascript functions */ string WHITEBOARD::define_functions( const DOC_CONTEXT &ctx, const WHITEBOARD_PREF &pref, unsigned board_width, unsigned board_height) { string val; // We have to respect the ratio of the board to specify // the width of the svg viewport // Same computation is done in draw_board // We consider that the height of the board always match SVG_BOARD_HEIGHT. // Only the width is computed. unsigned svg_board_width = board_width*SVG_BOARD_HEIGHT/board_height; // Clicking on the board val += "window.mbuttons=[0,0,0];\n"; val += "window.clickedWhen=0;\n"; val += "window.whi_gameselect = function(gameid,docnum,vboxx,vboxy,width,height,event){\n"; val += "\tvar d = new Date();\n"; val += "\tclickedWhen = d.getTime();\n"; val += "\tmbuttons[event.which-1] = 1;\n"; val += "\tvar elm = document.getElementById('board-'+gameid+','+docnum);\n"; val += "\tvar rect = elm.getBoundingClientRect();\n"; val += string_f("\tvar x = (event.clientX-rect.left)*%u/width + vboxx;\n",svg_board_width); val += string_f("\tvar y = (event.clientY-rect.top)*%u/height + vboxy;\n",SVG_BOARD_HEIGHT); val += "\tgameaction(event,'select:'+x+','+y+ ','+event.which+','+event.shiftKey+','+event.ctrlKey);\n"; val += "\tevent.stopPropagation();\n"; val += "\tevent.preventDefault();\n"; val += string_f("\tdocument.getElementById('text-%s').focus();\n",gameid.c_str()); val += "}\n"; val += "function whi_gamemouseup(event){\n"; val += "\tmbuttons[event.which-1] = 0;\n"; val += "}\n"; // Mouse wheel val += "window.whi_gamewheel = function(event){\n" "\tif (event.deltaY < 0){\n" "\t\tgameaction(event,'wheel:1,'+event.shiftKey+','+event.ctrlKey);\n" "\t}else{\n" "\t\tgameaction(event,'wheel:-1,'+event.shiftKey+','+event.ctrlKey);\n" "\t}\n" "\tevent.stopPropagation();\n" "}\n"; // Moving an object, wait some time after the click to perform the move (200ms). // So when a user just select an object, it does not move. val += "window.whi_gamemove = function(gameid,docnum,vboxx,vboxy,width,height,event){\n"; val += "\tvar d = new Date();\n"; val += "\tif(mbuttons[0] == 1 && d.getTime()-clickedWhen > 200){\n"; val += "\t\tvar elm = document.getElementById('board-'+gameid+','+docnum);\n"; val += "\t\tvar rect = elm.getBoundingClientRect();\n"; val += string_f("\t\tvar x = (event.clientX-rect.left)*%u/width + vboxx;\n",svg_board_width); val += string_f("\t\tvar y = (event.clientY-rect.top)*%u/height + vboxy;\n",SVG_BOARD_HEIGHT); val += "\t\tgameaction(event,'mousemove:'+x+','+y+','+event.shiftKey+','+event.ctrlKey);\n"; val += "\t}\n"; val += "\tevent.stopPropagation();\n"; val += "\tevent.preventDefault();\n"; val += "}\n"; val += documentd_js_loop_function("board","whi",ctx.docnum); // Updating an object val += "window.setboxtype = function(e,boxtype){\n" "\tif(boxtype==0){\n" "\t\te.removeAttribute('stroke-dasharray');\n" "\t}else if(boxtype==1){\n" "\t\te.setAttribute('stroke-dasharray','5,5');\n" "\t}else if(boxtype==2){\n" "\t\te.setAttribute('stroke-dasharray','2,5');\n" "\t}\n" "}\n"; val += "window.whi_setviewbox = function(x, y){\n"; val += string_f("\tvar e=document.getElementById('board-%s,%u');\n",gameid.c_str(),ctx.docnum); val += string_f("\te.setAttribute('viewBox',' ' + x + ' ' + y + ' %u %u');\n",svg_board_width,SVG_BOARD_HEIGHT); val += string_f("\tvar e=document.getElementById('vscroll-%s');\n",gameid.c_str()); val += string_f("\te.setAttribute('y',y*%u/%u);\n",board_height,4*SVG_BOARD_HEIGHT); val += string_f("\tvar e=document.getElementById('hscroll-%s');\n",gameid.c_str()); val += string_f("\te.setAttribute('x',x*%u/%u);\n",board_width,4*svg_board_width); val += "}\n"; // Remove all children from a parent val += "window.whi_remove_children = function(parent){\n" "\twhile (parent.firstChild) {\n" "\t\tparent.removeChild(parent.lastChild);\n" "\t}\n" "}\n"; val += "window.replace_foreign = function(id, x, y, w,h){\n" // "\tconsole.log (`replace_foreign id=${id} doc_lst.length=${doc_lst.length}`);\n" "\twhi_loop('foreignObject','f'+id,function(e,chs){\n" // "\t\tconsole.log (`fct foreign gameid=${chs.gameid} docnum=${chs.docnum} id=${id} x=${x} y=${y} width=${w} height=${h}`);\n" "\t\te.setAttribute('x',x);\n" "\t\te.setAttribute('y',y);\n" "\t\te.setAttribute('width',w);\n" "\t\te.setAttribute('height',h);\n" "\t});\n" "}\n"; val += "window.replace_ellipse = function(id, x, y, rx,ry,color,boxtype){\n" "\twhi_loop('ellipse','e'+id,function(e){\n" // "\t\tconsole.log ('fct ellipse id='+id);\n" "\t\te.setAttribute('cx',x);\n" "\t\te.setAttribute('cy',y);\n" "\t\te.setAttribute('rx',rx);\n" "\t\te.setAttribute('ry',ry);\n" "\t\te.style.stroke=color;\n" "\t\tsetboxtype(e,boxtype);\n" "\t});\n" // Move the foreignobject (HTML associated with the ellipse "\treplace_foreign(id,x-rx,y-ry,rx*2,ry*2);\n" "}\n"; val += "window.replace_rect = function(id, x, y, w,h, color,boxtype){\n" "\twhi_loop('rect','e'+id,function(e){\n" // "\t\tconsole.log ('fct rect id='+id);\n" "\t\te.setAttribute('x',x);\n" "\t\te.setAttribute('y',y);\n" "\t\te.setAttribute('rx',5);\n" "\t\te.setAttribute('ry',5);\n" "\t\te.setAttribute('width',w);\n" "\t\te.setAttribute('height',h);\n" "\t\te.style.stroke=color;\n" "\t\tsetboxtype(e,boxtype);\n" "\t});\n" // Move the foreignobject (HTML associated with the rect "\treplace_foreign(id,x,y,w,h);\n" "}\n"; val += "window.replace_typeline = function(id, x1, y1, x2, y2, color,boxtype){\n" "\twhi_loop('line',id,function(e){\n" "\t\te.setAttribute('x1',x1);\n" "\t\te.setAttribute('y1',y1);\n" "\t\te.setAttribute('x2',x2);\n" "\t\te.setAttribute('y2',y2);\n" "\t\te.style.stroke=color;\n" "\t\tsetboxtype(e,boxtype);\n" "\t});\n" "}\n"; val += "window.replace_typetext = function(id, x, y, color){\n" "\twhi_loop('circle',id,function(e){\n" "\t\te.setAttribute('cx',x);\n" "\t\te.setAttribute('cy',y);\n" "\t\te.setAttribute('r',4);\n" "\t\te.style.stroke=color;\n" "\t\te.style.fill=color;\n" "\t});\n" "}\n"; val += "window.replace_text = function(id, x, y, size,color, txt){\n" "\twhi_loop('text',id,function(e){\n" "\t\te.setAttribute('x',x);\n" "\t\te.setAttribute('y',y);\n" "\t\te.style.stroke=color;\n" "\t\te.style.fill=color;\n" "\t\te.style.fontSize=size;\n" "\t\te.innerHTML=txt;\n" "\t});\n" "}\n"; // line between a parent and sub-element val += "window.replace_line = function(id, x, y, angle, len, color,arrow){\n" "\twhi_loop('g',id,function(e){\n" "\t\twhi_remove_children(e);\n" "\t\te.style.stroke = color;\n" "\t\te.style.fill = color;\n" "\t\te.style.strokeWidth = '1';\n" "\t\tvar newElement = document.createElementNS('http://www.w3.org/2000/svg', 'path');\n" "\t\te.appendChild(newElement);\n" "\t\tlen-=10;\n" "\t\tif(arrow==1){\n" // ARROW_END "\t\t\tnewElement.setAttribute('d','M0,0 l'+(len-10)+',0 l-2,-5 l10,5 l-10,5 l2,-5');\n" "\t\t}else if(arrow==2){\n" // ARROW_START "\t\t\tnewElement.setAttribute('d','M0,0 l10,-5 l-2,5 l2,5 l-10,-5 l'+len+',0');\n" "\t\t}else if(arrow==3){\n" // ARROW_BOTH "\t\t\tnewElement.setAttribute('d','M0,0 l10,-5 l-2,5 l2,5 l-10,-5 l'+(len-10)+',0 l-2,-5 l10,5 l-10,5 l2,-5');\n" "\t\t}else if(arrow==4){\n" // ARROW_RETURN "\t\t\tnewElement.setAttribute('d','M0,0 l'+(len-10)+',0 l-2,-5 l10,5 l-10,5 l2,-5');\n" "\t\t\tvar path2 = document.createElementNS('http://www.w3.org/2000/svg', 'path');\n" "\t\t\te.appendChild(path2);\n" "\t\t\tpath2.style.fill = 'none';\n" "\t\t\tvar lenq=len-10;\n" "\t\t\tpath2.setAttribute('d','M'+lenq+',-7 q-'+(lenq/2)+',-20,-'+(lenq-10)+',0');\n" "\t\t\tvar subg = document.createElementNS('http://www.w3.org/2000/svg', 'g');\n" "\t\t\te.appendChild(subg);\n" "\t\t\tsubg.setAttribute('transform','rotate(-15,0,0)');\n" "\t\t\tvar path3 = document.createElementNS('http://www.w3.org/2000/svg', 'path');\n" "\t\t\tpath3.setAttribute('d','M10,-5 l2,-5 l-10,5 l10,5 l-2,-5');\n" "\t\t\tsubg.appendChild(path3);\n" "\t\t}else if(arrow==5){\n" // ARROW_LINE "\t\t\tnewElement.setAttribute('d','M0,0 l'+len+',0');\n" "\t\t}else if(arrow==6){\n" // ARROW_DASHLINE "\t\t\tnewElement.setAttribute('d','M0,0 l'+len+',0');\n" "\t\t\tnewElement.setAttribute('stroke-dasharray','5,5');\n" "\t\t}else if(arrow==7){\n" // ARROW_DOTLINE "\t\t\tnewElement.setAttribute('d','M0,0 l'+len+',0');\n" "\t\t\tnewElement.setAttribute('stroke-dasharray','2,4');\n" "\t\t}\n" "\t\te.setAttribute('transform','translate('+x+','+y+') rotate('+angle+',0,0) translate(5,0)');\n" "\t});\n" "}\n"; wordproc_set_gamepress(val,"whi_gamepress",true); // Remove an element val += "window.removeelm = function(id,tag){\n" "\twhi_loop(tag,id,function(e){\n" // "console.log(`removeelm tag=${tag} e=${e}`);\n" "\t\te.parentNode.removeChild(e);\n" "\t});\n" "}\n"; // Remove a template. Templates are global val += "window.removetemplate = function(id){\n" "\tvar e = document.getElementById(id);\n" "\tif (e != null){\n" "\t\te.parentNode.removeChild(e);\n" "\t}else{\n" "\t\tconsole.log('removetemplate: not found '+id);\n" "\t}\n" "}\n"; // Update the message field val += "window.WHIT_updmsg = function(color,msg){\n"; val += string_f("\tvar elm = document.getElementById('msg-%s');\n",gameid.c_str()); val += "\tif (elm != null){\n"; val += "\t\telm.style.color=color;\n"; val += "\t\telm.innerHTML=msg;\n"; val += "\t}\n"; val += "}\n"; // Copy a template in a foreignobject val += "window.whi_show_template = function(doc,tid,pid) {\n" "\tvar temp = document.getElementById(tid);\n" "\tvar clon = temp.content.cloneNode(true);\n" "\tvar parent = doc.getElementById(pid);\n" "console.log ('whi_show_template tid='+tid+'='+temp+' pid='+pid+'='+parent);\n" "\twhi_remove_children(parent);\n" "\tparent.appendChild(clon);\n" "}\n"; // Add an IMG tag to a foreignobject val += "window.whi_show_image = function(doc,url,radius,pid) {\n" "\tvar img = document.createElement('img');\n" "\timg.style.borderRadius = radius;\n" "\timg.src = url;\n" "\timg.style.width='100%';\n" "\tvar parent = doc.getElementById(pid);\n" "\twhi_remove_children(parent);\n" "\tparent.appendChild(img);\n" "}\n"; return val; } string WHITEBOARD::define_styles(const DOC_CONTEXT &ctx, const DOC_UI_SPECS_receive &sp) { string val; val += ".whitext{\n"; val += "\tfont-family: serif;\n"; val += "\tstroke-width: 0.5;\n"; //val += string_f("\tfont-size:%u;\n",sp.mobile ? sp.fontsize - 15 : sp.fontsize + 3); val += "}\n"; return val; } void WHITEBOARD::remove_session (const char *session) { auto p = prefs.find(session); if (p != prefs.end()) prefs.erase(p); } // be_del is an element which will be deleted. // if be_del is a text bullet, raise all text bullet with the same X coordinate and lower. void WHITEBOARD::move_lower_bullets(WHITEBOARD_ELM &be_del, unsigned fontsize, set ¬ify_ids) { if (be_del.type == TYPE_TEXT){ // We compute the position (nexty) of the item below // and we compute the diference. Each item is raised using this "movey" difference auto next = be_del.compute_next_item(fontsize,textsize); unsigned movey = next.second - be_del.y; for (auto &elm:elms){ auto &e = elm.second; if (e.type == TYPE_TEXT && e.x == be_del.x && e.y > be_del.y){ notify_ids.insert (e.id); e.y -= movey; // When we delete a bullet, the one just below become selected. if (e.y == be_del.y) e.selmode = SEL_SELECTED; } } } } bool WHITEBOARD::label_oper ( PARAM_STRING label, set ¬ify_ids, function f) { bool found = false; for (auto &e:elms){ if (e.second.label==label){ f(e.second); notify_ids.insert(e.first); found = true; break; } } return found; } /* Remove images and documents associated for all element in selects. */ void WHITEBOARD::remove_assign(const vector &selects, set ¬ify_ids, VARVAL ¬ify_var) { for (auto s:selects){ if (s->is_image() || s->is_imbed()){ notify_ids.insert(s->id); remove_foreign(*s,notify_var); if (s->box_type == BOX_HIDDEN) s->box_type = BOX_SOLID; s->clear_image(); s->clear_imbed(); } } } void WHITEBOARD::execstep ( const char *var, const char *val, const DOC_CONTEXT &ctx, const DOC_UI_SPECS_receive &sp, VARVAL &script_var, VARVAL ¬ify_var, set ¬ify_ids, std::vector &res) { auto &pref = prefs[ctx.session]; string error,status,api_error; setactivity(); string tmpvar,tmpval; if (strcmp(var,"kbd")==0){ unsigned lastline = 1000; unsigned lastcol=1000; wordproc_kbd(val,pref.mod,tmpvar,tmpval,lastline,lastcol); var = tmpvar.c_str(); val = tmpval.c_str(); } if (strcmp(var,REQ_PRINT)==0){ if (is_any_of(val,"","full")){ glocal pref; glocal ctx; glocal sp; glocal WHITEBOARD *doc = this; // tlmp_warning ("REQ_PRINT sp %u %u\n",sp.content_width,sp.content_height); VARVAL v; (val,this,ctx,sp,"whiteboard",true,is_eq(val,"full"),v); return glocal.doc->define_styles(glocal.ctx,glocal.sp); string ret = glocal.doc->define_functions(glocal.ctx,glocal.pref,board_width,board_height); return ret; #define BUTTON_NEWDOC 0 #define BUTTON_TEXT 1 #define BUTTON_ELLIPSE 2 #define BUTTON_RECT 3 #define BUTTON_LINE 4 #define BUTTON_HANDLINE 5 #define BUTTON_SELECT 6 #define BUTTON_STARMODE 7 #define BUTTON_IMBEDMODE 8 #define BUTTON_BOXTYPE 9 #define BUTTON_LINETYPE 10 #define BUTTON_TEXTPOS 11 #define BUTTON_INCTEXTSIZE 12 #define BUTTON_DECTEXTSIZE 13 #define BUTTON_IMAGE 14 #define BUTTON_LINKDOC 15 #define BUTTON_DELITEMS 16 #define BUTTON_UNDO 17 #define BUTTON_GRID 18 WHITEBOARD_MENU menu(specs); documentd_bar_button (lines,BUTTON_NEWDOC,menu.svg_clear,specs,false,MSG_R(I_RESETDOC)); documentd_button_space(lines); documentd_bar_button (lines,BUTTON_TEXT,menu.svg_text,specs,false,MSG_U(I_ADDTEXT,"Add text")); documentd_bar_button (lines,BUTTON_ELLIPSE,menu.svg_ellipse,specs,false,MSG_U(I_ADDCIRCLE,"Add circles/ellipses")); documentd_bar_button (lines,BUTTON_RECT,menu.svg_rect,specs,false,MSG_U(I_ADDRECT,"Add rectangles")); documentd_bar_button (lines,BUTTON_LINE,menu.svg_line,specs,false,MSG_U(I_ADDLINE,"Add lines")); if (documentd_getflag("whiteboard_handline")!=nullptr){ documentd_bar_button (lines,BUTTON_HANDLINE,menu.svg_handline,specs,false,MSG_U(I_ADDHANDLINE,"Add hand drawn line")); } documentd_button_space(lines); documentd_bar_button (lines,BUTTON_SELECT,menu.svg_select,specs,glocal.pref.selecting,MSG_U(I_SELECTITEMS,"Select elements")); documentd_bar_button (lines,BUTTON_STARMODE,menu.svg_star,specs,glocal.pref.starmode,MSG_U(I_SELECTPARENTS,"Select parents")); documentd_bar_button (lines,BUTTON_IMBEDMODE,menu.svg_parent,specs,glocal.pref.imbedmode,MSG_U(I_SELECTIMBED,"Select embedding parent")); // Actions on selected elements documentd_button_space(lines); documentd_bar_button (lines,BUTTON_BOXTYPE,menu.svg_dashrect,specs,false,MSG_U(I_SETBOXTYPE,"Select the box type of selected elements")); documentd_bar_button (lines,BUTTON_LINETYPE,menu.svg_linetype,specs,false,MSG_U(I_SELECTRELATION,"Select line relation between parents and selected elements")); documentd_bar_button (lines,BUTTON_TEXTPOS,menu.svg_textpos,specs,false,MSG_U(I_POSITIONTEXT,"Position the text of selected elements")); documentd_bar_button (lines,BUTTON_INCTEXTSIZE,menu.svg_inctextsize,specs,false,MSG_U(I_SIZETEXT,"Increase the size of texts")); documentd_bar_button (lines,BUTTON_DECTEXTSIZE,menu.svg_dectextsize,specs,false,MSG_U(I_DECSIZETEXT,"Decrease the size of texts")); documentd_bar_button (lines,BUTTON_IMAGE,menu.svg_image,specs,false,MSG_U(I_ASSIGNIMAGE,"Assign an image to selected elements")); documentd_bar_button (lines,BUTTON_LINKDOC,menu.svg_imbed,specs,false,MSG_U(I_ASSIGNDOCUMEN,"Link a document to selected elements")); documentd_button_space(lines); documentd_bar_button (lines,BUTTON_GRID,menu.svg_grid,specs,false,MSG_U(I_GRID,"Turn the grid on/off")); documentd_bar_button (lines,BUTTON_DELITEMS,menu.svg_delitems,specs,false,MSG_U(I_DELITEMS,"Delete selected elements")); //documentd_bar_button (lines,BUTTON_UNDO,menu.svg_undo,specs,false,MSG_U(I_UNDOEDIT,"Undo the last modification")); string val; onevent = "onkeydown='whi_gamepress(event);return false;'"; documentd_imbeds (glocal.doc,val,glocal.sp); val += glocal.doc->draw_board(glocal.pref.viewbox_x,glocal.pref.viewbox_y,SVG_BOARD_HEIGHT,board_width,board_height,sp.fontsize,ctx.docnum,true,script); return val; return string_f("\n" ,gameid,glocal.pref.viewbox_x*board_width/(4*SVG_BOARD_WIDTH),board_width/4,scroll_thick); return string_f("\n" ,gameid,glocal.pref.viewbox_y*board_height/(4*SVG_BOARD_HEIGHT),scroll_thick,board_height/4); res.emplace_back(move(v)); } }else if (strcmp(var,REQ_FUNCTIONS)==0){ VARVAL var; var.var = VAR_DEFSCRIPT; var.val = define_functions (ctx,pref,1000,1000); res.emplace_back(move(var)); }else if (strcmp(var,REQ_STYLES)==0){ VARVAL var; var.var = VAR_STYLES; var.val += define_styles(ctx,sp); res.emplace_back(move(var)); }else if (strcmp(var,REQ_REGION)==0){ // For embedding // tlmp_warning ("REQ_REGION sp %u %u\n",sp.content_width,sp.content_height); DOC_UI_SPECS_receive mysp; mysp.content_width = 1000; mysp.content_height = 1000; if (strlen(val)==2 && isalpha(val[0]) && isdigit(val[1])){ unsigned col = toupper(val[0])-'A'; unsigned row = val[1] - '1'; if (col < 4 && row < 4){ VARVAL var,var_script; var.var = VAR_CONTENT; var_script.var = VAR_DEFSCRIPT; var.val = draw_board (col*SVG_BOARD_WIDTH,row*SVG_BOARD_HEIGHT,SVG_BOARD_HEIGHT ,mysp.content_width,mysp.content_height,sp.fontsize,ctx.docnum,false,var_script.val); res.emplace_back(move(var)); res.emplace_back(move(var_script)); } } }else if (strcmp(var,REQ_CHAT)==0){ appendchat(val,notify_var.val,res,ctx); }else if (strcmp(var,REQ_GETFIELDS)==0){ VARVAL var; var.var = VAR_FIELDS; if (is_any_of(val,DIALOG_IMAGE,DIALOG_IMBED)){ auto selects = findselected(SEL_SELECTED); if (selects.size() > 0){ // We use the specs of the first selected elements. THey will all share the result auto s = selects[0]; if (strcmp(val,DIALOG_IMAGE)==0){ var.val = string_f("url:%s\n",s->imageurl.c_str()); }else{ var.val = string_f("docid:%s\n",s->embed.document.c_str()); var.val += string_f("region:%s\n",s->embed.region.c_str()); } } } res.emplace_back(var); }else if (strcmp(var,"dump")==0){ VARVAL var; var.var = "elements"; for (auto &s:elms){ var.val += string_f("\nelement: id=%u label=%s txt=%s type=%u selmode=%u boxtype=%u x=%u y=%u x1=%u y1=%u width=%u height=%u" " imageurl=%s document=%s region=%s docnum=%u" ,s.first,s.second.label.c_str(),s.second.txt.c_str(),s.second.type,s.second.selmode,s.second.box_type ,s.second.x,s.second.y,s.second.x1,s.second.y1,s.second.width,s.second.height ,s.second.get_image(),s.second.get_document(),s.second.get_region(),s.second.get_docnum()); if (s.second.subelms.size() > 0){ var.val += "\n\tsubelms[]="; for (auto &sub:s.second.subelms) var.val += string_f(" id=%d,arrow=%u",sub.id,sub.arrow); } if (s.second.insides.size() > 0){ var.val += "\n\tinsides[]="; for (auto id:s.second.insides) var.val += string_f(" id=%d",id); } for (auto &arrows:s.second.arrows){ var.val += "\narrows[]="; for (auto &a:arrows) var.val += string_f(" id=%d,arrow=%u,angle=%d",a.id,a.arrow,a.angle); } } var.val += "\nbaseelms[]="; for (auto id:baseelms){ var.val += string_f(" id=%u",id); } var.val += "\n"; res.emplace_back(var); }else if (is_any_of(var,KBD_PAGEUP,KBD_PAGEDOWN,KBD_HMOVE,KBD_VMOVE)){ unsigned box_x = pref.viewbox_x; unsigned box_y = pref.viewbox_y; if (is_eq(var,KBD_PAGEUP)){ if (pref.mod.shift){ limit_scroll (box_x,-SVG_BOARD_WIDTH,SVG_BOARD_WIDTH,4); }else{ limit_scroll (box_y,-SVG_BOARD_HEIGHT,SVG_BOARD_HEIGHT,4); } }else if (is_eq(var,KBD_PAGEDOWN)){ if (pref.mod.shift){ limit_scroll (box_x,SVG_BOARD_WIDTH,SVG_BOARD_WIDTH,4); }else{ limit_scroll (box_y,SVG_BOARD_HEIGHT,SVG_BOARD_HEIGHT,4); } }else if (is_eq(var,KBD_HMOVE)){ int move = is_eq(val,"1") ? SVG_BOARD_WIDTH/4 : -(SVG_BOARD_WIDTH/4); if (pref.mod.shift) move *= 4; limit_scroll (box_x,move,SVG_BOARD_WIDTH,4); }else if (is_eq(var,KBD_VMOVE)){ int move = is_eq(val,"1") ? SVG_BOARD_HEIGHT/4 : -(SVG_BOARD_HEIGHT/4); if (pref.mod.shift) move *= 4; limit_scroll (box_y,move,SVG_BOARD_HEIGHT,4); } if (box_x != pref.viewbox_x || box_y != pref.viewbox_y){ pref.viewbox_x = box_x; pref.viewbox_y = box_y; script_var.val += string_f("whi_%u.setviewbox(%u,%u);\n",ctx.docnum,box_x,box_y); } }else if (strcmp(var,REQ_FOCUS)==0){ setfocus(script_var); }else if (ctx.maywrite){ if (is_eq(var,KBD_DELETECHAR)){ if (pref.mod.shift){ auto selects = findselected(SEL_SELECTED); for (auto s:selects){ move_lower_bullets(*s,sp.fontsize,notify_ids); delete_elm (s->id,notify_var); } } }else if (is_any_of(var,KBD_BACKSPACE,KBD_INSERTCHAR,KBD_BREAK,KBD_TAB)){ auto selects = findselected(SEL_SELECTED); for (auto s:selects){ if (is_eq(var,KBD_BACKSPACE)){ documentd_eraselast(s->txt); }else if (is_eq(var,KBD_TAB)){ // We use the TAB key to change the width and height of an item by a fixed increment. // For text bullet, we use a larger increment. int incr = s->type == TYPE_TEXT ? 60 : 5; int grow = pref.mod.shift ? -incr : incr; s->resize(grow,0,false,false); }else if (is_eq(var,KBD_BREAK)){ // Duplicate items either below or to the right auto star_elms = findselected(SEL_STAR); auto imbed_elms = findselected(SEL_IMBED); if (selects.size() > 1){ error = MSG_U(E_DUPLICATEONE,"Duplication only applies to one element at once"); }else{ s->selmode = SEL_NONE; // Unselect the old element /* When placing the new element, either below or to the right, we must take the old element text position and size. */ auto next = s->compute_next_item(sp.fontsize,textsize); unsigned x = s->x; unsigned y = s->y; if (pref.mod.shift){ // Instead we duplicate the element to the right x = next.first; }else{ y = next.second; } unsigned id = place_newelm(s->type,x,y,s->width,s->height,star_elms,imbed_elms,notify_var,notify_ids); auto &newelm = elms[id]; // When duplicating a text element, we do not copy the text. It just creates an empty bullet if (newelm.type != TYPE_TEXT){ newelm.txt = s->txt; newelm.textpos = s->textpos; newelm.textsize = s->textsize; }else{ // We move lower all text element with the same X coordinate and larger Y for (auto &elm:elms){ auto &e = elm.second; if (e.id != newelm.id && e.type == TYPE_TEXT && e.x == newelm.x and e.y >= newelm.y){ notify_ids.insert(e.id); e.y += newelm.y - s->y; } } } } }else{ s->txt += val; } notify_ids.insert(s->id); } }else if (strcmp(var,"select")==0){ double dx,dy; unsigned button; bool shiftkey,ctrlkey; if (splitline(val,',',dx,dy,limits(button,1u,2u),shiftkey,ctrlkey)){ unsigned x = dx, y=dy; pref.lastx = x; pref.lasty = y; /* The logic for pref.selecting, star_mode and imbed_mode is different In pref.selecting, select one item unselect all others unless the shift key is activivated. This is standard. Selecting again the same item keeps it selected. This is useful when moving the item. In imbed_mode, only one item may be selected at once. Re-selecting the item unselect it. In star_mode, you may select multiple items without using the shift key. Re-selecting the item unselect it. */ if (button == 1 && pref.selecting && !shiftkey && !ctrlkey){ resetsel(SEL_SELECTED,notify_ids); } auto e = locate (x,y,sp.fontsize); // tlmp_warning ("select: val=%s x=%u y=%u e=%p",val,x,y,e); if (e != nullptr){ if (pref.selecting){ if (button == 2){ if (ctrlkey){ // We change the label position auto selects = findselected(SEL_SELECTED); for (auto s:selects){ s->changetextpos(); notify_ids.insert(s->id); } }else{ // We are cycling though the various line types connecting elms // shiftkey change the cycling direction changeline (shiftkey ? -1 : 1,error,notify_var,notify_ids); } }else if (shiftkey && ctrlkey){ if (e->selmode != SEL_IMBED){ resetsel(SEL_IMBED,notify_ids); // Only one SEL_IMBED at once e->selmode = SEL_IMBED; }else{ e->selmode = SEL_NONE; } }else if (ctrlkey && !is_any_of(e->type,TYPE_LINE,TYPE_HANDLINE)){ if (e->selmode != SEL_STAR){ e->selmode = SEL_STAR; }else{ e->selmode = SEL_NONE; } }else{ e->selmode = SEL_SELECTED; } }else if (pref.starmode){ e->selmode = e->selmode == SEL_STAR ? SEL_NONE : SEL_STAR; }else if (pref.imbedmode){ if (e->selmode != SEL_IMBED){ resetsel(SEL_IMBED,notify_ids); // Only one SEL_IMBED at once e->selmode = SEL_IMBED; }else{ e->selmode = SEL_NONE; } } notify_ids.insert(e->id); } } }else if (strcmp(var,"mousemove")==0){ auto selects = findselected(SEL_SELECTED); if (selects.size() > 0){ double dx,dy; bool shiftkey,ctrlkey; if (splitline(val,',',dx,dy,shiftkey,ctrlkey)){ unsigned x = dx, y = dy; int snapx = x; int snapy = y; int last_snapx = pref.lastx; int last_snapy = pref.lasty; if (gridcell != 0){ // snap to grid. We do not use the last mouse position, but the position // of the elm itself snapx = snap2grid(x,gridcell); snapy = snap2grid(y,gridcell); // We are using the x,y coordinates of one of the selected elements // We must find the one currently 'touched' by the mouse. for (auto e:selects){ if (e->is_inside(x,y,sp.fontsize,textsize)){ last_snapx = e->x; last_snapy = e->y; break; } } } if (snapx != last_snapx || snapy != last_snapy){ int movex = snapx-last_snapx; int movey = snapy-last_snapy; pref.lastx = snapx; pref.lasty = snapy; for (auto elm:selects){ if (elm->type == TYPE_LINE){ if(shiftkey){ check_limit (elm->x,movex); check_limit (elm->y,movey); }else if (ctrlkey){ check_limit (elm->x1,movex); check_limit (elm->y1,movey); }else{ elm->move(elms,movex,movey,notify_ids); } notify_ids.insert(elm->id); }else{ elm->move(elms,movex,movey,notify_ids); } } setmodified(ctx.username); } } } }else if (strcmp(var,"wheel")==0){ auto selects = findselected(SEL_SELECTED); if (selects.size() == 0){ error = MSG_R(E_NONODE); }else{ int grow; bool shiftkey,ctrlkey; if (splitline(val,',',grow,shiftkey,ctrlkey)){ grow *= gridcell == 0 ? 5 : gridcell; for (auto elm:selects){ if (is_any_of(elm->type,TYPE_ELLIPSE,TYPE_RECT)){ elm->resize(grow,gridcell,ctrlkey,shiftkey); notify_ids.insert(elm->id); }else{ error = MSG_U(E_ONLYRECTELLIPSE,"Resize only applies to rectangles and ellipses"); } } setmodified(ctx.username); } } }else if (strcmp(var,"newgame")==0){ int uval = atoi(val); auto oldpref = pref; if (uval == BUTTON_RELOAD){ documentd_action_reload(res); }else if (uval == BUTTON_NEWDOC){ VARVAL var; var.var = VAR_DIALOG; var.val = DIALOG_WHITEBOARD_NEW; res.emplace_back(var); }else if (is_any_of(uval,BUTTON_TEXT,BUTTON_ELLIPSE,BUTTON_RECT,BUTTON_LINE,BUTTON_HANDLINE)){ resetsel(SEL_SELECTED,notify_ids); auto star_elms = findselected(SEL_STAR); auto imbed_elms = findselected(SEL_IMBED); // tlmp_warning ("star_elm=%lu imbed_elm=%lu",star_elms.size(),imbed_elms.size()); static WHITEBOARD_ELMTYPE tbtype[]={TYPE_TEXT,TYPE_ELLIPSE,TYPE_RECT,TYPE_LINE,TYPE_HANDLINE}; auto type = tbtype[uval-BUTTON_TEXT]; const unsigned default_size= type == TYPE_TEXT ? 8 : 50; const unsigned x=50 + pref.viewbox_x; const unsigned y=50 + pref.viewbox_y; // tlmp_warning ("place x=%u y=%u box_x=%d box_y=%d\n",x,y,pref.viewbox_x,pref.viewbox_y); place_newelm(type,x,y,default_size,default_size,star_elms,imbed_elms,notify_var,notify_ids); setmodified(ctx.username); pref.selecting = true; pref.starmode = pref.imbedmode = false; }else if (is_any_of(uval,BUTTON_SELECT,BUTTON_STARMODE,BUTTON_IMBEDMODE)){ if (uval == BUTTON_SELECT){ pref.selecting = true; pref.starmode = pref.imbedmode = false; }else if (uval == BUTTON_STARMODE){ pref.starmode = true; pref.selecting = pref.imbedmode = false; }else if (uval == BUTTON_IMBEDMODE){ pref.imbedmode = true; pref.selecting = pref.starmode = false; } }else if (uval == BUTTON_BOXTYPE){ auto selects = findselected (SEL_SELECTED); if (selects.size()==0){ error = MSG_R(E_NONODE); }else{ for (auto s:selects){ s->box_type = (BOX_TYPE)((unsigned)s->box_type+1); // If there is an image (or embed) assigned to this item, BOX_HIDDEN // may be selected. The item will still be visible (and easily selectable) // If there is no image, then BOX_HIDDEN can't be selected. BOX_TYPE last = s->is_image() || s->is_imbed() ? BOX_LAST : BOX_HIDDEN; if (s->box_type == last) s->box_type = BOX_SOLID; notify_ids.insert (s->id); } } }else if (uval == BUTTON_LINETYPE){ changeline (1,error,notify_var,notify_ids); }else if (uval == BUTTON_TEXTPOS){ auto selects = findselected (SEL_SELECTED); if (selects.size()==0){ error = MSG_R(E_NONODE); }else{ for (auto s:selects){ s->changetextpos(); notify_ids.insert(s->id); } } }else if (is_any_of(uval,BUTTON_INCTEXTSIZE,BUTTON_DECTEXTSIZE)){ auto selects = findselected (SEL_SELECTED); int incr = uval == BUTTON_INCTEXTSIZE ? 1 : -1; if (selects.size()==0){ // No elements selected, so we change the default if (incr != -1 || textsize > 1){ textsize += incr; for (auto &e:elms) notify_ids.insert(e.first); }else{ error = "too small"; } }else{ for (auto s:selects){ s->changetextsize(incr); notify_ids.insert(s->id); } } }else if (is_any_of(uval,BUTTON_IMAGE,BUTTON_LINKDOC)){ auto selects = findselected (SEL_SELECTED); if (selects.size() == 0){ error = MSG_R(E_NONODE); }else if (uval == BUTTON_IMAGE){ bool valid_type = true; for (auto s:selects) if (!is_any_of(s->type,TYPE_ELLIPSE,TYPE_RECT)) valid_type = false; if (valid_type){ VARVAL var; var.var = VAR_DIALOG; var.val = DIALOG_IMAGE; res.emplace_back(var); }else{ error = MSG_U(E_IMGONRECTELLIPSE,"You can only apply an image to rectangles or ellipses"); } }else if (uval == BUTTON_LINKDOC){ bool only_rect = true; for (auto s:selects) if (s->type != TYPE_RECT) only_rect = false; if (only_rect){ VARVAL var; var.var = VAR_DIALOG; var.val = DIALOG_IMBED; res.emplace_back(var); }else{ error = MSG_U(E_LINKONRECT,"You can only link a document on squares/rectangles"); } } }else if (uval == BUTTON_DELITEMS){ auto selects = findselected (SEL_SELECTED); if (selects.size() == 0){ error = MSG_R(E_NONODE); }else{ for (auto s:selects){ move_lower_bullets(*s,sp.fontsize,notify_ids); delete_elm (s->id,notify_var); } } }else if (uval == BUTTON_GRID){ gridcell += 20; if (gridcell > 40) gridcell = 0; const char *tbcol[2]={"none","none"}; if (gridcell == 20){ tbcol[0] = "lightgray"; tbcol[1] = "lightgray"; }else if (gridcell == 40){ tbcol[0] = "lightgray"; } for (unsigned i=0; i<2; i++){ notify_var.val += string_f("var p = document.getElementById('grid%u-%s,%u');\n",i,gameid.c_str(),ctx.docnum); notify_var.val += string_f("p.style.stroke='%s';\n",tbcol[i]); } }else{ tlmp_error ("whiteboard newgame=%d",uval); } whiteboard_update_button (script_var.val, oldpref.selecting,pref.selecting, BUTTON_SELECT); whiteboard_update_button (script_var.val, oldpref.starmode,pref.starmode, BUTTON_STARMODE); whiteboard_update_button (script_var.val, oldpref.imbedmode,pref.imbedmode, BUTTON_IMBEDMODE); setfocus(script_var); }else if (strcmp(var,"image")==0){ vector fields; documentd_parsefields(val,fields); string url; for (auto &f:fields) if (f.var == "url") url = f.val; auto selects = findselected(SEL_SELECTED); if (selects.size() == 0){ api_error = MSG_R(E_NONODE); }else if (url.empty()){ // remove_assign below will remove any image or imbed // If a user click on the BUTTON_IMAGE with a selected imbed // he will get an empty dialog. If he clicks on submit // this will removing the imbed. // If the select object is not an image nor an imbed, remove_assign has no effects. size_t count = 0; for (auto s:selects) if (s->is_imbed()) count++; if (count != 0){ error = "some imbed"; }else{ remove_assign(selects,notify_ids,notify_var); } }else{ string paramurl = whiteboard_converturl (gameid,url); for (auto s:selects){ if (s->is_imbed()){ s->clear_imbed(); }else if (!s->is_image()){ addsvgelement(notify_var,gameid,"foreignObject",nullptr,0,string_f("f%u",s->id),s); } s->imageurl = url; notify_ids.insert(s->id); // Make sure the foreignObject is positionned notify_var.val += "whi_loop_board(function(svg){\n"; notify_var.val += string_f("\twhi_show_image(svg,'%s','%s','f%u');\n" ,paramurl.c_str(),s->type == TYPE_ELLIPSE ? "50%" : "5px",s->id); notify_var.val += "\t});\n"; } } }else if (strcmp(var,"imbed")==0){ vector fields; documentd_parsefields(val,fields); DOCUMENT_EMBED imbed; for (auto &f:fields){ if (f.var == "docid"){ imbed.document = f.val; }else if (f.var == "region"){ imbed.region = f.val; } } auto selects = findselected(SEL_SELECTED); if (selects.size() == 0){ api_error = MSG_R(E_NONODE); }else if (imbed.document.empty()){ remove_assign(selects,notify_ids,notify_var); }else{ // Allocate a document number. Note that the document may exist several time (same doc, same region) // We assign a different docnum for the same document so all views are updated when // the sub-document changes. unsigned docnum = 0; for (auto &e:elms){ auto &ee = e.second; if (ee.is_imbed()){ unsigned num = ee.get_docnum(); if (num > docnum) docnum = num; } } for (auto s:selects){ imbed.docnum = ++docnum; DOC_UI_SPECS_receive elm_sp; elm_sp.content_width = elm_sp.width = s->width; elm_sp.content_height = elm_sp.height = s->height; elm_sp.fontsize = 14; elm_sp.mobile = false; documentd_insert_imbed (this,notify_var,imbed,elm_sp); notify_var.val += string_f("doc_cur_gameid='%s';\n",gameid.c_str()); if (s->is_image()){ s->clear_image(); }else if (!s->is_imbed()){ addsvgelement(notify_var,gameid,"foreignObject",nullptr,0,string_f("f%u",s->id),s); } s->embed = imbed; notify_ids.insert(s->id); // Make sure the foreignObject is positionned notify_var.val += "whi_loop_board(function(svg){\n"; notify_var.val += string_f("\twhi_show_template(svg,'temp%u','f%u');\n",imbed.docnum,s->id); notify_var.val += "\t});\n"; } } // The other actions are Used to script modification of the document }else if (strcmp(var,"addelm")==0){ // val format: label "text" type x y width height string label,text,type; unsigned x,y,width,height; if(splitlineq(val,label,text,type,x,y,width,height)){ if (!is_any_of(type,"ellipse","rect","line","handline","text")){ api_error = MSG_U(E_ADDELMTYPE,"addelm: Invalid element type"); }else if (width == 0 || height == 0){ api_error = MSG_U(E_ADDELMSIZE,"addelm: Invalid dimension"); }else{ WHITEBOARD_ELMTYPE elmtype = TYPE_ELLIPSE; if (type == "rect"){ elmtype = TYPE_RECT; }else if (type == "line"){ elmtype = TYPE_LINE; }else if (type == "handline"){ elmtype = TYPE_HANDLINE; }else if (type == "text"){ elmtype = TYPE_TEXT; } auto star_elms = findselected(SEL_STAR); auto imbed_elms = findselected(SEL_IMBED); if (imbed_elms.size() == 1){ auto imb = imbed_elms[0]; x += (imb->x - imb->width/2); y += (imb->y - imb->height/2); } unsigned id = place_newelm(elmtype,x,y,width,height,star_elms,imbed_elms,notify_var,notify_ids); setmodified(ctx.username); auto &elm = elms[id]; elm.label = label; elm.txt = text; } } }else if (strcmp(var,"resetselect")==0){ // val format: 0|1|2 unsigned sel = atoi(val); static WHITEBOARD_SELMODE tbmode[]={SEL_SELECTED,SEL_STAR, SEL_IMBED}; if (sel < 3){ resetsel (tbmode[sel],notify_ids); }else if (sel == 3){ for (auto s:tbmode) resetsel (s,notify_ids); } }else if (strcmp(var,"selectline")==0){ // val format: 0|1|... ARROW_TYPE unsigned uval = atoi(val); if (uval < ARROW_LAST){ ARROW_TYPE type = (ARROW_TYPE)uval; auto parents = findselected(SEL_STAR); auto selects = findselected(SEL_SELECTED); if (parents.size() == 0){ api_error = MSG_R(E_NOPARENT); }else if (selects.size() == 0){ api_error = MSG_R(E_NONODE); }else{ changeline (parents,selects,0,type,notify_var,api_error); for (auto s:selects) notify_ids.insert(s->id); } } }else if (is_eq(var,"labeldelete")){ // val format: label bool found = false; for (auto &e:elms){ if (e.second.label==val){ delete_elm (e.first,notify_var); found = true; break; } } if (!found) api_error = string_f(MSG_U(E_LABELDELETE,"labeldelete: element %s not found"),val); }else if (is_eq(var,"settext")){ // val format: label text string label,text; if (splitlineq(val,' ',label,text)){ bool found = label_oper(label,notify_ids,[&text](auto &e){ e.txt = text; }); if (!found) api_error = string_f(MSG_U(E_LABELOPER,"%s: label %s not found"),"settext",label.c_str()); } }else if (is_eq(var,"settextsize")){ // val format: label size string label; int size; if (splitlineq(val,' ',label,size)){ bool found = label_oper(label,notify_ids,[size](auto &e){ e.textsize = size; }); if (!found) api_error = string_f(MSG_R(E_LABELOPER),"settextsize",label.c_str()); } }else if (is_any_of(var,"labelselect","boxtype","textpos")){ // val format: label 0|1|2 string label; unsigned sel; if (splitline(val,' ',label,sel)){ bool found = label_oper(label,notify_ids,[var,sel](auto &e){ if (is_eq(var,"labelselect")){ if (sel < 3){ static WHITEBOARD_SELMODE tbmode[]={SEL_SELECTED,SEL_STAR, SEL_IMBED}; e.selmode = tbmode[sel]; } }else if (is_eq(var,"boxtype")){ // BOX_HIDDEN is only available when there is an image or embed unsigned limit = 3; if (e.is_image() || e.is_imbed()) limit = 4; if (sel < limit){ e.box_type = (BOX_TYPE)sel; } }else if (is_eq(var,"textpos")){ if (sel < 5){ e.textpos = (WHITEBOARD_TEXTPOS)sel; } } }); if (!found) api_error = string_f(MSG_R(E_LABELOPER),var,label.c_str()); } }else if (is_eq(var,"resetgame")){ // val == 0: reset everything if (is_any_of(val,"","0")){ while (elms.size() > 0){ delete_elm(elms.begin()->second.id,notify_var); } resetgame(); }else{ unsigned endx = pref.viewbox_x + SVG_BOARD_WIDTH; unsigned endy = pref.viewbox_y + SVG_BOARD_HEIGHT; for (auto e=elms.begin(); e != elms.end();){ auto nexte = e; nexte++; auto &elm = e->second; if (elm.x >= pref.viewbox_x && elm.x < endx && elm.y >= pref.viewbox_y && elm.y < endy){ delete_elm (elm.id,notify_var); } e = nexte; } } setmodified(ctx.username); }else if (is_eq(var,"textsize")){ unsigned new_textsize = 0; if (splitline(val,new_textsize)){ textsize = new_textsize; for (auto &e:elms) notify_ids.insert(e.first); } }else if (is_eq(var,"bullettype")){ int type; string label; if (splitline(val,label,enums(type,{0,3}))){ bool found = label_oper(label,notify_ids,[type](auto &e){ // This is a hack. We store the type in box_type // Value 3 means HIDDEN. e.box_type = (BOX_TYPE)type; }); if (!found) api_error = string_f(MSG_R(E_LABELOPER),var,label.c_str()); }else{ api_error = string_f(MSG_U(E_APIOPER,"%s: invalid type"),var); } } }else{ error = MSG_R(E_READONLYDOC); } if (api_error.size() > 0){ VARVAL var; var.var = VAR_ERROR; var.val = move(api_error); res.emplace_back(move(var)); } if (error.size() > 0){ update_msg(false,error,"red",res); }else if (status.size() > 0){ update_msg(true,status,"red",res); }else{ update_msg(false," ","white",res); } } void WHITEBOARD::manyexec ( const vector &steps, const DOC_CONTEXT &ctx, const DOC_UI_SPECS_receive &sp, vector &res, std::vector &unotifies) { string cur_whi = string_f("doc_cur_gameid='%s';\n",gameid.c_str()); VARVAL script_var; script_var.var = VAR_SCRIPT; script_var.val = cur_whi; VARVAL notify_var; notify_var.var = VAR_NOTIFY; notify_var.val = cur_whi; set notify_ids; // Lines to update using SCRIPT for (auto &v:steps){ execstep (v.var,v.val,ctx,sp,script_var,notify_var,notify_ids,res); } sortsubelms(notify_ids); redraw(notify_ids,notify_var,sp.fontsize); if (notify_var.val != cur_whi) res.emplace_back(move(notify_var)); if (script_var.val != cur_whi) res.emplace_back(move(script_var)); }