// --------------------------------------------------------------------
// AppUi
// --------------------------------------------------------------------
/*

    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 "ipedoc.h"
#include "ipepage.h"
#include "ipestyle.h"
#include "ipeiml.h"
#include "ipepath.h"
#include "ipeutils.h"

#include "ipemodel.h"
#include "ipeversion.h"
#include "ipeq.h"

#include "appui.h"
#include "dialogs.h"
#include "props.h"
#include "styles.h"

#include "ipeoverlay.h"
#include "ipecreatetext.h"
#include "ipeeditpath.h"

#include <qaccel.h>
#include <qaction.h>
#include <qapplication.h>
#include <qclipboard.h>
#include <qcolordialog.h>
#include <qcombobox.h>
#include <qdatetime.h>
#include <qdir.h>
#include <qinputdialog.h>
#include <qimage.h>
#include <qlabel.h>
#include <qlistbox.h>
// #include <qlistview.h>
#include <qmenubar.h>
#include <qmessagebox.h>
#include <qpushbutton.h>
#include <qstatusbar.h>
#include <qtoolbar.h>
#include <qtoolbutton.h>
#include <qwidgetstack.h>

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

const char * aboutText =
"<qt><h1>%1</h1>"
"<p>(c) 1993-2004 Otfried Cheong</p>"
"<p>The extensible drawing editor Ipe creates figures "
"in Postscript and PDF format, "
"using LaTeX to format the text in the figures.</p>"
"<p>Ipe relies on the following fine pieces of software:"
"<ul>"
"<li> <b>PdfLaTeX</b> (www.pdftex.org)"
"<li> Some code from <b>Xpdf</b> (www.foolabs.com/xpdf)"
"<li> The GUI toolkit <b>Qt %3</b> (www.trolltech.com)"
"<li> The font rendering library <b>freetype 2</b> (www.freetype.org)"
"<li> The compression library <b>zlib</b> (www.gzip.org/zlib)"
"</ul>"
"<p>%2 is released under the GNU Public License.</p>"
"<p>See <tt>http://ipe.compgeom.org</tt> for details.</p>"
"</qt>";

QPixmap penguinIcon(int width);
QPixmap ipeIcon(const char* name);

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

class LayerBoxItem : public QListBoxText {
public:
  LayerBoxItem(const QString &text, bool emph)
    : QListBoxText(text), iEmph(emph) { /* nothing */ }
protected:
  void  paint( QPainter * );
private:
  bool iEmph;
};

void  LayerBoxItem::paint(QPainter *p)
{
  if (iEmph)
    p->setPen(Qt::red);
  QListBoxText::paint(p);
}

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

#if 0
class BookmarkItem : public QListViewItem {
public:
  BookmarkItem(QListViewItem *parent, QListViewItem *after, IpeString text,
	       int page, IpeSpinBox *sp)
    : QListViewItem(parent, after, QIpe(text)), iPage(page), iSpinBox(sp) {}
  BookmarkItem(QListView *parent, QListViewItem *after, IpeString text,
	       int page, IpeSpinBox *sp)
    : QListViewItem(parent, after, QIpe(text)), iPage(page), iSpinBox(sp) {}
  BookmarkItem(QListViewItem *parent, IpeString text,
	       int page, IpeSpinBox *sp)
    : QListViewItem(parent, QIpe(text)), iPage(page), iSpinBox(sp) {}
  BookmarkItem(QListView *parent, IpeString text,
	       int page, IpeSpinBox *sp)
    : QListViewItem(parent, QIpe(text)), iPage(page), iSpinBox(sp) {}
protected:
  virtual void activate();
private:
  int iPage;
  IpeSpinBox *iSpinBox;
};

void BookmarkItem::activate()
{
  iSpinBox->setValue(iPage + 1);
}
#endif

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

//! All windows closed, terminate Ipe.
void AppUi::closeEvent(QCloseEvent* ce)
{
  IpePreferences::Static()->Save();

  if (!iDoc->IsEdited()) {
    ce->accept();
    return;
  }

  switch (QMessageBox::information
	  (this, "Ipe",
	   tr("The document has been changed since the last save."),
	   tr("Save now"), tr("Cancel"), tr("Leave Anyway"), 0, 1)) {
  case 0:
    if (Save())
      ce->accept();
    else
      ce->ignore();
    break;
  case 1:
  default: // just for sanity
    ce->ignore(); break;
  case 2:
    ce->accept(); break;
  }
}

bool AppUi::eventFilter(QObject *obj, QEvent *e)
{
  if (e->type() != QEvent::KeyPress)
    return QMainWindow::eventFilter(obj, e);  // dispatch normally

  QKeyEvent *ev = (QKeyEvent *) e;
  ipeDebug("Key event (%x, %x)", ev->key(), ev->state());

  IpeOverlay *ov = iCanvas->Overlay();
  int key = ev->key();

  if (ov && key == Key_Escape)
    iCanvas->FinishOverlay();

  // a few keys are permitted to be directed to various widgets
  if (('0' <= key && key <= '9') ||
      key == Key_Escape || key == Key_Enter || key == Key_Backspace ||
      key == Key_Period || key == Key_Return || key == Key_Tab ||
      key == Key_Backtab || key == Key_Left || key == Key_Right ||
      key == Key_Up || key == Key_Down)
    // dispatch normally
    return QMainWindow::eventFilter(obj, e);

  // all other keys are either handled by the overlay
  // or are accelerators
  if (ov)
    ov->KeyPress(ev);

  QKeyEvent a(QEvent::Accel, key, ev->ascii(), ev->state(),
	      ev->text());
  a.ignore();
  // Enable/disable accelerators in Edit menu
  AboutToShowEditMenu();
  QApplication::sendEvent(this, &a);
  return true;
}

void AppUi::AboutToShowEditMenu()
{
  bool objs = Page()->HasSelection();
  // bool istext = false;
  bool isgroup = false;
  IpePage::iterator it = Page()->PrimarySelection();
  if (it != Page()->end()) {
    // istext = (it->Object()->AsText() != 0);
    isgroup = (it->Object()->AsGroup() != 0);
  }
  // TODO: move this functionality into Ipelib
  bool pathObjs = true;
  bool singlePathObjs = true;
  int objCount = 0;
  for (IpePage::iterator it = Page()->begin(); it != Page()->end(); ++it) {
    if (it->Select()) {
      ++objCount;
      IpePath *p = it->Object()->AsPath();
      // must be a single open subpath
      if (!p) {
	pathObjs = false;
	singlePathObjs = false;
      } else if (p->NumSubPaths() > 1 || p->SubPath(0)->Closed())
	singlePathObjs = false;
    }
  }
  iEditMenu->setItemEnabled(ECmdCut, objs);
  iEditMenu->setItemEnabled(ECmdCopy, objs);
  iEditMenu->setItemEnabled(ECmdDelete, objs);
  iEditMenu->setItemEnabled(ECmdGroup, objs);
  iEditMenu->setItemEnabled(ECmdFront, objs);
  iEditMenu->setItemEnabled(ECmdBack, objs);
  iEditMenu->setItemEnabled(ECmdDuplicate, objs);
  // iEditMenu->setItemEnabled(ECmdEdit, istext);
  iEditMenu->setItemEnabled(ECmdUngroup, isgroup);
  iEditMenu->setItemEnabled(ECmdComposePaths,
			    objs && pathObjs && objCount > 1);
  iEditMenu->setItemEnabled(ECmdJoinPaths,
			    objs && singlePathObjs && objCount > 1);

  if (iUndo.CanUndo()) {
    iEditMenu->setItemEnabled(ECmdUndo, true);
    iEditMenu->changeItem(ECmdUndo, tr("Undo %1").arg(QIpe(iUndo.UndoText())));
  } else {
    iEditMenu->setItemEnabled(ECmdUndo, false);
    iEditMenu->changeItem(ECmdUndo, tr("Undo"));
  }

  if (iUndo.CanRedo()) {
    iEditMenu->setItemEnabled(ECmdRedo, true);
    iEditMenu->changeItem(ECmdRedo, tr("Redo %1").arg(QIpe(iUndo.RedoText())));
  } else {
    iEditMenu->setItemEnabled(ECmdRedo, false);
    iEditMenu->changeItem(ECmdRedo, tr("Redo"));
  }
}

// --------------------------------------------------------------------
// Public slots
// --------------------------------------------------------------------

