// --------------------------------------------------------------------
// A page of a document.
// --------------------------------------------------------------------
/*

    This file is part of the extensible drawing editor Ipe.
    Copyright (C) 1993-2004  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 2 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.

*/

#include "ipepage.h"
#include "ipevisitor.h"
#include "ipegroup.h"
#include "ipepath.h"
#include "ipetext.h"
#include "ipemark.h"
#include "ipepainter.h"
#include "ipeiml.h"
#include "ipeutils.h"

// --------------------------------------------------------------------

/*! \class IpeLayer
  \ingroup doc
  \brief A layer of an IpePage.

  The objects on one IpePage can belong to any number of layers.
  Layers are orthogonal to the back-to-front ordering of objects, so a
  "layer" is just another attribute of the object.

  Layers have several attributes:

  - They may be editable or locked.  Objects in a locked layer cannot
    be selected, and a locked layer cannot be made active in the Ipe
    UI.  This more or less means that the contents of such a layer
    cannot be modified---but that's a consequence of the UI, Ipelib
    contains no special handling of locked layers.

  - A layer may be dimmed.

  - A layer may have snapping on or off---objects will behave
    magnetically only if their layer has snapping on.

  The PDF output generated for an IpePage depends on its \e
  views. Each view may list a number of layers to be displayed at that
  stage.  Multiple \e views may show different subsets of layers.
*/

//! Construct with name. Default attributes.
IpeLayer::IpeLayer(IpeString name)
{
  iName = name;
  iFlags = 0;
}

//! Construct from a single XML tag.
IpeLayer::IpeLayer(const IpeXmlAttributes &attr)
{
  iName = attr["name"];
  iFlags = 0;
  if (attr["visible"] == "dim")
    iFlags |= EDim;
  if (attr["edit"] == "no")
    iFlags |= ELocked;
}

//! Write a single XML tag describing this layer.
void IpeLayer::SaveAsXml(IpeStream &stream) const
{
  stream << "<layer name=\"" << iName << "\"";
  if (iFlags & EDim)
    stream << " visible=\"dim\"";
  if (iFlags & ELocked)
    stream << " edit=\"no\"";
  stream << "/>\n";
}

//! Set dimming.
void IpeLayer::SetDimmed(bool flag)
{
  iFlags &= ~EDim;
  if (flag) iFlags |= EDim;
}

//! Set locking.
void IpeLayer::SetLocked(bool flag)
{
  iFlags &= ~ELocked;
  if (flag) iFlags |= ELocked;
}

//! Set snapping.
void IpeLayer::SetSnapping(bool flag)
{
  iFlags &= ~ENoSnapping;
  if (!flag) iFlags |= ENoSnapping;
}

// --------------------------------------------------------------------

/*! \class IpeView
  \ingroup doc
  \brief A view of the page (set of layers, duration, effect, transition style)

  An IpePage contains a whole list of these.
*/

//! Construct default view.
IpeView::IpeView()
{
  iEffect = ENormal;
  iDuration = 0;
  iTransitionTime = 1;
}

//! Create a view from XML tag.
IpeView::IpeView(const IpeXmlAttributes &attr)
{
  iEffect = ENormal;
  iDuration = 0;
  iTransitionTime = 1;
  IpeLex st(attr["layers"]);
  do {
    iLayers.push_back(st.NextToken());
    st.SkipWhitespace();
  } while (!st.Eos());
  iActive = attr["active"];
  IpeString str;
  if (attr.Has("duration", str))
    iDuration = IpeLex(str).GetInt();
  if (attr.Has("transition", str))
    iTransitionTime = IpeLex(str).GetInt();
  if (attr.Has("effect", str))
    iEffect = TEffect(IpeLex(str).GetInt());
}

//! Write a single XML tag representing this view.
void IpeView::SaveAsXml(IpeStream &stream) const
{
  stream << "<view layers=\"";
  for (std::vector<IpeString>::const_iterator it = iLayers.begin();
       it != iLayers.end(); ++it) {
    if (it != iLayers.begin())
      stream << " ";
    stream << *it;
  }
  stream << "\"";
  if (!iActive.empty())
    stream << " active=\"" << iActive << "\"";
  if (iDuration > 0)
    stream << " duration=\"" << iDuration << "\"";
  if (iEffect != ENormal)
    stream << " effect=\"" << int(iEffect) << "\"";
  if (iTransitionTime > 1)
    stream << " transition=\"" << iTransitionTime << "\"";
  stream << "/>\n";
}


//! Write part of page dictionary.
/*! Write part of page dictionary indicating effect,
  including the two keys /Dur and /Trans. */
