diff options
Diffstat (limited to 'libkdegames/highscore')
20 files changed, 4987 insertions, 0 deletions
diff --git a/libkdegames/highscore/INSTALL b/libkdegames/highscore/INSTALL new file mode 100644 index 00000000..a16fa57d --- /dev/null +++ b/libkdegames/highscore/INSTALL @@ -0,0 +1,12 @@ +Installation notes for the highscore files ; this is only relevant if you +configured libkdegames with option --enable-highscore-dir=DIR (usually DIR is +/var/games) for using system-wide highscore files. + +For each game using the highscore system : + +- the game executable "mygame" should be installed sgid "games" + +- an empty file "mygame.scores" should be created in the directory pointed by +the configuration option. It should be owned by group "games" with read and +write permissions and should -not- be world readable (since it can contains +possibly sensitive information associating username with game usage). diff --git a/libkdegames/highscore/Makefile.am b/libkdegames/highscore/Makefile.am new file mode 100644 index 00000000..6fa18cc0 --- /dev/null +++ b/libkdegames/highscore/Makefile.am @@ -0,0 +1,19 @@ +noinst_LTLIBRARIES = libkhighscore.la + +INCLUDES = $(all_includes) + +libkhighscore_la_SOURCES = kconfigrawbackend.cpp \ + kfilelock.cpp khighscore.cpp kscoredialog.cpp \ + kexthighscore_item.cpp kexthighscore_internal.cpp \ + kexthighscore_tab.cpp kexthighscore_gui.cpp \ + kexthighscore.cpp + +include_HEADERS = khighscore.h kscoredialog.h \ + kexthighscore_item.h kexthighscore.h + +noinst_HEADERS = kconfigrawbackend.h \ + kfilelock.h kexthighscore_internal.h kexthighscore_tab.h \ + kexthighscore_gui.h + +METASOURCES = kconfigrawbackend.moc khighscore.moc kscoredialog.moc \ + kexthighscore_tab.moc kexthighscore_gui.moc diff --git a/libkdegames/highscore/kconfigrawbackend.cpp b/libkdegames/highscore/kconfigrawbackend.cpp new file mode 100644 index 00000000..a379ba23 --- /dev/null +++ b/libkdegames/highscore/kconfigrawbackend.cpp @@ -0,0 +1,62 @@ +/* + This file is part of the KDE games library + Copyright (C) 2003 Nicolas Hadacek <[email protected]> + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License version 2 as published by the Free Software Foundation. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#include "kconfigrawbackend.h" +#include "kconfigrawbackend.moc" + +#include <unistd.h> +#include <qfile.h> + + +KConfigRawBackEnd::KConfigRawBackEnd(KConfigBase *_config, int fd) + : KConfigINIBackEnd(_config, QString::null, "config", false), + _fd(fd), _stream(0) +{ + _file.open(IO_ReadOnly, _fd); +} + +KConfigRawBackEnd::~KConfigRawBackEnd() +{ + if (_stream) fclose(_stream); +} + +bool KConfigRawBackEnd::parseConfigFiles() +{ + _file.reset(); + parseSingleConfigFile(_file); + return true; +} + +void KConfigRawBackEnd::sync(bool bMerge) +{ + // write-sync is only necessary if there are dirty entries + if ( !pConfig->isDirty() || pConfig->isReadOnly() ) return; + + _file.reset(); + KEntryMap aTempMap; + getEntryMap(aTempMap, false, bMerge ? &_file : 0); + + if ( _stream==0 ) { + _stream = fdopen(_fd, "w"); + if ( _stream==0 ) return; + } + ftruncate(_fd, 0); + writeEntries(_stream, aTempMap); + fflush(_stream); +} diff --git a/libkdegames/highscore/kconfigrawbackend.h b/libkdegames/highscore/kconfigrawbackend.h new file mode 100644 index 00000000..4b780320 --- /dev/null +++ b/libkdegames/highscore/kconfigrawbackend.h @@ -0,0 +1,57 @@ +/* + This file is part of the KDE games library + Copyright (C) 2003 Nicolas Hadacek <[email protected]> + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License version 2 as published by the Free Software Foundation. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#ifndef _KCONFIGRAWBACKEND_H +#define _KCONFIGRAWBACKEND_H + +#include <qfile.h> + +#include <kconfigbackend.h> +#include <ksimpleconfig.h> + + +class KConfigRawBackEnd : public KConfigINIBackEnd +{ +public: + KConfigRawBackEnd(KConfigBase *_config, int fd); + ~KConfigRawBackEnd(); + + bool parseConfigFiles(); + + void sync(bool bMerge = true); + +private: + int _fd; + FILE *_stream; + QFile _file; + + class KConfigRawBackEndPrivate; + KConfigRawBackEndPrivate *d; +}; + +class KRawConfig : public KSimpleConfig +{ + Q_OBJECT +public: + KRawConfig(int fd, bool readOnly) + : KSimpleConfig(new KConfigRawBackEnd(this, fd), readOnly) {} +}; + + +#endif diff --git a/libkdegames/highscore/kexthighscore.cpp b/libkdegames/highscore/kexthighscore.cpp new file mode 100644 index 00000000..0ad9b3af --- /dev/null +++ b/libkdegames/highscore/kexthighscore.cpp @@ -0,0 +1,289 @@ +/* + This file is part of the KDE games library + Copyright (C) 2001-2004 Nicolas Hadacek ([email protected]) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License version 2 as published by the Free Software Foundation. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#include "kexthighscore.h" + +#include <qlayout.h> + +#include <kdebug.h> + +#include "kexthighscore_internal.h" +#include "kexthighscore_gui.h" + + +namespace KExtHighscore +{ + +//----------------------------------------------------------------------------- +ManagerPrivate *internal = 0; + +uint gameType() +{ + internal->checkFirst(); + return internal->gameType(); +} + +void setGameType(uint type) +{ + internal->setGameType(type); +} + +bool configure(QWidget *parent) +{ + internal->checkFirst(); + ConfigDialog *cd = new ConfigDialog(parent); + cd->exec(); + bool saved = cd->hasBeenSaved(); + delete cd; + return saved; +} + +void show(QWidget *parent, int rank) +{ + HighscoresDialog *hd = new HighscoresDialog(rank, parent); + hd->exec(); + delete hd; +} + +void submitScore(const Score &score, QWidget *widget) +{ + int rank = internal->submitScore(score, widget, + internal->showMode!=Manager::NeverShow); + + switch (internal->showMode) { + case Manager::AlwaysShow: + show(widget, -1); + break; + case Manager::ShowForHigherScore: + if ( rank!=-1) show(widget, rank); + break; + case Manager::ShowForHighestScore: + if ( rank==0 ) show(widget, rank); + break; + case Manager::NeverShow: + break; + } +} + +void show(QWidget *widget) +{ + internal->checkFirst(); + show(widget, -1); +} + +Score lastScore() +{ + internal->checkFirst(); + internal->hsConfig().readCurrentConfig(); + uint nb = internal->scoreInfos().maxNbEntries(); + return internal->readScore(nb-1); +} + +Score firstScore() +{ + internal->checkFirst(); + internal->hsConfig().readCurrentConfig(); + return internal->readScore(0); +} + + +//----------------------------------------------------------------------------- +Manager::Manager(uint nbGameTypes, uint maxNbEntries) +{ + Q_ASSERT(nbGameTypes); + Q_ASSERT(maxNbEntries); + if (internal) + kdFatal(11002) << "A highscore object already exists" << endl; + internal = new ManagerPrivate(nbGameTypes, *this); + internal->init(maxNbEntries); +} + +Manager::~Manager() +{ + delete internal; + internal = 0; +} + +void Manager::setTrackLostGames(bool track) +{ + internal->trackLostGames = track; +} + +void Manager::setTrackDrawGames(bool track) +{ + internal->trackDrawGames = track; +} + +void Manager::setShowStatistics(bool show) +{ + internal->showStatistics = show; +} + +void Manager::showStatistics(bool show) +{ + internal->showStatistics = show; +} + +void Manager::setShowDrawGamesStatistic(bool show) +{ + internal->showDrawGames = show; +} + +void Manager::setWWHighscores(const KURL &url, const QString &version) +{ + Q_ASSERT( url.isValid() ); + internal->serverURL = url; + const char *HS_WW_URL = "ww hs url"; + ConfigGroup cg; + if ( cg.config()->hasKey(HS_WW_URL) ) + internal->serverURL = cg.config()->readEntry(HS_WW_URL); + else cg.config()->writeEntry(HS_WW_URL, url.url()); + internal->version = version; +} + +void Manager::setScoreHistogram(const QMemArray<uint> &scores, + ScoreTypeBound type) +{ + Q_ASSERT( scores.size()>=2 ); + for (uint i=0; i<scores.size()-1; i++) + Q_ASSERT( scores[i]<scores[i+1] ); + internal->playerInfos().createHistoItems(scores, type==ScoreBound); +} + +void Manager::setShowMode(ShowMode mode) +{ + internal->showMode = mode; +} + +void Manager::setScoreType(ScoreType type) +{ + switch (type) { + case Normal: + return; + case MinuteTime: { + Item *item = createItem(ScoreDefault); + item->setPrettyFormat(Item::MinuteTime); + setScoreItem(0, item); + + item = createItem(MeanScoreDefault); + item->setPrettyFormat(Item::MinuteTime); + setPlayerItem(MeanScore, item); + + item = createItem(BestScoreDefault); + item->setPrettyFormat(Item::MinuteTime); + setPlayerItem(BestScore, item); + return; + } + } +} + +void Manager::submitLegacyScore(const Score &score) const +{ + internal->submitLocal(score); +} + +bool Manager::isStrictlyLess(const Score &s1, const Score &s2) const +{ + return s1.score()<s2.score(); +} + +Item *Manager::createItem(ItemType type) +{ + Item *item = 0; + switch (type) { + case ScoreDefault: + item = new Item((uint)0, i18n("Score"), Qt::AlignRight); + break; + case MeanScoreDefault: + item = new Item((double)0, i18n("Mean Score"), Qt::AlignRight); + item->setPrettyFormat(Item::OneDecimal); + item->setPrettySpecial(Item::DefaultNotDefined); + break; + case BestScoreDefault: + item = new Item((uint)0, i18n("Best Score"), Qt::AlignRight); + item->setPrettySpecial(Item::DefaultNotDefined); + break; + case ElapsedTime: + item = new Item((uint)0, i18n("Elapsed Time"), Qt::AlignRight); + item->setPrettyFormat(Item::MinuteTime); + item->setPrettySpecial(Item::ZeroNotDefined); + break; + } + return item; +} + +void Manager::setScoreItem(uint worstScore, Item *item) +{ + item->setDefaultValue(worstScore); + internal->scoreInfos().setItem("score", item); + internal->playerInfos().item("mean score") + ->item()->setDefaultValue(double(worstScore)); + internal->playerInfos().item("best score") + ->item()->setDefaultValue(worstScore); +} + +void Manager::addScoreItem(const QString &name, Item *item) +{ + internal->scoreInfos().addItem(name, item, true); +} + +void Manager::setPlayerItem(PlayerItemType type, Item *item) +{ + const Item *scoreItem = internal->scoreInfos().item("score")->item(); + uint def = scoreItem->defaultValue().toUInt(); + QString name; + switch (type) { + case MeanScore: + name = "mean score"; + item->setDefaultValue(double(def)); + break; + case BestScore: + name = "best score"; + item->setDefaultValue(def); + break; + } + internal->playerInfos().setItem(name, item); +} + +QString Manager::gameTypeLabel(uint gameType, LabelType type) const +{ + if ( gameType!=0 ) + kdFatal(11002) << "You need to reimplement KExtHighscore::Manager for " + << "multiple game types" << endl; + switch (type) { + case Icon: + case Standard: + case I18N: break; + case WW: return "normal"; + } + return QString::null; +} + +void Manager::addToQueryURL(KURL &url, const QString &item, + const QString &content) +{ + Q_ASSERT( !item.isEmpty() && url.queryItem(item).isNull() ); + + QString query = url.query(); + if ( !query.isEmpty() ) query += '&'; + query += item + '=' + KURL::encode_string(content); + url.setQuery(query); +} + +} // namescape diff --git a/libkdegames/highscore/kexthighscore.h b/libkdegames/highscore/kexthighscore.h new file mode 100644 index 00000000..2484f97b --- /dev/null +++ b/libkdegames/highscore/kexthighscore.h @@ -0,0 +1,367 @@ +/* + This file is part of the KDE games library + Copyright (C) 2001-2004 Nicolas Hadacek ([email protected]) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License version 2 as published by the Free Software Foundation. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#ifndef KEXTHIGHSCORE_H +#define KEXTHIGHSCORE_H + +#include "kexthighscore_item.h" + +#include <kurl.h> +#include <kdemacros.h> + +class QTabWidget; + + +namespace KExtHighscore +{ + +class Score; +class Item; + +class ManagerPrivate; +extern ManagerPrivate *internal; + +/** + * Get the current game type. + */ +KDE_EXPORT uint gameType(); + +/** + * Set the current game type. + */ +KDE_EXPORT void setGameType(uint gameType); + +/** + * Configure the highscores. + * @return true if the configuration has been modified and saved + */ +KDE_EXPORT bool configure(QWidget *parent); + +/** + * Show the highscores lists. + */ +KDE_EXPORT void show(QWidget *parent); + +/** + * Submit a score. See @ref Manager for usage example. + * + * @param widget a widget used as parent for error message box. + */ +KDE_EXPORT void submitScore(const Score &score, QWidget *widget); + +/** + * @return the last score in the local list of highscores. The worst possible + * score if there are less items than the maximum number. + */ +KDE_EXPORT Score lastScore(); + +/** + * @return the first score in the local list of highscores (the worst possible + * score if there is no entry). + */ +KDE_EXPORT Score firstScore(); + +/** + * This class manages highscores and players entries (several players can + * share the same highscores list if the libkdegame library is built to + * support a common highscores file; NOTE that to correctly implement such + * feature we probably need a locking mechanism in @ref KHighscore). + * + * You need one instance of this class during the application lifetime ; in + * main() just insert + * \code + * KExtHighscore::Manager highscoresManager; + * \endcode + * with the needed arguments. Use the derived class if you need to + * reimplement some of the default methods. + * + * This class has three functions : + * <ul> + * <li> Update the highscores list when new entries are submitted </li> + * <li> Display the highscores list and the players list </li> + * <li> Send query to an optionnal web server to support world-wide + * highscores </li> + * </ul> + * + * The highscores and the players lists contain several items described by + * the @ref Item class. + * + * The highscores list contains by default : + * <ul> + * <li> the player name (automatically set from the config value)</li> + * <li> the score value </li> + * <li> the time and date of the highscore (automatically set) </li> + * </ul> + * You can replace the score item (for e.g. displaying it differently) with + * setScoreItem or add an item with addScoreItem. + * + * The players list contains : + * <ul> + * <li> the player name (as defined by the user in the configuration + * dialog) </li> + * <li> the number of games played </li> + * <li> the mean score </li> + * <li> the best score </li> + * <li> the best score time and date </li> + * <li> the player comment (as defined by the user in the + * configuration dialog) </li> + * </ul> + * You can replace the best score and the mean score items + * by calling setPlayerItem. + * + * To submit a new score at game end, just construct a Score, set the + * score data and then call submitScore(). + * \code + * KExtHighscore::Score score(KExtHighscore::Won); + * score.setScore(myScore); + * KExtHighscore::submitScore(score, widget); + * \endcode + * You only need to set the score value with Score::setScore() + * and the value of the items that you have optionnally added + * with Score::setData() ; player name and date are set automatically. + */ +class KDE_EXPORT Manager +{ + public: + /** + * Constructor + * + * @param nbGameTypes the number of different game types (usually one). + * For example KMines has easy, normal and expert levels. + * @param maxNbEntries the maximum numbers of highscores entries (by game + * types) + */ + Manager(uint nbGameTypes = 1, uint maxNbEntries = 10); + virtual ~Manager(); + + /** + * Set the world-wide highscores. + * By default there is no world-wide highscores. + * + * Note: should be called at construction time. + * + * @param url the web server url + * @param version the game version which is sent to the web server (it can + * be useful for backward compatibility on the server side). + */ + void setWWHighscores(const KURL &url, const QString &version); + + /** + * Set if the number of lost games should be track for the world-wide + * highscores statistics. By default, there is no tracking. + * False by default. + * + * Note: should be called at construction time. + */ + void setTrackLostGames(bool track); + + /** + * @since 3.3 + * Set if the number of "draw" games should be track for the world-wide + * highscores statistics. By default, there is no tracking. + * False by default. + * + * Note: should be called at construction time. + */ + void setTrackDrawGames(bool track); + + /** + * @since 3.3 + * Set if the statistics tab should be shown in the highscores dialog. + * You only want to show this tab if it makes sense to lose or to win the + * game (for e.g. it makes no sense for a tetris game but it does for a + * minesweeper game). + * False by default. + * + * Note: should be called at construction time. + */ + void setShowStatistics(bool show); + + /** @obsolete */ + // KDE4 remove this + void showStatistics(bool show) KDE_DEPRECATED; + + /** + * @since 3.3 + * Set if draw games statistics should be shown (enable this if + * draws are possible in your game). + * False by default. + */ + void setShowDrawGamesStatistic(bool show); + + enum ScoreTypeBound { ScoreNotBound, ScoreBound }; + /** + * Set the ranges for the score histogram. + * + * Note: should be called at construction time. + */ + void setScoreHistogram(const QMemArray<uint> &scores, ScoreTypeBound type); + + /** + * Enumerate different conditions under which to show the + * high score dialog. + */ + enum ShowMode { AlwaysShow, ///< Always show the dialog + NeverShow, ///< Never show the dialog + ShowForHigherScore, ///< Show if score has improved + ShowForHighestScore ///< Only for the top spot + }; + /** + * Set how the highscores dialog is shown at game end. + * By default, the mode is ShowForHigherScore. + * + * Note: should be called at construction time. + */ + void setShowMode(ShowMode mode); + + /** + * Score type (@see setScoreType). + * @p Normal default score (unsigned integer without upper bound) + * @p MinuteTime score by time bound at 3599 seconds (for e.g. kmines) + */ + enum ScoreType { Normal, MinuteTime }; + /** + * Set score type. Helper method to quickly set the type of score. + * By default the type is Normal. + * + * Note: should be called at construction time. + */ + void setScoreType(ScoreType type); + + /** + * Some predefined item types. + * @p ScoreDefault default item for the score in the highscores list. + * @p MeanScoreDefault default item for the mean score (only show one decimal and + * 0 is shown as "--". + * @p BestScoreDefault default item for the best score (0 is shown as "--"). + * @p ElapsedTime optionnal item for elapsed time (maximum value is 3599 seconds). + */ + enum ItemType { ScoreDefault, MeanScoreDefault, BestScoreDefault, + ElapsedTime }; + /** + * Create a predefined item. + */ + static Item *createItem(ItemType type); + + /** + * Replace the default score item in the highscores list by the given one. + * @p worstScore is the worst possible score. By default it is 0. + * + * Note : This method should be called at construction time. + */ + void setScoreItem(uint worstScore, Item *item); + + /** + * Add an item in the highscores list (it will add a column to this list). + * + * Note : This method should be called at construction time. + */ + void addScoreItem(const QString &name, Item *item); + + enum PlayerItemType { MeanScore, BestScore }; + /** + * Replace an item in the players list. + * + * Note : This method should be called at construction time. + */ + void setPlayerItem(PlayerItemType type, Item *item); + + /** + * @return true if the first score is strictly worse than the second one. + * By default return <pre>s1.score()<s2.score()</pre>. You can reimplement + * this method if additional items added to @ref Score can further + * differentiate the scores (for e.g. the time spent). + * + * Note that you do not need to use directly this method, simply write + * <pre>s1<s2</pre> since the operator calls this method. + */ + virtual bool isStrictlyLess(const Score &s1, const Score &s2) const; + + /** + * Possible type of label (@see gameTypeLabel). + * @p Standard label used in config file. + * @p I18N label used to display the game type. + * @p WW label used when contacting the world-wide highscores server. + * @p Icon label used to load the icon corresponding to the game type. + */ + enum LabelType { Standard, I18N, WW, Icon }; + + /** + * @return the label corresponding to the game type. The default + * implementation works only for one game type : you need to reimplement + * this method if the number of game types is more than one. + */ + virtual QString gameTypeLabel(uint gameType, LabelType type) const; + + protected: + /** + * This method is called once for each player (ie for each user). You + * can reimplement it to convert old style highscores to the new mechanism + * (@see submitLegacyScore). By default this method does nothing. + * + * @param gameType the game type + */ + virtual void convertLegacy(uint gameType) { Q_UNUSED(gameType); } + + /** + * This method should be called from @ref convertLegacy. It is used + * to submit an old highscore (it will not be send over the network). + * For each score do something like: + * \code + * Score score(Won); + * score.setScore(oldScore); + * score.setData("name", name); + * submitLegacyScore(score); + * \endcode + * Note that here you can set the player "name" and the highscore "date" + * if they are known. + */ + void submitLegacyScore(const Score &score) const; + + /** + * This method is called before submitting a score to the world-wide + * highscores server. You can reimplement this method to add an entry + * with @ref addToQueryURL. By default this method does nothing. + * + * @param url the URL to query + * @param score the score to be submitted. + */ + virtual void additionalQueryItems(KURL &url, const Score &score) const + { Q_UNUSED(url); Q_UNUSED(score); } + + /** + * Add an entry to the url to be submitted (@see additionalQueryItems). + * + * @param url the URL to query + * @param item the item name + * @param content the item content + */ + static void addToQueryURL(KURL &url, const QString &item, + const QString &content); + + friend class ManagerPrivate; + + private: + Manager(const Manager &); + Manager &operator =(const Manager &); +}; + +} // namespace + +#endif diff --git a/libkdegames/highscore/kexthighscore_gui.cpp b/libkdegames/highscore/kexthighscore_gui.cpp new file mode 100644 index 00000000..547a885c --- /dev/null +++ b/libkdegames/highscore/kexthighscore_gui.cpp @@ -0,0 +1,552 @@ +/* + This file is part of the KDE games library + Copyright (C) 2001-2003 Nicolas Hadacek ([email protected]) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License version 2 as published by the Free Software Foundation. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#include "kexthighscore_gui.h" +#include "kexthighscore_gui.moc" + +#include <qlayout.h> +#include <qtextstream.h> +#include <qheader.h> +#include <qgrid.h> +#include <qvgroupbox.h> + +#include <kapplication.h> +#include <kmessagebox.h> +#include <kurllabel.h> +#include <kopenwith.h> +#include <krun.h> +#include <kfiledialog.h> +#include <ktempfile.h> +#include <kio/netaccess.h> +#include <kiconloader.h> + +#include "kexthighscore_internal.h" +#include "kexthighscore.h" +#include "kexthighscore_tab.h" + + +namespace KExtHighscore +{ + +//----------------------------------------------------------------------------- +ShowItem::ShowItem(QListView *list, bool highlight) + : KListViewItem(list), _highlight(highlight) +{} + +void ShowItem::paintCell(QPainter *p, const QColorGroup &cg, + int column, int width, int align) +{ + QColorGroup cgrp(cg); + if (_highlight) cgrp.setColor(QColorGroup::Text, red); + KListViewItem::paintCell(p, cgrp, column, width, align); +} + +//----------------------------------------------------------------------------- +ScoresList::ScoresList(QWidget *parent) + : KListView(parent) +{ + setSelectionMode(QListView::NoSelection); + setItemMargin(3); + setAllColumnsShowFocus(true); + setSorting(-1); + header()->setClickEnabled(false); + header()->setMovingEnabled(false); +} + +void ScoresList::addHeader(const ItemArray &items) +{ + addLineItem(items, 0, 0); +} + +QListViewItem *ScoresList::addLine(const ItemArray &items, + uint index, bool highlight) +{ + QListViewItem *item = new ShowItem(this, highlight); + addLineItem(items, index, item); + return item; +} + +void ScoresList::addLineItem(const ItemArray &items, + uint index, QListViewItem *line) +{ + uint k = 0; + for (uint i=0; i<items.size(); i++) { + const ItemContainer &container = *items[i]; + if ( !container.item()->isVisible() ) continue; + if (line) line->setText(k, itemText(container, index)); + else { + addColumn( container.item()->label() ); + setColumnAlignment(k, container.item()->alignment()); + } + k++; + } +} + +//----------------------------------------------------------------------------- +HighscoresList::HighscoresList(QWidget *parent) + : ScoresList(parent) +{} + +QString HighscoresList::itemText(const ItemContainer &item, uint row) const +{ + return item.pretty(row); +} + +void HighscoresList::load(const ItemArray &items, int highlight) +{ + clear(); + QListViewItem *line = 0; + for (int j=items.nbEntries()-1; j>=0; j--) { + QListViewItem *item = addLine(items, j, j==highlight); + if ( j==highlight ) line = item; + } + if (line) ensureItemVisible(line); +} + +//----------------------------------------------------------------------------- +HighscoresWidget::HighscoresWidget(QWidget *parent) + : QWidget(parent, "show_highscores_widget"), + _scoresUrl(0), _playersUrl(0), _statsTab(0), _histoTab(0) +{ + const ScoreInfos &s = internal->scoreInfos(); + const PlayerInfos &p = internal->playerInfos(); + + QVBoxLayout *vbox = new QVBoxLayout(this, KDialogBase::spacingHint()); + + _tw = new QTabWidget(this); + connect(_tw, SIGNAL(currentChanged(QWidget *)), SLOT(tabChanged())); + vbox->addWidget(_tw); + + // scores tab + _scoresList = new HighscoresList(_tw); + _scoresList->addHeader(s); + _tw->addTab(_scoresList, i18n("Best &Scores")); + + // players tab + _playersList = new HighscoresList(_tw); + _playersList->addHeader(p); + _tw->addTab(_playersList, i18n("&Players")); + + // statistics tab + if ( internal->showStatistics ) { + _statsTab = new StatisticsTab(_tw); + _tw->addTab(_statsTab, i18n("Statistics")); + } + + // histogram tab + if ( p.histogram().size()!=0 ) { + _histoTab = new HistogramTab(_tw); + _tw->addTab(_histoTab, i18n("Histogram")); + } + + // url labels + if ( internal->isWWHSAvailable() ) { + KURL url = internal->queryURL(ManagerPrivate::Scores); + _scoresUrl = new KURLLabel(url.url(), + i18n("View world-wide highscores"), this); + connect(_scoresUrl, SIGNAL(leftClickedURL(const QString &)), + SLOT(showURL(const QString &))); + vbox->addWidget(_scoresUrl); + + url = internal->queryURL(ManagerPrivate::Players); + _playersUrl = new KURLLabel(url.url(), + i18n("View world-wide players"), this); + connect(_playersUrl, SIGNAL(leftClickedURL(const QString &)), + SLOT(showURL(const QString &))); + vbox->addWidget(_playersUrl); + } +} + +void HighscoresWidget::changeTab(int i) +{ + if ( i!=_tw->currentPageIndex() ) + _tw->setCurrentPage(i); +} + +void HighscoresWidget::showURL(const QString &url) const +{ + (void)new KRun(KURL(url)); +} + +void HighscoresWidget::load(int rank) +{ + _scoresList->load(internal->scoreInfos(), rank); + _playersList->load(internal->playerInfos(), internal->playerInfos().id()); + if (_scoresUrl) + _scoresUrl->setURL(internal->queryURL(ManagerPrivate::Scores).url()); + if (_playersUrl) + _playersUrl->setURL(internal->queryURL(ManagerPrivate::Players).url()); + if (_statsTab) _statsTab->load(); + if (_histoTab) _histoTab->load(); +} + +//----------------------------------------------------------------------------- +HighscoresDialog::HighscoresDialog(int rank, QWidget *parent) + : KDialogBase(internal->nbGameTypes()>1 ? TreeList : Plain, + i18n("Highscores"), Close|User1|User2, Close, + parent, "show_highscores", true, true, + KGuiItem(i18n("Configure..."), "configure"), + KGuiItem(i18n("Export..."))), _rank(rank), _tab(0) +{ + _widgets.resize(internal->nbGameTypes(), 0); + + if ( internal->nbGameTypes()>1 ) { + for (uint i=0; i<internal->nbGameTypes(); i++) { + QString title = internal->manager.gameTypeLabel(i, Manager::I18N); + QString icon = internal->manager.gameTypeLabel(i, Manager::Icon); + QWidget *w = addVBoxPage(title, QString::null, + BarIcon(icon, KIcon::SizeLarge)); + if ( i==internal->gameType() ) createPage(w); + } + + connect(this, SIGNAL(aboutToShowPage(QWidget *)), + SLOT(createPage(QWidget *))); + showPage(internal->gameType()); + } else { + QVBoxLayout *vbox = new QVBoxLayout(plainPage()); + createPage(plainPage()); + vbox->addWidget(_widgets[0]); + setMainWidget(_widgets[0]); + } +} + +void HighscoresDialog::createPage(QWidget *page) +{ + internal->hsConfig().readCurrentConfig(); + _current = page; + bool several = ( internal->nbGameTypes()>1 ); + int i = (several ? pageIndex(page) : 0); + if ( _widgets[i]==0 ) { + _widgets[i] = new HighscoresWidget(page); + connect(_widgets[i], SIGNAL(tabChanged(int)), SLOT(tabChanged(int))); + } + uint type = internal->gameType(); + if (several) internal->setGameType(i); + _widgets[i]->load(uint(i)==type ? _rank : -1); + if (several) setGameType(type); + _widgets[i]->changeTab(_tab); +} + +void HighscoresDialog::slotUser1() +{ + if ( KExtHighscore::configure(this) ) + createPage(_current); +} + +void HighscoresDialog::slotUser2() +{ + KURL url = KFileDialog::getSaveURL(QString::null, QString::null, this); + if ( url.isEmpty() ) return; + if ( KIO::NetAccess::exists(url, true, this) ) { + KGuiItem gi = KStdGuiItem::save(); + gi.setText(i18n("Overwrite")); + int res = KMessageBox::warningContinueCancel(this, + i18n("The file already exists. Overwrite?"), + i18n("Export"), gi); + if ( res==KMessageBox::Cancel ) return; + } + KTempFile tmp; + internal->exportHighscores(*tmp.textStream()); + tmp.close(); + KIO::NetAccess::upload(tmp.name(), url, this); + tmp.unlink(); +} + +//----------------------------------------------------------------------------- +LastMultipleScoresList::LastMultipleScoresList( + const QValueVector<Score> &scores, QWidget *parent) + : ScoresList(parent), _scores(scores) +{ + const ScoreInfos &s = internal->scoreInfos(); + addHeader(s); + for (uint i=0; i<scores.size(); i++) addLine(s, i, false); +} + +void LastMultipleScoresList::addLineItem(const ItemArray &si, + uint index, QListViewItem *line) +{ + uint k = 1; // skip "id" + for (uint i=0; i<si.size()-2; i++) { + if ( i==3 ) k = 5; // skip "date" + const ItemContainer *container = si[k]; + k++; + if (line) line->setText(i, itemText(*container, index)); + else { + addColumn( container->item()->label() ); + setColumnAlignment(i, container->item()->alignment()); + } + } +} + +QString LastMultipleScoresList::itemText(const ItemContainer &item, + uint row) const +{ + QString name = item.name(); + if ( name=="rank" ) + return (_scores[row].type()==Won ? i18n("Winner") : QString::null); + QVariant v = _scores[row].data(name); + if ( name=="name" ) return v.toString(); + return item.item()->pretty(row, v); +} + +//----------------------------------------------------------------------------- +TotalMultipleScoresList::TotalMultipleScoresList( + const QValueVector<Score> &scores, QWidget *parent) + : ScoresList(parent), _scores(scores) +{ + const ScoreInfos &s = internal->scoreInfos(); + addHeader(s); + for (uint i=0; i<scores.size(); i++) addLine(s, i, false); +} + +void TotalMultipleScoresList::addLineItem(const ItemArray &si, + uint index, QListViewItem *line) +{ + const PlayerInfos &pi = internal->playerInfos(); + uint k = 1; // skip "id" + for (uint i=0; i<4; i++) { // skip additional fields + const ItemContainer *container; + if ( i==2 ) container = pi.item("nb games"); + else if ( i==3 ) container = pi.item("mean score"); + else { + container = si[k]; + k++; + } + if (line) line->setText(i, itemText(*container, index)); + else { + QString label = + (i==2 ? i18n("Won Games") : container->item()->label()); + addColumn(label); + setColumnAlignment(i, container->item()->alignment()); + } + } +} + +QString TotalMultipleScoresList::itemText(const ItemContainer &item, + uint row) const +{ + QString name = item.name(); + if ( name=="rank" ) return QString::number(_scores.size()-row); + if ( name=="nb games" ) + return QString::number( _scores[row].data("nb won games").toUInt() ); + QVariant v = _scores[row].data(name); + if ( name=="name" ) return v.toString(); + return item.item()->pretty(row, v); +} + + +//----------------------------------------------------------------------------- +ConfigDialog::ConfigDialog(QWidget *parent) + : KDialogBase(Swallow, i18n("Configure Highscores"), + Ok|Apply|Cancel, Cancel, + parent, "configure_highscores", true, true), + _saved(false), _WWHEnabled(0) +{ + QWidget *page = 0; + QTabWidget *tab = 0; + if ( internal->isWWHSAvailable() ) { + tab = new QTabWidget(this); + setMainWidget(tab); + page = new QWidget(tab); + tab->addTab(page, i18n("Main")); + } else { + page = new QWidget(this); + setMainWidget(page); + } + + QGridLayout *pageTop = + new QGridLayout(page, 2, 2, spacingHint(), spacingHint()); + + QLabel *label = new QLabel(i18n("Nickname:"), page); + pageTop->addWidget(label, 0, 0); + _nickname = new QLineEdit(page); + connect(_nickname, SIGNAL(textChanged(const QString &)), + SLOT(modifiedSlot())); + connect(_nickname, SIGNAL(textChanged(const QString &)), + SLOT(nickNameChanged(const QString &))); + + _nickname->setMaxLength(16); + pageTop->addWidget(_nickname, 0, 1); + + label = new QLabel(i18n("Comment:"), page); + pageTop->addWidget(label, 1, 0); + _comment = new QLineEdit(page); + connect(_comment, SIGNAL(textChanged(const QString &)), + SLOT(modifiedSlot())); + _comment->setMaxLength(50); + pageTop->addWidget(_comment, 1, 1); + + if (tab) { + _WWHEnabled + = new QCheckBox(i18n("World-wide highscores enabled"), page); + connect(_WWHEnabled, SIGNAL(toggled(bool)), + SLOT(modifiedSlot())); + pageTop->addMultiCellWidget(_WWHEnabled, 2, 2, 0, 1); + + // advanced tab + QWidget *page = new QWidget(tab); + tab->addTab(page, i18n("Advanced")); + QVBoxLayout *pageTop = + new QVBoxLayout(page, spacingHint(), spacingHint()); + + QVGroupBox *group = new QVGroupBox(i18n("Registration Data"), page); + pageTop->addWidget(group); + QGrid *grid = new QGrid(2, group); + grid->setSpacing(spacingHint()); + + label = new QLabel(i18n("Nickname:"), grid); + _registeredName = new KLineEdit(grid); + _registeredName->setReadOnly(true); + + label = new QLabel(i18n("Key:"), grid); + _key = new KLineEdit(grid); + _key->setReadOnly(true); + + KGuiItem gi = KStdGuiItem::clear(); + gi.setText(i18n("Remove")); + _removeButton = new KPushButton(gi, grid); + connect(_removeButton, SIGNAL(clicked()), SLOT(removeSlot())); + } + + load(); + enableButtonOK( !_nickname->text().isEmpty() ); + enableButtonApply(false); +} + +void ConfigDialog::nickNameChanged(const QString &text) +{ + enableButtonOK( !text.isEmpty() ); +} + + +void ConfigDialog::modifiedSlot() +{ + enableButtonApply(true && !_nickname->text().isEmpty() ); +} + +void ConfigDialog::accept() +{ + if ( save() ) { + KDialogBase::accept(); + kapp->config()->sync(); // safer + } +} + +void ConfigDialog::removeSlot() +{ + KGuiItem gi = KStdGuiItem::clear(); + gi.setText(i18n("Remove")); + int res = KMessageBox::warningContinueCancel(this, + i18n("This will permanently remove your " + "registration key. You will not be able to use " + "the currently registered nickname anymore."), + QString::null, gi); + if ( res==KMessageBox::Continue ) { + internal->playerInfos().removeKey(); + _registeredName->clear(); + _key->clear(); + _removeButton->setEnabled(false); + _WWHEnabled->setChecked(false); + modifiedSlot(); + } +} + +void ConfigDialog::load() +{ + internal->hsConfig().readCurrentConfig(); + const PlayerInfos &infos = internal->playerInfos(); + _nickname->setText(infos.isAnonymous() ? QString::null : infos.name()); + _comment->setText(infos.comment()); + if (_WWHEnabled) { + _WWHEnabled->setChecked(infos.isWWEnabled()); + if ( !infos.key().isEmpty() ) { + _registeredName->setText(infos.registeredName()); + _registeredName->home(false); + _key->setText(infos.key()); + _key->home(false); + } + _removeButton->setEnabled(!infos.key().isEmpty()); + } +} + +bool ConfigDialog::save() +{ + bool enabled = (_WWHEnabled ? _WWHEnabled->isChecked() : false); + + // do not bother the user with "nickname empty" if he has not + // messed with nickname settings ... + QString newName = _nickname->text(); + if ( newName.isEmpty() && !internal->playerInfos().isAnonymous() + && !enabled ) return true; + + if ( newName.isEmpty() ) { + KMessageBox::sorry(this, i18n("Please choose a non empty nickname.")); + return false; + } + if ( internal->playerInfos().isNameUsed(newName) ) { + KMessageBox::sorry(this, i18n("Nickname already in use. Please " + "choose another one")); + return false; + } + + int res = + internal->modifySettings(newName, _comment->text(), enabled, this); + if (res) { + load(); // needed to update view when "apply" is clicked + enableButtonApply(false); + } + _saved = true; + return res; +} + +//----------------------------------------------------------------------------- +AskNameDialog::AskNameDialog(QWidget *parent) + : KDialogBase(Plain, i18n("Enter Your Nickname"), Ok | Cancel, Ok, + parent, "ask_name_dialog") +{ + internal->hsConfig().readCurrentConfig(); + + QVBoxLayout *top = + new QVBoxLayout(plainPage(), marginHint(), spacingHint()); + QLabel *label = + new QLabel(i18n("Congratulations, you have won!"), plainPage()); + top->addWidget(label); + + QHBoxLayout *hbox = new QHBoxLayout(top); + label = new QLabel(i18n("Enter your nickname:"), plainPage()); + hbox->addWidget(label); + _edit = new QLineEdit(plainPage()); + _edit->setFocus(); + connect(_edit, SIGNAL(textChanged(const QString &)), SLOT(nameChanged())); + hbox->addWidget(_edit); + + top->addSpacing(spacingHint()); + _checkbox = new QCheckBox(i18n("Do not ask again."), plainPage()); + top->addWidget(_checkbox); + + nameChanged(); +} + +void AskNameDialog::nameChanged() +{ + enableButtonOK( !name().isEmpty() + && !internal->playerInfos().isNameUsed(name()) ); +} + +} // namespace diff --git a/libkdegames/highscore/kexthighscore_gui.h b/libkdegames/highscore/kexthighscore_gui.h new file mode 100644 index 00000000..e721299a --- /dev/null +++ b/libkdegames/highscore/kexthighscore_gui.h @@ -0,0 +1,207 @@ +/* + This file is part of the KDE games library + Copyright (C) 2001-02 Nicolas Hadacek ([email protected]) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License version 2 as published by the Free Software Foundation. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#ifndef KEXTHIGHSCORE_GUI_H +#define KEXTHIGHSCORE_GUI_H + +#include <qcheckbox.h> +#include <qlabel.h> +#include <qvbox.h> +#include <qtabwidget.h> + +#include <klistview.h> +#include <klineedit.h> +#include <kpushbutton.h> +#include <kdialogbase.h> + +#include "kexthighscore.h" + + +namespace KExtHighscore +{ + +class ItemContainer; +class ItemArray; +class Score; +class AdditionalTab; + +//----------------------------------------------------------------------------- +class ShowItem : public KListViewItem +{ + public: + ShowItem(QListView *, bool highlight); + + protected: + virtual void paintCell(QPainter *, const QColorGroup &, int column, + int width, int align); + + private: + bool _highlight; +}; + +class ScoresList : public KListView +{ + Q_OBJECT + public: + ScoresList(QWidget *parent); + + void addHeader(const ItemArray &); + + protected: + QListViewItem *addLine(const ItemArray &, uint index, bool highlight); + virtual QString itemText(const ItemContainer &, uint row) const = 0; + + private: + virtual void addLineItem(const ItemArray &, uint index, + QListViewItem *item); +}; + +//----------------------------------------------------------------------------- +class HighscoresList : public ScoresList +{ + Q_OBJECT + public: + HighscoresList(QWidget *parent); + + void load(const ItemArray &, int highlight); + + protected: + QString itemText(const ItemContainer &, uint row) const; +}; + +class HighscoresWidget : public QWidget +{ + Q_OBJECT + public: + HighscoresWidget(QWidget *parent); + + void load(int rank); + + signals: + void tabChanged(int i); + + public slots: + void changeTab(int i); + + private slots: + void showURL(const QString &) const; + void tabChanged() { emit tabChanged(_tw->currentPageIndex()); } + + private: + QTabWidget *_tw; + HighscoresList *_scoresList, *_playersList; + KURLLabel *_scoresUrl, *_playersUrl; + AdditionalTab *_statsTab, *_histoTab; +}; + +class HighscoresDialog : public KDialogBase +{ + Q_OBJECT + public: + HighscoresDialog(int rank, QWidget *parent); + + private slots: + void slotUser1(); + void slotUser2(); + void tabChanged(int i) { _tab = i; } + void createPage(QWidget *); + + private: + int _rank, _tab; + QWidget *_current; + QValueVector<HighscoresWidget *> _widgets; +}; + +//----------------------------------------------------------------------------- +class LastMultipleScoresList : public ScoresList +{ + Q_OBJECT +public: + LastMultipleScoresList(const QValueVector<Score> &, QWidget *parent); + +private: + void addLineItem(const ItemArray &, uint index, QListViewItem *line); + QString itemText(const ItemContainer &, uint row) const; + +private: + const QValueVector<Score> &_scores; +}; + +class TotalMultipleScoresList : public ScoresList +{ + Q_OBJECT +public: + TotalMultipleScoresList(const QValueVector<Score> &, QWidget *parent); + +private: + void addLineItem(const ItemArray &, uint index, QListViewItem *line); + QString itemText(const ItemContainer &, uint row) const; + +private: + const QValueVector<Score> &_scores; +}; + +//----------------------------------------------------------------------------- +class ConfigDialog : public KDialogBase +{ + Q_OBJECT + public: + ConfigDialog(QWidget *parent); + + bool hasBeenSaved() const { return _saved; } + + private slots: + void modifiedSlot(); + void removeSlot(); + void accept(); + void slotApply() { save(); } + void nickNameChanged(const QString &); + + private: + bool _saved; + QCheckBox *_WWHEnabled; + QLineEdit *_nickname, *_comment; + KLineEdit *_key, *_registeredName; + KPushButton *_removeButton; + + void load(); + bool save(); +}; + +//----------------------------------------------------------------------------- +class AskNameDialog : public KDialogBase +{ + Q_OBJECT + public: + AskNameDialog(QWidget *parent); + + QString name() const { return _edit->text(); } + bool dontAskAgain() const { return _checkbox->isChecked(); } + + private slots: + void nameChanged(); + + private: + QLineEdit *_edit; + QCheckBox *_checkbox; +}; + +} // namespace + +#endif diff --git a/libkdegames/highscore/kexthighscore_internal.cpp b/libkdegames/highscore/kexthighscore_internal.cpp new file mode 100644 index 00000000..a8395753 --- /dev/null +++ b/libkdegames/highscore/kexthighscore_internal.cpp @@ -0,0 +1,868 @@ +/* + This file is part of the KDE games library + Copyright (C) 2001-2004 Nicolas Hadacek ([email protected]) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License version 2 as published by the Free Software Foundation. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#include "kexthighscore_internal.h" + +#include <pwd.h> +#include <sys/types.h> +#include <unistd.h> + +#include <qfile.h> +#include <qlayout.h> +#include <qdom.h> + +#include <kglobal.h> +#include <kio/netaccess.h> +#include <kio/job.h> +#include <kmessagebox.h> +#include <kmdcodec.h> +#include <kdebug.h> + +#include "config.h" +#include "kexthighscore.h" +#include "kexthighscore_gui.h" +#include "kemailsettings.h" + + +namespace KExtHighscore +{ + +//----------------------------------------------------------------------------- +const char ItemContainer::ANONYMOUS[] = "_"; +const char ItemContainer::ANONYMOUS_LABEL[] = I18N_NOOP("anonymous"); + +ItemContainer::ItemContainer() + : _item(0) +{} + +ItemContainer::~ItemContainer() +{ + delete _item; +} + +void ItemContainer::setItem(Item *item) +{ + delete _item; + _item = item; +} + +QString ItemContainer::entryName() const +{ + if ( _subGroup.isEmpty() ) return _name; + return _name + "_" + _subGroup; +} + +QVariant ItemContainer::read(uint i) const +{ + Q_ASSERT(_item); + + QVariant v = _item->defaultValue(); + if ( isStored() ) { + internal->hsConfig().setHighscoreGroup(_group); + v = internal->hsConfig().readPropertyEntry(i+1, entryName(), v); + } + return _item->read(i, v); +} + +QString ItemContainer::pretty(uint i) const +{ + Q_ASSERT(_item); + return _item->pretty(i, read(i)); +} + +void ItemContainer::write(uint i, const QVariant &value) const +{ + Q_ASSERT( isStored() ); + Q_ASSERT( internal->hsConfig().isLocked() ); + internal->hsConfig().setHighscoreGroup(_group); + internal->hsConfig().writeEntry(i+1, entryName(), value); +} + +uint ItemContainer::increment(uint i) const +{ + uint v = read(i).toUInt() + 1; + write(i, v); + return v; +} + +//----------------------------------------------------------------------------- +ItemArray::ItemArray() + : _group(""), _subGroup("") // no null groups +{} + +ItemArray::~ItemArray() +{ + for (uint i=0; i<size(); i++) delete at(i); +} + +int ItemArray::findIndex(const QString &name) const +{ + for (uint i=0; i<size(); i++) + if ( at(i)->name()==name ) return i; + return -1; +} + +const ItemContainer *ItemArray::item(const QString &name) const +{ + int i = findIndex(name); + if ( i==-1 ) kdError(11002) << k_funcinfo << "no item named \"" << name + << "\"" << endl; + return at(i); +} + +ItemContainer *ItemArray::item(const QString &name) +{ + int i = findIndex(name); + if ( i==-1 ) kdError(11002) << k_funcinfo << "no item named \"" << name + << "\"" << endl; + return at(i); +} + +void ItemArray::setItem(const QString &name, Item *item) +{ + int i = findIndex(name); + if ( i==-1 ) kdError(11002) << k_funcinfo << "no item named \"" << name + << "\"" << endl; + bool stored = at(i)->isStored(); + bool canHaveSubGroup = at(i)->canHaveSubGroup(); + _setItem(i, name, item, stored, canHaveSubGroup); +} + +void ItemArray::addItem(const QString &name, Item *item, + bool stored, bool canHaveSubGroup) +{ + if ( findIndex(name)!=-1 ) + kdError(11002) << "item already exists \"" << name << "\"" << endl; + uint i = size(); + resize(i+1); + at(i) = new ItemContainer; + _setItem(i, name, item, stored, canHaveSubGroup); +} + +void ItemArray::_setItem(uint i, const QString &name, Item *item, + bool stored, bool canHaveSubGroup) +{ + at(i)->setItem(item); + at(i)->setName(name); + at(i)->setGroup(stored ? _group : QString::null); + at(i)->setSubGroup(canHaveSubGroup ? _subGroup : QString::null); +} + +void ItemArray::setGroup(const QString &group) +{ + Q_ASSERT( !group.isNull() ); + _group = group; + for (uint i=0; i<size(); i++) + if ( at(i)->isStored() ) at(i)->setGroup(group); +} + +void ItemArray::setSubGroup(const QString &subGroup) +{ + Q_ASSERT( !subGroup.isNull() ); + _subGroup = subGroup; + for (uint i=0; i<size(); i++) + if ( at(i)->canHaveSubGroup() ) at(i)->setSubGroup(subGroup); +} + +void ItemArray::read(uint k, Score &data) const +{ + for (uint i=0; i<size(); i++) { + if ( !at(i)->isStored() ) continue; + data.setData(at(i)->name(), at(i)->read(k)); + } +} + +void ItemArray::write(uint k, const Score &data, uint nb) const +{ + for (uint i=0; i<size(); i++) { + if ( !at(i)->isStored() ) continue; + for (uint j=nb-1; j>k; j--) at(i)->write(j, at(i)->read(j-1)); + at(i)->write(k, data.data(at(i)->name())); + } +} + +void ItemArray::exportToText(QTextStream &s) const +{ + for (uint k=0; k<nbEntries()+1; k++) { + for (uint i=0; i<size(); i++) { + const Item *item = at(i)->item(); + if ( item->isVisible() ) { + if ( i!=0 ) s << '\t'; + if ( k==0 ) s << item->label(); + else s << at(i)->pretty(k-1); + } + } + s << endl; + } +} + +//----------------------------------------------------------------------------- +class ScoreNameItem : public NameItem +{ + public: + ScoreNameItem(const ScoreInfos &score, const PlayerInfos &infos) + : _score(score), _infos(infos) {} + + QString pretty(uint i, const QVariant &v) const { + uint id = _score.item("id")->read(i).toUInt(); + if ( id==0 ) return NameItem::pretty(i, v); + return _infos.prettyName(id-1); + } + + private: + const ScoreInfos &_score; + const PlayerInfos &_infos; +}; + +//----------------------------------------------------------------------------- +ScoreInfos::ScoreInfos(uint maxNbEntries, const PlayerInfos &infos) + : _maxNbEntries(maxNbEntries) +{ + addItem("id", new Item((uint)0)); + addItem("rank", new RankItem, false); + addItem("name", new ScoreNameItem(*this, infos)); + addItem("score", Manager::createItem(Manager::ScoreDefault)); + addItem("date", new DateItem); +} + +uint ScoreInfos::nbEntries() const +{ + uint i = 0; + for (; i<_maxNbEntries; i++) + if ( item("score")->read(i)==item("score")->item()->defaultValue() ) + break; + return i; +} + +//----------------------------------------------------------------------------- +const char *HS_ID = "player id"; +const char *HS_REGISTERED_NAME = "registered name"; +const char *HS_KEY = "player key"; +const char *HS_WW_ENABLED = "ww hs enabled"; + +PlayerInfos::PlayerInfos() +{ + setGroup("players"); + + // standard items + addItem("name", new NameItem); + Item *it = new Item((uint)0, i18n("Games Count"),Qt::AlignRight); + addItem("nb games", it, true, true); + it = Manager::createItem(Manager::MeanScoreDefault); + addItem("mean score", it, true, true); + it = Manager::createItem(Manager::BestScoreDefault); + addItem("best score", it, true, true); + addItem("date", new DateItem, true, true); + it = new Item(QString::null, i18n("Comment"), Qt::AlignLeft); + addItem("comment", it); + + // statistics items + addItem("nb black marks", new Item((uint)0), true, true); // legacy + addItem("nb lost games", new Item((uint)0), true, true); + addItem("nb draw games", new Item((uint)0), true, true); + addItem("current trend", new Item((int)0), true, true); + addItem("max lost trend", new Item((uint)0), true, true); + addItem("max won trend", new Item((uint)0), true, true); + + struct passwd *pwd = getpwuid(getuid()); + QString username = pwd->pw_name; +#ifdef HIGHSCORE_DIRECTORY + internal->hsConfig().setHighscoreGroup("players"); + for (uint i=0; ;i++) { + if ( !internal->hsConfig().hasEntry(i+1, "username") ) { + _newPlayer = true; + _id = i; + break; + } + if ( internal->hsConfig().readEntry(i+1, "username")==username ) { + _newPlayer = false; + _id = i; + return; + } + } +#endif + internal->hsConfig().lockForWriting(); + KEMailSettings emailConfig; + emailConfig.setProfile(emailConfig.defaultProfileName()); + QString name = emailConfig.getSetting(KEMailSettings::RealName); + if ( name.isEmpty() || isNameUsed(name) ) name = username; + if ( isNameUsed(name) ) name= QString(ItemContainer::ANONYMOUS); +#ifdef HIGHSCORE_DIRECTORY + internal->hsConfig().writeEntry(_id+1, "username", username); + item("name")->write(_id, name); +#endif + + ConfigGroup cg; + _oldLocalPlayer = cg.config()->hasKey(HS_ID); + _oldLocalId = cg.config()->readUnsignedNumEntry(HS_ID); +#ifdef HIGHSCORE_DIRECTORY + if (_oldLocalPlayer) { // player already exists in local config file + // copy player data + QString prefix = QString("%1_").arg(_oldLocalId+1); + QMap<QString, QString> entries = + cg.config()->entryMap("KHighscore_players"); + QMap<QString, QString>::const_iterator it; + for (it=entries.begin(); it!=entries.end(); ++it) { + QString key = it.key(); + if ( key.find(prefix)==0 ) { + QString name = key.right(key.length()-prefix.length()); + if ( name!="name" || !isNameUsed(it.data()) ) + internal->hsConfig().writeEntry(_id+1, name, it.data()); + } + } + } +#else + _newPlayer = !_oldLocalPlayer; + if (_oldLocalPlayer) _id = _oldLocalId; + else { + _id = nbEntries(); + cg.config()->writeEntry(HS_ID, _id); + item("name")->write(_id, name); + } +#endif + _bound = true; + internal->hsConfig().writeAndUnlock(); +} + +void PlayerInfos::createHistoItems(const QMemArray<uint> &scores, bool bound) +{ + Q_ASSERT( _histogram.size()==0 ); + _bound = bound; + _histogram = scores; + for (uint i=1; i<histoSize(); i++) + addItem(histoName(i), new Item((uint)0), true, true); +} + +bool PlayerInfos::isAnonymous() const +{ + return ( name()==ItemContainer::ANONYMOUS ); +} + +uint PlayerInfos::nbEntries() const +{ + internal->hsConfig().setHighscoreGroup("players"); + QStringList list = internal->hsConfig().readList("name", -1); + return list.count(); +} + +QString PlayerInfos::key() const +{ + ConfigGroup cg; + return cg.config()->readEntry(HS_KEY, QString::null); +} + +bool PlayerInfos::isWWEnabled() const +{ + ConfigGroup cg; + return cg.config()->readBoolEntry(HS_WW_ENABLED, false); +} + +QString PlayerInfos::histoName(uint i) const +{ + const QMemArray<uint> &sh = _histogram; + Q_ASSERT( i<sh.size() || (_bound || i==sh.size()) ); + if ( i==sh.size() ) + return QString("nb scores greater than %1").arg(sh[sh.size()-1]); + return QString("nb scores less than %1").arg(sh[i]); +} + +uint PlayerInfos::histoSize() const +{ + return _histogram.size() + (_bound ? 0 : 1); +} + +void PlayerInfos::submitScore(const Score &score) const +{ + // update counts + uint nbGames = item("nb games")->increment(_id); + switch (score.type()) { + case Lost: + item("nb lost games")->increment(_id); + break; + case Won: break; + case Draw: + item("nb draw games")->increment(_id); + break; + }; + + // update mean + if ( score.type()==Won ) { + uint nbWonGames = nbGames - item("nb lost games")->read(_id).toUInt() + - item("nb draw games")->read(_id).toUInt() + - item("nb black marks")->read(_id).toUInt(); // legacy + double mean = (nbWonGames==1 ? 0.0 + : item("mean score")->read(_id).toDouble()); + mean += (double(score.score()) - mean) / nbWonGames; + item("mean score")->write(_id, mean); + } + + // update best score + Score best = score; // copy optionnal fields (there are not taken into account here) + best.setScore( item("best score")->read(_id).toUInt() ); + if ( best<score ) { + item("best score")->write(_id, score.score()); + item("date")->write(_id, score.data("date").toDateTime()); + } + + // update trends + int current = item("current trend")->read(_id).toInt(); + switch (score.type()) { + case Won: { + if ( current<0 ) current = 0; + current++; + uint won = item("max won trend")->read(_id).toUInt(); + if ( (uint)current>won ) item("max won trend")->write(_id, current); + break; + } + case Lost: { + if ( current>0 ) current = 0; + current--; + uint lost = item("max lost trend")->read(_id).toUInt(); + uint clost = -current; + if ( clost>lost ) item("max lost trend")->write(_id, clost); + break; + } + case Draw: + current = 0; + break; + } + item("current trend")->write(_id, current); + + // update histogram + if ( score.type()==Won ) { + const QMemArray<uint> &sh = _histogram; + for (uint i=1; i<histoSize(); i++) + if ( i==sh.size() || score.score()<sh[i] ) { + item(histoName(i))->increment(_id); + break; + } + } +} + +bool PlayerInfos::isNameUsed(const QString &newName) const +{ + if ( newName==name() ) return false; // own name... + for (uint i=0; i<nbEntries(); i++) + if ( newName.lower()==item("name")->read(i).toString().lower() ) return true; + if ( newName==i18n(ItemContainer::ANONYMOUS_LABEL) ) return true; + return false; +} + +void PlayerInfos::modifyName(const QString &newName) const +{ + item("name")->write(_id, newName); +} + +void PlayerInfos::modifySettings(const QString &newName, + const QString &comment, bool WWEnabled, + const QString &newKey) const +{ + modifyName(newName); + item("comment")->write(_id, comment); + ConfigGroup cg; + cg.config()->writeEntry(HS_WW_ENABLED, WWEnabled); + if ( !newKey.isEmpty() ) cg.config()->writeEntry(HS_KEY, newKey); + if (WWEnabled) cg.config()->writeEntry(HS_REGISTERED_NAME, newName); +} + +QString PlayerInfos::registeredName() const +{ + ConfigGroup cg; + return cg.config()->readEntry(HS_REGISTERED_NAME, QString::null); +} + +void PlayerInfos::removeKey() +{ + ConfigGroup cg; + + // save old key/nickname + uint i = 0; + QString str = "%1 old #%2"; + QString sk; + do { + i++; + sk = str.arg(HS_KEY).arg(i); + } while ( !cg.config()->readEntry(sk, QString::null).isEmpty() ); + cg.config()->writeEntry(sk, key()); + cg.config()->writeEntry(str.arg(HS_REGISTERED_NAME).arg(i), + registeredName()); + + // clear current key/nickname + cg.config()->deleteEntry(HS_KEY); + cg.config()->deleteEntry(HS_REGISTERED_NAME); + cg.config()->writeEntry(HS_WW_ENABLED, false); +} + +//----------------------------------------------------------------------------- +ManagerPrivate::ManagerPrivate(uint nbGameTypes, Manager &m) + : manager(m), showStatistics(false), showDrawGames(false), + trackLostGames(false), trackDrawGames(false), + showMode(Manager::ShowForHigherScore), + _first(true), _nbGameTypes(nbGameTypes), _gameType(0) +{} + +void ManagerPrivate::init(uint maxNbEntries) +{ + _hsConfig = new KHighscore(false, 0); + _playerInfos = new PlayerInfos; + _scoreInfos = new ScoreInfos(maxNbEntries, *_playerInfos); +} + +ManagerPrivate::~ManagerPrivate() +{ + delete _scoreInfos; + delete _playerInfos; + delete _hsConfig; +} + +KURL ManagerPrivate::queryURL(QueryType type, const QString &newName) const +{ + KURL url = serverURL; + QString nameItem = "nickname"; + QString name = _playerInfos->registeredName(); + bool withVersion = true; + bool key = false; + bool level = false; + + switch (type) { + case Submit: + url.addPath("submit.php"); + level = true; + key = true; + break; + case Register: + url.addPath("register.php"); + name = newName; + break; + case Change: + url.addPath("change.php"); + key = true; + if ( newName!=name ) + Manager::addToQueryURL(url, "new_nickname", newName); + break; + case Players: + url.addPath("players.php"); + nameItem = "highlight"; + withVersion = false; + break; + case Scores: + url.addPath("highscores.php"); + withVersion = false; + if ( _nbGameTypes>1 ) level = true; + break; + } + + if (withVersion) Manager::addToQueryURL(url, "version", version); + if ( !name.isEmpty() ) Manager::addToQueryURL(url, nameItem, name); + if (key) Manager::addToQueryURL(url, "key", _playerInfos->key()); + if (level) { + QString label = manager.gameTypeLabel(_gameType, Manager::WW); + if ( !label.isEmpty() ) Manager::addToQueryURL(url, "level", label); + } + + return url; +} + +// strings that needs to be translated (coming from the highscores server) +const char *DUMMY_STRINGS[] = { + I18N_NOOP("Undefined error."), + I18N_NOOP("Missing argument(s)."), + I18N_NOOP("Invalid argument(s)."), + + I18N_NOOP("Unable to connect to MySQL server."), + I18N_NOOP("Unable to select database."), + I18N_NOOP("Error on database query."), + I18N_NOOP("Error on database insert."), + + I18N_NOOP("Nickname already registered."), + I18N_NOOP("Nickname not registered."), + I18N_NOOP("Invalid key."), + I18N_NOOP("Invalid submit key."), + + I18N_NOOP("Invalid level."), + I18N_NOOP("Invalid score.") +}; + +const char *UNABLE_TO_CONTACT = + I18N_NOOP("Unable to contact world-wide highscore server"); + +bool ManagerPrivate::doQuery(const KURL &url, QWidget *parent, + QDomNamedNodeMap *map) +{ + KIO::http_update_cache(url, true, 0); // remove cache ! + + QString tmpFile; + if ( !KIO::NetAccess::download(url, tmpFile, parent) ) { + QString details = i18n("Server URL: %1").arg(url.host()); + KMessageBox::detailedSorry(parent, i18n(UNABLE_TO_CONTACT), details); + return false; + } + + QFile file(tmpFile); + if ( !file.open(IO_ReadOnly) ) { + KIO::NetAccess::removeTempFile(tmpFile); + QString details = i18n("Unable to open temporary file."); + KMessageBox::detailedSorry(parent, i18n(UNABLE_TO_CONTACT), details); + return false; + } + + QTextStream t(&file); + QString content = t.read().stripWhiteSpace(); + file.close(); + KIO::NetAccess::removeTempFile(tmpFile); + + QDomDocument doc; + if ( doc.setContent(content) ) { + QDomElement root = doc.documentElement(); + QDomElement element = root.firstChild().toElement(); + if ( element.tagName()=="success" ) { + if (map) *map = element.attributes(); + return true; + } + if ( element.tagName()=="error" ) { + QDomAttr attr = element.attributes().namedItem("label").toAttr(); + if ( !attr.isNull() ) { + QString msg = i18n(attr.value().latin1()); + QString caption = i18n("Message from world-wide highscores " + "server"); + KMessageBox::sorry(parent, msg, caption); + return false; + } + } + } + QString msg = i18n("Invalid answer from world-wide highscores server."); + QString details = i18n("Raw message: %1").arg(content); + KMessageBox::detailedSorry(parent, msg, details); + return false; +} + +bool ManagerPrivate::getFromQuery(const QDomNamedNodeMap &map, + const QString &name, QString &value, + QWidget *parent) +{ + QDomAttr attr = map.namedItem(name).toAttr(); + if ( attr.isNull() ) { + KMessageBox::sorry(parent, + i18n("Invalid answer from world-wide " + "highscores server (missing item: %1).").arg(name)); + return false; + } + value = attr.value(); + return true; +} + +Score ManagerPrivate::readScore(uint i) const +{ + Score score(Won); + _scoreInfos->read(i, score); + return score; +} + +int ManagerPrivate::rank(const Score &score) const +{ + uint nb = _scoreInfos->nbEntries(); + uint i = 0; + for (; i<nb; i++) + if ( readScore(i)<score ) break; + return (i<_scoreInfos->maxNbEntries() ? (int)i : -1); +} + +bool ManagerPrivate::modifySettings(const QString &newName, + const QString &comment, bool WWEnabled, + QWidget *widget) +{ + QString newKey; + bool newPlayer = false; + + if (WWEnabled) { + newPlayer = _playerInfos->key().isEmpty() + || _playerInfos->registeredName().isEmpty(); + KURL url = queryURL((newPlayer ? Register : Change), newName); + Manager::addToQueryURL(url, "comment", comment); + + QDomNamedNodeMap map; + bool ok = doQuery(url, widget, &map); + if ( !ok || (newPlayer && !getFromQuery(map, "key", newKey, widget)) ) + return false; + } + + bool ok = _hsConfig->lockForWriting(widget); // no GUI when locking + if (ok) { + // check again name in case the config file has been changed... + // if it has, it is unfortunate because the WWW name is already + // committed but should be very rare and not really problematic + ok = ( !_playerInfos->isNameUsed(newName) ); + if (ok) + _playerInfos->modifySettings(newName, comment, WWEnabled, newKey); + _hsConfig->writeAndUnlock(); + } + return ok; +} + +void ManagerPrivate::convertToGlobal() +{ + // read old highscores + KHighscore *tmp = _hsConfig; + _hsConfig = new KHighscore(true, 0); + QValueVector<Score> scores(_scoreInfos->nbEntries()); + for (uint i=0; i<scores.count(); i++) + scores[i] = readScore(i); + + // commit them + delete _hsConfig; + _hsConfig = tmp; + _hsConfig->lockForWriting(); + for (uint i=0; i<scores.count(); i++) + if ( scores[i].data("id").toUInt()==_playerInfos->oldLocalId()+1 ) + submitLocal(scores[i]); + _hsConfig->writeAndUnlock(); +} + +void ManagerPrivate::setGameType(uint type) +{ + if (_first) { + _first = false; + if ( _playerInfos->isNewPlayer() ) { + // convert legacy highscores + for (uint i=0; i<_nbGameTypes; i++) { + setGameType(i); + manager.convertLegacy(i); + } + +#ifdef HIGHSCORE_DIRECTORY + if ( _playerInfos->isOldLocalPlayer() ) { + // convert local to global highscores + for (uint i=0; i<_nbGameTypes; i++) { + setGameType(i); + convertToGlobal(); + } + } +#endif + } + } + + Q_ASSERT( type<_nbGameTypes ); + _gameType = kMin(type, _nbGameTypes-1); + QString str = "scores"; + QString lab = manager.gameTypeLabel(_gameType, Manager::Standard); + if ( !lab.isEmpty() ) { + _playerInfos->setSubGroup(lab); + str += "_" + lab; + } + _scoreInfos->setGroup(str); +} + +void ManagerPrivate::checkFirst() +{ + if (_first) setGameType(0); +} + +int ManagerPrivate::submitScore(const Score &ascore, + QWidget *widget, bool askIfAnonymous) +{ + checkFirst(); + + Score score = ascore; + score.setData("id", _playerInfos->id() + 1); + score.setData("date", QDateTime::currentDateTime()); + + // ask new name if anonymous and winner + const char *dontAskAgainName = "highscore_ask_name_dialog"; + QString newName; + KMessageBox::ButtonCode dummy; + if ( score.type()==Won && askIfAnonymous && _playerInfos->isAnonymous() + && KMessageBox::shouldBeShownYesNo(dontAskAgainName, dummy) ) { + AskNameDialog d(widget); + if ( d.exec()==QDialog::Accepted ) newName = d.name(); + if ( d.dontAskAgain() ) + KMessageBox::saveDontShowAgainYesNo(dontAskAgainName, + KMessageBox::No); + } + + int rank = -1; + if ( _hsConfig->lockForWriting(widget) ) { // no GUI when locking + // check again new name in case the config file has been changed... + if ( !newName.isEmpty() && !_playerInfos->isNameUsed(newName) ) + _playerInfos->modifyName(newName); + + // commit locally + _playerInfos->submitScore(score); + if ( score.type()==Won ) rank = submitLocal(score); + _hsConfig->writeAndUnlock(); + } + + if ( _playerInfos->isWWEnabled() ) + submitWorldWide(score, widget); + + return rank; +} + +int ManagerPrivate::submitLocal(const Score &score) +{ + int r = rank(score); + if ( r!=-1 ) { + uint nb = _scoreInfos->nbEntries(); + if ( nb<_scoreInfos->maxNbEntries() ) nb++; + _scoreInfos->write(r, score, nb); + } + return r; +} + +bool ManagerPrivate::submitWorldWide(const Score &score, + QWidget *widget) const +{ + if ( score.type()==Lost && !trackLostGames ) return true; + if ( score.type()==Draw && !trackDrawGames ) return true; + + KURL url = queryURL(Submit); + manager.additionalQueryItems(url, score); + int s = (score.type()==Won ? score.score() : (int)score.type()); + QString str = QString::number(s); + Manager::addToQueryURL(url, "score", str); + KMD5 context(QString(_playerInfos->registeredName() + str).latin1()); + Manager::addToQueryURL(url, "check", context.hexDigest()); + + return doQuery(url, widget); +} + +void ManagerPrivate::exportHighscores(QTextStream &s) +{ + uint tmp = _gameType; + + for (uint i=0; i<_nbGameTypes; i++) { + setGameType(i); + if ( _nbGameTypes>1 ) { + if ( i!=0 ) s << endl; + s << "--------------------------------" << endl; + s << "Game type: " + << manager.gameTypeLabel(_gameType, Manager::I18N) + << endl; + s << endl; + } + s << "Players list:" << endl; + _playerInfos->exportToText(s); + s << endl; + s << "Highscores list:" << endl; + _scoreInfos->exportToText(s); + } + + setGameType(tmp); +} + +} // namespace diff --git a/libkdegames/highscore/kexthighscore_internal.h b/libkdegames/highscore/kexthighscore_internal.h new file mode 100644 index 00000000..3b206877 --- /dev/null +++ b/libkdegames/highscore/kexthighscore_internal.h @@ -0,0 +1,277 @@ +/* + This file is part of the KDE games library + Copyright (C) 2001-2004 Nicolas Hadacek ([email protected]) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License version 2 as published by the Free Software Foundation. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#ifndef KEXTHIGHSCORE_INTERNAL_H +#define KEXTHIGHSCORE_INTERNAL_H + +#include <kapplication.h> +#include <kconfig.h> +#include <klocale.h> +#include <kurl.h> + +#include "khighscore.h" +#include "kexthighscore.h" + +class QTextStream; +class QTabWidget; +class QDomNamedNodeMap; + + +namespace KExtHighscore +{ + +class PlayerInfos; +class Score; +class Manager; + + +//----------------------------------------------------------------------------- +class RankItem : public Item +{ + public: + RankItem() + : Item((uint)0, i18n("Rank"), Qt::AlignRight) {} + + QVariant read(uint rank, const QVariant &) const { return rank; } + QString pretty(uint rank, const QVariant &) const + { return QString::number(rank+1); } +}; + +class NameItem : public Item +{ + public: + NameItem() + : Item(QString::null, i18n("Name"), Qt::AlignLeft) { + setPrettySpecial(Anonymous); + } +}; + +class DateItem : public Item +{ + public: + DateItem() + : Item(QDateTime(), i18n("Date"), Qt::AlignRight) { + setPrettyFormat(DateTime); + } +}; + +class SuccessPercentageItem : public Item +{ + public: + SuccessPercentageItem() + : Item((double)-1, i18n("Success"), Qt::AlignRight) { + setPrettyFormat(Percentage); + setPrettySpecial(NegativeNotDefined); + } +}; + +//----------------------------------------------------------------------------- +class ItemContainer +{ + public: + ItemContainer(); + ~ItemContainer(); + + void setItem(Item *item); + const Item *item() const { return _item; } + Item *item() { return _item; } + + void setName(const QString &name) { _name = name; } + QString name() const { return _name; } + + void setGroup(const QString &group) { _group = group; } + bool isStored() const { return !_group.isNull(); } + + void setSubGroup(const QString &subGroup) { _subGroup = subGroup; } + bool canHaveSubGroup() const { return !_subGroup.isNull(); } + + static const char ANONYMOUS[]; // name assigned to anonymous players + static const char ANONYMOUS_LABEL[]; + + QVariant read(uint i) const; + QString pretty(uint i) const; + void write(uint i, const QVariant &value) const; + // for UInt QVariant (return new value) + uint increment(uint i) const; + + private: + Item *_item; + QString _name, _group, _subGroup; + + QString entryName() const; + + ItemContainer(const ItemContainer &); + ItemContainer &operator =(const ItemContainer &); +}; + +//----------------------------------------------------------------------------- +/** + * Manage a bunch of @ref Item which are saved under the same group + * in KHighscores config file. + */ +class ItemArray : public QMemArray<ItemContainer *> +{ + public: + ItemArray(); + virtual ~ItemArray(); + + virtual uint nbEntries() const = 0; + + const ItemContainer *item(const QString &name) const; + ItemContainer *item(const QString &name); + + void addItem(const QString &name, Item *, bool stored = true, + bool canHaveSubGroup = false); + void setItem(const QString &name, Item *); + int findIndex(const QString &name) const; + + void setGroup(const QString &group); + void setSubGroup(const QString &subGroup); + + void read(uint k, Score &data) const; + void write(uint k, const Score &data, uint maxNbLines) const; + + void exportToText(QTextStream &) const; + + private: + QString _group, _subGroup; + + void _setItem(uint i, const QString &name, Item *, bool stored, + bool canHaveSubGroup); + + ItemArray(const ItemArray &); + ItemArray &operator =(const ItemArray &); +}; + +//----------------------------------------------------------------------------- +class ScoreInfos : public ItemArray +{ + public: + ScoreInfos(uint maxNbEntries, const PlayerInfos &infos); + + uint nbEntries() const; + uint maxNbEntries() const { return _maxNbEntries; } + + private: + uint _maxNbEntries; +}; + +//----------------------------------------------------------------------------- +class ConfigGroup : public KConfigGroupSaver +{ + public: + ConfigGroup(const QString &group = QString::null) + : KConfigGroupSaver(kapp->config(), group) {} +}; + +//----------------------------------------------------------------------------- +class PlayerInfos : public ItemArray +{ + public: + PlayerInfos(); + + bool isNewPlayer() const { return _newPlayer; } + bool isOldLocalPlayer() const { return _oldLocalPlayer; } + uint nbEntries() const; + QString name() const { return item("name")->read(_id).toString(); } + bool isAnonymous() const; + QString prettyName() const { return prettyName(_id); } + QString prettyName(uint id) const { return item("name")->pretty(id); } + QString registeredName() const; + QString comment() const { return item("comment")->pretty(_id); } + bool isWWEnabled() const; + QString key() const; + uint id() const { return _id; } + uint oldLocalId() const { return _oldLocalId; } + + void createHistoItems(const QMemArray<uint> &scores, bool bound); + QString histoName(uint i) const; + uint histoSize() const; + const QMemArray<uint> &histogram() const { return _histogram; } + + void submitScore(const Score &) const; + // return true if the nickname is already used locally + bool isNameUsed(const QString &name) const; + void modifyName(const QString &newName) const; + void modifySettings(const QString &newName, const QString &comment, + bool WWEnabled, const QString &newKey) const; + void removeKey(); + + private: + bool _newPlayer, _bound, _oldLocalPlayer; + uint _id, _oldLocalId; + QMemArray<uint> _histogram; +}; + +//----------------------------------------------------------------------------- +class ManagerPrivate +{ + public: + ManagerPrivate(uint nbGameTypes, Manager &manager); + void init(uint maxNbentries); + ~ManagerPrivate(); + + bool modifySettings(const QString &newName, const QString &comment, + bool WWEnabled, QWidget *widget); + + void setGameType(uint type); + void checkFirst(); + int submitLocal(const Score &score); + int submitScore(const Score &score, QWidget *widget, bool askIfAnonymous); + Score readScore(uint i) const; + + uint gameType() const { return _gameType; } + uint nbGameTypes() const { return _nbGameTypes; } + bool isWWHSAvailable() const { return !serverURL.isEmpty(); } + ScoreInfos &scoreInfos() { return *_scoreInfos; } + PlayerInfos &playerInfos() { return *_playerInfos; } + KHighscore &hsConfig() { return *_hsConfig; } + enum QueryType { Submit, Register, Change, Players, Scores }; + KURL queryURL(QueryType type, const QString &newName=QString::null) const; + + void exportHighscores(QTextStream &); + + Manager &manager; + KURL serverURL; + QString version; + bool showStatistics, showDrawGames, trackLostGames, trackDrawGames; + Manager::ShowMode showMode; + + private: + KHighscore *_hsConfig; + PlayerInfos *_playerInfos; + ScoreInfos *_scoreInfos; + bool _first; + const uint _nbGameTypes; + uint _gameType; + + // return -1 if not a local best score + int rank(const Score &score) const; + + bool submitWorldWide(const Score &score, QWidget *parent) const; + static bool doQuery(const KURL &url, QWidget *parent, + QDomNamedNodeMap *map = 0); + static bool getFromQuery(const QDomNamedNodeMap &map, const QString &name, + QString &value, QWidget *parent); + void convertToGlobal(); +}; + +} // namespace + +#endif diff --git a/libkdegames/highscore/kexthighscore_item.cpp b/libkdegames/highscore/kexthighscore_item.cpp new file mode 100644 index 00000000..48556e02 --- /dev/null +++ b/libkdegames/highscore/kexthighscore_item.cpp @@ -0,0 +1,312 @@ +/* + This file is part of the KDE games library + Copyright (C) 2001-2003 Nicolas Hadacek ([email protected]) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License version 2 as published by the Free Software Foundation. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#include "kexthighscore_item.h" + +#include <qlayout.h> +#include <kglobal.h> +#include <kdialogbase.h> +#include <kdebug.h> + +#include "khighscore.h" +#include "kexthighscore_internal.h" +#include "kexthighscore_gui.h" + + +namespace KExtHighscore +{ + +//----------------------------------------------------------------------------- +Item::Item(const QVariant &def, const QString &label, int alignment) + : _default(def), _label(label), _alignment(alignment), + _format(NoFormat), _special(NoSpecial) +{} + +Item::~Item() +{} + +QVariant Item::read(uint, const QVariant &value) const +{ + return value; +} + +void Item::setPrettyFormat(Format format) +{ + bool buint = ( _default.type()==QVariant::UInt ); + bool bdouble = ( _default.type()==QVariant::Double ); + bool bnum = ( buint || bdouble || _default.type()==QVariant::Int ); + + switch (format) { + case OneDecimal: + case Percentage: + Q_ASSERT(bdouble); + break; + case MinuteTime: + Q_ASSERT(bnum); + break; + case DateTime: + Q_ASSERT( _default.type()==QVariant::DateTime ); + break; + case NoFormat: + break; + } + + _format = format; +} + +void Item::setPrettySpecial(Special special) +{ + bool buint = ( _default.type()==QVariant::UInt ); + bool bnum = ( buint || _default.type()==QVariant::Double + || _default.type()==QVariant::Int ); + + switch (special) { + case ZeroNotDefined: + Q_ASSERT(bnum); + break; + case NegativeNotDefined: + Q_ASSERT(bnum && !buint); + break; + case DefaultNotDefined: + break; + case Anonymous: + Q_ASSERT( _default.type()==QVariant::String ); + break; + case NoSpecial: + break; + } + + _special = special; +} + +QString Item::timeFormat(uint n) +{ + Q_ASSERT( n<=3600 && n!=0 ); + n = 3600 - n; + return QString::number(n / 60).rightJustify(2, '0') + ':' + + QString::number(n % 60).rightJustify(2, '0'); +} + +QString Item::pretty(uint, const QVariant &value) const +{ + switch (_special) { + case ZeroNotDefined: + if ( value.toUInt()==0 ) return "--"; + break; + case NegativeNotDefined: + if ( value.toInt()<0 ) return "--"; + break; + case DefaultNotDefined: + if ( value==_default ) return "--"; + break; + case Anonymous: + if ( value.toString()==ItemContainer::ANONYMOUS ) + return i18n(ItemContainer::ANONYMOUS_LABEL); + break; + case NoFormat: + break; + } + + switch (_format) { + case OneDecimal: + return QString::number(value.toDouble(), 'f', 1); + case Percentage: + return QString::number(value.toDouble(), 'f', 1) + "%"; + case MinuteTime: + return timeFormat(value.toUInt()); + case DateTime: + if ( value.toDateTime().isNull() ) return "--"; + return KGlobal::locale()->formatDateTime(value.toDateTime()); + case NoSpecial: + break; + } + + return value.toString(); +} + +//----------------------------------------------------------------------------- +Score::Score(ScoreType type) + : _type(type) +{ + const ItemArray &items = internal->scoreInfos(); + for (uint i=0; i<items.size(); i++) + _data[items[i]->name()] = items[i]->item()->defaultValue(); +} + +Score::~Score() +{} + +const QVariant &Score::data(const QString &name) const +{ + Q_ASSERT( _data.contains(name) ); + return _data[name]; +} + +void Score::setData(const QString &name, const QVariant &value) +{ + Q_ASSERT( _data.contains(name) ); + Q_ASSERT( _data[name].type()==value.type() ); + _data[name] = value; +} + +bool Score::isTheWorst() const +{ + Score s; + return score()==s.score(); +} + +bool Score::operator <(const Score &score) +{ + return internal->manager.isStrictlyLess(*this, score); +} + +QDataStream &operator <<(QDataStream &s, const Score &score) +{ + s << (Q_UINT8)score.type(); + s << score._data; + return s; +} + +QDataStream &operator >>(QDataStream &s, Score &score) +{ + Q_UINT8 type; + s >> type; + score._type = (ScoreType)type; + s >> score._data; + return s; +} + +//----------------------------------------------------------------------------- +MultiplayerScores::MultiplayerScores() +{} + +MultiplayerScores::~MultiplayerScores() +{} + +void MultiplayerScores::clear() +{ + Score score; + for (uint i=0; i<_scores.size(); i++) { + _nbGames[i] = 0; + QVariant name = _scores[i].data("name"); + _scores[i] = score; + _scores[i].setData("name", name); + _scores[i]._data["mean score"] = double(0); + _scores[i]._data["nb won games"] = uint(0); + } +} + +void MultiplayerScores::setPlayerCount(uint nb) +{ + _nbGames.resize(nb); + _scores.resize(nb); + clear(); +} + +void MultiplayerScores::setName(uint i, const QString &name) +{ + _scores[i].setData("name", name); +} + +void MultiplayerScores::addScore(uint i, const Score &score) +{ + QVariant name = _scores[i].data("name"); + double mean = _scores[i].data("mean score").toDouble(); + uint won = _scores[i].data("nb won games").toUInt(); + _scores[i] = score; + _scores[i].setData("name", name); + _nbGames[i]++; + mean += (double(score.score()) - mean) / _nbGames[i]; + _scores[i]._data["mean score"] = mean; + if ( score.type()==Won ) won++; + _scores[i]._data["nb won games"] = won; +} + +void MultiplayerScores::show(QWidget *parent) +{ + // check consistency + if ( _nbGames.size()<2 ) kdWarning(11002) << "less than 2 players" << endl; + else { + bool ok = true; + uint nb = _nbGames[0]; + for (uint i=1; i<_nbGames.size(); i++) + if ( _nbGames[i]!=nb ) ok = false; + if (!ok) + kdWarning(11002) << "players have not same number of games" << endl; + } + + // order the players according to the number of won games + QValueVector<Score> ordered; + for (uint i=0; i<_scores.size(); i++) { + uint won = _scores[i].data("nb won games").toUInt(); + double mean = _scores[i].data("mean score").toDouble(); + QValueVector<Score>::iterator it; + for(it = ordered.begin(); it!=ordered.end(); ++it) { + uint cwon = (*it).data("nb won games").toUInt(); + double cmean = (*it).data("mean score").toDouble(); + if ( won<cwon || (won==cwon && mean<cmean) ) { + ordered.insert(it, _scores[i]); + break; + } + } + if ( it==ordered.end() ) ordered.push_back(_scores[i]); + } + + // show the scores + KDialogBase dialog(KDialogBase::Plain, i18n("Multiplayers Scores"), + KDialogBase::Close, KDialogBase::Close, + parent, "show_multiplayers_score", true, true); + QHBoxLayout *hbox = new QHBoxLayout(dialog.plainPage(), + KDialog::marginHint(), KDialog::spacingHint()); + + QVBox *vbox = new QVBox(dialog.plainPage()); + hbox->addWidget(vbox); + if ( _nbGames[0]==0 ) (void)new QLabel(i18n("No game played."), vbox); + else { + (void)new QLabel(i18n("Scores for last game:"), vbox); + (void)new LastMultipleScoresList(ordered, vbox); + } + + if ( _nbGames[0]>1 ) { + vbox = new QVBox(dialog.plainPage()); + hbox->addWidget(vbox); + (void)new QLabel(i18n("Scores for the last %1 games:") + .arg(_nbGames[0]), vbox); + (void)new TotalMultipleScoresList(ordered, vbox); + } + + dialog.enableButtonSeparator(false); + dialog.exec(); +} + +QDataStream &operator <<(QDataStream &s, const MultiplayerScores &score) +{ + s << score._scores; + s << score._nbGames; + return s; +} + +QDataStream &operator >>(QDataStream &s, MultiplayerScores &score) +{ + s >> score._scores; + s >> score._nbGames; + return s; +} + +} // namespace diff --git a/libkdegames/highscore/kexthighscore_item.h b/libkdegames/highscore/kexthighscore_item.h new file mode 100644 index 00000000..0200fabd --- /dev/null +++ b/libkdegames/highscore/kexthighscore_item.h @@ -0,0 +1,317 @@ +/* + This file is part of the KDE games library + Copyright (C) 2001-2003 Nicolas Hadacek ([email protected]) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License version 2 as published by the Free Software Foundation. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#ifndef KEXTHIGHSCORE_ITEM_H +#define KEXTHIGHSCORE_ITEM_H + +#include <qvariant.h> +#include <qnamespace.h> +#include <qmap.h> +#include <qvaluevector.h> +#include <kdemacros.h> +class QWidget; + + +namespace KExtHighscore +{ + +//----------------------------------------------------------------------------- +/** + * This class defines how to convert and how to display + * a highscore element (such as the score, the date, ...) or a player + * info (such as the player name, the best score, ...). + */ +class KDE_EXPORT Item +{ + public: + /** + * Possible display format. + * <ul> + * <li> @p NoFormat : no formatting (default) </li> + * <li> @p OneDecimal : with one decimal (only for Double) </li> + * <li> @p Percentage : with one decimal + % (only for Double) </li> + * <li> @p MinuteTime : MM:SS ie 3600 is 00:00, 1 is 59:59 and 0 is + * undefined (only for UInt, Int and Double) </li> + * <li> @p DateTime : date and time according to locale (only for + * DateTime) </li> + * </ul> + */ + enum Format { NoFormat, OneDecimal, Percentage, MinuteTime, + DateTime }; + + /** + * Possible special value for display format. + * <ul> + * <li> @p NoSpecial : no special value ; a null DateTime is replaced by + * "--" (default) </li> + * <li> ZeroNotDefined : 0 is replaced by "--" (only for UInt, Int and + * Double) </li> + * <li> @p NegativeNotDefined : negative values are replaced by "--" (only + * for Int and Double) </li> + * <li> @p DefaultNotDefined : default value is replaced by "--" </li> + * <li> @p Anonymous : replace the special value ItemBase::ANONYMOUS + * by i18n("anonymous") (only for String) </li> + * </ul> + */ + enum Special { NoSpecial, ZeroNotDefined, NegativeNotDefined, + DefaultNotDefined, Anonymous }; + + /** + * Constructor. + * + * @param def default value ; the QVariant also gives the type of data. + * Be sure to cast the value to the required type (for e.g. with uint). + * @param label the label corresponding to the item. If empty, the item + * is not shown. + * @param alignment the alignment of the item. + */ + Item(const QVariant &def = QVariant::Invalid, + const QString &label = QString::null, int alignment = Qt::AlignRight); + + virtual ~Item(); + + /** + * Set the display format. + * @see Format + */ + void setPrettyFormat(Format format); + + /** + * Set the special value for display. + * @see Special + */ + void setPrettySpecial(Special special); + + /** + * @return if the item is shown. + */ + bool isVisible() const { return !_label.isEmpty(); } + + /** + * Set the label. + */ + void setLabel(const QString &label) { _label = label; } + + /** + * @return the label. + */ + QString label() const { return _label; } + + /** + * @return the alignment. + */ + int alignment() const { return _alignment; } + + /** + * Set default value. + */ + void setDefaultValue(const QVariant &value) { _default = value; } + + /** + * @return the default value. + */ + const QVariant &defaultValue() const { return _default; } + + /** + * @return the converted value (by default the value is left + * unchanged). Most of the time you don't need to reimplement this method. + * + * @param i the element index ("rank" for score / "id" for player) + * @param value the value to convert + */ + virtual QVariant read(uint i, const QVariant &value) const; + + /** + * @return the string to be displayed. You may need to reimplement this + * method for special formatting (different from the standard ones). + * + * @param i the element index ("rank" for score / "id" for player) + * @param value the value to convert + */ + virtual QString pretty(uint i, const QVariant &value) const; + + private: + QVariant _default; + QString _label; + int _alignment; + Format _format; + Special _special; + + class ItemPrivate; + ItemPrivate *d; + + static QString timeFormat(uint); +}; + +//----------------------------------------------------------------------------- +/** + * Possible score type. + * @p Won the game has been won. + * @p Lost the game has been lost or has been aborted. + * @p Draw the game is a draw. + */ +enum ScoreType { Won = 0, Lost = -1, Draw = -2 }; + +/** + * This class contains data for a score. You should not inherit from + * this class but reimplement the methods in Highscores. + */ +class KDE_EXPORT Score +{ + public: + Score(ScoreType type = Won); + + ~Score(); + + /** + * @return the game type. + */ + ScoreType type() const { return _type; } + + /** + * Set the game type. + */ + void setType(ScoreType type) { _type = type; } + + /** + * @return the data associated with the named Item. + */ + const QVariant &data(const QString &name) const; + + /** + * Set the data associated with the named Item. Note that the + * value should have the type of the default value of the + * Item. + */ + void setData(const QString &name, const QVariant &value); + + /** + * @return the score value. + * + * Equivalent to <pre>data("score").toUInt()</pre>. + */ + uint score() const { return data("score").toUInt(); } + + /** + * Set the score value. + * + * Equivalent to <pre>setData("score", score)</pre>. + */ + void setScore(uint score) { setData("score", score); } + + /** + * @return true if this is the worst possible score (ie the default + * argument of ScoreItem). + */ + bool isTheWorst() const; + + /** + * Comparison operator. + * + * @see Manager::isStrictlyLess + */ + bool operator <(const Score &score); + + private: + ScoreType _type; + QMap<QString, QVariant> _data; + + class ScorePrivate; + ScorePrivate *d; + + friend class MultiplayerScores; + + friend QDataStream &operator <<(QDataStream &stream, const Score &score); + friend QDataStream &operator >>(QDataStream &stream, Score &score); +}; + +KDE_EXPORT QDataStream &operator <<(QDataStream &stream, const Score &score); +KDE_EXPORT QDataStream &operator >>(QDataStream &stream, Score &score); + +/** + * This class is used to store and show scores for multiplayer games. + * + * Example of use: + * Initialize the class: + * <pre> + * KExtHighscore::MultiScore ms(2); + * ms.setPlayerName(0, "player 1"); + * ms.setPlayerName(1, "player 2"); + * </pre> + * At the end of each game, add the score of each players: + * <pre> + * KExtHighscore::Score score(KExtHighscore::Won); + * score.setScore(100); + * ms.addScore(0, score); + * score.setType(KExtHighscore::Lost); + * score.setScore(20); + * ms.addScore(1, score); + * </pre> + */ +class KDE_EXPORT MultiplayerScores +{ + public: + MultiplayerScores(); + + ~MultiplayerScores(); + + /** + * Set the number of players and clear the scores. + */ + void setPlayerCount(uint nb); + + /** + * Set the name of player. + */ + void setName(uint player, const QString &name); + + /** + * Add the score of player. + */ + void addScore(uint player, const Score &score); + + /** + * Clear all scores. + */ + void clear(); + + /** + * Show scores. + */ + void show(QWidget *parent); + + private: + QValueVector<uint> _nbGames; + QValueVector<Score> _scores; + + class MultiplayerScoresPrivate; + MultiplayerScoresPrivate *d; + + friend QDataStream &operator <<(QDataStream &stream, + const MultiplayerScores &score); + friend QDataStream &operator >>(QDataStream &stream, + MultiplayerScores &score); +}; + +KDE_EXPORT QDataStream &operator <<(QDataStream &stream, const MultiplayerScores &score); +KDE_EXPORT QDataStream &operator >>(QDataStream &stream, MultiplayerScores &score); + +} // namespace + +#endif diff --git a/libkdegames/highscore/kexthighscore_tab.cpp b/libkdegames/highscore/kexthighscore_tab.cpp new file mode 100644 index 00000000..3e9cbe8a --- /dev/null +++ b/libkdegames/highscore/kexthighscore_tab.cpp @@ -0,0 +1,281 @@ +/* + This file is part of the KDE games library + Copyright (C) 2002 Nicolas Hadacek ([email protected]) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License version 2 as published by the Free Software Foundation. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#include "kexthighscore_tab.h" +#include "kexthighscore_tab.moc" + +#include <qlayout.h> +#include <qlabel.h> +#include <qvgroupbox.h> +#include <qgrid.h> +#include <qheader.h> + +#include <kdialogbase.h> +#include <klistview.h> +#include <kdebug.h> +#include <kglobal.h> + +#include "kexthighscore.h" +#include "kexthighscore_internal.h" + + +namespace KExtHighscore +{ + +//----------------------------------------------------------------------------- +PlayersCombo::PlayersCombo(QWidget *parent, const char *name) + : QComboBox(parent, name) +{ + const PlayerInfos &p = internal->playerInfos(); + for (uint i = 0; i<p.nbEntries(); i++) + insertItem(p.prettyName(i)); + insertItem(QString("<") + i18n("all") + '>'); + connect(this, SIGNAL(activated(int)), SLOT(activatedSlot(int))); +} + +void PlayersCombo::activatedSlot(int i) +{ + const PlayerInfos &p = internal->playerInfos(); + if ( i==(int)p.nbEntries() ) emit allSelected(); + else if ( i==(int)p.nbEntries()+1 ) emit noneSelected(); + else emit playerSelected(i); +} + +void PlayersCombo::load() +{ + const PlayerInfos &p = internal->playerInfos(); + for (uint i = 0; i<p.nbEntries(); i++) + changeItem(p.prettyName(i), i); +} + +//----------------------------------------------------------------------------- +AdditionalTab::AdditionalTab(QWidget *parent, const char *name) + : QWidget(parent, name) +{ + QVBoxLayout *top = new QVBoxLayout(this, KDialogBase::marginHint(), + KDialogBase::spacingHint()); + + QHBoxLayout *hbox = new QHBoxLayout(top); + QLabel *label = new QLabel(i18n("Select player:"), this); + hbox->addWidget(label); + _combo = new PlayersCombo(this); + connect(_combo, SIGNAL(playerSelected(uint)), + SLOT(playerSelected(uint))); + connect(_combo, SIGNAL(allSelected()), SLOT(allSelected())); + hbox->addWidget(_combo); + hbox->addStretch(1); +} + +void AdditionalTab::init() +{ + uint id = internal->playerInfos().id(); + _combo->setCurrentItem(id); + playerSelected(id); +} + +void AdditionalTab::allSelected() +{ + display(internal->playerInfos().nbEntries()); +} + +QString AdditionalTab::percent(uint n, uint total, bool withBraces) +{ + if ( n==0 || total==0 ) return QString::null; + QString s = QString("%1%").arg(100.0 * n / total, 0, 'f', 1); + return (withBraces ? QString("(") + s + ")" : s); +} + +void AdditionalTab::load() +{ + _combo->load(); +} + + +//----------------------------------------------------------------------------- +const char *StatisticsTab::COUNT_LABELS[Nb_Counts] = { + I18N_NOOP("Total:"), I18N_NOOP("Won:"), I18N_NOOP("Lost:"), + I18N_NOOP("Draw:") +}; +const char *StatisticsTab::TREND_LABELS[Nb_Trends] = { + I18N_NOOP("Current:"), I18N_NOOP("Max won:"), I18N_NOOP("Max lost:") +}; + +StatisticsTab::StatisticsTab(QWidget *parent) + : AdditionalTab(parent, "statistics_tab") +{ + // construct GUI + QVBoxLayout *top = static_cast<QVBoxLayout *>(layout()); + + QHBoxLayout *hbox = new QHBoxLayout(top); + QVBoxLayout *vbox = new QVBoxLayout(hbox); + QVGroupBox *group = new QVGroupBox(i18n("Game Counts"), this); + vbox->addWidget(group); + QGrid *grid = new QGrid(3, group); + grid->setSpacing(KDialogBase::spacingHint()); + for (uint k=0; k<Nb_Counts; k++) { + if ( Count(k)==Draw && !internal->showDrawGames ) continue; + (void)new QLabel(i18n(COUNT_LABELS[k]), grid); + _nbs[k] = new QLabel(grid); + _percents[k] = new QLabel(grid); + } + + group = new QVGroupBox(i18n("Trends"), this); + vbox->addWidget(group); + grid = new QGrid(2, group); + grid->setSpacing(KDialogBase::spacingHint()); + for (uint k=0; k<Nb_Trends; k++) { + (void)new QLabel(i18n(TREND_LABELS[k]), grid); + _trends[k] = new QLabel(grid); + } + + hbox->addStretch(1); + top->addStretch(1); +} + +void StatisticsTab::load() +{ + AdditionalTab::load(); + const PlayerInfos &pi = internal->playerInfos(); + uint nb = pi.nbEntries(); + _data.resize(nb+1); + for (uint i=0; i<_data.size()-1; i++) { + _data[i].count[Total] = pi.item("nb games")->read(i).toUInt(); + _data[i].count[Lost] = pi.item("nb lost games")->read(i).toUInt() + + pi.item("nb black marks")->read(i).toUInt(); // legacy + _data[i].count[Draw] = pi.item("nb draw games")->read(i).toUInt(); + _data[i].count[Won] = _data[i].count[Total] - _data[i].count[Lost] + - _data[i].count[Draw]; + _data[i].trend[CurrentTrend] = + pi.item("current trend")->read(i).toInt(); + _data[i].trend[WonTrend] = pi.item("max won trend")->read(i).toUInt(); + _data[i].trend[LostTrend] = + -(int)pi.item("max lost trend")->read(i).toUInt(); + } + + for (uint k=0; k<Nb_Counts; k++) _data[nb].count[k] = 0; + for (uint k=0; k<Nb_Trends; k++) _data[nb].trend[k] = 0; + for (uint i=0; i<_data.size()-1; i++) { + for (uint k=0; k<Nb_Counts; k++) + _data[nb].count[k] += _data[i].count[k]; + for (uint k=0; k<Nb_Trends; k++) + _data[nb].trend[k] += _data[i].trend[k]; + } + for (uint k=0; k<Nb_Trends; k++) + _data[nb].trend[k] /= (_data.size()-1); + + init(); +} + +QString StatisticsTab::percent(const Data &d, Count count) const +{ + if ( count==Total ) return QString::null; + return AdditionalTab::percent(d.count[count], d.count[Total], true); +} + +void StatisticsTab::display(uint i) +{ + const Data &d = _data[i]; + for (uint k=0; k<Nb_Counts; k++) { + if ( Count(k) && !internal->showDrawGames ) continue; + _nbs[k]->setText(QString::number(d.count[k])); + _percents[k]->setText(percent(d, Count(k))); + } + for (uint k=0; k<Nb_Trends; k++) { + QString s; + if ( d.trend[k]>0 ) s = '+'; + int prec = (i==internal->playerInfos().nbEntries() ? 1 : 0); + _trends[k]->setText(s + QString::number(d.trend[k], 'f', prec)); + } +} + +//----------------------------------------------------------------------------- +HistogramTab::HistogramTab(QWidget *parent) + : AdditionalTab(parent, "histogram_tab") +{ + // construct GUI + QVBoxLayout *top = static_cast<QVBoxLayout *>(layout()); + + _list = new KListView(this); + _list->setSelectionMode(QListView::NoSelection); + _list->setItemMargin(3); + _list->setAllColumnsShowFocus(true); + _list->setSorting(-1); + _list->header()->setClickEnabled(false); + _list->header()->setMovingEnabled(false); + top->addWidget(_list); + + _list->addColumn(i18n("From")); + _list->addColumn(i18n("To")); + _list->addColumn(i18n("Count")); + _list->addColumn(i18n("Percent")); + for (uint i=0; i<4; i++) _list->setColumnAlignment(i, AlignRight); + _list->addColumn(QString::null); + + const Item *sitem = internal->scoreInfos().item("score")->item(); + const PlayerInfos &pi = internal->playerInfos(); + const QMemArray<uint> &sh = pi.histogram(); + for (uint k=1; k<pi.histoSize(); k++) { + QString s1 = sitem->pretty(0, sh[k-1]); + QString s2; + if ( k==sh.size() ) s2 = "..."; + else if ( sh[k]!=sh[k-1]+1 ) s2 = sitem->pretty(0, sh[k]); + (void)new KListViewItem(_list, s1, s2); + } +} + +void HistogramTab::load() +{ + AdditionalTab::load(); + const PlayerInfos &pi = internal->playerInfos(); + uint n = pi.nbEntries(); + uint s = pi.histoSize() - 1; + _counts.resize((n+1) * s); + _data.fill(0, n+1); + for (uint k=0; k<s; k++) { + _counts[n*s + k] = 0; + for (uint i=0; i<n; i++) { + uint nb = pi.item(pi.histoName(k+1))->read(i).toUInt(); + _counts[i*s + k] = nb; + _counts[n*s + k] += nb; + _data[i] += nb; + _data[n] += nb; + } + } + + init(); +} + +void HistogramTab::display(uint i) +{ + const PlayerInfos &pi = internal->playerInfos(); + QListViewItem *item = _list->firstChild(); + uint s = pi.histoSize() - 1; + for (int k=s-1; k>=0; k--) { + uint nb = _counts[i*s + k]; + item->setText(2, QString::number(nb)); + item->setText(3, percent(nb, _data[i])); + uint width = (_data[i]==0 ? 0 : qRound(150.0 * nb / _data[i])); + QPixmap pixmap(width, 10); + pixmap.fill(blue); + item->setPixmap(4, pixmap); + item = item->nextSibling(); + } +} + +} // namespace diff --git a/libkdegames/highscore/kexthighscore_tab.h b/libkdegames/highscore/kexthighscore_tab.h new file mode 100644 index 00000000..ce47a75f --- /dev/null +++ b/libkdegames/highscore/kexthighscore_tab.h @@ -0,0 +1,117 @@ +/* + This file is part of the KDE games library + Copyright (C) 2002 Nicolas Hadacek ([email protected]) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License version 2 as published by the Free Software Foundation. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#ifndef KEXTHIGHSCORE_TAB_H +#define KEXTHIGHSCORE_TAB_H + +#include <qcombobox.h> +#include <qmemarray.h> + +class QLabel; +class KListView; + + +namespace KExtHighscore +{ + +//----------------------------------------------------------------------------- +class PlayersCombo : public QComboBox +{ + Q_OBJECT + public: + PlayersCombo(QWidget *parent = 0, const char *name = 0); + + void load(); + + signals: + void playerSelected(uint i); + void allSelected(); + void noneSelected(); + + private slots: + void activatedSlot(int i); +}; + +//----------------------------------------------------------------------------- +class AdditionalTab : public QWidget +{ + Q_OBJECT + public: + AdditionalTab(QWidget *parent, const char *name); + + virtual void load(); + + private slots: + void playerSelected(uint i) { display(i) ; } + void allSelected(); + + protected: + void init(); + static QString percent(uint n, uint total, bool withBraces = false); + virtual void display(uint i) = 0; + + private: + PlayersCombo *_combo; +}; + +//----------------------------------------------------------------------------- +class StatisticsTab : public AdditionalTab +{ + Q_OBJECT + public: + StatisticsTab(QWidget *parent); + + void load(); + + private: + enum Count { Total = 0, Won, Lost, Draw, Nb_Counts }; + static const char *COUNT_LABELS[Nb_Counts]; + enum Trend { CurrentTrend = 0, WonTrend, LostTrend, Nb_Trends }; + static const char *TREND_LABELS[Nb_Trends]; + struct Data { + uint count[Nb_Counts]; + double trend[Nb_Trends]; + }; + QMemArray<Data> _data; + QLabel *_nbs[Nb_Counts], *_percents[Nb_Counts], *_trends[Nb_Trends]; + + QString percent(const Data &, Count) const; + void display(uint i); +}; + +//----------------------------------------------------------------------------- +class HistogramTab : public AdditionalTab +{ + Q_OBJECT + public: + HistogramTab(QWidget *parent); + + void load(); + + private: + QMemArray<uint> _counts; + QMemArray<uint> _data; + KListView *_list; + + void display(uint i); +}; + +} // namespace + +#endif diff --git a/libkdegames/highscore/kfilelock.cpp b/libkdegames/highscore/kfilelock.cpp new file mode 100644 index 00000000..7dc83b96 --- /dev/null +++ b/libkdegames/highscore/kfilelock.cpp @@ -0,0 +1,88 @@ +/* + This file is part of the KDE games library + Copyright (C) 2003 Nicolas Hadacek <[email protected]> + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License version 2 as published by the Free Software Foundation. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#include "kfilelock.h" + +#include <unistd.h> +#include <sys/file.h> +#include <errno.h> + +#include <kdebug.h> + + +KFileLock::KFileLock(int fd) + : _fd(fd), _locked(false) +{} + +int KFileLock::lock() +{ + kdDebug(11002) << "lock fd=" << _fd << endl; +#ifdef F_SETLK +# ifndef SEEK_SET +# define SEEK_SET 0 +# endif + struct flock lock_data; + lock_data.l_type = F_WRLCK; + lock_data.l_whence = SEEK_SET; + lock_data.l_start = lock_data.l_len = 0; + if ( fcntl(_fd, F_SETLK, &lock_data)==-1 ) { + if ( errno==EAGAIN ) return -2; + return -1; + } +#else +# ifdef LOCK_EX + if ( flock (_fd, LOCK_EX|LOCK_NB)==-1 ) { + if ( errno==EWOULDBLOCK ) return -2; + return -1; + } +# else + if ( lockf(_fd, F_TLOCK, 0)==-1 ) { + if ( errno==EACCES ) return -2; + return -1; + } +# endif +#endif + _locked = true; + return 0; +} + +KFileLock::~KFileLock() +{ + unlock(); +} + +void KFileLock::unlock() +{ + if ( !_locked ) return; + kdDebug(11002) << "unlock" << endl; +# ifdef F_SETLK + struct flock lock_data; + lock_data.l_type = F_UNLCK; + lock_data.l_whence = SEEK_SET; + lock_data.l_start = lock_data.l_len = 0; + (void)fcntl(_fd, F_SETLK, &lock_data); +# else +# ifdef F_ULOCK + lockf(_fd, F_ULOCK, 0); +# else + flock(_fd, LOCK_UN); +# endif +# endif + _locked = false; +} diff --git a/libkdegames/highscore/kfilelock.h b/libkdegames/highscore/kfilelock.h new file mode 100644 index 00000000..2e1841ba --- /dev/null +++ b/libkdegames/highscore/kfilelock.h @@ -0,0 +1,53 @@ +/* + This file is part of the KDE games library + Copyright (C) 2003 Nicolas Hadacek <[email protected]> + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License version 2 as published by the Free Software Foundation. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ +#ifndef KFILELOCK_H +#define KFILELOCK_H + + +class KFileLock +{ +public: + KFileLock(int fd); + + /** Call unlock(). */ + ~KFileLock(); + + /** @return the file descriptor. */ + int fd() const { return _fd; } + + /* + * Lock the file. + * @return 0 on success, -1 on failure (no permission) and -2 if another + * process is currently locking the file. + */ + int lock(); + + /** Unlock the file. */ + void unlock(); + + /** @return true if we currently lock the file. */ + bool isLocked() const { return _locked; } + +private: + int _fd; + bool _locked; +}; + + +#endif diff --git a/libkdegames/highscore/khighscore.cpp b/libkdegames/highscore/khighscore.cpp new file mode 100644 index 00000000..4e1f68e5 --- /dev/null +++ b/libkdegames/highscore/khighscore.cpp @@ -0,0 +1,262 @@ +/* + This file is part of the KDE games library + Copyright (C) 2001 Andreas Beckermann ([email protected]) + Copyright (C) 2003 Nicolas Hadacek <[email protected]> + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License version 2 as published by the Free Software Foundation. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ +/* + $Id$ +*/ + +#include <config.h> +#include <fcntl.h> +#include <unistd.h> +#include <sys/file.h> + +#include <kapplication.h> +#include <ksimpleconfig.h> +#include <kglobal.h> +#include <kstdguiitem.h> +#include <klocale.h> +#include <kmessagebox.h> +#include <kdebug.h> +#include <kstaticdeleter.h> + +#include "khighscore.h" +#include "kconfigrawbackend.h" +#include "kfilelock.h" + +#define GROUP "KHighscore" + +class KHighscorePrivate +{ +public: + KHighscorePrivate() {} + + QString group; + bool global; +}; + +KFileLock *KHighscore::_lock = 0; +KRawConfig *KHighscore::_config = 0; +static KStaticDeleter<KFileLock> lockSD; +static KStaticDeleter<KRawConfig> configSD; + + +KHighscore::KHighscore(QObject* parent) + : QObject(parent) +{ + init(true); +} + +KHighscore::KHighscore(bool forceLocal, QObject* parent) + : QObject(parent) +{ + init(forceLocal); +} + +void KHighscore::init(bool forceLocal) +{ + d = new KHighscorePrivate; +#ifdef HIGHSCORE_DIRECTORY + d->global = !forceLocal; + if ( d->global && _lock==0 ) + kdFatal(11002) << "KHighscore::init should be called before!!" << endl; +#else + d->global = false; + Q_UNUSED(forceLocal); +#endif + readCurrentConfig(); +} + +bool KHighscore::isLocked() const +{ + return (d->global ? _lock->isLocked() : true); +} + +void KHighscore::readCurrentConfig() +{ + if ( d->global ) _config->reparseConfiguration(); +} + +void KHighscore::init(const char *appname) +{ +#ifdef HIGHSCORE_DIRECTORY + const QString filename = QString::fromLocal8Bit("%1/%2.scores") + .arg(HIGHSCORE_DIRECTORY).arg(appname); + int fd = open(filename.local8Bit(), O_RDWR); + if ( fd<0 ) kdFatal(11002) << "cannot open global highscore file \"" + << filename << "\"" << endl; + lockSD.setObject(_lock, new KFileLock(fd)); + configSD.setObject(_config, new KRawConfig(fd, true)); // read-only + + // drop the effective gid + int gid = getgid(); + setregid(gid, gid); +#else + Q_UNUSED(appname); +#endif +} + +bool KHighscore::lockForWriting(QWidget *widget) +{ + if ( isLocked() ) return true; + + bool first = true; + for (;;) { + kdDebug(11002) << "try locking" << endl; + // lock the highscore file (it should exist) + int result = _lock->lock(); + bool ok = ( result==0 ); + kdDebug(11002) << "locking system-wide highscore file res=" + << result << " (ok=" << ok << ")" << endl; + if (ok) { + readCurrentConfig(); + _config->setReadOnly(false); + return true; + } + + if ( !first ) { + KGuiItem item = KStdGuiItem::cont(); + item.setText(i18n("Retry")); + int res = KMessageBox::warningContinueCancel(widget, i18n("Cannot access the highscore file. Another user is probably currently writing to it."), QString::null, item, "ask_lock_global_highscore_file"); + if ( res==KMessageBox::Cancel ) break; + } else sleep(1); + first = false; + } + return false; +} + +void KHighscore::writeAndUnlock() +{ + if ( !d->global ) { + kapp->config()->sync(); + return; + } + if ( !isLocked() ) return; + + kdDebug(11002) << "unlocking" << endl; + _config->sync(); // write config + _lock->unlock(); + _config->setReadOnly(true); +} + +KHighscore::~KHighscore() +{ + writeAndUnlock(); + delete d; +} + +KConfig* KHighscore::config() const +{ + return (d->global ? _config : kapp->config()); +} + +void KHighscore::writeEntry(int entry, const QString& key, const QVariant& value) +{ + Q_ASSERT( isLocked() ); + KConfigGroupSaver cg(config(), group()); + QString confKey = QString("%1_%2").arg(entry).arg(key); + cg.config()->writeEntry(confKey, value); +} + +void KHighscore::writeEntry(int entry, const QString& key, int value) +{ + Q_ASSERT( isLocked() ); + KConfigGroupSaver cg(config(), group()); + QString confKey = QString("%1_%2").arg(entry).arg(key); + cg.config()->writeEntry(confKey, value); +} + +void KHighscore::writeEntry(int entry, const QString& key, const QString &value) +{ + Q_ASSERT (isLocked() ); + KConfigGroupSaver cg(config(), group()); + QString confKey = QString("%1_%2").arg(entry).arg(key); + cg.config()->writeEntry(confKey, value); +} + +QVariant KHighscore::readPropertyEntry(int entry, const QString& key, const QVariant& pDefault) const +{ + KConfigGroupSaver cg(config(), group()); + QString confKey = QString("%1_%2").arg(entry).arg(key); + return cg.config()->readPropertyEntry(confKey, pDefault); +} + +QString KHighscore::readEntry(int entry, const QString& key, const QString& pDefault) const +{ + KConfigGroupSaver cg(config(), group()); + QString confKey = QString("%1_%2").arg(entry).arg(key); + return cg.config()->readEntry(confKey, pDefault); +} + +int KHighscore::readNumEntry(int entry, const QString& key, int pDefault) const +{ + KConfigGroupSaver cg(config(), group()); + QString confKey = QString("%1_%2").arg(entry).arg(key); + return cg.config()->readNumEntry(confKey, pDefault); +} + +bool KHighscore::hasEntry(int entry, const QString& key) const +{ + KConfigGroupSaver cg(config(), group()); + QString confKey = QString("%1_%2").arg(entry).arg(key); + return cg.config()->hasKey(confKey); +} + +QStringList KHighscore::readList(const QString& key, int lastEntry) const +{ + QStringList list; + for (int i = 1; hasEntry(i, key) && ((lastEntry > 0) ? (i <= lastEntry) : true); i++) { + list.append(readEntry(i, key)); + } + return list; +} + +void KHighscore::writeList(const QString& key, const QStringList& list) +{ + for (int unsigned i = 1; i <= list.count(); i++) { + writeEntry(i, key, list[i - 1]); + } +} + +void KHighscore::setHighscoreGroup(const QString& group) +{ + d->group = group; +} + +const QString& KHighscore::highscoreGroup() const +{ + return d->group; +} + +QString KHighscore::group() const +{ + if ( highscoreGroup().isNull() ) + return (d->global ? QString::null : GROUP); + return (d->global ? highscoreGroup() + : QString("%1_%2").arg(GROUP).arg(highscoreGroup())); +} + +bool KHighscore::hasTable() const +{ return config()->hasGroup(group()); } + +void KHighscore::sync() +{ + writeAndUnlock(); +} + +#include "khighscore.moc" diff --git a/libkdegames/highscore/khighscore.h b/libkdegames/highscore/khighscore.h new file mode 100644 index 00000000..b1e3d25f --- /dev/null +++ b/libkdegames/highscore/khighscore.h @@ -0,0 +1,311 @@ +/* + This file is part of the KDE games library + Copyright (C) 2001 Andreas Beckermann ([email protected]) + Copyright (C) 2003 Nicolas Hadacek <[email protected]> + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License version 2 as published by the Free Software Foundation. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ +/* + $Id$ +*/ +#ifndef __KHIGHSCORE_H__ +#define __KHIGHSCORE_H__ + +#include <qstring.h> +#include <qobject.h> +#include <kdemacros.h> +class KConfig; +class KFileLock; +class KRawConfig; +class KHighscorePrivate; + +/** + * @short Class for managing highscore tables + * + * This is the KDE class for saving and reading highscore tables. It offers the + * possibility for system-wide highscore tables (configure with e.g. + * --enable-highscore-dir=/var/games) and a theoretically unlimited number of + * entries. + * + * You can specify different "keys" for an entry - just like the KConfig + * keys. But it will be prefixed with the number of the entry. For example you + * will probably use something like this to save the name of the player on the + * top of the list (ie the winner): + * \code + * highscore->writeEntry(1, "name", myPlayer->name()); + * \endcode + * Note that it doesn't really matter if you use "0" or "1" as the first entry + * of the list as long as your program always uses the same for the first + * entry. I recommend to use "1", as several convenience methods use this. + * + * You can also specify different groups using setHighscoreGroup. Just + * like the keys mentioned above the groups behave like groups in KConfig + * but are prefixed with "KHighscore_". The default group is just "KHighscore". + * You might use this e.g. to create different highscore tables like + * \code + * table->setHighscoreGroup("Easy"); + * // write the highscores for level "easy" to the table + * writeEasyHighscores(table); + * + * table->setHighscore("Player_1"); + * // write player specific highscores to the table + * writePlayerHighscores(table); + * \endcode + * As you can see above you can also use this to write the highscores of a + * single player, so the "best times" of a player. To write highscores for a + * specific player in a specific level you will have to use a more complex way: + * \code + * QString group = QString("%1_%2").arg(player).arg(level); + * table->setGroup(group); + * writeHighscore(table, player, level); + * \endcode + * + * Also note that you MUST NOT mark the key or the group for translation! I.e. + * don't use i18n() for the keys or groups! Here is the code to read the above + * written entry: + * \code + * QString firstName = highscore->readEntry(0, "name"); + * \endcode + * Easy, what? + * @author Andreas Beckermann <[email protected]> + **/ +class KDE_EXPORT KHighscore : public QObject +{ + Q_OBJECT +public: + /** @obsolete + * Constructor. The highscore file is forced to be local to support + * games using the old behaviour. + */ + KHighscore(QObject* parent = 0); + + /** + * Constructor. + * + * @param forceLocal if true, the local highscore file is used even + * when the configuration has been set to use a system-wide file. This + * is convenient for converting highscores from legacy applications. + * @param parent parent widget for this widget + * @since 3.2 + */ + KHighscore(bool forceLocal, QObject *parent); + + /** + * Read the current state of the highscore file. Remember that when + * it's not locked for writing, this file can change at any time. + * (This method is only useful for a system-wide highscore file). + * @since 3.2 + */ + void readCurrentConfig(); + + /** @since 3.2 + * This method open the system-wide highscore file using the effective + * group id of the game executable (which should be "games"). The + * effective group id is completely dropped afterwards. + * + * Note: this method should be called in main() before creating a + * KApplication and doing anything else (KApplication checks that the + * program is not suid/sgid and will exit the program for security + * reason if it is the case). + */ + static void init(const char *appname); + + /** @since 3.2 + * Lock the system-wide highscore file for writing (does nothing and + * return true if the local file is used). + * You should perform writing without GUI interaction to avoid + * blocking and don't forget to unlock the file as soon as possible + * with writeAndUnlock(). + * + * If the config file cannot be locked, + * the method waits for 1 second and, if it failed again, displays + * a message box asking for retry or cancel. + * @param widget used as the parent of the message box. + * + * @return false on error or if the config file is locked by another + * process. In such case, the config stays read-only. + */ + bool lockForWriting(QWidget *widget = 0); + + /** + * Effectively write and unlock the system-wide highscore file + * (@see lockForWriting). + * If using a local highscore file, it will sync the config. + * @since 3.2 + */ + void writeAndUnlock(); + + /** + * @return true if the highscore file is locked or if a local + * file is used. + * @since 3.2 + */ + bool isLocked() const; + + /** + * Destructor. + * If necessary, write and unlock the highscore file. + */ + ~KHighscore(); + + /** + * @param entry The number of the entry / the placing of the player + * @param key A key for this entry. E.g. "name" for the name of the + * player. Nearly the same as the usual keys in KConfig - but they + * are prefixed with the entry number + * @param value The value of this entry + **/ + void writeEntry(int entry, const QString& key, const QString& value); + + /** + * This is an overloaded member function, provided for convenience. + * It differs from the above function only in what argument(s) it accepts. + **/ + void writeEntry(int entry, const QString& key, int value); + + /** + * This is an overloaded member function, provided for convenience. + * It differs from the above function only in what argument(s) it accepts. + * See KConfigBase documentation for allowed QVariant::Type. + **/ + void writeEntry(int entry, const QString& key, const QVariant &value); + + /** + * Reads an entry from the highscore table. + * @param entry The number of the entry / the placing to be read + * @param key The key of the entry. E.g. "name" for the name of the + * player. Nearly the same as the usual keys in KConfig - but they + * are prefixed with the entry number + * @param pDefault This will be used as default value if the key+pair + * entry can't be found. + * @return The value of this entry+key pair or pDefault if the entry+key + * pair doesn't exist + **/ + QString readEntry(int entry, const QString& key, const QString& pDefault = QString::null) const; + + /** + * Read a numeric value. + * @param entry The number of the entry / the placing to be read + * @param key The key of the entry. E.g. "name" for the name of the + * player. Nearly the same as the usual keys in KConfig - but they + * are prefixed with the entry number + * @param pDefault This will be used as default value if the key+pair + * entry can't be found. + * @return The value of this entry+key pair or pDefault if the entry+key + * pair doesn't exist + **/ + int readNumEntry(int entry, const QString& key, int pDefault = -1) const; + + /** + * Read a QVariant entry. + * See KConfigBase documentation for allowed QVariant::Type. + * + * @return the value of this entry+key pair or pDefault if the entry+key + * pair doesn't exist or + */ + QVariant readPropertyEntry(int entry, const QString &key, const QVariant &pDefault) const; + + /** + * @return True if the highscore table conatins the entry/key pair, + * otherwise false + **/ + bool hasEntry(int entry, const QString& key) const; + + /** + * Reads a list of entries from the highscore table starting at 1 until + * lastEntry. If an entry between those numbers doesn't exist the + * function aborts reading even if after the missing entry is an + * existing one. The first entry of the list is the first placing, the + * last on is the last placing. + * @return A list of the entries of this key. You could also call + * readEntry(i, key) where i is from 1 to 20. Note that this function + * depends on "1" as the first entry! + * @param key The key of the entry. E.g. "name" for the name of the + * player. Nearly the same as the usual keys in KConfig - but they + * are prefixed with the entry number + * @param lastEntry the last entry which will be includes into the list. + * 1 will include a list with maximal 1 entry - 20 a list with maximal + * 20 entries. If lastEntry is <= 0 then rading is only stopped when when an + * entry does not exist. + **/ + QStringList readList(const QString& key, int lastEntry = 20) const; + + /** + * Writes a list of entries to the highscore table. + * + * The first entry is prefixed with "1". Using this method is a short + * way of calling writeEntry(i, key, list[i]) from i = 1 to + * list.count() + * @param key A key for the entry. E.g. "name" for the name of the + * player. Nearly the same as the usual keys in KConfig - but they + * are prefixed with the entry number + * @param list The list of values + **/ + void writeList(const QString& key, const QStringList& list); + + /** + * @return Whether a highscore table exists. You can use this + * function to indicate whether KHighscore created a highscore table + * before and - if not - read your old (non-KHighscore) table instead. + * This way you can safely read an old table and save it using + * KHighscore without losing any data + **/ + bool hasTable() const; + + /** @obsolete + * This does the same as writeAndUnlock(). + */ + void sync(); + + /** + * Set the new highscore group. The group is being prefixed with + * "KHighscore_" in the table. + * @param groupname The new groupname. E.g. use "easy" for the easy + * level of your game. If you use QString::null (the default) the + * default group is used. + **/ + void setHighscoreGroup(const QString& groupname = QString::null); + + /** + * @return The currently used group. This doesn't contain the prefix + * ("KHighscore_") but the same as setHighscoreGroup uses. The + * default is QString::null + **/ + const QString& highscoreGroup() const; + +protected: + /** + * @return A groupname to be used in KConfig. Used internally to + * prefix the value from highscoreGroup() with "KHighscore_" + **/ + QString group() const; + + /** + * @return A pointer to the KConfig object to be used. This is + * either kapp->config() (default) or a KSimpleConfig object for + * a system-wide highscore file. + **/ + KConfig* config() const; + + void init(bool forceLocal); + +private: + KHighscorePrivate* d; + + static KFileLock *_lock; // lock on system-wide highscore file + static KRawConfig *_config; // config for system-wide highscore file +}; + +#endif diff --git a/libkdegames/highscore/kscoredialog.cpp b/libkdegames/highscore/kscoredialog.cpp new file mode 100644 index 00000000..37155650 --- /dev/null +++ b/libkdegames/highscore/kscoredialog.cpp @@ -0,0 +1,411 @@ +/**************************************************************** +Copyright (c) 1998 Sandro Sigala <[email protected]>. +Copyright (c) 2001 Waldo Bastian <[email protected]> +All rights reserved. + +Permission to use, copy, modify, and distribute this software +and its documentation for any purpose and without fee is hereby +granted, provided that the above copyright notice appear in all +copies and that both that the copyright notice and this +permission notice and warranty disclaimer appear in supporting +documentation, and that the name of the author not be used in +advertising or publicity pertaining to distribution of the +software without specific, written prior permission. + +The author disclaim all warranties with regard to this +software, including all implied warranties of merchantability +and fitness. In no event shall the author be liable for any +special, indirect or consequential damages or any damages +whatsoever resulting from loss of use, data or profits, whether +in an action of contract, negligence or other tortious action, +arising out of or in connection with the use or performance of +this software. +****************************************************************/ + +#include "config.h" + +#include <qlabel.h> +#include <qlayout.h> +#include <qlineedit.h> +#include <qwidgetstack.h> +#include <qtimer.h> +#include <qevent.h> +#include <qptrvector.h> + +#include <kapplication.h> +#include <kconfig.h> +#include <klocale.h> +#include <kseparator.h> + +#include "kscoredialog.h" + +class KScoreDialog::KScoreDialogPrivate +{ +public: + QPtrList<FieldInfo> scores; + QWidget *page; + QGridLayout *layout; + QLineEdit *edit; + QPtrVector<QWidgetStack> stack; + QPtrVector<QLabel> labels; + QLabel *commentLabel; + QString comment; + int fields; + int newName; + int latest; + int nrCols; + bool loaded; + QString configGroup; + + QMap<int, int> col; + QMap<int, QString> header; + QMap<int, QString> key; + QString player; +}; + + +KScoreDialog::KScoreDialog(int fields, QWidget *parent, const char *oname) + : KDialogBase(parent, oname, true, i18n("High Scores"), Ok, Ok, true) +{ + d = new KScoreDialogPrivate(); + d->edit = 0; + d->fields = fields; + d->newName = -1; + d->latest = -1; + d->loaded = false; + d->nrCols = 0; + d->configGroup = "High Score"; + + d->scores.setAutoDelete(true); + d->header[Name] = i18n("Name"); + d->key[Name] = "Name"; + + d->header[Date] = i18n("Date"); + d->key[Date] = "Date"; + + d->header[Level] = i18n("Level"); + d->key[Level] = "Level"; + + d->header[Score] = i18n("Score"); + d->key[Score] = "Score"; + d->page = makeMainWidget(); + + connect(this, SIGNAL(okClicked()), SLOT(slotGotName())); +} + +KScoreDialog::~KScoreDialog() +{ + delete d; +} + +void KScoreDialog::setConfigGroup(const QString &group) +{ + d->configGroup = group; + d->loaded = false; +} + +void KScoreDialog::setComment(const QString &comment) +{ + d->comment = comment; +} + +void KScoreDialog::addField(int field, const QString &header, const QString &key) +{ + d->fields |= field; + d->header[field] = header; + d->key[field] = key; +} + +void KScoreDialog::setupDialog() +{ + d->nrCols = 1; + + for(int field = 1; field < d->fields; field = field * 2) + { + if (d->fields & field) + d->col[field] = d->nrCols++; + } + + d->layout = new QGridLayout(d->page, 15, d->nrCols, marginHint() + 20, spacingHint()); + d->layout->addRowSpacing(4, 15); + + d->commentLabel = new QLabel(d->page); + d->commentLabel->setAlignment(AlignVCenter | AlignHCenter); + d->layout->addMultiCellWidget(d->commentLabel, 1, 1, 0, d->nrCols-1); + + QFont bold = font(); + bold.setBold(true); + + QLabel *label; + d->layout->addColSpacing(0, 50); + label = new QLabel(i18n("Rank"), d->page); + d->layout->addWidget(label, 3, 0); + label->setFont(bold); + + for(int field = 1; field < d->fields; field = field * 2) + { + if (d->fields & field) + { + d->layout->addColSpacing(d->col[field], 50); + + label = new QLabel(d->header[field], d->page); + d->layout->addWidget(label, 3, d->col[field], field <= Name ? AlignLeft : AlignRight); + label->setFont(bold); + } + } + + KSeparator *sep = new KSeparator(Horizontal, d->page); + d->layout->addMultiCellWidget(sep, 4, 4, 0, d->nrCols-1); + + d->labels.resize(d->nrCols * 10); + d->stack.resize(10); + + QString num; + for (int i = 1; i <= 10; ++i) { + QLabel *label; + num.setNum(i); + label = new QLabel(i18n("#%1").arg(num), d->page); + d->labels.insert((i-1)*d->nrCols + 0, label); + d->layout->addWidget(label, i+4, 0); + if (d->fields & Name) + { + QWidgetStack *stack = new QWidgetStack(d->page); + d->stack.insert(i-1, stack); + d->layout->addWidget(stack, i+4, d->col[Name]); + label = new QLabel(d->page); + d->labels.insert((i-1)*d->nrCols + d->col[Name], label); + stack->addWidget(label); + stack->raiseWidget(label); + } + for(int field = Name * 2; field < d->fields; field = field * 2) + { + if (d->fields & field) + { + label = new QLabel(d->page); + d->labels.insert((i-1)*d->nrCols + d->col[field], label); + d->layout->addWidget(label, i+4, d->col[field], AlignRight); + } + } + } +} + +void KScoreDialog::aboutToShow() +{ + if (!d->loaded) + loadScores(); + + if (!d->nrCols) + setupDialog(); + + d->commentLabel->setText(d->comment); + if (d->comment.isEmpty()) + { + d->commentLabel->setMinimumSize(QSize(1,1)); + d->commentLabel->hide(); + d->layout->addRowSpacing(0, -15); + d->layout->addRowSpacing(2, -15); + } + else + { + d->commentLabel->setMinimumSize(d->commentLabel->sizeHint()); + d->commentLabel->show(); + d->layout->addRowSpacing(0, -10); + d->layout->addRowSpacing(2, 10); + } + d->comment = QString::null; + + QFont normal = font(); + QFont bold = normal; + bold.setBold(true); + + QString num; + for (int i = 1; i <= 10; ++i) { + QLabel *label; + num.setNum(i); + FieldInfo *score = d->scores.at(i-1); + label = d->labels[(i-1)*d->nrCols + 0]; + if (i == d->latest) + label->setFont(bold); + else + label->setFont(normal); + + if (d->fields & Name) + { + if (d->newName == i) + { + QWidgetStack *stack = d->stack[i-1]; + d->edit = new QLineEdit(d->player, stack); + d->edit->setMinimumWidth(40); + stack->addWidget(d->edit); + stack->raiseWidget(d->edit); + d->edit->setFocus(); + connect(d->edit, SIGNAL(returnPressed()), + this, SLOT(slotGotReturn())); + } + else + { + label = d->labels[(i-1)*d->nrCols + d->col[Name]]; + if (i == d->latest) + label->setFont(bold); + else + label->setFont(normal); + label->setText((*score)[Name]); + } + + } + for(int field = Name * 2; field < d->fields; field = field * 2) + { + if (d->fields & field) + { + label = d->labels[(i-1)*d->nrCols + d->col[field]]; + if (i == d->latest) + label->setFont(bold); + else + label->setFont(normal); + label->setText((*score)[field]); + } + } + } + d->latest = -1; + setFixedSize(minimumSizeHint()); +} + +void KScoreDialog::loadScores() +{ + QString key, value; + d->loaded = true; + d->scores.clear(); + KConfigGroup config(kapp->config(), d->configGroup.utf8()); + + d->player = config.readEntry("LastPlayer"); + + QString num; + for (int i = 1; i <= 10; ++i) { + num.setNum(i); + FieldInfo *score = new FieldInfo(); + for(int field = 1; field < d->fields; field = field * 2) + { + if (d->fields & field) + { + key = "Pos" + num + d->key[field]; + (*score)[field] = config.readEntry(key, "-"); + } + } + d->scores.append(score); + } +} + +void KScoreDialog::saveScores() +{ + QString key, value; + KConfigGroup config(kapp->config(), d->configGroup.utf8()); + + config.writeEntry("LastPlayer", d->player); + + QString num; + for (int i = 1; i <= 10; ++i) { + num.setNum(i); + FieldInfo *score = d->scores.at(i-1); + for(int field = 1; field < d->fields; field = field * 2) + { + if (d->fields & field) + { + key = "Pos" + num + d->key[field]; + config.writeEntry(key, (*score)[field]); + } + } + } + kapp->config()->sync(); +} + +int KScoreDialog::addScore(int newScore, const FieldInfo &newInfo, bool askName) +{ + return addScore(newScore, newInfo, askName, false); +} + +int KScoreDialog::addScore(int newScore, const FieldInfo &newInfo, bool askName, bool lessIsMore) +{ + if (!d->loaded) + loadScores(); + FieldInfo *score = d->scores.first(); + int i = 1; + for(; score; score = d->scores.next(), i++) + { + bool ok; + int num_score = (*score)[Score].toLong(&ok); + if (lessIsMore && !ok) + num_score = 1 << 30; + if (((newScore > num_score) && !lessIsMore) || + ((newScore < num_score) && lessIsMore)) + { + score = new FieldInfo(newInfo); + (*score)[Score].setNum(newScore); + d->scores.insert(i-1, score); + d->scores.remove(10); + d->latest = i; + if (askName) + d->newName = i; + else + saveScores(); + if (i == 1) + d->comment = i18n("Excellent!\nYou have a new high score!"); + else + d->comment = i18n("Well done!\nYou made it to the high score list!"); + return i; + } + } + return 0; +} + +void KScoreDialog::show() +{ + aboutToShow(); + KDialogBase::show(); +} + +void KScoreDialog::slotGotReturn() +{ + QTimer::singleShot(0, this, SLOT(slotGotName())); +} + +void KScoreDialog::slotGotName() +{ + if (d->newName == -1) return; + + d->player = d->edit->text(); + + (*d->scores.at(d->newName-1))[Name] = d->player; + saveScores(); + + QFont bold = font(); + bold.setBold(true); + + QLabel *label = d->labels[(d->newName-1)*d->nrCols + d->col[Name]]; + label->setFont(bold); + label->setText(d->player); + d->stack[(d->newName-1)]->raiseWidget(label); + delete d->edit; + d->edit = 0; + d->newName = -1; +} + +int KScoreDialog::highScore() +{ + if (!d->loaded) + loadScores(); + + return (*d->scores.first())[Score].toInt(); +} + +void KScoreDialog::keyPressEvent( QKeyEvent *ev) +{ + if ((d->newName != -1) && (ev->key() == Key_Return)) + { + ev->ignore(); + return; + } + KDialogBase::keyPressEvent(ev); +} + + +#include "kscoredialog.moc" diff --git a/libkdegames/highscore/kscoredialog.h b/libkdegames/highscore/kscoredialog.h new file mode 100644 index 00000000..4d4a76db --- /dev/null +++ b/libkdegames/highscore/kscoredialog.h @@ -0,0 +1,125 @@ +/**************************************************************** +Copyright (c) 1998 Sandro Sigala <[email protected]>. +Copyright (c) 2001 Waldo Bastian <[email protected]> +All rights reserved. + +Permission to use, copy, modify, and distribute this software +and its documentation for any purpose and without fee is hereby +granted, provided that the above copyright notice appear in all +copies and that both that the copyright notice and this +permission notice and warranty disclaimer appear in supporting +documentation, and that the name of the author not be used in +advertising or publicity pertaining to distribution of the +software without specific, written prior permission. + +The author disclaim all warranties with regard to this +software, including all implied warranties of merchantability +and fitness. In no event shall the author be liable for any +special, indirect or consequential damages or any damages +whatsoever resulting from loss of use, data or profits, whether +in an action of contract, negligence or other tortious action, +arising out of or in connection with the use or performance of +this software. +****************************************************************/ + +#ifndef KSCOREDIALOG_H +#define KSCOREDIALOG_H + +#include <qmap.h> +#include <qptrlist.h> + +#include <kdialogbase.h> +#include <kdemacros.h> +class QGridLayout; +class QLineEdit; +class QWidgetStack; + +/** + * A simple high score dialog. + */ +class KDE_EXPORT KScoreDialog : public KDialogBase { + Q_OBJECT + +public: + enum Fields { Name = 1 << 0, + Level = 1 << 1, + + Custom1 = 1 << 10, + Custom2 = 1 << 11, + Custom3 = 1 << 12, + + Date = 1 << 27, + Time = 1 << 28, + Score = 1 << 29 }; + + typedef QMap<int, QString> FieldInfo; + + /** + * @param fields Which fields should be listed. + * @param parent passed to parent QWidget constructor + * @param name passed to parent QWidget constructor + */ + KScoreDialog(int fields, QWidget *parent=0, const char *name=0); + + ~KScoreDialog(); + + /** + * @param group to use for reading/writing highscores from/to. By default + * the class will use "High Score" + */ + void setConfigGroup(const QString &group); + + /** + * @param comment to add when showing high-scores. + * The comment is only used once. + */ + void setComment(const QString &comment); + + /** + * Define an extra FieldInfo entry. + * @param field Id of this field + * @param header Header shown in the dialog for this field + * @param key used to store this field with. + */ + void addField(int field, const QString &header, const QString &key); + + /** + * Adds a new score to the list. + * + * @param newScore the score of this game. + * @param newInfo additional info about the score. + * @param askName Whether to prompt for the players name. + * @param lessIsMore If true, the lowest score is the best score. + * + * @returns The highscore position if the score was good enough to + * make it into the list (1 being topscore) or 0 otherwise. + */ + int addScore(int newScore, const FieldInfo &newInfo, bool askName, bool lessIsMore); + int addScore(int newScore, const FieldInfo &newInfo, bool askName=true); + + /** + * Returns the current best score. + */ + int highScore(); + + virtual void show(); + +private slots: + void slotGotReturn(); + void slotGotName(); + +private: + /* read scores */ + void loadScores(); + void saveScores(); + + void aboutToShow(); + void setupDialog(); + void keyPressEvent( QKeyEvent *ev); + +private: + class KScoreDialogPrivate; + KScoreDialogPrivate *d; +}; + +#endif // !KSCOREDIALOG_H |