//! This handles all Ipe commands that operate on an IpePage.
void AppUi::Cmd(int cmd)
{
  // no document loaded yet, ignore UI events
  if (!iDoc)
    return;

  statusBar()->clear();
  IpePage *page = Page();
  IpeLayer layer = page->Layer(iLayer);

  // check whether command requires a select object
  switch (cmd) {
  case ECmdGroup:
  case ECmdUngroup:
  case ECmdCut:
  case ECmdCopy:
  case ECmdFront:
  case ECmdBack:
  case ECmdDuplicate:
  case ECmdDelete:
  case ECmdComposePaths:
  case ECmdJoinPaths:
  case ECmdDecomposePath:
  case ECmdMoveToLayer:
    if (!page->HasSelection()) {
      statusBar()->message(tr("No selected object"));
      return;
    }
  default:
    break;
  }

  IpeAutoPtr<IpePage> originalPage(new IpePage(*page));
  page->SetEdited(false);
  QString undo;
  bool doUndo = false;

  switch (cmd) {
  case ECmdSelectAll:
    page->SelectAll(iVno);
    undo = tr("select all");
    // doUndo = true;
    break;
  case ECmdSelectLayer:
    page->SelectAllInLayer(iLayer);
    undo = tr("select all in layer");
    // doUndo = true;
    break;
  case ECmdFirstView:
    iViewNumber->setValue(1);
    break;
  case ECmdLastView:
    iViewNumber->setValue(page->CountViews());
    break;
  case ECmdNewView:
  case ECmdNewLayerNewView:
    {
      IpeView view;
      undo = tr("view creation");
      if (cmd == ECmdNewLayerNewView) {
	undo = tr("layer and view creation");
	iLayer = page->NewLayer(iLayer + 1);
	for (int i = 0; i <= iLayer; ++i)
	  view.AddLayer(page->Layer(i).Name());
      }
      view.SetActive(page->Layer(iLayer).Name());
      page->AddView(view, ++iVno);
      ViewChanged();
      break;
    }
  case ECmdDeleteView:
    if (page->CountViews() > 1) {
      page->DeleteView(iVno);
      if (iVno > 0 && iVno >= page->CountViews())
	--iVno;
      undo = tr("view deletion");
      ViewChanged();
    }
    break;
  case ECmdMoveToLayer:
    page->MoveToLayer(iLayer);
    undo = tr("move to layer");
    break;
  case ECmdGroup:
    page->Group(iLayer);
    undo = tr("grouping");
    break;
  case ECmdUngroup:
    if (!page->Ungroup(iLayer))
      return;
    undo = tr("ungrouping");
    break;
  case ECmdCut:
  case ECmdCopy:
    {
      IpeString data;
      IpeStringStream stream(data);
      page->Copy(stream, iDoc->StyleSheet());
      if (cmd == ECmdCut)
	page->Delete();
      else
	statusBar()->message(tr("Copied"));
      QClipboard *cb = QApplication::clipboard();
      // cb->setData(new IpeDrag(data, this));
#if QT_VERSION >= 0x030000
      cb->setText(QIpe(data), QClipboard::Selection);
#else
      cb->setText(QIpe(data));
#endif
      undo = tr("cut");
      break;
    }
  case ECmdPaste:
    {
      QClipboard *cb = QApplication::clipboard();
#if QT_VERSION >= 0x030000
      QString data = cb->text(QClipboard::Selection);
#else
      QString data = cb->text();
#endif
      // ipeDebug("Paste: %s", data.latin1());
      if (data.length() == 0) {
#ifndef WIN32
	if (iPasteBitmapId > 0 && !cb->image().isNull()) {
	  // paste image
	  RunIpelet(iPasteBitmapId);
	} else
#endif
	  statusBar()->message(tr("Nothing to paste"));
      } else if (data.left(14) != "<ipeselection>") {
	IpeRect r = iDoc->Properties().iMedia;
	IpeVector pos = 0.5 * (r.Min() + r.Max());
	IpeText *obj = new IpeText(iAttributes, IpeQ(data), pos,
				   IpeText::ELabel);
	OvSvcAddObject(obj);
      } else {
	XmlQStringSource source(data);
	if (!page->Paste(iLayer, source, iDoc->Repository()))
	  ErrMsg(tr("Cannot parse Ipe objects in clipboard"));
      }
      undo = tr("paste");
      break;
    }
  case ECmdFront:
    page->Front();
    undo = tr("move to front");
    break;
  case ECmdBack:
    page->Back();
    undo = tr("move to back");
    break;
  case ECmdDuplicate:
    page->Duplicate(iLayer);
    undo = tr("duplication");
    statusBar()->message(tr("Object duplicated"));
    break;
  case ECmdDelete:
    page->Delete();
    undo = tr("deletion");
    break;
  case ECmdNewLayer:
    {
      iLayer = page->NewLayer(iLayer + 1);
      page->View(iVno).AddLayer(page->Layer(iLayer).Name());
      page->SetEdited(true);
      UpdateLayers();
    }
    undo = tr("adding layer");
    break;
  case ECmdRenameLayer:
    {
      bool ok = false;
      QString text = QInputDialog::getText
	(tr("Rename layer"),
	 tr("Enter new layer name"),
	 QLineEdit::Normal,
	 QIpe(layer.Name()), &ok, this);
      if (ok && !text.isEmpty()) {
	if (page->FindLayer(IpeQ(text)) >= 0) {
	  statusBar()->message(tr("Layer '%1' already exists").arg(text));
	  break;
	}
	page->Layer(iLayer).SetName(IpeQ(text));
	page->SetEdited(true);
	UpdateLayers();
      }
    }
    undo = tr("renaming layer");
    break;
  case ECmdLineWidth:
    page->SetLineWidth(iAttributes.iLineWidth);
    undo = tr("setting line width");
    break;
  case ECmdDashStyle:
    page->SetDashStyle(iAttributes.iDashStyle);
    undo = tr("setting dash style");
    break;
  case ECmdTextSize:
    page->SetTextSize(iAttributes.iTextSize);
    undo = tr("setting text size");
    break;
  case ECmdMarkShape:
    page->SetMarkShape(iAttributes.iMarkShape);
    undo = tr("setting mark shape");
    break;
  case ECmdMarkSize:
    page->SetMarkSize(iAttributes.iMarkSize);
    undo = tr("setting mark size");
    break;
  case ECmdSetArrows:
    page->SetArrows(iAttributes.iForwardArrow,
		    iAttributes.iBackwardArrow,
		    iAttributes.iArrowSize);
    undo = tr("setting arrows");
    break;
  case ECmdArrowSize:
    page->SetArrowSize(iAttributes.iArrowSize);
    undo = tr("setting arrow size");
    break;
  case ECmdStroke:
    page->SetStroke(iAttributes.iStroke);
    undo = tr("setting stroke");
    break;
  case ECmdFill:
    page->SetFill(iAttributes.iFill);
    undo = tr("setting fill");
    break;
  case ECmdComposePaths:
    if (!page->ComposePaths(iLayer)) {
      statusBar()->message(tr("Only path objects can be composed"));
      break;
    }
    undo = tr("path composition");
    break;
  case ECmdJoinPaths:
    if (!page->JoinPaths(iLayer)) {
      statusBar()->message(tr("Incorrect objects in selection"));
      break;
    }
    undo = tr("joining paths");
    break;
  case ECmdDecomposePath:
    if (!page->DecomposePath(iLayer)) {
      statusBar()->message(tr("Primary selection is not a path object"));
      break;
    }
    undo = tr("path decomposition");
    break;
  }
  bool isEdited = page->IsEdited();
  if (originalPage->IsEdited())
    page->SetEdited(true);
  if (doUndo || isEdited) {
    // page has been modified, save in Undo stack
    AddUndo(new IpeUndoPageEdit(iPno, originalPage.Take(), IpeQ(undo)));
  }
  iCanvas->Update();
  SetCaption();
}

void AppUi::AddUndo(IpeUndoItem *item)
{
  iUndo.Add(item);
}

void AppUi::UndoCmd(int cmd)
{
  // no document loaded yet, ignore UI events
  if (!iDoc)
    return;
  statusBar()->clear();
  int pno = -1;
  switch (cmd) {
  case ECmdUndo:
    if (iUndo.CanUndo())
      pno = iUndo.Undo(iDoc);
    break;
  case ECmdRedo:
    if (iUndo.CanRedo())
      pno = iUndo.Redo(iDoc);
    break;
  }
  if (pno >= 0)
    iPno = pno;
  if (iPno >= int(iDoc->size()))
    iPno = iDoc->size() - 1;
  iDoc->SetEdited(true);
  PageChanged();
}