void IpeView::PageDictionary(IpeStream &stream) const
{
  if (iDuration > 0)
    stream << "/Dur " << iDuration << "\n";
  if (iEffect != ENormal) {
    stream << "/Trans << /D " << iTransitionTime << " /S ";
    switch (iEffect) {
    case ESplitHI: stream << "/Split /Dm /H /M /I"; break;
    case ESplitHO: stream << "/Split /Dm /H /M /O"; break;
    case ESplitVI: stream << "/Split /Dm /V /M /I"; break;
    case ESplitVO: stream << "/Split /Dm /V /M /O"; break;
    case EBlindsH: stream << "/Blinds /Dm /H"; break;
    case EBlindsV: stream << "/Blinds /Dm /V"; break;
    case EBoxI: stream << "/Box /M /I"; break;
    case EBoxO: stream << "/Box /M /O"; break;
    case EWipeLR: stream << "/Wipe /Di 0"; break;
    case EWipeBT: stream << "/Wipe /Di 90"; break;
    case EWipeRL: stream << "/Wipe /Di 180"; break;
    case EWipeTB: stream << "/Wipe /Di 270"; break;
    case EDissolve: stream << "/Dissolve"; break;
    case EGlitterLR: stream << "/Glitter /Di 0"; break;
    case EGlitterTB: stream << "/Glitter /Di 270"; break;
    case EGlitterD: stream << "/Glitter /Di 315"; break;
    case ENormal: break; // to satisfy compiler
    }
    stream << " >>\n";
  }
}

//! Does this view contain the layer \a name?
bool IpeView::HasLayer(IpeString name) const
{
  return (std::find(iLayers.begin(), iLayers.end(), name) != iLayers.end());
}

//! Remove the layer \a name from this view.
void IpeView::DeleteLayer(IpeString name)
{
  std::vector<IpeString>::iterator it =
    std::find(iLayers.begin(), iLayers.end(), name);
  if (it != iLayers.end())
    iLayers.erase(it);
}

//! Add layer \a name to this view.
/*! It is okay if the view already contains this layer. */
void IpeView::AddLayer(IpeString name)
{
  if (!HasLayer(name))
    iLayers.push_back(name);
}

// --------------------------------------------------------------------

/*! \class IpePage
  \ingroup doc
  \brief An Ipe document page.

  Its main ingredients are a sequence of IpePgObjects, a list of
  IpeLayers, and a list of IpeViews.

  If you need to keep track of whether a document has been modified,
  you have to call SetEdited(true) whenever you modify an IpePgObject.

  The functions to modify the layer sequence and the views set the
  edited flag themselves.
*/

//! The default constructor creates a new empty page.
/*! This page still needs a layer and a view to be usable! */
IpePage::IpePage()
{
  iEdited = false;
  iGridSize = 0;
}

// --------------------------------------------------------------------

//! Save page in XML format.
void IpePage::SaveAsXml(IpePainter &painter, IpeStream &stream) const
{
  stream << "<page";
  if (iGridSize > 0)
    stream << " gridsize=\"" << iGridSize << "\"";
  if (!iSection[0].empty()) {
    stream << " section=\"";
    stream.PutXmlString(iSection[0]);
    stream << "\"";
  }
  if (!iSection[1].empty()) {
    stream << " subsection=\"";
    stream.PutXmlString(iSection[1]);
    stream << "\"";
  }
  stream << ">\n";
  for (int i = 0; i < CountLayers(); ++i)
    Layer(i).SaveAsXml(stream);
  for (int i = 0; i < CountViews(); ++i)
    View(i).SaveAsXml(stream);
  int currentLayer = -1;
  for (const_iterator it = begin(); it != end(); ++it) {
    IpeString layer;
    if (it->Layer() != currentLayer) {
      currentLayer = it->Layer();
      layer = Layer(currentLayer).iName;
    }
    it->Object()->SaveAsXml(painter, stream, layer);
  }
  stream << "</page>\n";
}

#if 0
//! Modify a layer.
/*! Sets the edited flag. */
void IpePage::SetLayer(int index, const IpeLayer &layer)
{
  assert(0 <= index && index < int(iLayers.size()));
  iLayers[index] = layer;
  iEdited = true;
}
#endif

//! Add a new layer at \a index (at the end if \a index is negative).
/*! Returns index of new layer, and sets edited flag.
  Layer numbers of all objects on page are adjusted if necessary.
 */
