SudokuPanel.java :  » Game » HoDoKu-2-0-1 » sudoku » Java Open Source

Java Open Source » Game » HoDoKu 2 0 1 
HoDoKu 2 0 1 » sudoku » SudokuPanel.java
/*
 * Copyright (C) 2008/09/10  Bernhard Hobiger
 *
 * This file is part of HoDoKu.
 *
 * HoDoKu 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 3 of the License, or
 * (at your option) any later version.
 *
 * HoDoKu 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 HoDoKu. If not, see <http://www.gnu.org/licenses/>.
 */

package sudoku;

import java.awt.AlphaComposite;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Cursor;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Polygon;
import java.awt.RenderingHints;
import java.awt.Stroke;
import java.awt.Toolkit;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.awt.geom.CubicCurve2D;
import java.awt.image.BufferedImage;
import java.awt.print.PageFormat;
import java.awt.print.Printable;
import java.awt.print.PrinterException;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.ResourceBundle;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.Stack;
import java.util.TreeMap;
import java.util.TreeSet;
import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.metadata.IIOInvalidTreeException;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.metadata.IIOMetadataNode;
import javax.imageio.stream.FileImageOutputStream;
import javax.swing.ImageIcon;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import org.w3c.dom.Node;

/**
 *
 * @author  Bernhard Hobiger
 */
public class SudokuPanel extends javax.swing.JPanel implements Printable {
    // Konstante
    private static final int DELTA = 5; // Abstand zwischen den Quadraten in Pixel
    private static final int DELTA_RAND = 5; // Abstand zu den Rndern
    // Konfigurationseigenschaften
    private boolean showCandidates = Options.getInstance().isShowCandidates(); // Alle mglichen Kandidaten anzeigen
    private int candidateMode = SudokuCell.USER; // welche Kandidaten sollen gesetzt werden
    private boolean showWrongValues = Options.getInstance().isShowWrongValues();    // falsche Werte mit anderer Farbe
    private boolean showDeviations = Options.getInstance().isShowDeviations();  // Werte und Kandidaten, die von der Lsung abweichen
    private boolean invalidCells = Options.getInstance().invalidCells; // true: ungltige Zellen, false: mgliche Zellen
    private boolean showInvalidOrPossibleCells = false;  // Ungltige/Mgliche Zellen fr showHintCellValue mit anderem Hintergrund
    private int showHintCellValue = 0;
    private boolean showAllCandidatesAkt = false; // bei alle Kandidaten anzeigen (nur aktive Zelle)
    private boolean showAllCandidates = false; // bei alle Kandidaten anzeigen (alle Zellen)
    private int delta = DELTA; // Zwischenraum zwischen Blcken
    private int deltaRand = DELTA_RAND; // Zwischenraum zu den Rndern
    private Font valueFont;    // Font fr die Zellenwerte
    private Font candidateFont; // Font fr die Kandidaten
    // interne Variable
    private Sudoku sudoku; // Daten fr das Sudoku
    private Sudoku solvedSudoku; // Lsung fr Anzeige von Fehlern
    private SudokuSolver solver; // Lsung fr das Sudoku
    private SudokuCreator creator; // Lsung mit BruteForce (Dancing Links)
    private MainFrame mainFrame; // fr Oberflche
    private CellZoomPanel cellZoomPanel; // active cell display and color chooser
    private SolutionStep step;   // fr Anzeige der Hinweise
    private int chainIndex = -1; // if != -1, only the chain with the right index is shown
    private List<Integer> alsToShow = new ArrayList<Integer>(); // if chainIndex is != -1, alsToShow contains the indices of the ALS, that are part of the chain
    private int oldWidth; // Breite des Panels, als das letzte Mal Fonts erzeugt wurden
    private int width;    // Breite des Panels, auf Quadrat normiert
    private int height;   // Hhe des Panels, auf Quadrat normiert
    private int cellSize; // Kantenlnge einer Zelle
    private int startSX;  // x-Koordinate des linken oberen Punktes des Sudoku
    private int startSY;  // y-Koordinate des linken oberen Punktes des Sudoku
    private Graphics2D g2; // zum Zeichnen, spart eine Menge Parameter
    private CubicCurve2D.Double cubicCurve = new CubicCurve2D.Double(); // fr Chain-Pfeile
    private Polygon arrow = new Polygon(); // Pfeilspitze
    private Stroke arrowStroke = new BasicStroke(1.5f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND); // Pfeilspitzen abrunden
    private Stroke strongLinkStroke = new BasicStroke(1.5f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND); // strong links durchziehen
    private Stroke weakLinkStroke = new BasicStroke(1.5f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND,
            10.0f, new float[]{5.0f}, 0.0f); // weak links punkten
    private List<Point> points = new ArrayList<Point>(200);
    private double arrowLengthFactor = 1.0 / 6.0;
    private double arrowHeightFactor = 1.0 / 3.0;
    private int aktLine = 4; // aktuell markiertes Feld (Zeile)
    private int aktCol = 4;  // aktuell markiertes Feld (Spalte)
    private int shiftLine = -1; // second cell for creating regions with the keyboard (shift pressed)
    private int shiftCol = -1; // second cell for creating regions with the keyboard (shift pressed)

    // Undo/Redo
    private Stack<Sudoku> undoStack = new Stack<Sudoku>();
    private Stack<Sudoku> redoStack = new Stack<Sudoku>();
    // coloring: contains cell index + index in coloringColors[]
    private SortedMap<Integer, Integer> coloringMap = new TreeMap<Integer, Integer>();
    // coloring canddiates: contains cell index * 10 + candidate + index in coloringColors[]
    private SortedMap<Integer, Integer> coloringCandidateMap = new TreeMap<Integer, Integer>();
    // indicates wether coloring is active (-1 means "not active"
    private int aktColorIndex = -1;
    // coloring is meant for cells or candidates
    private boolean colorCells = true;
    // Cursor for coloring: shows the strong color
    private Cursor colorCursor = null;
    // Cursor for coloring: shows the weak color
    private Cursor colorCursorShift = null;
    // old cursor for reset
    private Cursor oldCursor = null;
    // if more than one cell is selected, the indices of all selected cells are stored here
    private SortedSet<Integer> selectedCells = new TreeSet<Integer>();
    // Array containing all "Make x" menu items from the popup menu
    private JMenuItem[] makeItems = null;
    // Array containing all "Exclude x" menu items from the popup menu
    private JMenuItem[] excludeItems = null;
    // Array containing all "Toggle color x" menu items from the popup menu
    private JMenuItem[] toggleColorItems = null;

    /** Creates new form SudokuPanel */
    public SudokuPanel(MainFrame mf) {
        mainFrame = mf;
        sudoku = new Sudoku();
        sudoku.resetCandidates();
        setShowCandidates(Options.getInstance().isShowCandidates());
        creator = new SudokuCreator();
        solver = SudokuSolver.getInstance();
        solver.setSudoku(sudoku.clone());
        solver.solve();

        initComponents();

        makeItems = new JMenuItem[] { 
            make1MenuItem, make2MenuItem, make3MenuItem, make4MenuItem, make5MenuItem,
            make6MenuItem, make7MenuItem, make8MenuItem, make9MenuItem
        };
        excludeItems = new JMenuItem[] {
            exclude1MenuItem, exclude2MenuItem, exclude3MenuItem,
            exclude4MenuItem, exclude5MenuItem, exclude6MenuItem,
            exclude7MenuItem, exclude8MenuItem, exclude9MenuItem
        };
        toggleColorItems = new JMenuItem[] {
            color1aMenuItem, color1bMenuItem, color2aMenuItem, color2bMenuItem,
            color3aMenuItem, color3bMenuItem, color4aMenuItem, color4bMenuItem,
            color5aMenuItem, color5bMenuItem
        };
        setColorIconsInPopupMenu();
        updateCellZoomPanel();
    }

