----------------------------------------------------------------------
-- tools.lua
----------------------------------------------------------------------
--[[

    This file is part of the extensible drawing editor Ipe.
    Copyright (C) 1993-2009  Otfried Cheong

    Ipe 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.

    As a special exception, you have permission to link Ipe with the
    CGAL library and distribute executables, as long as you follow the
    requirements of the Gnu General Public License in regard to all of
    the software in the executable aside from CGAL.

    Ipe 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 Ipe; if not, you can find it at
    "http://www.gnu.org/copyleft/gpl.html", or write to the Free
    Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.

--]]

function externalEditor(d, field)
  local text = d:get(field)
  local fname = os.tmpname()
  local f = io.open(fname, "w")
  f:write(text)
  f:close()
  ipeui.WaitDialog(string.format(prefs.external_editor, fname))
  f = io.open(fname, "r")
  text = f:read("*all")
  f:close()
  os.remove(fname)
  d:set(field, text)
end

function addEditorField(d, field, row, col)
  if config.platform ~= "win" and prefs.external_editor then
    d:add("editor", "button",
	  { label="&Editor",
	    action=function (d) externalEditor(d, field) end },
	  row, col)
  end
end

----------------------------------------------------------------------

local function circleshape(center, radius)
  return { type="ellipse";
	   ipe.Matrix(radius, 0, 0, radius, center.x, center.y) }
end

local function segmentshape(v1, v2)
  return { type="curve", closed=false; { type="segment"; v1, v2 } }
end

local function boxshape(v1, v2)
  return { type="curve", closed=true;
	   { type="segment"; v1, V(v1.x, v2.y) },
	   { type="segment"; V(v1.x, v2.y), v2 },
	   { type="segment"; v2, V(v2.x, v1.y) } }
end

local function arcshape(center, radius, alpha, beta)
  local a = ipe.Arc(ipe.Matrix(radius, 0, 0, radius, center.x, center.y),
		    alpha, beta)
  local v1 = center + radius * ipe.Direction(alpha)
  local v2 = center + radius * ipe.Direction(beta)
  return { type="curve", closed=false; { type="arc", arc=a; v1, v2 } }
end

local function rarcshape(center, radius, alpha, beta)
  local a = ipe.Arc(ipe.Matrix(radius, 0, 0, -radius, center.x, center.y),
		    alpha, beta)
  local v1 = center + radius * ipe.Direction(alpha)
  local v2 = center + radius * ipe.Direction(beta)
  return { type="curve", closed=false; { type="arc", arc=a; v1, v2 } }
end

----------------------------------------------------------------------

local VERTEX = 1
local SPLINE = 2
local QUAD = 3
local BEZIER = 4
local ARC = 5

LINESTOOL = {}
LINESTOOL.__index = LINESTOOL

-- mode is one of "lines", "polygons", "splines"
function LINESTOOL:new(model, mode)
  local tool = {}
  setmetatable(tool, LINESTOOL)
  tool.model = model
  tool.mode = mode
  local v = model.ui:pos()
  tool.v = { v, v }
  model.ui:setAutoOrigin(v)
  if mode == "splines" then
    tool.t = { VERTEX, SPLINE }
  else
    tool.t = { VERTEX, VERTEX }
  end
  model.ui:shapeTool(tool)
  tool.setColor(1.0, 0, 0)
  return tool
end