int IpePage::AddLayer(const IpeLayer &layer, int index)
{
  iEdited = true;
  if (index < 0) {
    iLayers.push_back(layer);
    return (iLayers.size() - 1);
  } else {
    IpeLayerSeq::iterator it = iLayers.begin() + index;
    iLayers.insert(it, layer);
    // adjust all objects!
    for (iterator pit = begin(); pit != end(); ++pit) {
      int l = pit->Layer();
      if (l >= index)
	pit->SetLayer(l + 1);
    }
    return index;
  }
}

//! Find layer with given name.
/*! Returns -1 if not found. */
int IpePage::FindLayer(IpeString name) const
{
  for (int i = 0; i < CountLayers(); ++i)
    if (Layer(i).Name() == name)
      return i;
  return -1;
}

const char * const layerNames[] = {
  "alpha", "beta", "gamma", "delta", "epsilon", "zeta", "eta",
  "theta", "iota", "kappa", "lambda", "mu", "nu", "xi",
  "omicron", "pi", "rho", "sigma", "tau", "phi", "chi", "xi",
  "omega" };

//! Create a new layer with unique name.
/*! The layer is inserted at index \a index, or appended if
  \a index is negative.
  Returns index of new layer. */
int IpePage::NewLayer(int index)
{
  iEdited = true;
  for (int i = 0; i < int(sizeof(layerNames)/sizeof(const char *)); ++i) {
    if (FindLayer(layerNames[i]) < 0)
      return AddLayer(IpeLayer(layerNames[i]), index);
  }
  char name[20];
  int i = 1;
  for (;;) {
    std::sprintf(name, "alpha%d", i);
    if (FindLayer(name) < 0)
      return AddLayer(IpeLayer(name), index);
    i++;
  }
}

//! Deletes an empty layer from the page.
/*! All objects are adjusted.  Panics if there are objects in the
  deleted layer, of if it is the only layer.
  The layer is also removed from all views.
*/
void IpePage::DeleteLayer(int index)
{
  assert(iLayers.size() > 1);
  for (iterator it = begin(); it != end(); ++it) {
    int k = it->Layer();
    assert(k != index);
    if (k > index)
      it->SetLayer(k-1);
  }

  // remove from all views
  IpeString name = iLayers[index].Name();
  for (int i = 0; i < CountViews(); ++i)
    iViews[i].DeleteLayer(name);

  iLayers.erase(iLayers.begin() + index);
  iEdited = true;
}

//! Does a view exist where this layer is active?
bool IpePage::IsLayerActiveInView(int index) const
{
  IpeString name = iLayers[index].Name();
  for (int i = 0; i < CountViews(); ++i)
    if (iViews[i].Active() == name)
      return true;
  return false;
}

//! Set the views for the page.
/*! This sets the edited flag. */
void IpePage::SetViews(const IpeViewSeq &views)
{
  iViews = views;
  iEdited = true;
}

#if 0
//! Sets one view of the page.
/*! This sets the edited flag. */
void IpePage::SetView(int index, const IpeView &view)
{
  assert(0 <= index && index < int(iViews.size()));
  iViews[index] = view;
  iEdited = true;
}
#endif

//! Add a view at position \a index.
/*! The view is appended at the end if \a index is negative. */
void IpePage::AddView(const IpeView &view, int index)
{
  if (index < 0)
    iViews.push_back(view);
  else
    iViews.insert(iViews.begin() + index, view);
  iEdited = true;
}

//! Delete view at \a index.
void IpePage::DeleteView(int index)
{
  iViews.erase(iViews.begin() + index);
  iEdited = true;
}

class TextBoxVisitor : public IpeVisitor {
public:
  virtual void VisitText(const IpeText *obj);
public:
  IpeRect iR;
  IpeScalar iY;
};

void TextBoxVisitor::VisitText(const IpeText *t)
{
  // look at all minipage objects that span the same horizontal extent
  if (t->Type() == IpeText::ETextbox) {
    IpeScalar bottom = (t->Matrix() * t->Position()).iY - t->TotalHeight();
    if (bottom < iY)
      iY = bottom;
  }
}

//! Computes text box.
/*! Takes into account media size, margins, and text objects already
  on the page .*/
IpeRect IpePage::TextBox(const IpeRect &media,
			 const IpeStyleSheet *sheet) const
{
  IpeVector tl, br;
  sheet->FindMargins(tl, br);
  TextBoxVisitor vis;
  vis.iR = IpeRect(media.Min() + IpeVector(tl.iX, br.iY),
		   media.Max() - IpeVector(br.iX, tl.iY));
  vis.iY = vis.iR.Max().iY;
  for (const_iterator it = begin(); it != end(); ++it)
    vis(*it);
  return IpeRect(vis.iR.Min(), IpeVector(vis.iR.Max().iX, vis.iY));
}

