//-----------------------------------------------------------------------------
//  Copyright (C) 2002-2025 Thomas S. Ullrich
//
//  This file is part of "xyscan".
//
//  This file may be used under the terms of the GNU General Public License.
//  This project is free software; you can redistribute it and/or modify it
//  under the terms of the GNU General Public License.
//  
//  Author: Thomas S. Ullrich
//  Last update: Nov 5, 2025
//-----------------------------------------------------------------------------
#include <QPrinter>
#include <QtPrintSupport>
#include <QScreen>
#include <QList>
#include <QFile>
#include <QFileInfo>
#include <QPdfDocument>

#include "xyscanWindow.h"
#include "xyscanVersion.h"
#include "xyscanHelpBrowser.h"
#include "xyscanUpdater.h"
#include "xyscanGraphicsView.h"
#include "xyscanDataTable.h"
#include "xyscanErrorBarScanModeToolBox.h"
#include "xyscanScanTasksHandler.h"
#include "xyscanAbout.h"
#include "xyscanMarkerMaps.h"

#include <cmath>
#include <cfloat>
#include <iostream>

using namespace std;

#define PR(x) cout << #x << " = " << (x) << endl;

xyscanWindow::xyscanWindow(const QString& commandLineImageFile) : xyscanBaseWindow()
{
    //
    //  Complete the xyscan Window.
    //  The basic layout and all elements are already
    //  created in the base class xyscanBaseWindow. Here
    //  we mostly connect slots to signals and do some
    //  polishing.
    //
    connectMenuActions();
    connectToolBoxActions();
    createSettingMarkerActions();
    connectDockingWidgets();

    //
    //  Create sound "click" sound effect
    //
    mSoundEffect.setSource(QUrl::fromLocalFile(":/sounds/buttonClick.wav"));
    mSoundEffect.setVolume(0.2);
    mSoundEffect.setLoopCount(1);
    
    //
    //  Connect signals and slots not handled in tool box creators
    //
    connect(mImageView, &xyscanGraphicsView::fileDropped, this, &xyscanWindow::loadDroppedFile);
    connect(&mUpdater, &xyscanUpdater::userReminded, this, &xyscanWindow::userWasRemindedOfAvailableUpdate);
    connect(mDeleteLastAction, &QAction::triggered, mDataTable, &xyscanDataTable::deleteLast);
    connect(mDeleteAllAction, &QAction::triggered, mDataTable, &xyscanDataTable::deleteAll);
    connect(mSignificantDigitsActionGroup, &QActionGroup::triggered, this, &xyscanWindow::updateSignificantDigits);
    
    //
    //  Shortcuts to Coordinates and Axis Settings pages
    //  that are the most used ones.
    //
    QShortcut *shortcut1 = new QShortcut(Qt::CTRL | Qt::Key_C, mToolBox);
    connect(shortcut1, &QShortcut::activated, [this](){mToolBox->setCurrentIndex(3);});
    QShortcut *shortcut2 = new QShortcut(Qt::CTRL | Qt::Key_A, mToolBox);
    connect(shortcut2, &QShortcut::activated, [this](){mToolBox->setCurrentIndex(1);});

    //
    //  Shortcuts (for developer only)
    //
    QShortcut *mCalibrationLinPlotShortcut = new QShortcut(QKeySequence(tr("Ctrl+Shift+Left")), this);
    connect(mCalibrationLinPlotShortcut, &QShortcut::activated, [this](){this->loadCalibrationPlot(false);});
    QShortcut *mCalibrationLogPlotShortcut = new QShortcut(QKeySequence(tr("Ctrl+Shift+Right")), this);
    connect(mCalibrationLogPlotShortcut, &QShortcut::activated, [this](){this->loadCalibrationPlot(true);});

    //
    //  Initialize remaining data member
    //
    enablePathMeasuring(false);
    mMeasurePathLength = 0;

    mCurrentPixmap = 0;
    mCurrentShadowImage = 0;
    for (int i=0; i<4; i++)
        mMarkerPixel[i] = mMarkerPlotCoordinate[i] = 0;
    mClearHistoryAction->setDisabled(true);
    mScanTasksHandler = 0;
    
    //
    //  Read the preferences from file.
    //  This comes late in the init phase
    //  by purpose since some parts need to be
    //  created first. Don't move this line.
    //
    loadSettings();

    //
    //  Disable all buttons that are useless 
    //  at startup when no image is loaded ...
    //
    mSetLowerXButton->setDisabled(true);
    mSetUpperXButton->setDisabled(true);
    mSetLowerYButton->setDisabled(true);
    mSetUpperYButton->setDisabled(true);
    mLogXRadioButton->setDisabled(true);
    mLogYRadioButton->setDisabled(true);    
    mAngleSpinBox->setDisabled(true);    
    mScaleSpinBox->setDisabled(true);
    mErrorBarScanModeToolBox->reset();
    mErrorBarScanModeToolBox->disable();
    mSaveAction->setDisabled(true);
    mPrintAction->setDisabled(true);
    mShowPrecisionAction->setDisabled(true);
    mDeleteLastAction->setDisabled(true);
    mDeleteAllAction->setDisabled(true);
    mEditCommentAction->setDisabled(true);
    mEditCrosshairColorAction->setDisabled(true);
    mEditMarkerColorAction->setDisabled(true);
    mEditPathColorAction->setDisabled(true);
    mStartMeasureButton->setDisabled(true);
    enableSignificantDigitsMenu(false);
    mScreenScanIsOn = false;
    mMeasuringIsOn = false;

    //
    //  Give window some reasonable initial size
    //
    this->resize(800, 480);
    this->setMinimumHeight(450); // good size ensuring all toolbox items are visible
    
    //
    //  Event filter:
    //  We filter (i) graphics scene events here
    //  to handle mouse events to, e.g move the cursor
    //  and (ii) receive all events from the
    //  application to intercept tool tips in order
    //  to ignore them in case the user disabled
    //  tooltips (Help menu). See also main().
    //  We also catch events from the Zoom docking
    //  window to allow to update the view when resized.
    //
    mImageScene->installEventFilter(this);
    mZoomDock->installEventFilter(this);
    qApp->installEventFilter(this);
    
    //
    //  Auto check for updates. We ignore all error messages
    //  in that process. Only if an update is present a info
    //  dialog pops up. To not annoy the user we do this only
    //  3-4 days after the last reminder.
    //
    QDateTime now = QDateTime::currentDateTime();
    if (mLastReminderTime.daysTo(now) > 3) {
        mUpdater.setNextCheckIsSilent(true);
        checkForUpdates();
    }

    //
    //   Deal with the case where a file to open was passed via the command
    //   line. Handling of command line args is done in xyscanMain.
    //
    //  The issue here is that if we launch the openFromFile()
    //  right away the whole xyscan window is not build yet and
    //  prevents the proper resizing of the main window around
    //  the image (see loadPixmap()). Then the image shows up
    //  only partly visible. So we need to delay and use a simple
    //  one shot timer for that. 0.5 s delay is more than enough
    //  to popup xyscan and short enough for the user to not
    //  feel the delay.
    //
    if (!commandLineImageFile.isEmpty()) {
        if (!QFile::exists(commandLineImageFile)) {
            cout << tr("File '%1' does not exist.").arg(commandLineImageFile).toStdString() << endl;
            exit(2);
        }
        QTimer::singleShot(500, this, [this, file = commandLineImageFile]() {openFromFile(file);});
    }
}

xyscanWindow::~xyscanWindow() { /* no op */ }

void xyscanWindow::about() 
{
    mAboutDialog->show();
}

void xyscanWindow::checkForUpdates()
{
    mUpdater.checkForNewVersion(QUrl("https://rhig.physics.yale.edu/~ullrich/software/xyscan/xyscanLatestVersion.xml"));
}

bool xyscanWindow::checkForUnsavedData()
{
    //
    //  Allows the user to save the data before continuing.
    //  Method returns true when data was saved or explicitly
    //  discarded or false when Cancel was pressed.
    //
    if (!(mCurrentPixmap && mDataTable->rowCount() && !mDataTable->dataSaved())) return true;
    
    int ret = QMessageBox::warning(this, "xyscan",
                                   tr("Do you want to save the data you scanned?\n"
                                      "The content of the data table will be lost if you don’t save it."),
                                   QMessageBox::Save | QMessageBox::Discard |
                                   QMessageBox::Cancel, QMessageBox::Save);
    if (ret == QMessageBox::Save) {
        save();
        return true;
    }
    else if (ret == QMessageBox::Cancel)
        return false;
    else
        return true;
}

void xyscanWindow::clearHistory() 
{
    //
    //  Remove all stored recent files from submenu (File/Open Recent)
    //
    for (int i = 0; i < mRecentFiles.size(); ++i) {
        mRecentFileAction[i]->setVisible(false);
    }
    mRecentFiles.clear();
    mClearHistoryAction->setDisabled(true);
}

void xyscanWindow::clearMeasuredPath()
{
    QPainterPath empty;
    mMeasurePathLine->setPath(empty);
    mMeasurePathLength = 0;
    updateMeasureDisplay();
}

void xyscanWindow::closeEvent(QCloseEvent* e)
{
    //
    //  Reimplement, otherwise pressing the x button on the
    //  window frame closes the applications and we have
    //  no chance for storing the settings and checking
    //  for unsaved data.
    //
    finish(); 
    e->ignore();   // exit initiated in finish() or not at all
}

void xyscanWindow::connectDockingWidgets()
{
    // Zoom window
    connect(mZoomSpinBox, &QDoubleSpinBox::valueChanged, this, &xyscanWindow::zoomScale);
    // Histogram window
    connect(mHistogramModeComboBox, qOverload<int>(&QComboBox::currentIndexChanged), this, [this]{ updateHistogramDisplay(); });
    connect(mHistogramDock, &QDockWidget::visibilityChanged, this, [this]{ updateHistogramDisplay(); });
}

