initially adding the graph and presentation ipelets
[Misc/ipe.git] / ipelets / presentation / presentation.lua
1 ----------------------------------------------------------------------
2 -- presentation goodies for Ipe
3
4 --[[
5
6 SUMMARY
7
8  This ipelet adds a few goodies for presentation, including
9   1. Ability to create beamer-like boxes with/without header
10   2. Add a framed box for the selected objects (including text)
11   3. A function to deselect all selected on all pages
12   4. The boxes can be edited as text/path objects (press E)
13   5. A few items can be added to the style sheet to add preferred
14      symbolics for box colors etc. This has the benefit that this
15      preferences can be changed to affect all boxes (see below).
16
17   The design of these boxes is from Martin Nöllenburg's presentation
18   example posted on the Ipe7 wiki page.
19
20 STYLESHEET (CHANGING PREFERRED SETTINGS)
21
22  Example style sheet to cascade with presentation.isy is as follows.
23
24 ---- content of example.isy ---
25 example removed, not possible to upload this file to the ipe-wiki otherwise
26 ---- end content of example.isy ---
27
28  where:
29   tab_header= color of the tab header in a tabbed box
30   tab_body  = color of the tab body in a tabbed box
31   box_fill  = fill color a box
32   box_border= color of the box border
33   boxborder = linewidth of the box border
34
35  The preferred box mode (stroked/filled/strokedfilled) can be changed by
36  changing the hard-wired value (no stylesheet option) PREFERRED_BOX_MODE below
37
38  With the above style sheet, one can start an empty presentation using
39   ipe -sheet presentation -sheet /path/to/example.isy
40
41 SHORTCUT
42
43  Shortcuts to these functions can be changed as for other ipelets:
44         shortcuts.ipelet_x_presentation = "Key"
45  where x is the index (starting 1) of the sub-menu for the function
46
47 FILE/AUTHOR HISTORY
48
49  version  0. Initial Release. Zhengdao Wang 2010
50  version  1. add a line here if a change is made
51
52 LICENSE
53
54  This file can be distributed and modified under the terms of the GNU General
55  Public License as published by the Free Software Foundation; either version
56  3, or (at your option) any later version.
57
58  This file is distributed in the hope that it will be useful, but WITHOUT ANY
59  WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
60  FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
61  details.
62
63 --]]
64
65 ----------------------------------------------------------------------
66
67 label = "Presentation"
68
69 about = [[
70         Presentation Goodies: Add boxes around objects, deselect all,
71         add tabbed/boxed text.
72 ]]
73
74 V = ipe.Vector
75 indexOf=_G.indexOf
76
77 -- table storing the height of the first line box
78 Height={}
79 Spacing={}
80 Has_Height=nil
81
82 local c=10 -- corner size
83
84 local UNKNOWN=0
85 local TABBED_TEXT=1
86 local BOXED_TEXT=2
87 local BOXED_OTHER=3
88
89 local PREFERRED_BOX_MODE="strokedfilled" -- or stroked or filled
90
91 local BOX_DIALOG_SIZE={400,140}
92
93 -- initialize the Height table
94 local function init_spacing(model)
95         local p=model:page()
96         local xx=300
97         local iH={}
98         local sizes = model.doc:sheets():allNames("textsize")
99         for i,size in ipairs(sizes) do
100                 obj=ipe.Text({textsize=size, minipage=true}, "ABC\n\nDEF", V(xx,xx), xx)
101                 local layer=p:active(model.vno)
102                 p:insert(nil, obj, nil, layer)
103                 obj=ipe.Text({textsize=size, minipage=true}, "ABC", V(xx,xx), xx)
104                 p:insert(nil, obj, nil, layer)
105                 iH[#iH+1]=size
106         end
107         model.doc:runLatex()
108         for i=#sizes,1,-1 do
109                 Height[iH[i]]=xx-p:bbox(#p):bottomLeft().y
110                 p:remove(#p)
111                 Spacing[iH[i]]=(xx-p:bbox(#p):bottomLeft().y)/2-Height[iH[i]]
112                 p:remove(#p)
113         end
114         Has_Height=1
115 end
116
117 -- generate a path with given properties
118 local function path(model, shape, props)
119         local oldvalues={}
120         for k,v in pairs(props) do
121                 oldvalues[k]=model.attributes[k]
122                 model.attributes[k]=v
123         end
124         local obj = ipe.Path(model.attributes, shape)
125         for k,v in pairs(props) do
126                 model.attributes[k]=oldvalues[k]
127         end
128         if props.pen then obj:set('pen', props.pen) end
129         obj:set('pathmode', props.pathmode, props.stroke, props.fill)
130         return obj
131 end
132
133 -- create a square box #shape=3
134 local function boxshape_square(v1, v2)
135         return { type="curve", closed=true;
136                  { type="segment"; v1, V(v1.x, v2.y) }, -- L
137                  { type="segment"; V(v1.x, v2.y), v2 }, -- T
138                  { type="segment"; v2, V(v2.x, v1.y) } } -- R
139 end
140
141 -- create a square box with a pointer #shape=6
142 local function boxshape_square_pointer(v1, v2, v3, v4, v5)
143         local dx=v2.x-v1.x
144         local dy=v2.y-v1.y
145         local v3=v3 or V(v1.x+.4*dx, v2.y)
146         local v4=v4 or V(v1.x+.6*dx,v2.y+.3*dy)
147         local v5=v5 or V(v1.x+.5*dx,v2.y)
148         return { type="curve", closed=true;
149                  { type="segment"; v1, V(v1.x, v2.y) }, -- L
150                  { type="segment"; V(v1.x, v2.y), v3}, -- T
151                  { type="segment"; v3, v4}, -- P
152                  { type="segment"; v4, v5}, --P
153                  { type="segment"; v5, v2}, -- T
154                  { type="segment"; v2, V(v2.x, v1.y) } } -- R
155 end
156
157 -- create a header box: round corner on topRight
158 local function boxshape_roundTR(v1, v2)
159         return { type="curve", closed=true;
160                  { type="segment"; v1, V(v1.x, v2.y) },
161                  { type="segment"; V(v1.x, v2.y), V(v2.x-c, v2.y) },
162                  { type="bezier"; V(v2.x-c, v2.y), V(v2.x-c/2, v2.y),
163                         V(v2.x, v2.y-c/2), V(v2.x, v2.y-c) },
164                  { type="segment"; V(v2.x, v2.y-c), V(v2.x, v1.y) } }
165 end
166
167 -- create a body box: round corner on bottom Left
168 local function boxshape_roundLL(v1, v2)
169         return { type="curve", closed=true;
170                  { type="segment"; v2, V(v2.x, v1.y) },
171                  { type="segment"; V(v2.x, v1.y), V(v1.x+c, v1.y) },
172                  { type="bezier"; V(v1.x+c, v1.y), V(v1.x+c/2,v1.y),
173                         V(v1.x, v1.y+c/2), V(v1.x, v1.y+c) },
174                  { type="segment"; V(v1.x, v1.y+c), V(v1.x, v2.y) } }
175 end
176
177 -- create a body box: 4 round corners #shape=8
178 local function boxshape_round(v1, v2)
179         return { type="curve", closed=true;
180                  { type="segment"; V(v2.x, v2.y-c), V(v2.x, v1.y+c) }, -- R
181                  { type="bezier"; V(v2.x,v1.y+c), V(v2.x, v1.y+c/2),
182                         V(v2.x-c/2,v1.y), V(v2.x-c,v1.y)}, -- BR
183                  { type="segment"; V(v2.x-c, v1.y), V(v1.x+c, v1.y) }, -- B
184                  { type="bezier"; V(v1.x+c, v1.y), V(v1.x+c/2,v1.y),
185                         V(v1.x, v1.y+c/2), V(v1.x, v1.y+c) }, -- BL
186                  { type="segment"; V(v1.x, v1.y+c), V(v1.x, v2.y-c) }, -- L
187                  { type="bezier"; V(v1.x, v2.y-c), V(v1.x,v2.y-c/2),
188                         V(v1.x+c/2, v2.y), V(v1.x+c, v2.y) }, -- TL
189                  { type="segment"; V(v1.x+c, v2.y), V(v2.x-c, v2.y) }, -- T
190                  { type="bezier"; V(v2.x-c, v2.y), V(v2.x-c/2,v2.y),
191                         V(v2.x, v2.y-c/2), V(v2.x, v2.y-c) }, -- TR
192                 }
193 end
194
195 -- create a body box: 4 round corners, with pointer #shape=11
196 local function boxshape_round_pointer(v1, v2, v3, v4, v5)
197         local dx=v2.x-v1.x
198         local dy=v2.y-v1.y
199         local v3=v3 or V(v1.x+.4*dx, v2.y)
200         local v4=v4 or V(v1.x+.6*dx,v2.y+.3*dy)
201         local v5=v5 or V(v1.x+.5*dx,v2.y)
202         return { type="curve", closed=true;
203                  { type="segment"; V(v2.x, v2.y-c), V(v2.x, v1.y+c) }, -- R
204                  { type="bezier"; V(v2.x,v1.y+c), V(v2.x, v1.y+c/2),
205                         V(v2.x-c/2,v1.y), V(v2.x-c,v1.y)}, -- BR
206                  { type="segment"; V(v2.x-c, v1.y), V(v1.x+c, v1.y) }, -- B
207                  { type="bezier"; V(v1.x+c, v1.y), V(v1.x+c/2,v1.y),
208                         V(v1.x, v1.y+c/2), V(v1.x, v1.y+c) }, -- BL
209                  { type="segment"; V(v1.x, v1.y+c), V(v1.x, v2.y-c) }, -- L
210                  { type="bezier"; V(v1.x, v2.y-c), V(v1.x,v2.y-c/2),
211                         V(v1.x+c/2, v2.y), V(v1.x+c, v2.y) }, -- TL
212                  { type="segment"; V(v1.x+c, v2.y), v3}, -- T
213                  { type="segment"; v3, v4}, -- P
214                  { type="segment"; v4, v5}, --P
215                  { type="segment"; v5, V(v2.x-c, v2.y)}, -- T
216                  { type="bezier"; V(v2.x-c, v2.y), V(v2.x-c/2,v2.y),
217                         V(v2.x, v2.y-c/2), V(v2.x, v2.y-c) }, -- TR
218                 }
219 end
220
221 -- parse the values from a group obj
222 local function parse_group_values(model,prim)
223         local fs = model.doc:sheets():find("layout").framesize
224         local p=model:page()
225         local bbox=p:bbox(prim)
226         local pos=V(bbox:bottomLeft().x, bbox:topRight().y)
227
228         local elements=p[prim]:elements()
229         if #elements==4 then
230                 local hb,bb,ht,bt=elements[1],elements[2],elements[3],elements[4]
231                 if hb:type()=="path" and bb:type()=="path" and
232                         ht:type()=="text" and bt:type()=="text" then
233                         local values={htext=ht:text(),
234                                 btext=bt:text(),
235                                 pinned=(p[prim]:get("pinned")=="horizontal"),
236                                 fwidth=string.format('%.2f',
237                                         (bbox:topRight().x-bbox:bottomLeft().x)/fs.x),
238                                 hcolor=hb:get("fill"),
239                                 bcolor=bb:get("fill"),
240                                 size=ht:get("textsize")}
241                         return TABBED_TEXT,values,pos
242                 end
243         else
244                 local bb,bt=elements[1],elements[2]
245                 if bb:type()=="path" and #bb:shape()==1 and
246                         bb:shape()[1].closed==true and
247                         (#bb:shape()[1]==3 or #bb:shape()[1]==8 or
248                         #bb:shape()[1]==6 or #bb:shape()[1]==11) then
249                         if bt:type()=="text" then
250                                 local values={btext=bt:text(),
251                                         pinned=(p[prim]:get("pinned")=="horizontal"),
252                                         size=bt:get("textsize"),
253                                         fwidth=string.format('%.2f',
254                                                 (bbox:topRight().x-bbox:bottomLeft().x)/fs.x),
255                                         bcolor=bb:get("fill")}
256                                 if #bb:shape()[1]==6 then
257                                         pos=V(pos.x,bb:shape()[1][1][2].y)
258                                 elseif #bb:shape()[1]==11 then
259                                         pos=V(pos.x,bb:shape()[1][10][2].y)
260                                 end
261                                 return BOXED_TEXT,values,pos
262                         else
263                                 return BOXED_OTHER
264                         end
265                 end
266         end
267         return UNKNOWN
268 end
269
270 function mainWindow(model)
271    if model.ui.win == nil then
272       return model.ui
273    else
274       return model.ui:win()
275    end
276 end
277
278 -- Edit the values for the frame
279 local function edit_tabbed_values(model,values)
280         local d = ipeui.Dialog(mainWindow(model), "Create tabbed text")
281         local colors = model.doc:sheets():allNames("color")
282         local sizes = model.doc:sheets():allNames("textsize")
283         d:add("hlabel", "label", { label="Enter Header" }, 1, 1, 1, 1)
284         d:add("hcolor", "combo", colors, 1, 4)
285         d:add("htext", "input", { syntax="latex" }, 2, 1, 1, 4)
286         d:add("blabel", "label", { label="Enter Body"}, 3, 1, 1, 3)
287         d:add("bcolor", "combo", colors, 3, 4)
288         d:add("btext", "text", { syntax="latex" }, 4, 1, 1, 4)
289         d:add("size", "combo", sizes, 5, 1)
290         d:add("wlabel", "label", { label="width [0-1]"}, 5, 2, 1, 2)
291         d:add("fwidth", "input", {size=2}, 5, 3, 1, 1)
292         d:add("pinned", "checkbox", { label="pinned"}, 5, 4)
293         d:add("ok", "button", { label="&Ok", action="accept" }, 6, 4)
294         d:add("cancel", "button", { label="&Cancel", action="reject" }, 6, 3)
295         _G.addEditorField(d, "btext", 6, 2)
296         d:setStretch("row", 2, 1)
297         d:setStretch("column", 1, 1)
298         d:set("fwidth", "0.8")
299         d:set("pinned", 1)
300
301         if indexOf("tab_header", colors) then
302                 d:set("hcolor", indexOf("tab_header", colors))
303         elseif indexOf("darkblue", colors) then
304                 d:set("hcolor", indexOf("darkblue", colors))
305         else
306                 d:set("hcolor", indexOf("black", colors))
307         end
308
309         if indexOf("tab_body", colors) then
310                 d:set("bcolor", indexOf("tab_body", colors))
311         elseif indexOf("lightgray", colors) then
312                 d:set("bcolor", indexOf("lightgray", colors))
313         else
314                 d:set("bcolor", indexOf("white", colors))
315         end
316
317         if values then
318                 for k,v in pairs(values) do
319                         if k=="hcolor" or k=="bcolor" then v=indexOf(v, colors) end
320                         if k=="size" then v=indexOf(v, sizes) end
321                         d:set(k, v)
322                 end
323         end
324
325         local r = d:execute(prefs.editor_size)
326         if not r then return end
327         local newvalues={}
328         newvalues.htext=d:get("htext")
329         newvalues.btext=d:get("btext")
330         newvalues.pinned=d:get("pinned")
331         newvalues.fwidth=d:get("fwidth")
332         newvalues.size=sizes[d:get("size")]
333         newvalues.hcolor=colors[d:get("hcolor")]
334         newvalues.bcolor=colors[d:get("bcolor")]
335 --      if newvalues.fwidth=="" or tonumber(newvalues.fwidth)>.99 then
336 --              newvalues.pinned=true
337 --      end
338         return newvalues
339 end
340
341 -- measure the height a piece of given text
342 local function measure_height(model,text,size,width)
343         local p=model:page()
344         local obj= ipe.Text(model.attributes, text, V(0,0), width)
345         obj:set('textsize', size)
346         local layer=p:active(model.vno)
347         p:insert(nil, obj, nil, layer)
348         if not model.doc:runLatex() then
349                 p:remove(#p)
350                 return 100
351         end
352         local bbox=p:bbox(#p)
353         p:remove(#p)
354         return bbox:topRight().y-bbox:bottomLeft().y
355 end
356
357 -- Create boxed text
358 local function create_boxed(model,values, pos, prim)
359         local fs = model.doc:sheets():find("layout").framesize
360         local p = model:page()
361         local editmode=(prim~=nil)
362
363         local width fwidth=tonumber(values.fwidth)
364
365         if not fwidth or fwidth<0 or fwidth>1 then
366                 width=fs.x
367                 fwidth=1
368         else
369                 width=fwidth*fs.x
370         end
371
372         -- spacing
373         local s=Spacing[values.size]
374         local h=Height[values.size]
375         if not s or not h then
376                 init_spacing(model)
377                 s=Spacing[values.size]
378                 h=Height[values.size]
379         end
380
381         local bheight=measure_height(model,values.btext,values.size,width-2*s)
382
383         -- location
384         if not pos then
385                 x1=fs.x/2-width/2
386                 x2=fs.x/2+width/2
387                 y2=fs.y/2
388                 y1=y2-bheight-1.8*s
389         else
390                 x1=pos.x
391                 x2=x1+width
392                 y2=pos.y
393                 y1=y2-bheight-1.8*s
394         end
395         if fwidth>.99 then x1,x2=0,fs.x end
396
397         -- body text
398         pos=V(x1+s, y2-s)
399         local bt= ipe.Text(model.attributes, values.btext, pos, width-2*s)
400         bt:set('textsize', values.size)
401
402         -- body box
403         local shape2
404         if values.rounded then
405                 shape2 = { boxshape_round(V(x1,y1), V(x2,y2)) }
406         else
407                 shape2 = { boxshape_square(V(x1,y1), V(x2,y2)) }
408         end
409         local bb = path(model, shape2,
410                 {pathmode='filled', fill=values.bcolor, stroke="white"})
411
412         -- group object
413         local elements={bb,bt}
414         local obj=ipe.Group(elements)
415         -- obj:setMatrix(p[prim]:matrix()) -- currently not working
416         if values.pinned then obj:set('pinned', 'horizontal') end
417
418         if editmode then
419                 local t={original=p[prim]:clone(),
420                                 label="edit boxed text",
421                                 pno=model.pno,
422                                 vno=model.vno,
423                                 primary=prim,
424                                 final=obj }
425                 t.undo = function (t, doc)
426                                          doc[t.pno]:replace(t.primary, t.original)
427                                  end
428                 t.redo = function (t, doc)
429                                          doc[t.pno]:replace(t.primary, t.final)
430                                  end
431                 model:register(t)
432         else
433                 model:creation("create boxed text", obj)
434                 -- model.doc:runLatex() -- may crash the thing
435         end
436 end
437
438 -- Create the requested object from values
439 local function create_tabbed(model,values, pos, prim)
440         local fs = model.doc:sheets():find("layout").framesize
441         local p = model:page()
442         local editmode=(prim~=nil)
443
444         local width fwidth=tonumber(values.fwidth)
445
446         if not fwidth or fwidth<0 or fwidth>1 then
447                 width=fs.x
448                 fwidth=1
449         else
450                 width=fwidth*fs.x
451         end
452
453         -- spacing
454         local s=Spacing[values.size]
455         local h=Height[values.size]
456         if not s or not h then
457                 init_spacing(model)
458                 s=Spacing[values.size]
459                 h=Height[values.size]
460         end
461
462         local bheight=measure_height(model,values.btext,values.size,width-2*s)
463
464         -- location
465         if not pos then
466                 x1=fs.x/2-width/2
467                 x2=fs.x/2+width/2
468                 y2=fs.y/2
469                 y1=y2-h-bheight-3.8*s
470         else
471                 x1=pos.x
472                 x2=x1+width
473                 y2=pos.y
474                 y1=y2-h-bheight-3.8*s
475         end
476         if fwidth>.99 then x1,x2=0,fs.x end
477
478         -- header text
479         pos=V(x1+s, y2-s)
480         local ht= ipe.Text(model.attributes, values.htext, pos, width-2*s)
481         ht:set('stroke', 'white')
482         ht:set('textsize', values.size)
483
484         -- body text
485         pos=V(x1+s, y2-s-h-2*s)
486         local bt= ipe.Text(model.attributes, values.btext, pos, width-2*s)
487         bt:set('textsize', values.size)
488
489         -- header box
490         local shape1 = { boxshape_roundTR(V(x1,y2-h-2*s), V(x2,y2)) }
491         local hb = path(model, shape1,
492                 {pathmode='filled', fill=values.hcolor, stroke="white"})
493                 hb:set('pathmode', 'filled', "white", values.hcolor)
494
495         -- body box
496         local shape2 = { boxshape_roundLL(V(x1,y1), V(x2,y2-h-2*s)) }
497         local bb = path(model, shape2,
498                 {pathmode='filled', fill=values.bcolor, stroke="white"})
499
500         -- group object
501         local elements={hb,bb,ht,bt}
502         local obj=ipe.Group(elements)
503         if values.pinned then obj:set('pinned', 'horizontal') end
504
505         if editmode then
506                 local t={original=p[prim]:clone(),
507                                 label="edit tabbed text",
508                                 pno=model.pno,
509                                 vno=model.vno,
510                                 primary=prim,
511                                 final=obj }
512                 t.undo = function (t, doc)
513                                          doc[t.pno]:replace(t.primary, t.original)
514                                  end
515                 t.redo = function (t, doc)
516                                          doc[t.pno]:replace(t.primary, t.final)
517                                  end
518                 model:register(t)
519         else
520                 model:creation("create tabbed text", obj)
521                 -- model.doc:runLatex() -- may crash the thing
522         end
523
524 end
525
526 -- create the dialog for editing box properties
527 local function box_property_dialog(model)
528         local colors = model.doc:sheets():allNames("color")
529         local pens= model.doc:sheets():allNames("pen")
530         local pathmodes = {"stroked", "strokedfilled", "filled"}
531         local d = ipeui.Dialog(mainWindow(model), "Edit box properties")
532
533         d:add("rounded", "checkbox", { label="Round Corner"}, 1, 1, 1, 1)
534         d:add("pointer", "checkbox", { label="Pointer"}, 1, 2, 1, 1)
535         d:add("mlabel", "label", { label="Mode"}, 2, 1)
536         d:add("pathmode", "combo", pathmodes, 2, 2)
537         d:add("flabel", "label", { label="Fill Color" }, 3, 1)
538         d:add("fill", "combo", colors, 3, 2)
539         d:add("slabel", "label", { label="Stroke Color" }, 4, 1)
540         d:add("stroke", "combo", colors, 4, 2)
541         d:add("plabel", "label", { label="Line Width"}, 5, 1)
542         d:add("pen", "combo", pens, 5, 2)
543         d:add("cancel", "button", { label="&Cancel", action="reject" }, 6, 1)
544         d:add("ok", "button", { label="&Ok", action="accept" }, 6, 2)
545         d:setStretch("column", 2, 1)
546         return d
547 end
548
549 -- edit a box object
550 local function edit_box(model, prim)
551         local colors = model.doc:sheets():allNames("color")
552         local pens= model.doc:sheets():allNames("pen")
553         local pathmodes = {"stroked", "strokedfilled", "filled"}
554
555         local p=model:page()
556         local elements=p[prim]:elements()
557         local bb=elements[1]
558         local bbs=bb:shape()[1]
559
560         local d=box_property_dialog(model)
561
562         -- default values
563         d:set("rounded", #bbs>=7)
564         d:set("pathmode", indexOf(bb:get('pathmode'),pathmodes))
565         d:set("pointer", #bbs==6 or #bbs==11)
566         if indexOf(bb:get('stroke'),colors) then
567                 d:set("stroke", indexOf(bb:get('stroke'),colors) )
568         elseif not model.attributes.stroke then
569                 d:set("stroke", indexOf(model.attributes.stroke,colors) )
570         end
571         if indexOf(bb:get('fill'),colors) then
572                 d:set("fill", indexOf(bb:get('fill'),colors) )
573         elseif not model.attributes.fill then
574                 d:set("fill", indexOf(model.attributes.fill,colors) )
575         end
576         if indexOf(bb:get('pen'),pens) then
577                 d:set("pen", indexOf(bb:get('pen'),pens) )
578         elseif not model.attributes.pen then
579                 d:set("pen", indexOf(model.attributes.pen,pens) )
580         end
581
582         local r = d:execute(BOX_DIALOG_SIZE)
583         if not r then return end
584         local pathmode=pathmodes[d:get("pathmode")]
585         local stroke=colors[d:get("stroke")]
586         local fill=colors[d:get("fill")]
587         local pen=pens[d:get("pen")]
588
589         local boxshape
590         if d:get('rounded') and d:get('pointer') then
591                 boxshape=boxshape_round_pointer
592         elseif d:get('rounded') and not d:get('pointer') then
593                 boxshape=boxshape_round
594         elseif not d:get('rounded') and d:get('pointer') then
595                 boxshape=boxshape_square_pointer
596         else
597                 boxshape=boxshape_square
598         end
599
600         -- v1=BL, v2=TR, v3=P1, v4=P2, v5=P3. Pointer=(P1,P2,P3)
601         local v1,v2,v3,v4,v5,shape
602         if #bbs==3 then
603                 v1=bbs[1][1];v2=bbs[2][2]
604                 shape={ boxshape(v1,v2) }
605         elseif #bb:shape()[1]==6 then
606                 v1=bbs[1][1];v3=bbs[2][2]; v4=bbs[3][2]; v5=bbs[4][2];
607                 v2=bbs[5][2];
608                 shape={ boxshape(v1,v2,v3,v4,v5) }
609         elseif #bb:shape()[1]==8 then
610                 v1=V(bbs[5][1].x,bbs[3][1].y)
611                 v2=V(bbs[1][1].x,bbs[7][1].y)
612                 shape={ boxshape(v1,v2) }
613         elseif #bb:shape()[1]==11 then
614                 v1=V(bbs[5][1].x,bbs[3][1].y)
615                 v2=V(bbs[1][1].x,bbs[7][1].y)
616                 v3=bbs[7][2]; v4=bbs[8][2]; v5=bbs[9][2];
617                 shape={ boxshape(v1,v2,v3,v4,v5) }
618         end
619
620         local obj = path(model, shape,
621                 {pen=pen, pathmode=pathmode, stroke=stroke, fill=fill})
622
623         elements[1]=obj
624         local final = ipe.Group(elements)
625         final:setMatrix(p[prim]:matrix())
626
627         local t = { label="edit box", pno=model.pno, vno=model.vno,
628                                         layer=p:active(model.vno),
629                                         original=p[prim]:clone(),
630                                         primary=prim, final=final }
631         t.undo = function (t, doc)
632                                  doc[t.pno]:replace(t.primary, t.original)
633                          end
634         t.redo = function (t, doc)
635                                  doc[t.pno]:replace(t.primary, t.final)
636                          end
637         model:register(t)
638 end
639
640 -- Edit a group object
641 local function action_edit_group(model,prim,obj)
642         local otype,values,pos=parse_group_values(model,prim)
643         if otype==UNKNOWN then
644                 model:warning("Cannot edit this object")
645                 return
646         elseif otype==TABBED_TEXT then
647                 local newvalues=edit_tabbed_values(model, values)
648                 if not newvalues then return end
649                 if newvalues.htext=="" then
650                         newvalues.rounded=true
651                         create_boxed(model,newvalues,pos,prim)
652                 else
653                         create_tabbed(model,newvalues,pos,prim)
654                 end
655         elseif otype==BOXED_OTHER or otype==BOXED_TEXT then
656                 edit_box(model,prim)
657         end
658 end
659
660 -- saving the old function
661 function _G.MODEL:presentation_backup_actinon_edit () end
662 _G.MODEL.presentation_backup_action_edit = _G.MODEL.action_edit
663
664 -- modify the global edit action
665 function _G.MODEL:action_edit()
666         local p = self:page()
667         local prim = p:primarySelection()
668         if not prim then
669            self:presentation_backup_action_edit()
670            return 
671         end
672         local obj = p[prim]
673         if obj:type() == "group" then
674            action_edit_group(self, prim, obj)
675         else
676            self:presentation_backup_action_edit()
677         end
678 end
679
680 -- Run to create a new object
681 function tabbedboxed(model)
682         local values=edit_tabbed_values(model)
683         if not values then return end
684         if values.htext=="" then
685                 values.rounded=true
686                 create_boxed(model,values)
687         else
688                 create_tabbed(model,values)
689         end
690 end
691
692 -- deselect all selected
693 function deselectAll(model)
694         local doc = model.doc
695         for i,p in doc:pages() do
696                 p:deselectAll()
697         end
698 end
699
700 -- box the selected objects
701 function boxit(model)
702         local p=model:page()
703         local box = ipe.Rect()
704         local elements={0}
705         for i,obj,sel,layer in p:objects() do
706                 if sel then
707                         box:add(p:bbox(i))
708                         elements[#elements+1]=obj:clone()
709                 end
710         end
711         if #elements==1 then
712                 model.ui:explain('No selection to box')
713                 return
714         end
715         local s=8
716         local layout = model.doc:sheets():find("layout")
717 --      local maxx=layout.framesize.x
718
719         local x1=box:bottomLeft().x-s
720 --      if x1 < 0 then x1 = 0 end
721         local y1=box:bottomLeft().y-s
722
723         local x2=box:topRight().x+s
724 --      if x2 > maxx then x2 = maxx end
725         local y2=box:topRight().y+s
726
727         local d=box_property_dialog(model)
728
729         local colors = model.doc:sheets():allNames("color")
730         local pens= model.doc:sheets():allNames("pen")
731         local pathmodes = {"stroked", "strokedfilled", "filled"}
732
733         -- default values
734         d:set("rounded", true)
735
736         d:set("pathmode", indexOf(PREFERRED_BOX_MODE,pathmodes))
737
738         if indexOf("box_border",colors) then
739                 d:set("stroke", indexOf("box_border",colors))
740         elseif model.attributes.stroke then
741                 d:set("stroke", indexOf(model.attributes.stroke,colors) )
742         end
743
744         if indexOf("box_fill",colors) then
745                 d:set("fill", indexOf("box_fill",colors))
746         elseif model.attributes.fill then
747                 d:set("fill", indexOf(model.attributes.fill,colors) )
748         end
749         
750         if indexOf("boxborder",pens) then
751                 d:set("pen", indexOf("boxborder",pens))
752         elseif model.attributes.pen then
753                 d:set("pen", indexOf(model.attributes.pen,pens) )
754         end
755
756         local r = d:execute(BOX_DIALOG_SIZE)
757
758         if not r then return end
759         local pathmode=pathmodes[d:get("pathmode")]
760         local stroke=colors[d:get("stroke")]
761         local fill=colors[d:get("fill")]
762         local pen=pens[d:get("pen")]
763
764         local boxshape
765         if d:get('rounded') and d:get('pointer') then
766                 boxshape=boxshape_round_pointer
767         elseif d:get('rounded') and not d:get('pointer') then
768                 boxshape=boxshape_round
769         elseif not d:get('rounded') and d:get('pointer') then
770                 boxshape=boxshape_square_pointer
771         else
772                 boxshape=boxshape_square
773         end
774
775         local shape = { boxshape(V(x1,y1), V(x2,y2)) }
776
777         local obj = path(model, shape, {pen=pen, pathmode=pathmode, stroke=stroke, fill=fill})
778
779         elements[1]=obj
780         local final = ipe.Group(elements)
781
782         local t = { label="add box", pno=model.pno, vno=model.vno,
783                                         layer=p:active(model.vno), object=obj,
784                                         selection=model:selection(),
785                                         undo=_G.revertOriginal,
786                                         original=p:clone(),
787                                         final=final }
788         t.redo = function (t, doc)
789                                  local p = doc[t.pno]
790                                  for i = #t.selection,1,-1 do p:remove(t.selection[i]) end
791                                  p:insert(nil, t.final, 1, t.layer)
792                          end
793         model:register(t)
794 end
795
796 methods = {
797   { label = "Box It", run=boxit},
798   { label = "Tabbed/Boxed Text", run=tabbedboxed},
799   { label = "Deselect All", run=deselectAll},
800 }
801