//! Set whether page has been edited.
void IpePage::SetEdited(bool edited)
{
  iEdited = edited;
}

//! Set the section title at \a level.
/*! Level 0 is the section, level 1 the subsection.
  This marks the page as edited. */
void IpePage::SetSection(int level, IpeString name)
{
  iSection[level] = name;
  iEdited = true;
}

//! Returns true iff any object on the page is selected.
bool IpePage::HasSelection() const
{
  for (const_iterator it = begin(); it != end(); ++it) {
    if (it->Select() != IpePgObject::ENone)
      return true;
  }
  return false;
}

//! Returns the primary selection, or end().
IpePage::iterator IpePage::PrimarySelection()
{
  for (iterator it = begin(); it != end(); ++it) {
    if (it->Select() == IpePgObject::EPrimary)
      return it;
  }
  return end();
}

//! Deselect all objects.
void IpePage::DeselectAll()
{
  for (iterator it = begin(); it != end(); ++it)
    it->SetSelect(IpePgObject::ENone);
}

//! Deselect all objects in this layer.
void IpePage::DeselectLayer(int layer)
{
  for (iterator it = begin(); it != end(); ++it)
    if (it->Layer() == layer)
      it->SetSelect(IpePgObject::ENone);
}

//! Deselect all objects not in a layer of this view, or in a locked layer.
void IpePage::DeselectNotInView(int view)
{
  std::vector<bool> layer;
  MakeLayerTable(layer, view, true);
  for (iterator it = begin(); it != end(); ++it)
    if (!layer[it->Layer()])
      it->SetSelect(IpePgObject::ENone);
}

/*! If no object is the primary selection, make the topmost secondary
  selection the primary one. */
void IpePage::EnsurePrimarySelection()
{
  for (IpePage::const_iterator it = begin(); it != end(); ++it) {
    if (it->Select() == IpePgObject::EPrimary)
      return;
  }
  IpePage::reverse_iterator it = rbegin();
  while (it != rend() && it->Select() != IpePgObject::ESecondary)
    ++it;
  if (it != rend())
    it->SetSelect(IpePgObject::EPrimary);
}

//! Removes all selected objects from the page.
/*! They are added, in the same order, to \a seq. */
void IpePage::ExtractSelection(IpePgObjectSeq &seq)
{
  iterator it1;
  for (iterator it = begin(); it != end(); it = it1) {
    it1 = it;
    ++it1;
    if (it->Select() != IpePgObject::ENone) {
      seq.push_back(*it);
      erase(it);
      iEdited = true;
    }
  }
}

//! Delete currently selected objects
void IpePage::Delete()
{
  iterator it1;
  for (iterator it = begin(); it != end(); it = it1) {
    it1 = it;
    ++it1;
    if (it->Select() != IpePgObject::ENone) {
      erase(it);
      iEdited = true;
    }
  }
}

//! Select all objects visible and not locked in the view.
void IpePage::SelectAll(int view)
{
  std::vector<bool> layerVisible;
  MakeLayerTable(layerVisible, view, true);
  for (iterator it = begin(); it != end(); ++it) {
    if (layerVisible[it->Layer()] && !it->Select())
      it->SetSelect(IpePgObject::ESecondary);
  }
  EnsurePrimarySelection();
}

//! Select all objects in the given layer.
void IpePage::SelectAllInLayer(int layer)
{
  DeselectAll();
  IpePgObject::TSelect sel = IpePgObject::EPrimary;
  for (iterator it = begin(); it != end(); ++it) {
    if (it->Layer() == layer) {
      it->SetSelect(sel);
      sel = IpePgObject::ESecondary;
    }
  }
}

//! Group the selected objects together as a new group object.
/*! The new object is placed in \a layer. */
void IpePage::Group(int layer)
{
  IpePgObjectSeq objs;
  ExtractSelection(objs);
  IpeGroup *group = new IpeGroup;
  for (IpePgObjectSeq::iterator it = objs.begin(); it != objs.end(); ++it)
    group->push_back(it->Object()->Clone());
  push_back(IpePgObject(IpePgObject::EPrimary, layer, group));
  iEdited = true;
}

//! Move selected objects to indicated layer.
void IpePage::MoveToLayer(int layer)
{
  for (iterator it = begin(); it != end(); ++it) {
    if (it->Select())
      it->SetLayer(layer);
  }
  iEdited = true;
}

//! Ungroup the primary selection, place objects in \a layer.
/*! Panics if no primary selection, returns false if primary selection
  is not a group. */