void xyscanWindow::connectMenuActions()
{
    connect(mOpenAction, &QAction::triggered, this, &xyscanWindow::open);
    connect(mClearHistoryAction, &QAction::triggered, this, &xyscanWindow::clearHistory);
    connect(mSaveAction, &QAction::triggered, this, &xyscanWindow::save);
    connect(mPrintAction, &QAction::triggered, this, &xyscanWindow::print);
    connect(mFinishAction, &QAction::triggered, this, &xyscanWindow::finish);
    connect(mPasteImageAction, &QAction::triggered, this, &xyscanWindow::pasteImage);
    connect(mEditCrosshairColorAction, &QAction::triggered, this, &xyscanWindow::editCrosshairColor);
    connect(mEditMarkerColorAction, &QAction::triggered, this, &xyscanWindow::editMarkerColor);
    connect(mEditPathColorAction, &QAction::triggered, this, &xyscanWindow::editPathColor);
    connect(mEditCommentAction, &QAction::triggered, this, &xyscanWindow::editComment);
    connect(mShowPrecisionAction, &QAction::triggered, this, &xyscanWindow::showPrecision);
    connect(mScreenScanAction, &QAction::toggled, this, &xyscanWindow::enableScreenScan);
    connect(mAboutAction, &QAction::triggered, this, &xyscanWindow::about);
    connect(mHelpAction, &QAction::triggered, this, &xyscanWindow::help);
    connect(mCheckForUpdatesAction, &QAction::triggered, this, &xyscanWindow::checkForUpdates);
    for (int i = 0; i < mMaxRecentFiles; ++i)
        connect(mRecentFileAction[i], &QAction::triggered, this, &xyscanWindow::openRecent);
}

void xyscanWindow::connectToolBoxActions()
{
    //
    //  Adjustment Tool Box
    //
    connect(mAngleSpinBox, &QDoubleSpinBox::valueChanged, this, &xyscanWindow::rotateImage);
    connect(mScaleSpinBox, &QDoubleSpinBox::valueChanged, this, &xyscanWindow::scaleImage);

    //
    // Axis Settings Tool Box
    //
    connect(mLogXRadioButton, &QRadioButton::toggled, this, [this]{ updateWhenAxisScaleChanged(); });
    connect(mLogYRadioButton, &QRadioButton::toggled, this, [this]{ updateWhenAxisScaleChanged(); });

    //
    //  Measure Tool Box
    //
    connect(mStartMeasureButton, &QPushButton::toggled, this, &xyscanWindow::enableMeasuring);
    connect(mMeasureAngleRadiansCheckBox, &QCheckBox::toggled, this, [this]{ updateMeasureDisplay(); });
    connect(mMeasureScaleComboBox, qOverload<int>(&QComboBox::currentIndexChanged), this, [this]{ updateMeasureDisplay(); });
    connect(mMeasureUserScaleField, &QLineEdit::textEdited, this, [this]{ updateMeasureDisplay(); });
    connect(mClearMeasuredPathButton, &QPushButton::pressed, this, &xyscanWindow::clearMeasuredPath);
    connect(mMeasurePathCheckBox , &QCheckBox::checkStateChanged,
            [this](){this->enablePathMeasuring(mMeasurePathCheckBox->isChecked());});
    //
    //  Define shortcut and actions for the normalization of the user scale
    //  Doing the normalization on return is bad since the user might enter
    //  a proper scale already. Hence we use the Shift modifier or alternatively
    //  Ctrl+N.
    //
    connect(mMeasureUserScaleField, &QLineEdit::returnPressed, [this](){if (QGuiApplication::queryKeyboardModifiers()== Qt::ShiftModifier) this->normalizeUserScale();});
    QShortcut *shortcutN = new QShortcut(Qt::CTRL | Qt::Key_N, mMeasureItem);
    connect(shortcutN, &QShortcut::activated, [this](){this->normalizeUserScale();});

    QShortcut *shortcutM = new QShortcut(Qt::CTRL | Qt::Key_M, mMeasureItem);
    connect(shortcutM, &QShortcut::activated, [this](){this->enableMeasuring(!mMeasuringIsOn);});

}

void xyscanWindow::createSettingMarkerActions()
{
    connect(mSetLowerXButton, &QPushButton::clicked, [this](){this->setPlotCoordinateValueForMarker(mXLower);});
    connect(mSetUpperXButton, &QPushButton::clicked, [this](){this->setPlotCoordinateValueForMarker(mXUpper);});
    connect(mSetLowerYButton, &QPushButton::clicked, [this](){this->setPlotCoordinateValueForMarker(mYLower);});
    connect(mSetUpperYButton, &QPushButton::clicked, [this](){this->setPlotCoordinateValueForMarker(mYUpper);});

    QShortcut *shortcut1 = new QShortcut(Qt::CTRL | Qt::Key_1, mSetLowerXButton);
    QShortcut *shortcut2 = new QShortcut(Qt::CTRL | Qt::Key_2, mSetUpperXButton);
    QShortcut *shortcut3 = new QShortcut(Qt::CTRL | Qt::Key_3, mSetLowerYButton);
    QShortcut *shortcut4 = new QShortcut(Qt::CTRL | Qt::Key_4, mSetUpperYButton);

    connect(shortcut1, &QShortcut::activated, [this](){this->setPlotCoordinateValueForMarker(mXLower);});
    connect(shortcut2, &QShortcut::activated, [this](){this->setPlotCoordinateValueForMarker(mXUpper);});
    connect(shortcut3, &QShortcut::activated, [this](){this->setPlotCoordinateValueForMarker(mYLower);});
    connect(shortcut4, &QShortcut::activated, [this](){this->setPlotCoordinateValueForMarker(mYUpper);});    
}



void xyscanWindow::editComment()
{
    bool ok;
    QString txt = QInputDialog::getText(this, tr("xyscan - Comment"),
                                        tr("The following comment will be written "
                                           "together with the data when saved to file or when printed:\t"),
                                        QLineEdit::Normal, mDataTable->userComment(), &ok);
    if (ok) mDataTable->setUserComment(txt);
}

void xyscanWindow::editCrosshairColor()
{
    QColor newColor = QColorDialog::getColor (mCrosshairColor, this, tr("Crosshair Color")); // title doesn't show on macOS
    if (newColor.isValid()) 
        mCrosshairColor = newColor;
    mCrosshairV->setPen(QPen(mCrosshairColor));
    mCrosshairH->setPen(QPen(mCrosshairColor));
}

void xyscanWindow::editMarkerColor()
{
    QColor newColor = QColorDialog::getColor (mMarkerColor, this, tr("Marker Color"));
    if (!newColor.isValid()) return;
    
    mMarkerColor = newColor;
    mMeasureLine->setPen(QPen(mMarkerColor));
    QImage imgpm(marker_cross_xpm);
    int index = imgpm.color(0) == 0 ? 1 : 0;
    imgpm.setColor(index, mMarkerColor.rgb());
    QPixmap newpix = QPixmap::fromImage(imgpm);
    mPointMarker->setPixmap(newpix);
}

void xyscanWindow::editPathColor()
{
    QColor newColor = QColorDialog::getColor (mPathColor, this, tr("Path Color"));
    if (!newColor.isValid()) return;

    mPathColor = newColor;
    mMeasurePathLine->setPen(QPen(mPathColor));
}


void xyscanWindow::enableScreenScan(bool val)
{
    //
    //  If true load an empty (dummy) pixmap to trigger the initialization
    //  that is happening in loadPixmap(). We also need to disable the
    //  zoom window since it's view is tied to the scene in the window
    //  (the loaded pixmap) and not the screen behind the xyscan window.
    //  The xyscan window will turn transparent with an opacity of 70%.
    //
    
    if (val && !checkForUnsavedData()) {
        mScreenScanAction->setChecked(false);
        return;
    }
    
    if (val) {
        QPixmap *pm = new QPixmap(":images/empty.png");
        mCurrentSource = QString("Screen Scan");
        loadPixmap(pm);
        mMessageLabel->setText(tr("Ready for screen scan. No markers set."));
    }
    else {
        if (mCurrentSource == QString("Screen Scan")) {   // no new image loaded, onlt screen scan off
            mCurrentSource = QString();
            mMessageLabel->setText(tr("Ready"));
        }
        setWindowTitle("xyscan");
    }
    
    setWindowOpacity(val ? 0.7 : 1);
    if (mZoomDock->isVisible()) mZoomDock->setHidden(val);
    mZoomDock->toggleViewAction()->setDisabled(val);
    if (mHistogramDock->isVisible()) mHistogramDock->setHidden(val);
    mHistogramDock->toggleViewAction()->setDisabled(val);

    mScreenScanIsOn = val;
}

void xyscanWindow::enablePathMeasuring(bool state)
{
    mMeasurePathDisplay->setEnabled(state);
    mClearMeasuredPathButton->setEnabled(state);
}

void xyscanWindow::enableMeasuring(bool state)
{
    //
    //  Switches measuring process on/off
    //

    mMeasuringIsOn = state;
    
    if (state) {
        mStartMeasureButton->setText(tr("Stop Measuring"));
        if (!mStartMeasureButton->isChecked()) mStartMeasureButton->setChecked(true);
        mMessageLabel->setText(tr("Measuring tool is on."));
        mMeasuredDistanceLabel->setVisible(true);
        measurementController(true);
        if (mAngleSpinBox) mAngleSpinBox->setEnabled(false);  // no rotating while measureing is on
        if (mScaleSpinBox) mScaleSpinBox->setEnabled(false);  // and no scaling
    }
    else {
        mStartMeasureButton->setText(tr("Start Measuring"));
        mPointMarker->hide();
        mMeasureLine->hide();
        if (mStartMeasureButton->isChecked()) mStartMeasureButton->setChecked(false);
        mMessageLabel->setText(tr("Ready"));
        mMeasuredDistanceLabel->setVisible(false);
        int nmarkers = numberOfMarkersSet();
        if (mAngleSpinBox && !nmarkers) mAngleSpinBox->setEnabled(true);  // switch back on unless markers are set
        if (mScaleSpinBox && !nmarkers) mScaleSpinBox->setEnabled(true);  // ditto
    }
    clearMeasuredPath();
}