function LINESTOOL:last()
  return self.t[#self.t]
end

function LINESTOOL:has_segs(count)
  if #self.t < count then return false end
  local result = true
  for i = #self.t - count + 1,#self.t do
    if self.t[i] ~= VERTEX then return false end
  end
  return true
end

local function compute_arc(p0, pmid, p1)
  if p0 == p1 then return { type="segment", p0, p1 } end
  local l1 = ipe.Line(p0, (pmid- p0):normalized())
  local l2 = ipe.Line(p0, l1:normal())
  local bi1 = ipe.Bisector(p0, p1)
  local center = l2:intersects(bi1)
  local side = l1:side(p1)
  if side == 0.0 or not center then return { type="segment", p0, p1 } end
  local u = p0 - center
  local alpha = side * u:angle()
  local beta = side * ipe.normalizeAngle((p1 - center):angle(), alpha)
  local radius = u:len()
  local m = { radius, 0, 0, side * radius, center.x, center.y }
  return { type = "arc", p0, p1, arc = ipe.Arc(ipe.Matrix(m), alpha, beta) }
end

-- compute orientation of tangent to previous segment at its final point
function LINESTOOL:compute_orientation()
  if self.t[#self.t - 2] ~= ARC then
    return (self.v[#self.v - 1] - self.v[#self.v - 2]):angle()
  else
    -- only arc needs special handling
    local arc = compute_arc(self.v[#self.v - 3], self.v[#self.v - 2],
			    self.v[#self.v - 1])
    if arc.type == "arc" then
      local alpha, beta = arc.arc:angles()
      local dir = ipe.Direction(beta + math.pi / 2)
      return (arc.arc:matrix():linear() * dir):angle()
    else
      return (self.v[#self.v - 1] - self.v[#self.v - 3]):angle()
    end
  end
end

function LINESTOOL:compute()
  self.shape = { type="curve", closed=(self.mode == "polygons") }
  local i = 2
  while i <= #self.v do
    -- invariant: t[i-1] == VERTEX
    local seg
    if self.t[i] == VERTEX then
      seg = { type="segment", self.v[i-1], self.v[i] }
      i = i + 1
    elseif self.t[i] == SPLINE then
      local j = i
      while j <= #self.v and self.t[j] == SPLINE do j = j + 1 end
      if j > #self.v then j = #self.v end
      seg = { type="spline" }
      for k = i-1,j do seg[#seg+1] = self.v[k] end
      i = j + 1
    elseif self.t[i] == QUAD then
      seg = { type="quad", self.v[i-1], self.v[i], self.v[i+1] }
      i = i + 2
    elseif self.t[i] == BEZIER then
      seg = { type="bezier", self.v[i-1], self.v[i], self.v[i+1], self.v[i+2] }
      i = i + 3
    elseif self.t[i] == ARC then
      seg = compute_arc(self.v[i-1], self.v[i], self.v[i+1])
      i = i + 2
    end
    self.shape[#self.shape + 1] = seg
  end
  self.setShape( { self.shape } )
end

function LINESTOOL:mouseButton(button, modifiers, press)
  if not press then return end
  local v = self.model.ui:pos()
  self.v[#self.v] = v
  if button == 2 then
    if self:last() == SPLINE then self.t[#self.t] = VERTEX end
    self:compute()
    self.model.ui:finishTool()
    local obj = ipe.Path(self.model.attributes, { self.shape }, true)
    self.model:creation("create path", obj)
    return
  end
  self.v[#self.v + 1] = v
  self.model.ui:setAutoOrigin(v)
  if self:last() == SPLINE then
    local typ = modifiers.shift and VERTEX or SPLINE
    self.t[#self.t] = typ
    self.t[#self.t + 1] = typ
  else
    self.t[#self.t + 1] = VERTEX
  end
  self:compute()
  self.model.ui:update(false) -- update tool
  self:explain()
end

function LINESTOOL:mouseMove(button, modifiers)
  self.v[#self.v] = self.model.ui:pos()
  self:compute()
  self.model.ui:update(false) -- update tool
  self:explain()
end

function LINESTOOL:explain()
  local s
  if self:last() == SPLINE then
    s = ("Left: add ctrl point | Shift+Left: switch to line mode" ..
	 " | Del: delete ctrl point")
  else
    s = "Left: add vtx | Del: delete vtx"
  end
  s = s .. " | Right: final vtx"
  if self:has_segs(2) then
    s = s .. " | S: spline mode"
  end
  if self:has_segs(3) then
    s = s .. " | Q: quadratic Bezier | A: circle arc"
  end
  if self:has_segs(4) then
    s = s .. " | C: cubic Bezier"
  end
  if #self.v > 2 and self.t[#self.t - 1] == VERTEX then
    s = s .. " | Y: set axis"
  end
  self.model.ui:explain(s)
end

function LINESTOOL:key(code, modifiers, text)
  if text == "\127" then  -- Delete
    if #self.v > 2 then
      table.remove(self.v)
      table.remove(self.t)
      if self:last() == QUAD or self:last() == ARC then
	self.t[#self.t] = VERTEX
      end
      if self:last() == BEZIER then
	self.t[#self.t] = VERTEX
	self.t[#self.t-1] = VERTEX
      end
      self:compute()
      self.model.ui:update(false)
      self:explain()
    else
      self.model.ui:finishTool()
    end
    return true
  elseif text == "\027" then
    self.model.ui:finishTool()
    return true
  elseif self:has_segs(2) and text == "s" then
    self.t[#self.t] = SPLINE
    self:compute()
    self.model.ui:update(false)
    self:explain()
  elseif self:has_segs(3) and text == "q" then
    self.t[#self.t - 1] = QUAD
    self:compute()
    self.model.ui:update(false)
    self:explain()
  elseif self:has_segs(3) and text == "a" then
    self.t[#self.t - 1] = ARC
    self:compute()
    self.model.ui:update(false)
    self:explain()
  elseif self:has_segs(4) and text == "c" then
    self.t[#self.t - 1] = BEZIER
    self.t[#self.t - 2] = BEZIER
    self:compute()
    self.model.ui:update(false)
    self:explain()
  elseif #self.v > 2 and self.t[#self.t - 1] == VERTEX and text == "y" then
    -- set axis
    self.model.snap.with_axes = true
    self.model.snap.snapangle = true
    self.model.snap.origin = self.v[#self.v - 1]
    self.model.snap.orientation = self:compute_orientation()
    self.model.ui:setSnap(self.model.snap)
    self.model.ui:setActionState("snapangle", true)
    self.model.ui:update(true)   -- redraw coordinate system
    self:explain()
  else
    return false
  end
end

----------------------------------------------------------------------

BOXTOOL = {}
BOXTOOL.__index = BOXTOOL

function BOXTOOL:new(model, square)
  local tool = {}
  setmetatable(tool, BOXTOOL)
  tool.model = model
  tool.square = square
  local v = model.ui:pos()
  tool.v = { v, v }
  model.ui:shapeTool(tool)
  tool.setColor(1.0, 0, 0)
  return tool
end

function BOXTOOL:compute()
  self.v[2] = self.model.ui:pos()
  if self.square then
    local d = self.v[2] - self.v[1]
    local sign = V(d.x > 0 and 1 or -1, d.y > 0 and 1 or -1)
    local dd = math.max(math.abs(d.x), math.abs(d.y))
    self.v[2] = self.v[1] + dd * sign
  end
  self.shape = boxshape(self.v[1], self.v[2])
end

function BOXTOOL:mouseButton(button, modifiers, press)
  if not press then return end
  self:compute()
  self.model.ui:finishTool()
  local obj = ipe.Path(self.model.attributes, { self.shape })
  self.model:creation("create box", obj)
end

function BOXTOOL:mouseMove(button, modifiers)
  self:compute()
  self.setShape( { self.shape } )
  self.model.ui:update(false) -- update tool
end

function BOXTOOL:key(code, modifiers, text)
  if text == "\027" then
    self.model.ui:finishTool()
    return true
  else
    return false
  end
end

----------------------------------------------------------------------

SPLINEGONTOOL = {}
SPLINEGONTOOL.__index = SPLINEGONTOOL

function SPLINEGONTOOL:new(model)
  local tool = {}
  setmetatable(tool, SPLINEGONTOOL)
  tool.model = model
  local v = model.ui:pos()
  tool.v = { v, v }
  model.ui:shapeTool(tool)
  tool.setColor(1.0, 0, 0)
  return tool
end

function SPLINEGONTOOL:compute()
  if #self.v == 2 then
    self.shape = segmentshape(self.v[1], self.v[2])
  else
    self.shape = { type="closedspline" }
    for _,v in ipairs(self.v) do self.shape[#self.shape + 1] = v end
  end
  self.setShape( { self.shape } )
end

function SPLINEGONTOOL:explain()
  local s = "Left: Add vertex | Right: Add final vertex | Del: Delete vertex"
  self.model.ui:explain(s)
end

function SPLINEGONTOOL:mouseButton(button, modifiers, press)
  if not press then return end
  local v = self.model.ui:pos()
  self.v[#self.v] = v
  if button == 2 then
    self.model.ui:finishTool()
    local obj = ipe.Path(self.model.attributes, { self.shape })
    self.model:creation("create splinegon", obj)
    return
  end
  self.v[#self.v + 1] = v
  self:compute()
  self.model.ui:update(false) -- update tool
  self:explain()
end

function SPLINEGONTOOL:mouseMove(button, modifiers)
  self.v[#self.v] = self.model.ui:pos()
  self:compute()
  self.model.ui:update(false) -- update tool
end

function SPLINEGONTOOL:key(code, modifiers, text)
  if text == "\127" then  -- Delete
    if #self.v > 2 then
      table.remove(self.v)
      self:compute()
      self.model.ui:update(false)
      self:explain()
    else
      self.model.ui:finishTool()
    end
    return true
  elseif text == "\027" then
    self.model.ui:finishTool()
    return true
  else
    self:explain()
    return false
  end
end

----------------------------------------------------------------------

CIRCLETOOL = {}
CIRCLETOOL.__index = CIRCLETOOL

function CIRCLETOOL:new(model, mode)
  local tool = {}
  setmetatable(tool, CIRCLETOOL)
  tool.model = model
  tool.mode = mode
  local v = model.ui:pos()
  tool.v = { v, v, v }
  tool.cur = 2
  model.ui:shapeTool(tool)
  tool.setColor(1.0, 0, 0)
  return tool
end

function CIRCLETOOL:compute()
  if self.mode == "circle1" then
    self.shape = circleshape(self.v[1], (self.v[2] - self.v[1]):len())
  elseif self.mode == "circle2" then
    self.shape = circleshape(0.5 * (self.v[1] + self.v[2]),
			     (self.v[2] - self.v[1]):len() / 2.0)
  elseif self.mode == "circle3" then
    if self.cur == 2 or self.v[3] == self.v[2] then
      self.shape = circleshape(0.5 * (self.v[1] + self.v[2]),
			       (self.v[2] - self.v[1]):len() / 2.0)
    else
      local l1 = ipe.LineThrough(self.v[1], self.v[2])
      if math.abs(l1:side(self.v[3])) < 1e-9 then
	self.shape = segmentshape(self.v[1], self.v[2])
      else
	local l2 = ipe.LineThrough(self.v[2], self.v[3])
	local bi1 = ipe.Bisector(self.v[1], self.v[2])
	local bi2 = ipe.Bisector(self.v[2], self.v[3])
	local center = bi1:intersects(bi2)
	self.shape = circleshape(center, (self.v[1] - center):len())
      end
    end
  end
end

function CIRCLETOOL:mouseButton(button, modifiers, press)
  if not press then return end
  local v = self.model.ui:pos()
  -- refuse point identical to previous
  if v == self.v[self.cur - 1] then return end
  self.v[self.cur] = v
  self:compute()
  if self.cur == 3 or (self.mode ~= "circle3" and self.cur == 2) then
    self.model.ui:finishTool()
    local obj = ipe.Path(self.model.attributes, { self.shape })
    self.model:creation("create circle", obj)
  else
    self.cur = self.cur + 1
    self.model.ui:update(false)
  end
end

function CIRCLETOOL:mouseMove(button, modifiers)
  self.v[self.cur] = self.model.ui:pos()
  self:compute()
  self.setShape({ self.shape })
  self.model.ui:update(false) -- update tool
end

function CIRCLETOOL:key(code, modifiers, text)
  if text == "\027" then
    self.model.ui:finishTool()
    return true
  else
    return false
  end
end

----------------------------------------------------------------------

ARCTOOL = {}
ARCTOOL.__index = ARCTOOL

function ARCTOOL:new(model, mode)
  local tool = {}
  setmetatable(tool, ARCTOOL)
  tool.model = model
  tool.mode = mode
  local v = model.ui:pos()
  tool.v = { v, v, v }
  tool.cur = 2
  model.ui:shapeTool(tool)
  tool.setColor(1.0, 0, 0)
  return tool
end

function ARCTOOL:compute()
  local u = self.v[2] - self.v[1]
  if self.cur == 2 then
    if self.mode == "arc3" then
      self.shape = circleshape(0.5 * (self.v[1] + self.v[2]), u:len() / 2.0)
    else
      self.shape = circleshape(self.v[1], u:len())
    end
    return
  end
  local alpha = u:angle()
  local beta = (self.v[3] - self.v[1]):angle()
  if self.mode == "arc1" then
    self.shape = arcshape(self.v[1], u:len(), alpha, beta)
  elseif self.mode == "arc2" then
    self.shape = rarcshape(self.v[1], u:len(), alpha, beta)
  else
    local l1 = ipe.LineThrough(self.v[1], self.v[2])
    if math.abs(l1:side(self.v[3])) < 1e-9 or self.v[3] == self.v[2] then
      self.shape = segmentshape(self.v[1], self.v[2])
    else
      local l2 = ipe.LineThrough(self.v[2], self.v[3])
      local bi1 = ipe.Line(0.5 * (self.v[1] + self.v[2]), l1:normal())
      local bi2 = ipe.Line(0.5 * (self.v[2] + self.v[3]), l2:normal())
      local center = bi1:intersects(bi2)
      u = self.v[1] - center
      alpha = u:angle()
      beta = ipe.normalizeAngle((self.v[2] - center):angle(), alpha)
      local gamma = ipe.normalizeAngle((self.v[3] - center):angle(), alpha)
      if gamma > beta then
	self.shape = arcshape(center, u:len(), alpha, gamma)
      else
	self.shape = rarcshape(center, u:len(), alpha, gamma)
      end
    end
  end
end

function ARCTOOL:mouseButton(button, modifiers, press)
  if not press then return end
  local v = self.model.ui:pos()
  -- refuse point identical to previous
  if v == self.v[self.cur - 1] then return end
  self.v[self.cur] = v
  self:compute()
  if self.cur == 3 then
    self.model.ui:finishTool()
    local obj = ipe.Path(self.model.attributes, { self.shape }, true)
    self.model:creation("create arc", obj)
  else
    self.cur = self.cur + 1
    self.model.ui:update(false)
  end
end

function ARCTOOL:mouseMove(button, modifiers)
  self.v[self.cur] = self.model.ui:pos()
  self:compute()
  self.setShape({ self.shape })
  self.model.ui:update(false) -- update tool
end

function ARCTOOL:key(code, modifiers, text)
  if text == "\027" then
    self.model.ui:finishTool()
    return true
  else
    return false
  end
end

----------------------------------------------------------------------

INKTOOL = {}
INKTOOL.__index = INKTOOL

function INKTOOL:new(model)
  local tool = {}
  setmetatable(tool, INKTOOL)
  tool.model = model
  local v = model.ui:pos()
  tool.v = { v }
  model.ui:shapeTool(tool)
  tool.setColor(1.0, 0, 0)
  return tool
end

function INKTOOL:compute()
  self.shape = { type="curve", closed=false }
  for i = 2, #self.v do
    self.shape[#self.shape + 1] = { type="segment", self.v[i-1], self.v[i] }
  end
  self.setShape( { self.shape } )
end

function INKTOOL:mouseButton(button, modifiers, press)
  self.model.ui:finishTool()
  if self.shape then
    local obj = ipe.Path(self.model.attributes, { self.shape })
    self.model:creation("create ink path", obj)
  end
end

function INKTOOL:mouseMove(button, modifiers)
  self.v[#self.v + 1] = self.model.ui:pos()
  self:compute()
  self.model.ui:update(false) -- update tool
end

function INKTOOL:key(code, modifiers, text)
  if text == "\027" then
    self.model.ui:finishTool()
    return true
  else
    return false
  end
end

----------------------------------------------------------------------

function MODEL:createMark()
  local obj = ipe.Reference(self.attributes, self.attributes.markshape,
			    self.ui:pos())
  self:creation("create mark", obj)
end

----------------------------------------------------------------------

function MODEL:createText(mode)
  local prompt
  local pos = self.ui:pos()
  if mode == "math" then
    prompt = "Enter Latex source for math formula"
  else
    prompt = "Enter Latex source"
  end
  local d = ipeui.Dialog(self.ui, "Create text object")
  d:add("label", "label", { label=prompt }, 1, 1, 1, 4)
  d:add("text", "text", { syntax="latex" }, 2, 1, 1, 4)
  d:add("ok", "button", { label="&Ok", action="accept" }, 3, 4)
  d:add("cancel", "button", { label="&Cancel", action="reject" }, 3, 3)
  addEditorField(d, "text", 3, 2)
  d:setStretch("row", 2, 1)
  d:setStretch("column", 1, 1)
  local r = d:execute(prefs.editor_size)
  if r then
    local t = d:get("text")
    if mode == "math" then
      t = "$" .. t .. "$"
    end
    local obj = ipe.Text(self.attributes, t, pos)
    self:creation("create text", obj)
  end
end

----------------------------------------------------------------------

function MODEL:createParagraph(pos, width, pinned)
  local styles = self.doc:sheets():allNames("textstyle")
  local sizes = self.doc:sheets():allNames("textsize")
  local d = ipeui.Dialog(self.ui, "Create text object")
  d:add("label", "label", { label="Enter latex source" }, 1, 1, 1, 6)
  d:add("text", "text", { syntax="latex" }, 2, 1, 1, 6)
  d:add("ok", "button", { label="&Ok", action="accept" }, 3, 5)
  d:add("cancel", "button", { label="&Cancel", action="reject" }, 3, 6)
  addEditorField(d, "text", 3, 4)
  d:add("style", "combo", styles, 3, 1)
  d:add("size", "combo", sizes, 3, 2)
  d:setStretch("row", 2, 1)
  d:setStretch("column", 3, 1)
  local style = indexOf(self.attributes.textstyle, styles)
  if not style then style = indexOf("normal", styles) end
  local size = indexOf(self.attributes.textsize, sizes)
  if not size then size = indexOf("normal", sizes) end
  d:set("style", style)
  d:set("size", size)
  local r = d:execute(prefs.editor_size)
  if r then
    local t = d:get("text")
    local style = styles[d:get("style")]
    local size = sizes[d:get("size")]
    local obj = ipe.Text(self.attributes, t, pos, width)
    obj:set("textsize", size)
    obj:set("textstyle", style)
    if pinned then
      obj:set("pinned", "horizontal")
    end
    self:creation("create text paragraph", obj)
  end
end

function MODEL:action_insert_text_box()
  local r = self:page():textBox(self.doc:sheets())
  self:createParagraph(r:topLeft(), r:width(), true)
end

----------------------------------------------------------------------

PARAGRAPHTOOL = {}
PARAGRAPHTOOL.__index = PARAGRAPHTOOL

function PARAGRAPHTOOL:new(model)
  local tool = {}
  setmetatable(tool, PARAGRAPHTOOL)
  tool.model = model
  local v = model.ui:pos()
  tool.v = { v, v }
  model.ui:shapeTool(tool)
  tool.setColor(1.0, 0, 1.0)
  return tool
end

function PARAGRAPHTOOL:compute()
  self.v[2] = V(self.model.ui:pos().x, self.v[1].y - 20)
  self.shape = boxshape(self.v[1], self.v[2])
end

function PARAGRAPHTOOL:mouseButton(button, modifiers, press)
  if not press then return end
  self:compute()
  self.model.ui:finishTool()
  local pos = V(math.min(self.v[1].x, self.v[2].x), self.v[1].y)
  local wid = math.abs(self.v[2].x - self.v[1].x)
  self.model:createParagraph(pos, wid)
end

function PARAGRAPHTOOL:mouseMove(button, modifiers)
  self:compute()
  self.setShape( { self.shape } )
  self.model.ui:update(false) -- update tool
end

function PARAGRAPHTOOL:key(code, modifiers, text)
  if text == "\027" then
    self.model.ui:finishTool()
    return true
  else
    return false
  end
end

----------------------------------------------------------------------

CHANGEWIDTHTOOL = {}
CHANGEWIDTHTOOL.__index = CHANGEWIDTHTOOL

function CHANGEWIDTHTOOL:new(model, prim, obj)
  local tool = {}
  setmetatable(tool, CHANGEWIDTHTOOL)
  tool.model = model
  tool.prim = prim
  tool.obj = obj
  tool.pos = obj:matrix() * obj:position()
  tool.wid = obj:get("width")
  model.ui:shapeTool(tool)
  tool.setColor(1.0, 0, 1.0)
  tool.setShape( { boxshape(tool.pos, tool.pos + V(tool.wid, -20)) } )
  model.ui:update(false) -- update tool
  return tool
end

function CHANGEWIDTHTOOL:compute()
  local w = self.model.ui:pos()
  self.nwid = self.wid + (w.x - self.v.x)
  self.shape = boxshape(self.pos, self.pos + V(self.nwid, -20))
end

function CHANGEWIDTHTOOL:mouseButton(button, modifiers, press)
  if press then
    if not self.v then self.v = self.model.ui:pos() end
  else
    self:compute()
    self.model.ui:finishTool()
    self.model:setAttributeOfPrimary(self.prim, "width", self.nwid)
  end
end

function CHANGEWIDTHTOOL:mouseMove(button, modifiers)
  if self.v then
    self:compute()
    self.setShape( { self.shape } )
    self.model.ui:update(false) -- update tool
  end
end

function CHANGEWIDTHTOOL:key(code, modifiers, text)
  if text == "\027" then
    self.model.ui:finishTool()
    return true
  else
    return false
  end
end

function MODEL:action_change_width()
  local p = self:page()
  local prim = p:primarySelection()
  if not prim or p[prim]:type() ~= "text" or not p[prim]:get("minipage") then
    self.ui:explain("no selection or not a minipage object")
    return
  end
  CHANGEWIDTHTOOL:new(self, prim, p[prim])
end

----------------------------------------------------------------------

function MODEL:startTransform(mode, withShift)
  self:updateCloseSelection(false)
  if mode == "stretch" and withShift then mode = "scale" end
  self.ui:transformTool(self:page(), self.vno, mode, withShift,
			function (m) self:transformation(mode, m) end)
end

function MODEL:startModeTool(modifiers)
  if self.mode == "select" then
    self.ui:selectTool(self:page(), self.vno,
		       prefs.select_distance, modifiers.shift)
  elseif (self.mode == "translate" or self.mode == "stretch"
	  or self.mode == "rotate") then
    self:startTransform(self.mode, modifiers.shift)
  elseif self.mode == "pan" then
    self.ui:panTool(self:page(), self.vno)
  elseif self.mode == "rectangles" then
    BOXTOOL:new(self, modifiers.shift)
  elseif self.mode == "splinegons" then
    SPLINEGONTOOL:new(self)
  elseif (self.mode == "lines" or self.mode == "polygons" or
	  self.mode == "splines") then
    LINESTOOL:new(self, self.mode)
  elseif self.mode:sub(1,6) == "circle" then
    CIRCLETOOL:new(self, self.mode)
  elseif self.mode:sub(1,3) == "arc" then
    ARCTOOL:new(self, self.mode)
  elseif self.mode == "ink" then
    INKTOOL:new(self)
  elseif self.mode == "marks" then
    self:createMark()
  elseif self.mode == "label" or self.mode == "math" then
    self:createText(self.mode)
  elseif self.mode == "paragraph" then
    PARAGRAPHTOOL:new(self)
  else
    print("start mode tool:", self.mode)
  end
end

function MODEL:mouseAction(button, modifiers)
  -- print("Mouse action", button, modifiers.alt, modifiers.control)
  if button == 1 then
    -- left mouse button
    if modifiers.alt then
      self:startTransform("translate", modifiers.shift)
    elseif modifiers.control then
      self.ui:selectTool(self:page(), self.vno,
			 prefs.select_distance, modifiers.shift)
    else
      self:startModeTool(modifiers)
    end
  elseif button == 4 then
    -- middle mouse button
    self.ui:panTool(self:page(), self.vno)
  elseif button == 2 then
    -- right mouse button
    if modifiers.alt then
      self:startTransform("rotate", modifiers.shift)
    elseif modifiers.control then
      self:startTransform("stretch", modifiers.shift)
    elseif modifiers.shift then
      self.ui:panTool(self:page(), self.vno)
    else
      self:propertiesPopup()
    end
  end
end

----------------------------------------------------------------------

function MODEL:action_edit_text(prim, obj)
  local mp = obj:get("minipage")
  local styles = self.doc:sheets():allNames("textstyle")
  local sizes = self.doc:sheets():allNames("textsize")
  local d = ipeui.Dialog(self.ui, "Edit text object")
  d:add("label", "label", { label="Edit latex source" }, 1, 1, 1, 6)
  d:add("text", "text", { syntax="latex" }, 2, 1, 1, 6)
  d:add("ok", "button", { label="&Ok", action="accept" }, 3, 6)
  d:add("cancel", "button", { label="&Cancel", action="reject" }, 3, 5)
  addEditorField(d, "text", 3, 4)
  d:setStretch("row", 2, 1)
  d:setStretch("column", 3, 1)
  d:set("text", obj:text())
  local style = nil
  local size = nil
  if mp then
    style = indexOf(obj:get("textstyle"), styles)
    if style then
      d:add("style", "combo", styles, 3, 1)
      d:set("style", style)
    end
    size = indexOf(obj:get("textsize"), sizes)
    if size then
      d:add("size", "combo", sizes, 3, 2)
      d:set("size", size)
    end
  end
  local r = d:execute(prefs.editor_size)
  if not r or string.match(d:get("text"), "^%s*$") then return end
  local final = obj:clone()
  final:setText(d:get("text"))
  if style then final:set("textstyle", styles[d:get("style")]) end
  if size then final:set("textsize", sizes[d:get("size")]) end
  local t = { label="edit text",
	      pno=self.pno,
	      vno=self.vno,
	      original=obj: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
  self:register(t)
end

function MODEL:action_edit()
  local p = self:page()
  local prim = p:primarySelection()
  if not prim then self.ui:explain("no selection") return end
  local obj = p[prim]
  if obj:type() == "text" then
    self:action_edit_text(prim, obj)
  elseif obj:type() == "path" then
    self:action_edit_path(prim, obj)
  else
    self:warning("Cannot edit " .. obj:type() .. " object",
		 "Only text objects and path objects can be edited")
  end
end

----------------------------------------------------------------------

PASTETOOL = {}
PASTETOOL.__index = PASTETOOL

function PASTETOOL:new(model, elements, pos)
  local tool = {}
  _G.setmetatable(tool, PASTETOOL)
  tool.model = model
  tool.elements = elements
  tool.start = model.ui:pos()
  if pos then tool.start = pos end
  local obj = ipe.Group(elements)
  model.ui:pasteTool(obj, tool)
  tool.setColor(1.0, 0, 0)
  tool.translation = model.ui:pos() - tool.start
  tool.setMatrix(ipe.Translation(tool.translation))
  return tool
end

function PASTETOOL:mouseButton(button, modifiers, press)
  self.translation = self.model.ui:pos() - self.start
  self.model.ui:finishTool()
  local t = { label="paste objects at cursor",
	      pno = self.model.pno,
	      vno = self.model.vno,
	      elements = self.elements,
	      layer = self.model:page():active(self.model.vno),
	      translation = ipe.Translation(self.translation),
	    }
  t.undo = function (t, doc)
	     local p = doc[t.pno]
	     for i = 1,#t.elements do p:remove(#p) end
	   end
  t.redo = function (t, doc)
	     local p = doc[t.pno]
	     for i,obj in ipairs(t.elements) do
	       p:insert(nil, obj, 2, t.layer)
	       p:transform(#p, t.translation)
	     end
	     p:ensurePrimarySelection()
	   end
  self.model:page():deselectAll()
  self.model:register(t)
end

function PASTETOOL:mouseMove(button, modifiers)
  self.translation = self.model.ui:pos() - self.start
  self.setMatrix(ipe.Translation(self.translation))
  self.model.ui:update(false) -- update tool
end

function PASTETOOL:key(code, modifiers, text)
  if text == "\027" then
    self.model.ui:finishTool()
    return true
  else
    return false
  end
end

function MODEL:action_paste_at_cursor()
  local data = ipeui.clipboard()
  if data:sub(1,13) ~= "<ipeselection" then
    self:warning("No Ipe selection to paste")
    return
  end
  local pos
  local px, py = data:match('^<ipeselection pos="([%d%.]+) ([%d%.]+)"')
  if px then pos = V(tonumber(px), tonumber(py)) end
  local elements = ipe.Object(data)
  if not elements then
    self:warning("Could not parse Ipe selection on clipboard")
    return
  end
  PASTETOOL:new(self, elements, pos)
end

----------------------------------------------------------------------