bool IpePage::Ungroup(int layer)
{
  iterator it = PrimarySelection();
  const IpeGroup *group = it->Object()->AsGroup();
  if (!group)
    return false;
  for (IpeGroup::const_iterator it1 = group->begin();
       it1 != group->end(); ++it1) {
    IpeObject *obj = (*it1)->Clone();
    // apply attributes from group
    if (group->Stroke())
      obj->SetStroke(group->Stroke());
    obj->SetMatrix(group->Matrix() * obj->Matrix());
    IpeFillable *fobj = obj->AsFillable();
    if (fobj) {
      if (group->Fill())
	fobj->SetFill(group->Fill());
      if (group->LineWidth())
	fobj->SetLineWidth(group->LineWidth());
      if (group->DashStyle())
	fobj->SetDashStyle(group->DashStyle());
    }
    if (group->TextSize()) {
      if (obj->AsText())
	obj->AsText()->SetSize(group->TextSize());
      else if (obj->AsGroup())
	obj->AsGroup()->SetTextSize(group->TextSize());
    }
    if (group->MarkSize()) {
      if (obj->AsMark())
	obj->AsMark()->SetSize(group->MarkSize());
      else if (obj->AsGroup())
	obj->AsGroup()->SetMarkSize(group->MarkSize());
    }
    if (group->MarkShape()) {
      if (obj->AsMark())
	obj->AsMark()->SetShape(group->MarkShape());
      else if (obj->AsGroup())
	obj->AsGroup()->SetMarkShape(group->MarkShape());
    }
    push_back(IpePgObject(IpePgObject::ESecondary, layer, obj));
  }
  erase(it);
  iEdited = true;
  EnsurePrimarySelection();
  return true;
}

//! Move selected objects to front.
void IpePage::Front()
{
  IpePgObjectSeq objs;
  ExtractSelection(objs);
  for (IpePgObjectSeq::iterator it = objs.begin();
       it != objs.end(); ++it) {
    push_back(*it);
    iEdited = true;
  }
}

//! Move selected objects to back.
void IpePage::Back()
{
  IpePgObjectSeq objs;
  ExtractSelection(objs);
  iterator it1 = begin();
  if (Layer(0).Name() == "background") {
    // skip all objects in this layer
    while (it1 != end() && it1->Layer() == 0)
      ++it1;
  }
  for (IpePgObjectSeq::reverse_iterator it = objs.rbegin();
       it != objs.rend(); ++it) {
    insert(it1, *it);
    --it1;
    iEdited = true;
  }
}

//! Duplicate the selected objects into \a layer.
void IpePage::Duplicate(int layer)
{
  // remember number of elements on page
  iterator it = begin();
  int n = size();
  while (n-- > 0) {
    if (it->Select()) {
      push_back(IpePgObject(IpePgObject::ESecondary, layer,
			    it->Object()->Clone()));
      it->SetSelect(IpePgObject::ENone);  // unselect old
    }
    ++it;
    iEdited = true;
  }
  EnsurePrimarySelection();
}

//! Set stroke color of selected objects.
void IpePage::SetStroke(IpeAttribute color)
{
  for (iterator it = begin(); it != end(); ++it) {
    if (it->Select() != IpePgObject::ENone) {
      it->Object()->SetStroke(color);
      iEdited = true;
    }
  }
}

//! Set fill color of selected objects.
void IpePage::SetFill(IpeAttribute color)
{
  for (iterator it = begin(); it != end(); ++it) {
    if (it->Select() != IpePgObject::ENone) {
      IpeFillable *obj = it->Object()->AsFillable();
      if (obj) {
	obj->SetFill(color);
	iEdited = true;
      }
    }
  }
}

//! Set line width of selected objects.
void IpePage::SetLineWidth(IpeAttribute attr)
{
  for (iterator it = begin(); it != end(); ++it) {
    if (it->Select() != IpePgObject::ENone) {
      IpeFillable *obj = it->Object()->AsFillable();
      if (obj) {
	obj->SetLineWidth(attr);
	iEdited = true;
      }
    }
  }
}

//! Set line style of selected objects.
void IpePage::SetDashStyle(IpeAttribute attr)
{
  for (iterator it = begin(); it != end(); ++it) {
    if (it->Select() != IpePgObject::ENone) {
      IpeFillable *obj = it->Object()->AsFillable();
      if (obj) {
	obj->SetDashStyle(attr);
	iEdited = true;
      }
    }
  }
}