void xyscanWindow::enableSignificantDigitsMenu(bool state)
{
    if (state) {
        QLocale locale;
        double valX = fabs(locale.toDouble(mUpperXValueField->text()));
        unsigned int minDigitsX = (valX != 0) ? static_cast<unsigned int>(log10(valX))+1 : 1;
        double valY = fabs(locale.toDouble(mUpperYValueField->text()));
        unsigned int minDigitsY = (valY != 0) ? static_cast<int>(log10(valY))+1 : 1;
        unsigned int minDigits = max(minDigitsX, minDigitsY);

        mSignificantDigits = minDigits + 2;
        mSignificantDigitsActionGroup->setEnabled(true);
        for (unsigned int i = 0; i < mMaxSignificantDigits; ++i) {
            if (i < minDigits)
                mSignificantDigitsAction[i]->setVisible(false);
            else
                mSignificantDigitsAction[i]->setVisible(true);
        }
        updatePixelDisplay();
        updateMeasureDisplay();
        updatePlotCoordinateDisplay();
        updateStatusBarCoordinateDisplay();
    }
    else {
        mSignificantDigitsActionGroup->setDisabled(true);
        for (int i = 0; i < mMaxSignificantDigits; ++i) {
            mSignificantDigitsAction[i]->setVisible(true);
        }
        mSignificantDigits = 6;
    }
    
    mSignificantDigitsAction[mSignificantDigits]->setChecked(true);
}

void xyscanWindow::ensureCursorVisible()
{
    QRectF rect(mCrosshairH->pos().x()-25, mCrosshairV->pos().y()-25, 50, 50);
    mImageView->ensureVisible(rect);
}

bool xyscanWindow::eventFilter(QObject *obj, QEvent *event)
{
    if (!event) return false;

    //
    // Early handling of tooltip events: if tooltips are enabled, don't consume;
    // if disabled, swallow them. Keep this at the top so later branches don't interfere.
    //
    if (event->type() == QEvent::ToolTip) {
        if (!mShowTooltipsAction || mShowTooltipsAction->isChecked())
            return false;   // let Qt show the tooltip
        return true;        // swallow when disabled
    }
    
    //
    //  This is to avoid confusion on how to set axis marker values.
    //  When user clicks on the read-only field the same action as when
    //  pushing the 'Set' button takes place: the popup window opens
    //  to set the value. This has to be done here since the line edit
    //  fiels is read-only and signals don't work.
    //
    if (event->type() == QEvent::MouseButtonRelease) {
        if (obj == mLowerXValueField && mSetLowerXButton->isEnabled()) {
            setPlotCoordinateValueForMarker(mXLower);
        }
        else if (obj == mLowerYValueField && mSetLowerYButton->isEnabled()) {
            setPlotCoordinateValueForMarker(mYLower);
        }
         else if (obj == mUpperXValueField && mSetUpperXButton->isEnabled()) {
            setPlotCoordinateValueForMarker(mXUpper);
        }
        else if (obj == mUpperYValueField && mSetUpperYButton->isEnabled()) {
            setPlotCoordinateValueForMarker(mYUpper);
        }
    }

    //
    //  First we see if this is an FileOpen event passed
    //  along from the application. On the Mac this means
    //  the user opened an image file from the finder. 
    //
    if (obj == qApp) {
        if (event->type() == QEvent::FileOpen) {
            QFileOpenEvent* fileOpenEvent = dynamic_cast<QFileOpenEvent*>(event);
            if (fileOpenEvent) {
                if (!checkForUnsavedData()) return false;
                QString fname = fileOpenEvent->file();
                if(fname.isEmpty()) return false;
                this->openFromFile(fname);
                return true;    // handled it
            }
            else
                return false;
        }
        else
            return false;
    }

    //
//
    // If method retuns true, the event stops here otherwise its
    // passed down. Here we catch all dispatched events from the
    // image scene (canvas). 
    // 
    static bool leftMousePressedNearCrosshair = false;
    QGraphicsSceneMouseEvent *mevt;
    
    if (obj == mImageScene) {
        
        if (event->type() == QEvent::KeyPress) {
            handleKeyEvent(dynamic_cast<QKeyEvent*>(event));  // to complex to handle here
            return true;
        } 
        else if (event->type() == QEvent::GraphicsSceneMousePress) {
            mevt = dynamic_cast<QGraphicsSceneMouseEvent*>(event);  
            update();
            QPointF mpos = mevt->scenePos();
            QPointF cpos(mCrosshairH->pos().x(), mCrosshairV->pos().y());
            double d = (mpos.x()-cpos.x())*(mpos.x()-cpos.x())
                + (mpos.y()-cpos.y())*(mpos.y()-cpos.y());
            if (d < 50) leftMousePressedNearCrosshair = true;
            return true;
        }
        else if (event->type() == QEvent::GraphicsSceneMouseRelease) {
            leftMousePressedNearCrosshair = false;
            return true;
        }        
        else if ( (event->type() == QEvent::GraphicsSceneMouseMove && leftMousePressedNearCrosshair ) || event->type() == QEvent::GraphicsSceneMouseDoubleClick) {
            mevt = dynamic_cast<QGraphicsSceneMouseEvent*>(event);    
            //
            //  In principle straighforward except for the case
            //  where we scan for errors (x, y or both) and one
            //  line is hidden. The idea is that for an x-error scan
            //  mCrosshairH is hidden and stays at const y and
            //  for an y error scan mCrosshairV is hidden and stays
            //  at const x. So when lines are hidden (which is
            //  only the case during the error scan) we have to keep
            //  one coordinate const.
            //
            if (mCrosshairH->isVisible() && mCrosshairV->isVisible()) {
                mCrosshairH->setPos(mevt->scenePos());
                mCrosshairV->setPos(mevt->scenePos());
            }
            else if (mCrosshairH->isVisible() && !mCrosshairV->isVisible()) {
                QPointF pos = QPointF(mCrosshairV->pos().x(), mevt->scenePos().y());
                mCrosshairV->setPos(pos);
                mCrosshairH->setPos(pos);
            }
            else if (!mCrosshairH->isVisible() && mCrosshairV->isVisible()) {
                QPointF pos(mevt->scenePos().x(), mCrosshairH->pos().y());
                mCrosshairH->setPos(pos);
                mCrosshairV->setPos(pos);
            }
            
            //
            //  Handle the measurement tool
            //
            if (mMeasuringIsOn) {
                if (event->type() == QEvent::GraphicsSceneMouseDoubleClick)
                    measurementController();
                else
                    updateMeasureDisplay();
            }
            
            //
            //  Update all displays and fields impacted
            //
            updatePixelDisplay();
            updatePlotCoordinateDisplay();
            updateStatusBarCoordinateDisplay();
            ensureCursorVisible();
            updateZoomDisplay();
            updateHistogramDisplay();
            return true;
        }
        else {
            return false;
        }
    }

    //
    //   Filter events from the zoom docking window.
    //   In fact we do not filter anything here but
    //   throw it right back. All we do is to catch
    //   resize events and update the display.
    //
    if (obj == mZoomDock && event->type() == QEvent::Resize) { updateZoomDisplay(); }

    return QObject::eventFilter(obj, event);  // standard event processing
}

void xyscanWindow::finish() 
{
    //
    //  Gracefully end xyscan. 
    //  Check for unsaved data and write settings/preferences.
    //
    if (!checkForUnsavedData()) return;
    writeSettings();
    QApplication::exit(0);
}