void AppUi::SnapCmd(int id)
{
  // no document loaded yet, ignore UI events
  if (!iDoc)
    return;

  statusBar()->clear();

  // compute snapped mouse position WITHOUT angular snapping
  IpeVector pos = iCanvas->UnsnappedPos();
  iSnapData.SimpleSnap(pos, Page(), iSnapData.iSnapDistance / iCanvas->Zoom());

  switch (id) {
  case ECmdHere:
    iCanvas->SetPan(iCanvas->Pos());
    iCanvas->Update();
    return;
  case ECmdSetOriginSnap:
    iSnapData.iSnap |= IpeSnapData::ESnapAngle;
    iSnapAction[4]->setOn(true);
  case ECmdSetOrigin:
    iSnapData.iWithAxes = true;
    iSnapData.iOrigin = pos;
    iCanvas->SetSnap(iSnapData);
    iCanvas->Update();
    return;
  case ECmdResetOrigin:
    iSnapData.iWithAxes = false;
    iSnapData.iSnap &= ~IpeSnapData::ESnapAngle;
    iSnapAction[4]->setOn(false);
    iCanvas->SetSnap(iSnapData);
    iCanvas->Update();
    return;
  case ECmdSetDirectionSnap:
    iSnapData.iSnap |= IpeSnapData::ESnapAngle;
    iSnapAction[4]->setOn(true);
  case ECmdSetDirection:
    iSnapData.iWithAxes = true;
    iSnapData.iDir = (pos - iSnapData.iOrigin).Angle();
    iCanvas->SetSnap(iSnapData);
    iCanvas->Update();
    return;
  case ECmdSetLineSnap:
    iSnapData.iSnap |= IpeSnapData::ESnapAngle;
    iSnapAction[4]->setOn(true);
  case ECmdSetLine:
    if (!iSnapData.SetEdge(pos, Page())) {
      statusBar()->message(tr("Mouse position is not on an edge"));
    } else {
      iSnapData.iWithAxes = true;
      iCanvas->SetSnap(iSnapData);
      iCanvas->Update();
    }
    break;
  }
}

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

// Slots to connect to tool buttons

void AppUi::Cut()
{
  Cmd(ECmdCut);
}

void AppUi::Copy()
{
  Cmd(ECmdCopy);
}

void AppUi::Paste()
{
  Cmd(ECmdPaste);
}

void AppUi::EditObject()
{
  IpePage::iterator it = Page()->PrimarySelection();
  if (it == Page()->end()) {
    statusBar()->message(tr("No object selected"));
    return;
  }
  IpePgObject original(*it);
  if (it->Object()->AsText() &&
      IpeCreateText::Edit(it->Object()->AsText(), iDoc->StyleSheet())) {
    OvSvcAddUndoItem(it, Page(), original, tr("text object edit"));
    Page()->SetEdited(true);
    iCanvas->Update();
  } else if (it->Object()->AsPath()) {
    iCanvas->SetOverlay(new IpeEditPath(iCanvas, it->Object()->AsPath(),
					Page(), this, it));
  } else
    statusBar()->message(tr("No editable object selected"));
}

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

void AppUi::NewWindow()
{
  AppUi *appui = new AppUi;
  appui->NewDoc();
  if (IpePreferences::Static()->iMaximize)
    appui->showMaximized();
  else
    appui->show();
  appui->FitPage();
  statusBar()->clear();
}

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

void AppUi::NextView()
{
  if (iVno + 1 >= Page()->CountViews()) {
    if (iPno + 1 < int(iDoc->size())) {
      iPageNumber->stepUp();
    }
  } else
    iViewNumber->stepUp();
}

void AppUi::PreviousView()
{
  if (iVno == 0) {
    if (iPno > 0) {
      iPageNumber->stepDown();
      Cmd(ECmdLastView);
    }
  } else
    iViewNumber->stepDown();
}

void AppUi::FirstPage()
{
  iPageNumber->setValue(1);
  statusBar()->clear();
}

void AppUi::LastPage()
{
  iPageNumber->setValue(iDoc->size() - 1);
  statusBar()->clear();
}

static double AdjustPan(double cmin, double cmax,
			double omin, double omax,
			double pmin, double pmax)
{
  double dx = 0;

  // if objects stick out on both sides, there is nothing we can do
  if (omin <= cmin &&  omax >= cmax)
    return dx;

  if (omax > cmax && omin > cmin) {
    // we can see more objects if we shift canvas right
    dx = IpeMin(omin - cmin, omax - cmax);
  } else if (omin < cmin && omax < cmax) {
    // we can see more objects if we shift canvas left
    dx = -IpeMin(cmin - omin, cmax - omax);
  }

  // shift canvas
  cmin += dx;
  cmax += dx;

  // if canvas fully contained in media, done
  if (pmin <= cmin && pmax >= cmax)
    return dx;
  // if media contained in canvas, can't improve
  if (cmin < pmin && pmax < cmax)
    return dx;

  if (pmin > cmin) {
    // improvement possible by shifting canvas right
    if (omin > cmin)
      dx += IpeMin(omin - cmin, IpeMin(pmin - cmin, pmax - cmax));
  } else {
    // improvement possible by shifting canvas left
    if (omax < cmax)
      dx -= IpeMin(cmax - omax, IpeMin(cmax - pmax, cmin - pmin));
  }
  return dx;
}

//! Change resolution to 72 dpi and maximize interesting visible area.
/*! As suggested by Rene:

1) scale to the proper size, with the center of the canvas as the
   origin of the scaling.
2) If there is a horizontal and/or vertical translation that makes a
   larger part of the *bounding box* of the objects visible, then
   translate (and maximize the part of the bounding box that is
   visible).
3) If there is a horizontal and/or vertical translation that makes a
   larger part of the paper visible, then translate (and maximize the
   part of the paper that is visible), under the restriction that no
   part of the bounding box of the objects may be moved `out of sight'
   in this step. (Note that there may be objects outside the paper).
*/
void AppUi::NormalSize()
{
  iResolution->setValue(72000);

  IpeRect media = iDoc->Properties().iMedia;

  IpeRect bbox;
  for (IpePage::const_iterator it = Page()->begin();
       it != Page()->end(); ++it) {
    bbox.AddRect(it->BBox());
  }

  // size of canvas in user coordinates
  IpeVector s = (1.0 / iCanvas->Zoom()) *
    IpeVector(iCanvas->width(), iCanvas->height());

  IpeRect canvas(iCanvas->Pan() - 0.5 * s, iCanvas->Pan() + 0.5 * s);

  IpeVector pan;
  pan.iX = AdjustPan(canvas.Min().iX, canvas.Max().iX,
		     bbox.Min().iX, bbox.Max().iX,
		     media.Min().iX, media.Max().iX);
  pan.iY = AdjustPan(canvas.Min().iY, canvas.Max().iY,
		     bbox.Min().iY, bbox.Max().iY,
		     media.Min().iY, media.Max().iY);
  iCanvas->SetPan(iCanvas->Pan() + pan);
  statusBar()->clear();
}

void AppUi::FitBox(const IpeRect &box)
{
  if (box.IsEmpty())
    return;
  double xfactor = box.Width() > 0.0 ?
    (iCanvas->width() / box.Width()) : 20.0;
  double yfactor = box.Height() > 0.0 ?
    (iCanvas->height() / box.Height()) : 20.0;
  double zoom = (xfactor > yfactor) ? yfactor : xfactor;
  int resolution = int(zoom * 72000.0 + 0.5);
  if (resolution > iResolution->maxValue())
    resolution = iResolution->maxValue();
  else if (resolution < iResolution->minValue())
    resolution = iResolution->minValue();
  iCanvas->SetPan(0.5 * (box.Min() + box.Max()));
  iResolution->setValue(resolution);
  statusBar()->clear();
}

//! Do standard zoom (after loading new document)
void AppUi::DefaultZoom()
{
  if (iDoc->size() > 1)
    FitPage();
  else
    NormalSize();
}

//! Change resolution so that page is displayed fully.
void AppUi::FitPage()
{
  IpeRect box = iDoc->Properties().iMedia;
  FitBox(box);
}

void AppUi::FitObjects()
{
  IpeRect bbox;
  for (IpePage::const_iterator it = Page()->begin();
       it != Page()->end(); ++it) {
    bbox.AddRect(it->BBox());
  }
  FitBox(bbox);
}