//! Set arrows of selected objects.
void IpePage::SetArrows(bool forward, bool backward, IpeAttribute size)
{
  for (iterator it = begin(); it != end(); ++it) {
    if (it->Select() != IpePgObject::ENone) {
      IpePath *path = it->Object()->AsPath();
      if (path) {
	if (forward)
	  path->SetForwardArrow(size);
	else
	  path->SetForwardArrow(IpeAttribute());
	if (backward)
	  path->SetBackwardArrow(size);
	else
	  path->SetBackwardArrow(IpeAttribute());
	iEdited = true;
      }
    }
  }
}

//! Set arrow size of selected objects.
void IpePage::SetArrowSize(IpeAttribute size)
{
  for (iterator it = begin(); it != end(); ++it) {
    if (it->Select() != IpePgObject::ENone) {
      IpePath *path = it->Object()->AsPath();
      if (path) {
	if (!path->ForwardArrow().IsNull())
	  path->SetForwardArrow(size);
	if (!path->BackwardArrow().IsNull())
	  path->SetBackwardArrow(size);
	iEdited = true;
      }
    }
  }
}

//! Set text size of selected objects.
void IpePage::SetTextSize(IpeAttribute size)
{
  for (iterator it = begin(); it != end(); ++it) {
    if (it->Select() != IpePgObject::ENone) {
      IpeText *obj = it->Object()->AsText();
      if (obj) {
	obj->SetSize(size);
	it->InvalidateBBox();
	obj->SetXForm(0); // XForm is no longer useful
	iEdited = true;
      } else {
	IpeGroup *grp = it->Object()->AsGroup();
	if (grp) {
	  grp->SetTextSize(size);
	  iEdited = true;
	}
      }
    }
  }
}

//! Set mark size of selected objects.
void IpePage::SetMarkSize(IpeAttribute size)
{
  for (iterator it = begin(); it != end(); ++it) {
    if (it->Select() != IpePgObject::ENone) {
      IpeMark *obj = it->Object()->AsMark();
      if (obj) {
	obj->SetSize(size);
	iEdited = true;
      } else {
	IpeGroup *grp = it->Object()->AsGroup();
	if (grp) {
	  grp->SetMarkSize(size);
	  iEdited = true;
	}
      }
    }
  }
}

//! Set mark shape of selected objects.
void IpePage::SetMarkShape(int shape)
{
  for (iterator it = begin(); it != end(); ++it) {
    if (it->Select() != IpePgObject::ENone) {
      IpeMark *obj = it->Object()->AsMark();
      if (obj) {
	obj->SetShape(shape);
	iEdited = true;
      } else {
	IpeGroup *grp = it->Object()->AsGroup();
	if (grp) {
	  grp->SetMarkShape(shape);
	  iEdited = true;
	}
      }
    }
  }
}

//! Copy selected objects into the stream.
void IpePage::Copy(IpeStream &stream, const IpeStyleSheet *sheet) const
{
  IpeBitmapFinder bmFinder;
  for (const_iterator it = begin(); it != end(); ++it) {
    if (it->Select() != IpePgObject::ENone)
      it->Object()->Accept(bmFinder);
  }
  stream << "<ipeselection>\n";
  int id = 1;
  for (std::vector<IpeBitmap>::const_iterator it = bmFinder.iBitmaps.begin();
       it != bmFinder.iBitmaps.end(); ++it) {
    it->SaveAsXml(stream, id);
    it->SetObjNum(id);
    ++id;
  }
  IpePainter painter(sheet);
  for (const_iterator it = begin(); it != end(); ++it) {
    if (it->Select() != IpePgObject::ENone) {
      it->Object()->SaveAsXml(painter, stream, IpeString());
    }
  }
  stream << "</ipeselection>\n";
}

//! Copy whole page into the stream.
void IpePage::CopyPage(IpeStream &stream, const IpeStyleSheet *sheet) const
{
  IpeBitmapFinder bmFinder;
  bmFinder.ScanPage(this);
  stream << "<ipepage>\n";
  int id = 1;
  for (std::vector<IpeBitmap>::const_iterator it = bmFinder.iBitmaps.begin();
       it != bmFinder.iBitmaps.end(); ++it) {
    IpeBitmap bm = *it;
    bm.SaveAsXml(stream, id);
    bm.SetObjNum(id);
    ++id;
  }
  IpePainter painter(sheet);
  SaveAsXml(painter, stream);
  stream << "</ipepage>\n";
}