void xyscanWindow::handleKeyEvent(QKeyEvent* k)
{
    static bool previousShiftPressed = false;
    static bool previousAltPressed = false;
    static int  previousKey  = 0;
    bool shiftPressed = false;
    bool altPressed = false;
    int  key = 0;
    
    //
    //  There's a slight problem in Qt when Shift+arrow
    //  or Alt-arrow is pressed continuously: the Shift/Alt
    //  key gets lost.
    //  We repair that by checking for 'AutoRepeat' and
    //  remembering the Shift/Alt key state before 'AutoRepeat'
    //  was signaled.
    //
    if (k->isAutoRepeat() && !(k->key() == Qt::Key_Space)) {
        shiftPressed = previousShiftPressed;
        altPressed = previousAltPressed;
        key = previousKey;
    }
    else {
        shiftPressed = k->modifiers() & Qt::ShiftModifier;
        altPressed = k->modifiers() & Qt::AltModifier;
        key = k->key();
    }
    previousShiftPressed = shiftPressed;
    previousAltPressed = altPressed;
    previousKey = key;
    
    double dx_fine = 0.1;   // very fine move (alt+arrow)
    double dy_fine = 0.1;
    double dx_step = 1;     // simple move (arrow)
    double dy_step = 1;
    double dx_jump = 10;    // fast (shift+arrow)
    double dy_jump = 10;
    
    double dx, dy;
    if (shiftPressed) {
        dx = dx_jump;
        dy = dy_jump;
    }
    else if (altPressed) {
        dx = dx_fine;
        dy = dy_fine;
    }
    else {
        dx = dx_step;
        dy = dy_step;
    }

    switch(key) {
    case Qt::Key_Left:
        if (mCrosshairV) {
            mCrosshairV->setPos(mCrosshairV->pos() + QPointF(-dx, 0));
            mCrosshairH->setPos(mCrosshairH->pos() + QPointF(-dx, 0));
        }
        break;
    case Qt::Key_Right:
        if (mCrosshairV) {
            mCrosshairV->setPos(mCrosshairV->pos() + QPointF(dx, 0));
            mCrosshairH->setPos(mCrosshairH->pos() + QPointF(dx, 0));
        }
        break;
    case Qt::Key_Up:
        if (mCrosshairH) {
            mCrosshairV->setPos(mCrosshairV->pos() + QPointF(0, -dy));
            mCrosshairH->setPos(mCrosshairH->pos() + QPointF(0, -dy));
        }
        break;
    case Qt::Key_Down:
        if (mCrosshairH) {
            mCrosshairV->setPos(mCrosshairV->pos() + QPointF(0, dy));
            mCrosshairH->setPos(mCrosshairH->pos() + QPointF(0, dy));
        }
        break;
    case Qt::Key_Space:
        if (readyForScan()) {
            mSoundEffect.play();  // sound to indicate succesfull scan
            if (mMeasuringIsOn) enableMeasuring(false);

            //
            //   xyscanScanTasksHandler takes care of the scan sequence
            //
            if (!mScanTasksHandler) {
                mScanTasksHandler = new xyscanScanTasksHandler(this);
            }
            bool go = mScanTasksHandler->performTask();
            if (!go) { // nothing left to be done

                //
                //  Delete scan handler - done it's job
                //
                delete mScanTasksHandler;
                mScanTasksHandler = nullptr;

                //
                //  Enable all buttons that make sense once
                //  at least one data point is scanned ...
                //
                mSaveAction->setDisabled(false);
                mPrintAction->setDisabled(false);
                mDeleteLastAction->setDisabled(false);
                mDeleteAllAction->setDisabled(false);
            }
        }
        else {
            QMessageBox::information(0, "xyscan",
                                     tr("Cannot scan yet. Not sufficient information available to perform "
                                        "the coordinate transformation. You need to define 2 points on the "
                                        "x-axis (x1 & x1) and 2 on the y-axis (y1 & y2)."));
        } // ready to scan
        break;
    default:
            k->ignore();
            break;
    }
    
    //
    //  Update coordinate display if an arrow key was pressed 
    // 
    if (key == Qt::Key_Left || key == Qt::Key_Right || key == Qt::Key_Up || key == Qt::Key_Down) {
        if (mMeasuringIsOn) updateMeasureDisplay();
        updatePixelDisplay();
        updatePlotCoordinateDisplay();
        updateStatusBarCoordinateDisplay();
        ensureCursorVisible();
        updateZoomDisplay();
        updateHistogramDisplay();
    }
}

void xyscanWindow::help() 
{
    if (mHelpBrowser) {
        mHelpBrowser->show();
        mHelpBrowser->raise();
    }
    else
        QMessageBox::warning( 0, "xyscan",
                              tr("Sorry no help available.\n"
                              "Help files are missing in this installation. "
                              "Check your installation and reinstall if necessary."));    
}

void xyscanWindow::loadCalibrationPlot(bool isLog)
{
    //
    //  Load a plot that is useful to check calibration
    //  and various other things and set the markers
    //  right away. This is only for developing and testing.
    //  Plot is stored internally with the icons and logos.
    //  This slot is activated by shortcut Ctrl+Shift+C/L.
    //
    
    resetMarker();
    
    //
    //  Load pixmap
    //
    string name = ":images/calibration";
    name += (isLog ? "LogPlot" : "LinPlot");
    name += (QGuiApplication::primaryScreen()->devicePixelRatio() > 1 ? "@2x.png" : ".png");
    QPixmap *pixmap = new QPixmap(name.c_str());
    mCurrentSource = (isLog ? "Logarithmic Test Plot" : "Linear Test Plot");  // no tr()
    loadPixmap(pixmap);
    
    //
    //  Set marker
    //
    mMarkerPixel[mXLower] = 80.2;
    mMarkerPixel[mXUpper] = 719.4;
    mMarkerPixel[mYLower] = 518.2;
    mMarkerPixel[mYUpper] = 57.5;
    mMarkerPlotCoordinate[mXLower] = isLog ? 1e-4 : 0;
    mMarkerPlotCoordinate[mXUpper] = isLog ? 1 : 100;
    mMarkerPlotCoordinate[mYLower] = isLog ? 0.1 : 0;
    mMarkerPlotCoordinate[mYUpper] = isLog ? 1000 : 100;
    mMarker[mXLower]->setPos(mMarkerPixel[mXLower], 0);
    mMarker[mXUpper]->setPos(mMarkerPixel[mXUpper], 0);
    mMarker[mYLower]->setPos(0, mMarkerPixel[mYLower]);
    mMarker[mYUpper]->setPos(0, mMarkerPixel[mYUpper]);
    mLogXRadioButton->setChecked(isLog);
    mLogYRadioButton->setChecked(isLog);
    for (int i=mXLower; i<=mYUpper; i++) mMarker[i]->setVisible(true);
    
    QLocale locale;
    mLowerXValueField->setText(locale.toString(mMarkerPlotCoordinate[mXLower],'g', mSignificantDigits));
    mUpperXValueField->setText(locale.toString(mMarkerPlotCoordinate[mXUpper],'g', mSignificantDigits));
    mLowerYValueField->setText(locale.toString(mMarkerPlotCoordinate[mYLower],'g', mSignificantDigits));
    mUpperYValueField->setText(locale.toString(mMarkerPlotCoordinate[mYUpper],'g', mSignificantDigits));
    
    mStartMeasureButton->setDisabled(false);
    mMeasureScaleComboBox->setDisabled(false);
    auto model = dynamic_cast<QStandardItemModel*>(mMeasureScaleComboBox->model());
    auto item = model->item(mPlotUnits, 0);
    item->setEnabled(true);
    mAngleSpinBox->setDisabled(true);  // rotating would void marker position
    mScaleSpinBox->setDisabled(true);  // same for scaling (zoomig in/out)

    mDataTable->resetAll();            // all back to original state

    enableSignificantDigitsMenu(true);

    updatePlotCoordinateDisplay();
    updatePixelDisplay();
    updateStatusBarCoordinateDisplay();
    
    mMessageLabel->setText(tr("Ready to scan. Press space bar to record current cursor position."));
}

void xyscanWindow::loadDroppedFile(QString filename)
{
    if (!checkForUnsavedData()) return;
    openFromFile(filename);
}

void xyscanWindow::loadPixmap(QPixmap* pixmap)
{
    //
    //  All pixmaps/images displayed in xyscan go through here
    //  no matter how they were loaded.
    //
    QApplication::setOverrideCursor(Qt::WaitCursor);
    if (mCurrentPixmap) delete mCurrentPixmap;
    if (mCurrentShadowImage) delete mCurrentShadowImage;

    //
    //  If Screen scan is on, switch it off
    //
    if (mScreenScanIsOn) {
        enableScreenScan(false);
        mScreenScanAction->setChecked(false);
    }

    //
    //  If Measure tool is on, switch it off
    //
    if (mMeasuringIsOn) enableMeasuring(false);
    
    //
    //  Add pixmap to scene with lowest z to not
    //  obscure cursors and lines.
    //
    mImageAngle = 0;
    mImageScale = 1;
    mCurrentPixmap = mImageScene->addPixmap(*pixmap);
    mCurrentPixmap->setZValue(0);
    mImageScene->setSceneRect(0, 0,
                              pixmap->width()/pixmap->devicePixelRatio(),
                              pixmap->height()/pixmap->devicePixelRatio());
    update();
    
    //
    //  Create QImage copy (shadow) of pixmap for histogramming
    //
    mCurrentShadowImage = new QImage(pixmap->toImage()); 

    //
    //  Not bigger than the desktop
    //
    QSize theSize;
    QList<QScreen*> theScreens = QApplication::screens();
    if (theScreens.size())  theSize = theScreens[0]->availableSize();
    if (theSize.width() > width() + 50 &&
        theSize.height() > height() + 50)
        show();
    else
        showMaximized();

    //
    //  Show crosshair and position
    //
    mCrosshairV->setVisible(true);
    mCrosshairH->setVisible(true);
    mCrosshairH->setPos(pixmap->width()/2/pixmap->devicePixelRatio(),
                        pixmap->height()/2/pixmap->devicePixelRatio());
    mCrosshairV->setPos(pixmap->width()/2/pixmap->devicePixelRatio(),
                        pixmap->height()/2/pixmap->devicePixelRatio());
    updatePixelDisplay();
    updatePlotCoordinateDisplay(); // sets display to N/A if not ready
    updateStatusBarCoordinateDisplay();
    updateZoomDisplay();
    updateHistogramDisplay();
   
    //
    //  No markers shown yet
    //
    resetMarker();
    
    //
    //  Remove all traces of the previous scan
    //
    mAngleSpinBox->setValue(mImageAngle);
    mScaleSpinBox->setValue(mImageScale);
    mDataTable->resetAll();        // clear table
   
    //
    //  Enable all buttons that make sense to use
    //  once the image is loaded (or not).
    //
    mSetLowerXButton->setDisabled(false);
    mSetUpperXButton->setDisabled(false);
    mSetLowerYButton->setDisabled(false);
    mSetUpperYButton->setDisabled(false);
    mLogXRadioButton->setDisabled(false);
    mLogYRadioButton->setDisabled(false);
    mErrorBarScanModeToolBox->reset();
    mErrorBarScanModeToolBox->enable();
    mEditCommentAction->setDisabled(false);
    mEditCrosshairColorAction->setDisabled(false);
    mEditMarkerColorAction->setDisabled(false);
    mEditPathColorAction->setDisabled(false);
    mShowPrecisionAction->setDisabled(false);
    mAngleSpinBox->setDisabled(false);
    mScaleSpinBox->setDisabled(false);   
    mPrintAction->setDisabled(true);
    mStartMeasureButton->setDisabled(false);
    mMeasureScaleComboBox->setCurrentIndex(mPixelUnits);
    enableSignificantDigitsMenu(false);
    mHistogramDock->toggleViewAction()->setDisabled(false);


    //
    //  When plot is loaded pixels and user units should be allowed
    //  but not plot coordinates that require all markers to be set
    //
    mMeasureScaleComboBox->setDisabled(false);
    auto model = dynamic_cast<QStandardItemModel*>(mMeasureScaleComboBox->model());
    auto item = model->item(mPlotUnits, 0);
    item->setEnabled(false);

    mMessageLabel->setText(tr("Image loaded, no markers set."));

    //
    //  Add info to Plot Adjustement window
    //
    QString txt = tr("<html><table border=\"0\">" 
                     "<tr><td>Info:"
                     "<tr><td align=\"right\">&nbsp;&nbsp;&nbsp;&nbsp;Dimension:&nbsp;<td>%1 x %2"
                     "<tr><td align=\"right\">&nbsp;&nbsp;&nbsp;&nbsp;Depth:&nbsp;<td>%3 bit"
                     "<tr><td align=\"right\">&nbsp;&nbsp;&nbsp;&nbsp;Alpha channel:&nbsp;<td>%4"
                     "<tr><td align=\"right\">&nbsp;&nbsp;&nbsp;&nbsp;Device pixel ratio:&nbsp;<td>%5"
                     "</table></html>")
    .arg(pixmap->width()).arg(pixmap->height()).arg(pixmap->depth()).arg(pixmap->hasAlphaChannel() ? tr("Yes"):tr("No")).arg(pixmap->devicePixelRatio());
    mPlotInfoLabel->setText(txt);
    
    QApplication::restoreOverrideCursor();
    
    //
    //  Resize main window to fit pixmap 
    //  but not exceed screen size.
    //
    const int offset = 10;
    QSize offsetSize(10,10);  // small offset
    QSize mainSize = size();
    QSize viewSize = mImageView->size();
    QSize pixSize  = pixmap->size()/pixmap->devicePixelRatio();
    QApplication::restoreOverrideCursor();
    QSize deltaSize = pixSize-viewSize;
    
    QSize newSize = mainSize+deltaSize+offsetSize;
    
    QRect maxRect = QGuiApplication::primaryScreen()->availableGeometry();
    maxRect.setWidth(maxRect.width()-5*offset);
    maxRect.setHeight(maxRect.height()-5*offset);
    
    if (newSize.width() > maxRect.width()) newSize.setWidth(maxRect.width());
    if (newSize.height() > maxRect.height()) newSize.setHeight(maxRect.height());

    resize(newSize);
    
    QString newTitle("xyscan - ");
    QString fname = mCurrentSource;
    int i = fname.lastIndexOf('/');
    fname.remove(0, i+1);
    newTitle += fname;
    setWindowTitle(newTitle);
    
    updatePlotCoordinateDisplay();
    updateStatusBarCoordinateDisplay();
    update();
}

