/***************************************************************************
**
**  This file is part of SciFigs.
**
**  This library is free software; you can redistribute it and/or
**  modify it under the terms of the GNU Lesser General Public
**  License as published by the Free Software Foundation; either
**  version 2.1 of the License, or (at your option) any later version.
**
**  This file is distributed in the hope that it will be useful, but WITHOUT
**  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
**  FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
**  License for more details.
**
**  You should have received a copy of the GNU Lesser General Public
**  License along with this library; if not, write to the Free Software
**  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
**
**  See http://www.geopsy.org for more information.
**
**  Created: 2003-02-13
**  Copyright: 2003-2019
**    Marc Wathelet
**    Marc Wathelet (ULg, Liège, Belgium)
**    Marc Wathelet (LGIT, Grenoble, France)
**
***************************************************************************/

#ifndef NO_SVG
#include <QtSvg>
#endif
#if(QT_VERSION > QT_VERSION_CHECK(5, 0, 0))
#include <QtPrintSupport>
#endif

#include "GraphicSheet.h"
#include "OrderTip.h"
#include "PageLimit.h"
#include "SelectionWidget.h"
#include "SciFigsPreferences.h"
#include "GraphicObjectFactory.h"
#include "GraphicObjectGroup.h"
#include "AxisWindow.h"
#include "TextEdit.h"
#include "XMLSciFigs.h"
#include "SciFigsGlobal.h"
#include "MakeUpFilter.h"
#include "GraphicSheetProperties.h"
#include "SheetSelectionWidget.h"
#include "AlignGridParameters.h"

//#define SELECTION_DEBUG

namespace SciFigs {

  const QString GraphicSheet::xmlGraphicSheetTag="GraphicSheet";

  /*!
    \ingroup SciFigs
    \class GraphicSheet GraphicSheet.h
    \brief The GraphicSheet is a working sheet where any set of GraphicObject can be displayed

  */

  GraphicSheet::GraphicSheet(QWidget *parent) :
      QScrollArea(parent)
  {
    TRACE;
    setWidgetColor(this, Qt::white);
    setAutoFillBackground(true);
    setWidget(new QWidget(this));

    _statusBar=nullptr;
    _currentActiveSelection=nullptr;
    new QShortcut(Qt::Key_Tab, this, SLOT(selectNextChild()));
    new QShortcut(Qt::Key_Tab + Qt::CTRL, this, SLOT(addSelectNextChild()));
    _orderIndex=-1;
    _transparency=255;
    _transparentMask=false;
    setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
    _dragDelay.setSingleShot(true);
    _dragDelay.setInterval(50);
    connect(&_dragDelay, SIGNAL(timeout()), this, SLOT(dragObjectsDelayed()));
    addActions();
    _mouseTrackingRect=nullptr;
    _selection=new SheetSelectionWidget(this);
  }

  GraphicSheet::~GraphicSheet()
  {
    TRACE;
    if(proxy()) {
      proxy()->saveStates();
      selectObjects(nullptr);
      removeProperties(proxy());
    }
    delete _mouseTrackingRect;
  }

  /*!
    Initialize context menu
  */
  void GraphicSheet::addActions()
  {
    TRACE;
    QAction * a;

    a=new QAction(tr("&Properties"), this);
    a->setObjectName("Properties");
    a->setToolTip(tr("Set properties of object"));
    connect(a, SIGNAL(triggered(bool)) , this, SLOT(showProperties()));
    QWidget::addAction(a);
  }

  /*!
    Populate context menu with submenus

    This is mainly used by GraphicSheetMenu to copy the contextual menu bar to the context menu. Under Mac OS X, the
    contextual menu bar is not allowed, and Mac users have no access to the sheet actions. It gives the user an interface
    like in GIMP for Linux and Windows.
  */
  void GraphicSheet::addMenu(QMenu * m)
  {
    TRACE;
    QAction * a;

    if(actions().count()==1) {
      a=new QAction(this);
      a->setSeparator(true);
      QWidget::addAction(a);
    }

    a=new QAction(m->title(), this);
    a -> setMenu(m);
    QWidget::addAction(a);
  }

  /*!

  */
  void GraphicSheet::showProperties(uint category)
  {
    TRACE;
    initProxy();
    proxy()->saveStates();
    addProperties(proxy());
    GraphicObjectList wList=selectedObjects<GraphicObject>();
    GraphicObject * curObj;
    if(_currentActiveSelection) {
      curObj=_currentActiveSelection->widget();
    } else {
      curObj=nullptr;
    }
    for(int i=wList.count()-1; i>=0; i--) {
      GraphicObject * obj=wList.at(i);
      if(obj!=curObj) {
        obj->addProperties(proxy());
      }
    }
    if(curObj) {
      curObj->addProperties(proxy());
    }
    setPropertyTitle(this);
    raiseEditor(category);
    proxy()->restoreStates();
    proxy()->setValues();
  }

  uint GraphicSheet::_category=PropertyProxy::uniqueId();
  uint GraphicSheet::_tabPrint=PropertyProxy::uniqueId();

  /*!

  */
  void GraphicSheet::addProperties(PropertyProxy * pp)
  {
    TRACE;
    if(!pp->setCurrentCategory(_category)) {
      pp->addCategory(_category, tr("Sheet"), QIcon(":graphicsheet.png"));
    }
    if(pp->setCurrentTab(_tabPrint)) {
      pp->addReference(this);
    } else {
      GraphicSheetProperties * w=new GraphicSheetProperties;
      pp->addTab(_tabPrint, tr("Print"), w, this);
    }
  }

  /*!

  */
  void GraphicSheet::removeProperties(PropertyProxy * pp)
  {
    TRACE;
    if(pp->setCurrentCategory(_category)) {
      pp->removeTab(_tabPrint, this);
    }
  }

  void GraphicSheet::properties(PropertyWidget * w) const
  {
    TRACE;
    w->setValue(GraphicSheetProperties::Transparency, transparency());
    w->setValue(GraphicSheetProperties::TransparentMask, transparentMask());
  }

  void GraphicSheet::setProperty(uint, int pid, QVariant val)
  {
    TRACE;
    switch(pid) {
    case GraphicSheetProperties::Transparency:
      setTransparency(val.toInt());
      break;
    case GraphicSheetProperties::TransparentMask:
      setTransparentMask(val.toBool());
      break;
    }
  }

  /*!
    \fn GraphicObjectList GraphicSheet::objects() const
    Return the list of GraphicObject objects.
  */

  /*!
    Add object \a obj and show it. No need to call showObject().
  */
  void GraphicSheet::addObject(GraphicObject * obj, bool show)
  {
    TRACE;
    if(obj) {
      obj->setSheet(this);
      if(obj->objectName().isEmpty()) {
        obj->setObjectName("object");
      } else {
        // Make sure that its name is unique
        obj->setObjectName(obj->objectName());
      }
      if(show) {
        showObject(obj);
      }
    }
  }

  /*!
    This slot create and add an object according to the sender. It must be a QAction and the data must
    contain a pointer to a GraphicObjectCreator structure.
  */
  GraphicObject * GraphicSheet::addObject()
  {
    TRACE;
    QAction * a=qobject_cast<QAction *>(sender());
    if(!a || a->data().type()!=QVariant::String) return nullptr;
    GraphicObject * obj=GraphicObjectFactory::instance()->create(a->data().toString());
    if(!obj) {
      qWarning("Unkwown class name %s", a->data().toString().toLatin1().data());
      return nullptr;
    }
    obj->setPrintAnchor(currentOrigin() + Point2D(1.0, 1.0));
    obj->setRemovable(true);
    addObject(obj);
    autoResizeContent();
    return obj;
  }

  /*!
    Remove all objects from the sheet matching type.
  */
  void GraphicSheet::removeObjects(const QString& type, bool force)
  {
    TRACE;
    GraphicObjectList wList=objects();
    for(GraphicObjectList::iterator it=wList.begin(); it!=wList.end(); ++it) {
      GraphicObject * obj=*it;
      if(obj->xml_tagName()==type) {
        removeObject(obj, force);
        delete obj; // Force immediate removal required for command line action
      }
    }
  }

  /*!
    Remove object obj from the sheet.

    If object is not removable (see QObject::isRemovable()), it will be removed only if force is true
    (default value is true).

    Return true if object correctly removed.
  */
  bool GraphicSheet::removeObject(GraphicObject * obj, bool force)
  {
    TRACE;
    if(force || obj->isRemovable()) {
      SelectionWidget * tmp=obj->selectionWidget();
      if(proxy()) obj->removeProperties(proxy());
      if(_currentActiveSelection==tmp)
        _currentActiveSelection=nullptr;
      if(tmp) {
        tmp->widget()->setSelected(nullptr);
        tmp->deleteLater();
      }
      obj->close();
      autoResizeContent();
      return true;
    } else
      return false;
  }