//! Paste objects from XML source into \a layer.
/*! Returns false if XML source cannot be parsed. */
bool IpePage::Paste(int layer, IpeDataSource &source,
		    IpeRepository *rep)
{
  IpeImlParser parser(source, rep);
  IpePgObjectSeq seq;
  if (!parser.ParseSelection(seq) || seq.empty())
    return false;

  DeselectAll();
  IpePgObject::TSelect sel = IpePgObject::EPrimary;
  for (IpePgObjectSeq::const_iterator it = seq.begin();
       it != seq.end(); ++it) {
    push_back(IpePgObject(sel, layer, it->Object()->Clone()));
    sel = IpePgObject::ESecondary;
  }
  iEdited = true;
  return true;
}

//! Make a table of layers, indicating whether visible (and unlocked).
/*! Must be called with an empty \a layers vector. */
void IpePage::MakeLayerTable(std::vector<bool> &layers, int view,
			     bool excludeLocked) const
{
  for (int i = 0; i < CountLayers(); ++i) {
    bool inView = iViews[view].HasLayer(Layer(i).Name());
    if (excludeLocked && Layer(i).IsLocked())
      inView = false;
    layers.push_back(inView);
  }
}

//! If no selected object is close, select closest object.
/*! If there is a selected object at distance at most \a d from \a
  pos, return true.  Otherwise, check whether the closest object to \a
  pos has distance at most \a d.  If so, unselect everything, make
  this object the primary selection, and return true.  If not, return
  whether the page has a selection at all.

  If \a primaryOnly is \c true, the primary selection has to be at
  distance at most \a d, otherwise it'll be replaced as above.
*/
bool IpePage::UpdateCloseSelection(const IpeVector &pos, double d,
				   bool primaryOnly, int view)
{
  // check whether a selected object is close enough
  if (primaryOnly) {
    iterator it = PrimarySelection();
    if (it != end() && it->Distance(pos, d) < d)
      return true;
  } else {
    for (iterator it = begin(); it != end(); ++it) {
      if (it->Select() != IpePgObject::ENone &&
	  it->Distance(pos, d) < d)
	return true;
    }
  }

  // no --- find closest object
  std::vector<bool> layer;
  MakeLayerTable(layer, view, true);
#ifdef __BORLANDC__
  double d1;       // Borland C++ complains if initialized
#else
  double d1 = 0.0; // Some G++ versions complain if not!  Argh!
#endif
  iterator it1 = end();
  for (iterator it = begin(); it != end(); ++it) {
    if (layer[it->Layer()] && (d1 = it->Distance(pos, d)) < d) {
      d = d1;
      it1 = it;
    }
  }

  // closest object close enough?
  if (it1 != end()) {
    // deselect all, and select it
    for (iterator it = begin(); it != end(); ++it)
      it->SetSelect(IpePgObject::ENone);
    it1->SetSelect(IpePgObject::EPrimary);
    return true;
  }
  return HasSelection();
}

// --------------------------------------------------------------------

//! Create one path object with all the subpaths from the selection.
/*! The new object takes the attributes from the primary selection,
  and is placed in \a layer.
  The function returns false if non-path objects are selected. */
bool IpePage::ComposePaths(int layer)
{
  for (iterator it = begin(); it != end(); ++it) {
    if (it->Select() && !it->Object()->AsPath())
      return false;
  }
  IpePath *prim = PrimarySelection()->Object()->AsPath();
  IpeAllAttributes attr;
  attr.iStroke = prim->Stroke();
  attr.iFill = prim->Fill();
  attr.iLineWidth = prim->LineWidth();
  attr.iDashStyle = prim->DashStyle();
  attr.iForwardArrow = false;
  attr.iBackwardArrow = false;
  IpePath *obj = new IpePath(attr);

  iterator it1;
  for (iterator it = begin(); it != end(); it = it1) {
    it1 = it;
    ++it1;
    if (it->Select()) {
      IpePath *p = it->Object()->AsPath();
      for (int i = 0; i < p->NumSubPaths(); ++i)
	obj->AddSubPath(p->SubPath(i)->Transform(p->Matrix()));
      erase(it);
    }
  }
  push_back(IpePgObject(IpePgObject::EPrimary, layer, obj));
  iEdited = true;
  return true;
}

//! Decompose one path object into separate objects for the subpaths.
/*! The new objects are placed in \a layer.
  The function returns false if the primary selection is not a path object. */
