initially adding the graph and presentation ipelets
authorThomas Bläsius <thomas.blaesius@kit.edu>
Wed, 11 Sep 2013 15:20:42 +0000 (17:20 +0200)
committerThomas Bläsius <thomas.blaesius@kit.edu>
Wed, 11 Sep 2013 15:20:42 +0000 (17:20 +0200)
ipelets/graph/graph.lua [new file with mode: 0644]
ipelets/presentation/presentation.lua [new file with mode: 0644]

diff --git a/ipelets/graph/graph.lua b/ipelets/graph/graph.lua
new file mode 100644 (file)
index 0000000..52eb188
--- /dev/null
@@ -0,0 +1,277 @@
+----------------------------------------------------------------------
+-- graph ipelet
+----------------------------------------------------------------------
+label = "Graph"
+
+about = [[ Some features making it easier to work with graphs. ]]
+
+local deactivateGraphMode = false
+
+function toggleGraphMode ()
+   if deactivateGraphMode then
+      deactivateGraphMode = false
+   else 
+      deactivateGraphMode = true
+   end
+end
+
+local editing = false
+local currMarkId = nil
+
+--------------------------------------------------------------------------------
+-- add an edit action for marks ------------------------------------------------
+
+-- saving the old function
+function _G.MODEL:graph_backup_actinon_edit () end
+_G.MODEL.graph_backup_action_edit = _G.MODEL.action_edit
+
+-- adding support for objects of type reference
+function _G.MODEL:action_edit()
+   if deactivateGraphMode then
+      self:graph_backup_action_edit()
+      return
+   end
+   local p = self:page()
+   local prim = p:primarySelection()
+   if not prim then
+      self:graph_backup_action_edit()
+      return 
+   end
+   local obj = p[prim]
+   if obj:type() == "reference" then
+      action_edit_reference (self, prim, obj)     
+   else
+      self:graph_backup_action_edit()
+   end
+end
+
+
+-- starting to edit a mark
+function action_edit_reference(model, prim, obj)
+   editing = true
+   currMarkId = prim
+   local p = model:page()
+
+   local pos = obj:matrix() * obj:position()
+   -- print(pos)
+
+   -- creating a circle at the position of the mark
+   local ellipse = {type="ellipse"}
+   ellipse[1] = ipe.Matrix({10, 0, 0, 10, pos.x, pos.y})
+   -- print(ellipse[1])
+   local circ = ipe.Path(model.attributes, {ellipse})
+   p:insert(nil, circ, 0, p:layerOf(prim))
+
+   -- edit the circle instead of the mark itself
+   model:action_edit_path(#p, circ)
+
+   -- print("test")
+end
+
+--------------------------------------------------------------------------------
+-- pressing a key while editing the cycle --------------------------------------
+
+-- saving old function
+function _G.EDITTOOL:graph_backup_key(code, modifiers, text) end
+_G.EDITTOOL.graph_backup_key = _G.EDITTOOL.key
+
+-- overwriting
+function _G.EDITTOOL:key(code, modifiers, text)
+   self:graph_backup_key(code, modifiers, text)
+   if deactivateGraphMode then return end
+
+   -- react if and only if we are currently editing a mark and key ESC
+   -- or SPACE is pressed
+   if text ~= "\027" and code ~= 0x20 then return end
+   if not editing then return end
+
+   editing = false
+
+   -- finding new and old position
+   local p = self.model:page()
+   local circ = p[#p]
+   local mark = p[currMarkId]
+   local oldPos = mark:matrix() * mark:position()
+   local newPos = circ:shape()[1][1]:translation()
+
+   -- remove the intermediate step of moving the cycle from the undo
+   -- stack and remove the cycle itself
+   local undoSt = self.model.undo
+   p:remove(#p)
+   table.remove(undoSt)
+
+   -- new action for the undo stack moving the mark and all endpoints
+   -- ending at the mark
+   local t = { label = "edit reference",
+              pno = self.model.pno,
+              vno = self.model.vno,
+              selection = self.model:selection(),
+              original = self.model:page():clone(),
+              matrix = matrix,
+              undo = _G.revertOriginal,}
+   t.redo = function (t, doc)
+      p:transform(currMarkId, ipe.Translation(newPos-oldPos))
+      moveEndpoints(oldPos, newPos, p)
+   end
+   self.model:register(t)
+end
+
+-- function moving all endpoints and intermediate points in polylines
+-- to newPos, if the squared distance to oldPos is at most sqEps
+local sqEps = 1
+function moveEndpoints(oldPos, newPos, p)
+   for i, obj, sel, layer in p:objects() do
+      if obj:type() == "path" then
+        local shape = obj:shape()
+        for _, subPath in ipairs(shape) do
+           if (subPath["type"] == "curve") then
+              for _,seg in ipairs(subPath) do
+                 if (seg["type"] == "segment") then
+                    for j, point in ipairs(seg) do
+                       -- print(j, point, oldPos)
+                       if (obj:matrix() * point - oldPos):sqLen() < sqEps then
+                          seg[j] = obj:matrix():inverse() * newPos
+                          -- print("test", seg[j])
+                       end 
+                    end
+                 elseif (seg["type"] == "spline") then
+                    if (obj:matrix() * seg[1] - oldPos):sqLen() < sqEps then
+                       seg[1] = obj:matrix():inverse() * newPos
+                    end
+                    if (obj:matrix() * seg[#seg] - oldPos):sqLen() < sqEps then
+                       seg[#seg] = obj:matrix():inverse() * newPos
+                    end
+                 end
+              end
+           end
+           obj:setShape(shape)
+        end
+      end
+   end
+end
+
+
+--------------------------------------------------------------------------------
+-- working with groups ---------------------------------------------------------
+
+local function regroup(elem)
+   local groupElem = {}
+   for i, obj in ipairs(elem) do
+      if obj[1] ~= nil then
+        groupElem[#groupElem + 1] =  regroup(obj)
+      else 
+        groupElem[#groupElem + 1] = obj
+      end
+   end
+   return ipe.Group(groupElem)
+end
+
+local function ungroup(group)
+   local elem = group:elements()
+   local plainElem = {}
+   for i, obj in ipairs(elem) do
+      if (obj:type() == "group") then
+        local subElem, subPlainElem = ungroup(obj)
+        elem[i] = subElem;
+        for _, subObj in ipairs(subPlainElem) do
+           table.insert(plainElem, subObj)
+        end
+      else
+        table.insert(plainElem, obj)
+      end
+   end
+   return elem, plainElem
+end
+
+--------------------------------------------------------------------------------
+-- shorten paths ---------------------------------------------------------------
+
+function shortenObj(obj, lenSource, lenTarget)
+   if obj:type() == "path" then
+      local shape = obj:shape()
+      for _, subPath in ipairs(shape) do
+        local first = subPath[1]
+        local last = subPath[#subPath]
+        
+        local p1 = obj:matrix() * first[1]
+        local p2 = obj:matrix() * first[2]
+        local pDelta = p2 - p1
+        local pNorm = pDelta:normalized()
+        local newP1 =  p1 + pNorm*lenSource
+        
+        local q1 = obj:matrix() * last[#last]
+        local q2 = obj:matrix() * last[#last - 1]
+        local qDelta = q2 - q1
+        local qNorm = qDelta:normalized()
+        local newQ1 = q1 + qNorm*lenTarget
+
+        first[1] = obj:matrix():inverse() * newP1
+        last[#last] = obj:matrix():inverse() * newQ1
+      end
+      obj:setShape(shape)
+   end
+end
+
+function getString(model, string)
+   if ipeui.getString ~= nil then
+      return ipeui.getString(model.ui, "Enter length")
+   else 
+      return model:getString("Enter length")
+   end
+end
+
+function shorten(model, num)
+   num = num - 1
+   local lenTarget = 0
+   local lenSource = 0
+   -- local str = ipeui.getString(model.ui, "Enter length")
+   -- local str = model:getString("Enter length")
+   local str = getString(model, "Enter length")
+   if not str or str:match("^%s*$)") then return end
+   if num == 1 then -- shorten target
+      lenTarget = tonumber(str)
+   elseif num == 2 then -- shorten source
+      lenSource = tonumber(str)
+   elseif num == 3 then -- shorten both
+      lenTarget = tonumber(str)
+      lenSource = tonumber(str)
+   end
+
+   -- start to edit the edges
+   local t = { label = "shorten edges",
+              pno = model.pno,
+              vno = model.vno,
+              selection = model:selection(),
+              original = model:page():clone(),
+              matrix = matrix,
+              undo = _G.revertOriginal,}
+   t.redo = function (t, doc)
+      local p = doc[t.pno]
+      for _, i in ipairs(t.selection) do
+        p:setSelect(i, 2)
+      end
+      local p = doc[t.pno]
+      for i, obj, sel, layer in p:objects() do
+        if sel and obj:type() == "group" then
+           local elem, plainElem = ungroup(obj)
+           for _,subobj in pairs(plainElem) do
+              shortenObj(subobj, lenSource, lenTarget)
+           end
+           p:replace(i, regroup(elem))
+        elseif sel then
+           shortenObj(obj, lenSource, lenTarget)
+        end        
+      end
+   end
+   model:register(t)
+end
+
+
+methods = {
+   { label = "toggle graph mode", run=toggleGraphMode },
+   { label = "shorten target", run=shorten },
+   { label = "shorten source", run=shorten },
+   { label = "shorten both", run=shorten },
+}
+
+----------------------------------------------------------------------
diff --git a/ipelets/presentation/presentation.lua b/ipelets/presentation/presentation.lua
new file mode 100644 (file)
index 0000000..93516be
--- /dev/null
@@ -0,0 +1,801 @@
+----------------------------------------------------------------------
+-- presentation goodies for Ipe
+
+--[[
+
+SUMMARY
+
+ This ipelet adds a few goodies for presentation, including
+  1. Ability to create beamer-like boxes with/without header
+  2. Add a framed box for the selected objects (including text)
+  3. A function to deselect all selected on all pages
+  4. The boxes can be edited as text/path objects (press E)
+  5. A few items can be added to the style sheet to add preferred
+     symbolics for box colors etc. This has the benefit that this
+     preferences can be changed to affect all boxes (see below).
+
+  The design of these boxes is from Martin Nöllenburg's presentation
+  example posted on the Ipe7 wiki page.
+
+STYLESHEET (CHANGING PREFERRED SETTINGS)
+
+ Example style sheet to cascade with presentation.isy is as follows.
+
+---- content of example.isy ---
+example removed, not possible to upload this file to the ipe-wiki otherwise
+---- end content of example.isy ---
+
+ where:
+  tab_header= color of the tab header in a tabbed box
+  tab_body  = color of the tab body in a tabbed box
+  box_fill  = fill color a box
+  box_border= color of the box border
+  boxborder = linewidth of the box border
+
+ The preferred box mode (stroked/filled/strokedfilled) can be changed by
+ changing the hard-wired value (no stylesheet option) PREFERRED_BOX_MODE below
+
+ With the above style sheet, one can start an empty presentation using
+  ipe -sheet presentation -sheet /path/to/example.isy
+
+SHORTCUT
+
+ Shortcuts to these functions can be changed as for other ipelets:
+       shortcuts.ipelet_x_presentation = "Key"
+ where x is the index (starting 1) of the sub-menu for the function
+
+FILE/AUTHOR HISTORY
+
+ version  0. Initial Release. Zhengdao Wang 2010
+ version  1. add a line here if a change is made
+
+LICENSE
+
+ This file can be distributed and modified under the terms of the GNU General
+ Public License as published by the Free Software Foundation; either version
+ 3, or (at your option) any later version.
+
+ This file 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.
+
+--]]
+
+----------------------------------------------------------------------
+
+label = "Presentation"
+
+about = [[
+       Presentation Goodies: Add boxes around objects, deselect all,
+       add tabbed/boxed text.
+]]
+
+V = ipe.Vector
+indexOf=_G.indexOf
+
+-- table storing the height of the first line box
+Height={}
+Spacing={}
+Has_Height=nil
+
+local c=10 -- corner size
+
+local UNKNOWN=0
+local TABBED_TEXT=1
+local BOXED_TEXT=2
+local BOXED_OTHER=3
+
+local PREFERRED_BOX_MODE="strokedfilled" -- or stroked or filled
+
+local BOX_DIALOG_SIZE={400,140}
+
+-- initialize the Height table
+local function init_spacing(model)
+       local p=model:page()
+       local xx=300
+       local iH={}
+       local sizes = model.doc:sheets():allNames("textsize")
+       for i,size in ipairs(sizes) do
+               obj=ipe.Text({textsize=size, minipage=true}, "ABC\n\nDEF", V(xx,xx), xx)
+               local layer=p:active(model.vno)
+               p:insert(nil, obj, nil, layer)
+               obj=ipe.Text({textsize=size, minipage=true}, "ABC", V(xx,xx), xx)
+               p:insert(nil, obj, nil, layer)
+               iH[#iH+1]=size
+       end
+       model.doc:runLatex()
+       for i=#sizes,1,-1 do
+               Height[iH[i]]=xx-p:bbox(#p):bottomLeft().y
+               p:remove(#p)
+               Spacing[iH[i]]=(xx-p:bbox(#p):bottomLeft().y)/2-Height[iH[i]]
+               p:remove(#p)
+       end
+       Has_Height=1
+end
+
+-- generate a path with given properties
+local function path(model, shape, props)
+       local oldvalues={}
+       for k,v in pairs(props) do
+               oldvalues[k]=model.attributes[k]
+               model.attributes[k]=v
+       end
+       local obj = ipe.Path(model.attributes, shape)
+       for k,v in pairs(props) do
+               model.attributes[k]=oldvalues[k]
+       end
+       if props.pen then obj:set('pen', props.pen) end
+       obj:set('pathmode', props.pathmode, props.stroke, props.fill)
+       return obj
+end
+
+-- create a square box #shape=3
+local function boxshape_square(v1, v2)
+       return { type="curve", closed=true;
+                { type="segment"; v1, V(v1.x, v2.y) }, -- L
+                { type="segment"; V(v1.x, v2.y), v2 }, -- T
+                { type="segment"; v2, V(v2.x, v1.y) } } -- R
+end
+
+-- create a square box with a pointer #shape=6
+local function boxshape_square_pointer(v1, v2, v3, v4, v5)
+       local dx=v2.x-v1.x
+       local dy=v2.y-v1.y
+       local v3=v3 or V(v1.x+.4*dx, v2.y)
+       local v4=v4 or V(v1.x+.6*dx,v2.y+.3*dy)
+       local v5=v5 or V(v1.x+.5*dx,v2.y)
+       return { type="curve", closed=true;
+                { type="segment"; v1, V(v1.x, v2.y) }, -- L
+                { type="segment"; V(v1.x, v2.y), v3}, -- T
+                { type="segment"; v3, v4}, -- P
+                { type="segment"; v4, v5}, --P
+                { type="segment"; v5, v2}, -- T
+                { type="segment"; v2, V(v2.x, v1.y) } } -- R
+end
+
+-- create a header box: round corner on topRight
+local function boxshape_roundTR(v1, v2)
+       return { type="curve", closed=true;
+                { type="segment"; v1, V(v1.x, v2.y) },
+                { type="segment"; V(v1.x, v2.y), V(v2.x-c, v2.y) },
+                { type="bezier"; V(v2.x-c, v2.y), V(v2.x-c/2, v2.y),
+                       V(v2.x, v2.y-c/2), V(v2.x, v2.y-c) },
+                { type="segment"; V(v2.x, v2.y-c), V(v2.x, v1.y) } }
+end
+
+-- create a body box: round corner on bottom Left
+local function boxshape_roundLL(v1, v2)
+       return { type="curve", closed=true;
+                { type="segment"; v2, V(v2.x, v1.y) },
+                { type="segment"; V(v2.x, v1.y), V(v1.x+c, v1.y) },
+                { type="bezier"; V(v1.x+c, v1.y), V(v1.x+c/2,v1.y),
+                       V(v1.x, v1.y+c/2), V(v1.x, v1.y+c) },
+                { type="segment"; V(v1.x, v1.y+c), V(v1.x, v2.y) } }
+end
+
+-- create a body box: 4 round corners #shape=8
+local function boxshape_round(v1, v2)
+       return { type="curve", closed=true;
+                { type="segment"; V(v2.x, v2.y-c), V(v2.x, v1.y+c) }, -- R
+                { type="bezier"; V(v2.x,v1.y+c), V(v2.x, v1.y+c/2),
+                       V(v2.x-c/2,v1.y), V(v2.x-c,v1.y)}, -- BR
+                { type="segment"; V(v2.x-c, v1.y), V(v1.x+c, v1.y) }, -- B
+                { type="bezier"; V(v1.x+c, v1.y), V(v1.x+c/2,v1.y),
+                       V(v1.x, v1.y+c/2), V(v1.x, v1.y+c) }, -- BL
+                { type="segment"; V(v1.x, v1.y+c), V(v1.x, v2.y-c) }, -- L
+                { type="bezier"; V(v1.x, v2.y-c), V(v1.x,v2.y-c/2),
+                       V(v1.x+c/2, v2.y), V(v1.x+c, v2.y) }, -- TL
+                { type="segment"; V(v1.x+c, v2.y), V(v2.x-c, v2.y) }, -- T
+                { type="bezier"; V(v2.x-c, v2.y), V(v2.x-c/2,v2.y),
+                       V(v2.x, v2.y-c/2), V(v2.x, v2.y-c) }, -- TR
+               }
+end
+
+-- create a body box: 4 round corners, with pointer #shape=11
+local function boxshape_round_pointer(v1, v2, v3, v4, v5)
+       local dx=v2.x-v1.x
+       local dy=v2.y-v1.y
+       local v3=v3 or V(v1.x+.4*dx, v2.y)
+       local v4=v4 or V(v1.x+.6*dx,v2.y+.3*dy)
+       local v5=v5 or V(v1.x+.5*dx,v2.y)
+       return { type="curve", closed=true;
+                { type="segment"; V(v2.x, v2.y-c), V(v2.x, v1.y+c) }, -- R
+                { type="bezier"; V(v2.x,v1.y+c), V(v2.x, v1.y+c/2),
+                       V(v2.x-c/2,v1.y), V(v2.x-c,v1.y)}, -- BR
+                { type="segment"; V(v2.x-c, v1.y), V(v1.x+c, v1.y) }, -- B
+                { type="bezier"; V(v1.x+c, v1.y), V(v1.x+c/2,v1.y),
+                       V(v1.x, v1.y+c/2), V(v1.x, v1.y+c) }, -- BL
+                { type="segment"; V(v1.x, v1.y+c), V(v1.x, v2.y-c) }, -- L
+                { type="bezier"; V(v1.x, v2.y-c), V(v1.x,v2.y-c/2),
+                       V(v1.x+c/2, v2.y), V(v1.x+c, v2.y) }, -- TL
+                { type="segment"; V(v1.x+c, v2.y), v3}, -- T
+                { type="segment"; v3, v4}, -- P
+                { type="segment"; v4, v5}, --P
+                { type="segment"; v5, V(v2.x-c, v2.y)}, -- T
+                { type="bezier"; V(v2.x-c, v2.y), V(v2.x-c/2,v2.y),
+                       V(v2.x, v2.y-c/2), V(v2.x, v2.y-c) }, -- TR
+               }
+end
+
+-- parse the values from a group obj
+local function parse_group_values(model,prim)
+       local fs = model.doc:sheets():find("layout").framesize
+       local p=model:page()
+       local bbox=p:bbox(prim)
+       local pos=V(bbox:bottomLeft().x, bbox:topRight().y)
+
+       local elements=p[prim]:elements()
+       if #elements==4 then
+               local hb,bb,ht,bt=elements[1],elements[2],elements[3],elements[4]
+               if hb:type()=="path" and bb:type()=="path" and
+                       ht:type()=="text" and bt:type()=="text" then
+                       local values={htext=ht:text(),
+                               btext=bt:text(),
+                               pinned=(p[prim]:get("pinned")=="horizontal"),
+                               fwidth=string.format('%.2f',
+                                       (bbox:topRight().x-bbox:bottomLeft().x)/fs.x),
+                               hcolor=hb:get("fill"),
+                               bcolor=bb:get("fill"),
+                               size=ht:get("textsize")}
+                       return TABBED_TEXT,values,pos
+               end
+       else
+               local bb,bt=elements[1],elements[2]
+               if bb:type()=="path" and #bb:shape()==1 and
+                       bb:shape()[1].closed==true and
+                       (#bb:shape()[1]==3 or #bb:shape()[1]==8 or
+                       #bb:shape()[1]==6 or #bb:shape()[1]==11) then
+                       if bt:type()=="text" then
+                               local values={btext=bt:text(),
+                                       pinned=(p[prim]:get("pinned")=="horizontal"),
+                                       size=bt:get("textsize"),
+                                       fwidth=string.format('%.2f',
+                                               (bbox:topRight().x-bbox:bottomLeft().x)/fs.x),
+                                       bcolor=bb:get("fill")}
+                               if #bb:shape()[1]==6 then
+                                       pos=V(pos.x,bb:shape()[1][1][2].y)
+                               elseif #bb:shape()[1]==11 then
+                                       pos=V(pos.x,bb:shape()[1][10][2].y)
+                               end
+                               return BOXED_TEXT,values,pos
+                       else
+                               return BOXED_OTHER
+                       end
+               end
+       end
+       return UNKNOWN
+end
+
+function mainWindow(model)
+   if model.ui.win == nil then
+      return model.ui
+   else
+      return model.ui:win()
+   end
+end
+
+-- Edit the values for the frame
+local function edit_tabbed_values(model,values)
+        local d = ipeui.Dialog(mainWindow(model), "Create tabbed text")
+       local colors = model.doc:sheets():allNames("color")
+       local sizes = model.doc:sheets():allNames("textsize")
+       d:add("hlabel", "label", { label="Enter Header" }, 1, 1, 1, 1)
+       d:add("hcolor", "combo", colors, 1, 4)
+       d:add("htext", "input", { syntax="latex" }, 2, 1, 1, 4)
+       d:add("blabel", "label", { label="Enter Body"}, 3, 1, 1, 3)
+       d:add("bcolor", "combo", colors, 3, 4)
+       d:add("btext", "text", { syntax="latex" }, 4, 1, 1, 4)
+       d:add("size", "combo", sizes, 5, 1)
+       d:add("wlabel", "label", { label="width [0-1]"}, 5, 2, 1, 2)
+       d:add("fwidth", "input", {size=2}, 5, 3, 1, 1)
+       d:add("pinned", "checkbox", { label="pinned"}, 5, 4)
+       d:add("ok", "button", { label="&Ok", action="accept" }, 6, 4)
+       d:add("cancel", "button", { label="&Cancel", action="reject" }, 6, 3)
+       _G.addEditorField(d, "btext", 6, 2)
+       d:setStretch("row", 2, 1)
+       d:setStretch("column", 1, 1)
+       d:set("fwidth", "0.8")
+       d:set("pinned", 1)
+
+       if indexOf("tab_header", colors) then
+               d:set("hcolor", indexOf("tab_header", colors))
+       elseif indexOf("darkblue", colors) then
+               d:set("hcolor", indexOf("darkblue", colors))
+       else
+               d:set("hcolor", indexOf("black", colors))
+       end
+
+       if indexOf("tab_body", colors) then
+               d:set("bcolor", indexOf("tab_body", colors))
+       elseif indexOf("lightgray", colors) then
+               d:set("bcolor", indexOf("lightgray", colors))
+       else
+               d:set("bcolor", indexOf("white", colors))
+       end
+
+       if values then
+               for k,v in pairs(values) do
+                       if k=="hcolor" or k=="bcolor" then v=indexOf(v, colors) end
+                       if k=="size" then v=indexOf(v, sizes) end
+                       d:set(k, v)
+               end
+       end
+
+       local r = d:execute(prefs.editor_size)
+       if not r then return end
+       local newvalues={}
+       newvalues.htext=d:get("htext")
+       newvalues.btext=d:get("btext")
+       newvalues.pinned=d:get("pinned")
+       newvalues.fwidth=d:get("fwidth")
+       newvalues.size=sizes[d:get("size")]
+       newvalues.hcolor=colors[d:get("hcolor")]
+       newvalues.bcolor=colors[d:get("bcolor")]
+--     if newvalues.fwidth=="" or tonumber(newvalues.fwidth)>.99 then
+--             newvalues.pinned=true
+--     end
+       return newvalues
+end
+
+-- measure the height a piece of given text
+local function measure_height(model,text,size,width)
+       local p=model:page()
+       local obj= ipe.Text(model.attributes, text, V(0,0), width)
+       obj:set('textsize', size)
+       local layer=p:active(model.vno)
+       p:insert(nil, obj, nil, layer)
+       if not model.doc:runLatex() then
+               p:remove(#p)
+               return 100
+       end
+       local bbox=p:bbox(#p)
+       p:remove(#p)
+       return bbox:topRight().y-bbox:bottomLeft().y
+end
+
+-- Create boxed text
+local function create_boxed(model,values, pos, prim)
+       local fs = model.doc:sheets():find("layout").framesize
+       local p = model:page()
+       local editmode=(prim~=nil)
+
+       local width fwidth=tonumber(values.fwidth)
+
+       if not fwidth or fwidth<0 or fwidth>1 then
+               width=fs.x
+               fwidth=1
+       else
+               width=fwidth*fs.x
+       end
+
+       -- spacing
+       local s=Spacing[values.size]
+       local h=Height[values.size]
+       if not s or not h then
+               init_spacing(model)
+               s=Spacing[values.size]
+               h=Height[values.size]
+       end
+
+       local bheight=measure_height(model,values.btext,values.size,width-2*s)
+
+       -- location
+       if not pos then
+               x1=fs.x/2-width/2
+               x2=fs.x/2+width/2
+               y2=fs.y/2
+               y1=y2-bheight-1.8*s
+       else
+               x1=pos.x
+               x2=x1+width
+               y2=pos.y
+               y1=y2-bheight-1.8*s
+       end
+       if fwidth>.99 then x1,x2=0,fs.x end
+
+       -- body text
+       pos=V(x1+s, y2-s)
+       local bt= ipe.Text(model.attributes, values.btext, pos, width-2*s)
+       bt:set('textsize', values.size)
+
+       -- body box
+       local shape2
+       if values.rounded then
+               shape2 = { boxshape_round(V(x1,y1), V(x2,y2)) }
+       else
+               shape2 = { boxshape_square(V(x1,y1), V(x2,y2)) }
+       end
+       local bb = path(model, shape2,
+               {pathmode='filled', fill=values.bcolor, stroke="white"})
+
+       -- group object
+       local elements={bb,bt}
+       local obj=ipe.Group(elements)
+       -- obj:setMatrix(p[prim]:matrix()) -- currently not working
+       if values.pinned then obj:set('pinned', 'horizontal') end
+
+       if editmode then
+               local t={original=p[prim]:clone(),
+                               label="edit boxed text",
+                               pno=model.pno,
+                               vno=model.vno,
+                               primary=prim,
+                               final=obj }
+               t.undo = function (t, doc)
+                                        doc[t.pno]:replace(t.primary, t.original)
+                                end
+               t.redo = function (t, doc)
+                                        doc[t.pno]:replace(t.primary, t.final)
+                                end
+               model:register(t)
+       else
+               model:creation("create boxed text", obj)
+               -- model.doc:runLatex() -- may crash the thing
+       end
+end
+
+-- Create the requested object from values
+local function create_tabbed(model,values, pos, prim)
+       local fs = model.doc:sheets():find("layout").framesize
+       local p = model:page()
+       local editmode=(prim~=nil)
+
+       local width fwidth=tonumber(values.fwidth)
+
+       if not fwidth or fwidth<0 or fwidth>1 then
+               width=fs.x
+               fwidth=1
+       else
+               width=fwidth*fs.x
+       end
+
+       -- spacing
+       local s=Spacing[values.size]
+       local h=Height[values.size]
+       if not s or not h then
+               init_spacing(model)
+               s=Spacing[values.size]
+               h=Height[values.size]
+       end
+
+       local bheight=measure_height(model,values.btext,values.size,width-2*s)
+
+       -- location
+       if not pos then
+               x1=fs.x/2-width/2
+               x2=fs.x/2+width/2
+               y2=fs.y/2
+               y1=y2-h-bheight-3.8*s
+       else
+               x1=pos.x
+               x2=x1+width
+               y2=pos.y
+               y1=y2-h-bheight-3.8*s
+       end
+       if fwidth>.99 then x1,x2=0,fs.x end
+
+       -- header text
+       pos=V(x1+s, y2-s)
+       local ht= ipe.Text(model.attributes, values.htext, pos, width-2*s)
+       ht:set('stroke', 'white')
+       ht:set('textsize', values.size)
+
+       -- body text
+       pos=V(x1+s, y2-s-h-2*s)
+       local bt= ipe.Text(model.attributes, values.btext, pos, width-2*s)
+       bt:set('textsize', values.size)
+
+       -- header box
+       local shape1 = { boxshape_roundTR(V(x1,y2-h-2*s), V(x2,y2)) }
+       local hb = path(model, shape1,
+               {pathmode='filled', fill=values.hcolor, stroke="white"})
+               hb:set('pathmode', 'filled', "white", values.hcolor)
+
+       -- body box
+       local shape2 = { boxshape_roundLL(V(x1,y1), V(x2,y2-h-2*s)) }
+       local bb = path(model, shape2,
+               {pathmode='filled', fill=values.bcolor, stroke="white"})
+
+       -- group object
+       local elements={hb,bb,ht,bt}
+       local obj=ipe.Group(elements)
+       if values.pinned then obj:set('pinned', 'horizontal') end
+
+       if editmode then
+               local t={original=p[prim]:clone(),
+                               label="edit tabbed text",
+                               pno=model.pno,
+                               vno=model.vno,
+                               primary=prim,
+                               final=obj }
+               t.undo = function (t, doc)
+                                        doc[t.pno]:replace(t.primary, t.original)
+                                end
+               t.redo = function (t, doc)
+                                        doc[t.pno]:replace(t.primary, t.final)
+                                end
+               model:register(t)
+       else
+               model:creation("create tabbed text", obj)
+               -- model.doc:runLatex() -- may crash the thing
+       end
+
+end
+
+-- create the dialog for editing box properties
+local function box_property_dialog(model)
+       local colors = model.doc:sheets():allNames("color")
+       local pens= model.doc:sheets():allNames("pen")
+       local pathmodes = {"stroked", "strokedfilled", "filled"}
+       local d = ipeui.Dialog(mainWindow(model), "Edit box properties")
+
+       d:add("rounded", "checkbox", { label="Round Corner"}, 1, 1, 1, 1)
+       d:add("pointer", "checkbox", { label="Pointer"}, 1, 2, 1, 1)
+       d:add("mlabel", "label", { label="Mode"}, 2, 1)
+       d:add("pathmode", "combo", pathmodes, 2, 2)
+       d:add("flabel", "label", { label="Fill Color" }, 3, 1)
+       d:add("fill", "combo", colors, 3, 2)
+       d:add("slabel", "label", { label="Stroke Color" }, 4, 1)
+       d:add("stroke", "combo", colors, 4, 2)
+       d:add("plabel", "label", { label="Line Width"}, 5, 1)
+       d:add("pen", "combo", pens, 5, 2)
+       d:add("cancel", "button", { label="&Cancel", action="reject" }, 6, 1)
+       d:add("ok", "button", { label="&Ok", action="accept" }, 6, 2)
+       d:setStretch("column", 2, 1)
+       return d
+end
+
+-- edit a box object
+local function edit_box(model, prim)
+       local colors = model.doc:sheets():allNames("color")
+       local pens= model.doc:sheets():allNames("pen")
+       local pathmodes = {"stroked", "strokedfilled", "filled"}
+
+       local p=model:page()
+       local elements=p[prim]:elements()
+       local bb=elements[1]
+       local bbs=bb:shape()[1]
+
+       local d=box_property_dialog(model)
+
+       -- default values
+       d:set("rounded", #bbs>=7)
+       d:set("pathmode", indexOf(bb:get('pathmode'),pathmodes))
+       d:set("pointer", #bbs==6 or #bbs==11)
+       if indexOf(bb:get('stroke'),colors) then
+               d:set("stroke", indexOf(bb:get('stroke'),colors) )
+       elseif not model.attributes.stroke then
+               d:set("stroke", indexOf(model.attributes.stroke,colors) )
+       end
+       if indexOf(bb:get('fill'),colors) then
+               d:set("fill", indexOf(bb:get('fill'),colors) )
+       elseif not model.attributes.fill then
+               d:set("fill", indexOf(model.attributes.fill,colors) )
+       end
+       if indexOf(bb:get('pen'),pens) then
+               d:set("pen", indexOf(bb:get('pen'),pens) )
+       elseif not model.attributes.pen then
+               d:set("pen", indexOf(model.attributes.pen,pens) )
+       end
+
+       local r = d:execute(BOX_DIALOG_SIZE)
+       if not r then return end
+       local pathmode=pathmodes[d:get("pathmode")]
+       local stroke=colors[d:get("stroke")]
+       local fill=colors[d:get("fill")]
+       local pen=pens[d:get("pen")]
+
+       local boxshape
+       if d:get('rounded') and d:get('pointer') then
+               boxshape=boxshape_round_pointer
+       elseif d:get('rounded') and not d:get('pointer') then
+               boxshape=boxshape_round
+       elseif not d:get('rounded') and d:get('pointer') then
+               boxshape=boxshape_square_pointer
+       else
+               boxshape=boxshape_square
+       end
+
+       -- v1=BL, v2=TR, v3=P1, v4=P2, v5=P3. Pointer=(P1,P2,P3)
+       local v1,v2,v3,v4,v5,shape
+       if #bbs==3 then
+               v1=bbs[1][1];v2=bbs[2][2]
+               shape={ boxshape(v1,v2) }
+       elseif #bb:shape()[1]==6 then
+               v1=bbs[1][1];v3=bbs[2][2]; v4=bbs[3][2]; v5=bbs[4][2];
+               v2=bbs[5][2];
+               shape={ boxshape(v1,v2,v3,v4,v5) }
+       elseif #bb:shape()[1]==8 then
+               v1=V(bbs[5][1].x,bbs[3][1].y)
+               v2=V(bbs[1][1].x,bbs[7][1].y)
+               shape={ boxshape(v1,v2) }
+       elseif #bb:shape()[1]==11 then
+               v1=V(bbs[5][1].x,bbs[3][1].y)
+               v2=V(bbs[1][1].x,bbs[7][1].y)
+               v3=bbs[7][2]; v4=bbs[8][2]; v5=bbs[9][2];
+               shape={ boxshape(v1,v2,v3,v4,v5) }
+       end
+
+       local obj = path(model, shape,
+               {pen=pen, pathmode=pathmode, stroke=stroke, fill=fill})
+
+       elements[1]=obj
+       local final = ipe.Group(elements)
+       final:setMatrix(p[prim]:matrix())
+
+       local t = { label="edit box", pno=model.pno, vno=model.vno,
+                                       layer=p:active(model.vno),
+                                       original=p[prim]:clone(),
+                                       primary=prim, final=final }
+       t.undo = function (t, doc)
+                                doc[t.pno]:replace(t.primary, t.original)
+                        end
+       t.redo = function (t, doc)
+                                doc[t.pno]:replace(t.primary, t.final)
+                        end
+       model:register(t)
+end
+
+-- Edit a group object
+local function action_edit_group(model,prim,obj)
+       local otype,values,pos=parse_group_values(model,prim)
+       if otype==UNKNOWN then
+               model:warning("Cannot edit this object")
+               return
+       elseif otype==TABBED_TEXT then
+               local newvalues=edit_tabbed_values(model, values)
+               if not newvalues then return end
+               if newvalues.htext=="" then
+                       newvalues.rounded=true
+                       create_boxed(model,newvalues,pos,prim)
+               else
+                       create_tabbed(model,newvalues,pos,prim)
+               end
+       elseif otype==BOXED_OTHER or otype==BOXED_TEXT then
+               edit_box(model,prim)
+       end
+end
+
+-- saving the old function
+function _G.MODEL:presentation_backup_actinon_edit () end
+_G.MODEL.presentation_backup_action_edit = _G.MODEL.action_edit
+
+-- modify the global edit action
+function _G.MODEL:action_edit()
+       local p = self:page()
+       local prim = p:primarySelection()
+       if not prim then
+          self:presentation_backup_action_edit()
+          return 
+       end
+       local obj = p[prim]
+       if obj:type() == "group" then
+          action_edit_group(self, prim, obj)
+       else
+          self:presentation_backup_action_edit()
+       end
+end
+
+-- Run to create a new object
+function tabbedboxed(model)
+       local values=edit_tabbed_values(model)
+       if not values then return end
+       if values.htext=="" then
+               values.rounded=true
+               create_boxed(model,values)
+       else
+               create_tabbed(model,values)
+       end
+end
+
+-- deselect all selected
+function deselectAll(model)
+       local doc = model.doc
+       for i,p in doc:pages() do
+               p:deselectAll()
+       end
+end
+
+-- box the selected objects
+function boxit(model)
+       local p=model:page()
+       local box = ipe.Rect()
+       local elements={0}
+       for i,obj,sel,layer in p:objects() do
+               if sel then
+                       box:add(p:bbox(i))
+                       elements[#elements+1]=obj:clone()
+               end
+       end
+       if #elements==1 then
+               model.ui:explain('No selection to box')
+               return
+       end
+       local s=8
+       local layout = model.doc:sheets():find("layout")
+--     local maxx=layout.framesize.x
+
+       local x1=box:bottomLeft().x-s
+--     if x1 < 0 then x1 = 0 end
+       local y1=box:bottomLeft().y-s
+
+       local x2=box:topRight().x+s
+--     if x2 > maxx then x2 = maxx end
+       local y2=box:topRight().y+s
+
+       local d=box_property_dialog(model)
+
+       local colors = model.doc:sheets():allNames("color")
+       local pens= model.doc:sheets():allNames("pen")
+       local pathmodes = {"stroked", "strokedfilled", "filled"}
+
+       -- default values
+       d:set("rounded", true)
+
+       d:set("pathmode", indexOf(PREFERRED_BOX_MODE,pathmodes))
+
+       if indexOf("box_border",colors) then
+               d:set("stroke", indexOf("box_border",colors))
+       elseif model.attributes.stroke then
+               d:set("stroke", indexOf(model.attributes.stroke,colors) )
+       end
+
+       if indexOf("box_fill",colors) then
+               d:set("fill", indexOf("box_fill",colors))
+       elseif model.attributes.fill then
+               d:set("fill", indexOf(model.attributes.fill,colors) )
+       end
+       
+       if indexOf("boxborder",pens) then
+               d:set("pen", indexOf("boxborder",pens))
+       elseif model.attributes.pen then
+               d:set("pen", indexOf(model.attributes.pen,pens) )
+       end
+
+       local r = d:execute(BOX_DIALOG_SIZE)
+
+       if not r then return end
+       local pathmode=pathmodes[d:get("pathmode")]
+       local stroke=colors[d:get("stroke")]
+       local fill=colors[d:get("fill")]
+       local pen=pens[d:get("pen")]
+
+       local boxshape
+       if d:get('rounded') and d:get('pointer') then
+               boxshape=boxshape_round_pointer
+       elseif d:get('rounded') and not d:get('pointer') then
+               boxshape=boxshape_round
+       elseif not d:get('rounded') and d:get('pointer') then
+               boxshape=boxshape_square_pointer
+       else
+               boxshape=boxshape_square
+       end
+
+       local shape = { boxshape(V(x1,y1), V(x2,y2)) }
+
+       local obj = path(model, shape, {pen=pen, pathmode=pathmode, stroke=stroke, fill=fill})
+
+       elements[1]=obj
+       local final = ipe.Group(elements)
+
+       local t = { label="add box", pno=model.pno, vno=model.vno,
+                                       layer=p:active(model.vno), object=obj,
+                                       selection=model:selection(),
+                                       undo=_G.revertOriginal,
+                                       original=p:clone(),
+                                       final=final }
+       t.redo = function (t, doc)
+                                local p = doc[t.pno]
+                                for i = #t.selection,1,-1 do p:remove(t.selection[i]) end
+                                p:insert(nil, t.final, 1, t.layer)
+                        end
+       model:register(t)
+end
+
+methods = {
+  { label = "Box It", run=boxit},
+  { label = "Tabbed/Boxed Text", run=tabbedboxed},
+  { label = "Deselect All", run=deselectAll},
+}
+