  /*!
    Remove all objects of the sheet.

    If force is true, remove all objects even those not removable (default value is true).
  */
  void GraphicSheet::clear(bool force)
  {
    TRACE;
    GraphicObjectList wList=objects();
    int nRemoved=wList.count();
    for(GraphicObjectList::iterator it=wList.begin();it!=wList.end();++it) {
      if(removeObject(*it, force))
        nRemoved--;
    }
    switch(nRemoved) {
    case 0:
      break;
    case 1:
      Message::warning(MSG_ID, tr("Clearing sheet"), tr("1 object is not removable"), Message::ok());
      break;
    default:
      Message::warning(MSG_ID, tr("Clearing sheet"), tr("%1 objects are not removable").arg(nRemoved), Message::ok());
    }
  }

  void GraphicSheet::showObject(GraphicObject *obj)
  {
    TRACE;
    connect(obj, SIGNAL(wantToBeSelected(GraphicObject*, Qt::KeyboardModifiers)),
            this, SLOT(selectObjects(GraphicObject*, Qt::KeyboardModifiers)));
    connect(obj, SIGNAL(positionChanged()), this, SLOT(moveObject()));
    connect(obj, SIGNAL(wantToStartDragging()), this, SLOT(startDragging()));
    connect(obj, SIGNAL(draggedTo(QPoint)), this, SLOT(dragObjects(QPoint)));
    connect(obj, SIGNAL(sizeChanged()), this, SLOT(resizeObject()));
    obj->setParent(widget());
    obj->setPos();
    obj->setSize();
    obj->show();
    if(_orderIndex>=0) {
      showOrderIndex(true);
    }
  }

  void GraphicSheet::moveObjects(Point2D delta)
  {
    TRACE;
    GraphicObjectList selWList=selectedObjects<GraphicObject>();
    GraphicObject * w;
    foreach(w, selWList) {
      Point2D pos(w->printXAnchor(), w->printYAnchor());
      pos+=delta;
      w->setPrintXAnchor(pos.x());
      w->setPrintYAnchor(pos.y());
      moveObject(w);
    }
  }

  /*!
    Move one object to its new position on screen and extend viewport if necessary.
    Coordinates are read from object data.
  */
  void GraphicSheet::moveObject(GraphicObject * obj)
  {
    TRACE;
    if(!obj) {
      obj=qobject_cast<GraphicObject *>(sender());
    }
    if(obj) {
      obj->setPos();
      if(proxy()) {
         obj->updateGeometryProperties(proxy());
      }
      SelectionWidget * selObj=obj->selectionWidget();
      if(selObj) {
        selObj->move(selObj->x(obj->x()), selObj->y(obj->y()));
      }
      //if(obj->_orderTip) obj->_orderTip->move(obj->x()-widget()->x(),obj->y()-widget()->y());
      autoResizeContent();
    }
  }

  /*!
    Resize one object and extend viewport if necessary.
    Size is read from object data.
  */
  void GraphicSheet::resizeObject()
  {
    TRACE;
    GraphicObject * obj=static_cast<GraphicObject *>(sender());
    obj->setSize();
    SelectionWidget * selObj=obj->selectionWidget();
    if(selObj) {
      selObj->resize(selObj->width(), selObj->height());
    }
    autoResizeContent();
  }

  /*!
    dragObject() is called every time a mouseMoveEvent occur for a child in dragging mode.
    The number of event per second may be dramatic. A direct dragging of the objects will force a repaint to
    occur every time which is quite slow and CPU demanding.

    Effective dragging of object is always delayed by 50 milliseconds. If another event occur less than 50 milliseconds
    after a first one, \a delta modified to be as closed as possible to the current mouse position but no
    drag action is processed. At end of the 50 milliseconds, dragObjectsDelayed() is called to effectively drag the
    selected objects.
  */
  void GraphicSheet::dragObjects(QPoint p)
  {
    TRACE;
    _mouseTrackingPoint=mapFromGlobal(p);
    if(!_dragDelay.isActive()) {
      _dragDelay.start();
    }
  }

  void GraphicSheet::dragObjectsDelayed()
  {
    TRACE;
    Point d=widget()->mapFromParent(_mouseTrackingPoint);
    d-=GraphicObject::draggingPos();
    Point2D dcm(d);
    dcm/=SciFigsGlobal::screenResolution();
    GraphicObjectList selWList=selectedObjects<GraphicObject>();
    GraphicObject * w;
    foreach(w, selWList) {
      w->drag(dcm);
      moveObject(w);
    }
    if(scrollSelection() && GraphicObject::isDragging()) {
      QTimer::singleShot(500, this, SLOT(dragObjectsDelayed()));
    }
  }

  void GraphicSheet::selectObjects(GraphicObject * obj, Qt::KeyboardModifiers m)
  {
    TRACE;
    if(proxy()) {
      proxy()->saveStates();
    }
    SelectionWidget * selObj;
    if(obj) {
      selObj=obj->selectionWidget();
    } else {
      selObj=nullptr;
    }
    if(selObj) {
      selObj->setState(SelectionWidget::Activated);
      emit activeSelectionChanged(obj);
      if(proxy()) {
        obj->addProperties(proxy());
      }
    } else if(obj) {
      selObj=addSelection(obj);
      selObj->show(); // Here and not in addSelection(), bug observed with Qt 4.6 under Mac, show() calls GraphicObject::enterEvent()
      if(proxy()) {
        obj->addProperties(proxy());
      }
    }
    if(!(m & Qt::ControlModifier)) {
      // Unselect all
      QList<SelectionWidget *> wList=findChildren<SelectionWidget>();
      SelectionWidget * tmp;
      foreach(tmp, wList) {
        if(tmp!=selObj) {
          tmp->widget()->setSelected(nullptr);
          if(proxy()) {
            tmp->widget()->removeProperties(proxy());
          }
          delete tmp;
        }
      }
      // Bug fixed 28/10/04, if no control key, all selection widgets are removed
      // Object eventually pointed by current avtive selection is deleted.
      _currentActiveSelection=nullptr;
      // Bug fixed for Qt 4.3 : selection mark remains visible if sheet not updated
  #if(QT_VERSION < QT_VERSION_CHECK(4, 4, 0))
      update();
  #endif
      // Bug fixed with Qt 4.4: update makes the selection quite slow with a lot of objects
    }
    if(_currentActiveSelection && _currentActiveSelection!=selObj) {
      _currentActiveSelection->setState(SelectionWidget::Selected);
    }
    _currentActiveSelection=selObj;
    if(_currentActiveSelection && _statusBar)
      _statusBar->showMessage(tr("Hit CTRL+M to display popup menu of the active object"));
    if(proxy()) {
      proxy()->restoreStates();
      proxy()->setValues();
    }
  }

  void GraphicSheet::selectObjects(const QRect& r, Qt::KeyboardModifiers m)
  {
    TRACE;
    if(!(m & Qt::ControlModifier)) {
      unselectAll();
    } else {
      if(proxy()) {
        proxy()->saveStates();
      }
    }
    GraphicObject * w;
    GraphicObjectList wList=objects();
    foreach(w, wList) {
      QPoint tl=w->mapToParent(w->rect().topLeft());
      QPoint br=w->mapToParent(w->rect().bottomRight());
      if(QRect(tl, br).intersects(r)) {
        selectObjects(w, Qt::ControlModifier);
      }
    }
    if(proxy()) {
      proxy()->restoreStates();
      proxy()->setValues();
    }
  }

  SelectionWidget * GraphicSheet::addSelection(GraphicObject * obj)
  {
    TRACE;
    SelectionWidget * selObj=new SelectionWidget(obj, this);
    selObj->setParent(widget());
    selObj->move(selObj->x(obj->x()), selObj->y(obj->y()));
    selObj->resize(selObj->width(), selObj->height());
    selObj->stackUnder(obj);
    return selObj;
  }

  /*!
    Used only by GraphContent::zoom, GraphContent::unzoom and GraphicObject::keyPopupMenu()
    These functions are not used in general. Some general code review may eventuelly remove
    this function.
  */
  GraphicObject * GraphicSheet::activeObject(const char * className)
  {
    TRACE;
    if(_currentActiveSelection) {
      GraphicObject * obj=_currentActiveSelection->widget();
      if(!className || obj->inherits(className))
        return obj;
    }
    return nullptr;
  }

  void GraphicSheet::autoResizeContent()
  {
    TRACE;
    // greatest right-bottom coordinates of child graphs;
    // iterate on all graphs
    GraphicObjectList objectList=objects();
    int maxRight=-INT_MAX, maxBottom=-INT_MAX;
    GraphicObject * obj;
    foreach(obj, objectList) {
      int right=obj->x()+obj->width()+qRound(obj->printRightMargin() * SciFigsGlobal::screenResolution());
      int bottom=obj->y()+obj->height()+qRound(obj->printBottomMargin() * SciFigsGlobal::screenResolution());
      if(right>maxRight)
        maxRight=right;
      if(bottom > maxBottom)
        maxBottom=bottom;
    }
    if(maxRight>0) {
      widget()->resize(maxRight, maxBottom);
    }
  }