void xyscanWindow::loadSettings()
{
    QSettings settings(QSettings::NativeFormat, QSettings::UserScope, "tu", "xyscan");
    move(settings.value("xyscan/position", QPoint(75, 45)).toPoint());
    bool toolTips = settings.value("xyscan/showToolTips", true).toBool();
    mShowTooltipsAction->setChecked(toolTips);
    mOpenFileDirectory = settings.value("xyscan/lastOpenFileDirectory", QDir::homePath()).toString();
    mSaveFileDirectory = settings.value("xyscan/lastSaveFileDirectory", QDir::homePath()).toString();
    mRecentFiles = settings.value("xyscan/recentFiles").toStringList();
    
    while (mRecentFiles.size() > mMaxRecentFiles)
        mRecentFiles.removeLast();
    for (int i = 0; i < mRecentFiles.size(); ++i) {
        QString text = tr("&%1  %2").arg(i + 1).arg(QFileInfo(mRecentFiles[i]).fileName());
        mRecentFileAction[i]->setText(text);
        mRecentFileAction[i]->setData(mRecentFiles[i]);
        mRecentFileAction[i]->setVisible(true);
    }
    for (int j = mRecentFiles.size(); j < mMaxRecentFiles; ++j)
        mRecentFileAction[j]->setVisible(false);

    if (mRecentFiles.size()) mClearHistoryAction->setDisabled(false);
    
    //
    //   Crosshair, marker, and path colors
    //
    QVariant var = settings.value("xyscan/crosshairColor", mCrosshairColor);
    QColor theColor = var.value<QColor>();
    if (theColor.isValid()) mCrosshairColor = theColor;
    mCrosshairV->setPen(QPen(mCrosshairColor));
    mCrosshairH->setPen(QPen(mCrosshairColor));

    var = settings.value("xyscan/pathColor", mPathColor);
    theColor = var.value<QColor>();
    if (theColor.isValid()) mPathColor = theColor;
    mMeasurePathLine->setPen(QPen(mPathColor));

    var = settings.value("xyscan/markerColor", mMarkerColor);
    theColor = var.value<QColor>();
    if (theColor.isValid()) mMarkerColor = theColor;
    mMeasureLine->setPen(QPen(mMarkerColor));
    QImage imgpm(marker_cross_xpm);
    int index = imgpm.color(0) == 0 ? 1 : 0;
    imgpm.setColor(index, mMarkerColor.rgb());
    QPixmap newpix = QPixmap::fromImage(imgpm);
    mPointMarker->setPixmap(newpix);

    //
    //  Set default far enough in the past so it triggers an initial check
    //
    QDateTime defaultDateTime = QDateTime::currentDateTime();
    mLastReminderTime = settings.value("xyscan/lastReminderTime",
                                       defaultDateTime.addDays(-10)).toDateTime();
}

pair<double, double> xyscanWindow::measureDistance(QPointF& startPoint, QPointF& endPoint)
{
    double dx = 0;
    double dy = 0;

    if (mMeasureScaleComboBox->currentIndex() == mPixelUnits) {
        dx = endPoint.x()-startPoint.x();
        dy = startPoint.y()-endPoint.y();  // switch y
    }
    else if (mMeasureScaleComboBox->currentIndex() == mPlotUnits) {
        QPointF startPlotCoordinate = scan(&startPoint);
        QPointF endPlotCoordinate = scan(&endPoint);
        dx = endPlotCoordinate.x()-startPlotCoordinate.x();
        dy = endPlotCoordinate.y()-startPlotCoordinate.y();
    }
    else if (mMeasureScaleComboBox->currentIndex() == mUserUnits) {
        dx = endPoint.x()-startPoint.x();
        dy = startPoint.y()-endPoint.y();  // switch y
        dx *= mMeasureUserScale;
        dy *= mMeasureUserScale;
    }

    return make_pair(dx, dy);
}

void xyscanWindow::measurementController(bool ignorePathSettings)
{
    //
    //   We get here after essentially after a double click
    //   with the mouse as this requires some action that cannot
    //   be handled in the plain updateMeasureDisplay().
    //
    if (!mMeasuringIsOn) return;

    QPointF pos(mCrosshairH->pos().x(), mCrosshairV->pos().y());
    QPointF truePos(pos);
    pos -= QPointF(mPointMarker->pixmap().width()/2+mPointMarkerOffset, mPointMarker->pixmap().height()/2+mPointMarkerOffset);
    mPointMarker->setPos(pos);
    mPointMarker->show();
    mMeasureLine->show();

    if (mMeasurePathCheckBox->isChecked() && !ignorePathSettings) {
        QPainterPath path = mMeasurePathLine->path();
        if (path.elementCount() == 0)
            path.moveTo(truePos);

        QPointF startPos = path.currentPosition();
        path.lineTo(truePos);
        mMeasurePathLine->setPath(path);
        QPointF endPos = path.currentPosition();
        mMeasurePathLine->show();
        pair<double, double> d = measureDistance(startPos, endPos);
        mMeasurePathLength += sqrt(d.first*d.first+d.second*d.second);
    }
    updateMeasureDisplay();
}

void xyscanWindow::normalizeUserScale()
{
    if (!mMeasuringIsOn) return;
    
    QLocale locale;
    bool ok;
    double a = locale.toDouble(mMeasureUserScaleField->text(), &ok);
    if (!ok || a < 0) return;
    double b = locale.toDouble(mMeasureDRDisplay->text(), &ok);
    if (!ok) return;
    QString str = locale.toString(a*a/b, 'g', mSignificantDigits); // dx, dy, or dr were already scaled by 'a', hence a^2
    mMeasureUserScaleField->setText(str);
    updateMeasureDisplay();
}

int  xyscanWindow::numberOfMarkersSet()
{
    int n = 0;
    for (int i=0; i<4; i++)
        if (mMarker[i]->isVisible()) n++;
    return n;
}

void xyscanWindow::open() 
{
    if (!checkForUnsavedData()) return;
    
    //
    //  We get here when the user uses the open action
    //  in the file menu.
    //  Check which image formats are supported and use
    //  them as filters for the file dialog.
    //  Here we add pdf as valid format, which we handle
    //  separatly using QtPDF.
    //
    QString formats(tr("Images ("));
    QList<QByteArray> imgFormats = QImageReader::supportedImageFormats();
    imgFormats.append("pdf");
    for (int i=0; i< imgFormats.size() ; i++) {
        formats += tr("*.%1").arg(QString(imgFormats[i]));
        if (i < imgFormats.size()-1) formats += tr(" ");
    }
    formats += tr(")");
    
    QString filename = QFileDialog::getOpenFileName(this, tr("Open File"),
                                                    mOpenFileDirectory,
                                                    formats);   
    if (filename.isEmpty()) return;
    
    openFromFile(filename);
}