void AppUi::FitSelection()
{
  IpeRect bbox;
  for (IpePage::const_iterator it = Page()->begin();
       it != Page()->end(); ++it) {
    if (it->Select() != IpePgObject::ENone)
      bbox.AddRect(it->BBox());
  }
  FitBox(bbox);
}

//! Slot to create a new page after the current one.
void AppUi::CreatePage()
{
  ++iPno;
  IpePage *page = iDoc->NewPage(iSnapData.iGridSize);
  iDoc->insert(iDoc->begin() + iPno, page);
  PageChanged();
  iDoc->SetEdited(true);
  statusBar()->clear();
  AddUndo(new IpeUndoPageIns(iPno, IpeQ(tr("page creation"))));
}

//! Slot to delete current page --- if it is empty.
void AppUi::DeletePage()
{
  if (iDoc->size() == 1) {
    statusBar()->message(tr("Cannot delete the only page."));
    return;
  }
  IpePage *page = Page();
  if (page->size() > 0) {
    statusBar()->message(tr("Page is not empty, not deleted."));
    return;
  }
  // undo stack takes ownership of page
  AddUndo(new IpeUndoPageDel(iPno, (*iDoc)[iPno], IpeQ(tr("page deletion"))));
  // erase pointer only
  iDoc->erase(iDoc->begin() + iPno);
  if (iPno == int(iDoc->size()))
    --iPno;
  PageChanged();
  iDoc->SetEdited(true);
  statusBar()->clear();
}

void AppUi::CopyPage(int cut)
{
  if (cut && iDoc->size() == 1) {
    statusBar()->message(tr("Cannot cut the only page."));
    return;
  }
  IpeString data;
  IpeStringStream stream(data);
  Page()->CopyPage(stream, iDoc->StyleSheet());
  QClipboard *cb = QApplication::clipboard();
  // cb->setData(new IpeDrag(data, this));
  cb->setText(QIpe(data));

  if (cut) {
    IpePage *original = Page();
    iDoc->erase(iDoc->begin() + iPno);
    if (iPno == int(iDoc->size()))
      --iPno;
    PageChanged();
    iDoc->SetEdited(true);
    // undo stack takes ownership of original page
    AddUndo(new IpeUndoPageDel(iPno, original, IpeQ(tr("cut page"))));
  } else
    statusBar()->message(tr("Page copied"));
}

void AppUi::PastePage()
{
  QClipboard *cb = QApplication::clipboard();
  QString data = cb->text();
  // if (!IpeDrag::decode(cb->data(), data) ||
  if (data.left(9) != "<ipepage>") {
    statusBar()->message(tr("No Ipe page to paste"));
    return;
  }
  XmlQStringSource source(data);
  IpeImlParser parser(source, iDoc->Repository());
  IpePage *page = parser.ParsePageSelection();
  if (!page) {
    ErrMsg(tr("Could not parse page on clipboard"));
    return;
  }
  ++iPno;
  iDoc->insert(iDoc->begin() + iPno, page);
  PageChanged();
  iDoc->SetEdited(true);
  AddUndo(new IpeUndoPageIns(iPno, IpeQ(tr("paste page"))));
  statusBar()->clear();
}

//! Slot to edit document properties.
void AppUi::EditDocProps()
{
  IpeDocument::SProperties props = iDoc->Properties();
  DialogDocumentProperties *dialog =
    new DialogDocumentProperties(this, props, iDoc->StyleSheet());
  if (dialog->exec() == QDialog::Accepted) {
    if (props.iPreamble != iDoc->Properties().iPreamble)
      iNeedLatex = true;
    iDoc->SetProperties(props);
    // paper size may have changed
    PageChanged();
  }
  statusBar()->clear();
}

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

//! Slot to set mode (connected to object buttons and menu).
void AppUi::SetCreator(QAction *action)
{
  statusBar()->clear();
  for (iMode = 0; iMode < IpeOverlayFactory::Num; iMode++) {
    if (action == iModeAction[iMode])
      return;
  }
}

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

//! Slot to run Latex on text objects.
/*! Returns true if document can now be saved to PDF.  Does not run
  Latex if \a force is \c false and no text objects have been
  modified.
 */
bool AppUi::RunLatex(int force)
{
  statusBar()->message(tr("Running Latex ..."));
  QStringList sd;
  sd.append(QDir::current().path());
  if (!iFileName.isNull()) {
    QFileInfo fi(iFileName);
    sd.append(fi.dirPath(true));
  }
  QString logFile;
  switch (IpeModel::RunLatex(iDoc, force ? true : iNeedLatex, sd, logFile)) {
  case IpeModel::ErrNoText:
    statusBar()->message(tr("No text objects in document, "
			    "no need to run Pdflatex."));
    iNeedLatex = false;
    return true;
  case IpeModel::ErrNoDir:
    ErrMsg(tr("<qt>Directory '%1' does not exist and cannot be created.</qt>")
	   .arg(IpePreferences::Static()->iLatexDir));
    statusBar()->clear();
    return false;
  case IpeModel::ErrWritingSource:
    ErrMsg(tr("<qt>Error writing Latex source</qt>"));
    statusBar()->clear();
    return false;
  case IpeModel::ErrOldPdfLatex:
    ErrMsg(tr("<qt>Your installed version of Pdflatex is too old."
	      "Please install a more recent version (at least version 0.14f)."
	      "Until you do so, Ipe cannot convert text objects.</qt>"));
    statusBar()->clear();
    return false;
  case IpeModel::ErrLatexOutput:
    ErrMsg(tr("<qt>Something is wrong with the PDF file generated by "
	      "Pdflatex.  "
#ifdef WIN32
	      "<hr/>You may also perform a <em>Refresh Filename Database</em> "
	      "in <u>MikTeX Options</u> and try again."
#else
	      "Please consult the stderr output to identify the problem."
#endif
	      "</qt>"));
    statusBar()->clear();
    return false;
  case IpeModel::ErrLatex:
    {
      DialogLatexError *dialog = new DialogLatexError(0, logFile);
      dialog->exec();
    }
    statusBar()->clear();
    return false;
  case IpeModel::ErrNone:
    statusBar()->message(tr("Latex run completed successfully"));
    iCanvas->SetFontPool(IpePreferences::Static()->iAntiAlias,
			 iDoc->FontPool());
    iCanvas->Update();
    iNeedLatex = false;
    return true;
  case IpeModel::ErrAlreadyHaveForm:
    statusBar()->clear();
    return true;
  default:
    assert(false);
    return false;
  };
}

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

//! Slot to edit Ipe preferences.
void AppUi::EditPrefs()
{
  statusBar()->clear();
  IpePreferences *prefs = IpePreferences::Static();

  DialogPreferences *dialog =
    new DialogPreferences(this, *prefs, iSnapData.iGridSize);
  if (dialog->exec() == QDialog::Accepted)
    prefs->Save(); // will cause call to PreferencesChanged
}

void AppUi::PreferencesChanged()
{
  IpePreferences *prefs = IpePreferences::Static();
  iAttributes.iTransformable = prefs->iTransformable;
  iSnapData.iSnapDistance = prefs->iSnapDistance;
  iSnapData.iSelectDistance = prefs->iSelectDistance;
  iCanvas->SetSnap(iSnapData);
  iCanvas->Update();
  setUsesBigPixmaps(prefs->iBigToolButtons);
  QApplication::setFont(prefs->iFont, true);
  statusBar()->clear();
}

void AppUi::EditViews()
{
  statusBar()->clear();
  IpePage *page = Page();
  QStringList layers;
  for (int i = 0; i < page->CountLayers(); ++i) {
    layers.append(QIpe(page->Layer(i).iName));
  }
  IpeViewSeq views = page->Views();
  QStringList sections;
  sections += QIpe(page->Section(0));
  sections += QIpe(page->Section(1));
  DialogPageViews *dialog = new DialogPageViews(this, layers, views, sections);
  if (dialog->exec() == QDialog::Accepted) {
    AddUndo(new IpeUndoViews(iPno, page->Views(), IpeQ("view change")));
    page->SetViews(views);
    page->SetSection(0, IpeQ(sections[0]));
    page->SetSection(1, IpeQ(sections[1]));
    qDebug("New sections: %s %s", sections[0].latin1(),
	   sections[1].latin1());
    PageChanged();
  }
}