    /** This method is called from within the constructor to
     * initialize the form.
     * WARNING: Do NOT modify this code. The content of this method is
     * always regenerated by the Form Editor.
     */
    // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
    private void initComponents() {

        cellPopupMenu = new javax.swing.JPopupMenu();
        make1MenuItem = new javax.swing.JMenuItem();
        make2MenuItem = new javax.swing.JMenuItem();
        make3MenuItem = new javax.swing.JMenuItem();
        make4MenuItem = new javax.swing.JMenuItem();
        make5MenuItem = new javax.swing.JMenuItem();
        make6MenuItem = new javax.swing.JMenuItem();
        make7MenuItem = new javax.swing.JMenuItem();
        make8MenuItem = new javax.swing.JMenuItem();
        make9MenuItem = new javax.swing.JMenuItem();
        jSeparator1 = new javax.swing.JSeparator();
        exclude1MenuItem = new javax.swing.JMenuItem();
        exclude2MenuItem = new javax.swing.JMenuItem();
        exclude3MenuItem = new javax.swing.JMenuItem();
        exclude4MenuItem = new javax.swing.JMenuItem();
        exclude5MenuItem = new javax.swing.JMenuItem();
        exclude6MenuItem = new javax.swing.JMenuItem();
        exclude7MenuItem = new javax.swing.JMenuItem();
        exclude8MenuItem = new javax.swing.JMenuItem();
        exclude9MenuItem = new javax.swing.JMenuItem();
        excludeSeveralMenuItem = new javax.swing.JMenuItem();
        jSeparator2 = new javax.swing.JSeparator();
        color1aMenuItem = new javax.swing.JMenuItem();
        color1bMenuItem = new javax.swing.JMenuItem();
        color2aMenuItem = new javax.swing.JMenuItem();
        color2bMenuItem = new javax.swing.JMenuItem();
        color3aMenuItem = new javax.swing.JMenuItem();
        color3bMenuItem = new javax.swing.JMenuItem();
        color4aMenuItem = new javax.swing.JMenuItem();
        color4bMenuItem = new javax.swing.JMenuItem();
        color5aMenuItem = new javax.swing.JMenuItem();
        color5bMenuItem = new javax.swing.JMenuItem();

        java.util.ResourceBundle bundle = java.util.ResourceBundle.getBundle("intl/SudokuPanel"); // NOI18N
        make1MenuItem.setText(bundle.getString("SudokuPanel.popup.make1")); // NOI18N
        make1MenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                make1MenuItemActionPerformed(evt);
            }
        });
        cellPopupMenu.add(make1MenuItem);

        make2MenuItem.setText(bundle.getString("SudokuPanel.popup.make2")); // NOI18N
        make2MenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                make1MenuItemActionPerformed(evt);
            }
        });
        cellPopupMenu.add(make2MenuItem);

        make3MenuItem.setText(bundle.getString("SudokuPanel.popup.make3")); // NOI18N
        make3MenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                make1MenuItemActionPerformed(evt);
            }
        });
        cellPopupMenu.add(make3MenuItem);

        make4MenuItem.setText(bundle.getString("SudokuPanel.popup.make4")); // NOI18N
        make4MenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                make1MenuItemActionPerformed(evt);
            }
        });
        cellPopupMenu.add(make4MenuItem);

        make5MenuItem.setText(bundle.getString("SudokuPanel.popup.make5")); // NOI18N
        make5MenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                make1MenuItemActionPerformed(evt);
            }
        });
        cellPopupMenu.add(make5MenuItem);

        make6MenuItem.setText(bundle.getString("SudokuPanel.popup.make6")); // NOI18N
        make6MenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                make1MenuItemActionPerformed(evt);
            }
        });
        cellPopupMenu.add(make6MenuItem);

        make7MenuItem.setText(bundle.getString("SudokuPanel.popup.make7")); // NOI18N
        make7MenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                make1MenuItemActionPerformed(evt);
            }
        });
        cellPopupMenu.add(make7MenuItem);

        make8MenuItem.setText(bundle.getString("SudokuPanel.popup.make8")); // NOI18N
        make8MenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                make1MenuItemActionPerformed(evt);
            }
        });
        cellPopupMenu.add(make8MenuItem);

        make9MenuItem.setText(bundle.getString("SudokuPanel.popup.make9")); // NOI18N
        make9MenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                make1MenuItemActionPerformed(evt);
            }
        });
        cellPopupMenu.add(make9MenuItem);
        cellPopupMenu.add(jSeparator1);

        exclude1MenuItem.setText(bundle.getString("SudokuPanel.popup.exclude1")); // NOI18N
        exclude1MenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                exclude1MenuItemActionPerformed(evt);
            }
        });
        cellPopupMenu.add(exclude1MenuItem);

        exclude2MenuItem.setText(bundle.getString("SudokuPanel.popup.exclude2")); // NOI18N
        exclude2MenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                exclude1MenuItemActionPerformed(evt);
            }
        });
        cellPopupMenu.add(exclude2MenuItem);

        exclude3MenuItem.setText(bundle.getString("SudokuPanel.popup.exclude3")); // NOI18N
        exclude3MenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                exclude1MenuItemActionPerformed(evt);
            }
        });
        cellPopupMenu.add(exclude3MenuItem);

        exclude4MenuItem.setText(bundle.getString("SudokuPanel.popup.exclude4")); // NOI18N
        exclude4MenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                exclude1MenuItemActionPerformed(evt);
            }
        });
        cellPopupMenu.add(exclude4MenuItem);

        exclude5MenuItem.setText(bundle.getString("SudokuPanel.popup.exclude5")); // NOI18N
        exclude5MenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                exclude1MenuItemActionPerformed(evt);
            }
        });
        cellPopupMenu.add(exclude5MenuItem);

        exclude6MenuItem.setText(bundle.getString("SudokuPanel.popup.exclude6")); // NOI18N
        exclude6MenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                exclude1MenuItemActionPerformed(evt);
            }
        });
        cellPopupMenu.add(exclude6MenuItem);

        exclude7MenuItem.setText(bundle.getString("SudokuPanel.popup.exclude7")); // NOI18N
        exclude7MenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                exclude1MenuItemActionPerformed(evt);
            }
        });
        cellPopupMenu.add(exclude7MenuItem);

        exclude8MenuItem.setText(bundle.getString("SudokuPanel.popup.exclude8")); // NOI18N
        exclude8MenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                exclude1MenuItemActionPerformed(evt);
            }
        });
        cellPopupMenu.add(exclude8MenuItem);

        exclude9MenuItem.setText(bundle.getString("SudokuPanel.popup.exclude9")); // NOI18N
        exclude9MenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                exclude1MenuItemActionPerformed(evt);
            }
        });
        cellPopupMenu.add(exclude9MenuItem);

        excludeSeveralMenuItem.setText(bundle.getString("SudokuPanel.popup.several")); // NOI18N
        excludeSeveralMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                excludeSeveralMenuItemActionPerformed(evt);
            }
        });
        cellPopupMenu.add(excludeSeveralMenuItem);
        cellPopupMenu.add(jSeparator2);

        color1aMenuItem.setText(bundle.getString("SudokuPanel.popup.color1a")); // NOI18N
        color1aMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                color1aMenuItemActionPerformed(evt);
            }
        });
        cellPopupMenu.add(color1aMenuItem);

        color1bMenuItem.setText(bundle.getString("SudokuPanel.popup.color1b")); // NOI18N
        color1bMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                color1aMenuItemActionPerformed(evt);
            }
        });
        cellPopupMenu.add(color1bMenuItem);

        color2aMenuItem.setText(bundle.getString("SudokuPanel.popup.color2a")); // NOI18N
        color2aMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                color1aMenuItemActionPerformed(evt);
            }
        });
        cellPopupMenu.add(color2aMenuItem);

        color2bMenuItem.setText(bundle.getString("SudokuPanel.popup.color2b")); // NOI18N
        color2bMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                color1aMenuItemActionPerformed(evt);
            }
        });
        cellPopupMenu.add(color2bMenuItem);

        color3aMenuItem.setText(bundle.getString("SudokuPanel.popup.color3a")); // NOI18N
        color3aMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                color1aMenuItemActionPerformed(evt);
            }
        });
        cellPopupMenu.add(color3aMenuItem);

        color3bMenuItem.setText(bundle.getString("SudokuPanel.popup.color3b")); // NOI18N
        color3bMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                color1aMenuItemActionPerformed(evt);
            }
        });
        cellPopupMenu.add(color3bMenuItem);

        color4aMenuItem.setText(bundle.getString("SudokuPanel.popup.color4a")); // NOI18N
        color4aMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                color1aMenuItemActionPerformed(evt);
            }
        });
        cellPopupMenu.add(color4aMenuItem);

        color4bMenuItem.setText(bundle.getString("SudokuPanel.popup.color4b")); // NOI18N
        color4bMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                color1aMenuItemActionPerformed(evt);
            }
        });
        cellPopupMenu.add(color4bMenuItem);

        color5aMenuItem.setText(bundle.getString("SudokuPanel.popup.color5a")); // NOI18N
        color5aMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                color1aMenuItemActionPerformed(evt);
            }
        });
        cellPopupMenu.add(color5aMenuItem);

        color5bMenuItem.setText(bundle.getString("SudokuPanel.popup.color5b")); // NOI18N
        color5bMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                color1aMenuItemActionPerformed(evt);
            }
        });
        cellPopupMenu.add(color5bMenuItem);

        setBackground(new java.awt.Color(255, 255, 255));
        setMinimumSize(new java.awt.Dimension(300, 300));
        setPreferredSize(new java.awt.Dimension(600, 600));
        addMouseListener(new java.awt.event.MouseAdapter() {
            public void mouseClicked(java.awt.event.MouseEvent evt) {
                formMouseClicked(evt);
            }
        });
        addKeyListener(new java.awt.event.KeyAdapter() {
            public void keyPressed(java.awt.event.KeyEvent evt) {
                formKeyPressed(evt);
            }
            public void keyReleased(java.awt.event.KeyEvent evt) {
                formKeyReleased(evt);
            }
        });

        javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this);
        this.setLayout(layout);
        layout.setHorizontalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGap(0, 600, Short.MAX_VALUE)
        );
        layout.setVerticalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGap(0, 600, Short.MAX_VALUE)
        );
    }// </editor-fold>//GEN-END:initComponents

    private void formKeyReleased(java.awt.event.KeyEvent evt) {//GEN-FIRST:event_formKeyReleased
        handleKeysReleased(evt);
        updateCellZoomPanel();
        mainFrame.fixFocus();
    }//GEN-LAST:event_formKeyReleased

    private void formKeyPressed(java.awt.event.KeyEvent evt) {//GEN-FIRST:event_formKeyPressed
        int keyCode = evt.getKeyCode();
        switch (keyCode) {
            case KeyEvent.VK_ESCAPE:
                mainFrame.coloringPanelClicked(-1);
                clearRegion();
                break;
            default:
                handleKeys(evt);
        }
        updateCellZoomPanel();
        mainFrame.fixFocus();
    }//GEN-LAST:event_formKeyPressed

    /**
     * New mouse control for version 2.0:
     * <ul>
     * <li>clicking a cell sets the cursor to the cell (not in coloring mode)</li>
     * <li>holding shift or ctrl down while clicking selects a region of cells</li>
     * <li>double clicking a cell with only one candidate left sets that candidate in the cell</li>
     * <li>double clicking a cell containing a Hidden Single sets that cell if filters are applied for
     *     the candidate</li>
     * <li>double clicking a candidate with ctrl pressed toggles the candidate</li>
     * <li>right click on a cell activates the context menu</li>
     * </ul>
     * If {@link #aktColorIndex} is set (ne -1), coloring mode is in effect and the mouse
     * behaviour changes completely (whether a cell or a candidate should be colored
     * is decided by {@link #colorCells}):
     * <ul>
     * <li>left click on a cell/candidate toggles the color on the cell/candidate</li>
     * <li>left click on a cell/candidate with shift pressed toggles the alternate color on the cell/candidate</li>
     * </ul>
     * Context menu:<br>
     * The context menu for a single cell shows entries to set the cell to all remaining candidates, entries
     * to remove all remaining candidates (including one entry to remove multiple candidates in one move)
     * and entries for coloring. If a region of cells is selected, setting cells is not possible.
     * 
     * @param evt
     */
    private void formMouseClicked(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_formMouseClicked
        // undo/Redo siehe handleKeys()
        undoStack.push(sudoku.clone());
        boolean changed = false;
        //undoStack.push(sudoku.clone()); - nach unten geschoben

        int line = getLine(evt.getPoint());
        int col = getCol(evt.getPoint());
        int cand = getCandidate(evt.getPoint(), line, col);
        //System.out.println("line/col/cand " + line + "/" + col + "/" + cand);
        boolean ctrlPressed = (evt.getModifiersEx() & MouseEvent.CTRL_DOWN_MASK) != 0;
        boolean shiftPressed = (evt.getModifiersEx() & MouseEvent.SHIFT_DOWN_MASK) != 0;
        if (line >= 0 && line <= 8 && col >= 0 && col <= 8) {
            //System.out.println((evt.getButton() == MouseEvent.BUTTON1) + "/" + (evt.getButton() == MouseEvent.BUTTON2) + "/" + (evt.getButton() == MouseEvent.BUTTON3));
            if (evt.getButton() == MouseEvent.BUTTON3) {
                // bring up popup menu
                showPopupMenu(line, col);
            } else {
                if (aktColorIndex != -1) {
                    // coloring is active
                    int colorNumber = aktColorIndex;
                    if (shiftPressed || evt.getButton() == MouseEvent.BUTTON2) {
                        if (colorNumber % 2 == 0) {
                            colorNumber++;
                        } else {
                            colorNumber--;
                        }
                    }
                    //System.out.println(line + "/" + col + "/" + cand + "/" + colorNumber + "/" + colorCells);
                    if (colorCells) {
                        // coloring for cells
                        handleColoring(line, col, -1, colorNumber);
                    } else {
                        // coloring for candidates
                        if (cand != -1) {
                            handleColoring(line, col, cand, colorNumber);
                        }
                    }
                    // we do adjust the selected cell (ranges are not allowed in coloring)
                    aktLine = line;
                    aktCol = col;
                } else if (evt.getButton() == MouseEvent.BUTTON1) {
                    // in normal mode we only react to the left mouse button
                    //System.out.println("BUTTON1/" + evt.getClickCount() + "/" + ctrlPressed + "/" + cand);
                    SudokuCell cell = sudoku.getCell(line, col);
                    if (evt.getClickCount() == 2) {
                        if (ctrlPressed) {
                            if (cand != -1) {
                                // toggle candidate
                                if (cell.isCandidate(candidateMode, cand)) {
                                    sudoku.setCandidate(line, col, candidateMode, cand, false);
                                } else {
                                    sudoku.setCandidate(line, col, candidateMode, cand, true);
                                }
                                clearRegion();
                                changed = true;
                            }
                        } else {
                            if (cell.getValue() == 0) {
                                if (cell.getAnzCandidates(candidateMode) == 1) {
                                    // Naked single -> set it!
                                    int actCand = cell.getAllCandidates(candidateMode)[0];
                                    setCell(line, col, actCand);
//                                // if filters are applied, change digit to act digit
//                                if (showHintCellValue != 0) {
//                                    showHintCellValue = actCand;
//                                }
                                    changed = true;
                                } else if (showHintCellValue != 0 && isHiddenSingle(showHintCellValue, line, col)) {
                                    // Hidden Single -> it
                                    setCell(line, col, showHintCellValue);
                                    changed = true;
                                } else if (cand != -1) {
                                    // candidate double clicked -> set it
                                    // (only if that candidate is still set in the cell)
                                    if (cell.isCandidate(candidateMode, cand)) {
                                        setCell(line, col, cand);
                                    }
                                    changed = true;
                                }
                            }
                        }
                    } else if (evt.getClickCount() == 1) {
                        if (ctrlPressed) {
                            // select additional cell
                            if (selectedCells.size() == 0) {
                                // the last selected cell is not yet in the set
                                selectedCells.add(Sudoku.getIndex(aktLine, aktCol));
                                selectedCells.add(Sudoku.getIndex(line, col));
                                aktLine = line;
                                aktCol = col;
                            } else {
                                int index = Sudoku.getIndex(line, col);
                                if (selectedCells.contains(index)) {
                                    selectedCells.remove(index);
                                } else {
                                    selectedCells.add(Sudoku.getIndex(line, col));
                                }
                                aktLine = line;
                                aktCol = col;
                            }
                        } else if (shiftPressed) {
                            if (Options.getInstance().useShiftForRegionSelect) {
                                // select range of cells
                                selectRegion(line, col);
                            } else {
                                if (cand != -1) {
                                    // toggle candidate
                                    if (cell.isCandidate(candidateMode, cand)) {
                                        sudoku.setCandidate(line, col, candidateMode, cand, false);
                                    } else {
                                        sudoku.setCandidate(line, col, candidateMode, cand, true);
                                    }
                                    clearRegion();
                                    changed = true;
                                }
                            }
                        } else {
                            // select single cell, delete old markings if available
                            aktLine = line;
                            aktCol = col;
                            clearRegion();
                        }
                    }
                }
            }
            if (changed) {
                redoStack.clear();
            } else {
                undoStack.pop();
            }
            updateCellZoomPanel();
            mainFrame.check();
            repaint();
        }
    }//GEN-LAST:event_formMouseClicked

    private void make1MenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_make1MenuItemActionPerformed
        popupSetCell((JMenuItem)evt.getSource());
    }//GEN-LAST:event_make1MenuItemActionPerformed

    private void exclude1MenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_exclude1MenuItemActionPerformed
        popupExcludeCandidate((JMenuItem)evt.getSource());
    }//GEN-LAST:event_exclude1MenuItemActionPerformed

    private void excludeSeveralMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_excludeSeveralMenuItemActionPerformed
        String input = JOptionPane.showInputDialog(this, ResourceBundle.getBundle("intl/SudokuPanel").getString("SudokuPanel.cmessage"),
                ResourceBundle.getBundle("intl/SudokuPanel").getString("SudokuPanel.ctitle"), JOptionPane.QUESTION_MESSAGE);
        if (input != null) {
            undoStack.push(sudoku.clone());
            boolean changed = false;
            for (int i = 0; i < input.length(); i++) {
                char digit = input.charAt(i);
                if (Character.isDigit(digit)) {
                    if (removeCandidateFromActiveCells(Character.getNumericValue(digit))) {
                        changed = true;
                    }
                }
            }
            if (changed) {
                redoStack.clear();
            } else {
                undoStack.pop();
            }
            updateCellZoomPanel();
            mainFrame.check();
            repaint();
        }
    }//GEN-LAST:event_excludeSeveralMenuItemActionPerformed

    private void color1aMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_color1aMenuItemActionPerformed
        popupToggleColor((JMenuItem)evt.getSource());
    }//GEN-LAST:event_color1aMenuItemActionPerformed

    /**
     * Loads all relevant objects into <code>state</code>. If <code>copy</code> is true,
     * all objects are copied.<br>
     * Some objects have to be copied regardless of parameter <code>copy</code>.
     * @param state
     * @param copy
     */
    public void getState(GuiState state, boolean copy) {
        // items that dont have to be copied
        state.chainIndex = chainIndex;
        // items that must be copied anyway
        state.undoStack = (Stack<Sudoku>) undoStack.clone();
        state.redoStack = (Stack<Sudoku>) redoStack.clone();
        state.coloringMap = (SortedMap<Integer, Integer>) ((TreeMap) coloringMap).clone();
        state.coloringCandidateMap = (SortedMap<Integer, Integer>) ((TreeMap) coloringCandidateMap).clone();
        // items that might be null (and therefore wont be copied)
        state.sudoku = sudoku;
        state.solvedSudoku = solvedSudoku;
        state.step = step;
        if (copy) {
            state.sudoku = (Sudoku) sudoku.clone();
            if (solvedSudoku != null) {
                state.solvedSudoku = (Sudoku) solvedSudoku.clone();
            }
            if (step != null) {
                state.step = (SolutionStep) step.clone();
            }
        }
    }

    /**
     * Loads back a saved state. Whether the objects had been copied
     * before is irrelevant here.<br>
     * The optional objects {@link GuiState#undoStack} and {@link GuiState#redoStack}
     * can be null. If this is the case they are cleared.
     * @param state
     */
    public void setState(GuiState state) {
        chainIndex = state.chainIndex;
        if (state.undoStack != null) {
            undoStack = state.undoStack;
        } else {
            undoStack.clear();
        }
        if (state.redoStack != null) {
            redoStack = state.redoStack;
        } else {
            redoStack.clear();
        }
        if (state.coloringMap != null) {
            coloringMap = state.coloringMap;
        } else {
            coloringMap.clear();
        }
        if (state.coloringCandidateMap != null) {
            coloringCandidateMap = state.coloringCandidateMap;
        } else {
            coloringCandidateMap.clear();
        }
        sudoku = state.sudoku;
        sudoku.synchronizeSets();
        solvedSudoku = state.solvedSudoku;
        step = state.step;
        updateCellZoomPanel();
        mainFrame.check();
        repaint();
    }