void xyscanWindow::openFromFile(const QString& filename)
{
    //
    //  All pixmap/images associated to a file are loaded
    //  are handled here. For images pasted from the clipboard
    //  see pasteImage().
    //
    
    mOpenFileDirectory = filename;
    mOpenFileDirectory.truncate(mOpenFileDirectory.lastIndexOf('/'));
    
    if (!QFile::exists(filename)) {
        QMessageBox::warning(0, "xyscan", tr("File '%1' does not exist.").arg(filename));
        return;
    }
    
    QMimeDatabase mimeDb;
    QMimeType mtype = mimeDb.mimeTypeForFile(filename);
    bool isPdf = mtype.name() == QString("application/pdf");
    
    //
    //  Create the pixel map of the plot.
    //  PDF plots are handled by QtPDF.
    //
    QPixmap	*pixmap = 0;
    if (isPdf) {
        
        //
        //   Load PDF file
        //
        QPdfDocument document(this);
        if (document.load(filename) != QPdfDocument::Error::None) {
            QMessageBox::warning(0, "xyscan", tr("Cannot open PDF file '%1'.").arg(filename));
            return;
        }
        
        //
        //   Check the page count in the file. If there's
        //   more than one let the user pick which page to load.
        //
        int numPages = document.pageCount();
        int pageNumber = 1;
        bool ok;
        
        if (numPages == 0) {
            QMessageBox::warning(0, "xyscan", tr("PDF file '%1' is empty (no pages).").arg(filename));
            return;
        }
        else if (numPages > 1) {
            pageNumber = QInputDialog::getInt(this, tr("Select Page"),
                                              tr("The PDF file has %1 pages.\nSelect the desired page:").arg(numPages),
                                              1, 1, numPages, 1, &ok, Qt::Sheet);
            if (!ok) return;
        }
        
        //
        //   Now deal with the size with which to render the page
        //
        QSizeF pageSize = document.pagePointSize(pageNumber-1);  // page counts starts at 0
        double pdfScale = QInputDialog::getDouble(this, tr("Scale"),
                                                  tr("PDF can be scaled before loading.\n"
                                                     "Original resolution is %1 x %2.\n"
                                                     "Enter scale:").arg(static_cast<int>(pageSize.width())).arg(static_cast<int>(pageSize.height())),
                                                  1, 0.5, 100, 2, &ok, Qt::Sheet, 0.25);
        pdfScale *= QGuiApplication::primaryScreen()->devicePixelRatio();
        QSize renderSize(pageSize.width()*pdfScale, pageSize.height()*pdfScale);
        
        //
        //   Now render PDF page into QImage and ultimately into a pixmap
        //
        QImage image = document.render(pageNumber-1, renderSize);
        pixmap = new QPixmap(QPixmap::fromImage(image));
        pixmap->setDevicePixelRatio(QGuiApplication::primaryScreen()->devicePixelRatio());
        
    }
    else {
        pixmap = new QPixmap(filename);
    }
    
    if (pixmap->isNull()) {
        QMessageBox::warning(0, "xyscan", tr("Cannot load image from file '%1'. "
                                             "Either the file content is damaged or the "
                                             "image file format is not supported.").arg(filename));
        return;
    }
    
    //
    //  Store the file in the recent file list
    //  and enable the referring actions in the
    //  File submenu (Open Recent).
    //
    mClearHistoryAction->setDisabled(false);
    mRecentFiles.removeAll(filename);
    mRecentFiles.prepend(filename);
    while (mRecentFiles.size() > mMaxRecentFiles) mRecentFiles.removeLast();
    
    for (int i = 0; i < mRecentFiles.size(); ++i) {
        QString text = tr("&%1  %2").arg(i + 1).arg(QFileInfo(mRecentFiles[i]).fileName());
        mRecentFileAction[i]->setText(text);
        mRecentFileAction[i]->setData(mRecentFiles[i]);
        mRecentFileAction[i]->setVisible(true);
    }
    for (int j = mRecentFiles.size(); j < mMaxRecentFiles; ++j) {
        mRecentFileAction[j]->setVisible(false);
    }
    
    mCurrentSource = filename;
    
    loadPixmap(pixmap);
}


void xyscanWindow::openRecent() 
{
    if (!checkForUnsavedData()) return;
    QAction *action = qobject_cast<QAction *>(sender());
    if (action) {
        QString filename = action->data().toString();
        openFromFile(filename);
    }
}

void xyscanWindow::pasteImage()
{
    //
    //  Broken in Qt 5.0.1 on Mac
    //  Wait for next version/release
    //
    
    QPixmap *pixmap = 0;
    
    const QClipboard *clipboard = QApplication::clipboard();
    const QMimeData *mimeData = clipboard->mimeData();
        
    if (mimeData->hasImage()) {
        QImage image = qvariant_cast<QImage>(mimeData->imageData());
        pixmap = new QPixmap(QPixmap::fromImage(image));
    }
    
    if (!pixmap || pixmap->isNull()) {
        QMessageBox::warning(0, "xyscan", tr("Cannot load image from clipboard.\n"
                                             "Either the clipboard does not contain an "
                                             "image or it contains an image in an "
                                             "unsupported image format."));
        return;
    }
    
    mCurrentSource = "Clipboard";
    
    loadPixmap(pixmap);
}

void xyscanWindow::print()
{
    //
    //  Create document for printing.
    //  xyscanDataTable does all the formatting and writing.
    //
    QTextDocument document;
    QImage original;
    if (mCurrentSource == QString("Screen Scan")) {
        mDataTable->writePrinterDocument(document, mCurrentSource);  // no image
    }
    else {
        original = mCurrentPixmap->pixmap().toImage();
        mDataTable->writePrinterDocument(document, mCurrentSource, &original);
    }
    
    //
    //  Start printer dialog and print the document we just created
    //
    QPrinter printer(QPrinter::HighResolution);
    QPrintDialog *dlg = new QPrintDialog(&printer, this);
    dlg->setWindowTitle(tr("xyscan - Print"));
    if (dlg->exec() != QDialog::Accepted) return;
    document.print(&printer);

    mMessageLabel->setText(tr("Ready"));
}

bool xyscanWindow::readyForScan()
{
    QLocale locale;
    bool OK;
    bool allMarkers = (numberOfMarkersSet() == 4);
    bool allAxisValues = true;

    ignore = locale.toDouble(mLowerXValueField->text(), &OK); if (!OK) allAxisValues = false;
    ignore = locale.toDouble(mUpperXValueField->text(), &OK); if (!OK) allAxisValues = false;
    ignore = locale.toDouble(mLowerYValueField->text(), &OK); if (!OK) allAxisValues = false;
    ignore = locale.toDouble(mUpperYValueField->text(), &OK); if (!OK) allAxisValues = false;
    if (allMarkers && allAxisValues)
        return true;
    else
        return false;
}

void xyscanWindow::resetMarker()
{
    mMarker[mXLower]->setVisible(false);    
    mMarker[mXUpper]->setVisible(false);    
    mMarker[mYLower]->setVisible(false);    
    mMarker[mYUpper]->setVisible(false);
    
    mLowerXValueField->setText(tr(""));
    mUpperXValueField->setText(tr(""));
    mLowerYValueField->setText(tr(""));
    mUpperYValueField->setText(tr(""));

    QString resetText = QString::fromWCharArray(L"\u22ef");
    mLowerXValueField->setPlaceholderText(resetText);
    mUpperXValueField->setPlaceholderText(resetText);
    mLowerYValueField->setPlaceholderText(resetText);
    mUpperYValueField->setPlaceholderText(resetText);

    mAngleSpinBox->setDisabled(false);    
    mScaleSpinBox->setDisabled(false);    
}

void xyscanWindow::rotateImage(double d)
{
    QSize s = mCurrentPixmap->pixmap().size();
    double x = s.width()/2;
    double y = s.height()/2;
    if (mCurrentPixmap) {
        // rotate around center
        mCurrentPixmap->setTransform(QTransform().translate(x, y).scale(mImageScale, mImageScale).rotate(-d).translate(-x, -y));
        mImageAngle = -d;
        QRectF rectf = mCurrentPixmap->sceneBoundingRect();
        mImageScene->setSceneRect(rectf);

        //
        //  Right now, have no solution to get the proper
        //  coordinate in the pixmap after rotating. Disable for now.
        //
        if (mImageScale != 1 || mImageAngle != 0) {
            if (mHistogramDock->isVisible()) mHistogramDock->setHidden(true);
            mHistogramDock->toggleViewAction()->setDisabled(true);
        }
        else
            mHistogramDock->toggleViewAction()->setDisabled(false);
        update();
    }
}

void xyscanWindow::save()
{
    QString fileName =  QFileDialog::getSaveFileName(this, tr("Save File"),
                                                     mSaveFileDirectory+QString("/xyscan.txt"),
                                                     tr("Text (*.txt);;Root Macro (*.C);;CSV (*.csv)"));
    if (fileName.isEmpty()) return;
    
    mSaveFileDirectory = fileName;
    mSaveFileDirectory.truncate(mSaveFileDirectory.lastIndexOf('/'));
    
    QFile file(fileName);
    if (!file.open(QFile::WriteOnly | QFile::Text)) {
        QMessageBox::warning(this, "xyscan",
                             tr("Cannot write file %1:\n%2.")
                             .arg(fileName)
                             .arg(file.errorString()));
        return;
    }
    
    bool writeAsRootMacro = fileName.endsWith(".C");     
    bool writeAsCsv = fileName.endsWith(".csv");

    QApplication::setOverrideCursor(Qt::WaitCursor);
    
    if (writeAsRootMacro) {
        mDataTable->writeRootMacro(file, mCurrentSource,
                                   mLogXRadioButton->isChecked(),
                                   mLogYRadioButton->isChecked(),
                                   mMarkerPlotCoordinate);
        mMessageLabel->setText(tr("Data saved as ROOT macro '%1'.").arg(fileName));
    }
    else if (writeAsCsv) {
        mDataTable->writeCsvFile(file, mCurrentSource);
        mMessageLabel->setText(tr("Data saved as csv file '%1'.").arg(fileName));
    }
    else {
        mDataTable->writeTextFile(file, mCurrentSource);
        mMessageLabel->setText(tr("Data saved in '%1'.").arg(fileName));
    }
    
    QApplication::restoreOverrideCursor();
}

