File indexing completed on 2024-04-28 04:32:22

0001 /*
0002  * SPDX-FileCopyrightText: 2007-2010 Kare Sars <kare dot sars at iki dot fi>
0003  * SPDX-FileCopyrightText: 2009 Matthias Nagl <matthias at nagl dot info>
0004  * SPDX-FileCopyrightText: 2009 Grzegorz Kurtyka <grzegorz dot kurtyka at gmail dot com>
0005  * SPDX-FileCopyrightText: 2007-2008 Gilles Caulier <caulier dot gilles at gmail dot com>
0006  * SPDX-FileCopyrightText: 2014 Gregor Mitsch : port to KDE5 frameworks
0007  * SPDX-FileCopyrightText: 2021 Alexander Stippich <a.stippich@gmx.net>
0008  *
0009  * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
0010  */
0011 
0012 #include "ksanewidget.h"
0013 #include "ksanewidget_p.h"
0014 
0015 #include <unistd.h>
0016 
0017 #include <QApplication>
0018 #include <QList>
0019 #include <QLabel>
0020 #include <QSplitter>
0021 #include <QPointer>
0022 #include <QIcon>
0023 #include <QShortcut>
0024 
0025 #include <KPasswordDialog>
0026 #ifdef HAVE_KF5WALLET
0027 #include <KWallet>
0028 #endif
0029 
0030 #include <KLocalizedString>
0031 
0032 #include "ksanedevicedialog.h"
0033 #include <ksane_debug.h>
0034 
0035 namespace KSaneIface
0036 {
0037 
0038 KSaneWidget::KSaneWidget(QWidget *parent)
0039     : QWidget(parent), d(new KSaneWidgetPrivate(this))
0040 {
0041     d->m_ksaneCoreInterface = new KSaneCore::Interface();
0042 
0043     connect(d->m_ksaneCoreInterface, &KSaneCore::Interface::scannedImageReady, d, &KSaneWidgetPrivate::imageReady);
0044     connect(d->m_ksaneCoreInterface, &KSaneCore::Interface::scanFinished, d, &KSaneWidgetPrivate::scanDone);
0045     connect(d->m_ksaneCoreInterface, &KSaneCore::Interface::userMessage, d, &KSaneWidgetPrivate::alertUser);
0046     connect(d->m_ksaneCoreInterface, &KSaneCore::Interface::scanProgress, d, &KSaneWidgetPrivate::updateProgress);
0047     connect(d->m_ksaneCoreInterface, &KSaneCore::Interface::batchModeCountDown, d, &KSaneWidgetPrivate::updateCountDown);
0048     connect(d->m_ksaneCoreInterface, &KSaneCore::Interface::availableDevices, d, &KSaneWidgetPrivate::signalDevListUpdate);
0049     connect(d->m_ksaneCoreInterface, &KSaneCore::Interface::buttonPressed, this, &KSaneWidget::buttonPressed);
0050 
0051     // Create the static UI
0052     // create the preview
0053     d->m_previewViewer = new KSaneViewer(&(d->m_previewImg), this);
0054     connect(d->m_previewViewer, &KSaneViewer::newSelection,
0055             d, &KSaneWidgetPrivate::handleSelection);
0056 
0057     d->m_warmingUp = new QLabel;
0058     d->m_warmingUp->setText(i18n("Waiting for the scan to start."));
0059     d->m_warmingUp->setAlignment(Qt::AlignCenter);
0060     d->m_warmingUp->hide();
0061 
0062     d->m_countDown = new QLabel;
0063     d->m_countDown->setAlignment(Qt::AlignCenter);
0064     d->m_countDown->hide();
0065 
0066     d->m_progressBar = new QProgressBar;
0067     d->m_progressBar->setMaximum(100);
0068 
0069     d->m_cancelBtn = new QPushButton;
0070     d->m_cancelBtn->setIcon(QIcon::fromTheme(QStringLiteral("process-stop")));
0071     d->m_cancelBtn->setToolTip(i18n("Cancel current scan operation"));
0072     connect(d->m_cancelBtn, &QPushButton::clicked, this, &KSaneWidget::cancelScan);
0073 
0074     d->m_activityFrame = new QWidget;
0075     QHBoxLayout *progress_lay = new QHBoxLayout(d->m_activityFrame);
0076     progress_lay->setContentsMargins(0, 0, 0, 0);
0077     progress_lay->addWidget(d->m_progressBar, 100);
0078     progress_lay->addWidget(d->m_warmingUp, 100);
0079     progress_lay->addWidget(d->m_countDown, 100);
0080     progress_lay->addWidget(d->m_cancelBtn, 0);
0081     d->m_activityFrame->hide();
0082 
0083     d->m_zInBtn  = new QToolButton(this);
0084     d->m_zInBtn->setAutoRaise(true);
0085     d->m_zInBtn->setIcon(QIcon::fromTheme(QStringLiteral("zoom-in")));
0086     d->m_zInBtn->setToolTip(i18n("Zoom In"));
0087     connect(d->m_zInBtn, &QToolButton::clicked, d->m_previewViewer, &KSaneViewer::zoomIn);
0088 
0089     d->m_zOutBtn = new QToolButton(this);
0090     d->m_zOutBtn->setAutoRaise(true);
0091     d->m_zOutBtn->setIcon(QIcon::fromTheme(QStringLiteral("zoom-out")));
0092     d->m_zOutBtn->setToolTip(i18n("Zoom Out"));
0093     connect(d->m_zOutBtn, &QToolButton::clicked, d->m_previewViewer, &KSaneViewer::zoomOut);
0094 
0095     d->m_zSelBtn = new QToolButton(this);
0096     d->m_zSelBtn->setAutoRaise(true);
0097     d->m_zSelBtn->setIcon(QIcon::fromTheme(QStringLiteral("zoom-fit-best")));
0098     d->m_zSelBtn->setToolTip(i18n("Zoom to Selection"));
0099     connect(d->m_zSelBtn, &QToolButton::clicked, d->m_previewViewer, &KSaneViewer::zoomSel);
0100 
0101     d->m_zFitBtn = new QToolButton(this);
0102     d->m_zFitBtn->setAutoRaise(true);
0103     d->m_zFitBtn->setIcon(QIcon::fromTheme(QStringLiteral("document-preview")));
0104     d->m_zFitBtn->setToolTip(i18n("Zoom to Fit"));
0105     connect(d->m_zFitBtn, &QToolButton::clicked, d->m_previewViewer, &KSaneViewer::zoom2Fit);
0106 
0107     d->m_clearSelBtn = new QToolButton(this);
0108     d->m_clearSelBtn->setAutoRaise(true);
0109     d->m_clearSelBtn->setIcon(QIcon::fromTheme(QStringLiteral("edit-clear")));
0110     d->m_clearSelBtn->setToolTip(i18n("Clear Selections"));
0111     connect(d->m_clearSelBtn, &QToolButton::clicked, d->m_previewViewer, &KSaneViewer::clearSelections);
0112 
0113     QShortcut *prevShortcut = new QShortcut(QKeySequence(QStringLiteral("Ctrl+P")), this);
0114     connect(prevShortcut, &QShortcut::activated, d, &KSaneWidgetPrivate::startPreviewScan);
0115 
0116     QShortcut *scanShortcut = new QShortcut(QKeySequence(QStringLiteral("Ctrl+S")), this);
0117     connect(scanShortcut, &QShortcut::activated, d, &KSaneWidgetPrivate::startFinalScan);
0118 
0119     d->m_prevBtn = new QPushButton(this);
0120     d->m_prevBtn->setIcon(QIcon::fromTheme(QStringLiteral("document-import")));
0121     d->m_prevBtn->setToolTip(i18n("Scan Preview Image (%1)", prevShortcut->key().toString()));
0122     d->m_prevBtn->setText(i18nc("Preview button text", "Preview"));
0123     connect(d->m_prevBtn, &QToolButton::clicked, d, &KSaneWidgetPrivate::startPreviewScan);
0124 
0125     d->m_scanBtn = new QPushButton(this);
0126     d->m_scanBtn->setIcon(QIcon::fromTheme(QStringLiteral("document-save")));
0127     d->m_scanBtn->setToolTip(i18n("Scan Final Image (%1)", scanShortcut->key().toString()));
0128     d->m_scanBtn->setText(i18nc("Final scan button text", "Scan"));
0129     d->m_scanBtn->setFocus(Qt::OtherFocusReason);
0130     setFocusProxy(d->m_scanBtn);
0131     connect(d->m_scanBtn, &QToolButton::clicked, d, &KSaneWidgetPrivate::startFinalScan);
0132 
0133     d->m_btnFrame = new QWidget;
0134     QHBoxLayout *btn_lay = new QHBoxLayout(d->m_btnFrame);
0135     btn_lay->setContentsMargins(0, 0, 0, 0);
0136     btn_lay->addWidget(d->m_zInBtn);
0137     btn_lay->addWidget(d->m_zOutBtn);
0138     btn_lay->addWidget(d->m_zSelBtn);
0139     btn_lay->addWidget(d->m_zFitBtn);
0140     btn_lay->addWidget(d->m_clearSelBtn);
0141     btn_lay->addStretch(100);
0142     btn_lay->addWidget(d->m_prevBtn);
0143     btn_lay->addWidget(d->m_scanBtn);
0144 
0145     // calculate the height of the waiting/scanning/buttons frames to avoid jumpiness.
0146     int minHeight = d->m_btnFrame->sizeHint().height();
0147     if (d->m_activityFrame->sizeHint().height() > minHeight) {
0148         minHeight = d->m_activityFrame->sizeHint().height();
0149     }
0150     d->m_btnFrame->setMinimumHeight(minHeight);
0151     d->m_activityFrame->setMinimumHeight(minHeight);
0152     d->m_warmingUp->setMinimumHeight(minHeight);
0153     d->m_countDown->setMinimumHeight(minHeight);
0154 
0155     d->m_previewFrame = new QWidget;
0156     QVBoxLayout *preview_layout = new QVBoxLayout(d->m_previewFrame);
0157     preview_layout->setContentsMargins(0, 0, 0, 0);
0158     preview_layout->addWidget(d->m_previewViewer, 100);
0159     preview_layout->addWidget(d->m_activityFrame, 0);
0160     preview_layout->addWidget(d->m_btnFrame, 0);
0161 
0162     // Create Options Widget
0163     d->m_optsTabWidget = new QTabWidget();
0164 
0165     // Add the basic options tab
0166     d->m_basicScrollA = new QScrollArea();
0167     d->m_basicScrollA->setWidgetResizable(true);
0168     d->m_basicScrollA->setFrameShape(QFrame::NoFrame);
0169     d->m_optsTabWidget->addTab(d->m_basicScrollA, i18n("Basic Options"));
0170 
0171     // Add the advanced options tab
0172     d->m_advancedScrollA = new QScrollArea();
0173     d->m_advancedScrollA->setWidgetResizable(true);
0174     d->m_advancedScrollA->setFrameShape(QFrame::NoFrame);
0175     d->m_optsTabWidget->addTab(d->m_advancedScrollA, i18n("Advanced Options"));
0176 
0177     // Add the other options tab
0178     d->m_otherScrollA = new QScrollArea;
0179     d->m_otherScrollA->setWidgetResizable(true);
0180     d->m_otherScrollA->setFrameShape(QFrame::NoFrame);
0181     d->m_optsTabWidget->addTab(d->m_otherScrollA, i18n("Scanner Specific Options"));
0182 
0183     d->m_splitter = new QSplitter(this);
0184     d->m_splitter->addWidget(d->m_optsTabWidget);
0185     d->m_splitter->setStretchFactor(0, 0);
0186     d->m_splitter->addWidget(d->m_previewFrame);
0187     d->m_splitter->setStretchFactor(1, 100);
0188 
0189     d->m_optionsCollapser = new SplitterCollapser(d->m_splitter, d->m_optsTabWidget);
0190 
0191     QHBoxLayout *base_layout = new QHBoxLayout(this);
0192     base_layout->addWidget(d->m_splitter);
0193     base_layout->setContentsMargins(0, 0, 0, 0);
0194 
0195     // disable the interface in case no device is opened.
0196     d->m_optsTabWidget->setDisabled(true);
0197     d->m_previewViewer->setDisabled(true);
0198     d->m_btnFrame->setDisabled(true);
0199 
0200 }
0201 
0202 KSaneWidget::~KSaneWidget()
0203 {
0204     delete d->m_ksaneCoreInterface;
0205     delete d;
0206 }
0207 
0208 QString KSaneWidget::deviceName() const
0209 {
0210     return d->m_ksaneCoreInterface->deviceName();
0211 }
0212 
0213 QString KSaneWidget::deviceVendor() const
0214 {
0215     return d->m_ksaneCoreInterface->deviceVendor();
0216 }
0217 
0218 QString KSaneWidget::deviceModel() const
0219 {
0220     return d->m_ksaneCoreInterface->deviceModel();
0221 }
0222 
0223 QString KSaneWidget::selectDevice(QWidget *parent)
0224 {
0225     QString selected_name;
0226     QPointer<KSaneDeviceDialog> sel = new KSaneDeviceDialog(parent);
0227     connect(d->m_ksaneCoreInterface, &KSaneCore::Interface::availableDevices, sel, &KSaneDeviceDialog::updateDevicesList);
0228     connect(sel, &KSaneDeviceDialog::requestReloadList, d->m_ksaneCoreInterface, &KSaneCore::Interface::reloadDevicesList);
0229 
0230     d->m_ksaneCoreInterface->reloadDevicesList();
0231 
0232     if (sel->exec() == QDialog::Accepted) {
0233         selected_name = sel->getSelectedName();
0234     }
0235 
0236     delete sel;
0237     return selected_name;
0238 }
0239 
0240 bool KSaneWidget::openDevice(const QString &deviceName)
0241 {
0242     KPasswordDialog               *dlg;
0243 #ifdef HAVE_KF5WALLET
0244     KWallet::Wallet               *saneWallet;
0245 #endif
0246     QString                        myFolderName = QStringLiteral("ksane");
0247     QMap<QString, QString>         wallet_entry;
0248 
0249     KSaneCore::Interface::OpenStatus status = d->m_ksaneCoreInterface->openDevice(deviceName);
0250     if (status == KSaneCore::Interface::OpeningFailed) {
0251         return false;
0252     }
0253 
0254     bool password_dialog_ok = true;
0255 
0256     // prepare wallet for authentication and create password dialog
0257     if (status == KSaneCore::Interface::OpeningDenied) {
0258 #ifdef HAVE_KF5WALLET
0259         saneWallet = KWallet::Wallet::openWallet(KWallet::Wallet::LocalWallet(), winId());
0260 
0261         if (saneWallet) {
0262             dlg = new KPasswordDialog(this, KPasswordDialog::ShowUsernameLine | KPasswordDialog::ShowKeepPassword);
0263             if (!saneWallet->hasFolder(myFolderName)) {
0264                 saneWallet->createFolder(myFolderName);
0265             }
0266             saneWallet->setFolder(myFolderName);
0267             saneWallet->readMap(deviceName, wallet_entry);
0268             if (!wallet_entry.empty() || true) {
0269                 dlg->setUsername(wallet_entry[QStringLiteral("username")]);
0270                 dlg->setPassword(wallet_entry[QStringLiteral("password")]);
0271                 dlg->setKeepPassword(true);
0272             }
0273         } else
0274 #endif
0275         {
0276             dlg = new KPasswordDialog(this, KPasswordDialog::ShowUsernameLine);
0277         }
0278         dlg->setPrompt(i18n("Authentication required for resource: %1", deviceName));
0279 
0280     }
0281 
0282     // sane_open failed due to insufficient authorization
0283     // retry opening device with user provided data assisted with kwallet records
0284     while (status == KSaneCore::Interface::OpeningDenied) {
0285 
0286         password_dialog_ok = dlg->exec();
0287         if (!password_dialog_ok) {
0288             delete dlg;
0289             return false; //the user canceled
0290         }
0291 
0292         // add/update the device user-name and password for authentication
0293         status = d->m_ksaneCoreInterface->openRestrictedDevice(deviceName, dlg->username(), dlg->password());
0294 
0295 #ifdef HAVE_KF5WALLET
0296         // store password in wallet on successful authentication
0297         if (dlg->keepPassword() && status != KSaneCore::Interface::OpeningDenied) {
0298             QMap<QString, QString> entry;
0299             entry[QStringLiteral("username")] = dlg->username();
0300             entry[QStringLiteral("password")] = dlg->password();
0301             if (saneWallet) {
0302                 saneWallet->writeMap(deviceName, entry);
0303             }
0304         }
0305 #endif
0306     }
0307 
0308     // Create the options interface
0309     d->createOptInterface();
0310 
0311     // Enable the interface
0312     d->m_optsTabWidget->setDisabled(false);
0313     d->m_previewViewer->setDisabled(false);
0314     d->m_btnFrame->setDisabled(false);
0315 
0316     // estimate the preview size and create an empty image
0317     // this is done so that you can select scan area without
0318     // having to scan a preview.
0319     d->updatePreviewSize();
0320     QTimer::singleShot(1000, d->m_previewViewer, &KSaneViewer::zoom2Fit);
0321     return true;
0322 }
0323 
0324 bool KSaneWidget::closeDevice()
0325 {
0326     bool result = d->m_ksaneCoreInterface->closeDevice();
0327     if (!result) {
0328         return false;
0329     }
0330     d->clearDeviceOptions();
0331     // disable the interface until a new device is opened.
0332     d->m_optsTabWidget->setDisabled(true);
0333     d->m_previewViewer->setDisabled(true);
0334     d->m_btnFrame->setDisabled(true);
0335 
0336     return true;
0337 }
0338 
0339 void KSaneWidget::startScan()
0340 {
0341     if (d->m_btnFrame->isEnabled()) {
0342         d->m_cancelMultiScan = false;
0343         d->startFinalScan();
0344     } else {
0345         // if the button frame is disabled, there is no open device to scan from
0346         Q_EMIT scanDone(KSaneWidget::ErrorGeneral, QString());
0347     }
0348 }
0349 
0350 void KSaneWidget::startPreviewScan()
0351 {
0352     if (d->m_btnFrame->isEnabled()) {
0353         d->m_cancelMultiScan = false;
0354         d->startPreviewScan();
0355     } else {
0356         // if the button frame is disabled, there is no open device to scan from
0357         Q_EMIT scanDone(KSaneWidget::ErrorGeneral, QString());
0358     }
0359 }
0360 
0361 void KSaneWidget::cancelScan()
0362 {
0363     d->m_cancelMultiScan = true;
0364     d->m_ksaneCoreInterface->stopScan();
0365 }
0366 
0367 void KSaneWidget::setPreviewResolution(float dpi)
0368 {
0369     d->m_previewDPI = dpi;
0370 }
0371 
0372 void KSaneWidget::getOptionValues(QMap <QString, QString> &opts)
0373 {
0374     opts.clear();
0375     opts = d->m_ksaneCoreInterface->getOptionsMap();
0376 }
0377 
0378 bool KSaneWidget::getOptionValue(const QString &option, QString &value)
0379 {
0380     const auto optionsMap = d->m_ksaneCoreInterface->getOptionsMap();
0381     auto it = optionsMap.constBegin();
0382     while (it != optionsMap.constEnd()) {
0383         if(it.key() == option) {
0384             value = it.value();
0385             return !value.isEmpty();
0386         }
0387         it++;
0388     }
0389     return false;
0390 }
0391 
0392 int KSaneWidget::setOptionValues(const QMap <QString, QString> &options)
0393 {
0394     int ret = 0;
0395 
0396     ret = d->m_ksaneCoreInterface->setOptionsMap(options);
0397 
0398     if ((d->m_splitGamChB) &&
0399             (d->m_optGamR) &&
0400             (d->m_optGamG) &&
0401             (d->m_optGamB)) {
0402         // check if the current gamma values are identical. if they are identical,
0403         // uncheck the "Separate color intensity tables" checkbox
0404         QVariant redGamma = d->m_optGamR->value();
0405         QVariant greenGamma = d->m_optGamG->value();
0406         QVariant blueGamma = d->m_optGamB->value();
0407 
0408         if ((redGamma == greenGamma) && (greenGamma == blueGamma)) {
0409             d->m_splitGamChB->setChecked(false);
0410             // set the values to the common gamma widget
0411             d->m_commonGamma->setValues(redGamma);
0412         } else {
0413             d->m_splitGamChB->setChecked(true);
0414         }
0415     }
0416     return ret;
0417 }
0418 
0419 bool KSaneWidget::setOptionValue(const QString &option, const QString &value)
0420 {
0421     if (d->m_scanOngoing) {
0422         return false;
0423     }
0424 
0425     const auto optionsList = d->m_ksaneCoreInterface->getOptionsList();
0426     for (auto &writeOption : optionsList) {
0427         if (writeOption->name() == option) {
0428             if (writeOption->setValue(value)) {
0429                 if ((d->m_splitGamChB) &&
0430                         (d->m_optGamR) &&
0431                         (d->m_optGamG) &&
0432                         (d->m_optGamB) &&
0433                         ((writeOption == d->m_optGamR) ||
0434                         (writeOption == d->m_optGamG) ||
0435                         (writeOption == d->m_optGamB))) {
0436                     // check if the current gamma values are identical. if they are identical,
0437                     // uncheck the "Separate color intensity tables" checkbox
0438                     QVariant redGamma = d->m_optGamR->value();
0439                     QVariant greenGamma = d->m_optGamG->value();
0440                     QVariant blueGamma = d->m_optGamB->value();
0441                     if ((redGamma == greenGamma) && (greenGamma == blueGamma)) {
0442                         d->m_splitGamChB->setChecked(false);
0443                         // set the values to the common gamma widget
0444                         d->m_commonGamma->setValues(redGamma);
0445                     } else {
0446                         d->m_splitGamChB->setChecked(true);
0447                     }
0448                 }
0449                 return true;
0450             }
0451         }
0452     }
0453     return false;
0454 }
0455 
0456 void KSaneWidget::enableAutoSelect(bool enable)
0457 {
0458     d->m_autoSelect = enable;
0459 }
0460 
0461 float KSaneWidget::scanAreaWidth()
0462 {
0463     float result = 0.0;
0464     if (d->m_optBrX) {
0465         if (d->m_optBrX->valueUnit() == KSaneCore::Option::UnitPixel) {
0466             result = d->m_optBrX->maximumValue().toFloat();
0467             float dpi = 0;
0468             if (d->m_optRes) {
0469                 dpi = d->m_optRes->value().toFloat();
0470             }
0471             if (dpi < 1) {
0472                 qCDebug(KSANE_LOG) << "Broken DPI value";
0473                 dpi = 1.0;
0474             }
0475             result = result / dpi / 25.4;
0476         } else if (d->m_optBrX->valueUnit() == KSaneCore::Option::UnitMilliMeter) {
0477             result = d->m_optBrX->maximumValue().toFloat();
0478         }
0479     }
0480     return result;
0481 }
0482 
0483 float KSaneWidget::scanAreaHeight()
0484 {
0485     float result = 0.0;
0486     if (d->m_optBrY) {
0487         if (d->m_optBrY->valueUnit() == KSaneCore::Option::UnitPixel) {
0488             result = d->m_optBrY->maximumValue().toFloat();
0489             float dpi = 0;
0490             if (d->m_optRes) {
0491                 dpi = d->m_optRes->value().toFloat();
0492             }
0493             if (dpi < 1) {
0494                 qCDebug(KSANE_LOG) << "Broken DPI value";
0495                 dpi = 1.0;
0496             }
0497             result = result / dpi / 25.4;
0498         } else if (d->m_optBrY->valueUnit() == KSaneCore::Option::UnitMilliMeter) {
0499             result = d->m_optBrY->maximumValue().toFloat();
0500         }
0501     }
0502     return result;
0503 }
0504 
0505 void KSaneWidget::setSelection(QPointF topLeft, QPointF bottomRight)
0506 {
0507     if (!d->m_optBrX || !d->m_optBrY || !d->m_optTlX || !d->m_optTlY) {
0508         return;
0509     }
0510     if (topLeft.x() < 0.0 || topLeft.y() < 0.0 || bottomRight.x() < 0.0 || bottomRight.y() < 0.0) {
0511         d->m_previewViewer->clearActiveSelection();
0512         return;
0513     }
0514 
0515     float tlxRatio = d->scanAreaToRatioX(topLeft.x());
0516     float tlyRatio = d->scanAreaToRatioY(topLeft.y());
0517     float brxRatio = d->scanAreaToRatioX(bottomRight.x());
0518     float bryRatio = d->scanAreaToRatioX(bottomRight.y());
0519 
0520     d->m_previewViewer->setSelection(tlxRatio, tlyRatio, brxRatio, bryRatio);
0521 }
0522 
0523 }  // NameSpace KSaneIface
0524 
0525 #include "moc_ksanewidget.cpp"