//    public void loadFromFile(Sudoku sudoku, Sudoku solvedSudoku) {
//        this.sudoku = sudoku;
//        this.solvedSudoku = solvedSudoku;
//        redoStack.clear();
//        undoStack.clear();
//        coloringMap.clear();
//        coloringCandidateMap.clear();
//        step = null;
//        setChainInStep(-1);
//        updateCellZoomPanel();
//        mainFrame.check();
//        repaint();
//    }

    private void checkShowAllCandidates(int modifiers, int keyCode) {
        // wenn <Shift> und <Ctrl> gedrckt sind, soll showAllCandidatesAkt true sein, sonst false
        boolean oldShowAllCandidatesAkt = showAllCandidatesAkt;
        showAllCandidatesAkt = false;
        if ((modifiers & KeyEvent.SHIFT_DOWN_MASK) != 0 && (modifiers & KeyEvent.CTRL_DOWN_MASK) != 0) {
            showAllCandidatesAkt = true;
        }
        // wenn <Shift> und <Alt> gedrckt sind, soll showAllCandidates true sein, sonst false
        boolean oldShowAllCandidates = showAllCandidates;
        showAllCandidates = false;
        if ((modifiers & KeyEvent.SHIFT_DOWN_MASK) != 0 && (modifiers & KeyEvent.ALT_DOWN_MASK) != 0) {
            showAllCandidates = true;
        }
        if (oldShowAllCandidatesAkt != showAllCandidatesAkt || oldShowAllCandidates != showAllCandidates) {
            repaint();
        }
    }

    public void handleKeysReleased(KeyEvent evt) {
        // wenn <Left-Shift> und <Left-Ctrl> gedrckt sind, soll showAllCandidatesAkt true sein, sonst false
        int modifiers = evt.getModifiersEx();
        int keyCode = 0; // getKeyCode() liefert immer noch die zuletzt gedrckte Taste

        checkShowAllCandidates(modifiers, keyCode);

        if (aktColorIndex >= 0) {
            if (getCursor() == colorCursorShift) {
                setCursor(colorCursor);
                //System.out.println("normal cursor set");
            }
        }
    }

    public void handleKeys(KeyEvent evt) {
        // Undo/Redo: alten Zustand speichern, wenn nichts gendert wurde, wieder entfernen
        boolean changed = false;
        undoStack.push(sudoku.clone());

        int keyCode = evt.getKeyCode();
        int modifiers = evt.getModifiersEx();

        // wenn <Shift> und <Ctrl> gedrckt sind, soll showAllCandidatesAkt true sein, sonst false
        // wenn <Shift> und <Alt> gedrckt sind, soll showAllCandidates true sein, sonst false
        checkShowAllCandidates(modifiers, keyCode);

        // if only <shift> is pressed and coloring is active, the cursor should change to complementary color
        if (aktColorIndex >= 0) {
            if ((modifiers & KeyEvent.SHIFT_DOWN_MASK) != 0) {
                if (getCursor() == colorCursor) {
                    setCursor(colorCursorShift);
                    //System.out.println("cursor shift set");
                }
            }
        }

        // "normale" Tastaturbehandlung
        SudokuCell cell = sudoku.getCell(aktLine, aktCol);
        // bei keyPressed funktioniert getKeyChar() nicht zuverlssig, daher die Zahl selbst ermitteln
        int number = 0;
        boolean clearSelectedRegion = true;
        switch (keyCode) {
            case KeyEvent.VK_DOWN:
                if ((modifiers & KeyEvent.CTRL_DOWN_MASK) != 0 &&
                        (modifiers & KeyEvent.SHIFT_DOWN_MASK) != 0 &&
                        showHintCellValue != 0) {
                    // go to next filtered candidate
                    int index = findNextHintCandidate(aktLine, aktCol, keyCode);
                    aktLine = Sudoku.getLine(index);
                    aktCol = Sudoku.getCol(index);
                } else if (aktLine < 8) {
                    // go to the next line
                    aktLine++;
                    if ((modifiers & KeyEvent.CTRL_DOWN_MASK) != 0) {
                        // go to the next unset cell
                        while (aktLine < 8 && sudoku.getCell(aktLine, aktCol).getValue() != 0) {
                            aktLine++;
                        }
                    } else if ((modifiers & KeyEvent.SHIFT_DOWN_MASK) != 0) {
                        // expand the selected region
                        aktLine--;
                        setShift();
                        shiftLine++;
                        selectRegion(shiftLine, shiftCol);
                        clearSelectedRegion = false;
                    }
                }
                if (clearSelectedRegion) {
                    clearRegion();
                }
                break;
            case KeyEvent.VK_UP:
                if ((modifiers & KeyEvent.CTRL_DOWN_MASK) != 0 &&
                        (modifiers & KeyEvent.SHIFT_DOWN_MASK) != 0 &&
                        showHintCellValue != 0) {
                    // go to next filtered candidate
                    int index = findNextHintCandidate(aktLine, aktCol, keyCode);
                    aktLine = Sudoku.getLine(index);
                    aktCol = Sudoku.getCol(index);
                } else if (aktLine > 0) {
                    // go to the next line
                    aktLine--;
                    if ((modifiers & KeyEvent.CTRL_DOWN_MASK) != 0) {
                        // go to the next unset cell
                        while (aktLine > 0 && sudoku.getCell(aktLine, aktCol).getValue() != 0) {
                            aktLine--;
                        }
                    } else if ((modifiers & KeyEvent.SHIFT_DOWN_MASK) != 0) {
                        // expand the selected region
                        aktLine++;
                        setShift();
                        shiftLine--;
                        selectRegion(shiftLine, shiftCol);
                        clearSelectedRegion = false;
                    }
                }
                if (clearSelectedRegion) {
                    clearRegion();
                }
                break;
            case KeyEvent.VK_RIGHT:
                if ((modifiers & KeyEvent.CTRL_DOWN_MASK) != 0 &&
                        (modifiers & KeyEvent.SHIFT_DOWN_MASK) != 0 &&
                        showHintCellValue != 0) {
                    // go to next filtered candidate
                    int index = findNextHintCandidate(aktLine, aktCol, keyCode);
                    aktLine = Sudoku.getLine(index);
                    aktCol = Sudoku.getCol(index);
                } else if (aktCol < 8) {
                    // go to the next line
                    aktCol++;
                    if ((modifiers & KeyEvent.CTRL_DOWN_MASK) != 0) {
                        // go to the next unset cell
                        while (aktCol < 8 && sudoku.getCell(aktLine, aktCol).getValue() != 0) {
                            aktCol++;
                        }
                    } else if ((modifiers & KeyEvent.SHIFT_DOWN_MASK) != 0) {
                        // expand the selected region
                        aktCol--;
                        setShift();
                        shiftCol++;
                        selectRegion(shiftLine, shiftCol);
                        clearSelectedRegion = false;
                    }
                }
                if (clearSelectedRegion) {
                    clearRegion();
                }
                break;
            case KeyEvent.VK_LEFT:
                if ((modifiers & KeyEvent.CTRL_DOWN_MASK) != 0 &&
                        (modifiers & KeyEvent.SHIFT_DOWN_MASK) != 0 &&
                        showHintCellValue != 0) {
                    // go to next filtered candidate
                    int index = findNextHintCandidate(aktLine, aktCol, keyCode);
                    aktLine = Sudoku.getLine(index);
                    aktCol = Sudoku.getCol(index);
                } else if (aktCol > 0) {
                    // go to the next col
                    aktCol--;
                    if ((modifiers & KeyEvent.CTRL_DOWN_MASK) != 0) {
                        // go to the next unset cell
                        while (aktCol > 0 && sudoku.getCell(aktLine, aktCol).getValue() != 0) {
                            aktCol--;
                        }
                    } else if ((modifiers & KeyEvent.SHIFT_DOWN_MASK) != 0) {
                        // expand the selected region
                        aktCol++;
                        setShift();
                        shiftCol--;
                        selectRegion(shiftLine, shiftCol);
                        clearSelectedRegion = false;
                    }
                }
                if (clearSelectedRegion) {
                    clearRegion();
                }
                break;
            case KeyEvent.VK_HOME:
                if ((modifiers & KeyEvent.SHIFT_DOWN_MASK) != 0) {
                    setShift();
                    if ((modifiers & KeyEvent.CTRL_DOWN_MASK) != 0) {
                        shiftLine = 0;
                    } else {
                        shiftCol = 0;
                    }
                    selectRegion(shiftLine, shiftCol);
                    clearSelectedRegion = false;
                } else {
                    if ((modifiers & KeyEvent.CTRL_DOWN_MASK) != 0) {
                        aktLine = 0;
                    } else {
                        aktCol = 0;
                    }
                }
                if (clearSelectedRegion) {
                    clearRegion();
                }
                break;
            case KeyEvent.VK_END:
                if ((modifiers & KeyEvent.SHIFT_DOWN_MASK) != 0) {
                    setShift();
                    if ((modifiers & KeyEvent.CTRL_DOWN_MASK) != 0) {
                        shiftLine = 8;
                    } else {
                        shiftCol = 8;
                    }
                    selectRegion(shiftLine, shiftCol);
                    clearSelectedRegion = false;
                } else {
                    if ((modifiers & KeyEvent.CTRL_DOWN_MASK) != 0) {
                        aktLine = 8;
                    } else {
                        aktCol = 8;
                    }
                }
                if (clearSelectedRegion) {
                    clearRegion();
                }
                break;
            case KeyEvent.VK_9:
            case KeyEvent.VK_NUMPAD9:
                number++;
            case KeyEvent.VK_8:
            case KeyEvent.VK_NUMPAD8:
                number++;
            case KeyEvent.VK_7:
            case KeyEvent.VK_NUMPAD7:
                number++;
            case KeyEvent.VK_6:
            case KeyEvent.VK_NUMPAD6:
                number++;
            case KeyEvent.VK_5:
            case KeyEvent.VK_NUMPAD5:
                number++;
            case KeyEvent.VK_4:
            case KeyEvent.VK_NUMPAD4:
                number++;
            case KeyEvent.VK_3:
            case KeyEvent.VK_NUMPAD3:
                number++;
            case KeyEvent.VK_2:
            case KeyEvent.VK_NUMPAD2:
                number++;
            case KeyEvent.VK_1:
            case KeyEvent.VK_NUMPAD1:
                number++;
                //int number = Character.digit(evt.getKeyChar(), 10);
                if (selectedCells.isEmpty()) {
                    if ((modifiers & KeyEvent.CTRL_DOWN_MASK) == 0) {
                        // Zelle setzen
                        setCell(aktLine, aktCol, number);
                        changed = true;
                    } else if ((modifiers & KeyEvent.SHIFT_DOWN_MASK) == 0) {
                        // only when shift is NOT pressed (if pressed its a menu accelerator)
                        toggleCandidateInCell(aktLine, aktCol, number);
                        changed = true;
                    }
                }
                break;
            case KeyEvent.VK_BACK_SPACE:
            case KeyEvent.VK_DELETE:
            case KeyEvent.VK_0:
            case KeyEvent.VK_NUMPAD0:
                if ((modifiers & KeyEvent.CTRL_DOWN_MASK) == 0) {
                    // Zelle lschen
                    if (cell.getValue() != 0 && !cell.isFixed()) {
                        sudoku.setCell(aktLine, aktCol, 0);
                        changed = true;
                    }
                }
                break;
            case KeyEvent.VK_F9:
                number++;
            case KeyEvent.VK_F8:
                number++;
            case KeyEvent.VK_F7:
                number++;
            case KeyEvent.VK_F6:
                number++;
            case KeyEvent.VK_F5:
                number++;
            case KeyEvent.VK_F4:
                number++;
            case KeyEvent.VK_F3:
                number++;
            case KeyEvent.VK_F2:
                number++;
            case KeyEvent.VK_F1:
                number++;
                if ((modifiers & KeyEvent.ALT_DOWN_MASK) == 0) {
                    // pressing <Alt><F4> changes the selection ... not good
                    if (getShowHintCellValue() == number && isShowInvalidOrPossibleCells()) {
                        if ((modifiers & KeyEvent.CTRL_DOWN_MASK) == 0) {
                            setShowHintCellValue(0);
                            setShowInvalidOrPossibleCells(false);
                        } else {
                            invalidCells = !invalidCells;
                        }
                    } else {
                        setShowHintCellValue(number);
                        setShowInvalidOrPossibleCells(true);
                        if ((modifiers & KeyEvent.CTRL_DOWN_MASK) != 0) {
                            invalidCells = !invalidCells;
                        }
                    }
                }
                break;
            case KeyEvent.VK_SPACE:
                if (isShowInvalidOrPossibleCells() && selectedCells.isEmpty()) {
                    int candidate = getShowHintCellValue();
                    toggleCandidateInCell(aktLine, aktCol, candidate);
                    changed = true;
//                    if (cell.getValue() == 0) {
//                        if (cell.isCandidate(candidateMode, candidate)) {
//                            sudoku.setCandidate(aktLine, aktCol, candidateMode, candidate, false);
//                            changed = true;
//                        } else {
//                            sudoku.setCandidate(aktLine, aktCol, candidateMode, candidate, true);
//                            changed = true;
//                        }
//                    }
                }
                break;
            case KeyEvent.VK_E:
                number++;
            case KeyEvent.VK_D:
                number++;
            case KeyEvent.VK_C:
                number++;
            case KeyEvent.VK_B:
                number++;
            case KeyEvent.VK_A:
                // if ctrl or alt or meta is pressed, it's a shortcut
                if ((modifiers & KeyEvent.ALT_DOWN_MASK) != 0 ||
                        (modifiers & KeyEvent.ALT_GRAPH_DOWN_MASK) != 0 ||
                        (modifiers & KeyEvent.CTRL_DOWN_MASK) != 0 ||
                        (modifiers & KeyEvent.META_DOWN_MASK) != 0) {
                    // do nothing!
                    break;
                }
                // calculate index in coloringColors[]
                number *= 2;
                if ((modifiers & KeyEvent.SHIFT_DOWN_MASK) != 0) {
                    number++;
                }
                handleColoring(-1, number);
//                int index = Sudoku.getIndex(aktLine, aktCol);
//                if (coloringMap.containsKey(index) && coloringMap.get(index) == number) {
//                    // pressing the same key on the same cell twice removes the coloring
//                    coloringMap.remove(index);
//                } else {
//                    // either newly colored cell or change of cell color
//                    coloringMap.put(index, number);
//                }
                break;
            case KeyEvent.VK_R:
                clearColoring();
                break;
            case KeyEvent.VK_GREATER:
            case KeyEvent.VK_LESS:
            default:
                // doesnt work on all keyboards :-(
                // more precisely: doesnt work, if the keyboard layout in the OS
                // doesnt match the physical layout of the keyboard
                char ch = evt.getKeyChar();
                if (ch == '<' || ch == '>') {
                    boolean isUp = evt.getKeyChar() == '>';
                    if (isShowInvalidOrPossibleCells()) {
                        if (isUp) {
                            showHintCellValue++;
                            if (showHintCellValue > 9) {
                                showHintCellValue = 1;
                            }
                        } else {
                            showHintCellValue--;
                            if (showHintCellValue < 1) {
                                showHintCellValue = 9;
                            }
                        }
                    }
                }
                break;
        }
        if (changed) {
            // Undo wurde schon behandelt, Redo ist nicht mehr mglich
            redoStack.clear();
        } else {
            // kein Undo ntig -> wieder entfernen
            undoStack.pop();
        }
        updateCellZoomPanel();
        mainFrame.check();
        repaint();
    }

    /**
     * Clears a selected region of cells
     */
    private void clearRegion() {
        selectedCells.clear();
        shiftLine = -1;
        shiftCol = -1;
    }

    /**
     * Initializes {@link #shiftLine}/{@link #shiftCol} for
     * selecting regions of cells using the keyboard
     */
    private void setShift() {
        if (shiftLine == -1) {
            shiftLine = aktLine;
            shiftCol = aktCol;
        }
    }

    /**
     * Select all cells in the rectangle defined by
     * {@link #aktLine}/{@link #aktCol} and line/col
     * @param line
     * @param col
     */
    private void selectRegion(int line, int col) {
        selectedCells.clear();
        if (line == aktLine && col == aktCol) {
            // same cell clicked twice -> no region selected -> do nothing
        } else {
            // every cell in the region gets selected, aktLine and aktCol are not changed
            int cStart = col < aktCol ? col : aktCol;
            int lStart = line < aktLine ? line : aktLine;
            for (int i = cStart; i <= cStart + Math.abs(col - aktCol); i++) {
                for (int j = lStart; j <= lStart + Math.abs(line - aktLine); j++) {
                    selectedCells.add(Sudoku.getIndex(j, i));
                }
            }
        }
    }

    /**
     * Finds the next colored cell, if filters are applied. mode gives the
     * direction in which to search (as KeyEvent). The search wraps at
     * sudoku boundaries.
     * @param line
     * @param col
     * @param mode
     * @return
     */
    private int findNextHintCandidate(int line, int col, int mode) {
        int index = Sudoku.getIndex(line, col);
        if (showHintCellValue == 0) {
            return index;
        }
        switch (mode) {
            case KeyEvent.VK_DOWN:
                // let's start with the next line
                line++;
                if (line == 9) {
                    line = 0;
                    col++;
                    if (col == 9) {
                        return index;
                    }
                }
                for (int i = col; i < 9; i++) {
                    int j = i == col ? line : 0;
                    for (; j < 9; j++) {
                        if (sudoku.getCell(j, i).getValue() == 0 &&
                                sudoku.getCell(j, i).isCandidate(candidateMode, showHintCellValue)) {
                            return Sudoku.getIndex(j, i);
                        }
                    }
                }
                break;
            case KeyEvent.VK_UP:
                // let's start with the previous line
                line--;
                if (line < 0) {
                    line = 8;
                    col--;
                    if (col < 0) {
                        return index;
                    }
                }
                for (int i = col; i >= 0; i--) {
                    int j = i == col ? line : 8;
                    for (; j >= 0; j--) {
                        if (sudoku.getCell(j, i).getValue() == 0 &&
                                sudoku.getCell(j, i).isCandidate(candidateMode, showHintCellValue)) {
                            return Sudoku.getIndex(j, i);
                        }
                    }
                }
                break;
            case KeyEvent.VK_LEFT:
                // lets start left
                index--;
                if (index < 0) {
                    return index + 1;
                }
                while (index >= 0) {
                    if (sudoku.getCell(index).getValue() == 0 &&
                            sudoku.getCell(index).isCandidate(candidateMode, showHintCellValue)) {
                        return index;
                    }
                    index--;
                }
                if (index < 0) {
                    index = Sudoku.getIndex(line, col);
                }
                break;
            case KeyEvent.VK_RIGHT:
                // lets start right
                index++;
                if (index >= sudoku.getCells().length) {
                    return index - 1;
                }
                while (index < sudoku.getCells().length) {
                    if (sudoku.getCell(index).getValue() == 0 &&
                            sudoku.getCell(index).isCandidate(candidateMode, showHintCellValue)) {
                        return index;
                    }
                    index++;
                }
                if (index >= sudoku.getCells().length) {
                    index = Sudoku.getIndex(line, col);
                }
                break;
        }
        return index;
    }
    
    /**
     * Removes all coloring info
     */
    public void clearColoring() {
        coloringMap.clear();
        coloringCandidateMap.clear();
        setActiveColor(-1);
        updateCellZoomPanel();
        mainFrame.check();
    }

    /**
     * Handles coloring for all selected cells, delegates to {@link #handleColoring(int, int, int, int)}
     * (see description there).
     * @param candidate
     * @param colorNumber
     */
    private void handleColoring(int candidate, int colorNumber) {
        if (selectedCells.isEmpty()) {
            handleColoring(aktLine, aktCol, candidate, colorNumber);
        } else {
            for (int index : selectedCells) {
                handleColoring(Sudoku.getLine(index), Sudoku.getCol(index), candidate, colorNumber);
            }
        }
    }

    /**
     * Toggles Color for candidate in active cell; only called from
     * {@link CellZoomPanel}.
     * 
     * @param candidate
     */
    public void handleColoring(int candidate) {
        handleColoring(aktLine, aktCol, candidate, aktColorIndex);
        repaint();
        updateCellZoomPanel();
        mainFrame.fixFocus();
    }
    
    /**
     * Handles the coloring of a cell or a candidate. If candidate equals -1, a cell
     * is to be coloured, else a candidate. If the target is already colored and the
     * new color matches the old one, coloring is removed, else it is set
     * to the new color.
     * @param line
     * @param col
     * @param candidate
     * @param colorNumber
     */
    private void handleColoring(int line, int col, int candidate, int colorNumber) {
        SortedMap<Integer,Integer> map = coloringMap;
        int key = Sudoku.getIndex(line, col);
        if (candidate != -1) {
            key = key * 10 + candidate;
            map = coloringCandidateMap;
        }
        if (map.containsKey(key) && map.get(key) == colorNumber) {
            // pressing the same key on the same cell twice removes the coloring
            map.remove(key);
        } else {
            // either newly colored cell or change of cell color
            map.put(key, colorNumber);
        }
        updateCellZoomPanel();
    }

    /**
     * Handles "set value" done in {@link CellZoomPanel}. Should not be
     * used otherwise.
     * @param number
     */
    public void setCellFromCellZoomPanel(int number) {
        undoStack.push(sudoku.clone());
        setCell(aktLine, aktCol, number);
        updateCellZoomPanel();
        mainFrame.check();
        repaint();
    }

    private void setCell(int line, int col, int number) {
        SudokuCell cell = sudoku.getCell(line, col);
        if (!cell.isFixed() && cell.getValue() != number) {
            // Setzen ist mglich, auf lschen prfen
            if (cell.getValue() != 0) {
                sudoku.setCell(line, col, 0);
            }
            sudoku.setCell(line, col, number);
        }
    }

    /**
     * Toggles candidate in all active cells (all cells in {@link #selectedCells} or
     * cell denoted by {@link #aktLine}/{@link #aktCol} if {@link #selectedCells} is
     * empty).<br>
     *
     * Uses {@link #candidateMode} to determine the right type of candidate.
     * @param candidate
     */
    private void toggleCandidateInAktCells(int candidate) {
        if (selectedCells.isEmpty()) {
            toggleCandidateInCell(aktLine, aktCol, candidate);
        } else {
            for (int index : selectedCells) {
                toggleCandidateInCell(Sudoku.getLine(index), Sudoku.getCol(index), candidate);
            }
        }
    }

    /**
     * Toggles candidate in the cell denoted by line/col. Uses {@link #candidateMode}.
     * @param line
     * @param col
     * @param candidate
     */
    private void toggleCandidateInCell(int line, int col, int candidate) {
        SudokuCell cell = sudoku.getCell(line, col);
        if (cell.getValue() == 0) {
            if (cell.isCandidate(candidateMode, candidate)) {
                sudoku.setCandidate(line, col, candidateMode, candidate, false);
            } else {
                sudoku.setCandidate(line, col, candidateMode, candidate, true);
            }
        }
        updateCellZoomPanel();
    }

    /**
     * Creates an image of the current sudoku in the given size.
     * 
     * @param size
     * @return
     */
    public BufferedImage getSudokuImage(int size) {
        BufferedImage fileImage = new BufferedImage(size, size, BufferedImage.TYPE_3BYTE_BGR);
        Graphics2D g = fileImage.createGraphics();
        this.g2 = g;
        g2.setColor(Color.WHITE);
        g2.fillRect(0, 0, size, size);
        drawPage(size, size, true, false);
        return fileImage;
    }

    /**
     * Writes an image of the current sudoku as png into a file. The image
     * is <code>size</code> pixels wide and high, the resolution in the png
     * file is set to <code>dpi</code>.
     * 
     * @param file
     * @param size
     * @param dpi
     */
    public void saveSudokuAsPNG(File file, int size, int dpi) {
        BufferedImage fileImage = getSudokuImage(size);
        writePNG(fileImage, dpi, file);
    }

    @Override
    public int print(Graphics graphics, PageFormat pageFormat, int pageIndex) throws PrinterException {
        if (pageIndex > 0) {
            return Printable.NO_SUCH_PAGE;
        }

        // Graphics2D-Objekt herrichten
        Graphics2D printG2 = (Graphics2D) graphics;
        printG2.translate(pageFormat.getImageableX(), pageFormat.getImageableY());
        int printWidth = (int) pageFormat.getImageableWidth();
        int printHeight = (int) pageFormat.getImageableHeight();

        // berschrift
        printG2.setFont(Options.getInstance().getBigFont());
        String title = MainFrame.VERSION;
        FontMetrics metrics = printG2.getFontMetrics();
        int textWidth = metrics.stringWidth(title);
        int textHeight = metrics.getHeight();
        int y = 2 * textHeight;
        printG2.drawString(title, (printWidth - textWidth) / 2, textHeight);

        // Level
        printG2.setFont(Options.getInstance().getSmallFont());
        if (sudoku != null && sudoku.getLevel() != null) {
            title = sudoku.getLevel().getName() + " (" + sudoku.getScore() + ")";
            metrics = printG2.getFontMetrics();
            textWidth = metrics.stringWidth(title);
            textHeight = metrics.getHeight();
            printG2.drawString(title, (printWidth - textWidth) / 2, y);
            y += textHeight;
        }

        printG2.translate(0, y);
        this.g2 = printG2;
        drawPage(printWidth, printHeight, true);
        return Printable.PAGE_EXISTS;
    }

    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        g2 = (Graphics2D) g;
        drawPage(getBounds().width, getBounds().height);
    }

    private void drawPage(int totalWidth, int totalHeight) {
        drawPage(totalWidth, totalHeight, false, true);
    }

    private void drawPage(int totalWidth, int totalHeight, boolean isPrint) {
        drawPage(totalWidth, totalHeight, isPrint, true);
    }

    private void drawPage(int totalWidth, int totalHeight, boolean isPrint, boolean mitRand) {
        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);

        // Gre bestimmen und quadratisch machen
        width = totalWidth;
        height = totalHeight;
        width = (height < width) ? height : width;
        height = (width < height) ? width : height;

        // Zwischenrume nicht mehr konstant
        delta = DELTA;
        deltaRand = DELTA_RAND;

        if (Options.getInstance().getDrawMode() == 1) {
            delta = 0;
        }

        // Gre der einzelnen Zellen bestimmen und Mae anpassen (Rundungsfehler!)
        if (mitRand) {
            cellSize = (width - 4 * delta - 2 * deltaRand) / 9;
        } else {
            cellSize = (width - 4 * delta) / 9;
        }
        width = height = cellSize * 9 + 4 * delta;
        startSX = (totalWidth - width) / 2;
        if (isPrint && mitRand) {
            startSY = 0;
        } else {
            startSY = (totalHeight - height) / 2;
        }

        // Fonts festlegen
        // ACHTUNG: Bei jeder nderung Fonts neu setzen!
        Font tmpFont = Options.getInstance().getDefaultValueFont();
        if (valueFont != null) {
            if (!valueFont.getName().equals(tmpFont.getName()) ||
                    valueFont.getStyle() != tmpFont.getStyle() ||
                    valueFont.getSize() != ((int) (cellSize * Options.getInstance().getValueFontFactor()))) {
                valueFont = new Font(tmpFont.getName(), tmpFont.getStyle(),
                        (int) (cellSize * Options.getInstance().getValueFontFactor()));
            }
        }
        tmpFont = Options.getInstance().getDefaultCandidateFont();
        if (candidateFont != null) {
            if (!candidateFont.getName().equals(tmpFont.getName()) ||
                    candidateFont.getStyle() != tmpFont.getStyle() ||
                    candidateFont.getSize() != ((int) (cellSize * Options.getInstance().getCandidateFontFactor()))) {
                candidateFont = new Font(tmpFont.getName(), tmpFont.getStyle(),
                        (int) (cellSize * Options.getInstance().getCandidateFontFactor()));
            }
        }
        if (oldWidth != width) {
            oldWidth = width;
            valueFont = new Font(Options.getInstance().getDefaultValueFont().getName(),
                    Options.getInstance().getDefaultValueFont().getStyle(),
                    (int) (cellSize * Options.getInstance().getValueFontFactor()));
            candidateFont = new Font(Options.getInstance().getDefaultCandidateFont().getName(),
                    Options.getInstance().getDefaultCandidateFont().getStyle(),
                    (int) (cellSize * Options.getInstance().getCandidateFontFactor()));
        }

        // Zellen zeichnen
        int dx = 0, dy = 0, dcx = 0, dcy = 0, ddx = 0, ddy = 0;
        for (int line = 0; line < 9; line++) {
            for (int col = 0; col < 9; col++) {
                // Zelle holen
                SudokuCell cell = sudoku.getCell(line, col);

                // Hintergrund zeichnen
                g2.setColor(Options.getInstance().getDefaultCellColor()); // normal ist wei

                int cellIndex = Sudoku.getIndex(line, col);
//                if (line == aktLine && col == aktCol && !isPrint) {
//                    g2.setColor(Options.getInstance().getAktCellColor());
//                }
//                if (selectedCells.contains(cellIndex) && ! isPrint) {
//                    g2.setColor(Options.getInstance().getAktCellColor());
//                }
                boolean isSelected = (selectedCells.isEmpty() && line == aktLine && col == aktCol) || selectedCells.contains(cellIndex);
                if (isSelected && ! isPrint) {
                    g2.setColor(Options.getInstance().getAktCellColor());
                }
                // check if the candidate denoted by showHintCellValue is a valid candidate; if showCandidates == false,
                // this can be done by SudokuCell.isCandidateValid(); if it is true, candidates entered by the user
                // are highlighted, regardless of validity
                boolean candidateValid = false;
                if (showHintCellValue != 0) {
                    if (showCandidates) {
                        candidateValid = cell.isCandidateValid(candidateMode, showHintCellValue);
                    } else {
                        candidateValid = cell.isCandidate(candidateMode, showHintCellValue);
                    }
                }
                if (isShowInvalidOrPossibleCells() && isInvalidCells() &&
                        (cell.getValue() != 0 || (showHintCellValue != 0 && !candidateValid))) {
//                        (cell.getValue() != 0 || (getShowHintCellValue() != 0 && !cell.isCandidateValid(SudokuCell.PLAY, getShowHintCellValue())))) {
                    g2.setColor(Options.getInstance().getInvalidCellColor());
                }
                if (isShowInvalidOrPossibleCells() && !isInvalidCells() && cell.getValue() == 0 &&
                        showHintCellValue != 0 && candidateValid) {
//                        getShowHintCellValue() != 0 && cell.isCandidateValid(SudokuCell.PLAY, getShowHintCellValue())) {
                    g2.setColor(Options.getInstance().getPossibleCellColor());
                }
                //if (cell.getValue() == 0 && coloringMap.containsKey(cellIndex)) {
                if (coloringMap.containsKey(cellIndex)) {
                    // coloring
                    g2.setColor(Options.getInstance().getColoringColors()[coloringMap.get(cellIndex)]);
                }
                g2.fillRect(getX(line, col), getY(line, col), cellSize, cellSize);
                if (isSelected && !isPrint && g2.getColor() != Options.getInstance().getAktCellColor()) {
                    g2.setColor(Options.getInstance().getAktCellColor());
                    g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.5f));
                    g2.fillRect(getX(line, col), getY(line, col), cellSize, cellSize);
                    g2.setPaintMode();
                }


                // Wert zeichnen
                int startX = getX(line, col);
                int startY = getY(line, col);
                if (cell.getValue() != 0) {
                    // Wert vorhanden: zeichnen
                    g2.setColor(Options.getInstance().getCellValueColor());
                    if (cell.isFixed()) {
                        g2.setColor(Options.getInstance().getCellFixedValueColor());
                    } else if (isShowWrongValues() == true && !sudoku.checkIsValidValue(line, col, cell.getValue())) {
                        g2.setColor(Options.getInstance().getWrongValueColor());
                    } else if (isShowDeviations() && solvedSudoku != null && cell.getValue() != solvedSudoku.getCell(line, col).getValue()) {
                        g2.setColor(Options.getInstance().getDeviationColor());
                    }
                    g2.setFont(valueFont);
                    dx = (cellSize - g2.getFontMetrics().stringWidth("8")) / 2;
                    dy = (cellSize + g2.getFontMetrics().getAscent()) / 2;
                    g2.drawString(Integer.toString(cell.getValue()), startX + dx, startY + dy);
                } else {
                    g2.setFont(candidateFont);
                    // alle vorhandenen Kandidaten gleichmig ber die Zelle verteilen
                    // wird auch aufgerufen, wenn hoDrawMode gesetzt ist und <Shift>+<Ctrl> gedrckt
                    // ist; muss in diesem Fall alle Kandidaten anzeigen
                    int type = candidateMode;
                    if (showAllCandidates || showAllCandidatesAkt && line == aktLine && col == aktCol) {
                        type = SudokuCell.ALL;
                    }
                    if (showCandidates) {
                        type = SudokuCell.PLAY;
                    }
                    int third = cellSize / 3;
                    dcx = (third - g2.getFontMetrics().stringWidth("8")) / 2;
                    dcy = (third + g2.getFontMetrics().getAscent()) / 2;
                    ddx = (int) (g2.getFontMetrics().stringWidth("8") * Options.getInstance().getHintBackFactor());
                    ddy = (int) (g2.getFontMetrics().getAscent() * Options.getInstance().getHintBackFactor());
                    for (int i = 1; i <= 9; i++) {
                        if (cell.isCandidate(type, i) ||
                                (isShowCandidates() && isShowDeviations() && solvedSudoku != null && i == solvedSudoku.getCell(line, col).getValue())) {
                            g2.setColor(Options.getInstance().getCandidateColor());
                            if (isShowWrongValues() == true && !cell.isCandidateValid(type, i)) {
                                g2.setColor(Options.getInstance().getWrongValueColor());
                            }
                            if (!cell.isCandidate(type, i) && isShowDeviations() && solvedSudoku != null &&
                                    i == solvedSudoku.getCell(line, col).getValue()) {
                                g2.setColor(Options.getInstance().getDeviationColor());
                            }
                            int shiftX = ((i - 1) % 3) * third;
                            int shiftY = ((i - 1) / 3) * third;
                            Color hintColor = null;
                            Color candColor = null;
                            if (step != null) {
                                int index = Sudoku.getIndex(line, col);
                                if (step.getIndices().indexOf(index) >= 0 && step.getValues().indexOf(i) >= 0) {
                                    hintColor = Options.getInstance().getHintCandidateBackColor();
                                    candColor = Options.getInstance().getHintCandidateColor();
                                }
                                int alsIndex = step.getAlsIndex(index, chainIndex);
                                if (alsIndex != -1 && ((chainIndex == -1 && ! step.getType().isKrakenFish()) || alsToShow.contains(alsIndex))) {
                                    hintColor = Options.getInstance().getHintCandidateAlsBackColors()[alsIndex % Options.getInstance().getHintCandidateAlsBackColors().length];
                                    candColor = Options.getInstance().getHintCandidateAlsColors()[alsIndex % Options.getInstance().getHintCandidateAlsColors().length];
                                }
                                for (int k = 0; k < step.getChains().size(); k++) {
                                    if (step.getType().isKrakenFish() && chainIndex == -1) {
                                        // Index 0 means show no chain at all
                                        continue;
                                    }
                                    if (chainIndex != -1 && k != chainIndex) {
                                        // show only one chain in Forcing Chains/Nets
                                        continue;
                                    }
                                    Chain chain = step.getChains().get(k);
                                    for (int j = chain.start; j <= chain.end; j++) {
                                        if (chain.chain[j] == Integer.MIN_VALUE) {
                                            // Trennmarker fr mins -> ignorieren
                                            continue;
                                        }
                                        int chainEntry = Math.abs(chain.chain[j]);
                                        int index1 = -1, index2 = -1, index3 = -1;
                                        if (Chain.getSNodeType(chainEntry) == Chain.NORMAL_NODE) {
                                            index1 = Chain.getSCellIndex(chainEntry);
                                        }
                                        if (Chain.getSNodeType(chainEntry) == Chain.GROUP_NODE) {
                                            index1 = Chain.getSCellIndex(chainEntry);
                                            index2 = Chain.getSCellIndex2(chainEntry);
                                            index3 = Chain.getSCellIndex3(chainEntry);
                                        }
                                        if ((index == index1 || index == index2 || index == index3) && Chain.getSCandidate(chainEntry) == i) {
                                            if (Chain.isSStrong(chainEntry)) {
                                                // strong link
                                                hintColor = Options.getInstance().getHintCandidateBackColor();
                                                candColor = Options.getInstance().getHintCandidateColor();
                                            } else {
                                                hintColor = Options.getInstance().getHintCandidateFinBackColor();
                                                candColor = Options.getInstance().getHintCandidateFinColor();
                                            }
                                        }
                                    }
                                }
                                for (Candidate cand : step.getFins()) {
                                    if (cand.getIndex() == index && cand.getValue() == i) {
                                        hintColor = Options.getInstance().getHintCandidateFinBackColor();
                                        candColor = Options.getInstance().getHintCandidateFinColor();
                                    }
                                }
                                for (Candidate cand : step.getEndoFins()) {
                                    if (cand.getIndex() == index && cand.getValue() == i) {
                                        hintColor = Options.getInstance().getHintCandidateEndoFinBackColor();
                                        candColor = Options.getInstance().getHintCandidateEndoFinColor();
                                    }
                                }
                                if (step.getValues().contains(i) && step.getColorCandidates().containsKey(index)) {
                                    hintColor = Options.getInstance().getColoringColors()[step.getColorCandidates().get(index)];
                                    candColor = Options.getInstance().getCandidateColor();
                                }
                                for (Candidate cand : step.getCandidatesToDelete()) {
                                    if (cand.getIndex() == index && cand.getValue() == i) {
                                        hintColor = Options.getInstance().getHintCandidateDeleteBackColor();
                                        candColor = Options.getInstance().getHintCandidateDeleteColor();
                                    }
                                }
                                for (Candidate cand : step.getCannibalistic()) {
                                    if (cand.getIndex() == index && cand.getValue() == i) {
                                        hintColor = Options.getInstance().getHintCandidateCannibalisticBackColor();
                                        candColor = Options.getInstance().getHintCandidateCannibalisticColor();
                                    }
                                }
                            }
                            Color coloringColor = null;
                            if (coloringCandidateMap.containsKey(cellIndex * 10 + i)) {
                                //if (coloringMap.containsKey(cellIndex)) {
                                // coloring
                                coloringColor = Options.getInstance().coloringColors[coloringCandidateMap.get(cellIndex * 10 + i)];
                            }
                            Color oldColor = g2.getColor();
                            if (coloringColor != null) {
                                g2.setColor(coloringColor);
                                g2.fillRect(startX + shiftX + dcx - 2 * (ddy - ddx) / 3, startY + shiftY + dcy - 4 * ddy / 5 - 1, ddy, ddy);
                            }
                            if (hintColor != null) {
                                g2.setColor(hintColor);
                                g2.fillOval(startX + shiftX + dcx - 2 * (ddy - ddx) / 3, startY + shiftY + dcy - 4 * ddy / 5 - 1, ddy, ddy);
                            }
                            //g2.setColor(candColor);
                            g2.setColor(oldColor);
                            g2.drawString(Integer.toString(i), startX + dcx + shiftX, startY + dcy + shiftY);
                        }
                    }
                }
            }
        }

        // Rahmen zeichnen: muss am Schluss sein, wegen der Hintergrnde
        switch (Options.getInstance().getDrawMode()) {
            case 0:
                g2.setStroke(new BasicStroke(2));
                g2.setColor(Options.getInstance().getGridColor());
                g2.drawRect(startSX, startSY, width, height);
                drawBlockLine(delta + startSX, 1 * delta + startSY, true);
                drawBlockLine(delta + startSX, 2 * delta + startSY + 3 * cellSize, true);
                drawBlockLine(delta + startSX, 3 * delta + startSY + 6 * cellSize, true);
                break;
            case 1:
                g2.setStroke(new BasicStroke(2));
                if (width > 1000) {
                    g2.setStroke(new BasicStroke(4));
                }
                g2.setColor(Options.getInstance().getInnerGridColor());
                drawBlockLine(delta + startSX, 1 * delta + startSY, false);
                drawBlockLine(delta + startSX, 2 * delta + startSY + 3 * cellSize, false);
                drawBlockLine(delta + startSX, 3 * delta + startSY + 6 * cellSize, false);
                g2.setColor(Options.getInstance().getGridColor());
                g2.drawRect(startSX, startSY, width, height);
                for (int i = 0; i < 3; i++) {
                    g2.drawLine(startSX, startSY + i * 3 * cellSize, startSX + 9 * cellSize, startSY + i * 3 * cellSize);
                    g2.drawLine(startSX + i * 3 * cellSize, startSY, startSX + i * 3 * cellSize, startSY + 9 * cellSize);
                }
                break;
        }

        // Chains zeichnen, wenn vorhanden
        if (step != null && step.getChains().size() != 0) {
            // es gibt mindestens eine Chain
            // zuerst alle Punkte sammeln (auch zu lschende Kandidaten und ALS)
            points.clear();
            //for (Chain chain : step.getChains()) {
            for (int ci = 0; ci < step.getChainAnz(); ci++) {
                if (step.getType().isKrakenFish() && chainIndex == -1) {
                    continue;
                }
                if (chainIndex != -1 && chainIndex != ci) {
                    continue;
                }
                Chain chain = step.getChains().get(ci);
                for (int i = chain.start; i <= chain.end; i++) {
                    int che = Math.abs(chain.chain[i]);
                    points.add(getChainPoint(Chain.getSCellIndex(che), Chain.getSCandidate(che), cellSize, dcx, dcy, ddx, ddy));
                    if (Chain.getSNodeType(che) == Chain.GROUP_NODE) {
                        int indexC = Chain.getSCellIndex2(che);
                        if (indexC != -1) {
                            points.add(getChainPoint(indexC, Chain.getSCandidate(che), cellSize, dcx, dcy, ddx, ddy));
                        }
                        indexC = Chain.getSCellIndex3(che);
                        if (indexC != -1) {
                            points.add(getChainPoint(indexC, Chain.getSCandidate(che), cellSize, dcx, dcy, ddx, ddy));
                        }
                    }
                }
            }
            for (Candidate cand : step.getCandidatesToDelete()) {
                points.add(getChainPoint(cand.index, cand.value, cellSize, dcx, dcy, ddx, ddy));
            }
            //for (AlsInSolutionStep als : step.getAlses()) {
            for (int ai = 0; ai < step.getAlses().size(); ai++) {
                if (step.getType().isKrakenFish() && chainIndex == -1) {
                    continue;
                }
                if (chainIndex != -1 && ! alsToShow.contains(ai)) {
                    continue;
                }
                AlsInSolutionStep als = step.getAlses().get(ai);
                for (int i = 0; i < als.indices.size(); i++) {
                    int index = als.indices.get(i);
                    short[] cands = sudoku.getCell(index).getAllCandidates();
                    for (int j = 0; j < cands.length; j++) {
                        points.add(getChainPoint(index, cands[j], cellSize, dcx, dcy, ddx, ddy));
                    }
                }
            }
            // dann zeichnen
            //for (Chain chain : step.getChains()) {
            for (int ci = 0; ci < step.getChainAnz(); ci++) {
                if (step.getType().isKrakenFish() && chainIndex == -1) {
                    continue;
                }
                if (chainIndex != -1 && ci != chainIndex) {
                    continue;
                }
                Chain chain = step.getChains().get(ci);
                drawChain(g2, chain, cellSize, dcx, dcy, ddx, ddy);
            }
        }
    }

    private Point getChainPoint(int index, int cand, int cellSize, int dcx, int dcy, int ddx, int ddy) {
        return new Point(getCandKoord(index, cand, cellSize, dcx, dcy, ddx, ddy, true),
                getCandKoord(index, cand, cellSize, dcx, dcy, ddx, ddy, false));
    }

    /**
     * Zuerst eine Liste mit den Koordinaten aller Knoten der Chain erstellen. Dann wird
     * gezeichnet:
     *   - Koordinaten der Endpunkte berechnen
     *   - Prfen, ob ein anderer Knoten auf der Strecke liegt
     *   - Wenn ja, mit Bezier-Kurve zeichnen (Tangenten 45 Grad zur Gerade)
     *       - Richtung zunchst einmal immer oben/links
     *   - Zu kurze Strecken nicht bercksichtigen
     */
    private void drawChain(Graphics2D g2, Chain chain, int cellSize,
            int dcx, int dcy, int ddx, int ddy) {
        //System.out.println("Chain: " + chain.start + "/" + chain.end + "/" + chain.chain);
        int[] ch = chain.chain;
        //List<Point> points1 = new ArrayList<Point>(chain.end - chain.start + 1);
        List<Point> points1 = new ArrayList<Point>(chain.end + 1);
        //for (int i = chain.start; i <= chain.end; i++) {
        for (int i = 0; i <= chain.end; i++) {
            if (i < chain.start) {
                points1.add(null);
                continue;
            }
            int che = Math.abs(ch[i]);
            points1.add(new Point(getCandKoord(Chain.getSCellIndex(che), Chain.getSCandidate(che), cellSize, dcx, dcy, ddx, ddy, true),
                    getCandKoord(Chain.getSCellIndex(che), Chain.getSCandidate(che), cellSize, dcx, dcy, ddx, ddy, false)));
        }
        Stroke oldStroke = g2.getStroke();
        int oldChe = 0;
        int oldIndex = 0;
        int index = 0;
        for (int i = chain.start; i < chain.end; i++) {
            // nur zeichnen, wenn zwischen zwei verschiedenen Zellen
            if (ch[i + 1] == Integer.MIN_VALUE) {
                // Trennmarker -> ignorieren
                continue;
            }
            index = i;
            int che = Math.abs(ch[i]);
            int che1 = Math.abs(ch[i + 1]);
            if (ch[i] > 0 && ch[i + 1] < 0) {
                oldChe = che;
                oldIndex = i;
            }
            if (ch[i] == Integer.MIN_VALUE && ch[i + 1] < 0) {
                che = oldChe;
                index = oldIndex;
            }
            if (ch[i] < 0 && ch[i + 1] > 0) {
                che = oldChe;
                index = oldIndex;
            }
            if (Chain.getSCellIndex(che) == Chain.getSCellIndex(che1)) {
                continue;
            }
            g2.setColor(Options.getInstance().getArrowColor());
            if (Chain.isSStrong(che1)) {
                g2.setStroke(strongLinkStroke);
            } else {
                g2.setStroke(weakLinkStroke);
            }
            drawArrow(g2, index, Chain.getSCandidate(che), i + 1, Chain.getSCandidate(che1), cellSize, dcx, dcy, ddx, ddy, points1);
        }
        g2.setStroke(oldStroke);

    }

    private void drawArrow(Graphics2D g2, int index1, int cand1, int index2, int cand2, int cellSize,
            int dcx, int dcy, int ddx, int ddy, List<Point> points1) {
        // Start- und Endpunkte des Pfeils berechnen
        Point p1 = new Point(points1.get(index1));
        Point p2 = new Point(points1.get(index2));
        int length = (int) Math.sqrt((p2.x - p1.x) * (p2.x - p1.x) + (p2.y - p1.y) * (p2.y - p1.y));
        double deltaX = p2.x - p1.x;
        double deltaY = p2.y - p1.y;
        // Quadranten-Anpassung nicht vergessen
        int quadrant = getQuadrant(deltaX, deltaY);
        double alpha = getAlpha(deltaX, deltaY, quadrant);
        adjustEndPoints(p1, p2, alpha, quadrant, ddy);

        // Prfen, ob ein anderer Kandidat auf der direkten Linie liegt
        double epsilon = 0.1;
        double dx1 = p2.x - p1.x;
        double dy1 = p2.y - p1.y;
        boolean doesIntersect = false;
        for (int i = 0; i < points.size(); i++) {
            if (points.get(i).equals(points1.get(index1)) || points.get(i).equals(points1.get(index2))) {
                continue;
            }
            Point point = points.get(i);
            double dx2 = point.x - p1.x;
            double dy2 = point.y - p1.y;
            // Kontrolle mit hnlichkeitssatz
            if (Math.signum(dx1) == Math.signum(dx2) && Math.signum(dy1) == Math.signum(dy2) &&
                    Math.abs(dx2) <= Math.abs(dx1) && Math.abs(dy2) <= Math.abs(dy1)) {
                // Punkt knnte auf der Geraden liegen
                if (dx1 == 0.0 || dy1 == 0.0 || Math.abs(dx1 / dy1 - dx2 / dy2) < epsilon) {
                    // Punkt liegt auf der Geraden
                    doesIntersect = true;
                    break;
                }
            }
        }
        if (length < 2 * ddy) {
            doesIntersect = true;
        }

        // Werte fr Pfeilspitze vorbereiten
        double aAlpha = alpha;
        int aQuadrant = quadrant;

        // Grundlinie zeichnen
        if (doesIntersect) {
            int bezierLength = 20;
            // Wenn die Punkte zu nahe beieinander sind, geht die Ausgangspunktberechnung schief
            if (length < 2 * ddy) {
                bezierLength = length / 4;
            }
            // Die Endpunkte mssen um 45 Grad gedreht werden: beim Startpunkt gegen den
            // Uhrzeigersinn, beim Endpunkt im Uhrzeigersinn
            rotatePoint(points1.get(index1), p1, -Math.PI / 4.0);
            rotatePoint(points1.get(index2), p2, Math.PI / 4.0);

            aAlpha = alpha - Math.PI / 4.0;
            if (quadrant == 2) {
                aAlpha = alpha + Math.PI / 4.0;
            }
            int bX1 = (int) (p1.x + bezierLength * Math.cos(aAlpha));
            int bY1 = (int) (p1.y + bezierLength * Math.sin(aAlpha));
            aAlpha = alpha + Math.PI / 4.0;
            if (quadrant == 2) {
                aAlpha = 5 * Math.PI / 4.0 - alpha;
            }
            int bX2 = (int) (p2.x - bezierLength * Math.cos(aAlpha));
            int bY2 = (int) (p2.y - bezierLength * Math.sin(aAlpha));
            if (quadrant == 2) {
                bY2 = (int) (p2.y + bezierLength * Math.sin(aAlpha));
            }
            cubicCurve.setCurve(p1.x, p1.y, bX1, bY1, bX2, bY2, p2.x, p2.y);
            g2.draw(cubicCurve);

        } else {
            g2.drawLine(p1.x, p1.y, p2.x, p2.y);
        }

        // Pfeilspitzen zeichnen
        g2.setStroke(arrowStroke);
        double arrowLength = cellSize * arrowLengthFactor;
        double arrowHeight = arrowLength * arrowHeightFactor;
        if (length > (arrowLength * 2 + ddy)) {
            if (doesIntersect) {
                double angleKorr = (length - 40.0) / 30.0 * 0.2;
                if (angleKorr > 0.3) {
                    angleKorr = 0.3;
                }
                if (quadrant == 2) {
                    aAlpha += angleKorr;
                } else {
                    aAlpha -= angleKorr;
                }
            }
            // Pfeilspitzen zeichnen
            double sin = Math.sin(aAlpha);
            double cos = Math.cos(aAlpha);
            if (aQuadrant == 2) {
                sin = -sin;
            }
            int aX = p2.x - (int) (cos * arrowLength);
            int aY = p2.y - (int) (sin * arrowLength);
            int daX = (int) (sin * arrowHeight);
            int daY = (int) (cos * arrowHeight);
            arrow.reset();
            arrow.addPoint(aX - daX, aY + daY);
            arrow.addPoint(p2.x, p2.y);
            arrow.addPoint(aX + daX, aY - daY);
            g2.fill(arrow);
            g2.draw(arrow);
        }
    }

    /**
     * p2 wird um angle Grad um p1 gedreht
     */
    private void rotatePoint(Point p1, Point p2, double angle) {
        // in den Nullpunkt verschieben
        p2.x -= p1.x;
        p2.y -= p1.y;

        // um angle rotieren
        double sinAngle = Math.sin(angle);
        double cosAngle = Math.cos(angle);
        double xact = p2.x;
        double yact = p2.y;
        p2.x = (int) (xact * cosAngle - yact * sinAngle);
        p2.y = (int) (xact * sinAngle + yact * cosAngle);

        // und zurckschieben
        p2.x += p1.x;
        p2.y += p1.y;
    }

    private void adjustEndPoints(Point p1, Point p2, double alpha, int quadrant, int ddy) {
        double tmpDelta = ddy / 2 + 4;
        int pX = (int) (tmpDelta * Math.cos(alpha));
        int pY = (int) (tmpDelta * Math.sin(alpha));
        if (quadrant == 2) {
            pY = -pY;
        }
        p1.x += pX;
        p1.y += pY;
        p2.x -= pX;
        p2.y -= pY;
    }

    private double getAlpha(double deltaX, double deltaY, int quadrant) {
        double alpha = Math.atan(deltaY / deltaX);
        // Quadranten-Anpassung nicht vergessen
        if (quadrant == 2) {
            alpha = Math.PI - alpha;
        } else if (quadrant == 3) {
            alpha += Math.PI;
        }
        return alpha;
    }

    private int getQuadrant(double deltaX, double deltaY) {
        int quadrant = 1;
        if (deltaX < 0.0) {
            if (deltaY < 0.0) {
                quadrant = 2;
            } else {
                quadrant = 3;
            }
        } else {
            if (deltaY > 0.0) {
                quadrant = 4;
            }
        }
        return quadrant;
    }

    private int getCandKoord(int index, int cand, int cellSize, int dcx, int dcy, int ddx, int ddy, boolean isX) {
        int third = cellSize / 3;
        int startX = getX(Sudoku.getLine(index), Sudoku.getCol(index));
        int startY = getY(Sudoku.getLine(index), Sudoku.getCol(index));
        int shiftX = ((cand - 1) % 3) * third;
        int shiftY = ((cand - 1) / 3) * third;
        if (isX) {
            return startX + shiftX + dcx - 2 * (ddy - ddx) / 3 + ddy / 2;
        } else {
            return startY + shiftY + dcy - 4 * ddy / 5 - 1 + ddy / 2;
        }
    }

    private int getX(int line, int col) {
        int x = col * cellSize + delta + startSX;
        if (col > 2) {
            x += delta;
        }
        if (col > 5) {
            x += delta;
        }
        return x;
    }

    private int getY(int line, int col) {
        int y = line * cellSize + delta + startSY;
        if (line > 2) {
            y += delta;
        }
        if (line > 5) {
            y += delta;
        }
        return y;
    }

    private int getLine(Point p) {
        double tmp = p.y - startSY - delta;
        if ((tmp >= 3 * cellSize && tmp <= 3 * cellSize + delta) ||
                (tmp >= 6 * cellSize + delta && tmp <= 6 * cellSize + 2 * delta)) {
            return -1;
        }
        if (tmp > 3 * cellSize) {
            tmp -= delta;
        }
        if (tmp > 6 * cellSize) {
            tmp -= delta;
        }
        return (int) Math.ceil((tmp / cellSize) - 1);
    }

    private int getCol(Point p) {
        double tmp = p.x - startSX - delta;
        if ((tmp >= 3 * cellSize && tmp <= 3 * cellSize + delta) ||
                (tmp >= 6 * cellSize + delta && tmp <= 6 * cellSize + 2 * delta)) {
            return -1;
        }
        if (tmp > 3 * cellSize) {
            tmp -= delta;
        }
        if (tmp > 6 * cellSize) {
            tmp -= delta;
        }
        return (int) Math.ceil((tmp / cellSize) - 1);
    }

    /**
     * Checks whether a candidate has been clicked. The correct values
     * for font metrics and candidate factors are ignored: the valid
     * candidate region is simple the corresponding ninth of the cell.
     * To adjust for the ignored values the "clickable region" of a candidate
     * is reduced by a sixth in every direction.
     *
     * @param p The point of a mouse click
     * @param line The line, in which p lies (may be -1 for "invalid")
     * @param col The column, in which p lies (may be -1 for "invalid")
     * @return The number of a candidate, if a click could be confirmed, or else -1
     */
    private int getCandidate(Point p, int line, int col) {
        // check if a cell was clicked
        if (line < 0 || col < 0) {
            // clicked between cells -> cant mean a candidate
            return -1;
        }
        // calculate the coordinates of the left upper corner of the cell
        //System.out.println("startSX = " + startSX + ", startSY = " + startSY + ", cellSize = " + cellSize + ", delta = " + delta);
        double startX = startSX + col * cellSize;
        if (col > 2) {
            startX += delta;
        }
        if (col > 5) {
            startX += delta;
        }
        double startY = startSY + line * cellSize;
        if (line > 2) {
            startY += delta;
        }
        if (line > 5) {
            startY += delta;
        }
        // now check if a candidate was clicked
        int candidate = -1;
        double cs3 = cellSize / 3.0;
        double dx = cs3 * 2.0 / 3.0;
        double leftDx = cs3 / 6.0;
        //System.out.println("p = " + p + ", startX = " + startX + ", startY = " + startY + ", dx = " + dx + ", leftDX = " + leftDx);
        for (int i = 0; i < 3; i++) {
            for (int j = 0; j < 3; j++) {
                double sx = startX + i * cs3 + leftDx;
                double sy = startY + j * cs3 + leftDx;
                //System.out.println("cand = " + (j * 3 + i + 1) + ", sx = " + sx + ", sy = " + sy);
                if (p.x >= sx && p.x <= sx + dx &&
                        p.y >= sy && p.y <= sy + dx) {
                    // canddiate was clicked
                    candidate = j * 3 + i + 1;
                    //System.out.println("Candidate clicked: " + candidate);
                    return candidate;
                }
            }
        }
        return -1;
    }

    public void setActiveColor(int colorNumber) {
        aktColorIndex = colorNumber;
        if (aktColorIndex < 0) {
            // reset everything to normal
            if (oldCursor != null) {
                setCursor(oldCursor);
                colorCursor = null;
                colorCursorShift = null;
            }
        } else {
            // create new Cursors and set them
            if (oldCursor == null) {
                oldCursor = getCursor();
            }
            createColorCursors();
            setCursor(colorCursor);
        }
        // no region selectes are allowed in coloring
        clearRegion();
        updateCellZoomPanel();
    }

    public int getActiveColor() {
        return aktColorIndex;
    }

    public void resetActiveColor() {
        int temp = aktColorIndex;
        setActiveColor(-1);
        setActiveColor(temp);
    }

    private void drawBlockLine(int x, int y, boolean withRect) {
        drawBlock(x, y, withRect);
        drawBlock(x + 3 * cellSize + delta, y, withRect);
        drawBlock(x + 6 * cellSize + 2 * delta, y, withRect);
    }

    private void drawBlock(int x, int y, boolean withRect) {
        if (withRect) {
            g2.drawRect(x, y, 3 * cellSize, 3 * cellSize);
        }
        g2.drawLine(x, y + 1 * cellSize, x + 3 * cellSize, y + 1 * cellSize);
        g2.drawLine(x, y + 2 * cellSize, x + 3 * cellSize, y + 2 * cellSize);
        g2.drawLine(x + 1 * cellSize, y, x + 1 * cellSize, y + 3 * cellSize);
        g2.drawLine(x + 2 * cellSize, y, x + 2 * cellSize, y + 3 * cellSize);
    }
    // Variables declaration - do not modify//GEN-BEGIN:variables
    private javax.swing.JPopupMenu cellPopupMenu;
    private javax.swing.JMenuItem color1aMenuItem;
    private javax.swing.JMenuItem color1bMenuItem;
    private javax.swing.JMenuItem color2aMenuItem;
    private javax.swing.JMenuItem color2bMenuItem;
    private javax.swing.JMenuItem color3aMenuItem;
    private javax.swing.JMenuItem color3bMenuItem;
    private javax.swing.JMenuItem color4aMenuItem;
    private javax.swing.JMenuItem color4bMenuItem;
    private javax.swing.JMenuItem color5aMenuItem;
    private javax.swing.JMenuItem color5bMenuItem;
    private javax.swing.JMenuItem exclude1MenuItem;
    private javax.swing.JMenuItem exclude2MenuItem;
    private javax.swing.JMenuItem exclude3MenuItem;
    private javax.swing.JMenuItem exclude4MenuItem;
    private javax.swing.JMenuItem exclude5MenuItem;
    private javax.swing.JMenuItem exclude6MenuItem;
    private javax.swing.JMenuItem exclude7MenuItem;
    private javax.swing.JMenuItem exclude8MenuItem;
    private javax.swing.JMenuItem exclude9MenuItem;
    private javax.swing.JMenuItem excludeSeveralMenuItem;
    private javax.swing.JSeparator jSeparator1;
    private javax.swing.JSeparator jSeparator2;
    private javax.swing.JMenuItem make1MenuItem;
    private javax.swing.JMenuItem make2MenuItem;
    private javax.swing.JMenuItem make3MenuItem;
    private javax.swing.JMenuItem make4MenuItem;
    private javax.swing.JMenuItem make5MenuItem;
    private javax.swing.JMenuItem make6MenuItem;
    private javax.swing.JMenuItem make7MenuItem;
    private javax.swing.JMenuItem make8MenuItem;
    private javax.swing.JMenuItem make9MenuItem;
    // End of variables declaration//GEN-END:variables

    public Sudoku getSudoku() {
        return sudoku;
    }

    public Sudoku getSolvedSudoku() {
        return solvedSudoku;
    }

    public int getCandidateMode() {
        return candidateMode;
    }

    public boolean isShowCandidates() {
        return showCandidates;
    }

    public void setShowCandidates(boolean showCandidates) {
        this.showCandidates = showCandidates;
        if (showCandidates) {
            candidateMode = SudokuCell.PLAY;
        } else {
            candidateMode = SudokuCell.USER;
        }
        repaint();
    }

    public boolean isShowWrongValues() {
        return showWrongValues;
    }

    public void setShowWrongValues(boolean showWrongValues) {
        this.showWrongValues = showWrongValues;
        repaint();
    }

    public boolean undoPossible() {
        //System.out.println("undoStack: " + undoStack + "/" + undoStack.size());
        return undoStack.size() > 0;
    }

    public boolean redoPossible() {
        //System.out.println("redoStack: " + redoStack + "/" + redoStack.size());
        return redoStack.size() > 0;
    }

    public void undo() {
        if (undoPossible()) {
            redoStack.push(sudoku);
            sudoku = undoStack.pop();
            updateCellZoomPanel();
            mainFrame.check();
            repaint();
        }
    }

    public void redo() {
        if (redoPossible()) {
            undoStack.push(sudoku);
            sudoku = redoStack.pop();
            updateCellZoomPanel();
            mainFrame.check();
            repaint();
        }
    }

    /**
     * Clears undo/redo. Is called from {@link SolutionPanel} when a step
     * is double clicked.
     */
    public void clearUndoRedo() {
        undoStack.clear();
        redoStack.clear();
    }

    public void setSudoku(Sudoku newSudoku) {
        setSudoku(newSudoku.getSudoku(), false);
    }

    public void setSudoku(Sudoku newSudoku, boolean alreadySolved) {
        setSudoku(newSudoku.getSudoku(), alreadySolved);
    }

    public void setSudoku(String init) {
        setSudoku(init, false);
    }

    public void setSudoku(String init, boolean alreadySolved) {
        step = null;
        setChainInStep(-1);
        undoStack.clear();
        redoStack.clear();
        coloringMap.clear();
        if (init == null || init.length() == 0) {
            sudoku = new Sudoku();
            solvedSudoku = new Sudoku();
        } else {
            sudoku.setSudoku(init);
            // the sudoku must be set in the solver to reset the step list
            // (otherwise the result panels are not updated correctly)
            sudoku.setLevel(Options.getInstance().getDifficultyLevels()[DifficultyType.EASY.ordinal()]);
            sudoku.setScore(0);
            Sudoku tmpSudoku = sudoku.clone();
            if (! alreadySolved) {
                getSolver().setSudoku(tmpSudoku);
            }
            solvedSudoku = sudoku.clone();
            boolean unique = true;
            boolean sudokuCompleted = solvedSudoku.isSolved();
            if (! sudokuCompleted) {
                unique = creator.validSolution(solvedSudoku);
            }
            if (!unique) {
                JOptionPane.showMessageDialog(this,
                        java.util.ResourceBundle.getBundle("intl/SudokuPanel").getString("SudokuPanel.multiple_solutions"),
                        java.util.ResourceBundle.getBundle("intl/SudokuPanel").getString("SudokuPanel.invalid_puzzle"),
                        JOptionPane.ERROR_MESSAGE);
            } else {
                if (! sudokuCompleted) {
                    solvedSudoku = creator.getSolvedSudoku().clone();
                }
                if (!sudoku.checkSudoku(solvedSudoku)) {
                    JOptionPane.showMessageDialog(this,
                            java.util.ResourceBundle.getBundle("intl/SudokuPanel").getString("SudokuPanel.wrong_values"),
                            java.util.ResourceBundle.getBundle("intl/SudokuPanel").getString("SudokuPanel.invalid_puzzle"),
                            JOptionPane.ERROR_MESSAGE);
                } else {
                    if (!alreadySolved) {
                        //Sudoku tmpSudoku = sudoku.clone();
                        //getSolver().setSudoku(tmpSudoku);
                        getSolver().solve(true);
                    }
//                sudoku.setLevel(tmpSudoku.getLevel());
//                sudoku.setScore(tmpSudoku.getScore());
                    sudoku.setLevel(getSolver().getSudoku().getLevel());
                    sudoku.setScore(getSolver().getSudoku().getScore());
                }
            }
        }
        updateCellZoomPanel();
        if (mainFrame != null) {
            mainFrame.check();
        }
        repaint();
    }

    public String getSudokuString(ClipboardMode mode) {
        return sudoku.getSudoku(mode, step);
    }

    public SudokuSolver getSolver() {
        return solver;
    }

    public SolutionStep getNextStep(boolean singlesOnly) {
        step = getSolver().getHint(sudoku, singlesOnly);
        setChainInStep(-1);
        repaint();
        return step;
    }

    public void setStep(SolutionStep step) {
        this.step = step;
        setChainInStep(-1);
        repaint();
    }

    public SolutionStep getStep() {
        return step;
    }

    public void setChainInStep(int chainIndex) {
        if (step == null) {
            chainIndex = -1;
        } else if (step.getType().isKrakenFish() && chainIndex > -1) {
            chainIndex--;
        }
        if (chainIndex >= 0 && chainIndex > step.getChainAnz() - 1) {
            chainIndex = -1;
        }
        //System.out.println("chainIndex = " + chainIndex);
        this.chainIndex = chainIndex;
        alsToShow.clear();
        if (chainIndex != -1) {
            Chain chain = step.getChains().get(chainIndex);
            for (int i = chain.start; i <= chain.end; i++) {
                if (chain.getNodeType(i) == Chain.ALS_NODE) {
                    alsToShow.add(Chain.getSAlsIndex(chain.chain[i]));
                }
            }
        }
//        StringBuffer tmp = new StringBuffer();
//        tmp.append("setChainInStep(" + chainIndex + "): ");
//        for (int i = 0; i < alsToShow.size(); i++) {
//            tmp.append(alsToShow.get(i) + " ");
//        }
//        System.out.println(tmp);
        repaint();
    }

    public void doStep() {
        if (step != null) {
            undoStack.push(sudoku.clone());
            redoStack.clear();
            getSolver().doStep(sudoku, step);
            step = null;
            setChainInStep(-1);
            updateCellZoomPanel();
            mainFrame.check();
            repaint();
        }
    }

    public void abortStep() {
        step = null;
        setChainInStep(-1);
        repaint();
    }

    public int getAnzFilled() {
        return sudoku.getAnzFilled();
    }

    public void setNoClues() {
        sudoku.setNoClues();
        repaint();
    }

    public boolean isInvalidCells() {
        return invalidCells;
    }

    public void setInvalidCells(boolean invalidCells) {
        this.invalidCells = invalidCells;
    }

    public boolean isShowInvalidOrPossibleCells() {
        return showInvalidOrPossibleCells;
    }

    public void setShowInvalidOrPossibleCells(boolean showInvalidOrPossibleCells) {
        this.showInvalidOrPossibleCells = showInvalidOrPossibleCells;
    }

    public int getShowHintCellValue() {
        return showHintCellValue;
    }

    public void setShowHintCellValue(int showHintCellValue) {
        this.showHintCellValue = showHintCellValue;
    }

    public boolean isShowDeviations() {
        return showDeviations;
    }

    public void setShowDeviations(boolean showDeviations) {
        this.showDeviations = showDeviations;
        mainFrame.check();
        repaint();
    }

    /**
     * Schreibt ein BufferedImage in eine PNG-Datei. Dabei wird die Auflsung
     * in die Metadaten der Datei geschrieben, was alles etwas kompliziert
     * macht.
     * @param bi Zu zeichnendes Bild
     * @param dpi Auflsung in dots per inch
     * @param fileName Pfad und Name der neuen Bilddatei
     */
    private void writePNG(BufferedImage bi, int dpi, File file) {
        Iterator i = ImageIO.getImageWritersByFormatName("png");
        //are there any jpeg encoders available?

        if (i.hasNext()) //there's at least one ImageWriter, just use the first one
        {
            ImageWriter imageWriter = (ImageWriter) i.next();
            //get the param
            ImageWriteParam param = imageWriter.getDefaultWriteParam();
            ImageTypeSpecifier its = new ImageTypeSpecifier(bi.getColorModel(), bi.getSampleModel());

            //get metadata
            IIOMetadata iomd = imageWriter.getDefaultImageMetadata(its, param);

            String formatName = "javax_imageio_png_1.0";//this is the DOCTYPE of the metadata we need

            Node node = iomd.getAsTree(formatName);

            // standardmig ist nur IHDR gesetzt, pHYs dazufgen
            int dpiRes = (int) (dpi / 2.54 * 100);
            IIOMetadataNode res = new IIOMetadataNode("pHYs");
            res.setAttribute("pixelsPerUnitXAxis", String.valueOf(dpiRes));
            res.setAttribute("pixelsPerUnitYAxis", String.valueOf(dpiRes));
            res.setAttribute("unitSpecifier", "meter");
            node.appendChild(res);

            try {
                iomd.setFromTree(formatName, node);
            } catch (IIOInvalidTreeException e) {
                JOptionPane.showMessageDialog(this, e.getLocalizedMessage(),
                        java.util.ResourceBundle.getBundle("intl/SudokuPanel").getString("SudokuPanel.error"),
                        JOptionPane.ERROR_MESSAGE);
            }
            //attach the metadata to an image
            IIOImage iioimage = new IIOImage(bi, null, iomd);
            try {
                FileImageOutputStream out = new FileImageOutputStream(file);
                imageWriter.setOutput(out);
                imageWriter.write(iioimage);
                out.close();
                
                String companionFileName = file.getPath();
                if (companionFileName.toLowerCase().endsWith(".png")) {
                    companionFileName = companionFileName.substring(0, companionFileName.length() - 4);
                }
                companionFileName += ".txt";
                PrintWriter cOut = new PrintWriter(new BufferedWriter(new FileWriter(companionFileName)));
                cOut.println(getSudokuString(ClipboardMode.CLUES_ONLY));
                cOut.println(getSudokuString(ClipboardMode.LIBRARY));
                cOut.println(getSudokuString(ClipboardMode.PM_GRID));
                if (step != null) {
                    cOut.println(getSudokuString(ClipboardMode.PM_GRID_WITH_STEP));
                }
                cOut.close();
            } catch (IOException e) {
                JOptionPane.showMessageDialog(this, e.getLocalizedMessage(),
                        java.util.ResourceBundle.getBundle("intl/SudokuPanel").getString("SudokuPanel.error"),
                        JOptionPane.ERROR_MESSAGE);
            }
        }
    }

    /**
     * @return the colorCells
     */
    public boolean isColorCells() {
        return colorCells;
    }

    /**
     * @param colorCells the colorCells to set
     */
    public void setColorCells(boolean colorCells) {
        this.colorCells = colorCells;
        updateCellZoomPanel();
    }

    /**
     * Creates cursors for coloring: The color is specified by {@link #aktColorIndex},
     * cursors for both colors of the pair are created and stored in {@link #colorCursor}
     * and {@link #colorCursorShift}.
     */
    private void createColorCursors() {
        try {
            Point cursorHotSpot = new Point(2, 4);
            BufferedImage img1 = ImageIO.read(getClass().getResource("/img/c_color.png"));
            Graphics2D gImg1 = (Graphics2D) img1.getGraphics();
            gImg1.setColor(Options.getInstance().coloringColors[aktColorIndex]);
            gImg1.fillRect(19, 18, 12, 12);
            //System.out.println(aktColorIndex + "/" + Options.getInstance().coloringColors[aktColorIndex]);
            colorCursor = Toolkit.getDefaultToolkit().createCustomCursor(img1, cursorHotSpot, "c_strong");
            
            BufferedImage img2 = ImageIO.read(getClass().getResource("/img/c_color.png"));
            Graphics2D gImg2 = (Graphics2D) img2.getGraphics();
            if (aktColorIndex % 2 == 0) {
                gImg2.setColor(Options.getInstance().coloringColors[aktColorIndex + 1]);
            } else {
                gImg2.setColor(Options.getInstance().coloringColors[aktColorIndex - 1]);
            }
            //System.out.println(aktColorIndex + "/" + Options.getInstance().coloringColors[aktColorIndex + 1]);
            gImg2.fillRect(19, 18, 12, 12);
            colorCursorShift = Toolkit.getDefaultToolkit().createCustomCursor(img2, cursorHotSpot, "c_weak");
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }

    /**
     * Checks whether the candidate in the given cell is a Hidden Single.
     * 
     * @param candidate
     * @param line
     * @param col
     * @return
     */
    private boolean isHiddenSingle(int candidate, int line, int col) {
        SimpleSolver ss = (SimpleSolver) solver.getSpecialisedSolver(SimpleSolver.class);
        List<SolutionStep> steps = ss.findAllHiddenXle(sudoku, 1, true);
        for (SolutionStep act : steps) {
            if (act.getType() == SolutionType.HIDDEN_SINGLE && act.getValues().get(0) == candidate &&
                    act.getIndices().get(0) == Sudoku.getIndex(line, col)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Sets all the color icons in the popup menu.
     */
    public void setColorIconsInPopupMenu() {
        setColorIconInPopupMenu(color1aMenuItem, Options.getInstance().coloringColors[0]);
        setColorIconInPopupMenu(color1bMenuItem, Options.getInstance().coloringColors[1]);
        setColorIconInPopupMenu(color2aMenuItem, Options.getInstance().coloringColors[2]);
        setColorIconInPopupMenu(color2bMenuItem, Options.getInstance().coloringColors[3]);
        setColorIconInPopupMenu(color3aMenuItem, Options.getInstance().coloringColors[4]);
        setColorIconInPopupMenu(color3bMenuItem, Options.getInstance().coloringColors[5]);
        setColorIconInPopupMenu(color4aMenuItem, Options.getInstance().coloringColors[6]);
        setColorIconInPopupMenu(color4bMenuItem, Options.getInstance().coloringColors[7]);
        setColorIconInPopupMenu(color5aMenuItem, Options.getInstance().coloringColors[8]);
        setColorIconInPopupMenu(color5bMenuItem, Options.getInstance().coloringColors[9]);
    }

    /**
     * Creates an icon (rectangle showing color) and sets it on the MenuItem.
     * @param item
     * @param color
     */
    private void setColorIconInPopupMenu(JMenuItem item, Color color) {
        try {
            BufferedImage img = ImageIO.read(getClass().getResource("/img/c_icon.png"));
            Graphics2D gImg = (Graphics2D) img.getGraphics();
            gImg.setColor(color);
            gImg.fillRect(1, 1, 12, 12);
            item.setIcon(new ImageIcon(img));
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }

    /**
     * Collects the intersection or union of all valid candidates in all
     * selected cells. Used to adjust the popup menu.
     * @param intersection
     * @return
     */
    private SudokuSet collectCandidates(boolean intersection) {
        SudokuSet resultSet = new SudokuSet();
        SudokuSet tmpSet = new SudokuSet();
        if (intersection) {
            resultSet.setAll();
        }
        if (selectedCells.isEmpty()) {
            if (sudoku.getCell(aktLine, aktCol).getValue() == 0) {
                // get candidates only when cell is not set!
                sudoku.getCell(aktLine, aktCol).getCandidateSet(tmpSet, candidateMode);
                if (intersection) {
                    resultSet.and(tmpSet);
                } else {
                    resultSet.or(tmpSet);
                }
            }
        } else {
            for (int index : selectedCells) {
                if (sudoku.getCell(index).getValue() == 0) {
                    // get candidates only when cell is not set!
                    sudoku.getCell(index).getCandidateSet(tmpSet, candidateMode);
                    if (intersection) {
                        resultSet.and(tmpSet);
                    } else {
                        resultSet.or(tmpSet);
                    }
                }
            }
        }
        return resultSet;
    }

    /**
     * Brings up the popup menu for the cell at line/col. If the cell is
     * already set, no menu is displayed. For every other cell the contents
     * of the menu is restricted to sensible actions.<br>
     * If a region of cells is selected, "Make x" is restricted to
     * candidates, that appear in all cells, "Exclude x" is restricted
     * to the combined set of candidates in all cells.
     * @param line
     * @param col
     */
    private void showPopupMenu(int line, int col) {
        jSeparator2.setVisible(true);
        SudokuCell cell = sudoku.getCell(line, col);
        if (cell.getValue() != 0 && selectedCells.isEmpty()) {
            // cell is already set -> no popup!
            return;
        }
        if (selectedCells.isEmpty()) {
            aktLine = line;
            aktCol = col;
        }
        excludeSeveralMenuItem.setVisible(false);
        for (int i = 1; i <= 9; i++) {
            makeItems[i - 1].setVisible(false);
            excludeItems[i - 1].setVisible(false);
        }
        SudokuSet candSet = collectCandidates(true);
        for (int i = 0; i < candSet.size(); i++) {
            makeItems[candSet.get(i) - 1].setVisible(true);
        }
        candSet = collectCandidates(false);
        if (candSet.size() > 1) {
            if (candSet.size() > 2) {
                excludeSeveralMenuItem.setVisible(true);
            }
            for (int i = 0; i < candSet.size(); i++) {
                excludeItems[candSet.get(i) - 1].setVisible(true);
            }
        } else {
            jSeparator2.setVisible(false);
        }
        cellPopupMenu.show(this, getX(line, col) + cellSize, getY(line, col));
    }

    /**
     * Handles activation of a "Make x" menu item. The selected number is
     * set in all selected cells (if they are not already set).
     * @param menuItem
     */
    private void popupSetCell(JMenuItem menuItem) {
        int candidate = -1;
        for (int i = 0; i < makeItems.length; i++) {
            if (makeItems[i] == menuItem) {
                candidate = i + 1;
                break;
            }
        }
        if (candidate != -1) {
            undoStack.push(sudoku.clone());
            boolean changed = false;
            if (selectedCells.isEmpty()) {
                if (sudoku.getCell(aktLine, aktCol).getValue() == 0) {
                    setCell(aktLine, aktCol, candidate);
                    changed = true;
                }
            } else {
                for (int index : selectedCells) {
                    if (sudoku.getCell(index).getValue() == 0) {
                        setCell(Sudoku.getLine(index), Sudoku.getCol(index), candidate);
                        changed = true;
                    }
                }
            }
            if (changed) {
                redoStack.clear();
            } else {
                undoStack.pop();
            }
            updateCellZoomPanel();
            mainFrame.fixFocus();
            repaint();
        }
    }

    /**
     * Removes the candidate from all selected cells.
     * @param candidate
     * @return true if sudoku is changed, false otherwise
     */
    private boolean removeCandidateFromActiveCells(int candidate) {
        boolean changed = false;
        if (selectedCells.isEmpty()) {
            SudokuCell cell = sudoku.getCell(aktLine, aktCol);
            if (cell.getValue() == 0 && cell.isCandidate(candidateMode, candidate)) {
                sudoku.setCandidate(aktLine, aktCol, candidateMode, candidate, false);
                changed = true;
            }
        } else {
            for (int index : selectedCells) {
                SudokuCell cell = sudoku.getCell(index);
                if (cell.getValue() == 0 && cell.isCandidate(candidateMode, candidate)) {
                    sudoku.setCandidate(index, candidateMode, candidate, false);
                    changed = true;
                }
            }
        }
        return changed;
    }

    /**
     * Handles candidate changed done in {@link CellZoomPanel}. Should not be
     * used otherwise.
     * @param candidate
     */
    public void toggleOrRemoveCandidateFromCellZoomPanel(int candidate) {
        if (candidate != -1) {
            undoStack.push(sudoku.clone());
            boolean changed = false;
            if (selectedCells.isEmpty()) {
                SudokuCell cell = sudoku.getCell(aktLine, aktCol);
                if (cell.isCandidate(candidateMode, candidate)) {
                    sudoku.setCandidate(aktLine, aktCol, candidateMode, candidate, false);
                } else {
                    sudoku.setCandidate(aktLine, aktCol, candidateMode, candidate, true);
                }
                changed = true;
            } else {
                changed = removeCandidateFromActiveCells(candidate);
            }
            if (changed) {
                redoStack.clear();
            } else {
                undoStack.pop();
            }
            updateCellZoomPanel();
            mainFrame.check();
            repaint();
        }
    }

    /**
     * Handles activation of an "Exclude x" menu item. The selected number is
     * deleted in all selected cells (if they present).
     * @param menuItem
     */
    private void popupExcludeCandidate(JMenuItem menuItem) {
        int candidate = -1;
        for (int i = 0; i < excludeItems.length; i++) {
            if (excludeItems[i] == menuItem) {
                candidate = i + 1;
                break;
            }
        }
        if (candidate != -1) {
            undoStack.push(sudoku.clone());
            boolean changed = removeCandidateFromActiveCells(candidate);
            if (changed) {
                redoStack.clear();
            } else {
                undoStack.pop();
            }
            updateCellZoomPanel();
            mainFrame.check();
            repaint();
        }
    }

    /**
     * Handles activation of an "Toggle color x" menu item. Th color is set in the
     * cell if not present or deleted if already present.
     * @param menuItem
     */
    private void popupToggleColor(JMenuItem menuItem) {
        int color = -1;
        for (int i = 0; i < toggleColorItems.length; i++) {
            if (toggleColorItems[i] == menuItem) {
                color = i;
                break;
            }
        }
        if (color != -1) {
            //removeCandidateFromActiveCells(color);
            // coloring is active
            handleColoring(aktLine, aktCol, -1, color);
            updateCellZoomPanel();
            mainFrame.check();
            repaint();
        }
    }

    /**
     * @return the cellZoomPanel
     */
    public CellZoomPanel getCellZoomPanel() {
        return cellZoomPanel;
    }

    /**
     * @param cellZoomPanel the cellZoomPanel to set
     */
    public void setCellZoomPanel(CellZoomPanel cellZoomPanel) {
        this.cellZoomPanel = cellZoomPanel;
    }

    /**
     * Update the {@link CellZoomPanel}. FOr more information see
     * {@link CellZoomPanel#update(sudoku.SudokuSet, sudoku.SudokuSet, int, boolean, java.util.SortedMap, java.util.SortedMap) CellZoomPanel.update()}.
     */
    private void updateCellZoomPanel() {
        if (cellZoomPanel != null) {
            SudokuCell cell = sudoku.getCell(aktLine, aktCol);
            boolean singleCell = selectedCells.isEmpty() && cell.getValue() == 0;
            int index = Sudoku.getIndex(aktLine, aktCol);
            if (aktColorIndex == -1) {
                // normal operation -> collect candidates for selected cell(s)
                if (cell.getValue() != 0 && selectedCells.isEmpty()) {
                    // cell is already set -> nothing can be selected
                    cellZoomPanel.update(SudokuSetBase.EMPTY_SET, SudokuSetBase.EMPTY_SET, -1, index, false, singleCell, null, null);
                    return;
                } else {
                    SudokuSet valueSet = collectCandidates(true);
                    SudokuSet candSet = collectCandidates(false);
                    cellZoomPanel.update(valueSet, candSet, -1, index, false, singleCell, null, null);
                    return;
                }
            } else {
                if (! selectedCells.isEmpty() || (selectedCells.isEmpty() && cell.getValue() != 0)) {
                    // no coloring, when set of cells is selected
                    cellZoomPanel.update(SudokuSetBase.EMPTY_SET, SudokuSetBase.EMPTY_SET, aktColorIndex, index, colorCells, singleCell, null, null);
                    return;
                } else  {
                    SudokuSet valueSet = collectCandidates(true);
                    SudokuSet candSet = collectCandidates(false);
                    cellZoomPanel.update(valueSet, candSet, aktColorIndex, index, colorCells, singleCell, coloringMap, coloringCandidateMap);
                    return;
                }
            }
        }
    }

    /**
     * Gets a 81 character string. For every digit in that string, the corresponding cell is set
     * as a given.
     *
     * @param givens
     */
    public void setGivens(String givens) {
        undoStack.push(sudoku.clone());
        sudoku.setGivens(givens);
        updateCellZoomPanel();
        repaint();
        mainFrame.check();
    }
}
java2s.com  | Contact Us | Privacy Policy
Copyright 2009 - 12 Demo Source and Support. All rights reserved.
All other trademarks are property of their respective owners.