void xyscanWindow::scaleImage(double z)
{
    QSize s = mCurrentPixmap->pixmap().size();
    double x = s.width()/2;
    double y = s.height()/2;
    if (mCurrentPixmap) {
        // scale around center
        mCurrentPixmap->setTransform(QTransform().translate(x, y).scale(z, z).rotate(mImageAngle).translate(-x, -y));
        mImageScale = z;        
        QRectF rectf = mCurrentPixmap->sceneBoundingRect();
        mImageScene->setSceneRect(rectf);

        //
        //  Right now, have no solution to get the proper
        //  coordinate in the pixmap after scaling. Disable for now.
        //
        if (mImageScale != 1 || mImageAngle != 0) {
            if (mHistogramDock->isVisible()) mHistogramDock->setHidden(true);
            mHistogramDock->toggleViewAction()->setDisabled(true);
        }
        else
            mHistogramDock->toggleViewAction()->setDisabled(false);

        update();
    }
}

QPointF xyscanWindow::scan(QPointF* auxPos)
{
    //
    //  This is where the transformation from pixel
    //  to graph coordinates is performed. 
    //
    
    QPointF xy(0,0);
 
    bool logx = mLogXRadioButton->isChecked();
    bool logy = mLogYRadioButton->isChecked();
        
    //
    //  Consistency and numerics checks
    //
    QMessageBox::StandardButton reply;
    
    if (!readyForScan()) {
        QMessageBox::information(0, "xyscan",
                                 tr("Cannot scan yet. Not sufficient information available to perform "
                                    "the coordinate transformation. You need to define 2 points on the "
                                    "x-axis (x1 & x1) and 2 on the y-axis (y1 & y2)."));
        return xy;
    }    
    
    if (fabs(mMarkerPixel[mXUpper]-mMarkerPixel[mXLower]) < 2) {
        reply = QMessageBox::critical(0, "xyscan",
                                      tr("Error in calculating transformation.\n"
                                         "Markers on x-axis are less than two pixels apart. "
                                         "Cannot continue with current settings."),
                                      QMessageBox::Reset|QMessageBox::Ignore, QMessageBox::Reset);
        if (reply == QMessageBox::Reset) {resetMarker();}
        return xy;
    }
    if (fabs(mMarkerPixel[mYUpper]-mMarkerPixel[mYLower]) < 2) {
        reply = QMessageBox::critical(0, "xyscan",
                                      tr("Error in calculating transformation.\n"
                                         "Markers on y-axis are less than two pixels apart. "
                                         "Cannot continue with current settings."),
                                      QMessageBox::Reset|QMessageBox::Ignore, QMessageBox::Reset);
        if (reply == QMessageBox::Reset) {resetMarker();}
        return xy;
    }
    
    if (logx) {
        if (mMarkerPlotCoordinate[mXUpper] <= 0 || mMarkerPlotCoordinate[mXLower] <= 0) {
            QMessageBox::critical(0, "xyscan",
                                  tr("Error in calculating transformation.\n"
                                     "Logarithmic x-axis selected but negative (or zero) values assigned to markers. "
                                     "Cannot continue with current settings."));
            return xy;
        }
    }
    
    if (logy) {
        if (mMarkerPlotCoordinate[mYUpper] <= 0 || mMarkerPlotCoordinate[mYLower] <= 0) {
            QMessageBox::critical(0, "xyscan",
                                  tr("Error in calculating transformation.\n"
                                     "Logarithmic y-axis selected but negative (or zero) values assigned to markers. "
                                     "Cannot continue with current settings."));
            return xy;
        }
    }
    
    mShowPrecisionAction->setDisabled(false); // OK from here on

    //
    //  Coordinate transformation
    //
    
    double m11, m22, dx, dy;
    if (logx) {
        m11 = (log10(mMarkerPlotCoordinate[mXUpper])-log10(mMarkerPlotCoordinate[mXLower]))/(mMarkerPixel[mXUpper]-mMarkerPixel[mXLower]);
        dx = log10(mMarkerPlotCoordinate[mXUpper])-m11*mMarkerPixel[mXUpper];
    }
    else {
        m11 = (mMarkerPlotCoordinate[mXUpper]-mMarkerPlotCoordinate[mXLower])/(mMarkerPixel[mXUpper]-mMarkerPixel[mXLower]);
        dx = mMarkerPlotCoordinate[mXUpper]-m11*mMarkerPixel[mXUpper];
    }
    if (logy) {
        m22 = (log10(mMarkerPlotCoordinate[mYUpper])-log10(mMarkerPlotCoordinate[mYLower]))/(mMarkerPixel[mYUpper]-mMarkerPixel[mYLower]);
        dy = log10(mMarkerPlotCoordinate[mYUpper])-m22*mMarkerPixel[mYUpper];
    }
    else {
        m22 = (mMarkerPlotCoordinate[mYUpper]-mMarkerPlotCoordinate[mYLower])/(mMarkerPixel[mYUpper]-mMarkerPixel[mYLower]);
        dy = mMarkerPlotCoordinate[mYUpper]-m22*mMarkerPixel[mYUpper];
    }
    QTransform M(m11, 0, 0, m22, dx, dy);
    QPointF cross;
    if (auxPos) {
        cross.setX(auxPos->x());
        cross.setY(auxPos->y());
    }
    else {
        cross.setX(mCrosshairV->pos().x());
        cross.setY(mCrosshairH->pos().y());
    }
    xy = M.map(cross);
    if (logx) xy.setX(pow(10.,xy.x()));
    if (logy) xy.setY(pow(10.,xy.y()));
    
    return xy;
}

void xyscanWindow::setPlotCoordinateValueForMarker(int markerType)
{
    QLocale locale;
    
    bool ok;
    double r = mMarkerPlotCoordinate[markerType];
    char axis = (markerType == mXLower || markerType == mXUpper) ? 'x' : 'y';
    QGraphicsLineItem* lineToDisappear = (axis == 'x') ? mCrosshairH : mCrosshairV;
    lineToDisappear->setVisible(false);

    QString uplow = (markerType == mXLower || markerType == mYLower) ? QString(tr("lower")) : QString(tr("upper"));
    QString txt = QString(tr("Enter the %1 %2-value at marker position:").arg(uplow).arg(axis));

    QString ret = QInputDialog::getText(this, "xyscan", txt,
                                        QLineEdit::Normal, locale.toString(r), &ok);
    
    lineToDisappear->setVisible(true);

    if (!ok) return;
    r = locale.toDouble(ret, &ok);
    if (!ok) {
        QMessageBox::warning( 0, "xyscan",
                              tr("%1 is not a valid floating point number.").arg(ret));
        return;
    }
    
    mMarkerPlotCoordinate[markerType] = r;
    QString str = locale.toString(r, 'g', mSignificantDigits);
    if (markerType == mXLower)
        mLowerXValueField->setText(str);
    else if (markerType == mXUpper)
        mUpperXValueField->setText(str);
    else if (markerType == mYLower)
        mLowerYValueField->setText(str);
    else if (markerType == mYUpper)
        mUpperYValueField->setText(str);
    
    QPointF pos(mCrosshairH->pos().x(), mCrosshairV->pos().y());

    if (markerType == mXLower || markerType == mXUpper)
        mMarkerPixel[markerType] = pos.x();
    else
        mMarkerPixel[markerType] = pos.y();

    mMarker[markerType]->setPos(pos);
    mMarker[markerType]->setVisible(true);

    if (readyForScan()) {
        mMessageLabel->setText(tr("Ready to scan. Press space bar to record current cursor position."));
        mStartMeasureButton->setDisabled(false);
        mMeasureScaleComboBox->setDisabled(false);
        auto model = dynamic_cast<QStandardItemModel*>(mMeasureScaleComboBox->model());
        auto item = model->item(mPlotUnits, 0);
        item->setEnabled(true);
        enableSignificantDigitsMenu(true);
    }
    else
        mMessageLabel->setText(QString(tr("%1 of 4 markers set.")).arg(numberOfMarkersSet()));

    if (mMeasuringIsOn) updateMeasureDisplay(); // any change of marker does change measure
    mAngleSpinBox->setDisabled(true);  // rotating would void marker position
    mScaleSpinBox->setDisabled(true);  // same for scaling (zoomig in/out)
}

void xyscanWindow::showPrecision()
{
    QPointF point, currentV, currentH;
    QPointF left, right, up, down;
    QString str;
    double mdx, pdx, mdy, pdy;
    if (readyForScan()) {
        currentV = mCrosshairV->pos();
        currentH = mCrosshairH->pos();
        point = scan();
        
        mCrosshairV->moveBy(-1,0);
        mCrosshairH->moveBy(-1,0);
        left = scan();
        mCrosshairV->setPos(currentV);
        mCrosshairH->setPos(currentH);
        
        mCrosshairV->moveBy(1,0);
        mCrosshairH->moveBy(1,0);
        right = scan();
        mCrosshairV->setPos(currentV);
        mCrosshairH->setPos(currentH);

        mCrosshairV->moveBy(0,1);
        mCrosshairH->moveBy(0,1);
        down = scan();
        mCrosshairV->setPos(currentV);
        mCrosshairH->setPos(currentH);
        
        mCrosshairV->moveBy(0,-1);
        mCrosshairH->moveBy(0,-1);
        up = scan();
        mCrosshairV->setPos(currentV);
        mCrosshairH->setPos(currentH);
        
        mdx = fabs(point.x()-left.x());
        pdx = fabs(point.x()-right.x());
        mdy = fabs(point.y()-down.y());
        pdy = fabs(point.y()-up.y());
        QLocale locale;
        str = tr("Estimated precision at current point:\ndx "
                 "= +%1  -%2\ndy = +%3  -%4")
               .arg(locale.toString(pdx)).arg(locale.toString(mdx,'g', mSignificantDigits))
               .arg(locale.toString(pdy)).arg(locale.toString(mdy,'g', mSignificantDigits));
    }
    else {
        str = tr("Cannot determin precision yet.\nPlace all 4 markers first.");
    }
    
    //
    //  Sheet attached to main window on Mac
    //
    QMessageBox msgBox(QMessageBox::NoIcon,
                       "xyscan",
                       str,
                       QMessageBox::Ok,
                       this,
                       Qt::Sheet);
    msgBox.setWindowModality(Qt::WindowModal);
    
    msgBox.exec();
}