  /*!
    Return the bottom of the sheet in cm. It is the maximum bottom of all objects.
  */
  double GraphicSheet::printBottom()
  {
    TRACE;
    GraphicObjectList objectList=objects();
    double maxv=0.0;
    GraphicObject * obj;
    foreach(obj, objectList) {
      double v=obj->printBottom();
      if(v>maxv)
        maxv=v;
    }
    return maxv;
  }

  /*!
    Return the right of the sheet in cm. It is the maximum right of all objects.
  */
  double GraphicSheet::printRight()
  {
    TRACE;
    GraphicObjectList objectList=objects();
    double maxv=0.0;
    GraphicObject * obj;
    foreach(obj, objectList) {
      double v=obj->printRight();
      if(v > maxv)
        maxv=v;
    }
    return maxv;
  }

  void GraphicSheet::printSize(double& x0, double& y0, double& x1, double& y1,
                               double& mx0, double& my0, double& mx1, double& my1)
  {
    TRACE;
    // iterate on all graphs
    x0=std::numeric_limits<double>::infinity();
    y0=std::numeric_limits<double>::infinity();
    x1=-std::numeric_limits<double>::infinity();
    y1=-std::numeric_limits<double>::infinity();
    mx0=std::numeric_limits<double>::infinity();
    my0=std::numeric_limits<double>::infinity();
    mx1=-std::numeric_limits<double>::infinity();
    my1=-std::numeric_limits<double>::infinity();
    GraphicObjectList objectList=objects();
    GraphicObject * obj;
    foreach(obj, objectList) {
      double left=obj->printLeft();
      double right=left+obj->printWidth();
      double top=obj->printTop();
      double bottom=top+obj->printHeight();
      double m_left=left-obj->printLeftMargin();
      double m_right=right+obj->printRightMargin();
      double m_top=top-obj->printTopMargin();
      double m_bottom=bottom+obj->printBottomMargin();
      if(left<x0) x0=left;
      if(right>x1) x1=right;
      if(top<y0) y0=top;
      if(bottom>y1) y1=bottom;
      if(m_left<mx0) mx0=m_left;
      if(m_right>mx1) mx1=m_right;
      if(m_top<my0) my0=m_top;
      if(m_bottom>my1) my1=m_bottom;
    }
  }

  void GraphicSheet::showOrderIndex(bool isOn)
  {
    TRACE;
    GraphicObjectList objectList=objects();
    GraphicObject * obj;
    if(isOn) {
      if(_statusBar)
        _statusBar->showMessage(tr("Select objects to change their order"));
      if(_orderIndex<0)
        _orderIndex=0;
      for(int i=0; i<objectList.count(); i++) {
        obj=objectList.at(i);
        obj->setOrderIndex(i + 1);
      }
    } else {
      if(_statusBar)
        _statusBar->showMessage("");
      _orderIndex=-1;
      foreach(obj, objectList) obj->setOrderIndex(-1);
    }
  }

  /*!
    Redraw all parts of all objects if autoDeepUpdate() is switched on, else do a normal update.
  */
  void GraphicSheet::deepUpdate()
  {
    TRACE;
    GraphicObjectList objectList=objects();
    GraphicObject * obj;
    foreach(obj, objectList) {
      obj->deepUpdate();
      if(obj->selectionWidget())
        obj->selectionWidget() ->update();
    }
  }

  /*!
    Redraw only some parts of the objects, generally not including the slow drawing actions.

    Use deepUpdate() or deepUpdateNow() to redraw all parts.
  */
  void GraphicSheet::update()
  {
    TRACE;
    GraphicObjectList objectList=objects();
    GraphicObject * obj;
    foreach(obj, objectList) {
      obj->update();
      if(obj->selectionWidget())
        obj->selectionWidget() ->update();
    }
  }

  int GraphicSheet::maximumResolution()
  {
    TRACE;
    GraphicObjectList objectList=objects();
    GraphicObject * obj;
    int printResolution=0;
    foreach(obj, objectList) {
      int val=obj->printResolution();
      if(printResolution < val)
        printResolution=val;
    }
    return printResolution;
  }

  /*!
    Export all objects of the sheet to a file, \a fileFormat may be any of the format accepted by
    QImage, SVG, PDF, PAGE or LAYER.

    If \a dpi is different from 0, it override the resolution specified in object properties.
  */
  void GraphicSheet::exportFile(QString fileName, QString fileFormat, int dpi)
  {
    TRACE;
    if(fileFormat.isEmpty()) {
      QFileInfo fi(fileName);
      fileFormat=fi.suffix().toUpper();
    }
    if(fileFormat.toLower()=="page") {
      fileSave(fileName);
    } else if(fileFormat.toLower()=="layer") {
      selectAll();
      saveLayers(fileName);
    } else {
      exportImage(fileName, fileFormat, dpi);
    }
  }

  /*!
    Export all objects of the sheet to an image file, \a imageFormat may be any of the format accepted by
    QImage. Additionally, 'SVG' and 'PDF' are accepted.

    If \a dpi is different from 0, it override the resolution specified in object properties.
  */
  void GraphicSheet::exportImage(QString fileName, QString imageFormat, int dpi)
  {
    TRACE;
    if(imageFormat.isEmpty()) {
      QFileInfo fi(fileName);
      imageFormat=fi.suffix().toUpper();
    }
    if(fileName.isEmpty()) {
      QFileInfo fi(_currentFile);
      fileName=Message::getSaveFileName(tr("Export image as ..."), GraphicObject::allImageFilter, fi.baseName());
    }
    if(!fileName.isEmpty()) {
      if(imageFormat.isEmpty()) {
        imageFormat=Settings::getFilter("filter-"+QString::number(qHash(GraphicObject::allImageFilter))).
                      section(' ', 0, 0);
      }
      if(imageFormat=="PDF") {
        QPrinter printer(QPrinter::HighResolution);
        if(dpi==0)
          printer.setResolution(maximumResolution());
        else
          printer.setResolution(dpi);
        printer.setFullPage(true);
        printer.setOutputFileName(fileName);
        printer.setOutputFormat(QPrinter::PdfFormat);
        QApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
        print(printer);
      } else if(imageFormat =="SVG") {
        printSvg(fileName, dpi);
      } else { // Pixel based image format
        QPixmap pixmap=image(dpi);
        if(!pixmap.isNull()) {
          // Save pixmap to disk
          if(!pixmap.save(fileName, imageFormat.toLatin1().data())) {
            Message::warning(MSG_ID, tr("Exporting as image"), tr("Failed to export image to file %1 with format %2")
                                .arg(fileName).arg(imageFormat.toLatin1().data()));
          }
        }
      }
    }
  }

  QPixmap GraphicSheet::image(int dpi)
  {
    TRACE;
    QApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
    QProgressDialog * progress=nullptr;
    GraphicObjectList objectList=objects();
    GraphicObject * obj;
    int nObjects=objectList.count();
    if(nObjects==0) {
      Message::warning(MSG_ID, tr("Drawing image"), tr("No object in sheet, nothing to draw."), Message::cancel());
      return QPixmap();
    }
    progress=new QProgressDialog(tr("Drawing ..."), QString(), 0, objectList.count(), this);
    progress->setWindowTitle(tr("Please wait"));
    progress->setMaximum(nObjects-1);

    if(dpi==0) dpi=maximumResolution();
    double resolution=dpi/2.54;
    double factor=resolution/SciFigsGlobal::screenResolution();
    // Find limits of actual sheet
    double x0, y0, x1, y1, mx0, my0, mx1, my1;
    // Scale fonts and eventually adjust size of object for printing
    FontScales originalFonts;
    for(int i=0; i<nObjects; i++) {
      obj=objectList.at(i);
      obj->scaleFonts(originalFonts, factor);
      obj->setPrintSize(resolution);
    }
    printSize(x0, y0, x1, y1, mx0, my0, mx1, my1);
    int totalw=qRound((mx1-mx0)*resolution);
    int totalh=qRound((my1-my0)*resolution);
    QPixmap pixmap;
    if(totalw>0 && totalh>0) {
      pixmap=QPixmap(totalw, totalh);
      QPainter p;
      p.begin(&pixmap);
      p.eraseRect(0, 0, totalw, totalh);
      p.save();
      p.translate(-qRound(mx0*resolution), -qRound(my0*resolution));
      // send print command to all graph positionning correctly like on the screen
      // iterate on all graphs
      for(int i=0; i<nObjects; i++) {
        obj=objectList.at(i);
        //App::log(tr("---- Drawing %1\n").arg(obj->objectName()) );
        obj->print(p, resolution, 0, 0, false);
        if(progress) progress->setValue(i);
      }
      p.restore();
      //SciFigsGlobal::footprint(p, resolution, totalw, totalh);
      p.end();
      if(_transparentMask) {
        progress->reset();
        QBitmap mask=SciFigsGlobal::colorMask(pixmap, 0xFFFFFFFF);
        p.begin(&mask);
        p.translate(-mx0 * resolution, -my0 * resolution);
        for(int i=0; i<nObjects; i++) {
          obj=objectList.at(i);
          App::log(tr("---- Setting mask %1\n").arg(obj->objectName()) );
          obj->print(p, resolution, 0, 0, true);
          if(obj->transparentMask())
            obj->updateMask();
          if(progress) progress->setValue(i);
        }
      }
    } else {
      Message::warning(MSG_ID, tr("Drawing image"), tr("Null size for image (%1, %2).").arg(totalw).arg(totalh), Message::cancel());
    }
    // Restore fonts and eventually screen size of objects
    for(int i=0; i<nObjects; i++) {
      obj=objectList.at(i);
      obj->restoreScaleFonts(originalFonts);
      obj->setPrintSize(SciFigsGlobal::screenResolution());
    }
    if(progress) {
      delete progress;
    }
    return pixmap;
  }