//! Slot to show Ipe manual.
void AppUi::Manual()
{
  QDir dir(IpePreferences::Static()->iDocDir);
  QString url = dir.filePath("manual.html");
  StartManual(url);
  statusBar()->clear();
}

void AppUi::StyleSheets()
{
  statusBar()->clear();
  DialogStyles *dialog = new DialogStyles(this, iDoc);
  dialog->exec();
  // need to force update of canvas information (i.e. stylesheet)
  PageChanged();
  ShowStyleInUi();
  // AddUndo(new IpeUndoViews(iPno, page->Views(),
  // IpeQ("style sheet change")));
}

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

void AppUi::RunIpelet(int id)
{
  statusBar()->clear();
  int ipelet = id >> 12;
  int function = id & 0xfff;

  IpePage *original = new IpePage(*Page());
  iIpeletNoUndo = false;
  iIpeletMaster[ipelet]->Run(function, Page(), this);
  Page()->SetEdited(true);
  if (iIpeletNoUndo) {
    delete original;
    iUndo.Clear();
  } else
    AddUndo(new IpeUndoPageEdit(iPno, original, IpeQ(tr("Ipelet result"))));
  iCanvas->Update();
  UpdateLayers();
  SetCaption();
}

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

//! Slot to display about window.
void AppUi::About()
{
  QMessageBox about(tr("About Ipe"),
		    QString(aboutText).arg(IPE_VERSION).arg(IPE_VERSION)
		    .arg(QT_VERSION_STR),
		    QMessageBox::NoIcon,
		    QMessageBox::Ok|QMessageBox::Default,
		    QMessageBox::NoButton,
		    QMessageBox::NoButton);
  about.setIconPixmap(penguinIcon(100));
  about.setButtonText(QMessageBox::Ok, tr("Dismiss"));
  about.exec();
  statusBar()->clear();
}

// --------------------------------------------------------------------
// Private slots
// --------------------------------------------------------------------

void AppUi::AbsoluteAttributes()
{
  iAbsoluteAttributes = !iAbsoluteAttributes;
  iEditMenu->setItemChecked(EAbsoluteAttributesMenuId, iAbsoluteAttributes);
  SwitchAttributeUi();
  statusBar()->clear();
  if (iAbsoluteAttributes) {
    // when switching to absolute, set absolute values from symbolic ones
    const IpeStyleSheet *sheet = iDoc->StyleSheet();
    iAttributes.iStroke = sheet->Find(iAttributes.iStroke);
    iAbsStrokeColor->setPixmap
      (ColorPixmap(iAttributes.iStroke, sheet));
    iAttributes.iFill = sheet->Find(iAttributes.iFill);
    iAbsFillColor->setPixmap
      (ColorPixmap(iAttributes.iFill, sheet));
    ConvertAbsolute(iAbsLineWidth, iAttributes.iLineWidth);
    ConvertAbsolute(iAbsMarkSize, iAttributes.iMarkSize);
    ConvertAbsolute(iAbsArrowSize, iAttributes.iArrowSize);
    IpeAttribute ts = iDoc->StyleSheet()->Find(iAttributes.iTextSize);
    if (ts.IsNumeric())
      iAbsTextSize->setValueFromAttribute(ts.Number().Internal());
    else
      iAbsTextSize->setValueFromAttribute(10000); // 10 pt
  } else {
    // when switching to symbolic, use previous symbolic settings
    // and update iAttributes
    iAttributes.iStroke =
      iSyms[IpeAttribute::EColor][iStrokeColor->currentItem()];
    iAttributes.iFill =
      iSyms[IpeAttribute::EColor][iFillColor->currentItem()];
    iAttributes.iLineWidth =
      iSyms[IpeAttribute::ELineWidth][iLineWidth->currentItem()];
    iAttributes.iTextSize =
      iSyms[IpeAttribute::ETextSize][iTextSize->currentItem()];
    iAttributes.iMarkSize =
      iSyms[IpeAttribute::EMarkSize][iMarkSize->currentItem()];
    iAttributes.iArrowSize =
      iSyms[IpeAttribute::EArrowSize][iArrowSize->currentItem()];
  }
}

void AppUi::AbsoluteSnapping()
{
  iAbsoluteSnapping = !iAbsoluteSnapping;
  iSnapMenu->setItemChecked(EAbsoluteSnappingMenuId, iAbsoluteSnapping);
  SwitchSnapUi();
  statusBar()->clear();
  if (iAbsoluteSnapping) {
    // when switching to absolute,
    // set absolute values from current setting
    iAbsGridSize->Set(iSnapData.iGridSize);
    iAbsAngleSize->setValueFromAttribute(iSnapAngle.Internal());
  } else {
    // when switching to symbolic, use previous symbolic settings
    iSnapData.iGridSize = AbsValue(iSyms[IpeAttribute::EGridSize]
				   [iGridSize->currentItem()]).ToInt();
    iSnapAngle = AbsValue(iSyms[IpeAttribute::EAngleSize]
			  [iAngleSize->currentItem()]);
    iSnapData.iAngleSize = IpeAngle::Degrees(iSnapAngle.ToDouble());
    iCanvas->SetSnap(iSnapData);
    if (iSnapData.iWithAxes || iSnapData.iGridVisible)
      iCanvas->Update();
  }
}

void AppUi::PageChanged(int no)
{
  iPno = no - 1;
  PageChanged();
}

void AppUi::ViewChanged(int no)
{
  iVno = no - 1;
  ViewChanged();
}

void AppUi::ResolutionChanged(int resolution)
{
  // compute zoom factor from resolution: 72 dpi = value 72000 = 1.0
  double zoom = resolution / 72000.0;
  iCanvas->SetZoom(zoom);
  statusBar()->clear();
}

void AppUi::LineWidthChanged(int id)
{
  iAttributes.iLineWidth = iSyms[IpeAttribute::ELineWidth][id];
  Cmd(ECmdLineWidth);
}

void AppUi::AbsLineWidthChanged(int val)
{
  iAttributes.iLineWidth =
    iDoc->Repository()->ToAttribute(IpeAttribute::ELineWidth,
				    IpeFixed::FromInternal(val));
  Cmd(ECmdLineWidth);
}

void AppUi::DashStyleChanged(int id)
{
  iAttributes.iDashStyle = iSyms[IpeAttribute::EDashStyle][id];
  Cmd(ECmdDashStyle);
}

void AppUi::TextSizeChanged(int id)
{
  iAttributes.iTextSize = iSyms[IpeAttribute::ETextSize][id];
  Cmd(ECmdTextSize);
}

void AppUi::AbsTextSizeChanged(int val)
{
  iAttributes.iTextSize =
    iDoc->Repository()->ToAttribute(IpeAttribute::ETextSize,
				    IpeFixed::FromInternal(val));
  Cmd(ECmdTextSize);
}

void AppUi::MarkShapeChanged(int id)
{
  iAttributes.iMarkShape = id + 1;
  Cmd(ECmdMarkShape);
}

void AppUi::MarkSizeChanged(int id)
{
  iAttributes.iMarkSize = iSyms[IpeAttribute::EMarkSize][id];
  Cmd(ECmdMarkSize);
}

void AppUi::AbsMarkSizeChanged(int val)
{
  iAttributes.iMarkSize =
    iDoc->Repository()->ToAttribute(IpeAttribute::EMarkSize,
				    IpeFixed::FromInternal(val));
  Cmd(ECmdMarkSize);
}

void AppUi::ArrowChanged(int id)
{
  iAttributes.iForwardArrow = (id & 1) != 0;
  iAttributes.iBackwardArrow = (id & 2) != 0;
  Cmd(ECmdSetArrows);
}

void AppUi::ArrowSizeChanged(int id)
{
  iAttributes.iArrowSize = iSyms[IpeAttribute::EArrowSize][id];
  Cmd(ECmdArrowSize);
}

void AppUi::AbsArrowSizeChanged(int val)
{
  iAttributes.iArrowSize =
    iDoc->Repository()->ToAttribute(IpeAttribute::EArrowSize,
				    IpeFixed::FromInternal(val));
  Cmd(ECmdArrowSize);
}

void AppUi::GridSizeChanged(int id)
{
  iSnapData.iGridSize = AbsValue(iSyms[IpeAttribute::EGridSize][id]).ToInt();
  iCanvas->SetSnap(iSnapData);
  if (iSnapData.iGridVisible)
    iCanvas->Update();
  if (Page())
    Page()->SetGridSize(iSnapData.iGridSize);
  statusBar()->clear();
}

