File indexing completed on 2024-04-14 04:02:25

0001 /*
0002     This file is part of the KDE games lskat program
0003     SPDX-FileCopyrightText: 2006 Martin Heni <kde@heni-online.de>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-or-later
0006 */
0007 
0008 #include "mainwindow.h"
0009 
0010 // Include files for Qt
0011 #include <QAction>
0012 #include <QDir>
0013 #include <QKeySequence>
0014 #include <QPointer>
0015 #include <QRandomGenerator>
0016 #include <QStatusBar>
0017 #include <QStandardPaths>
0018 
0019 // KF includes
0020 #include <KActionCollection>
0021 #include <KConfigGroup>
0022 #include <KMessageBox>
0023 #include <KSharedConfig>
0024 #include <KGameStandardAction>
0025 #include <KLocalizedString>
0026 #include <KSelectAction>
0027 // Application specific includes
0028 #include "lskat_debug.h"
0029 #include "lskatglobal.h"
0030 #include "gameview.h"
0031 #include "abstractengine.h"
0032 #include "engine_two.h"
0033 #include "display_two.h"
0034 #include "config_two.h"
0035 #include "display_intro.h"
0036 #include "deck.h"
0037 #include "player.h"
0038 #include "mouseinput.h"
0039 #include "aiinput.h"
0040 #include "namedialogwidget.h"
0041 #include "fromlibkdegames/kcarddialog.h"
0042 #include "fromlibkdegames/carddeckinfo.h"
0043 
0044 // Configuration file
0045 #include <config-src.h>
0046 
0047 // Forward declarations
0048 const int ADVANCE_PERIOD =  20;
0049 
0050 // Shortcut to access the actions
0051 #define ACTION(x)   (actionCollection()->action(x))
0052 
0053 using namespace InputDevice;
0054 
0055 // Construct the main application window
0056 Mainwindow::Mainwindow(QWidget *parent)
0057           : KXmlGuiWindow(parent)
0058 {
0059     // Reset stuff
0060     mDeck        = nullptr;
0061     mEngine      = nullptr;
0062     mDisplay     = nullptr;
0063     mView        = nullptr;
0064     mLSkatConfig = nullptr;
0065     mCanvas      = nullptr;
0066     mTheme       = nullptr;
0067 
0068     // Read theme files
0069     QStringList themeList;
0070     const QStringList dirs = QStandardPaths::locateAll(QStandardPaths::AppDataLocation, QStringLiteral("themes"), QStandardPaths::LocateDirectory);
0071     for (const QString& dir : dirs) {
0072         const QStringList fileNames = QDir(dir).entryList({QStringLiteral("*.desktop")});
0073         themeList.reserve(themeList.size() + fileNames.size());
0074         for (const QString& file : fileNames) {
0075             themeList.append(dir + QLatin1Char('/') + file);
0076         }
0077     }
0078     if (themeList.isEmpty())
0079     {
0080         KMessageBox::error(this, i18n("Installation error: No theme list found."));
0081         QTimer::singleShot(0, this, &QWidget::close);
0082         return;
0083     }
0084 
0085     // Read theme files
0086     for (const QString &themePath : std::as_const(themeList)) {
0087         KConfig themeInfo(themePath, KConfig::SimpleConfig);
0088         KConfigGroup themeGroup(&themeInfo, QStringLiteral("Theme"));
0089         QString name = themeGroup.readEntry("Name", QString());
0090         QString file = themeGroup.readEntry("File", QString());
0091         bool isDefault = themeGroup.readEntry("Default", false);
0092         mThemeFiles[name] = file;
0093         if (mThemeDefault.isNull()) mThemeDefault = name;
0094         if (isDefault) mThemeDefault = name;
0095 
0096         if (global_debug > 0) qCDebug(LSKAT_LOG) << "Found theme: " << themePath << " Name(i18n)=" << name << " File=" << file << " default=" << isDefault;
0097     }
0098     mThemeIndexNo = themeIdxFromName(mThemeDefault);
0099 
0100     // Create menus etc
0101     initGUI();
0102 
0103     // The LSkat config
0104     mLSkatConfig = new ConfigTwo(this);
0105     connect(mLSkatConfig, &ConfigTwo::signalInputType, this, &Mainwindow::setInputType);
0106     mLSkatConfig->reset();
0107 
0108     // Read game properties and set default values (after config)
0109     readProperties();
0110 
0111     // TODO: Bugfix: Needs to be here if initGUI is before readProperties
0112     if (global_debug > 0) qCDebug(LSKAT_LOG) << "Setting current theme item to " << mThemeIndexNo;
0113     ((KSelectAction *)ACTION(QStringLiteral("theme")))->setCurrentItem(mThemeIndexNo);
0114 
0115     // Get the card deck
0116     const quint32 seed = QRandomGenerator::global()->generate();
0117     if (global_debug > 0) qCDebug(LSKAT_LOG) << "Random seed" << seed;
0118     mDeck = new Deck(seed, this);
0119 
0120     // Theme manager
0121     QString themeFile = themefileFromIdx(mThemeIndexNo);
0122     if (global_debug > 0) qCDebug(LSKAT_LOG) << "Load theme" << themeFile << " no=" << mThemeIndexNo;
0123     mTheme  = new ThemeManager(mCardTheme, themeFile, this, this->width());
0124     if (mTheme->checkTheme() != 0)
0125     {
0126         KMessageBox::error(this, i18n("Installation error: Theme file error."));
0127         QTimer::singleShot(0, this, &QWidget::close);
0128         return;
0129     }
0130 
0131     // Overall view
0132     mCanvas        = new QGraphicsScene(this);
0133     mView          = new GameView(QSize(880, 675), ADVANCE_PERIOD, mCanvas, mTheme, this);
0134 
0135     // Create intro
0136     mGameMode      = Intro;
0137     mDisplay       = new DisplayIntro(mDeck, mCanvas, mTheme, ADVANCE_PERIOD, mView);
0138     setCentralWidget(mView);
0139     connect(mView, &GameView::signalLeftMousePress, this, &Mainwindow::menuNewLSkatGame);
0140 
0141     // Create GUI
0142     setupGUI();
0143 
0144     statusBar()->showMessage(i18n("Welcome to Skat! Please start a new game."));
0145 
0146     // Skip intro?
0147     if (global_skip_intro)
0148     {
0149         menuNewLSkatGame();
0150     }
0151     // Start game automatically in demo mode
0152     else if (global_demo_mode)
0153     {
0154         // Start intro
0155         mDisplay->start();
0156         QTimer::singleShot(12500, this, &Mainwindow::menuNewLSkatGame);
0157     }
0158     else
0159     {
0160         // Start intro
0161         mDisplay->start();
0162     }
0163     if (global_debug > 0) qCDebug(LSKAT_LOG) << "Mainwindow setup constructor done";
0164 }
0165 
0166 // Destructor
0167 Mainwindow::~Mainwindow()
0168 {
0169     saveProperties();
0170     delete mEngine;
0171     delete mDisplay;
0172     delete mLSkatConfig;
0173     delete mDeck;
0174     delete mView;
0175     delete mCanvas;
0176     delete mTheme;
0177 }
0178 
0179 // Called by KMainWindow when the last window of the application is
0180 void Mainwindow::closeEvent(QCloseEvent *event)
0181 {
0182     if (mEngine)
0183     {
0184         mEngine->stopGame();
0185     }
0186     saveProperties();
0187     KXmlGuiWindow::closeEvent(event);
0188 }
0189 
0190 // Retrieve a theme file name from the menu index number
0191 QString Mainwindow::themefileFromIdx(int idx)
0192 {
0193     QStringList list(mThemeFiles.keys());
0194     list.sort();
0195     QString themeFile = mThemeFiles[list.at(idx)];
0196     return themeFile;
0197 }
0198 
0199 // Retrieve a theme idx from a theme name
0200 int Mainwindow::themeIdxFromName(const QString &name)
0201 {
0202     QStringList list(mThemeFiles.keys());
0203     list.sort();
0204     for (int i = 0; i < list.size(); ++i)
0205     {
0206         if (list[i] == name) return i;
0207     }
0208     qCCritical(LSKAT_LOG) << "Theme index lookup failed for " << name;
0209     return 0;
0210 }
0211 
0212 // Save properties
0213 void Mainwindow::saveProperties()
0214 {
0215     KConfig *config = KSharedConfig::openConfig().data();
0216 
0217     // Program data
0218     KConfigGroup cfg = config->group(QStringLiteral("ProgramData"));
0219     cfg.writeEntry("startplayer", mStartPlayer);
0220     cfg.writeEntry("ThemeIndexNo", mThemeIndexNo);
0221 
0222     // LSkat data
0223     mLSkatConfig->save(config);
0224     config->sync();
0225 }
0226 
0227 // Load properties
0228 void Mainwindow::readProperties()
0229 {
0230     KConfig *config = KSharedConfig::openConfig().data();
0231 
0232     // Program data
0233     KConfigGroup cfg = config->group(QStringLiteral("ProgramData"));
0234 
0235     // Theme number
0236     mThemeIndexNo = cfg.readEntry("ThemeIndexNo", themeIdxFromName(mThemeDefault));
0237     if (mThemeIndexNo >= mThemeFiles.size()) mThemeIndexNo = 0;
0238 
0239     // Read card path
0240     mCardTheme  = CardDeckInfo::deckName(cfg);
0241 
0242     int no = cfg.readEntry("startplayer", 0);
0243     setStartPlayer(no);
0244     mLSkatConfig->load(config);
0245 }
0246 
0247 // Create a input with the given type
0248 AbstractInput *Mainwindow::createInput(
0249                                  InputDeviceType inputType,
0250                                  AbstractDisplay *display,
0251                                  AbstractEngine *engine)
0252 {
0253     AbstractInput *input = nullptr;
0254 
0255     // Always use AI input in demo mode
0256     if (global_demo_mode)
0257     {
0258         inputType = TypeAiInput;
0259     }
0260 
0261     // Create the player input
0262     if (inputType == TypeMouseInput)
0263     {
0264         MouseInput *mouseInput = new MouseInput(this);
0265         connect(mView, &GameView::signalLeftMousePress,
0266                 mouseInput, &MouseInput::mousePress);
0267         connect(mouseInput, &MouseInput::signalConvertMousePress,
0268                 display, &AbstractDisplay::convertMousePress);
0269         connect(mouseInput, &MouseInput::signalPlayerInput,
0270                 engine, &AbstractEngine::playerInput);
0271         input = mouseInput;
0272         if (global_debug > 0) qCDebug(LSKAT_LOG) << "Create MOUSE INPUT";
0273     }
0274     else if (inputType == TypeAiInput)
0275     {
0276         AiInput *aiInput = new AiInput((EngineTwo *)engine, this);
0277         connect(aiInput, &AiInput::signalPlayerInput,
0278                 engine, &AbstractEngine::playerInput);
0279         input = aiInput;
0280         if (global_debug > 0) qCDebug(LSKAT_LOG) << "Create AI INPUT";
0281     }
0282     else
0283     {
0284         qCCritical(LSKAT_LOG) << "Unsupported input device type" << inputType;
0285     }
0286 
0287     return input;
0288 }
0289 
0290 // Start a new game
0291 void Mainwindow::startGame()
0292 {
0293     // Enable game action
0294     const QString endName(KGameStandardAction::name(KGameStandardAction::End));
0295     ACTION(endName)->setEnabled(true);
0296 
0297     // Deal cards to player - Shuffle card deck and reset pile
0298     mDeck->shuffle();
0299 
0300     // Draw Trump
0301     Suite trump = mDeck->randomTrump();
0302 
0303     // Loop all players in the game
0304     QHashIterator<int,Player *> it = mLSkatConfig->playerIterator();
0305     while (it.hasNext())
0306     {
0307         it.next();
0308         Player *player = it.value();
0309         player->setDeck(mDeck);
0310         // Deal cards
0311         player->deal(16);
0312         // Store trump
0313         player->setTrump(trump);
0314     }
0315 
0316     // Start display
0317     mDisplay->start();
0318 
0319     // Start the game engine
0320     mEngine->startGame(trump, mStartPlayer);
0321 
0322     // Start player for next game
0323     setStartPlayer(1-mStartPlayer);
0324 
0325     //  statusBar()->clearMessage();
0326 }
0327 
0328 // Here a game over is signalled
0329 void Mainwindow::gameOver(int /*winner*/)
0330 {
0331     const QString endName(KGameStandardAction::name(KGameStandardAction::End));
0332     ACTION(endName)->setEnabled(false);
0333     statusBar()->showMessage(i18n("Game Over. Please start a new game."));
0334 
0335     // Automatically restart game in demo mode
0336     if (global_demo_mode)
0337     {
0338         QTimer::singleShot(10000, this, &Mainwindow::menuNewLSkatGame);
0339     }
0340 }
0341 
0342 // Show next player
0343 void Mainwindow::nextPlayer(Player *player)
0344 {
0345     int no       = player->id() + 1;
0346     QString name = player->name();
0347     statusBar()->showMessage(i18nc("Player name and number", "Next move for %1 (player %2)", name, no));
0348 }
0349 
0350 // Setup the GUI
0351 void Mainwindow::initGUI()
0352 {
0353     QAction *action;
0354 
0355     // Start a new game
0356     action = KGameStandardAction::gameNew(this, &Mainwindow::menuNewLSkatGame, actionCollection());
0357     if (global_demo_mode) action->setEnabled(false);
0358 
0359     // Clear all time statistics
0360     action = KGameStandardAction::clearStatistics(this, &Mainwindow::menuClearStatistics, actionCollection());
0361     action->setWhatsThis(i18n("Clears the all time statistics which is kept in all sessions."));
0362     if (global_demo_mode) action->setEnabled(false);
0363 
0364     // End a game
0365     action = KGameStandardAction::end(this, &Mainwindow::menuEndGame, actionCollection());
0366     action->setWhatsThis(i18n("Ends a currently played game. No winner will be declared."));
0367     action->setEnabled(false);
0368 
0369     // Quit the program
0370     action = KGameStandardAction::quit(this, &QWidget::close, actionCollection());
0371     action->setWhatsThis(i18n("Quits the program."));
0372 
0373     // Determine start player
0374     KSelectAction *startPlayerAct = new KSelectAction(i18n("Starting Player"), this);
0375     actionCollection()->addAction(QStringLiteral("startplayer"), startPlayerAct);
0376     connect(startPlayerAct, &KSelectAction::indexTriggered, this, &Mainwindow::menuStartplayer);
0377     startPlayerAct->setToolTip(i18n("Changing starting player..."));
0378     startPlayerAct->setWhatsThis(i18n("Chooses which player begins the next game."));
0379     startPlayerAct->setItems({
0380         i18n("Player &1"),
0381         i18n("Player &2"),
0382     });
0383     if (global_demo_mode) startPlayerAct->setEnabled(false);
0384 
0385     // Determine who plays player 1
0386     KSelectAction *player1Act = new KSelectAction(i18n("Player &1 Played By"), this);
0387     actionCollection()->addAction(QStringLiteral("player1"), player1Act);
0388     connect(player1Act, &KSelectAction::indexTriggered, this, &Mainwindow::menuPlayer1By);
0389     player1Act->setToolTip(i18n("Changing who plays player 1..."));
0390     player1Act->setWhatsThis(i18n("Changing who plays player 1."));
0391     const QStringList inputDevices {
0392         i18n("&Mouse"),
0393         i18n("&Computer"),
0394     };
0395     player1Act->setItems(inputDevices);
0396     if (global_demo_mode) player1Act->setEnabled(false);
0397 
0398     // Determine who plays player 2
0399     KSelectAction *player2Act = new KSelectAction(i18n("Player &2 Played By"), this);
0400     actionCollection()->addAction(QStringLiteral("player2"), player2Act);
0401     connect(player2Act, &KSelectAction::indexTriggered, this, &Mainwindow::menuPlayer2By);
0402     player2Act->setToolTip(i18n("Changing who plays player 2..."));
0403     player2Act->setWhatsThis(i18n("Changing who plays player 2."));
0404     player2Act->setItems(inputDevices);
0405     if (global_demo_mode) player2Act->setEnabled(false);
0406 
0407     // Add all theme files to the menu
0408     QStringList themes(mThemeFiles.keys());
0409     themes.sort();
0410 
0411     KSelectAction *themeAct = new KSelectAction(i18n("&Theme"), this);
0412     actionCollection()->addAction(QStringLiteral("theme"), themeAct);
0413     themeAct->setItems(themes);
0414     connect(themeAct, &KSelectAction::indexTriggered, this, &Mainwindow::changeTheme);
0415     if (global_debug > 0) qCDebug(LSKAT_LOG) << "Setting current theme item to " << mThemeIndexNo;
0416     themeAct->setCurrentItem(mThemeIndexNo);
0417     themeAct->setToolTip(i18n("Changing theme..."));
0418     themeAct->setWhatsThis(i18n("Changing theme."));
0419 
0420     // Choose card deck
0421     QAction *action1 = actionCollection()->addAction(QStringLiteral("select_carddeck"));
0422     action1->setText(i18n("Select &Card Deck..."));
0423     connect(action1, &QAction::triggered, this, &Mainwindow::menuCardDeck);
0424     action1->setToolTip(i18n("Configure card decks..."));
0425     action1->setWhatsThis(i18n("Choose how the cards should look."));
0426 
0427     // Change player names
0428     action = actionCollection()->addAction(QStringLiteral("change_names"));
0429     action->setText(i18n("&Change Player Names..."));
0430     connect(action, &QAction::triggered, this, &Mainwindow::menuPlayerNames);
0431     if (global_demo_mode) action->setEnabled(false);
0432 }
0433 
0434 // Choose start player
0435 void Mainwindow::menuStartplayer()
0436 {
0437     int i = ((KSelectAction *)ACTION(QStringLiteral("startplayer")))->currentItem();
0438     setStartPlayer(i);
0439 }
0440 
0441 // Change the theme of the game
0442 void Mainwindow::changeTheme(int idx)
0443 {
0444     mThemeIndexNo = idx;
0445     QString themeFile = themefileFromIdx(idx);
0446     if (global_debug > 0) qCDebug(LSKAT_LOG) << "Select theme " << themeFile;
0447     mTheme->updateTheme(themeFile);
0448 }
0449 
0450 // Select input for player 1
0451 void Mainwindow::menuPlayer1By()
0452 {
0453     int i = ((KSelectAction *)ACTION(QStringLiteral("player1")))->currentItem();
0454     mLSkatConfig->setInputType(0, (InputDeviceType)i);
0455 }
0456 
0457 // Select input for player 2
0458 void Mainwindow::menuPlayer2By()
0459 {
0460     int i = ((KSelectAction *)ACTION(QStringLiteral("player2")))->currentItem();
0461     mLSkatConfig->setInputType(1, (InputDeviceType)i);
0462 }
0463 
0464 // Choose a card deck
0465 void Mainwindow::menuCardDeck()
0466 {
0467     QString front = mCardTheme;
0468 
0469     KConfigGroup grp = KSharedConfig::openConfig()->group(QStringLiteral("ProgramData"));
0470     KCardWidget *cardwidget = new KCardWidget();
0471     QPointer<KCardDialog> dlg;
0472 
0473     cardwidget->readSettings(grp);
0474     dlg = new KCardDialog(cardwidget);
0475     if (dlg->exec() == QDialog::Accepted)
0476     {
0477         // Always store the settings, other things than the deck may have changed
0478         cardwidget->saveSettings(grp);
0479         grp.sync();
0480         if (global_debug > 0) qCDebug(LSKAT_LOG) << "NEW CARDDECK: " << front;
0481         bool change = false; // Avoid unnecessary changes
0482         if (!cardwidget->deckName().isEmpty() && cardwidget->deckName() != mCardTheme)
0483         {
0484             mCardTheme    = cardwidget->deckName();
0485             change = true;
0486         }
0487         if (change)
0488         {
0489             mTheme->updateCardTheme(mCardTheme);
0490             mView->update(); // Be on the safe side and update
0491         }
0492     }
0493     delete dlg;
0494 }
0495 
0496 // Clear all time statistics
0497 void Mainwindow::menuClearStatistics()
0498 {
0499     QString message;
0500     message = i18n("Do you really want to clear the all time "
0501                     "statistical data?");
0502 
0503     if (KMessageBox::PrimaryAction == KMessageBox::questionTwoActions(this,
0504                                                        message,
0505                                                        QString(),
0506                                                        KStandardGuiItem::clear(),
0507                                                        KStandardGuiItem::cancel()))
0508     {
0509         QHashIterator<int,Player *> it = mLSkatConfig->playerIterator();
0510         while (it.hasNext())
0511         {
0512             it.next();
0513             Player *player = it.value();
0514             player->clear();
0515         }
0516     }
0517 }
0518 
0519 // Abort a game
0520 void Mainwindow::menuEndGame()
0521 {
0522     if (mEngine)
0523     {
0524         mEngine->stopGame();
0525     }
0526 }
0527 
0528 // Start a new game
0529 void Mainwindow::menuNewLSkatGame()
0530 {
0531     disconnect(mView, &GameView::signalLeftMousePress, this, &Mainwindow::menuNewLSkatGame);
0532 
0533     Player *p1 = mLSkatConfig->player(0);
0534     Player *p2 = mLSkatConfig->player(1);
0535 
0536     // Stop running games
0537     if (mEngine)
0538     {
0539         mEngine->stopGame();
0540     }
0541 
0542     // Get rid of old stuff?
0543     if (true || (mGameMode != LSkat)) // Yes! Fixes bugs 330308 and 228067.
0544     // Jenkins objected to the indentation if the above was simply commented out.
0545     {
0546         // Set new game mode
0547         mGameMode = LSkat;
0548 
0549         // Start deleting
0550         delete mDisplay;
0551         delete mEngine;
0552 
0553         auto *display = new DisplayTwo(mDeck, mCanvas, mTheme, ADVANCE_PERIOD, mView);
0554         mDisplay = display;
0555         mEngine = new EngineTwo(this, mDeck, (DisplayTwo *)mDisplay);
0556         connect(mEngine, &AbstractEngine::signalGameOver, this, &Mainwindow::gameOver);
0557         connect(mEngine, &AbstractEngine::signalNextPlayer, this, &Mainwindow::nextPlayer);
0558 
0559         // Connect player score widget updates
0560         connect(p1, &Player::signalUpdate, display, &DisplayTwo::updatePlayer);
0561         connect(p2, &Player::signalUpdate, display, &DisplayTwo::updatePlayer);
0562 
0563         mEngine->addPlayer(0, p1);
0564         mEngine->addPlayer(1, p2);
0565     }// end if
0566 
0567     // Create inputs and store in player
0568     AbstractInput *input1 = createInput(mLSkatConfig->inputType(0), mDisplay, mEngine);
0569     p1->setInput(input1);
0570     AbstractInput *input2 = createInput(mLSkatConfig->inputType(1), mDisplay, mEngine);
0571     p2->setInput(input2);
0572 
0573     statusBar()->showMessage(i18n("Dealing cards..."));
0574 
0575     // Start game
0576     startGame();
0577 }
0578 
0579 // Change the player names in a dialog
0580 void Mainwindow::menuPlayerNames()
0581 {
0582     QPointer<NameDialogWidget> dlg = new NameDialogWidget(this);
0583     for (int i = 0; i < 2; i++)
0584     {
0585         Player *p = mLSkatConfig->player(i);
0586         dlg->setName(i, p->name());
0587     }
0588 
0589     if (dlg->exec() == QDialog::Accepted)
0590     {
0591         for (int i = 0; i < 2; i++)
0592         {
0593             Player *p = mLSkatConfig->player(i);
0594             p->setName(dlg->name(i));
0595         }
0596     }
0597 
0598     delete dlg;
0599 }
0600 
0601 // Set the start player.
0602 void Mainwindow::setStartPlayer(int no)
0603 {
0604     mStartPlayer = no;
0605     ((KSelectAction *)ACTION(QStringLiteral("startplayer")))->setCurrentItem(mStartPlayer);
0606 }
0607 
0608 // Set the input type for a given player number.
0609 void Mainwindow::setInputType(int no, InputDeviceType type)
0610 {
0611     Player *p = nullptr;
0612     // Player 1
0613     if (no == 0)
0614     {
0615         ((KSelectAction *)ACTION(QStringLiteral("player1")))->setCurrentItem((int)type);
0616         p = mLSkatConfig->player(0);
0617     }
0618     else if (no == 1)
0619     {
0620         ((KSelectAction *)ACTION(QStringLiteral("player2")))->setCurrentItem((int)type);
0621         p = mLSkatConfig->player(1);
0622     }
0623 
0624     // Exchange player input at runtime
0625     if (mEngine && p && mDisplay && mEngine->isGameRunning())
0626     {
0627         AbstractInput *input = createInput(type, mDisplay, mEngine);
0628         p->setInput(input);
0629     }
0630 }
0631 
0632 #include "moc_mainwindow.cpp"