  void GraphicSheet::printSvg(const QString& fileName, int dpi)
  {
    TRACE;
  #ifdef NO_SVG
    Q_UNUSED(fileName)
    Q_UNUSED(dpi)
    App::log(1, tr("SciFigs not compiled with SVG support.\n");
  #else
    QApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
    QProgressDialog * progress=nullptr;
    GraphicObjectList objectList=objects();
    GraphicObject * obj;
    int nObjects=objectList.count();
    // TODO Batch hidden mode
    //if(!app.qsa().hiddenMode()) {
      progress=new QProgressDialog(tr("Drawing ..."), QString(), 0, objectList.count(), this);
      progress->setWindowTitle(tr("Please wait"));
      progress->setMaximum(nObjects-1);
    //}

    if(dpi==0) dpi=maximumResolution();
    double resolution=dpi/2.54;
    double factor=resolution/SciFigsGlobal::screenResolution();
    // Find limits of actual sheet
    double x0, y0, x1, y1, mx0, my0, mx1, my1;
    // Scale fonts and eventually adjust size of object for printing
    FontScales originalFonts;
    for(int i=0; i<nObjects; i++) {
      obj=objectList.at(i);
      obj->scaleFonts(originalFonts, factor);
      obj->setPrintSize(resolution);
    }
    printSize(x0, y0, x1, y1, mx0, my0, mx1, my1);
    int totalw=qRound((mx1 - mx0) * resolution);
    int totalh=qRound((my1 - my0) * resolution);
    QSvgGenerator gen;
    gen.setFileName(fileName);
    gen.setResolution(dpi);
    gen.setSize(QSize(totalw, totalh));
    QPainter p;
    p.begin(&gen);
    if(!_transparentMask) p.eraseRect(0, 0, totalw, totalh);
    p.save();
    p.translate(-mx0 * resolution, -my0 * resolution);
    // send print command to all graph positionning correctly like on the screen
    // iterate on all graphs
    for(int i=0; i<nObjects; i++) {
      obj=objectList.at(i);
      //App::log(tr("---- Drawing %1\n").arg(obj->objectName()) );
      obj->print(p, resolution, 0, 0, false);
      if(progress) progress->setValue(i);
    }
    p.restore();
    SciFigsGlobal::footprint(p, resolution, totalw, totalh);
    p.end();
    // Restore fonts and eventually screen size of objects
    for(int i=0; i<nObjects; i++) {
      obj=objectList.at(i);
      obj->restoreScaleFonts(originalFonts);
      obj->setPrintSize(SciFigsGlobal::screenResolution());
    }
    if(progress) {
      delete progress;
    }
  #endif
  }

  void GraphicSheet::print(int dpi)
  {
    TRACE;
    QPrinter printer(QPrinter::HighResolution);
    if(dpi==0)
      printer.setResolution(maximumResolution());
    else
      printer.setResolution(dpi);
    printer.setFullPage(true);
    if(Settings::printSetup(&printer)) {
      QApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
      print(printer);
    }
  }

  void GraphicSheet::print(QPrinter& printer)
  {
    TRACE;
    updatePageLimits(printer);
    QPainter p;
    if(p.begin(&printer)) {
      // send print command to all graph positionning correctly like on the screen
      // iterate on all graphs to plot new points
      GraphicObjectList objectList=objects();
      GraphicObject * obj;
      // Get page area and number of pages in x and y direction
      int wScrPage=qRound(printer.widthMM()*SciFigsGlobal::screenResolution()*0.1);
      int hScrPage=qRound(printer.heightMM()*SciFigsGlobal::screenResolution()*0.1);
      int wPtrPage=qRound(printer.widthMM()*printer.resolution()/25.4);
      int hPtrPage=qRound(printer.heightMM()*printer.resolution()/25.4);
      if(wScrPage==0 || hScrPage==0) {
        printer.abort();
        return ;
      }
      int xpages=1+widget()->width()/wScrPage;
      int ypages=1+widget()->height()/hScrPage;
      QProgressDialog * progress=nullptr;
      progress=new QProgressDialog(tr("Printing ..."), QString(), 0, objectList.count() * xpages * ypages, this);
      progress->setWindowTitle(tr("Please wait"));

      int iprogress=0;
      double dotpercm=printer.resolution()/2.54;
      for(int ix=0; ix<xpages; ix++) {
        for(int iy=0; iy<ypages; iy++) {
          if(ix>0 || iy>0) {
            printer.newPage();
          }
          QRect r(ix*wScrPage, iy*hScrPage, wScrPage, hScrPage);
          foreach(obj, objectList) {
            if(r.intersects(obj->geometry())) {
              obj->print(p, dotpercm, ix*wPtrPage, iy*hPtrPage, false);
            }
            if(progress) {
              ++iprogress;
              progress->setValue(iprogress);
            }
          }
        }
      }
      p.end();
      if(progress) {
        progress->setValue(iprogress);
        delete progress;
      }
    } else {
      Message::warning(MSG_ID, tr("Printing..."), tr("Error encountered while printing. If you are printing to a pdf file, "
                                                     "check file permissions."));
    }
  }

  /*!
    Refresh the limits of pages
  */
  void GraphicSheet::updatePageLimits(QPrinter& p)
  {
    TRACE;
    // Remove all existing limits
    PageLimit * limit;
    QList<PageLimit *> pageList=findChildren<PageLimit>();
    foreach(limit, pageList) delete limit;
    // Create new limits
    int wpage=qRound(p.widthMM()*SciFigsGlobal::screenResolution()*0.1);
    int hpage=qRound(p.heightMM()*SciFigsGlobal::screenResolution()*0.1);
    int w=wpage, h=hpage;
    while(w<widget()->width()) {
      limit=new PageLimit(PageLimit::Vertical, widget());
      limit->move(w, 0);
      limit->show();
      w += wpage;
    }
    while(h<widget()->height()) {
      limit=new PageLimit(PageLimit::Horizontal, widget());
      limit->move(0, h);
      limit->show();
      h += hpage;
    }
  }


  /*!
    Copy all properties of the sheet and included objects to the clipboard
  */
  void GraphicSheet::copyMakeUp()
  {
    TRACE;
    XMLSciFigs s;
    MakeUpFilter * d=new MakeUpFilter(this);
    d->setXml(s.saveString(this, XMLSciFigs::MakeUp));
    if(d->exec()==QDialog::Accepted) {
      QClipboard * cb=QApplication::clipboard();
      QMimeData * mime=new QMimeData;
      mime->setData("XMLSciFigs::SheetMakeUp", d->saveByteArray());
      cb->setMimeData(mime, QClipboard::Clipboard);
    }
    delete d;
  }

  /*!
    Paste properties from the clipboard if there are
  */
  void GraphicSheet::pasteMakeUp()
  {
    TRACE;
    static QString title=tr("Paste make-up");
    QClipboard * cb=QApplication::clipboard();
    const QMimeData * mime=cb->mimeData(QClipboard::Clipboard);
    if(mime->hasFormat("XMLSciFigs::SheetMakeUp")) {
      QByteArray makeup=mime->data("XMLSciFigs::SheetMakeUp");
      XMLErrorReport xmler(XMLErrorReport::Read);
      xmler.setTitle(title);
      XMLSciFigs s;
      xmler.exec(s.restoreByteArray(makeup, this, XMLSciFigs::MakeUp));
      deepUpdate();
    }
  }

  /*!
    Save all properties of the sheet and included objects
  */
  void GraphicSheet::saveMakeUp(QString fileName)
  {
    TRACE;
    XMLSciFigs s;
    MakeUpFilter * d=new MakeUpFilter(this);
    d->setXml(s.saveString(this, XMLSciFigs::MakeUp));
    if(d->exec()==QDialog::Accepted) {
      if(fileName.isEmpty()) {
        fileName=Message::getSaveFileName(tr("Save make-up as ..."),
                                                tr("Sheet make-up (*.mkup)"));
      }
      if(fileName.length() > 0) {
        d->saveFile(fileName);
      }
    }
    delete d;
  }

  /*!
    Restore all properties of the sheet and included objects from fileName
  */
  void GraphicSheet::restoreMakeUp(QString fileName)
  {
    TRACE;
    static QString title=tr("Open an exisiting make-up");
    if(fileName.isEmpty()) {
      fileName=Message::getOpenFileName(title, tr("Sheet make-up (*.mkup)"));
    }
    if(fileName.length() > 0) {
      MessageContext mc;
      XMLErrorReport xmler(XMLErrorReport::Read);
      xmler.setTitle(title);
      xmler.setFileName(fileName);
      QFileInfo fi(fileName);
      XMLSciFigs s;
      xmler.exec(s.restoreFile(fileName, this, XMLSciFigs::MakeUp));
    }
  }

  /*!
    Save layers of selected axiswindows
  */
  void GraphicSheet::saveLayers(QString fileName)
  {
    TRACE;
    QList<AxisWindow *> objectList=selectedObjects<AxisWindow>();
    if(objectList.isEmpty()) {
      if(!selectAll(tr("Save layers"))) return;
      objectList=selectedObjects<AxisWindow>();
    }
    if(fileName.isEmpty()) {
      fileName=Message::getSaveFileName(tr("Save layers as..."),
                                              tr("Graph layers (*.layer)"));
    }
    if(fileName.length() > 0) {
      if(objectList.count()==1) {
        objectList.first()->graphContent()->saveLayers(fileName);
      } else {
        QFileInfo fi(fileName);
        QDir d(fi.absolutePath());
        QString baseName;
        if(fi.suffix()=="layer") {
          baseName=d.absoluteFilePath(fi.completeBaseName() + "_%1.layer");
        } else {
          baseName=d.absoluteFilePath(fi.fileName() + "_%1");
        }
        for(int i=0; i<objectList.count(); i++) {
          objectList.at(i)->graphContent()->saveLayers(baseName.arg(i, 4, 10, QChar('0')));
        }
      }
    }
  }

  /*!
    Add layers to selected axiswindows
  */
  void GraphicSheet::appendLayers()
  {
    TRACE;
    QString title=tr("Append layers");
    QList<AxisWindow *> objectList=selectedObjects<AxisWindow>();
    if(objectList.isEmpty()) {
      if(!selectAll(title)) return;
      objectList=selectedObjects<AxisWindow>();
    }
    QStringList fileNames;
    fileNames=Message::getOpenFileNames(title, tr("Graph layers (*.layer)"));
    if(!fileNames.isEmpty()) {
      int n=objectList.count();
      if(fileNames.count()!=n) {
        if(Message::warning(MSG_ID, title, tr("The number of selected graphs (%1) is not the same as the number "
                                  "of files (%2). Do you want to continue?").arg(n).arg(fileNames.count()),
                                  Message::yes(), Message::no())==Message::Answer1) return;
        if(n>fileNames.count()) n=fileNames.count();
      }
      std::sort(fileNames.begin(), fileNames.end());
      for(int i=0; i<n; i++) {
        objectList.at(i)->graphContent()->appendLayers(fileNames.at(i));
      }
    }
  }

  void GraphicSheet::mousePressEvent (QMouseEvent * e)
  {
    TRACE;
    switch (e->button()) {
    case Qt::LeftButton:
      if(!_mouseTrackingRect) {
        _mouseTrackingRect=new QRect;
        _mouseTrackingRect->moveTopLeft(widget()->mapFromParent(e->pos()));
      }
      return;
    case Qt::RightButton: {
        QMenu m;
        m.addActions(actions());
        m.exec(mapToGlobal(e->pos()));
      }
      return;
    default:
      break;
    }
    QScrollArea::mousePressEvent(e);
  }

  bool GraphicSheet::isMouseTrackingActive() const
  {
    TRACE;
    return _mouseTrackingRect &&
           abs(_mouseTrackingRect->width())>5 &&
           abs(_mouseTrackingRect->height())>5;
  }

  void GraphicSheet::mouseReleaseEvent (QMouseEvent * e)
  {
    TRACE;
    switch (e->button()) {
    case Qt::LeftButton:
      if(isMouseTrackingActive()) {
        selectObjects(*_mouseTrackingRect, e->modifiers());
      } else {
        selectObjects(nullptr, e->modifiers());
      }
      if(_mouseTrackingRect) {
        delete _mouseTrackingRect;
        _mouseTrackingRect=nullptr;
        _selection->update();
      }
      return;
    default:
      break;
    }
    QScrollArea::mouseReleaseEvent(e);
  }

  void GraphicSheet::mouseMoveEvent(QMouseEvent * e)
  {
    TRACE;
    if(_mouseTrackingRect) {
      _mouseTrackingPoint=e->pos();
      scrollSelection();
    } else {
      QScrollArea::mouseMoveEvent(e);
    }
  }

  void GraphicSheet::resizeEvent(QResizeEvent *e)
  {
    TRACE;
    _selection->resize(width(), height());
    QScrollArea::resizeEvent(e);
  }

  /*!
    if _mouseTrackingPoint is on the edge, scroll the area.
    The action is repeated automatically if tracking selection rectangle.
    Return true if _mouseTrackingPoint is on the edge.
  */
  bool GraphicSheet::scrollSelection()
  {
    TRACE;
    const QPoint& p=_mouseTrackingPoint;
    bool scrolled=false;
    if(p.x()<10) {
      horizontalScrollBar()->setValue(horizontalScrollBar()->value()+p.x()-10);
      scrolled=true;
    } else if(p.x()>width()-10) {
      horizontalScrollBar()->setValue(horizontalScrollBar()->value()+p.x()-width()+10);
      scrolled=true;
    }
    if(p.y()<10) {
      verticalScrollBar()->setValue(verticalScrollBar()->value()+p.y()-10);
      scrolled=true;
    } else if(p.y()>height()-10) {
      verticalScrollBar()->setValue(verticalScrollBar()->value()+p.y()-height()+10);
      scrolled=true;
    }
    // If tracking selection rectangle, repeat scroll if the mouse remains on the edge
    if(_mouseTrackingRect) {
      _mouseTrackingRect->setBottomRight(widget()->mapFromParent(_mouseTrackingPoint));
      _selection->update();
      if(scrolled) {
        QTimer::singleShot(500, this, SLOT(scrollSelection()));
      }
    }
    return scrolled;
  }

  /*!
    Select next child (in the stack order, just after the current object).
    This function is called after a TAB action.
  */
  void GraphicSheet::selectNextChild()
  {
    TRACE;
    GraphicObjectList objectList=objects();
    if(objectList.isEmpty()) return;
    GraphicObject * cur;
    if(_currentActiveSelection)
      cur=_currentActiveSelection->widget();
    else
      cur=nullptr;
    GraphicObjectList::iterator it;
    for(it=objectList.begin(); it!=objectList.end(); ++it) {
      if(*it==cur) {++it;break;}
    }
    if(it!=objectList.end()) {
      selectObjects(*it, Qt::NoModifier);
    } else {
      selectObjects(objectList.first(), Qt::NoModifier);
    }
  }

  /*!
    Select next child (in the stack order, just after the current object).
    Compared to selectNextChild(), the current object remains selected.
    This function is called after a SHIFT+TAB action.
  */
  void GraphicSheet::addSelectNextChild()
  {
    TRACE;
    GraphicObjectList objectList=objects();
    if(objectList.isEmpty()) return;
    GraphicObject * cur;
    if(_currentActiveSelection)
      cur=_currentActiveSelection->widget();
    else
      cur=nullptr;
    GraphicObjectList::iterator it;
    for(it=objectList.begin(); it!=objectList.end(); ++it) {
      if(*it==cur) {++it;break;}
    }
    if(it!=objectList.end()) {
      selectObjects(*it, Qt::ControlModifier);
    } else {
      selectObjects(objectList.first(), Qt::ControlModifier);
    }
  }

  /*!
    Returns the coordinates of the current top left visible corner
    Used for example in Makepage to insert new objects at the top of the visible
    part and not at the top of the sheet.
  */
  Point2D GraphicSheet::currentOrigin()
  {
    TRACE;
    Point2D o;
    if(widget() ->width() > 0) {
      o.setX(-widget() ->x()/SciFigsGlobal::screenResolution());
      o.setY(-widget() ->y()/SciFigsGlobal::screenResolution());
    } else {
      o.setX(0);
      o.setY(0);
    }
    return o;
  }

  void GraphicSheet::selectAll()
  {
    TRACE;
    GraphicObjectList objectList=objects();
    GraphicObject * obj;
    if(proxy()) {
      proxy()->saveStates();
      proxy()->blockUpdates(true);
    }
    foreach(obj, objectList) selectObjects(obj, Qt::ControlModifier);
    if(proxy()) proxy()->blockUpdates(false);
  }

  /*!
    If not graph is selected, warns user and select all graphs if confirmed by user.
    It returns false if user answer no (cancel).
  */
  bool GraphicSheet::selectAll(QString actionTitle)
  {
    TRACE;
    if(Message::question(MSG_ID, actionTitle, tr("No graph is currently selected. The action you choose apply only "
                              "to the selected graphs. Do you want to select all of them?"),
                              Message::yes(), Message::no())==Message::Answer1) return false;
    selectAll();
    return true;
  }

  /*!
    Align two objects with repect to a relative position along X axis

    localPosition should be between 0 (left) and 1 (right)
  */
  void GraphicSheet::alignX(double localPosition)
  {
    TRACE;
    if(_currentActiveSelection) {
      GraphicObject * obj=_currentActiveSelection->widget();
      double refPos=obj->printLeft()+localPosition*obj->printWidth();
      GraphicObjectList objectList=selectedObjects<GraphicObject>();
      foreach(obj, objectList) {
        obj->setPrintXAnchor(refPos-localPosition*obj->printWidth());
        moveObject(obj);
      }
    }
  }

  /*!
    Align two objects with repect to a relative position along Y axis

    localPosition should be between 0 (bottom) and 1 (top)
  */
  void GraphicSheet::alignY(double localPosition)
  {
    TRACE;
    if(_currentActiveSelection) {
      GraphicObject * obj=_currentActiveSelection->widget();
      double refPos=obj->printTop()+localPosition*obj->printHeight();
      GraphicObjectList objectList=selectedObjects<GraphicObject>();
      foreach(obj, objectList) {
        obj->setPrintYAnchor(refPos-localPosition*obj->printHeight());
        moveObject(obj);
      }
    }
  }

  /*!
    Align objects to a grid relative to active object
  */
  void GraphicSheet::alignGrid()
  {
    TRACE;
    if(_currentActiveSelection) {
      AlignGridParameters * d=new AlignGridParameters(this);
      Settings::getWidget(d);
      if(d->exec()==QDialog::Accepted) {
        Settings::setWidget(d);

        Point2D step=d->step();
        GraphicObject * obj=_currentActiveSelection->widget();
        Point2D origin=Point2D(obj->printLeft(), obj->printTop());
        GraphicObjectList objectList=selectedObjects<GraphicObject>();
        foreach(obj, objectList) {
          Point2D pos=Point2D(obj->printLeft(), obj->printTop());
          pos-=origin;
          pos/=step;
          pos.round();
          pos*=step;
          pos+=origin;
          obj->setPrintLeft(pos.x());
          obj->setPrintTop(pos.y());
          moveObject(obj);
        }
      }
      delete d;
    }
  }


  /*!
    Bring to front all selected objects. It works also for stand-alone object because it is a classical
    re-organization of the children stack of a Qwidget.
  */
  void GraphicSheet::raiseSelection()
  {
    TRACE;
    GraphicObjectList objectList=selectedObjects<GraphicObject>();
    GraphicObject * obj;
    foreach(obj, objectList) {
      obj->raise();
      if(obj->selectionWidget())
        obj->selectionWidget() ->stackUnder(obj);
    }
    if(_orderIndex>=0)
      showOrderIndex(true);
  }

  /*!
    Send to back all selected objects. It works also for stand-alone object because it is a classical re-organization of the children stack of a Qwidget.
  */
  void GraphicSheet::lowerSelection()
  {
    TRACE;
    GraphicObjectList objectList=selectedObjects<GraphicObject>();
    GraphicObject * obj;
    foreach(obj, objectList) {
      obj->lower();
      obj->selectionWidget()->lower();
    }
    if(_orderIndex>=0)
      showOrderIndex(true);
  }

  void GraphicSheet::setOrderIndex(GraphicObject * obj)
  {
    TRACE;
    if(_orderIndex>=0) {
      GraphicObjectList objectList=objects();
      GraphicObjectList::iterator it;
      int i=0;
      for(it=objectList.begin();it!=objectList.end();++it, i++) {
        if(i==_orderIndex)
          break;
      }
      if(it==objectList.end()) {
        it=objectList.begin();
        _orderIndex=0;
      }
      GraphicObject * objAbove=*it;
      if(objAbove) {
        obj->stackUnder(objAbove);
        if(obj->selectionWidget())
          obj->selectionWidget() ->stackUnder(obj);
        _orderIndex++;
      }
      showOrderIndex(true);
    }
  }

  void GraphicSheet::addFileActions(QMenu * m, QToolBar * tb, bool shortcuts)
  {
    TRACE;
    QAction * a;

    a=new QAction(QIcon(":filenew.png"), tr("&New"), this);
    if(shortcuts) {
      a->setShortcut(tr("Ctrl+N"));
    }
    a->setStatusTip(tr("Empty sheet"));
    connect(a, SIGNAL(triggered()), this, SLOT(fileNew()));
    if(m)
      m->addAction(a);
    if(tb)
      tb->addAction(a);

    a=new QAction(QIcon(":fileopen.png"), tr("&Open"), this);
    if(shortcuts) {
      a->setShortcut(tr("Ctrl+O"));
    }
    a->setStatusTip(tr("Open an existing sheet and add it to the current one (*.page files)"));
    connect(a, SIGNAL(triggered()), this, SLOT(fileOpen()));
    if(m)
      m->addAction(a);
    if(tb)
      tb->addAction(a);

    QMenu * recentMenu=new QMenu(this);
    a=new QAction(QIcon(":fileopen.png"), tr("Open recent"), this);
    a->setStatusTip(tr("Open a recent existing sheet add it to the current one"));
    a->setMenu(recentMenu);
    connect(a, SIGNAL(triggered()), this, SLOT(openMostRecentFile()));
    connect(recentMenu, SIGNAL(aboutToShow()), this, SLOT(showOpenRecentMenu()));
    if(m)
      m->addAction(a);

    a=new QAction(QIcon(":filesave.png"), tr("&Save"), this);
    if(shortcuts) {
      a->setShortcut(tr("Ctrl+S"));
    }
    a->setStatusTip(tr("Save sheet to disk"));
    connect(a, SIGNAL(triggered()), this, SLOT(fileSave()));
    if(m)
      m->addAction(a);
    if(tb)
      tb->addAction(a);

    a=new QAction(tr("Save &As..."), this);
    a->setStatusTip(tr("Save sheet to disk under a new name"));
    connect(a, SIGNAL(triggered()), this, SLOT(fileSaveAs()));
    if(m)
      m->addAction(a);

    if(m)
      m->addSeparator();

    a=new QAction(tr("&Preferences"), this);
    a->setStatusTip(tr("Customize SciFigs"));
    connect(a, SIGNAL(triggered()), this, SLOT(setPreferences()));
    m->addAction(a);


    if(m)
      m->addSeparator();

    a=new QAction(QIcon(":fileprint.png"), tr("&Print"), this);
    if(shortcuts) {
      a->setShortcut(tr("Ctrl+P"));
    }
    a->setStatusTip(tr("Print sheet"));
    connect(a, SIGNAL(triggered()), this, SLOT(print()));
    if(m)
      m->addAction(a);
    if(tb)
      tb->addAction(a);

    a=new QAction(tr("&Export image"), this);
    if(shortcuts) {
      a->setShortcut(tr("Ctrl+E"));
    }
    a->setStatusTip(tr("Export sheet as image"));
    connect(a, SIGNAL(triggered()), this, SLOT(exportImage()));
    if(m)
      m->addAction(a);
  }

  void GraphicSheet::addEditActions(QMenu * m, QToolBar * tb, bool shortcuts)
  {
    TRACE;
    QAction * a;

    /*a=new QAction(QIcon(":editundo.png"), tr("&Undo"), this);
    if(shortcuts) a->setShortcut(tr("Ctrl+Z"));
    a->setStatusTip(tr("Undo last operation"));
    connect(a, SIGNAL(triggered()), this, SLOT(undo()));
    if(m) m->addAction(a);
    if(tb) tb->addAction(a);

    a=new QAction(QIcon(":editredo.png"), tr("Re&do"), this);
    a->setShortcut(tr("Ctrl+Shift+Z"));
    a->setStatusTip(tr("Redo last operation"));
    connect(a, SIGNAL(triggered()), this, SLOT(redo()));
    if(m) m->addAction(a);
    if(tb) tb->addAction(a);

    if(m) m->addSeparator();
    if(tb) tb->addSeparator();*/

    a=new QAction(QIcon(":editcut.png"), tr("Cu&t"), this);
    if(shortcuts) a->setShortcut(tr("Ctrl+X"));
    a->setStatusTip(tr("Cut the current selection's contents to the "
                         "clipboard"));
    connect(a, SIGNAL(triggered()), this, SLOT(cut()));
    if(m) m->addAction(a);
    if(tb) tb->addAction(a);

    a=new QAction(QIcon(":editcopy.png"), tr("&Copy"), this);
    if(shortcuts) a->setShortcut(tr("Ctrl+C"));
    a->setStatusTip(tr("Copy the current selection's contents to the "
                         "clipboard"));
    connect(a, SIGNAL(triggered()), this, SLOT(copy()));
    if(m) m->addAction(a);
    if(tb) tb->addAction(a);

    a=new QAction(tr("Copy &image"), this);
    if(shortcuts) a->setStatusTip(tr("Copy the sheet as an image to the clipboard"));
    connect(a, SIGNAL(triggered()), this, SLOT(copyImage()));
    if(m) m->addAction(a);

    a=new QAction(QIcon(":editpaste.png"), tr("&Paste"), this);
    if(shortcuts) a->setShortcut(tr("Ctrl+V"));
    a->setStatusTip(tr("Paste the clipboard's contents in the sheet"));
    connect(a, SIGNAL(triggered()), this, SLOT(paste()));
    if(m) m->addAction(a);
    if(tb) tb->addAction(a);

    if(m) m->addSeparator();
    if(tb) tb->addSeparator();

    a=new QAction(tr("&Select all"), this);
    if(shortcuts) a->setShortcut(tr("Ctrl+A"));
    a->setStatusTip(tr("Select all object of the sheet"));
    connect(a, SIGNAL(triggered()), this, SLOT(selectAll()));
    if(m) m->addAction(a);

    a=new QAction(QIcon(":ordertool.png"), tr("&Order"), this);
    if(shortcuts) a->setShortcut(tr("Ctrl+O"));
    a->setStatusTip(tr("Order objects of the sheet"));
    a->setCheckable(true);
    connect(a, SIGNAL(toggled(bool)), this, SLOT(showOrderIndex(bool)));
    if(m) m->addAction(a);
    if(tb) tb->addAction(a);
  }

  void GraphicSheet::addFormatActions(QMenu * m, bool shortcuts)
  {
    TRACE;
    QAction * a;

    a=new QAction(tr("&Copy make-up"), this);
    a->setStatusTip(tr("Copy the sheet format to the clipboard"));
    connect(a, SIGNAL(triggered()), this, SLOT(copyMakeUp()));
    if(m)
      m->addAction(a);

    a=new QAction(tr("&Paste make-up"), this);
    a->setStatusTip(tr("Set the sheet format from the clipboard"));
    connect(a, SIGNAL(triggered()), this, SLOT(pasteMakeUp()));
    if(m)
      m->addAction(a);

    a=new QAction(tr("&Save make-up"), this);
    if(shortcuts) {
      a->setShortcut(tr("Ctrl+U"));
    }
    connect(a, SIGNAL(triggered()), this, SLOT(saveMakeUp()));
    if(m)
      m->addAction(a);

    a=new QAction(tr("&Restore make-up"), this);
    if(shortcuts) {
      a->setShortcut(tr("Ctrl+Y"));
    }
    connect(a, SIGNAL(triggered()), this, SLOT(restoreMakeUp()));
    if(m)
      m->addAction(a);

    if(m) m->addSeparator();

    a=new QAction(tr("&Save layers"), this);
    a->setToolTip(tr("Save all layers of selected plots in multiple .layer files"));
    connect(a, SIGNAL(triggered(bool)) , this, SLOT(saveLayers()));
    if(m)
      m->addAction(a);

    a=new QAction(tr("&Append layers"), this);
    a->setToolTip(tr("Add layers to the selected plots from multiple .layer files"));
    connect(a, SIGNAL(triggered(bool)) , this, SLOT(appendLayers()));
    if(m)
      m->addAction(a);

    if(m) m->addSeparator();

    a=new QAction(tr("&Properties"), this);
    connect(a, SIGNAL(triggered()), this, SLOT(showProperties()));
    if(m)
      m->addAction(a);
  }

  void GraphicSheet::addInsertActions(QMenu * m, QToolBar * tb, bool shortcuts)
  {
    TRACE;
    QAction * a;
    // Automatic detection of available GraphicObject's
    QList<int> idList=GraphicObjectFactory::instance()->registeredIds();
    int id;
    foreach(id, idList) {
      GraphicObjectCreator * c=GraphicObjectFactory::instance()->creator(id);
      if(!c->menuTitle().isEmpty()) {
        a=new QAction(c->icon(), c->menuTitle(), this);
        if(shortcuts) {
          a->setShortcut(c->shortcut());
        }
        a->setStatusTip(c->toolTip());
        a->setData(c->tagName());
        connect(a, SIGNAL(triggered()), this, SLOT(addObject()));
        if(m) {
          m->addAction(a);
        }
        if(tb) {
          tb->addAction(a);
        }
      }
    }
  }

  void GraphicSheet::fileNew()
  {
    TRACE;
    clear(false);
    _currentFile="";
    emit currentFileChanged(_currentFile);
  }

  void GraphicSheet::fileOpen()
  {
    TRACE;
    QString fileName=Message::getOpenFileName(tr("Open an existing document"), tr("Page file (*.page)"));
    // Opening a file may take some time, refresh quickly after dialog disappeared
    QApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
    if(fileOpen(fileName)) {
      addRecentFile(fileName);
    }
  }

  /*!
    Load the .page file with a shift of \a dx and \a dy (in cm)
  */
  bool GraphicSheet::fileOpen(QString fileName, double dx, double dy)
  {
    TRACE;
    bool ret=false;
    if(fileName.length()>0) {
      QFileInfo fi(fileName);
      fileName=fi.absoluteFilePath();
      if(_statusBar) {
        _statusBar->showMessage(tr("Loading file ..."));
      }
      _loadedObjects.clear();
      XMLSciFigs s;
      XMLErrorReport xmler(XMLErrorReport::Read);
      xmler.setTitle(tr("Open page file"));
      xmler.setFileName(fileName);
      if(xmler.exec(s.restoreFile(fileName, this, XMLSciFigs::Page))) {
        // Move all loaded object by dx and dy
        if(dx!=0.0 || dy!=0.0) {
          GraphicObject * obj;
          foreach (obj, _loadedObjects) {
            obj->setPrintXAnchor(obj->printXAnchor()+dx);
            obj->setPrintYAnchor(obj->printYAnchor()+dy);
            moveObject(obj);
          }
        }
        _loadedObjects.clear();
        if(_currentFile.isEmpty()) {
          _currentFile=fileName;
          emit currentFileChanged(_currentFile);
        }
        ret=true;
      }
      if(_statusBar) {
        _statusBar->showMessage(QString());
      }
    }
    return ret;
  }

  void GraphicSheet::fileSave(QString fileName)
  {
    TRACE;
    _currentFile=fileName;
    fileSave();
    emit currentFileChanged(_currentFile);
  }

  void GraphicSheet::fileSave()
  {
    TRACE;
    if(_currentFile.isEmpty()) {
      fileSaveAs();
      return ;
    }
    // Save current file
    XMLSciFigs s;
    s.saveFile(_currentFile, this, XMLSciFigs::Page);
    addRecentFile(_currentFile);
  }

  void GraphicSheet::fileSaveAs()
  {
    TRACE;
    QString fileName=Message::getSaveFileName(tr("Save page as ..."), tr("Page file (*.page)"));
    if(fileName.isEmpty()) {
      return;
    }
    _currentFile=fileName;
    fileSave();
    emit currentFileChanged(_currentFile);
  }

  void GraphicSheet::undo()
  {
    TRACE;
  }

  void GraphicSheet::redo()
  {
    TRACE;
  }

  void GraphicSheet::cut()
  {
    TRACE;
    copy();
    if(_currentActiveSelection) {
      _currentActiveSelection->widget()->deleteObject();
    }
  }

  void GraphicSheet::copy()
  {
    TRACE;
    QClipboard * cb=QApplication::clipboard();
    XMLSciFigs s;
    QByteArray selObjects=s.saveByteArray(this, XMLSciFigs::Page | XMLSciFigs::Selection);
    QMimeData * mime=new QMimeData;
    mime->setData("XMLSciFigs::Page", selObjects);
    cb->setMimeData(mime, QClipboard::Clipboard);
  }

  void GraphicSheet::copyImage()
  {
    TRACE;
    QClipboard * cb=QApplication::clipboard();
    QPixmap pixmap=image();
    cb->setPixmap(pixmap, QClipboard::Clipboard);
  }

  void GraphicSheet::paste()
  {
    TRACE;
    QClipboard * cb=QApplication::clipboard();
    const QMimeData * mime=cb->mimeData(QClipboard::Clipboard);
    QByteArray objects;
    if(mime->hasFormat("XMLSciFigs::Page")) {
      objects=mime->data("XMLSciFigs::Page");
    } else if(mime->hasFormat("XMLSciFigs")) {        // Compatibility
      objects=mime->data("XMLSciFigs");
    } else {
      return;
    }
    XMLSciFigs s;
    _loadedObjects.clear();
    s.restoreByteArray(objects, this, XMLSciFigs::Page);
    // Move all pasted objects to current visible area
    // First first the to left corner of the pasted objects
    Point topLeft(std::numeric_limits<double>::infinity(),std::numeric_limits<double>::infinity());
    GraphicObject * obj;
    foreach (obj, _loadedObjects) {
      Point p(obj->printLeft(obj->printWidth()), obj->printTop(obj->printHeight()));
      if(p.x()<topLeft.x()) topLeft.setX(p.x());
      if(p.y()<topLeft.y()) topLeft.setY(p.y());
    }
    double dx=currentOrigin().x() - topLeft.x() + 1;
    double dy=currentOrigin().y() - topLeft.y() + 1;
    foreach (obj, _loadedObjects) {
      obj->setPrintXAnchor(obj->printXAnchor() + dx);
      obj->setPrintYAnchor(obj->printYAnchor() + dy);
      moveObject(obj);
    }
    // Select all pasted objects
    selectObjects(nullptr);
    foreach (obj, _loadedObjects) {
      selectObjects(obj, Qt::ControlModifier);
    }
    _loadedObjects.clear();
  }

  void GraphicSheet::setPreferences ()
  {
    TRACE;
    SciFigsPreferences * d=new SciFigsPreferences(this);
    Settings::getWidget(d);
    if(d->exec()==QDialog::Accepted) {
      Settings::setWidget(d);
      d->savePluginList();
    }
    delete d;
  }

  void GraphicSheet::xml_writeChildren(XML_WRITECHILDREN_ARGS) const
  {
    TRACE;
    GraphicObjectList objectList;
    XMLSciFigs * scifigsContext=static_cast<XMLSciFigs *>(context);
    if(scifigsContext->selection()) {
      objectList=selectedObjects<GraphicObject>();
    } else {
      objectList=objects();
    }
    GraphicObject * obj;
    static const QString key("objectName");
    XMLSaveAttributes att;
    QString& value=att.add(key);
    foreach (obj, objectList) {
      value=obj->objectName();
      obj->xml_save(s, context, att);
    }
  }

  XMLMember GraphicSheet::xml_member(XML_MEMBER_ARGS)
  {
    TRACE;
    static const QString tmp("objectName");
    XMLSciFigs * scifigsContext=static_cast<XMLSciFigs *>(context);
    XMLRestoreAttributeIterator it=attributes.find(tmp);
    if(it!=attributes.end()) {
      if(scifigsContext->data()) {
        GraphicObject * obj=GraphicObjectFactory::instance()->create(tag.toString());
        if(obj) {
          obj->setRemovable(true);
          addObject(obj);
          return XMLMember(obj);
        } else {
          App::log(tr("Unknown object type %1\n").arg(tag.toString()) );
          return XMLMember(XMLMember::Unknown);
        }
      } else {
        GraphicObjectList list=objects();
        GraphicObject * obj=list.findObject(tag.toString(), it.value().toString());
        if(obj) {
          return XMLMember(obj);
        } else {
          obj=list.findObject(tag.toString(), nullptr);
          if(obj) {
            if(Message::warning(MSG_ID,"Restore",
                                     tr("Cannot find an object of type %1 and named %2,\n"
                                        "but an object of type %3 and named %4 has been found.\n"
                                        "Do you want to restore properties to this object?").
                                     arg(tag.toString()).arg(it.value().toString()).
                                     arg(tag.toString()).arg(obj->objectName()),
                                     Message::yes(),Message::no(), true)==Message::Answer0) {
              return XMLMember(obj);
            } else return XMLMember(XMLMember::Unknown);
          } else {
            App::log(tr("Cannot find an object of type %1\n").arg(tag.toString()) );
            return XMLMember(XMLMember::Unknown);
          }
        }
      }
    } else if(scifigsContext->makeUp()) {
      return qobject_member(this, tag, attributes, context);
    }
    return XMLMember(XMLMember::Unknown);
  }

  void GraphicSheet::xml_polishChild(XML_POLISHCHILD_ARGS)
  {
    TRACE;
    Q_UNUSED(context)
    showObject(static_cast<GraphicObject *>(child));
    _loadedObjects.append(static_cast<GraphicObject *>(child));
  }

  bool GraphicSheet::xml_polish(XML_POLISH_ARGS)
  {
    TRACE;
    Q_UNUSED(context)
    autoResizeContent();
    update();
    return true;
  }

  void GraphicSheet::print(bool outputToFile, QString name, int dpi)
  {
    TRACE;
    QPrinter printer(QPrinter::HighResolution);
    if(dpi==0) printer.setResolution(maximumResolution());
    else printer.setResolution(dpi);
    printer.setFullPage(true);
    if(outputToFile) printer.setOutputFileName(name);
    else printer.setPrinterName(name);
    print(printer);
  }

  GraphicObject * GraphicSheet::object(QString objectName)
  {
    TRACE;
    return findChild<GraphicObject *>(objectName);;
  }

  GraphicObject * GraphicSheet::object(int index)
  {
    TRACE;
    GraphicObjectList objectList=objects();
    GraphicObjectList::iterator it;
    if(index>=0 && index<objectList.count()) {
      return objectList.at(index);
    } else {
      qWarning("Index out of bounds (%i)\n", index);
      return nullptr;
    }
  }

  void GraphicSheet::setTransparentMask(bool val)
  {
    TRACE;
    _transparentMask=val;
    //if(!val) clearMask();
  }

  void GraphicSheet::addRecentFile(const QString& fileName)
  {
    TRACE;
    if(!CoreApplication::instance()->isBatch()) {
      Settings::setHistory("RecentFiles", fileName, 20);
    }
  }

  void GraphicSheet::showOpenRecentMenu()
  {
    TRACE;
    QStringList docs=Settings::getHistory("RecentFiles");
    QMenu * m=qobject_cast<QMenu *>(sender());
    if(!m) {
      return;
    }
    m->clear();
    QAction * a;
    for(QStringList::iterator it=docs.begin(); it!=docs.end(); ++it) {
      QFileInfo fi(*it);
      if(fi.exists()) {
        a=new QAction(fi.completeBaseName(), this);
        a->setStatusTip(fi.absoluteFilePath());
        connect(a, SIGNAL(triggered()), this, SLOT(openRecentFile()));
        m->addAction(a);
      } else {
        Settings::removeHistory("RecentFiles", *it);
      }
    }
  }

  void GraphicSheet::openRecentFile()
  {
    TRACE;
    QAction * a=qobject_cast<QAction *>(sender());
    fileOpen(a->statusTip());
  }

  void GraphicSheet::openMostRecentFile()
  {
    TRACE;
    QStringList files=Settings::getHistory("RecentFiles");
    if(files.isEmpty()) {
      fileOpen();
    } else {
      fileOpen(files.first());
    }
  }

  void GraphicSheet::groupObjects()
  {
    TRACE;
    GraphicObjectList selWList=selectedObjects<GraphicObject>();
    if(selWList.count()>1) {
      GraphicObjectGroup * g=new GraphicObjectGroup(nullptr);
      g->setObjectName("Group");
      addObject(g);

      GraphicObject * w;
      bool allRemovable=true;
      foreach(w, selWList) {
        w->disconnect(this);
        g->addObject(w);
        if(!w->isRemovable()) {
          allRemovable=false;
        }
      }
      if(allRemovable) {
        g->setRemovable(true);
      }
      showObject(g);
      g->resize();
    }
  }

  void GraphicSheet::ungroupObjects()
  {
    TRACE;
    QList<GraphicObjectGroup *> selGList=selectedObjects<GraphicObjectGroup>();
    GraphicObjectGroup * g;
    int nUngrouped=selGList.count();
    foreach(g, selGList) {
      if(g->isRemovable()) {
        GraphicObjectList wList=g->objects();
        GraphicObject * w;
        foreach(w, wList) {
          w->setSelectable(true);
          w->setPrintLeft(g->printLeft()+w->printLeft());
          w->setPrintTop(g->printTop()+w->printTop());
          showObject(w);
        }
        removeObject(g);
        nUngrouped--;
      }
    }
    switch(nUngrouped) {
    case 0:
      break;
    case 1:
      Message::warning(MSG_ID, tr("Ungrouping"), tr("1 object is not removable"), Message::ok());
      break;
    default:
      Message::warning(MSG_ID, tr("Ungrouping"), tr("%1 objects are not removable").arg(nUngrouped), Message::ok());
    }
  }

  AxisWindow * GraphicSheet::addGraph()
  {
    TRACE;
    AxisWindow * w=new AxisWindow;
    w->setObjectName("graph");
    addObject(w, true);
    return w;
  }

  TextEdit * GraphicSheet::addText()
  {
    TRACE;
    TextEdit * te=new TextEdit;
    te->setTextAsData(true);
    addObject(te, true);
    return te;
  }

} // namespace SciFigs