void AppUi::AbsGridSizeChanged(int val)
{
  iSnapData.iGridSize = val;
  iCanvas->SetSnap(iSnapData);
  if (iSnapData.iGridVisible)
    iCanvas->Update();
  if (Page())
    Page()->SetGridSize(iSnapData.iGridSize);
  statusBar()->clear();
}

void AppUi::AngleSizeChanged(int id)
{
  iSnapAngle = AbsValue(iSyms[IpeAttribute::EAngleSize][id]);
  iSnapData.iAngleSize = IpeAngle::Degrees(iSnapAngle.ToDouble());
  iCanvas->SetSnap(iSnapData);
  if (iSnapData.iWithAxes)
    iCanvas->Update();
  statusBar()->clear();
}

void AppUi::AbsAngleSizeChanged(int val)
{
  iSnapAngle = IpeFixed::FromInternal(val);
  iSnapData.iAngleSize = IpeAngle::Degrees(iSnapAngle.ToDouble());
  iCanvas->SetSnap(iSnapData);
  if (iSnapData.iWithAxes)
    iCanvas->Update();
  statusBar()->clear();
}

//! Snapping was changed.
void AppUi::SnapChanged(bool)
{
  iSnapData.iSnap = 0;
  for (int i = 0; i < 6; ++i) {
    if (iSnapAction[i]->isOn())
      iSnapData.iSnap |= (1 << i);
  }
  if (iSnapData.iSnap & IpeSnapData::ESnapAngle)
    iSnapData.iWithAxes = true;
  iCanvas->SetSnap(iSnapData);
  statusBar()->clear();
}

void AppUi::GridVisible()
{
  iSnapData.iGridVisible = !iSnapData.iGridVisible;
  iZoomMenu->setItemChecked(EGridVisibleMenuId, iSnapData.iGridVisible);
  iCanvas->SetSnap(iSnapData);
  iCanvas->Update();
  statusBar()->clear();
}

void AppUi::CoordinatesVisible()
{
  if (iMouse->isVisible())
    iMouse->hide();
  else
    iMouse->show();
  iZoomMenu->setItemChecked(ECoordinatesVisibleMenuId, iMouse->isVisible());
}

//! Slot for the absolute stroke color button.
void AppUi::SetStrokeColor()
{
  QColor stroke = QColorDialog::getColor(Qt::red, 0);
  SetColorFromQColor(iAttributes.iStroke, stroke, iAbsStrokeColor);
  Cmd(ECmdStroke);
}

//! Slot for the absolute fill color button.
void AppUi::SetFillColor()
{
  QColor fill = QColorDialog::getColor(Qt::red, 0);
  SetColorFromQColor(iAttributes.iFill, fill, iAbsFillColor);
  Cmd(ECmdFill);
}

//! Slot for the stroke color comboBox.
void AppUi::SetStrokeColorName(int item)
{
  iAttributes.iStroke = iSyms[IpeAttribute::EColor][item];
  Cmd(ECmdStroke);
}

//! Slot for the fill color comboBox.
void AppUi::SetFillColorName(int item)
{
  iAttributes.iFill = iSyms[IpeAttribute::EColor][item];
  Cmd(ECmdFill);
}

// ----------------------------------------------------------
// Private utility functions
// ----------------------------------------------------------

void AppUi::PageChanged()
{
  iVno = 0;
  iLayer = 0;
  iPageNumber->Set(iPno + 1, iDoc->size());
  SetGridSizeFromPage();
  ViewChanged();

#if 0
  // update bookmarks
  iBookmarks->clear();
  QListViewItem *cursec = 0;
  QListViewItem *cursubsec = 0;
  for (uint i = 0; i < iDoc->size(); ++i) {
    IpeString s = (*iDoc)[i]->Section(0);
    IpeString ss = (*iDoc)[i]->Section(1);
    if (!s.empty()) {
      if (cursec)
	cursec = new BookmarkItem(iBookmarks, cursec, s, i, iPageNumber);
      else
	cursec = new BookmarkItem(iBookmarks, s, i, iPageNumber);
      cursubsec = 0;
    }
    if (cursec && !ss.empty()) {
      if (cursubsec)
	cursubsec = new BookmarkItem(cursec, cursubsec, ss, i, iPageNumber);
      else
	cursubsec = new BookmarkItem(cursec, ss, i, iPageNumber);
    }
  }
#else
  iBookmarks->clear();
  for (uint i = 0; i < iDoc->size(); ++i) {
    IpeString s = (*iDoc)[i]->Section(0);
    IpeString ss = (*iDoc)[i]->Section(1);
    if (!s.empty())
      iBookmarks->insertItem(QIpe(s));
    if (!ss.empty())
      iBookmarks->insertItem(" + " + QIpe(ss));
  }
  if (iBookmarks->count() > 0)
    iBookmarkTools->show();
  else
    iBookmarkTools->hide();
#endif
}

void AppUi::BookmarkSelected(int index)
{
  int count = 0;
  for (uint i = 0; i < iDoc->size(); ++i) {
    IpeString s = (*iDoc)[i]->Section(0);
    IpeString ss = (*iDoc)[i]->Section(1);
    if (!s.empty()) {
      if (count == index)
	iPageNumber->setValue(i + 1);
      ++count;
    }
    if (!ss.empty()) {
      if (count == index)
	iPageNumber->setValue(i + 1);
      ++count;
    }
  }
}

void AppUi::ViewChanged()
{
  IpePage *page = Page();
  iViewNumber->Set(iVno + 1, page->CountViews());
  int i = page->FindLayer(page->View(iVno).Active());
  if (i >= 0)
    iLayer = i;
  page->DeselectNotInView(iVno);
  page->EnsurePrimarySelection();
  iCanvas->SetPage(page, iVno, iDoc->StyleSheet(),
		   iDoc->Properties().iMedia, PageColor());
  UpdateLayers();
  SetCaption();
  if (iDoc->TotalViews() > 1)
    iPageTools->show();
  else
    iPageTools->hide();
  statusBar()->clear();
}

void AppUi::UpdateLayers()
{
  IpePage *page = Page();
  // make sure it doesn't try to change layers
  iLayerList->disconnect(this);
  iLayerList->clear();
  for (int i = 0; i < page->CountLayers(); ++i) {
    const IpeLayer &l = page->Layer(i);
    QString s;
    if (l.IsDimmed())
      s += "D";
    if (l.IsLocked())
      s += "L";
    if (!l.IsSnapping())
      s += "S";
    if (!s.isEmpty())
      s += " ";
    s += QIpe(l.iName);
    iLayerList->insertItem(new LayerBoxItem(s, (i == iLayer)));
    iLayerList->setSelected(i, page->View(iVno).HasLayer(l.iName));
  }
  // reconnect signals
  connect(iLayerList,
	  SIGNAL(rightButtonPressed(QListBoxItem *, const QPoint &)),
	  this,
	  SLOT(LayerRightPress(QListBoxItem *, const QPoint &)));
  connect(iLayerList, SIGNAL(selectionChanged()),
	  this, SLOT(LayerSelectionChanged()));
}

