summaryrefslogtreecommitdiff
path: root/quacker/graphicalboard.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'quacker/graphicalboard.cpp')
-rw-r--r--quacker/graphicalboard.cpp1494
1 files changed, 1494 insertions, 0 deletions
diff --git a/quacker/graphicalboard.cpp b/quacker/graphicalboard.cpp
new file mode 100644
index 0000000..194d4c9
--- /dev/null
+++ b/quacker/graphicalboard.cpp
@@ -0,0 +1,1494 @@
+/*
+ * Quackle -- Crossword game artificial intelligence and analysis tool
+ * Copyright (C) 2005-2006 Jason Katz-Brown and John O'Laughlin.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+ * 02110-1301 USA
+ */
+
+#include <iostream>
+#include <math.h>
+
+#include <QtGui>
+
+#include <game.h>
+#include <move.h>
+#include <boardparameters.h>
+#include <quackleio/util.h>
+
+#include "geometry.h"
+#include "graphicalboard.h"
+#include "quackersettings.h"
+#include "settings.h"
+
+const double GraphicalBoardFrame::s_markOtherLengthMultiplier = 0.6;
+
+const double TileWidget::s_defaultLetterScale = 0.7;
+
+GraphicalBoard::GraphicalBoard(QWidget *parent)
+ : BoardWithQuickEntry(parent)
+{
+ m_boardFrame = new GraphicalBoardFrame;
+ connect(m_boardFrame, SIGNAL(localCandidateChanged(const Quackle::Move &)), this, SLOT(setLocalCandidate(const Quackle::Move &)));
+
+ m_boardWrapper = new QWidget;
+
+ QVBoxLayout *helperLayout = new QVBoxLayout(m_boardWrapper);
+ Geometry::setupInnerLayout(helperLayout);
+
+ m_vlayout->addWidget(m_boardWrapper);
+ helperLayout->addWidget(m_boardFrame);
+ m_vlayout->setStretchFactor(m_boardWrapper, 10);
+
+ m_subviews.push_back(m_boardFrame);
+ connectSubviewSignals();
+}
+
+GraphicalBoard::~GraphicalBoard()
+{
+}
+
+void GraphicalBoard::expandToFullWidth()
+{
+ m_boardFrame->expandToSize(m_boardWrapper->size());
+}
+
+void GraphicalBoard::resizeEvent(QResizeEvent * /* event */)
+{
+ QTimer::singleShot(0, this, SLOT(expandToFullWidth()));
+}
+
+///////////////////
+
+GraphicalBoardFrame::GraphicalBoardFrame(QWidget *parent)
+ : View(parent), m_ignoreRack(false), m_alwaysShowVerboseLabels(false), m_boardSize(0, 0), m_sideLength(0)
+{
+ setFrameStyle(QFrame::StyledPanel | QFrame::Raised);
+ setLineWidth(2);
+
+ setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum);
+ setFocusPolicy(Qt::StrongFocus);
+
+ QColor color(PixmapCacher::self()->markColor);
+ QPalette customPalette;
+ customPalette.setColor(QPalette::Light, color.light(s_highlightFactor));
+ customPalette.setColor(QPalette::Mid, color);
+ customPalette.setColor(QPalette::Dark, color.dark(130));
+ setPalette(customPalette);
+
+ PixmapCacher::self()->tileFont = font();
+#if defined(Q_WS_WIN)
+ PixmapCacher::self()->tileFont.setFamily(QString("Arial"));
+#endif // Q_WS_WIN
+
+ //expandToSize(QSize(15 * 25, 15 * 25));
+
+ m_board.prepareEmptyBoard();
+ m_candidate = Quackle::Move::createNonmove();
+ resetArrow();
+}
+
+GraphicalBoardFrame::~GraphicalBoardFrame()
+{
+ PixmapCacher::cleanUp();
+}
+
+void GraphicalBoardFrame::staticDrawPosition(const Quackle::GamePosition &position, const QSize &size, QPixmap *pixmap)
+{
+ GraphicalBoardFrame doozy;
+ doozy.positionChanged(position);
+ doozy.expandToSize(size);
+ doozy.generateBoardPixmap(pixmap);
+}
+
+void GraphicalBoardFrame::positionChanged(const Quackle::GamePosition &position)
+{
+ m_board = position.board();
+ m_rack = position.currentPlayer().rack();
+ m_ignoreRack = !position.currentPlayer().racksAreKnown();
+
+ m_board.updateBritishness();
+
+ resetArrow();
+ m_candidate = position.moveMade();
+
+ prepare();
+}
+
+void GraphicalBoardFrame::prepare()
+{
+ drawBoard(m_board);
+ drawMove(m_candidate);
+
+ if (isOnBoard(m_arrowRoot))
+ drawArrow(m_arrowRoot, m_arrowDirection);
+
+ generateBoardPixmap(&m_pixmap);
+ update();
+}
+
+void GraphicalBoardFrame::expandToSize(const QSize &maxSize)
+{
+ m_maxSize = maxSize;
+
+ // if empty we delay this call
+ if (m_boardSize.isEmpty())
+ return;
+ else
+ {
+ // TODO we get here too often while resizing
+
+ // do calculations in terms of one axis, arbitrarily, width
+ QSize lastCandidate;
+ QSize candidate(0, 0);
+ do
+ {
+ lastCandidate = candidate;
+ candidate += m_boardSize;
+ }
+ while (candidate.width() < m_maxSize.width() && candidate.height() < m_maxSize.height());
+
+ const int maxWidth = lastCandidate.width();
+
+ const double numBlocksWidthwise = ((double)m_boardSize.width() + s_markOtherLengthMultiplier);
+
+ m_sideLength = (int)floor((maxWidth - frameWidth() * 2) / numBlocksWidthwise);
+
+ if (m_sideLength < 0)
+ m_sideLength = 0;
+
+ resizeWidgets(m_sideLength);
+
+ const int shorterMarkWidth = markAt(QSize(0, 0))->size().width();
+ const int shorterMarkHeight = markAt(QSize(0, 0))->size().height();
+ m_tilesOffset.setX(shorterMarkWidth);
+ m_tilesOffset.setY(shorterMarkHeight);
+
+ m_sizeForBoard.setWidth(shorterMarkWidth + m_boardSize.width() * m_sideLength);
+ m_sizeForBoard.setHeight(shorterMarkHeight + m_boardSize.height() * m_sideLength);
+ setMaximumSize(m_sizeForBoard + QSize(frameWidth() * 2, frameWidth() * 2));
+
+ prepare();
+ }
+}
+
+void GraphicalBoardFrame::generateBoardPixmap(QPixmap *pixmap)
+{
+ if (m_sizeForBoard.isEmpty())
+ {
+ *pixmap = QPixmap(0, 0);
+ return;
+ }
+
+ *pixmap = QPixmap(m_sizeForBoard);
+ QPainter painter(pixmap);
+
+ for (QSize currentTile(0, 0); currentTile.height() < m_boardSize.height(); currentTile.setHeight(currentTile.height() + 1))
+ {
+ for (currentTile.setWidth(0); currentTile.width() < m_boardSize.width(); currentTile.setWidth(currentTile.width() + 1))
+ {
+ TileWidget *tile = tileAt(currentTile);
+ if (!tile)
+ continue;
+
+ painter.drawPixmap(coordinatesOfTile(currentTile), tile->tilePixmap());
+ }
+ }
+
+ for (int row = 0; row <= m_boardSize.height(); ++row)
+ {
+ const QSize location(0, row);
+ painter.drawPixmap(coordinatesOfMark(location), markAt(location)->tilePixmap());
+ }
+
+ for (int col = 1; col <= m_boardSize.width(); ++col)
+ {
+ const QSize location(col, 0);
+ painter.drawPixmap(coordinatesOfMark(location), markAt(location)->tilePixmap());
+ }
+}
+
+void GraphicalBoardFrame::drawBoard(const Quackle::Board &board)
+{
+ QSize newBoardSize(board.width(), board.height());
+
+ if (m_boardSize != newBoardSize)
+ {
+ // if it was empty, we need to recalculate
+ // tile widths
+ bool wasEmpty = m_boardSize.isEmpty();
+
+ deleteWidgets();
+ m_boardSize = newBoardSize;
+ recreateWidgets();
+
+ if (wasEmpty)
+ expandToSize(m_maxSize);
+ }
+
+ for (QSize currentTile(0, 0); currentTile.height() < m_boardSize.height(); currentTile.setHeight(currentTile.height() + 1))
+ {
+ for (currentTile.setWidth(0); currentTile.width() < m_boardSize.width(); currentTile.setWidth(currentTile.width() + 1))
+ {
+ Quackle::Board::TileInformation info(board.tileInformation(currentTile.height(), currentTile.width()));
+
+ TileWidget *tile = tileAt(currentTile);
+ if (!tile)
+ continue;
+
+ tile->setInformation(info);
+
+ tile->setArrowDirection(NoArrow);
+ tile->setCemented(info.tileType == Quackle::Board::LetterTile);
+ tile->prepare();
+ }
+ }
+}
+
+TileWidget *GraphicalBoardFrame::tileAt(const QSize &loc)
+{
+ QSize mapEntry(loc + QSize(1, 1));
+ if (!m_tileWidgets.contains(mapEntry))
+ {
+ cerr << "error! graphical board can't find a tile" << endl;
+ return 0;
+ }
+
+ return m_tileWidgets.value(mapEntry);
+}
+
+QSize GraphicalBoardFrame::locationForPosition(const QPoint &pos)
+{
+ QPoint semiRegularized = (pos - QPoint(frameWidth(), frameWidth()) - m_tilesOffset);
+ QSize finalSize((int)floor((double)semiRegularized.x() / m_sideLength), (int)floor((double)semiRegularized.y() / m_sideLength));
+ return finalSize;
+}
+
+TileWidget *GraphicalBoardFrame::markAt(const QSize &loc)
+{
+ if (!m_tileWidgets.contains(loc))
+ {
+ cerr << "error! graphical board can't find a mark" << endl;
+ return 0;
+ }
+
+ return m_tileWidgets.value(loc);
+}
+
+void GraphicalBoardFrame::addTile(const QSize &loc, TileWidget *tile)
+{
+ m_tileWidgets.insert(loc + QSize(1, 1), tile);
+}
+
+void GraphicalBoardFrame::removeTile(const QSize &loc)
+{
+ m_tileWidgets.remove(loc + QSize(1, 1));
+}
+
+void GraphicalBoardFrame::addMark(const QSize &loc, MarkWidget *tile)
+{
+ m_tileWidgets.insert(loc, tile);
+}
+
+void GraphicalBoardFrame::removeMark(const QSize &loc)
+{
+ m_tileWidgets.remove(loc);
+}
+
+QPoint GraphicalBoardFrame::coordinatesOfTile(const QSize &loc)
+{
+ QPoint point(loc.width(), loc.height());
+ return (point * m_sideLength) + m_tilesOffset;
+}
+
+QPoint GraphicalBoardFrame::coordinatesOfMark(const QSize &loc)
+{
+ QPoint point(loc.width(), loc.height());
+
+ if (loc == QSize(0, 0))
+ return point;
+
+ QPoint vector(point.x() == 0? 0 : 1, point.y() == 0? 0 : 1);
+
+ return ((point - vector) * m_sideLength + QPoint(m_tilesOffset.x() * vector.x(), m_tilesOffset.y() * vector.y()));
+}
+
+void GraphicalBoardFrame::drawMove(const Quackle::Move &move)
+{
+ if (move.action == Quackle::Move::Place)
+ {
+ if (move.tiles().empty())
+ return;
+
+ const QSize startTile(move.startcol, move.startrow);
+
+ const Quackle::LetterString::const_iterator end(move.tiles().end());
+ int i = 0;
+ for (Quackle::LetterString::const_iterator it = move.tiles().begin(); it != end; ++it, ++i)
+ {
+ // if this is a cemented letter that we shouldn't overwrite
+ if (move.isAlreadyOnBoard(*it))
+ continue;
+
+ QSize currentTile(startTile);
+ if (move.horizontal)
+ currentTile.setWidth(currentTile.width() + i);
+ else
+ currentTile.setHeight(currentTile.height() + i);
+
+ TileWidget *tileWidget = tileAt(currentTile);
+ if (!tileWidget)
+ continue;
+
+ Quackle::Board::TileInformation info;
+ info.tileType = Quackle::Board::LetterTile;
+
+ info.isBlank = QUACKLE_ALPHABET_PARAMETERS->isBlankLetter(*it);
+ info.letter = QUACKLE_ALPHABET_PARAMETERS->clearBlankness(*it);
+
+ tileWidget->setInformation(info);
+ tileWidget->setCemented(false);
+ tileWidget->prepare();
+ }
+ }
+}
+
+void GraphicalBoardFrame::drawArrow(const QSize &location, int arrowDirection)
+{
+ TileWidget *tile = tileAt(location);
+ if (!tile)
+ return;
+
+ tile->setArrowDirection(arrowDirection);
+ tile->prepare();
+}
+
+void GraphicalBoardFrame::deleteWidgets()
+{
+ if (m_boardSize.isEmpty())
+ return;
+
+ for (QSize currentTile(0, 0); currentTile.height() < m_boardSize.height(); currentTile.setHeight(currentTile.height() + 1))
+ {
+ for (currentTile.setWidth(0); currentTile.width() < m_boardSize.width(); currentTile.setWidth(currentTile.width() + 1))
+ {
+ delete tileAt(currentTile);
+ removeTile(currentTile);
+ }
+ }
+
+ for (int row = 0; row <= m_boardSize.height(); ++row)
+ {
+ delete markAt(QSize(0, row));
+ removeMark(QSize(0, row));
+ }
+
+ for (int col = 1; col <= m_boardSize.width(); ++col)
+ {
+ delete markAt(QSize(col, 0));
+ removeMark(QSize(col, 0));
+ }
+}
+
+void GraphicalBoardFrame::recreateWidgets()
+{
+ Quackle::Board emptyBoard;
+ emptyBoard.prepareEmptyBoard();
+
+ for (QSize currentTile(0, 0); currentTile.height() < m_boardSize.height(); currentTile.setHeight(currentTile.height() + 1))
+ {
+ for (currentTile.setWidth(0); currentTile.width() < m_boardSize.width(); currentTile.setWidth(currentTile.width() + 1))
+ {
+ TileWidget *newTile = new TileWidget;
+
+ newTile->setLocation(currentTile);
+ newTile->setAlwaysShowVerboseLabels(m_alwaysShowVerboseLabels);
+ newTile->setOriginalInformation(emptyBoard.tileInformation(currentTile.height(), currentTile.width()));
+
+ addTile(currentTile, newTile);
+ }
+ }
+
+ for (int row = 0; row <= m_boardSize.height(); ++row)
+ {
+ MarkWidget *newMark = new MarkWidget;
+
+ if (row == 0)
+ newMark->setCapstone();
+ else
+ newMark->setRow(row);
+
+ addMark(QSize(0, row), newMark);
+ }
+
+ for (int col = 1; col <= m_boardSize.width(); ++col)
+ {
+ MarkWidget *newMark = new MarkWidget;
+ newMark->setCol(col);
+ addMark(QSize(col, 0), newMark);
+ }
+}
+
+void GraphicalBoardFrame::resizeWidgets(int sideLength)
+{
+ PixmapCacher::self()->invalidate();
+
+ bool firstTile = true;
+ for (QSize currentTile(0, 0); currentTile.height() < m_boardSize.height(); currentTile.setHeight(currentTile.height() + 1))
+ {
+ for (currentTile.setWidth(0); currentTile.width() < m_boardSize.width(); currentTile.setWidth(currentTile.width() + 1))
+ {
+ TileWidget *tile = tileAt(currentTile);
+ if (!tile)
+ continue;
+
+ tile->setSideLength(sideLength);
+
+ if (firstTile)
+ {
+ emit tileFontChanged(tile->actualLetterFont());
+ firstTile = false;
+ }
+ }
+ }
+
+ for (int row = 0; row <= m_boardSize.height(); ++row)
+ {
+ TileWidget *mark = markAt(QSize(0, row));
+ mark->setSideLength(sideLength);
+ }
+
+ for (int col = 1; col <= m_boardSize.width(); ++col)
+ {
+ TileWidget *mark = markAt(QSize(col, 0));
+ mark->setSideLength(sideLength);
+ }
+}
+
+void GraphicalBoardFrame::flushPixmapsAndRedraw()
+{
+ m_boardSize = QSize();
+ prepare();
+}
+
+void GraphicalBoardFrame::paintEvent(QPaintEvent *event)
+{
+ QPainter painter(this);
+ painter.drawPixmap(contentsRect().topLeft(), m_pixmap);
+ painter.end();
+
+ QFrame::paintEvent(event);
+}
+
+void GraphicalBoardFrame::mousePressEvent(QMouseEvent *event)
+{
+ setFocus();
+
+ if (!wantMousePressEvent(event))
+ {
+ event->ignore();
+ return;
+ }
+
+ const QPoint pos(event->pos());
+
+ QSize location = locationForPosition(pos);
+
+ if (location.isValid())
+ tileClicked(location, event);
+
+ event->accept();
+}
+
+void GraphicalBoardFrame::keyPressEvent(QKeyEvent *event)
+{
+ enum { Backspace = 0, Delete, Submit, Commit, Append } mode;
+
+ switch (event->key())
+ {
+ case Qt::Key_Backspace:
+ mode = Backspace;
+ break;
+
+ case Qt::Key_Delete:
+ mode = Delete;
+ break;
+
+ case Qt::Key_Return:
+ case Qt::Key_Enter:
+ mode = event->modifiers() & Qt::ShiftModifier || event->modifiers() & Qt::ControlModifier || event->modifiers() & Qt::MetaModifier? Commit : Submit;
+ break;
+
+ default:
+ mode = Append;
+ break;
+ }
+
+ if (!hasCandidate())
+ {
+ event->ignore();
+ return;
+ }
+
+ switch (mode)
+ {
+
+ // these modes need a candidate
+ case Submit:
+ submitHandler();
+ break;
+
+ case Commit:
+ commitHandler();
+ break;
+
+ default:
+ if (!hasArrow())
+ {
+ event->ignore();
+ return;
+ }
+
+ switch (mode)
+ {
+
+ // these modes need an arrow
+ case Delete:
+ deleteHandler();
+ break;
+
+ case Backspace:
+ backspaceHandler();
+ break;
+
+ case Append:
+ if (event->modifiers() & Qt::AltModifier || event->modifiers() & Qt::ControlModifier || event->text().isEmpty())
+ {
+ event->ignore();
+ return;
+ }
+
+ appendHandler(event->text(), event->modifiers() & Qt::ShiftModifier);
+ break;
+
+ case Commit:
+ case Submit:
+ break;
+ }
+ break;
+ }
+
+ prepare();
+
+ event->accept();
+}
+
+bool GraphicalBoardFrame::wantMousePressEvent(const QMouseEvent *event) const
+{
+ return (event->button() == Qt::LeftButton);
+}
+
+Quackle::Move GraphicalBoardFrame::flip(const Quackle::Move &flippee)
+{
+ if (!m_board.isConnected(flippee))
+ return flippee;
+
+ Quackle::MoveList words = m_board.allWordsFormedBy(flippee);
+ Quackle::Move flipped = words.front();
+ if (flipped.tiles().length() > 1)
+ {
+ return flipped;
+ }
+ else
+ {
+ return flippee;
+ }
+}
+
+void GraphicalBoardFrame::prettifyAndSetLocalCandidate(const Quackle::Move &candidate)
+{
+ m_candidate = candidate;
+ m_candidate.setPrettyTiles(m_board.prettyTilesOfMove(m_candidate));
+
+ if (candidate.action == Quackle::Move::Place && candidate.wordTilesWithNoPlayThru().empty())
+ {
+ emit statusMessage(tr("Click again to switch arrow orientation, or type a word."));
+ return;
+ }
+
+ //UVcout << "m_candidate: " << m_candidate << endl;
+
+ if (m_candidate.wordTilesWithNoPlayThru().length() == 1)
+ {
+ emit localCandidateChanged(flip(m_candidate));
+
+ emit statusMessage(tr("Press Enter to add %1 to candidate list, or Control+Enter to commit to it immediately.").arg(QuackleIO::Util::moveToDetailedString(flip(m_candidate))));
+ }
+ else
+ {
+ emit localCandidateChanged(m_candidate);
+
+ emit statusMessage(tr("Press Enter to add %1 to candidate list, or Control+Enter to commit to it immediately.").arg(QuackleIO::Util::moveToDetailedString(m_candidate)));
+ }
+}
+
+void GraphicalBoardFrame::setLocalCandidate(const Quackle::Move &candidate)
+{
+ m_candidate = candidate;
+ resetArrow();
+ prepare();
+}
+
+void GraphicalBoardFrame::backspaceHandler()
+{
+ unsigned int hoppedTiles = 0;
+ QSize currentTile(m_arrowRoot);
+ while (true)
+ {
+ const QSize previousTile = currentTile - arrowVector();
+
+ bool stopHere;
+
+ if (!isOnBoard(previousTile))
+ stopHere = true;
+ else
+ {
+ Quackle::Board::TileInformation previousTileInformation(m_board.tileInformation(previousTile.height(), previousTile.width()));
+
+ stopHere = previousTileInformation.tileType != Quackle::Board::LetterTile;
+ }
+
+ ++hoppedTiles;
+
+ if (stopHere)
+ {
+ m_arrowRoot = previousTile;
+ break;
+ }
+
+ currentTile = previousTile;
+ }
+
+ Quackle::LetterString tiles(m_candidate.tiles());
+
+ if (hoppedTiles > tiles.length())
+ tiles = Quackle::LetterString();
+ else
+ tiles = Quackle::String::left(m_candidate.tiles(), m_candidate.tiles().length() - hoppedTiles);
+
+ if (tiles.empty())
+ {
+ Quackle::Move originalMove;
+ Quackle::LetterString hoppedLetters;
+
+ currentTile = m_arrowRoot;
+ while (true)
+ {
+ const QSize previousTile = currentTile - arrowVector();
+
+ bool stopHere;
+
+ if (!isOnBoard(previousTile))
+ stopHere = true;
+ else
+ {
+ Quackle::Board::TileInformation previousTileInformation(m_board.tileInformation(previousTile.height(), previousTile.width()));
+
+ stopHere = previousTileInformation.tileType != Quackle::Board::LetterTile;
+ }
+
+ if (stopHere)
+ {
+ const bool horizontal = m_arrowDirection == ArrowRight;
+ m_candidate = Quackle::Move::createPlaceMove(currentTile.height(), currentTile.width(), horizontal, hoppedLetters);
+ break;
+ }
+
+ hoppedLetters += QUACKLE_PLAYED_THRU_MARK;
+ currentTile = previousTile;
+ }
+ }
+ else
+ {
+ m_candidate.setTiles(tiles);
+ }
+
+ ensureCandidatePlacedProperly();
+ prettifyAndSetLocalCandidate(m_candidate);
+}
+
+void GraphicalBoardFrame::deleteHandler()
+{
+ setLocalCandidate(Quackle::Move::createNonmove());
+}
+
+void GraphicalBoardFrame::submitHandler()
+{
+ QTimer::singleShot(0, this, SLOT(setGlobalCandidate()));
+}
+
+void GraphicalBoardFrame::commitHandler()
+{
+ QTimer::singleShot(0, this, SLOT(setAndCommitGlobalCandidate()));
+}
+
+void GraphicalBoardFrame::setGlobalCandidate()
+{
+ if (m_candidate.action == Quackle::Move::Place && m_candidate.wordTilesWithNoPlayThru().empty())
+ {
+ emit statusMessage(tr("Click again to switch arrow orientation, or type a word."));
+ return;
+ }
+
+ if (m_candidate.wordTilesWithNoPlayThru().length() == 1)
+ {
+ emit setCandidateMove(flip(m_candidate));
+ }
+ else
+ {
+ emit setCandidateMove(m_candidate);
+ }
+}
+
+void GraphicalBoardFrame::setAndCommitGlobalCandidate()
+{
+ setGlobalCandidate();
+ emit commit();
+}
+
+void GraphicalBoardFrame::appendHandler(const QString &text, bool shiftPressed)
+{
+ if (!isOnBoard(m_arrowRoot))
+ return;
+
+ if (!hasCandidate())
+ return;
+
+ Quackle::LetterString appendedLetterString(QuackleIO::Util::encode(text));
+
+ if (appendedLetterString.length() == 0 || Quackle::String::front(appendedLetterString) < QUACKLE_FIRST_LETTER)
+ return;
+
+ if (shiftPressed)
+ appendedLetterString = Quackle::String::setBlankness(appendedLetterString);
+ else
+ appendedLetterString = Quackle::String::clearBlankness(appendedLetterString);
+
+ Quackle::Move newCandidate(m_candidate);
+ Quackle::LetterString newTiles(m_candidate.tiles() + appendedLetterString);
+ if (!m_ignoreRack && !m_rack.contains(Quackle::String::usedTiles(newTiles)))
+ {
+ Quackle::LetterString blankedNewTiles(m_candidate.tiles() + Quackle::String::setBlankness(appendedLetterString));
+
+ if (m_rack.contains(Quackle::String::usedTiles(blankedNewTiles)))
+ {
+ newTiles = blankedNewTiles;
+ }
+ }
+
+ newCandidate.setTiles(newTiles);
+
+ Quackle::LetterString hoppedTiles;
+
+ QSize currentTile(m_arrowRoot);
+ while (true)
+ {
+ const QSize nextTile = currentTile + arrowVector();
+
+ bool stopHere = false;
+ bool resetArrowAfter = false;
+
+ if (!isOnBoard(nextTile))
+ {
+ stopHere = true;
+ }
+ else
+ {
+ Quackle::Board::TileInformation nextTileInformation(m_board.tileInformation(nextTile.height(), nextTile.width()));
+
+ if (nextTileInformation.tileType != Quackle::Board::LetterTile)
+ stopHere = true;
+ }
+
+ if (stopHere)
+ {
+ newCandidate.setTiles(newCandidate.tiles() + hoppedTiles);
+
+ if (resetArrowAfter)
+ resetArrow();
+ else
+ m_arrowRoot = nextTile;
+
+ break;
+ }
+
+ hoppedTiles += QUACKLE_PLAYED_THRU_MARK;
+ currentTile = nextTile;
+ }
+
+ prettifyAndSetLocalCandidate(newCandidate);
+}
+
+void GraphicalBoardFrame::tileClicked(const QSize &tileLocation, const QMouseEvent * /* event */)
+{
+ Quackle::Board::TileInformation info(m_board.tileInformation(tileLocation.height(), tileLocation.width()));
+ if (info.tileType == Quackle::Board::LetterTile)
+ return;
+
+ if (m_arrowRoot == tileLocation)
+ {
+ ++m_arrowDirection;
+ if (m_arrowDirection == ArrowWorm)
+ m_arrowDirection = s_firstArrowDirection;
+ }
+ else
+ m_arrowDirection = s_firstArrowDirection;
+
+ m_arrowRoot = tileLocation;
+
+ Quackle::Move originalMove;
+ Quackle::LetterString hoppedTiles;
+
+ QSize currentTile(tileLocation);
+ while (true)
+ {
+ const QSize previousTile = currentTile - arrowVector();
+
+ bool stopHere;
+
+ if (!isOnBoard(previousTile))
+ stopHere = true;
+ else
+ {
+ Quackle::Board::TileInformation previousTileInformation(m_board.tileInformation(previousTile.height(), previousTile.width()));
+
+ stopHere = previousTileInformation.tileType != Quackle::Board::LetterTile;
+ }
+
+ if (stopHere)
+ {
+ const bool horizontal = m_arrowDirection == ArrowRight;
+ originalMove = Quackle::Move::createPlaceMove(currentTile.height(), currentTile.width(), horizontal, hoppedTiles);
+ break;
+ }
+
+ hoppedTiles += QUACKLE_PLAYED_THRU_MARK;
+ currentTile = previousTile;
+ }
+
+ prettifyAndSetLocalCandidate(originalMove);
+
+ // TODO work some cleverness so we can do this!
+ //emit setCandidateMove(Quackle::Move::createNonmove());
+
+ for (QSize currentTile(0, 0); currentTile.height() < m_boardSize.height(); currentTile.setHeight(currentTile.height() + 1))
+ for (currentTile.setWidth(0); currentTile.width() < m_boardSize.width(); currentTile.setWidth(currentTile.width() + 1))
+ if (currentTile != tileLocation)
+ {
+ TileWidget *tile = tileAt(currentTile);
+ if (!tile)
+ continue;
+
+ tile->setArrowDirection(NoArrow);
+ }
+
+ prepare();
+}
+
+void GraphicalBoardFrame::resetArrow()
+{
+ m_arrowRoot = QSize(-1, -1);
+ m_arrowDirection = NoArrow;
+}
+
+bool GraphicalBoardFrame::hasArrow() const
+{
+ return m_arrowRoot.isValid();
+}
+
+QSize GraphicalBoardFrame::arrowVector() const
+{
+ const bool horizontal = (m_arrowDirection == ArrowRight);
+ return horizontal? QSize(1, 0) : QSize(0, 1);
+}
+
+bool GraphicalBoardFrame::hasCandidate() const
+{
+ return m_candidate.isAMove();
+}
+
+void GraphicalBoardFrame::ensureCandidatePlacedProperly()
+{
+ if (m_candidate.tiles().empty())
+ {
+ m_candidate.startrow = m_arrowRoot.height();
+ m_candidate.startcol = m_arrowRoot.width();
+ }
+}
+
+bool GraphicalBoardFrame::isOnBoard(const QSize &location) const
+{
+ return (location.width() >= 0 && location.width() < m_boardSize.width() && location.height() >= 0 && location.height() < m_boardSize.height());
+}
+
+////////////////
+
+unsigned int qHash(const QColor &color)
+{
+ return color.rgb();
+}
+
+PixmapCacher *PixmapCacher::m_self = 0;
+PixmapCacher *PixmapCacher::self()
+{
+ if (m_self == 0)
+ m_self = new PixmapCacher();
+
+ return m_self;
+}
+
+void PixmapCacher::cleanUp()
+{
+ delete m_self;
+ m_self = 0;
+}
+
+PixmapCacher::PixmapCacher()
+{
+ arrowColor = QColor("black");
+ letterColor = QColor("#6e6e6e");
+ britishLetterColor = QColor("#703d3d");
+
+ // tiles on rack will be of different sizes and thus are slightly
+ // altered to fool the pixmap cacher
+ rackColor = letterColor.light(101);
+
+ DLSColor = QColor("cornflowerblue");
+ TLSColor = QColor("slateblue");
+ DWSColor = QColor("palevioletred");
+ TWSColor = QColor("firebrick");
+
+ QLSColor = QColor("blueviolet").light();
+ QWSColor = QColor("goldenrod");
+
+ nothingColor = QColor("gainsboro");
+
+ cementedLetterTextColor = QColor("#f8f8ff");
+ cementedBritishLetterTextColor = QColor("#FFC0C0");
+ uncementedLetterTextColor = QColor("khaki");
+
+ markColor = QColor("tan");
+ markTextColor = markColor.dark();
+}
+
+bool PixmapCacher::contains(const QColor &color) const
+{
+ return m_pixmaps.contains(color);
+}
+
+QPixmap PixmapCacher::get(const QColor &color) const
+{
+ return m_pixmaps.value(color);
+}
+
+void PixmapCacher::put(const QColor &color, const QPixmap &pixmap)
+{
+ m_pixmaps.insert(color, pixmap);
+}
+
+void PixmapCacher::invalidate()
+{
+ m_pixmaps.clear();
+}
+
+////////////////
+
+TileWidget::TileWidget()
+ : m_cemented(false), m_arrowDirection(GraphicalBoardFrame::NoArrow), m_alwaysShowVerboseLabels(false)
+{
+}
+
+TileWidget::~TileWidget()
+{
+}
+
+void TileWidget::setSideLength(int sideLength)
+{
+ setOurSize(sideLength, sideLength);
+
+ if (!m_pixmap.isNull())
+ prepare();
+}
+
+int TileWidget::sideLength() const
+{
+ return m_size.width();
+}
+
+QSize TileWidget::size() const
+{
+ return m_size;
+}
+
+void TileWidget::setInformation(const Quackle::Board::TileInformation &information)
+{
+ m_information = information;
+}
+
+Quackle::Board::TileInformation TileWidget::information() const
+{
+ return m_information;
+}
+
+void TileWidget::setOriginalInformation(const Quackle::Board::TileInformation &originalInformation)
+{
+ if (originalInformation.tileType == Quackle::Board::BonusSquareTile)
+ m_backgroundColor = tileColor(originalInformation);
+}
+
+void TileWidget::setLocation(const QSize &location)
+{
+ m_location = location;
+}
+
+void TileWidget::setCemented(bool cemented)
+{
+ m_cemented = cemented;
+}
+
+bool TileWidget::cemented() const
+{
+ return m_cemented;
+}
+
+void TileWidget::setArrowDirection(int arrowDirection)
+{
+ m_arrowDirection = arrowDirection;
+}
+
+GraphicalBoardFrame::ArrowDirection TileWidget::arrowDirection() const
+{
+ return (GraphicalBoardFrame::ArrowDirection)m_arrowDirection;
+}
+
+void TileWidget::prepare()
+{
+ m_pixmap = generateTilePixmap();
+}
+
+const QPixmap &TileWidget::tilePixmap()
+{
+ return m_pixmap;
+}
+
+QColor TileWidget::tileColor()
+{
+ return tileColor(m_information);
+}
+
+QColor TileWidget::tileColor(const Quackle::Board::TileInformation &information)
+{
+ QColor ret;
+
+ PixmapCacher *cache = PixmapCacher::self();
+
+ switch (information.tileType)
+ {
+ case Quackle::Board::LetterTile:
+ if (information.isOnRack)
+ ret = cache->rackColor;
+ else if (information.isBritish && QuackerSettings::self()->britishColoring == TileBritishColoring)
+ ret = cache->britishLetterColor;
+ else
+ ret = cache->letterColor;
+ break;
+
+ case Quackle::Board::BonusSquareTile:
+ switch (information.bonusSquareType)
+ {
+ case Quackle::Board::LetterBonus:
+ if (information.bonusMultiplier == 2)
+ ret = cache->DLSColor;
+ else if (information.bonusMultiplier == 3)
+ ret = cache->TLSColor;
+ else if (information.bonusMultiplier == 4)
+ ret = cache->QLSColor;
+ else
+ {
+ // TODO general case
+ }
+ break;
+
+ case Quackle::Board::WordBonus:
+ if (information.bonusMultiplier == 2)
+ ret = cache->DWSColor;
+ else if (information.bonusMultiplier == 3)
+ ret = cache->TWSColor;
+ else if (information.bonusMultiplier == 4)
+ ret = cache->QWSColor;
+ else
+ {
+ // TODO general case
+ }
+ break;
+
+ case Quackle::Board::NoBonus:
+ // urps, this won't happen
+ ret = cache->nothingColor;
+ break;
+ }
+ break;
+
+ case Quackle::Board::NothingTile:
+ ret = cache->nothingColor;
+ break;
+ }
+
+ return ret;
+}
+
+QColor TileWidget::backgroundColor()
+{
+ return m_backgroundColor;
+}
+
+QColor TileWidget::letterTextColor()
+{
+ QColor ret;
+
+ if (m_arrowDirection != GraphicalBoardFrame::NoArrow)
+ ret = PixmapCacher::self()->arrowColor;
+ else if (m_cemented)
+ {
+ if (m_information.isBritish && QuackerSettings::self()->britishColoring == TextBritishColoring)
+ ret = PixmapCacher::self()->cementedBritishLetterTextColor;
+ else
+ ret = PixmapCacher::self()->cementedLetterTextColor;
+ }
+ else if (m_information.letter == QUACKLE_NULL_MARK && m_information.isStartLocation)
+ ret = PixmapCacher::self()->arrowColor;
+ else if (m_information.letter == QUACKLE_NULL_MARK)
+ ret = tileColor().light(145);
+ else
+ ret = PixmapCacher::self()->uncementedLetterTextColor;
+
+ return ret;
+}
+
+QString TileWidget::letterText()
+{
+ int glyphIndex;
+
+ if (m_arrowDirection == GraphicalBoardFrame::ArrowRight)
+ glyphIndex = 0;
+ else if (m_arrowDirection == GraphicalBoardFrame::ArrowDown)
+ glyphIndex = 1;
+ else if (m_information.letter == QUACKLE_NULL_MARK)
+ {
+ if (m_information.isStartLocation)
+ glyphIndex = 2;
+ else
+ {
+ if (shouldShowVerboseLabels())
+ {
+ if (m_information.bonusSquareType == Quackle::Board::LetterBonus)
+ return QString("%1LS").arg(m_information.bonusMultiplier);
+ else if (m_information.bonusSquareType == Quackle::Board::WordBonus)
+ return QString("%1WS").arg(m_information.bonusMultiplier);
+ }
+
+ return QString::null;
+ }
+ }
+ else
+ return QuackleIO::Util::sanitizeUserVisibleLetterString(QuackleIO::Util::letterToQString(m_information.letter));
+
+ QChar preferredGlyphs[] = {QChar(0x21d2), QChar(0x21d3), QChar(0x2606)}; // double arrows, white star
+ QChar secondaryGlyphs[] = {QChar(0x2192), QChar(0x2193), QChar( '*')}; // single arrows, ASCII star
+ QChar asciiGlyphs[] = {QChar( '>'), QChar( 'v'), QChar( '*')}; // 7-bit
+ QFontMetrics metrics(letterFont());
+
+#ifdef FORCE_SECONDARY_ARROW_GLYPHS
+ QChar macSecondaryGlyphs[] = {QChar(0x2192), QChar(0x2193), QChar(0x2606)}; // single arrows, white star
+ return QString(macSecondaryGlyphs[glyphIndex]);
+#else
+ if (metrics.inFont(preferredGlyphs[0]) &&
+ metrics.inFont(preferredGlyphs[1]) &&
+ metrics.inFont(preferredGlyphs[2]))
+ return QString(preferredGlyphs[glyphIndex]);
+ else if (metrics.inFont(secondaryGlyphs[0]) &&
+ metrics.inFont(secondaryGlyphs[1]) &&
+ metrics.inFont(secondaryGlyphs[2]))
+ return QString(secondaryGlyphs[glyphIndex]);
+ else
+ return QString(asciiGlyphs[glyphIndex]);
+#endif
+}
+
+QFont TileWidget::letterFont()
+{
+ // Sorry for the hardcoded constants :( :(
+ if (m_information.isStartLocation && m_information.tileType != Quackle::Board::LetterTile)
+ return scaledFont(0.7);
+ else if (m_arrowDirection == GraphicalBoardFrame::NoArrow && m_information.tileType == Quackle::Board::BonusSquareTile)
+ return scaledFont(.35);
+ else
+ {
+ double initialScale = 0.7;
+ if (m_information.tileType == Quackle::Board::LetterTile)
+ {
+ QString text = letterText();
+ if (text.length() > 1)
+ {
+ initialScale = 1.0 / text.length();
+ }
+
+ }
+
+ double scale = m_information.isBlank? initialScale * 5 / 7 : initialScale;
+
+ QFont ret = scaledFont(scale);
+ ret.setBold(true);
+ return ret;
+ }
+}
+
+QFont TileWidget::actualLetterFont()
+{
+ return scaledFont(1);
+}
+
+QString TileWidget::miniText()
+{
+ if (m_information.letter != QUACKLE_NULL_MARK && QuackerSettings::self()->scoreLabels)
+ {
+ if (m_information.isBlank)
+ return QString::null;
+ else
+ return QString::number(QUACKLE_ALPHABET_PARAMETERS->score(m_information.letter));
+ }
+ else
+ return QString::null;
+}
+
+QColor TileWidget::miniTextColor()
+{
+ return tileColor().light(170);
+}
+
+QFont TileWidget::miniFont()
+{
+ return scaledFont(miniText().length() > 1? .275 : .325);
+}
+
+bool TileWidget::shouldShowVerboseLabels() const
+{
+ return m_alwaysShowVerboseLabels || QuackerSettings::self()->verboseLabels;
+}
+
+QFont TileWidget::scaledFont(float multiplier)
+{
+ const int smallerSideLength = qMin(size().width(), size().height());
+
+ if (smallerSideLength == 0)
+ return PixmapCacher::self()->tileFont;
+
+ QFont ret(PixmapCacher::self()->tileFont);
+ ret.setPixelSize((int)(smallerSideLength * multiplier));
+ return ret;
+}
+
+void TileWidget::setOurSize(const QSize &size)
+{
+ m_size = size;
+}
+
+void TileWidget::setOurSize(int width, int height)
+{
+ setOurSize(QSize(width, height));
+}
+
+QPixmap TileWidget::generateTilePixmap()
+{
+ const QSize currentSize(size());
+
+ if (currentSize.isEmpty())
+ return QPixmap();
+
+ const QRect borderRect(0, 0, currentSize.width(), currentSize.height());
+ const int maxSideLength = qMax(currentSize.width(), currentSize.height());
+ const int borderWidth = 1;
+
+ const QPointF midpoint((double)(currentSize.width() + borderWidth) / 2, (double)(currentSize.height() + borderWidth) / 2);
+ const QColor color(tileColor());
+
+ QPixmap ret(currentSize);
+
+ if (PixmapCacher::self()->contains(color))
+ ret = PixmapCacher::self()->get(color);
+ else
+ {
+ //UVcout << "cache miss for color " << color.rgb() << endl;
+ double radius = maxSideLength / 2 * 1.41 + maxSideLength / 10 + 2;
+
+ // could be used for cool effect -- the color of the bonus square we're obscuring
+ //const QColor outerColor(backgroundColor());
+
+ QRadialGradient gradient(QPointF(radius, radius), radius * 3, QPointF(radius / 3, radius / 3));
+ gradient.setColorAt(0, color.light(GraphicalBoardFrame::s_highlightFactor));
+ gradient.setColorAt(.95, color.dark(GraphicalBoardFrame::s_highlightFactor));
+
+ QPainter painter(&ret);
+ painter.setBrush(gradient);
+
+ painter.drawEllipse((int)(midpoint.x() - radius), (int)(midpoint.y() - radius), (int)(radius * 2), (int)(radius * 2));
+
+ QPalette customPalette;
+ customPalette.setColor(QPalette::Light, color.light(GraphicalBoardFrame::s_highlightFactor));
+ customPalette.setColor(QPalette::Dark, color);
+ customPalette.setColor(QPalette::Mid, color);
+
+ qDrawShadePanel(&painter, borderRect.x(), borderRect.y(), borderRect.width(), borderRect.height(), customPalette, false, borderWidth);
+
+ PixmapCacher::self()->put(color, ret);
+ }
+
+ const QString nanism = miniText();
+ const bool hasNanism = !nanism.isEmpty();
+
+ const QString text = letterText();
+ if (!text.isEmpty())
+ {
+ QPainter painter(&ret);
+ painter.setFont(letterFont());
+ QPen pen(letterTextColor());
+ painter.setPen(pen);
+ painter.setBrush(Qt::NoBrush);
+
+ const QRectF textSize(painter.boundingRect(borderRect, text));
+ const QPointF startPoint(midpoint - textSize.bottomRight() / 2);
+ const QPointF roundedStartPoint(floor(startPoint.x()), floor(startPoint.y()));
+
+ QRectF textRect(textSize);
+ textRect.moveTo(roundedStartPoint);
+
+ painter.drawText(textRect, Qt::TextDontClip, text);
+
+ if (m_information.isBlank)
+ {
+ painter.setBrush(Qt::NoBrush);
+ pen.setWidth(1);
+ painter.setPen(pen);
+
+ const int border = currentSize.width() / 5;
+ painter.drawRect(QRect(border, border, currentSize.width() - 2 * border, currentSize.height() - 2 * border));
+ }
+ }
+
+ if (hasNanism)
+ {
+ QPainter painter(&ret);
+ painter.setFont(miniFont());
+ QPen pen(miniTextColor());
+ painter.setPen(pen);
+ painter.setBrush(Qt::NoBrush);
+
+ const QRectF textSize(painter.boundingRect(borderRect, nanism));
+ const QPointF startPoint((midpoint * (nanism.length() > 1? 1.65 : 1.68)) - textSize.bottomRight() / 2);
+ const QPointF roundedStartPoint(floor(startPoint.x()), floor(startPoint.y()));
+
+ QRectF textRect(textSize);
+ textRect.moveTo(roundedStartPoint);
+
+ painter.drawText(textRect, Qt::TextDontClip, nanism);
+ }
+
+ return ret;
+}
+
+bool operator<(const QSize &firstSize, const QSize &secondSize)
+{
+ return (firstSize.height() + (QUACKLE_MAXIMUM_BOARD_SIZE + 1) * firstSize.width()) < (secondSize.height() + (QUACKLE_MAXIMUM_BOARD_SIZE + 1) * secondSize.width());
+}
+
+//////////////////
+
+MarkWidget::MarkWidget()
+ : m_horizontal(true), m_capstone(false)
+{
+ m_information.tileType = Quackle::Board::LetterTile;
+}
+
+MarkWidget::~MarkWidget()
+{
+}
+
+void MarkWidget::setRow(int row)
+{
+ m_horizontal = false;
+ m_letterText = QString::number(row);
+}
+
+void MarkWidget::setCol(int col)
+{
+ m_horizontal = true;
+ m_letterText = QChar('A' + col - 1);
+}
+
+void MarkWidget::setCapstone()
+{
+ m_capstone = true;
+ m_letterText = QString::null;
+}
+
+void MarkWidget::setSideLength(int sideLength)
+{
+ const int otherLength = (int)(sideLength * GraphicalBoardFrame::s_markOtherLengthMultiplier);
+
+ if (m_capstone)
+ setOurSize(otherLength, otherLength);
+ else if (m_horizontal)
+ setOurSize(sideLength, otherLength);
+ else
+ setOurSize(otherLength, sideLength);
+
+ prepare();
+}
+
+QColor MarkWidget::tileColor()
+{
+ // we slightly alter colors to fool the pixmap cacher!
+ if (m_capstone)
+ return PixmapCacher::self()->markColor.light(101);
+ else if (m_horizontal)
+ return PixmapCacher::self()->markColor;
+ else
+ return PixmapCacher::self()->markColor.dark(101);
+}
+
+QColor MarkWidget::letterTextColor()
+{
+ return PixmapCacher::self()->markTextColor;
+}
+
+QFont MarkWidget::letterFont()
+{
+ return scaledFont(0.7);
+}
+
+QString MarkWidget::letterText()
+{
+ return m_letterText;
+}