File indexing completed on 2024-04-28 04:04:48

0001 /***************************************************************************
0002  *   Copyright 2005-2007 Francesco Rossi <redsh@email.it>                  *
0003  *   Copyright 2006-2007 Mick Kappenburg <ksudoku@kappendburg.net>         *
0004  *   Copyright 2006-2008 Johannes Bergmeier <johannes.bergmeier@gmx.net>   *
0005  *   Copyright 2012      Ian Wadham <iandw.au@gmail.com>                   *
0006  *   Copyright 2015      Ian Wadham <iandw.au@gmail.com>                   *
0007  *                                                                         *
0008  *   This program is free software; you can redistribute it and/or modify  *
0009  *   it under the terms of the GNU General Public License as published by  *
0010  *   the Free Software Foundation; either version 2 of the License, or     *
0011  *   (at your option) any later version.                                   *
0012  *                                                                         *
0013  *   This program is distributed in the hope that it will be useful,       *
0014  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
0015  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
0016  *   GNU General Public License for more details.                          *
0017  *                                                                         *
0018  *   You should have received a copy of the GNU General Public License     *
0019  *   along with this program; if not, write to the                         *
0020  *   Free Software Foundation, Inc.,                                       *
0021  *   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.         *
0022  ***************************************************************************/
0023 
0024 #include "puzzleprinter.h"
0025 #include "globals.h"
0026 
0027 #include <QPrintDialog>
0028 #include <QPainter>
0029 #include <QLine>
0030 
0031 #include <KLocalizedString>
0032 #include <KMessageBox>
0033 
0034 #include "puzzle.h"
0035 #include "skgraph.h"
0036 #include "ksudokugame.h"
0037 
0038 #include "settings.h"
0039 
0040 PuzzlePrinter::PuzzlePrinter(QWidget * parent)
0041     :
0042     QObject(parent),
0043     m_parent(parent),
0044     m_printer(nullptr),
0045     m_p(nullptr),
0046     m_quadrant(0),
0047     m_across(2),
0048     m_down(2)
0049 {
0050 }
0051 
0052 PuzzlePrinter::~PuzzlePrinter()
0053 {
0054 }
0055 
0056 void PuzzlePrinter::print (const ksudoku::Game & game)
0057 {
0058     const ksudoku::Puzzle * puzzle = game.puzzle();
0059     const SKGraph *         graph  = puzzle->graph();
0060     if (graph->sizeZ() > 1) {
0061         KMessageBox::information (m_parent,
0062             i18n("Sorry, cannot print three-dimensional puzzles."));
0063         return;
0064     }
0065     const int leastCellsToFit = 20; // Avoids huge cells in small puzzles.
0066 
0067     // Set up a QPrinter and a QPainter and allocate space on the page.
0068     bool pageFilled = setupOutputDevices (leastCellsToFit, graph->sizeX());
0069     if (! m_printer) {
0070     return;             // The user did not select a printer.
0071     }
0072 
0073     // Draw the puzzle grid and its contents.
0074     bool hasBlocks   = (graph->specificType() != Mathdoku);
0075     bool hasCages    = ((graph->specificType() == Mathdoku) ||
0076                     (graph->specificType() == KillerSudoku));
0077     bool killerStyle = (graph->specificType() == KillerSudoku);
0078 
0079     if (hasBlocks) {            // Only Mathdoku has no blocks.
0080     drawBlocks (puzzle, graph);
0081     }
0082     if (hasCages) {         // Mathdoku and KillerSudoku have cages.
0083     drawCages (puzzle, graph, killerStyle); // KillerSudoku has blocks too.
0084     }
0085     drawValues (game, graph);       // Print starting and filled-in values.
0086 
0087     if (pageFilled) {
0088         endPrint();         // Print immediately.
0089     }
0090     else {
0091         KMessageBox::information (m_parent,
0092             i18n ("The KSudoku setting for printing several puzzles per page "
0093                   "is currently selected.\n\n"
0094                   "Your puzzle will be printed when no more will fit on the "
0095                   "page or when KSudoku terminates."));
0096     }
0097 }
0098 
0099 void PuzzlePrinter::endPrint()
0100 {
0101     if (m_p != nullptr) {
0102         // The current print output goes to the printer when the painter ends.
0103         m_p->end();
0104         delete m_p;
0105         m_p = nullptr;
0106         m_quadrant = 0;
0107         KMessageBox::information (m_parent,
0108             i18n ("KSudoku has sent output to your printer."));
0109     }
0110 }
0111 
0112 bool PuzzlePrinter::setupOutputDevices (int leastCellsToFit, int puzzleWidth)
0113 {
0114     // The printer and painter objects can persist between print requests, so
0115     // (if required) we can print several puzzles per page and defer printing
0116     // until the page is full or KSudoku terminates and the painter ends itself.
0117     // NOTE: Must create painter before using functions like m_printer->width().
0118     if (m_printer == nullptr) {
0119         m_printer = new QPrinter (QPrinter::HighResolution);
0120         auto * dialog = new QPrintDialog(m_printer, m_parent);
0121         dialog->setWindowTitle(i18n("Print Sudoku Puzzle"));
0122         if (dialog->exec() != QDialog::Accepted) {
0123             delete m_printer;
0124             m_printer = nullptr;
0125             return false;
0126         }
0127     }
0128     if (m_p == nullptr) {
0129         m_p = new QPainter (m_printer); // Start a new print job.
0130     }
0131     m_printMulti = Settings::printMulti();
0132 
0133     // Calculate the printed dimensions of the puzzle.
0134     m_printer->setFullPage (false);     // Allow minimal margins.
0135     int pixels  = qMin (m_printer->width(), m_printer->height());
0136     int space   = pixels - (pixels / 20);   // Allow about 2.5% each side.
0137     int pCells  = qMax (leastCellsToFit, puzzleWidth);  // Cells to allocate.
0138     m_sCell     = space / pCells;       // Size of each cell.
0139     int size    = puzzleWidth * m_sCell;    // Size of the whole puzzle.
0140 
0141     // Check if we require more than one puzzle per page and if they would fit.
0142     bool manyUp = m_printMulti && (pixels > (m_across * size));
0143     int margin1 = manyUp ? (pixels - m_across * size) / (m_across + 1)  // > 1.
0144                          : (pixels - size) / 2;             // = 1.
0145     pixels      = qMax (m_printer->width(), m_printer->height());
0146     int margin2 = manyUp ? (pixels - m_down * size) / (m_down + 1)  // > 1.
0147                          : (pixels - size) / 2;             // = 1.
0148 
0149     // Check for landscape vs. portrait mode and set the margins accordingly.
0150     m_topX = (m_printer->width() < m_printer->height())? margin1 : margin2;
0151     m_topY = (m_printer->width() < m_printer->height())? margin2 : margin1;
0152 
0153     // If new puzzle will not fit a quadrant and page is not empty, flush page.
0154     if ((m_quadrant > 0) && (! manyUp)) {
0155         m_printer->newPage();
0156         m_quadrant = 0;
0157     }
0158     m_topX = manyUp ? m_topX + (m_quadrant%m_across) * (m_topX + size) : m_topX;
0159     m_topY = manyUp ? m_topY + (m_quadrant/m_across) * (m_topY + size) : m_topY;
0160     m_quadrant = manyUp ? (m_quadrant + 1) : (m_across * m_down);
0161 
0162     // Set up pen-parameters for the heavy and light line-drawing.
0163     int thin    = m_sCell / 40; // Allow 2.5%.
0164     int thick   = (thin > 0) ? 2 * thin : 2;
0165 
0166     m_light.setColor (QColor("#888888"));
0167     m_light.setWidth (thin);
0168     m_heavy.setColor (QColor(QStringLiteral("black")));
0169     m_heavy.setWidth (thick);
0170     m_heavy.setCapStyle (Qt::RoundCap);
0171     m_dashes.setColor (QColor(QStringLiteral("black")));
0172     m_dashes.setWidth (thin);
0173     m_dashes.setStyle (Qt::DashLine);
0174 
0175     // Return true if the page will be filled up after drawing the puzzle.
0176     return ((! manyUp) || (m_quadrant >= (m_across * m_down)));
0177 }
0178 
0179 void PuzzlePrinter::drawBlocks (const ksudoku::Puzzle * puzzle,
0180                 const SKGraph * graph)
0181 {
0182     QList<int> edges (graph->size(), 0);    // One bitmap per cell.
0183     int order = graph->order();
0184 
0185     for (int n = 0; n < graph->cliqueCount(); n++) {
0186         // Find out which groups are blocks of cells, not rows or columns.
0187         QList<int> clique = graph->clique (n);
0188         int x = graph->cellPosX (clique.at (0));
0189         int y = graph->cellPosY (clique.at (0));
0190         bool isRow = true;
0191         bool isCol = true;
0192         for (int k = 1; k < order; k++) {
0193             if (graph->cellPosX (clique.at (k)) != x) isRow = false;
0194             if (graph->cellPosY (clique.at (k)) != y) isCol = false;
0195         }
0196         if (isRow || isCol) continue;   // Skip rows and columns.
0197 
0198         // Mark the outside edges of each block.
0199     markEdges (clique, puzzle, graph, edges);
0200     }
0201 
0202     // Draw each cell in the puzzle.
0203     for (int n = 0; n < graph->size(); n++) {
0204         if (puzzle->value (n) < 0) {
0205             continue;               // Do not draw unused cells.
0206     }
0207     drawCell (graph->cellPosX (n), graph->cellPosY (n), edges.at (n));
0208     }
0209 }
0210 
0211 void PuzzlePrinter::drawCages (const ksudoku::Puzzle * puzzle,
0212                    const SKGraph * graph, bool killerStyle)
0213 {
0214     QList<int> edges (graph->size(), 0);    // One bitmap per cell.
0215     for (int n = 0; n < graph->cageCount(); n++) {
0216         // Mark the outside edges of each cage.
0217     markEdges (graph->cage (n), puzzle, graph, edges);
0218     }
0219     if (killerStyle) {
0220     drawKillerSudokuCages (graph, edges);
0221     }
0222     else {
0223     // Draw each cell in the puzzle.
0224     for (int n = 0; n < graph->size(); n++) {
0225         if (puzzle->value (n) < 0) {
0226         continue;           // Do not draw unused cells.
0227         }
0228         drawCell (graph->cellPosX (n), graph->cellPosY (n), edges.at (n));
0229     }
0230     }
0231     for (int n = 0; n < graph->cageCount(); n++) {
0232     drawCageLabel (graph, n, killerStyle);
0233     }
0234 }
0235 
0236 void PuzzlePrinter::markEdges (const QList<int> & cells,
0237                    const ksudoku::Puzzle * puzzle,
0238                    const SKGraph * graph, QList<int> & edges)
0239 {
0240     const int All = (1 << Left) + (1 << Right) + (1 << Above) + (1 << Below);
0241 
0242     int nCells = cells.count();
0243     for (int k = 0; k < nCells; k++) {
0244     int cell = cells.at (k);
0245     int x = graph->cellPosX (cell);
0246     int y = graph->cellPosY (cell);
0247     int nb[4] = {-1, -1, -1, -1};
0248     int lim = graph->sizeX() - 1;
0249 
0250     // Start with all edges: remove them as neighbours are found.
0251     int edge = All;
0252     nb[Left]  = (x > 0)   ? graph->cellIndex (x-1, y) : -1;
0253     nb[Right] = (x < lim) ? graph->cellIndex (x+1, y) : -1;
0254     nb[Above] = (y > 0)   ? graph->cellIndex (x, y-1) : -1;
0255     nb[Below] = (y < lim) ? graph->cellIndex (x, y+1) : -1;
0256     for (int neighbour = 0; neighbour < 4; neighbour++) {
0257         if ((nb[neighbour] < 0) || (puzzle->value(nb[neighbour]) < 0)) {
0258         continue;       // No neighbour on this side.
0259         }
0260         for (int cl = 0; cl < nCells; cl++) {
0261         if (cells.at (cl) == nb[neighbour]) {
0262             edge = edge - (1 << neighbour);
0263         }
0264         }
0265     }
0266     // Check for size-1 cages or detached cells as in XSudoku diagonals.
0267     if ((edge == All) && (graph->specificType() != Mathdoku) &&
0268         (graph->specificType() != KillerSudoku)) {
0269         edge = (1 << Detached);
0270     }
0271     edges [cell] |= edge;   // Merge the edges found for this cell.
0272     }
0273 }
0274 
0275 void PuzzlePrinter::drawCell (int posX, int posY, int edge)
0276 {
0277     int x = m_topX + m_sCell * posX;
0278     int y = m_topY + m_sCell * posY;
0279     QRect rect (x, y, m_sCell, m_sCell);
0280     if (edge & (1 << Detached)) {       // Shade a cell, as in XSudoku.
0281     m_p->fillRect (rect, QColor ("#DDDDDD"));
0282     }
0283     m_p->setPen (m_light);          // First draw every cell light.
0284     m_p->drawRect (rect);
0285     m_p->setPen (m_heavy);          // Draw block boundaries heavy.
0286     if (edge & (1<<Left))  m_p->drawLine (x, y, x, y + m_sCell);
0287     if (edge & (1<<Right)) m_p->drawLine (x+m_sCell, y, x+m_sCell, y+m_sCell);
0288     if (edge & (1<<Above)) m_p->drawLine (x, y, x+m_sCell, y);
0289     if (edge & (1<<Below)) m_p->drawLine (x, y+m_sCell, x+m_sCell, y+m_sCell);
0290 }
0291 
0292 void PuzzlePrinter::drawValues (const ksudoku::Game& game, const SKGraph* graph)
0293 {
0294     const QString labels = (graph->base() <= 3) ? QStringLiteral("123456789") :
0295                                QStringLiteral("ABCDEFGHIJKLMNOPQRSTUVWXY");
0296     // Set font size 60% height of cell.
0297     QFont f = m_p->font();
0298     f.setPixelSize ((m_sCell * 6) / 10);
0299     m_p->setFont (f);
0300 
0301     for (int n = 0; n < graph->size(); n++) {
0302         int v = game.value (n) - 1;
0303     if (v < 0) {
0304         continue;                           // Skip empty or unused cells.
0305     }
0306 
0307         // Draw original puzzle values heavy: filled-in/solution values light.
0308     int x = m_topX + m_sCell * graph->cellPosX (n);
0309     int y = m_topY + m_sCell * graph->cellPosY (n);
0310     QRect rect (x, y, m_sCell, m_sCell);
0311     m_p->setPen (game.given (n) ? m_heavy : m_light);
0312     m_p->drawText (rect, Qt::AlignCenter, labels.mid (v, 1));
0313     }
0314 }
0315 
0316 void PuzzlePrinter::drawKillerSudokuCages (const SKGraph* graph,
0317                                            const QList<int> & edges)
0318 {
0319     // Killer Sudokus have cages AND groups: so the cages are drawn differently.
0320     // We keep the outer wall of the cage on our left and draw a dashed line
0321     // just inside that boundary.  This reduces ugly criss-crossing of lines.
0322     //
0323     // Directions and related arrays are all in clockwise order.
0324     enum  Direction {East, South, West, North, nDirections};
0325     const Direction rightTurn [nDirections] = {South, West, North, East};
0326     const Direction leftTurn [nDirections]  = {North, East, South, West};
0327     const int       wallOnLeft [nDirections] =
0328                 {1 << Above, 1 << Right, 1 << Below, 1 << Left};
0329     const int       wallAhead [nDirections] =
0330                 {1 << Right, 1 << Below, 1 << Left, 1 << Above};
0331 
0332     const int       deltaX  [nDirections] = {+1, 0, -1, 0};
0333     const int       deltaY  [nDirections] = {0, +1, 0, -1};
0334 
0335     int   cellInc [nDirections] = {graph->order(), +1, -graph->order(), -1};
0336     int   offset    = (m_sCell + 6) / 12;
0337     int   longSide  = m_sCell;
0338     int   shortSide = m_sCell - offset - offset;
0339 
0340     m_p->setPen (m_dashes);
0341 
0342     for (int n = 0; n < graph->cageCount(); n++) {
0343     int topLeft = graph->cageTopLeft(n);
0344     int cell    = topLeft;
0345     int edge    = edges.at (cell);
0346     int startX  = m_topX + m_sCell * graph->cellPosX (cell) + offset;
0347     int startY  = m_topY + m_sCell * graph->cellPosY (cell) + offset;
0348     int dx      = 0;
0349     int dy      = 0;
0350     QLine line (startX, startY, startX, startY);
0351     Direction direction = East;
0352 
0353     // Keep drawing until we get back to the starting cell and direction.
0354     do {
0355         // If there is a wall on the left, follow it.
0356         if (edge & wallOnLeft [direction]) {
0357         if (edge & wallAhead [direction]) {
0358             // Go to wall (shortSide), draw line, turn right, new line.
0359             dx = deltaX [direction] * shortSide;
0360             dy = deltaY [direction] * shortSide;
0361             line.setLine (line.x1(), line.y1(),
0362                   line.x2() + dx, line.y2() + dy);
0363             m_p->drawLine (line);
0364             direction = rightTurn [direction];
0365             line.setLine (line.x2(), line.y2(), line.x2(), line.y2());
0366         }
0367         else {
0368             // Extend to start of next cell (longSide).
0369             dx = deltaX [direction] * longSide;
0370             dy = deltaY [direction] * longSide;
0371             line.setLine (line.x1(), line.y1(),
0372                   line.x2() + dx, line.y2() + dy);
0373             cell = cell + cellInc [direction];
0374             edge = edges.at (cell);
0375         }
0376         }
0377         // Else, if there is no wall on the left ...
0378         else {
0379         // Draw line, turn left, new line, go to start of next cell.
0380         m_p->drawLine (line);
0381         direction = leftTurn [direction];
0382         dx = deltaX [direction] * (longSide - shortSide);
0383         dy = deltaY [direction] * (longSide - shortSide);
0384         line.setLine (line.x2(), line.y2(),
0385                   line.x2() + dx, line.y2() + dy);
0386         cell = cell + cellInc [direction];
0387         edge = edges.at (cell);
0388 
0389         }
0390     } while (! ((cell == topLeft) && (direction == East)));
0391     }   // Draw next cage.
0392 }
0393 
0394 void PuzzlePrinter::drawCageLabel (const SKGraph* graph, int n,
0395                    bool killerStyle)
0396 {
0397     if (graph->cage (n).size() < 2) {
0398     return;
0399     }
0400 
0401     int topLeft = graph->cageTopLeft (n);
0402     int cellX = m_topX + m_sCell * graph->cellPosX (topLeft);
0403     int cellY = m_topY + m_sCell * graph->cellPosY (topLeft);
0404 
0405     QString cLabel = QString::number (graph->cageValue (n));
0406     if (! killerStyle) {    // No operator is shown in KillerSudoku.
0407     cLabel = cLabel + QStringLiteral(" /-x+").mid(graph->cageOperator (n), 1);
0408     }
0409 
0410     QFont f = m_p->font();
0411     f.setPixelSize ((m_sCell + 3)/4);
0412     f.setBold (true);
0413     m_p->setFont(f);
0414     QFontMetrics fm(f);
0415     int w = fm.boundingRect(cLabel).width();
0416     int a = fm.ascent();
0417     int m = (fm.boundingRect(QLatin1Char('1')).width()+1)/3;    // Left margin = 1/3 width of '1'.
0418 
0419     if (killerStyle) {
0420     // Cover part of the dashed line, to make a background for the text.
0421     m_p->fillRect(cellX + m, cellY + m, w + w/10, a /* h */, Qt::white);
0422     }
0423     // Note: Origin of text is on baseline to left of first character.
0424     m_p->drawText (cellX + m, cellY + a, cLabel);
0425 }
0426 
0427 #include "moc_puzzleprinter.cpp"