void AppUi::LayerRightPress(QListBoxItem *item, const QPoint &pos)
{
  enum { EActivate, EDelete, EHide, EUnhide, EDim, EUndim,
	 ELock, EUnlock, ESnap, EUnsnap };

  IpePage *page = Page();

  int index = iLayerList->index(item);
  if (index < 0)
    return;
  IpeLayer &layer = page->Layer(index);

  QPopupMenu *menu = new QPopupMenu;
  if (index != iLayer) {
    if (!layer.IsLocked())
      menu->insertItem(tr("Set as active"), EActivate);
    menu->insertItem(tr("Delete"), EDelete);
  }
  if (layer.IsDimmed())
    menu->insertItem(tr("Undim"), EUndim);
  else
    menu->insertItem(tr("Dim"), EDim);
  if (layer.IsLocked())
    menu->insertItem(tr("Unlock layer"), EUnlock);
  else if (index != iLayer && !page->IsLayerActiveInView(index))
    menu->insertItem(tr("Lock layer"), ELock);
  if (layer.IsSnapping())
    menu->insertItem(tr("Disable snapping"), EUnsnap);
  else
    menu->insertItem(tr("Enable snapping"), ESnap);

  IpeAutoPtr<IpePage> originalPage = new IpePage(*page);

  switch (menu->exec(pos)) {
  case EActivate:
    iLayer = index;
    if (page->CountViews() > 0) {
      page->View(iVno).SetActive(page->Layer(iLayer).Name());
      page->SetEdited(true);
      AddUndo(new IpeUndoPageEdit(iPno, originalPage.Take(),
				  IpeQ(tr("active layer change"))));
    }
    // return immediately, don't use layer undo below
    UpdateLayers();
    return;
  case EDelete:
    if (page->CountLayers() > 1 && index != iLayer) {
      IpePage::const_iterator it = page->begin();
      while (it != page->end() && it->Layer() != index)
	++it;
      if (it != page->end()) {
	statusBar()->message(tr("Layer is not empty, not deleted"));
	return;
      }
      if (iLayer > index)
	--iLayer;
      page->DeleteLayer(index);
      AddUndo(new IpeUndoPageEdit(iPno, originalPage.Take(),
				  IpeQ(tr("deleting layer"))));
      UpdateLayers();
    }
    // return immediately, don't use layer undo below
    return;
  case EDim:
    layer.SetDimmed(true);
    break;
  case EUndim:
    layer.SetDimmed(false);
    break;
  case ELock:
    layer.SetLocked(true);
    page->DeselectLayer(index); // no object in this layer must be selected
    break;
  case EUnlock:
    layer.SetLocked(false);
    break;
  case ESnap:
    layer.SetSnapping(true);
    break;
  case EUnsnap:
    layer.SetSnapping(false);
    break;
  default:
    // nothing
    return;
  }
  page->SetEdited(true);
  AddUndo(new IpeUndoPageEdit(iPno, originalPage.Take(),
			      IpeQ(tr("layer modification"))));
  UpdateLayers();
  iCanvas->Update();
  SetCaption();
}

void AppUi::LayerSelectionChanged()
{
  IpePage *page = Page();
  IpePage *original = new IpePage(*page);
  IpeView &view = page->View(iVno);
  view.ClearLayers();
  for (int i = 0; i < page->CountLayers(); ++i) {
    if (iLayerList->isSelected(i))
      view.AddLayer(page->Layer(i).Name());
  }
  view.SetActive(page->Layer(iLayer).Name());
  AddUndo(new IpeUndoPageEdit(iPno, original, IpeQ(tr("view modification"))));
  page->DeselectNotInView(iVno);
  page->EnsurePrimarySelection();
  page->SetEdited(true);
  iCanvas->Update();
  statusBar()->message(tr("View modified"));
  SetCaption();
}

IpeFixed AppUi::AbsValue(IpeAttribute attr)
{
  IpeAttribute abs = iDoc->StyleSheet()->Find(attr);
  return iDoc->Repository()->ToScalar(abs);
}

void AppUi::ConvertAbsolute(DecimalSpinBox *spin, IpeAttribute &attr)
{
  IpeAttribute abs = iDoc->StyleSheet()->Find(attr);
  IpeFixed value = iDoc->Repository()->ToScalar(abs);
  spin->setValueFromAttribute(value.Internal());
  attr = abs;
}

//! Switch to displaying absolute or symbolic values
void AppUi::SwitchAttributeUi()
{
  bool a = (iAbsoluteAttributes ? 1 : 0);
  iStrokeColorStack->raiseWidget(a);
  iFillColorStack->raiseWidget(a);
  iLineWidthStack->raiseWidget(a);
  iArrowSizeStack->raiseWidget(a);
  iTextSizeStack->raiseWidget(a);
  iMarkSizeStack->raiseWidget(a);
}

//! Switch to displaying absolute or symbolic snap values
void AppUi::SwitchSnapUi()
{
  bool a = (iAbsoluteSnapping ? 1 : 0);
  iGridSizeStack->raiseWidget(a);
  iAngleSizeStack->raiseWidget(a);
}

void AppUi::ResetCombo(IpeKind kind,
		       QComboBox *combo,
		       IpeAttribute &attr,
		       QPixmap *pixmap)
{
  combo->clear();
  for (IpeAttributeSeq::const_iterator it = iSyms[kind].begin();
       it != iSyms[kind].end(); ++it) {
    if (pixmap)
      combo->insertItem(*pixmap, QIpe(iDoc->Repository()->String(*it)));
    else
      combo->insertItem(QIpe(iDoc->Repository()->String(*it)));
  }
  combo->setCurrentItem(0);
  attr = iSyms[kind][0];
}

void AppUi::ShowStyleInUi()
{
  iDoc->StyleSheet()->AllNames(iSyms);

  iFillColor->clear();
  iStrokeColor->clear();
  for (IpeAttributeSeq::const_iterator
	 it = iSyms[IpeAttribute::EColor].begin();
       it != iSyms[IpeAttribute::EColor].end(); ++it) {
    InsertColor(iStrokeColor, *it);
    InsertColor(iFillColor, *it);
  }
  iStrokeColor->setCurrentItem(1);
  iAttributes.iStroke = iSyms[IpeAttribute::EColor][1];
  iAbsStrokeColor->setPixmap(ColorPixmap(iAttributes.iStroke,
					 iDoc->StyleSheet()));

  iFillColor->setCurrentItem(0);
  iAttributes.iFill = iSyms[IpeAttribute::EColor][0];
  iAbsFillColor->setPixmap(ColorPixmap(iAttributes.iFill, iDoc->StyleSheet()));

  iDashStyle->clear();
  for (IpeAttributeSeq::const_iterator
	 it = iSyms[IpeAttribute::EDashStyle].begin();
       it != iSyms[IpeAttribute::EDashStyle].end(); ++it) {
    iDashStyle->insertItem(QIpe(iDoc->Repository()->String(*it)));
  }
  iDashStyle->setCurrentItem(0);
  iAttributes.iDashStyle = iSyms[IpeAttribute::EDashStyle][0];

  QPixmap lwIcon = ipeIcon("lineWidth");
  QPixmap arrowIcon = ipeIcon("arrow");
  QPixmap abcIcon = ipeIcon("paragraph");
  QPixmap markIcon = ipeIcon("marks");

  ResetCombo(IpeAttribute::ELineWidth, iLineWidth,
	     iAttributes.iLineWidth, &lwIcon);
  ResetCombo(IpeAttribute::ETextSize, iTextSize,
	     iAttributes.iTextSize, &abcIcon);
  ResetCombo(IpeAttribute::EArrowSize, iArrowSize,
	     iAttributes.iArrowSize, &arrowIcon);
  ResetCombo(IpeAttribute::EMarkSize, iMarkSize,
	     iAttributes.iMarkSize, &markIcon);

  IpeAttribute grid;
  ResetCombo(IpeAttribute::EGridSize, iGridSize, grid);
  iSnapData.iGridSize = AbsValue(grid).ToInt();

  IpeAttribute angle;
  ResetCombo(IpeAttribute::EAngleSize, iAngleSize, angle);
  iSnapAngle = AbsValue(angle);
  iSnapData.iAngleSize = IpeAngle::Degrees(iSnapAngle.ToDouble());
  iCanvas->SetSnap(iSnapData);
}

void AppUi::InsertColor(QComboBox *combo, IpeAttribute sym)
{
  combo->insertItem(ColorPixmap(sym, iDoc->StyleSheet()),
		    QIpe(iDoc->Repository()->String(sym)));
}

// Set gridsize box from page information
void AppUi::SetGridSizeFromPage()
{
  int gs = Page()->GridSize();
  if (gs > 0) {
    // find correct symbolic gridsize
    int sym = -1;
    for (uint i = 0; i < iSyms[IpeAttribute::EGridSize].size(); ++i) {
      if (AbsValue(iSyms[IpeAttribute::EGridSize][i]).ToInt() == gs) {
	sym = i;
	break;
      }
    }
    // change absolute snapping mode
    if (iAbsoluteSnapping && sym >= 0 || !iAbsoluteSnapping && sym < 0)
      AbsoluteSnapping();
    if (iAbsoluteSnapping) {
      iAbsGridSize->Set(gs);
    } else
      iGridSize->setCurrentItem(sym);
    iSnapData.iGridSize = gs;
    iCanvas->SetSnap(iSnapData);
    if (iSnapData.iGridVisible)
      iCanvas->Update();
  }
}

