/* 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 . */ /* calc is a spreadsheet program. */ #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" #include "doc_calc.h" using namespace std; #include "proto/documentd_calc.protoch" #include "proto/documentd_calc.protoh" static const char *tbalign[]={"","left","center","right"}; string CELL_COOR::tostring() const { if (line == (unsigned short)-1 || col == (unsigned short)-1){ return ""; }else if (col < 26){ return string_f("%c%u",'A'+col,line+1); }else if (col < 26*26){ unsigned letter1 = col / 26; unsigned letter2 = col % 26; return string_f("%c%c%u",'A'+letter1,'A'+letter2,line+1); }else if (col < 26*26*26){ unsigned letter1 = col / (26*26); unsigned left = col % (26*26); unsigned letter2 = left / 26; unsigned letter3 = left % 26; return string_f("%c%c%c%u",'A'+letter1,'A'+letter2,'A'+letter3,line+1); }else{ unsigned letter1 = col / (26*26*26); unsigned left = col % (26*26*26); unsigned letter2 = left / (26*26); left %= (26*26); unsigned letter3 = left / 26; unsigned letter4 = left % 26; return string_f("%c%c%c%c%u",'A'+letter1,'A'+letter2,'A'+letter3,'A'+letter4,line+1); } } GAME_P make_CALC() { return make_shared(); } CALC::CALC() { } void CALC::save(DOC_WRITER &w, bool) { documentd_calc_header(&w,revision); vector cells; for (auto &g:grid){ CELL c; c.l = g.first.line; c.c = g.first.col; c.text = g.second.text; cells.emplace_back(move(c)); } documentd_calc_cells(&w,cells); vector formats; for (auto &f:col_formats){ COLFORMAT cf; cf.col = f.first; cf.width = f.second.width; cf.precision = f.second.precision; cf.align = f.second.align; formats.push_back(cf); } documentd_calc_col_formats(&w,formats); vector sprefs; for (auto &p:prefs){ // We only save prefs that were used for editing/moving. if (p.second.is_modified()){ CALCPREF s; s.session = p.first; s.curline = p.second.cursor.line; s.curcol = p.second.cursor.col; s.offline = p.second.offset_line; s.offcol = p.second.offset_col; sprefs.emplace_back(s); } } documentd_calc_prefs(&w,sprefs); vector schat; documentd_copychat (schat,chat); documentd_calc_chat(&w,schat); } void CALC::load(DOC_READER &r, std::string &msg) { resetgame(); glocal revision; glocal grid; glocal col_formats; glocal chat; glocal prefs; prefs.clear(); chat.clear(); (&r); glocal.revision = revision; for (auto &c:cells){ auto &cc = glocal.grid[CELL_COOR(c.l,c.c)]; cc.text = c.text; cc.eval0(); } for (auto &f:formats){ auto &lf = glocal.col_formats[f.col]; lf.width = f.width; lf.precision = f.precision; lf.align = f.align; } for (auto &p:prefs){ auto &pp = glocal.prefs[p.session]; pp.cursor.line = p.curline; pp.cursor.col = p.curcol; pp.offset_line = p.offline; pp.offset_col = p.offcol; } for (auto l:lines) glocal.chat.emplace_back(l.time,l.line); tlmp_error ("Invalid record while reading whiteboard file: %s",msg); set ids; string error; eval(ids,error); } void CALC::resetgame() { grid.clear(); col_formats.clear(); } const char *CALC::getclass() const { return "CALC"; } void CALC::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 ("CALC::exec called"); } const unsigned TBL_FIXED_LINES=40; const unsigned TBL_FIXED_COLUMNS=26; const unsigned DEFAULT_COL_WIDTH=100; string CALC::draw_board ( const DOC_CONTEXT &ctx, CALC_PREF &pref, unsigned board_width, unsigned board_height, unsigned fontsize, unsigned docnum, bool editmode, // Enable onXXXX function const CELL_COOR &area, // Show only the cells defined by pref.offset_line/col and area.line/col if area.line>0 string &script) { glocal fontsize; glocal string val; glocal script; unsigned draw_lines = TBL_FIXED_LINES; unsigned draw_columns = TBL_FIXED_COLUMNS; bool draw_headings = true; if (area.line != 0){ draw_lines = area.line; draw_columns = area.col; draw_headings = false; } script += string_f("doc_cur_gameid='%s';\n",gameid.c_str()); // Generate JS objects script += string_f( "var calc_%u={\n" "\tgameid:'%s',docnum:%u,offline:%u,offcol:%u,lines:%u,columns:%u,heading:%u" "};\n" "calc_%u.gameselect=function(event){\n" "\tcalc_gameselect(this.gameid,this.docnum,event);\n" "};\n" "calc_%u.gamemove=function(event){\n" "\tcalc_gamemove(this.gameid,this.docnum,event);\n" "};\n" ,docnum ,gameid.c_str(),docnum,pref.offset_line,pref.offset_col,draw_lines,draw_columns,draw_headings ,docnum ,docnum); // This function helps update a cell. We pass an absolute cell coordinate and function f is called (or not) // with the proper td object. script += string_f("calc_%u.selcell=function(tds,absline,abscol,f){\n",docnum); script += "\tvar line = absline-this.offline;\n" "\tvar col = abscol-this.offcol;\n"; script += "\tif(line >= 0 && line < this.lines && col >= 0 && col < this.columns){\n"; script += "\t\tvar td=tds[(line+this.heading)*(this.columns+this.heading)+col+this.heading];\n"; script += "\t\tf(td);\n" "\t}\n" "};\n"; // Update the columns width specification script += string_f("calc_%u.updcolwidth=function(e,col,width){\n",ctx.docnum); script += "\tvar cols=e.getElementsByTagName('col');\n"; script += "\tif (col >= this.offcol && col < this.offcol+this.columns){\n"; script += "\t\tcol -= this.offcol;\n"; script += "\t\tif (this.heading) col++;\n"; script += "\t\tcols[col].style.minWidth=width;\n"; script += "\t\tcols[col].style.width=width;\n"; script += "\t\tcols[col].style.maxWidth=width;\n"; script += "\t}\n"; script += "};\n"; // Update the offline,offcol of the current object, Update the heading as well script += string_f("calc_%u.updoff=function(offline,offcol){\n",ctx.docnum); script += "\tthis.offline=offline;\n" "\tthis.offcol=offcol;\n" "\tif(this.heading){\n" "\t\tvar tbl=document.getElementById('tbl-'+this.gameid+','+this.docnum);\n" "\t\tvar tds=tbl.getElementsByTagName('td');\n"; script += "\t\tfor (var i=1; i<=this.lines; i++){\n"; script += "\t\t\ttds[i*(this.columns+1)].innerHTML=i+this.offline;\n"; script += "\t\t}\n"; script += "\t\tfor (var i=0; i\n",board_width,board_height); }else{ glocal.val += "
\n"; } glocal.val += string_f("\n",gameid.c_str(),docnum,onfuncs.c_str()); if (draw_headings){ glocal.val += "\n"; // This is for the column holding line numbers } for (unsigned col=0; col < draw_columns; col++){ unsigned width = DEFAULT_COL_WIDTH; auto c = col_formats.find(pref.offset_col+col); if (c != col_formats.end()) width = c->second.width; glocal.val += string_f("\n",width,width,width); } // column names (letters) if (draw_headings){ glocal.val += ""; if (draw_headings) glocal.val += string_f("
"; for (unsigned i=0; i<26; i++){ const char *color = pref.cursor.col == i ? "lightblue" : "#E8E8E8"; unsigned col = i+pref.offset_col; string coltxt; if (col < 26){ coltxt = 'A'+col; }else{ unsigned letter1 = col / 26; unsigned letter2 = col % 26; coltxt = " "; coltxt[0] = 'A' + letter1; coltxt[1] = 'A' + letter2; } glocal.val += string_f("%s",color,coltxt.c_str()); } } for (unsigned i=0; i < draw_lines; i++){ unsigned line = i+pref.offset_line; const char *color = pref.cursor.line == line ? "lightblue" : "#E8E8E8"; glocal.val += "
%d",color,line+1); for (unsigned j=0; j < draw_columns; j++){ unsigned col = j+pref.offset_col; auto c = grid.find(CELL_COOR(line,col)); const char *bgcol = "white"; if (editmode){ for (auto &p:prefs){ if (p.second.cursor.line == line && p.second.cursor.col == col){ if (p.first == ctx.session){ bgcol = "lightblue"; break; }else{ bgcol = "lightgray"; } } } } if (c != grid.end()){ const char *color = c->second.getcolor(); const char *align = c->second.getalign(); auto f = col_formats.find(col); unsigned precision = 2; if (f != col_formats.end()){ if (f->second.align != COL_DEFAULT) align = tbalign[f->second.align]; precision = f->second.precision; } if (strcmp(bgcol,"white")!=0 || strcmp(color,"black")!=0 || strcmp(align,"left")!=0){ glocal.val += string_f("%s" ,bgcol,color,align,c->second.gettext(precision).c_str()); }else{ glocal.val += string_f("%s",c->second.gettext(precision).c_str()); } }else if (strcmp(bgcol,"white")!=0){ glocal.val += string_f("",bgcol); }else{ glocal.val += ""; } } glocal.val += "\n"; } glocal.val += "
\n"; glocal.val += "
\n"; return glocal.val; }
/* Create the javascript functions */ string CALC::define_functions( const DOC_CONTEXT &ctx, const CALC_PREF &pref) { string val; wordproc_set_gamepress(val,"calc_gamepress",false); // Return the last visible line, needed by gamepress, shared with doc_wordproc val += "window.getlastline = function(){\n"; val += string_f("\tvar elm = document.getElementById('tbl-%s,%u');\n",gameid.c_str(),ctx.docnum); val += "\tvar rect = elm.parentNode.getBoundingClientRect();\n"; val += "\tvar trs = elm.getElementsByTagName('tr');\n"; val += "\tvar last=25;\n"; val += "\tfor (var i=0; i tags (so with an empty value). */ void CALC::update_cells(set &cells, VARVAL &var, bool optim) { if (cells.size() > 0){ // We always draw a fixed number of lines and columns. There is no ids for td tag. // So we count them var.val += "calc_loop_board(function(e,calc){\n"; var.val += "\tvar tds=e.getElementsByTagName('td');\n"; var.val += "\tconsole.log('update_cells calc='+calc);\n"; for (auto &c:cells){ string sessions; for (auto &p:prefs){ if (p.second.cursor == c){ if (sessions.size() > 0) sessions += ','; sessions += "'" + p.first + "'"; } } auto g = grid.find(c); if (!optim || g != grid.end() || sessions.size() > 0){ var.val += string_f("\tcalc.selcell(tds,%u,%u,function(td){\n",c.line,c.col); if (g != grid.end()){ const char *color = g->second.getcolor(); const char *align = g->second.getalign(); unsigned precision = 2; auto f = col_formats.find(c.col); if (f != col_formats.end()){ precision = f->second.precision; if (f->second.align != COL_DEFAULT) align = tbalign[f->second.align]; } var.val += string_f("\t\ttd.innerHTML='%s';\n",documentd_escape(g->second.gettext(precision)).c_str()); var.val += string_f("\t\ttd.style.color='%s';\n",color); var.val += string_f("\t\ttd.style.textAlign='%s';\n",align); }else{ var.val += "\t\ttd.innerHTML='';\n"; } if (sessions.size() > 0){ var.val += string_f("\t\tset_td_bg(td,[%s]);\n",sessions.c_str()); }else{ var.val += "\t\ttd.style.background='white';\n"; } var.val += "\t});\n"; } } var.val += "});\n"; } } /* Change the name of the current cell in the top status line. */ void CALC::update_cellname(CALC_PREF &pref, VARVAL &var) { var.val += string_f("var elm = document.getElementById('cell-%s');\n",gameid.c_str()); var.val += "if (elm != null){\n"; var.val += string_f("\telm.innerHTML='%s';\n",pref.cursor.tostring().c_str()); var.val += "}\n"; } /* Change the content of the edit field. */ void CALC::update_celledit(CALC_PREF &pref, VARVAL &var) { var.val += string_f("var elm = document.getElementById('edit-%s');\n",gameid.c_str()); var.val += "if (elm != null){\n"; const char *celltext = ""; auto c = grid.find(pref.cursor); if (c != grid.end()) celltext = c->second.text.c_str(); var.val += string_f("\telm.value='%s';\n",documentd_escape(celltext).c_str()); var.val += "}\n"; } /* Update the content of one cell. If the content is empty, the cell is removed from the grid map. */ void CALC::update_onecell (CALC_PREF &pref, PARAM_STRING buf) { if (buf.ptr[0] != '\0'){ auto &c = grid[pref.cursor]; c.text = buf.ptr; c.state = CELL_STATE_UNKNOWN; c.eval0(); }else{ auto g = grid.find(pref.cursor); if (g != grid.end()){ grid.erase(g); } } } void CALC::update_col_width(VARVAL &var, unsigned col) { unsigned width = DEFAULT_COL_WIDTH; auto f = col_formats.find(col); if (f != col_formats.end()) width = f->second.width; var.val += "calc_loop_board(function(e,calc){\n"; var.val += string_f("\tcalc.updcolwidth(e,%u,%u);\n",col,width); var.val += "});\n"; } void CALC::update_offsets(VARVAL &var, const DOC_CONTEXT &ctx, CALC_PREF &pref) { var.val += string_f("calc_%u.updoff(%u,%u);\n",ctx.docnum,pref.offset_line,pref.offset_col); } /* Insert a line or a column in the grid. This is done by copying the grid into a vector, updating the coordinate and rebuilding the grid. We update the formula as well. */ void CALC::insert_line_col(unsigned line, unsigned col, int offline, int offcol) { struct KEYVAL { CELL_COOR coor; CALC_CELL cell; KEYVAL(const CELL_COOR &_coor, const CALC_CELL &_cell):coor(_coor),cell(_cell){} }; vector vec; CELL_COOR ref (line,col); for (auto &g:grid){ CELL_COOR coor(g.first); if (coor.line >= line) coor.line++; if (coor.col >= col) coor.col++; g.second.applyoffset(ref,offline,offcol); vec.emplace_back(coor,g.second); } grid.clear(); for (auto &v:vec) grid[v.coor] = v.cell; struct KEYFORMAT{ unsigned col; CALC_COL_FORMAT format; KEYFORMAT (unsigned _col, const CALC_COL_FORMAT &_format) :col(_col),format(_format){} }; vector vecf; for (auto &f:col_formats){ unsigned first = f.first; if (first >= col) first++; vecf.emplace_back(first,f.second); } col_formats.clear(); for (auto &v:vecf) col_formats[v.col] = v.format; } void CALC::delete_line_col(unsigned line, unsigned col, int offline, int offcol) { struct KEYVAL { CELL_COOR coor; CALC_CELL cell; KEYVAL(const CELL_COOR &_coor, const CALC_CELL &_cell):coor(_coor),cell(_cell){} }; vector vec; CELL_COOR ref (line,col); for (auto &g:grid){ CELL_COOR coor(g.first); if (coor.line != line && coor.col != col){ if (coor.line > line) coor.line--; if (coor.col > col) coor.col--; g.second.applyoffset(ref,offline,offcol); vec.emplace_back(coor,g.second); } } grid.clear(); for (auto &v:vec) grid[v.coor] = v.cell; struct KEYFORMAT{ unsigned col; CALC_COL_FORMAT format; KEYFORMAT (unsigned _col, const CALC_COL_FORMAT &_format) :col(_col),format(_format){} }; vector vecf; for (auto &f:col_formats){ unsigned first = f.first; if (first != col){ if (first > col) first--; vecf.emplace_back(first,f.second); } } col_formats.clear(); for (auto &v:vecf) col_formats[v.col] = v.format; } /* Insert one line in the grid. Generate the javascript code. */ void CALC::insert_line(VARVAL &var, unsigned line) { insert_line_col(line,(unsigned)-1,1,0); // For each listener, we must remove the last and insert a new one, it it applies // The first contains the heading. var.val += "calc_loop_board(function(tbl,calc){\n"; var.val += string_f("\tvar offl=%u-calc.offline;\n",line); var.val += "console.log('offl='+offl);\n"; var.val += string_f("\tif (offl >= 0 && offl < %u){\n",TBL_FIXED_LINES); var.val += "\t\tvar trs = tbl.getElementsByTagName('tr');\n"; var.val += string_f("\t\ttrs[%u].parentNode.removeChild(trs[%u]);\n",TBL_FIXED_LINES,TBL_FIXED_LINES); var.val += "\t\tvar newtr = document.createElement('tr');\n"; var.val += "\t\ttrs[0].parentNode.insertBefore(newtr,trs[offl+1]);\n"; var.val += "\t\tvar newtd = document.createElement('td');\n"; var.val += "\t\tnewtd.style.textAlign='center';\n"; var.val += "\t\tnewtr.appendChild(newtd);\n"; var.val += string_f("\t\tfor (var i=0; i<%u; i++){\n",TBL_FIXED_COLUMNS); var.val += "\t\t\tvar newtd = document.createElement('td');\n"; var.val += "\t\t\tnewtr.appendChild(newtd);\n"; var.val += "\t\t}\n"; // Clear the cursor for all users on this line map sessions; // Sessions associated with one column on that line for (auto &p:prefs){ if (p.second.offset_line > line) p.second.offset_line++; if (p.second.cursor.line > line) p.second.cursor.line++; if (p.second.cursor.line == line){ auto &s = sessions[p.second.cursor.col]; if (s.size() > 0) s += ','; s += "'" + p.first + "'"; } } for (auto &s:sessions){ var.val += string_f("\t\tvar offc = %u - calc.offcol;\n",s.first); var.val += string_f("\t\tif(offc >= 0 && offc < %u){\n",TBL_FIXED_COLUMNS); var.val += "\t\t\tvar tds=trs[offl+2].getElementsByTagName('td');\n"; var.val += "\t\t\ttds[0].style.background='#E8E8E8';\n"; var.val += "\t\t\ttds[offc+1].style.background='white';\n"; var.val += "\t\t\tvar tds=trs[offl+1].getElementsByTagName('td');\n"; var.val += string_f("\t\t\tset_td_bg(tds[0],[%s]);\n",s.second.c_str()); var.val += string_f("\t\t\tset_td_bg(tds[offc+1],[%s]);\n",s.second.c_str()); var.val += "\t\t}\n"; } var.val += "\t}\n"; var.val += string_f("\tif(calc.offline > %u) calc.offline++;\n",line); var.val += "\tcalc.updoff(calc.offline,calc.offcol);\n"; var.val += "});\n"; } void CALC::insert_col(VARVAL &var, unsigned col) { insert_line_col((unsigned)-1,col,0,1); // For each listener, we must remove the last of a line and insert a new one, if it applies var.val += "calc_loop_board(function(tbl,calc){\n"; var.val += string_f("\tvar offc=%u-calc.offcol;\n",col); var.val += "console.log('offc='+offc);\n"; var.val += "\tif (offc >= 0 && offc < calc.columns){\n"; // Remove last col, insert one var.val += "\t\tvar cols = tbl.getElementsByTagName('col');\n"; var.val += "\t\tcols[0].parentNode.removeChild(cols[calc.columns]);\n"; var.val += "\t\tvar newcol = document.createElement('col');\n"; var.val += "\t\tcols[0].parentNode.insertBefore(newcol,cols[offc+calc.heading]);\n"; // Remote last td and insert one for each tr var.val += "\t\tvar trs = tbl.getElementsByTagName('tr');\n"; var.val += string_f("\t\tfor (var i=0; i<=%u; i++){\n",TBL_FIXED_LINES); var.val += "\t\t\tvar tds = trs[i].getElementsByTagName('td');\n"; var.val += string_f("\t\t\ttds[0].parentNode.removeChild(tds[%u]);\n",TBL_FIXED_COLUMNS); var.val += "\t\t\tvar newtd = document.createElement('td');\n"; var.val += "\t\t\tif (i==0){\n"; var.val += "\t\t\t\tnewtd.style.textAlign='center';\n"; var.val += "\t\t\t\tnewtd.style.background='#E8E8E8';\n"; var.val += "\t\t\t}\n"; var.val += "\t\t\ttds[0].parentNode.insertBefore(newtd,tds[offc+1]);\n"; var.val += "\t\t}\n"; var.val += "\t\tcalc.updoff(calc.offline,calc.offcol);\n"; // Clear the cursor for all users on this column // Clear and set the background on the column header var.val += "\t\tvar defcol='#E8E8E8';\n"; var.val += string_f("\t\tfor (var i=0; i<=%u; i++){\n",TBL_FIXED_LINES); var.val += "\t\t\tvar tds=trs[i].getElementsByTagName('td');\n"; var.val += "\t\t\tvar bgcol = window.getComputedStyle(tds[offc+2], null).getPropertyValue('background-color');\n"; var.val += "console.log('bgcol='+bgcol);\n"; var.val += "\t\t\tif(bgcol!=defcol){\n"; var.val += "\t\t\t\ttds[offc+1].style.background=bgcol;\n"; var.val += "\t\t\t\ttds[offc+2].style.background=defcol;\n"; var.val += "\t\t\t}else{\n"; var.val += "\t\t\t\ttds[offc+1].style.background=defcol;\n"; var.val += "\t\t\t}\n"; var.val += "\t\t\tdefcol='white';\n"; var.val += "\t\t}\n"; var.val += "\t}\n"; var.val += "});\n"; update_col_width(var,col); } void CALC::delete_line(VARVAL &var, unsigned line) { delete_line_col (line,(unsigned)-1,-1,0); // For each listener, we must remove the line and append a new one at the end, if it applies var.val += "calc_loop_board(function(tbl,calc){\n"; var.val += string_f("\tvar offl=%u-calc.offline;\n",line); var.val += "console.log('offl='+offl);\n"; var.val += string_f("\tif (offl >= 0 && offl < %u){\n",TBL_FIXED_LINES); var.val += "\t\tvar trs = tbl.getElementsByTagName('tr');\n"; var.val += "\t\ttrs[0].parentNode.removeChild(trs[offl+1]);\n"; var.val += "\t\tvar newtr = document.createElement('tr');\n"; var.val += "\t\ttrs[0].parentNode.appendChild(newtr);\n"; var.val += "\t\tvar newtd = document.createElement('td');\n"; var.val += "\t\tnewtd.style.textAlign='center';\n"; var.val += "\t\tnewtr.appendChild(newtd);\n"; var.val += string_f("\t\tfor (var i=0; i<%u; i++){\n",TBL_FIXED_COLUMNS); var.val += "\t\t\tvar newtd = document.createElement('td');\n"; var.val += "\t\t\tnewtr.appendChild(newtd);\n"; var.val += "\t\t}\n"; // Set the cursor for all users on this line map sessions; // Sessions associated with one column on that line for (auto &p:prefs){ if (p.second.offset_line > line) p.second.offset_line--; if (p.second.cursor.line > line) p.second.cursor.line--; if (p.second.cursor.line == line){ auto &s = sessions[p.second.cursor.col]; if (s.size() > 0) s += ','; s += "'" + p.first + "'"; } } for (auto &s:sessions){ var.val += string_f("\t\tvar offc = %u - calc.offcol;\n",s.first); var.val += string_f("\t\tif(offc >= 0 && offc < %u){\n",TBL_FIXED_COLUMNS); var.val += "\t\t\tvar tds=trs[offl+1].getElementsByTagName('td');\n"; var.val += string_f("\t\t\tset_td_bg(tds[0],[%s]);\n",s.second.c_str()); var.val += string_f("\t\t\tset_td_bg(tds[offc+1],[%s]);\n",s.second.c_str()); var.val += "\t\t}\n"; } var.val += "\t}\n"; var.val += string_f("\tif(calc.offline > %u) calc.offline--;\n",line); var.val += "\tcalc.updoff(calc.offline,calc.offcol);\n"; var.val += "});\n"; } void CALC::delete_col(VARVAL &var, unsigned col) { delete_line_col ((unsigned)-1,col,0,-1); // For each listener, we must remove the col of each line and append a new one, if it applies var.val += "calc_loop_board(function(tbl,calc){\n"; var.val += string_f("\tvar offc=%u-calc.offcol;\n",col); var.val += "console.log('offc='+offc);\n"; var.val += string_f("\tif (offc >= 0 && offc < %u){\n",TBL_FIXED_COLUMNS); // move the cursor for all users on this column to the next var.val += "\t\tvar trs = tbl.getElementsByTagName('tr');\n"; // Clear and set the background on the column header var.val += "\t\tvar defcol='#E8E8E8';\n"; var.val += string_f("\t\tfor (var i=0; i<%u; i++){\n",TBL_FIXED_LINES); var.val += "\t\t\tvar tds=trs[i].getElementsByTagName('td');\n"; var.val += "\t\t\tif(tds[offc+1].style.background!=defcol){\n"; var.val += "\t\t\t\ttds[offc+2].style.background=tds[offc+1].style.background;\n"; var.val += "\t\t\t}else{\n"; var.val += "\t\t\t\ttds[offc+2].style.background=defcol;\n"; var.val += "\t\t\t}\n"; var.val += "\t\t\tdefcol='white';\n"; var.val += "\t\t}\n"; // Remove current col, append one var.val += "\t\tvar cols = tbl.getElementsByTagName('col');\n"; var.val += "\t\tcols[0].parentNode.removeChild(cols[offc+1]);\n"; var.val += "\t\tvar newcol = document.createElement('col');\n"; var.val += string_f("\t\tnewcol.style.minWidth='%upx';\n",DEFAULT_COL_WIDTH); var.val += string_f("\t\tnewcol.style.maxWidth='%upx';\n",DEFAULT_COL_WIDTH); var.val += "\t\tcols[0].parentNode.appendChild(newcol);\n"; // Remote cur td and append one for each tr var.val += string_f("\t\tfor (var i=0; i<=%u; i++){\n",TBL_FIXED_LINES); var.val += "\t\t\tvar tds = trs[i].getElementsByTagName('td');\n"; var.val += "\t\t\ttds[0].parentNode.removeChild(tds[offc+1]);\n"; var.val += "\t\t\tvar newtd = document.createElement('td');\n"; var.val += "\t\t\tif (i==0){\n"; var.val += "\t\t\t\tnewtd.style.textAlign='center';\n"; var.val += "\t\t\t\tnewtd.style.background='#E8E8E8';\n"; var.val += "\t\t\t}\n"; var.val += "\t\t\ttds[0].parentNode.appendChild(newtd);\n"; var.val += "\t\t}\n"; var.val += "\t\tcalc.updoff(calc.offline,calc.offcol);\n"; var.val += "\t}\n"; var.val += "});\n"; } // Execute a one line scroll void CALC::vscroll (VARVAL &var, CALC_PREF &pref, int move) { unsigned old_offset = pref.offset_line; if (move == -1){ if (pref.offset_line > 0) pref.offset_line--; }else{ pref.offset_line++; } if (old_offset != pref.offset_line){ set update_ids; unsigned newline = move < 0 ? 0 : TBL_FIXED_LINES; newline += pref.offset_line; for (unsigned col=0; col 0) pref.offset_col--; }else{ pref.offset_col++; } if (old_offset != pref.offset_col){ set update_ids; unsigned newcol = move < 0 ? 0 : TBL_FIXED_COLUMNS; newcol += pref.offset_col; for (unsigned line=0; line void CALC::remove_session (const char *session) { auto p = prefs.find(session); if (p != prefs.end()) prefs.erase(p); //tlmp_warning ("CALC::remove_session %s -> %d %lu",session,p!=prefs.end(),prefs.size()); } void CALC::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, std::vector &unotifies, string &error, string &status) { auto &pref = prefs[ctx.session]; string api_error; setactivity(); string tmpvar,tmpval; unsigned lastline = 25; unsigned lastcol = 8; if (strcmp(var,"kbd")==0){ wordproc_kbd(val,pref.mod,tmpvar,tmpval,lastline,lastcol); var = tmpvar.c_str(); val = tmpval.c_str(); //tlmp_warning ("var=%s val=%s lastline=%u lastcol=%u",var,val,lastline,lastcol); } if (strcmp(var,REQ_PRINT)==0){ if (is_any_of(val,"","full")){ VARVAL v; glocal CALC *doc = this; glocal pref; glocal grid; (val,this,ctx,sp,"calc",true,is_eq(val,"full"),v); return glocal.doc->define_styles(ctx,sp); return glocal.doc->define_functions(ctx,glocal.pref); #define BUTTON_NEWDOC 0 #define BUTTON_COLSTYLE 1 #define BUTTON_INSCOLLINE 2 CALC_MENU menu(specs); documentd_bar_button (lines,BUTTON_NEWDOC,menu.svg_clear,specs,false,MSG_U(I_RESETDOC,"Reset document")); documentd_bar_button (lines,BUTTON_INSCOLLINE,"+",specs,false,MSG_U(I_INSCOLLINE,"Insert/Delete lines or columns")); documentd_bar_button (lines,BUTTON_COLSTYLE,"F",specs,false,MSG_U(I_FORMATCELL,"Specify format for column cells")); lines += string_f("page 1\n",gameid); lines += string_f("%s\n",gameid,glocal.pref.cursor.tostring().c_str()); const char *content = ""; auto g = glocal.grid.find(glocal.pref.cursor); if (g != glocal.grid.end()) content = g->second.text.c_str(); lines += string_f("\n" ,gameid,documentd_escape_html(content).c_str()); onevent = "onkeydown='calc_gamepress(event);return false;'"; return glocal.doc->draw_board(ctx,glocal.pref,board_width,board_height,sp.fontsize,ctx.docnum,true,CELL_COOR(),script); return string_f("\n" ,gameid,glocal.pref.offset_col*board_width/(4*board_width),board_width/4,scroll_thick); return string_f("\n" ,gameid,glocal.pref.offset_line*board_height/(4*board_height),scroll_thick,board_height/4); res.emplace_back(move(v)); } }else if (strcmp(var,REQ_FOCUS)==0){ setfocus(script_var); }else if (strcmp(var,REQ_FUNCTIONS)==0){ VARVAL var; var.var = VAR_DEFSCRIPT; var.val = define_functions (ctx,pref); 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 // A region is either a range (a1:c3) or a name (not done yet) CELL_COOR start,end; if (calc_parserange(val,start,end)){ CALC_PREF p; p.offset_line = start.line; p.offset_col = start.col; CELL_COOR area; area.line = end.line - start.line+1; area.col = end.col - start.col+1; VARVAL var,var_script; var.var = VAR_CONTENT; var_script.var = VAR_DEFSCRIPT; var.val += "\n"; // trick to pass rendering information to the parent document var.val += draw_board(ctx,p,0,0,sp.fontsize,ctx.docnum,false,area,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 (strcmp(val,DIALOG_CALC_FORMATCELL)==0){ // We return the format of the currently selected cell, or selected column. auto c = col_formats.find(pref.cursor.col); CALC_COL_FORMAT format; if (c != col_formats.end()) format = c->second; var.val = string_f("width:%u\nprecision:%u\nalign:%u\n",format.width,format.precision,format.align); } res.emplace_back(var); }else if (strcmp(var,"dump")==0){ VARVAL var; var.var = "elements"; var.val += "\n"; static const char *tb[]={"UNKNOWN","NUM","TEXT","FORMULAERR","FORMULA","COMPUTING","EVALED"}; static_assert(sizeof(tb)/sizeof(tb[0])==CELL_STATE_LAST,"tb is incomplete"); for (auto &g:grid){ var.val += string_f("[%u,%u]=%s,%s,%lf\n",g.first.line,g.first.col,tb[g.second.state],g.second.text.c_str(),g.second.value); } res.emplace_back(var); }else if (is_any_of(var,KBD_PAGEUP,KBD_PAGEDOWN)){ if (is_eq(var,KBD_PAGEUP)){ if (pref.offset_line < lastline){ if (pref.cursor.line >= lastline) pref.cursor.line -= pref.offset_line; pref.offset_line = 0; }else{ pref.offset_line -= lastline; pref.cursor.line -= lastline; } }else{ pref.offset_line += lastline; pref.cursor.line += lastline; } update_offsets(script_var,ctx,pref); set ids; for (unsigned line=0; linesecond.text; } if (is_eq(var,KBD_DELETECHAR)){ buf.clear(); }else if (is_eq(var,KBD_BACKSPACE)){ documentd_eraselast(buf); }else{ buf += val; } update_onecell (pref,buf); update_celledit (pref,script_var); notify_ids.insert(pref.cursor); setmodified(ctx.username); }else if (is_any_of(var,KBD_VMOVE,KBD_BREAK)){ int move; if (is_eq(var,KBD_BREAK)){ move = 1; }else{ move = atoi(val); } if (move > 0){ pref.cursor.line += move; }else if (-move <= pref.cursor.line){ pref.cursor.line += move; } if (pref.cursor.line < pref.offset_line || pref.cursor.line > pref.offset_line+lastline-1){ vscroll (script_var,pref,move); } }else if (is_any_of(var,KBD_HMOVE,KBD_TAB)){ int move; bool shift = pref.mod.shift; if (is_eq(var,KBD_TAB)){ move = shift ? -1 : 1; shift = false; }else{ move = atoi(val); } if (shift){ // Modify the current column width unsigned width = DEFAULT_COL_WIDTH; auto c = col_formats.find(pref.cursor.col); if (c != col_formats.end()) width = c->second.width; width += move*5; if (width != DEFAULT_COL_WIDTH){ col_formats[pref.cursor.col].width = width; }else if (c != col_formats.end()){ col_formats.erase(c); } update_col_width(notify_var,pref.cursor.col); setmodified (ctx.username); }else{ if (move > 0){ pref.cursor.col += move; }else if (-move <= pref.cursor.col){ pref.cursor.col += move; } if (pref.cursor.col < pref.offset_col || pref.cursor.col > pref.offset_col+lastcol-1){ hscroll (script_var,pref,move); } } }else if (strcmp(var,"select")==0){ auto tb = str_splitline(val,','); if (tb.size() == 6){ unsigned cell = atoi(tb[0].c_str()); #if 0 unsigned x = atoi(tb[1].c_str()); unsigned y = atoi(tb[2].c_str()); unsigned button = atoi(tb[3].c_str()); bool shiftkey = tb[4] == "true"; bool ctrlkey = tb[5] == "true"; #endif unsigned line = pref.offset_line + cell/(TBL_FIXED_COLUMNS+1); unsigned col = pref.offset_col + cell%(TBL_FIXED_COLUMNS+1); if (line > 0 && col > 0){ pref.cursor.line = line-1; pref.cursor.col = col-1; }else if (line == 0){ // Select one column }else if (col == 0){ // Select one line } } }else if (strcmp(var,"mousemove")==0){ }else if (strcmp(var,"newgame")==0){ int uval = atoi(val); if (uval == BUTTON_RELOAD){ documentd_action_reload(res); }else if (uval == BUTTON_NEWDOC){ VARVAL var; var.var = VAR_DIALOG; var.val = DIALOG_CALC_NEW; res.emplace_back(var); }else if (uval == BUTTON_COLSTYLE){ VARVAL var; var.var = VAR_DIALOG; var.val = DIALOG_CALC_FORMATCELL; res.emplace_back(var); }else if (uval == BUTTON_INSCOLLINE){ VARVAL var; var.var = VAR_DIALOG; var.val = DIALOG_CALC_INSCOLLINE; res.emplace_back(var); }else{ tlmp_error ("calc newgame=%d",uval); } setfocus(script_var); }else if (strcmp(var,"edit")==0){ const char *pt = strchr(val,','); if (pt != nullptr){ // we only care about the key transfered if it is a Enter, so we change cell // tlmp_warning ("edit val=%s",val); string key = string(val,pt-val); pt++; update_onecell(pref,pt); notify_ids.insert (pref.cursor); setmodified(ctx.username); if (key=="Enter"){ pref.cursor.line++; notify_ids.insert (pref.cursor); setfocus(script_var); }else if (key=="Tab"){ pref.cursor.col++; notify_ids.insert (pref.cursor); setfocus(script_var); } } // The other actions are Used to script modifications of the document }else if (strcmp(var,"resetgame")==0){ resetgame(); // We erased all the td tag in the table except the first line and the first column notify_var.val += "calc_loop_board(function(tbl){\n"; notify_var.val += "\tvar tds = tbl.getElementsByTagName('td');\n"; notify_var.val += string_f("\tfor (var i=1; i<=%u; i++){\n",TBL_FIXED_LINES); notify_var.val += string_f("\t\tfor (var j=1; j<=%u; j++){\n",TBL_FIXED_COLUMNS); notify_var.val += string_f("\t\t\ttds[i*%u+j].innerHTML = '';\n",TBL_FIXED_COLUMNS+1); notify_var.val += "\t\t}\n"; notify_var.val += "\t}\n"; notify_var.val += "\tvar cols = tbl.getElementsByTagName('col');\n"; notify_var.val += string_f("\tfor (var j=1; j<=%u; j++){\n",TBL_FIXED_COLUMNS); notify_var.val += string_f("\t\tcols[j].style.minWidth='%upx';\n",DEFAULT_COL_WIDTH); notify_var.val += string_f("\t\tcols[j].style.maxWidth='%upx';\n",DEFAULT_COL_WIDTH); notify_var.val += "\t}\n"; notify_var.val += "});\n"; // Reset position of all users for (auto &p:prefs){ if (p.second.is_modified()) p.second.reset(); } update_offsets (notify_var,ctx,pref); setmodified(ctx.username); }else if (strcmp(var,"inscolline")==0){ if (strcmp(val,"inslines")==0){ insert_line (notify_var,pref.cursor.line); }else if (strcmp(val,"inscolumns")==0){ insert_col (notify_var,pref.cursor.col); }else if (strcmp(val,"dellines")==0){ delete_line (notify_var,pref.cursor.line); }else if (strcmp(val,"delcolumns")==0){ delete_col (notify_var,pref.cursor.col); } update_celledit (pref,script_var); setmodified(ctx.username); setfocus (script_var); }else if (strcmp(var,"formatcol")==0){ vector fields; documentd_parsefields(val,fields); CALC_COL_FORMAT format; bool error = false; for (auto &f:fields){ if (f.var == "width"){ format.width = atoi(f.val.c_str());; }else if (f.var == "precision"){ format.precision = atoi(f.val.c_str()); }else if (f.var == "align"){ static map s2a={ {"default",COL_DEFAULT},{"left",COL_LEFT},{"center",COL_CENTER},{"right",COL_RIGHT}}; auto s = s2a.find(f.val); if (s != s2a.end()){ format.align = s->second; }else{ error = true; } } } if (!error){ CALC_COL_FORMAT old_format; if (format.is_default()){ auto c = col_formats.find(pref.cursor.col); if (c != col_formats.end()){ old_format = c->second; col_formats.erase(c); } }else{ auto &f = col_formats[pref.cursor.col]; old_format = f; f = format; } if (format.width != old_format.width){ update_col_width(notify_var,pref.cursor.col); } if (format.precision != old_format.precision || format.align != old_format.align){ // We have to update all cells in the column, but we only send updates for // cells actually visible by some users. for (auto &pp:prefs){ auto &p = pp.second; if (pref.cursor.col >= p.offset_col && pref.cursor.col < p.offset_col + TBL_FIXED_COLUMNS){ CELL_COOR cell (0,pref.cursor.col); for (cell.line=p.offset_line; cell.line < p.offset_line+TBL_FIXED_LINES; cell.line++) notify_ids.insert(cell); } } } setmodified(ctx.username); } }else if (strcmp(var,"setcells")==0){ auto tb = str_splitlineq(val,','); CELL_COOR start,end; if (tb.size() >= 2 && calc_parserange(tb[0],start,end)){ if (start.line != end.line){ api_error = MSG_U(E_RANGELINE,"range must be on the same line"); }else if (start.col > end.col){ api_error = MSG_R(E_IVLDRANGE_COL); }else if ((unsigned)(end.col-start.col)+1 != tb.size()-1){ api_error = MSG_U(E_IVLDNBCELLS,"invalid number of cells supplied"); }else{ for (unsigned i=1; i 0) content += ','; content += getvals ? c.gettext(2) : c.text; nocol++; if (nocol == nbcol){ content += '\n'; nocol = 0; } }); VARVAL var; var.var = VAR_RESULT; var.val = move(content); res.emplace_back(move(var)); } } if (old_cursor != pref.cursor){ notify_ids.insert(old_cursor); notify_ids.insert(pref.cursor); update_cellname (pref,script_var); update_celledit (pref,script_var); update_lines_cols (pref,old_cursor,pref.cursor,script_var); } }else{ error = MSG_U(E_READONLYDOC,"You do not have write access to this document"); } if (api_error.size() > 0){ VARVAL var; var.var = VAR_ERROR; var.val = move(api_error); res.emplace_back(move(var)); } if (!pref.is_modified()){ // The prefs was allocated on the fly at the start of this function, but were not used auto p = prefs.find(ctx.session); if (p != prefs.end()) prefs.erase(p); } } void CALC::manyexec ( const vector &steps, const DOC_CONTEXT &ctx, const DOC_UI_SPECS_receive &sp, vector &res, 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 string error,status; for (auto &v:steps){ execstep (v.var,v.val,ctx,sp,script_var,notify_var,notify_ids,res,unotifies,error,status); } if (error.size() == 0){ reset_eval(); eval (notify_ids,error); } 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); } update_cells(notify_ids,notify_var,false); if (notify_var.val != cur_whi) res.emplace_back(move(notify_var)); if (script_var.val != cur_whi) res.emplace_back(move(script_var)); }