bool IpePage::DecomposePath(int layer)
{
  IpePath *prim = PrimarySelection()->Object()->AsPath();
  if (!prim)
    return false;
  IpeAllAttributes attr;
  attr.iStroke = prim->Stroke();
  attr.iFill = prim->Fill();
  attr.iLineWidth = prim->LineWidth();
  attr.iDashStyle = prim->DashStyle();
  attr.iForwardArrow = false;
  attr.iBackwardArrow = false;
  IpeMatrix tfm = prim->Matrix();

  for (int i = 0; i < prim->NumSubPaths(); ++i) {
    IpeSubPath *sp = prim->SubPath(i)->Clone();
    IpePath *obj = new IpePath(attr);
    obj->SetMatrix(tfm);
    obj->AddSubPath(sp);
    push_back(IpePgObject(IpePgObject::ESecondary, layer, obj));
  }
  erase(PrimarySelection());
  EnsurePrimarySelection();
  iEdited = true;
  return true;
}

// --------------------------------------------------------------------

const double joinThreshold = 0.000001;

struct SSubPath {
  IpeVector iV[2];
  const IpeSegmentSubPath *iPath;  // not owned
  bool iFlip;
};

static int FindPartner(const IpeVector &v, SSubPath *subs, int beg, int end)
{
  while (beg != end) {
    if ((v - subs[beg].iV[0]).SqLen() < joinThreshold)
      return beg;
    if ((v - subs[beg].iV[1]).SqLen() < joinThreshold) {
      IpeVector t = subs[beg].iV[0];
      subs[beg].iV[0] = subs[beg].iV[1];
      subs[beg].iV[1] = t;
      subs[beg].iFlip = true;
      return beg;
    }
    ++beg;
  }
  return -1;
}

IpePath *IpePage::DoJoinPaths(IpePath *prim, SSubPath *subs, int size)
{
  bool closePath =
    ((subs[0].iV[0] - subs[size-1].iV[1]).SqLen() < joinThreshold);

  IpeAllAttributes attr;
  attr.iStroke = prim->Stroke();
  attr.iFill = prim->Fill();
  attr.iLineWidth = prim->LineWidth();
  attr.iDashStyle = prim->DashStyle();
  attr.iForwardArrow = false;
  attr.iBackwardArrow = false;

  IpeSegmentSubPath *sp = new IpeSegmentSubPath;

  for (int i = 0; i < size; ++i) {
    const IpeSegmentSubPath *p = subs[i].iPath;
    if (subs[i].iFlip) {
      for (int j = p->NumSegments() - 1; j >= 0; --j)
	sp->AppendReversed(p->Segment(j));
    } else {
      for (int j = 0; j < p->NumSegments(); ++j)
	sp->Append(p->Segment(j));
    }
  }

  IpePath *obj = new IpePath(attr);
  sp->SetClosed(closePath);
  obj->AddSubPath(sp);
  return obj;
}

//! Join paths into one long path.
/*! Create one path object with the open subpaths from the selection
  joined into one long subpath.
  The new object takes the attributes from the primary selection,
  and is placed in \a layer.
  The function returns \c false if objects are selected that do not consist
  of open subpaths only.
*/
bool IpePage::JoinPaths(int layer)
{
  std::vector<SSubPath> subs;
  for (iterator it = begin(); it != end(); ++it) {
    if (it->Select()) {
      IpePath *p = it->Object()->AsPath();
      // must be a single open subpath
      if (!p || p->NumSubPaths() > 1 || p->SubPath(0)->Closed())
	return false;
      const IpeSegmentSubPath *sp = p->SubPath(0)->AsSegs();
      SSubPath sub;
      sub.iPath = sp;
      sub.iV[0] = sp->Segment(0).CP(0);
      IpePathSegment back = sp->Segment(-1);
      sub.iV[1] = back.CP(back.NumCP() - 1);
      sub.iFlip = false;
      subs.push_back(sub);
    }
  }
  // match up endpoints
  if (FindPartner(subs[0].iV[1], &subs[0], 1, subs.size()) < 0) {
    // not found, flip subs[0] and try again
    IpeVector t = subs[0].iV[0];
    subs[0].iV[0] = subs[0].iV[1];
    subs[0].iV[1] = t;
    subs[0].iFlip = true;
  }
  for (int i = 0; i < int(subs.size()) - 2; ++i) {
    // invariant: subs[0] .. subs[i] form a chain
    int j = FindPartner(subs[i].iV[1], &subs[0], i+1, subs.size());
    if (j < 0)
      return false; // no match
    // flip i+1 and j
    if (j != i+1) {
      SSubPath sub = subs[i+1];
      subs[i+1] = subs[j];
      subs[j] = sub;
    }
  }
  IpePath *prim = PrimarySelection()->Object()->AsPath();
  IpePath *obj = DoJoinPaths(prim, &subs[0], subs.size());
  // erase the selected objects
  Delete();
  push_back(IpePgObject(IpePgObject::EPrimary, layer, obj));
  iEdited = true;
  return true;
}

// --------------------------------------------------------------------