/*! Set \c colorToSet from \c qColor, and update the color in the \c
  button.
*/
void AppUi::SetColorFromQColor(IpeAttribute &colorToSet, QColor &qColor,
			       QPushButton *button)
{
  QPixmap pixmap;
  pixmap.resize(14, 14);
  pixmap.fill(qColor);
  button->setPixmap(pixmap);
  int r, g, b;
  qColor.rgb(&r, &g, &b);
  colorToSet = iDoc->Repository()->
    ToAttribute(IpeColor(r / 255.0, g / 255.0, b / 255.0));
}

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

void AppUi::ErrMsg(QString str)
{
  QMessageBox::warning(this, "Ipe", "<qt>" + str + "</qt>", tr("Dismiss"));
}

void AppUi::InsertTextBox()
{
  // need to use media and margins
  IpeRect r = Page()->TextBox(iDoc->Properties().iMedia, iDoc->StyleSheet());
  IpeCreateText::New(0, iCanvas, this, IpeCreateText::EMinipage, &r);
}

void AppUi::InsertItemBox()
{
  // need to use media and margins
  IpeRect r = Page()->TextBox(iDoc->Properties().iMedia, iDoc->StyleSheet());
  IpeCreateText::New(0, iCanvas, this, IpeCreateText::EMinipage, &r,
		     "\\begin{ITEM}\n\\end{ITEM}\n");
}

#if QT_VERSION >= 0x030000
QKeySequence AppUi::Key(const char *src, const char *comment)
{
  QString transl = qApp->translate("Key", src, comment);
  QTranslatorMessage m("Key", src, comment, transl);
  iKeyTranslator.insert(m);
  return QKeySequence(transl);
}
#else
int AppUi::Key(const char *src, const char *comment)
{
  QString transl = qApp->translate("Key", src, comment);
  QTranslatorMessage m("Key", src, comment, transl);
  iKeyTranslator.insert(m);
  int key = QAccel::stringToKey(transl);
  if ((key & CTRL) && (key & 0xffff) < 0x100)
    key &= ~UNICODE_ACCEL;
  return key;
}
#endif

IpeAttribute AppUi::PageColor()
{
  if (IpePreferences::Static()->iWhitePaper)
    return IpeAttribute::White();
  return iDoc->Repository()->ToAttribute(IpeColor(1.0, 1.0, 0.5));
}

// --------------------------------------------------------------------
// IpeCanvasServices interface
// --------------------------------------------------------------------

IpeBuffer AppUi::StandardFont(IpeString fontName)
{
  return IpePreferences::Static()->StandardFont(fontName);
}

void AppUi::CvSvcRequestOverlay(QMouseEvent *ev)
{
  IpeOverlayFactory f(ev, iCanvas, Page(), iVno, this);
  // remember current mouse position
  iMouseBase = iCanvas->Pos();
  // need to select right Overlay
  if (ev->button() == RightButton) {
    if (IpePreferences::Static()->iRightMouseSelects &&
	!(ev->state() & ControlButton)) {
      f.CreateOverlay(IpeOverlayFactory::ESelecting);
    } else {
      double d = IpePreferences::Static()->iSelectDistance / iCanvas->Zoom();
      if (Page()->UpdateCloseSelection(iCanvas->Pos(), d, true, iVno)) {
	AttributePopup *pop =
	  new AttributePopup(Page()->PrimarySelection(), Page(),
			     iCanvas, iLayer,
			     iDoc->StyleSheet(), iSyms, this);
	pop->Exec();
      }
      SetCaption();
    }
  } else if (ev->button() == MidButton) {
    if ((ev->state() & ControlButton) && (ev->state() & ShiftButton))
      f.CreateOverlay(IpeOverlayFactory::EMoving);
    else if (ev->state() & ControlButton)
      f.CreateOverlay(IpeOverlayFactory::ERotating);
    else if (ev->state() & (AltButton|ShiftButton))
      f.CreateOverlay(IpeOverlayFactory::EPanning);
    else
      f.CreateOverlay(IpeOverlayFactory::EMoving);
  } else {
    // Left button
    if (ev->state() & (AltButton|ControlButton))
      f.CreateOverlay(IpeOverlayFactory::EStretching);
    else
      f.CreateOverlay(iMode);
  }
}

void AppUi::CvSvcSetDrawingMode(bool mode)
{
  for (uint i = 0; i < menuBar()->count(); ++i) {
    menuBar()->setItemEnabled(menuBar()->idAt(i), !mode);
  }
  menuBar()->setItemEnabled(EZoomMenuId, true);
  menuBar()->setItemEnabled(ESnapMenuId, true);
  iPageTools->setEnabled(!mode);
  iFileTools->setEnabled(!mode);
  iModeActionGroup->setEnabled(!mode);
}

void AppUi::CvSvcWheelZoom(int delta)
{
  if (delta > 0)
    iResolution->stepUp();
  else
    iResolution->stepDown();
}

inline void Adjust(IpeScalar &x)
{
  if (IpeAbs(x) < 1e-12)
    x = 0.0;
}

void AppUi::CvSvcMousePosition(const IpeVector &pos)
{
  if (iMouse->isVisible()) {
    IpeVector v = pos;
    if (iSnapData.iWithAxes) {
      v = v - iSnapData.iOrigin;
      v = IpeLinear(-iSnapData.iDir) * v;
    }
    Adjust(v.iX);
    Adjust(v.iY);
    QString s;
    s.sprintf("%g,%g", v.iX, v.iY);
    if (!iFileTools->isEnabled()) {
      IpeVector u = pos - iMouseBase;
      if (iSnapData.iWithAxes)
	u = IpeLinear(-iSnapData.iDir) * u;
      Adjust(u.iX);
      Adjust(u.iY);
      QString r;
      r.sprintf(" (%+g,%+g)", u.iX, u.iY);
      s += r;
    }
    iMouse->setText(s);
  }
}

// --------------------------------------------------------------------
// IpeOverlayServices interface
// --------------------------------------------------------------------

void AppUi::OvSvcAddObject(IpeObject *obj)
{
  IpePage *page = Page();
  page->DeselectAll();
  bool sel = page->View(iVno).HasLayer(page->Layer(iLayer).Name());
  page->push_back(IpePgObject((sel ? IpePgObject::EPrimary :
			       IpePgObject::ENone),
			      iLayer, obj));
  page->SetEdited(true);
  AddUndo(new IpeUndoObjInsertion(iPno, IpeQ(tr("object insertion"))));
  SetCaption();
  if (!sel)
    ErrMsg(tr("Active layer is not visible, "
	      "so your new object will be invisible."));
}

const IpeAllAttributes &AppUi::OvSvcAttributes()
{
  return iAttributes;
}

const IpeStyleSheet *AppUi::OvSvcStyleSheet()
{
  return iDoc->StyleSheet();
}

void AppUi::OvSvcAddUndoItem(IpePage *page, QString s)
{
  AddUndo(new IpeUndoPageEdit(iPno, page, IpeQ(s)));
}

void AppUi::OvSvcAddUndoItem(IpePage::iterator it, IpePage *page,
			     const IpePgObject &original, QString s)
{
  AddUndo(new IpeUndoObjectEdit(iPno, it, page, original, IpeQ(s)));
}

// --------------------------------------------------------------------
// IpeletHelper interface
// --------------------------------------------------------------------

void AppUi::Message(const char *msg)
{
  statusBar()->message(msg);
}

int AppUi::MessageBox(const char *text, const char *button1,
		      const char *button2, const char *button3)
{
  return (QMessageBox::information(this, "Ipelet",
				   QString("<qt>") + text + "</qt>",
				   button1, button2, button3));
}

bool AppUi::GetString(const char *prompt, IpeString &str)
{
  bool ok = false;
  QString text = QInputDialog::getText("Ipelet", prompt,
				       QLineEdit::Normal,
				       QIpe(str), &ok, this);
  if (ok)
    str = IpeQ(text);
  return ok;
}

const IpeStyleSheet *AppUi::StyleSheet()
{
  return iDoc->StyleSheet();
}

const IpeDocument *AppUi::Document()
{
  return iDoc;
}

IpeDocument *AppUi::EditDocument()
{
  iDoc->SetEdited(true);
  iIpeletNoUndo = true;
  return iDoc;
}

int AppUi::CurrentPage() const
{
  return iPno;
}

int AppUi::CurrentView() const
{
  return iVno;
}

int AppUi::CurrentLayer() const
{
  return iLayer;
}

const IpeAllAttributes &AppUi::Attributes() const
{
  return iAttributes;
}

const IpeSnapData &AppUi::SnapData() const
{
  return iSnapData;
}

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