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"