void xyscanWindow::updateHistogramDisplay()
{
    if (!mHistogramDock->isVisible()) return;
    if (!mCurrentPixmap) return;
    if (!mCurrentShadowImage || mCurrentShadowImage->isNull()) return;

    int histoMode = mHistogramModeComboBox->currentIndex();

    int x = mCrosshairH->pos().x()*mCurrentShadowImage->devicePixelRatio();
    int y = mCrosshairH->pos().y()*mCurrentShadowImage->devicePixelRatio();

    QList<QBarSet *> list = mBarSeries->barSets();

    //
    //   Horizontal histogram
    //
    double value;
    for (int ix=x-mHistogramRange; ix<=x+mHistogramRange; ix++) {
        if (mCurrentShadowImage->valid(ix, y) &&
                (histoMode == mHorizontalAndVerticalHistogram || histoMode == mHorizontalHistogram)) {
            QRgb rgb = mCurrentShadowImage->pixel(ix, y);
            if (rgb == 0 && mCurrentShadowImage->hasAlphaChannel())
                value = 0;
            else
                value =  255-qGray(rgb);
        }
        else
            value = 0;
        list[0]->replace(ix-(x-mHistogramRange), value);
    }

    //
    //   Vertical histogram
    //
    for (int iy=y-mHistogramRange; iy<=y+mHistogramRange; iy++) {
        if (mCurrentShadowImage->valid(x, iy) &&
                (histoMode == mHorizontalAndVerticalHistogram || histoMode == mVerticalHistogram)) {
            QRgb rgb = mCurrentShadowImage->pixel(x, iy);
            if (rgb == 0 && mCurrentShadowImage->hasAlphaChannel())
                value = 0;
            else
                value =  255-qGray(rgb);
        }
        else
            value = 0;
        list[1]->replace(iy-(y-mHistogramRange), value);
    }
}

void xyscanWindow::updatePixelDisplay()
{
    if (!mCurrentPixmap) return;
    
    QLocale local;
    QString str;
    str = local.toString(mCrosshairH->pos().x(), 'g', mSignificantDigits);
    mPixelXDisplay->setText(str);
    str = local.toString(mCrosshairH->pos().y(), 'g', mSignificantDigits);
    mPixelYDisplay->setText(str);
}

void xyscanWindow::updateMeasureDisplay()
{
    QLocale locale;
    
    //
    //  This method is invoked every time the cursor moves,
    //  a double mouse clicks happen, or any input field in
    //  the Measure tool item is altered.
    //
    if (mMeasureScaleComboBox->currentIndex() == mUserUnits) {
        QPalette palette;
        bool ok;
        mMeasureScaleValueLabel->setVisible(true);
        mMeasureUserScaleField->setVisible(true);
        mMeasureUserScale = locale.toDouble(mMeasureUserScaleField->text(), &ok);
        if (mMeasureUserScale < 0 || !ok) {
            mMeasureUserScale = 0;
            palette.setColor(QPalette::Text, Qt::red);
            mMeasureUserScaleField->setPalette(palette);
        }
        else {
            mMeasureUserScaleField->setPalette(palette);
        }
    }
    else {
        mMeasureScaleValueLabel->setVisible(false);
        mMeasureUserScaleField->setVisible(false);
    }
    
    if (!mMeasuringIsOn) return;
    
    //
    //  Get start and end point
    //
    QPointF endPoint(mCrosshairV->pos().x(), mCrosshairH->pos().y());
    QPointF startPoint(mPointMarker->pos());
    startPoint += QPointF(mPointMarker->pixmap().width()/2+mPointMarkerOffset, mPointMarker->pixmap().height()/2+mPointMarkerOffset); 
    
    //
    //  Calculate the distance
    //
    pair<double, double> d = measureDistance(startPoint, endPoint);
    double dx = d.first;
    double dy = d.second;
    double dr = sqrt(dx*dx+dy*dy);
    double dphi = atan2(dy, dx);
    if (dphi < 0) dphi = 2*M_PI+dphi;  // since atan2() returns -pi to pi
    
    //
    //  Update display
    //  Note that the casewhen paths are measured this is handled in
    //  in the measurementController().
    //
    QString str;
    str = locale.toString(dx, 'g', mSignificantDigits);
    mMeasureDXDisplay->setText(str);
    str = locale.toString(dy, 'g', mSignificantDigits);
    mMeasureDYDisplay->setText(str);
    str = locale.toString(dr, 'g', mSignificantDigits);
    mMeasureDRDisplay->setText(str);
    str = locale.toString((mMeasureAngleRadiansCheckBox->isChecked() ? dphi : dphi*180/M_PI),
                          'g', mSignificantDigits);
    mMeasureDADisplay->setText(str);


    if (mMeasurePathCheckBox->isChecked()) {
        str = locale.toString(mMeasurePathLength, 'g', mSignificantDigits);
    }
    else {
        str = locale.toString(dr, 'g', mSignificantDigits);
    }
    mMeasurePathDisplay->setText(str);
    mMeasuredDistanceLabel->setText(str);

    //
    //  Update the line
    //
    mMeasureLine->setLine(startPoint.x(), startPoint.y(), endPoint.x(), endPoint.y());
}

void xyscanWindow::updateSignificantDigits(QAction *action)
{
    for (int i=0; i<mMaxSignificantDigits; i++) {
        if (mSignificantDigitsAction[i] == action) {
            mSignificantDigits = i;
            break;
        }
    }
    updatePixelDisplay();
    updateMeasureDisplay();
    updatePlotCoordinateDisplay();
    updateStatusBarCoordinateDisplay();
}

void xyscanWindow::updatePlotCoordinateDisplay()
{
    //
    //  Display plot coordinates. QLocale takes care of
    //  the '.' versus ',' in chosen language.
    //  QLocale::toString() formats by default to 'g'
    //  with 6 digit precision.
    //
    QLocale local;
    if (readyForScan()) {
        QPointF xy = scan();
        QString str;
        str = local.toString(xy.x(), 'g', mSignificantDigits);
        mPlotXDisplay->setText(str);
        str = local.toString(xy.y(), 'g', mSignificantDigits);
        mPlotYDisplay->setText(str);
    }
    else {
        mPlotXDisplay->setText(tr("N/A"));
        mPlotYDisplay->setText(tr("N/A"));
    }
}

void xyscanWindow::updateStatusBarCoordinateDisplay()
{
    //
    //  This updates the coordinate display on the right
    //  end of the status bar. Different things are displayed
    //  depending on the status.
    //  In case the measure tool is on there's a 3rd label
    //  displaying the measured distance (in blue). This
    //  is update in updateMeasureDisplay() since the distance
    //  is only calculated locally.
    //
    if (!mCurrentPixmap) return;

    QLocale local;
    if (readyForScan()) {
        //
        //  In scan mode we always display the plot coordintes ...
        //
        QPointF xy = scan();
        mXCoordinateLabel->setText(local.toString(xy.x(), 'g', mSignificantDigits));
        mYCoordinateLabel->setText(local.toString(xy.y(), 'g', mSignificantDigits));
    }
    else {
        //
        //  ... otherwise pixel coordinates
        //
        mXCoordinateLabel->setText(local.toString(mCrosshairH->pos().x(), 'g', mSignificantDigits));
        mYCoordinateLabel->setText(local.toString(mCrosshairH->pos().y(), 'g', mSignificantDigits));
    }
}

void xyscanWindow::updateWhenAxisScaleChanged()
{
    updatePixelDisplay();
    updatePlotCoordinateDisplay();
    updateStatusBarCoordinateDisplay();
    updateZoomDisplay();
}

void xyscanWindow::updateZoomDisplay()
{
    mZoomView->centerOn(mCrosshairH->pos().x(), mCrosshairV->pos().y());
    update();
}

void xyscanWindow::userWasRemindedOfAvailableUpdate()
{
    mLastReminderTime = QDateTime::currentDateTime();
}

void xyscanWindow::writeSettings()
{
    QSettings settings(QSettings::NativeFormat, QSettings::UserScope, "tu", "xyscan");
    settings.setValue("xyscan/Version", xyscanVersionString());  // not used but good to have
    settings.setValue("xyscan/position", pos());
    settings.setValue("xyscan/showToolTips", mShowTooltipsAction->isChecked());
    settings.setValue("xyscan/lastOpenFileDirectory", mOpenFileDirectory);
    settings.setValue("xyscan/lastSaveFileDirectory", mSaveFileDirectory);
    settings.setValue("xyscan/recentFiles", mRecentFiles);
    settings.setValue("xyscan/crosshairColor", mCrosshairColor);
    settings.setValue("xyscan/markerColor", mMarkerColor);
    settings.setValue("xyscan/pathColor", mPathColor);
    settings.setValue("xyscan/lastReminderTime", mLastReminderTime);
}

void xyscanWindow::zoomScale(double s)
{
    double relativeScale = s/mZoomScale;
    mZoomView->scale(relativeScale, relativeScale);
    mZoomScale = s;
    updateZoomDisplay();
}
