diff --git a/src/main/java/featurecat/lizzie/Config.java b/src/main/java/featurecat/lizzie/Config.java index cee1c529b..8cf48c50a 100644 --- a/src/main/java/featurecat/lizzie/Config.java +++ b/src/main/java/featurecat/lizzie/Config.java @@ -1,17 +1,31 @@ package featurecat.lizzie; import featurecat.lizzie.theme.Theme; +import featurecat.lizzie.util.WindowPosition; import java.awt.Color; -import java.io.*; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; import java.nio.file.Files; import java.nio.file.Paths; -import java.util.*; -import javax.swing.*; -import org.json.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.json.JSONTokener; public class Config { public String language = "en"; + public boolean panelUI = false; public boolean showBorder = false; public boolean showMoveNumber = false; public int onlyLastMoveNumber = 0; @@ -160,6 +174,7 @@ public Config() throws IOException { theme = new Theme(uiConfig); + panelUI = uiConfig.optBoolean("panel-ui", false); showBorder = uiConfig.optBoolean("show-border", false); showMoveNumber = uiConfig.getBoolean("show-move-number"); onlyLastMoveNumber = uiConfig.optInt("only-last-move-number"); @@ -185,7 +200,7 @@ public Config() throws IOException { showCoordinates = uiConfig.optBoolean("show-coordinates"); replayBranchIntervalSeconds = uiConfig.optDouble("replay-branch-interval-seconds", 1.0); colorByWinrateInsteadOfVisits = uiConfig.optBoolean("color-by-winrate-instead-of-visits"); - boardPositionProportion = uiConfig.optInt("board-postion-proportion", 4); + boardPositionProportion = uiConfig.optInt("board-position-proportion", 4); winrateStrokeWidth = theme.winrateStrokeWidth(); minimumBlunderBarWidth = theme.minimumBlunderBarWidth(); shadowSize = theme.shadowSize(); @@ -414,6 +429,7 @@ private JSONObject createDefaultConfig() { ui.put("append-winrate-to-comment", false); ui.put("replay-branch-interval-seconds", 1.0); ui.put("gtp-console-style", defaultGtpConsoleStyle); + ui.put("panel-ui", false); config.put("ui", ui); return config; } @@ -437,10 +453,8 @@ private JSONObject createPersistConfig() { // ui.put("window-width", 687); // ui.put("max-alpha", 240); - // Main Window Position & Size - ui.put("main-window-position", new JSONArray("[]")); - ui.put("gtp-console-position", new JSONArray("[]")); - ui.put("window-maximized", false); + // Window Position & Size + ui = WindowPosition.create(ui); config.put("filesystem", filesys); @@ -464,24 +478,10 @@ private void writeConfig(JSONObject config, File file) throws IOException, JSONE } public void persist() throws IOException { - boolean windowIsMaximized = Lizzie.frame.getExtendedState() == JFrame.MAXIMIZED_BOTH; - - JSONArray mainPos = new JSONArray(); - if (!windowIsMaximized) { - mainPos.put(Lizzie.frame.getX()); - mainPos.put(Lizzie.frame.getY()); - mainPos.put(Lizzie.frame.getWidth()); - mainPos.put(Lizzie.frame.getHeight()); - } - persistedUi.put("main-window-position", mainPos); - JSONArray gtpPos = new JSONArray(); - gtpPos.put(Lizzie.gtpConsole.getX()); - gtpPos.put(Lizzie.gtpConsole.getY()); - gtpPos.put(Lizzie.gtpConsole.getWidth()); - gtpPos.put(Lizzie.gtpConsole.getHeight()); - persistedUi.put("gtp-console-position", gtpPos); - persistedUi.put("board-postion-propotion", Lizzie.frame.BoardPositionProportion); - persistedUi.put("window-maximized", windowIsMaximized); + + // Save the window position + persistedUi = WindowPosition.save(persistedUi); + writeConfig(this.persisted, new File(persistFilename)); } diff --git a/src/main/java/featurecat/lizzie/Lizzie.java b/src/main/java/featurecat/lizzie/Lizzie.java index b0306564d..478cfcc50 100644 --- a/src/main/java/featurecat/lizzie/Lizzie.java +++ b/src/main/java/featurecat/lizzie/Lizzie.java @@ -3,18 +3,22 @@ import featurecat.lizzie.analysis.Leelaz; import featurecat.lizzie.gui.GtpConsolePane; import featurecat.lizzie.gui.LizzieFrame; +import featurecat.lizzie.gui.LizzieMain; +import featurecat.lizzie.gui.MainFrame; import featurecat.lizzie.rules.Board; import java.io.File; import java.io.IOException; import java.util.Optional; -import javax.swing.*; +import javax.swing.JOptionPane; +import javax.swing.UIManager; +import javax.swing.UnsupportedLookAndFeelException; import org.json.JSONArray; /** Main class. */ public class Lizzie { public static Config config; + public static MainFrame frame; public static GtpConsolePane gtpConsole; - public static LizzieFrame frame; public static Board board; public static Leelaz leelaz; public static String lizzieVersion = "0.7"; @@ -26,7 +30,7 @@ public static void main(String[] args) throws IOException { mainArgs = args; config = new Config(); board = new Board(); - frame = new LizzieFrame(); + frame = config.panelUI ? new LizzieMain() : new LizzieFrame(); gtpConsole = new GtpConsolePane(frame); gtpConsole.setVisible(config.leelazConfig.optBoolean("print-comms", false)); try { @@ -67,7 +71,7 @@ public static void shutdown() { JOptionPane.showConfirmDialog( null, "Do you want to save this SGF?", "Save SGF?", JOptionPane.OK_CANCEL_OPTION); if (ret == JOptionPane.OK_OPTION) { - LizzieFrame.saveFile(); + frame.saveFile(); } } board.autosaveToMemory(); diff --git a/src/main/java/featurecat/lizzie/analysis/Leelaz.java b/src/main/java/featurecat/lizzie/analysis/Leelaz.java index caaf956ab..6c2426f01 100644 --- a/src/main/java/featurecat/lizzie/analysis/Leelaz.java +++ b/src/main/java/featurecat/lizzie/analysis/Leelaz.java @@ -117,6 +117,7 @@ public void startEngine(String engineCommand) throws IOException { return; } + isLoaded = false; commands = splitCommand(engineCommand); // Get weight name @@ -247,6 +248,9 @@ private void parseLine(String line) { } else if (line.equals("\n")) { // End of response } else if (line.startsWith("info")) { + if (!isLoaded) { + Lizzie.frame.refresh(); + } isLoaded = true; // Clear switching prompt switching = false; @@ -256,7 +260,7 @@ private void parseLine(String line) { // This should not be stale data when the command number match this.bestMoves = parseInfo(line.substring(5)); notifyBestMoveListeners(); - Lizzie.frame.repaint(); + Lizzie.frame.refresh(1); // don't follow the maxAnalyzeTime rule if we are in analysis mode if (System.currentTimeMillis() - startPonderTime > maxAnalyzeTimeMillis && !Lizzie.board.inAnalysisMode()) { @@ -264,13 +268,16 @@ private void parseLine(String line) { } } } else if (line.contains(" -> ")) { + if (!isLoaded) { + Lizzie.frame.refresh(); + } isLoaded = true; if (isResponseUpToDate() || isThinking && (!isPondering && Lizzie.frame.isPlayingAgainstLeelaz || isInputCommand)) { bestMoves.add(MoveData.fromSummary(line)); notifyBestMoveListeners(); - Lizzie.frame.repaint(); + Lizzie.frame.refresh(1); } } else if (line.startsWith("play")) { // In lz-genmove_analyze @@ -302,7 +309,8 @@ private void parseLine(String line) { } else if (isThinking && !isPondering) { if (Lizzie.frame.isPlayingAgainstLeelaz || isInputCommand) { Lizzie.board.place(params[1]); - togglePonder(); + // TODO Do not ponder when playing against Leela Zero + // togglePonder(); if (!isInputCommand) { isPondering = false; } @@ -528,6 +536,7 @@ public void togglePonder() { } else { sendCommand("name"); // ends pondering } + Lizzie.frame.updateBasicInfo(); } /** End the process */ diff --git a/src/main/java/featurecat/lizzie/gui/BasicInfoPane.java b/src/main/java/featurecat/lizzie/gui/BasicInfoPane.java new file mode 100644 index 000000000..0eab1c084 --- /dev/null +++ b/src/main/java/featurecat/lizzie/gui/BasicInfoPane.java @@ -0,0 +1,166 @@ +package featurecat.lizzie.gui; + +import static java.awt.image.BufferedImage.TYPE_INT_ARGB; + +import featurecat.lizzie.Lizzie; +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Font; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.RenderingHints; +import java.awt.image.BufferedImage; + +/** The window used to display the game. */ +public class BasicInfoPane extends LizziePane { + + private LizzieMain owner; + + public BasicInfoPane(LizzieMain owner) { + super(owner); + this.owner = owner; + setVisible(true); + } + + private BufferedImage cachedImage; + + /** + * Draws the game board and interface + * + * @param g0 not used + */ + @Override + protected void paintComponent(Graphics g0) { + super.paintComponent(g0); + + int x = 0; // getX(); + int y = 0; // getY(); + int width = getWidth(); + int height = getHeight(); + + // initialize + + cachedImage = new BufferedImage(width, height, TYPE_INT_ARGB); + Graphics2D g = (Graphics2D) cachedImage.getGraphics(); + g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + + if (Lizzie.config.showCaptured) { + if (owner == null) { + g.drawImage(owner.getBasicInfoContainer(this), x, y, null); + } else { + g.drawImage(owner.getBasicInfoContainer(this), x, y, null); + } + drawCaptured(g, x, y, width, height); + } + + // cleanup + g.dispose(); + + // draw the image + Graphics2D bsGraphics = (Graphics2D) g0; // bs.getDrawGraphics(); + bsGraphics.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); + bsGraphics.drawImage(cachedImage, 0, 0, null); + + // cleanup + bsGraphics.dispose(); + // bs.show(); + } + + private void drawCaptured(Graphics2D g, int posX, int posY, int width, int height) { + // Draw border + g.setColor(new Color(0, 0, 0, 130)); + g.fillRect(posX, posY, width, height); + + // border. does not include bottom edge + int strokeRadius = Lizzie.config.showBorder ? 3 : 1; + g.setStroke(new BasicStroke(strokeRadius == 1 ? strokeRadius : 2 * strokeRadius)); + if (Lizzie.config.showBorder) { + g.drawLine( + posX + strokeRadius, + posY + strokeRadius, + posX - strokeRadius + width, + posY + strokeRadius); + g.drawLine( + posX + strokeRadius, + posY + 3 * strokeRadius, + posX + strokeRadius, + posY - strokeRadius + height); + g.drawLine( + posX - strokeRadius + width, + posY + 3 * strokeRadius, + posX - strokeRadius + width, + posY - strokeRadius + height); + } + + // Draw middle line + g.drawLine( + posX - strokeRadius + width / 2, + posY + 3 * strokeRadius, + posX - strokeRadius + width / 2, + posY - strokeRadius + height); + g.setColor(Color.white); + + // Draw black and white "stone" + int diam = height / 3; + int smallDiam = diam / 2; + int bdiam = diam, wdiam = diam; + if (Lizzie.board != null) { + if (Lizzie.board.inScoreMode()) { + // do nothing + } else if (Lizzie.board.getHistory().isBlacksTurn()) { + wdiam = smallDiam; + } else { + bdiam = smallDiam; + } + } else { + bdiam = smallDiam; + } + g.setColor(Color.black); + g.fillOval( + posX + width / 4 - bdiam / 2, posY + height * 3 / 8 + (diam - bdiam) / 2, bdiam, bdiam); + + g.setColor(Color.WHITE); + g.fillOval( + posX + width * 3 / 4 - wdiam / 2, posY + height * 3 / 8 + (diam - wdiam) / 2, wdiam, wdiam); + + // Draw captures + String bval, wval; + setPanelFont(g, (float) (height * 0.18)); + if (Lizzie.board == null) { + return; + } + if (Lizzie.board.inScoreMode()) { + double score[] = Lizzie.board.getScore(Lizzie.board.scoreStones()); + bval = String.format("%.0f", score[0]); + wval = String.format("%.1f", score[1]); + } else { + bval = String.format("%d", Lizzie.board.getData().blackCaptures); + wval = String.format("%d", Lizzie.board.getData().whiteCaptures); + } + + g.setColor(Color.WHITE); + int bw = g.getFontMetrics().stringWidth(bval); + int ww = g.getFontMetrics().stringWidth(wval); + boolean largeSubBoard = Lizzie.config.showLargeSubBoard(); + int bx = (largeSubBoard ? diam : -bw / 2); + int wx = (largeSubBoard ? bx : -ww / 2); + + g.drawString(bval, posX + width / 4 + bx, posY + height * 7 / 8); + g.drawString(wval, posX + width * 3 / 4 + wx, posY + height * 7 / 8); + + // Status Indicator + int statusDiam = height / 8; + g.setColor((Lizzie.leelaz != null && Lizzie.leelaz.isPondering()) ? Color.GREEN : Color.RED); + g.fillOval( + posX - strokeRadius + width / 2 - statusDiam / 2, + posY + height * 3 / 8 + (diam - statusDiam) / 2, + statusDiam, + statusDiam); + } + + private void setPanelFont(Graphics2D g, float size) { + Font font = new Font(Lizzie.config.fontName, Font.PLAIN, (int) size); + g.setFont(font); + } +} diff --git a/src/main/java/featurecat/lizzie/gui/BasicLizziePaneUI.java b/src/main/java/featurecat/lizzie/gui/BasicLizziePaneUI.java new file mode 100644 index 000000000..683bb21bc --- /dev/null +++ b/src/main/java/featurecat/lizzie/gui/BasicLizziePaneUI.java @@ -0,0 +1,679 @@ +package featurecat.lizzie.gui; + +import featurecat.lizzie.Lizzie; +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Component; +import java.awt.Container; +import java.awt.Dialog; +import java.awt.Dimension; +import java.awt.Frame; +import java.awt.Graphics; +import java.awt.IllegalComponentStateException; +import java.awt.Insets; +import java.awt.LayoutManager; +import java.awt.Point; +import java.awt.Window; +import java.awt.event.ContainerEvent; +import java.awt.event.ContainerListener; +import java.awt.event.FocusEvent; +import java.awt.event.FocusListener; +import java.awt.event.KeyEvent; +import java.awt.event.MouseEvent; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.awt.event.WindowListener; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import javax.swing.InputMap; +import javax.swing.JComponent; +import javax.swing.JDialog; +import javax.swing.JRootPane; +import javax.swing.KeyStroke; +import javax.swing.LookAndFeel; +import javax.swing.RootPaneContainer; +import javax.swing.SwingConstants; +import javax.swing.SwingUtilities; +import javax.swing.UIManager; +import javax.swing.event.MouseInputListener; +import javax.swing.plaf.ComponentUI; + +public class BasicLizziePaneUI extends LizziePaneUI implements SwingConstants { + protected LizziePane lizziePane; + private boolean floating; + private int floatingX; + private int floatingY; + private RootPaneContainer floatingLizziePane; + protected DragWindow dragWindow; + private Container dockingSource; + protected int focusedCompIndex = -1; + private Dimension originSize = null; + + protected MouseInputListener dockingListener; + protected PropertyChangeListener propertyListener; + + protected ContainerListener lizziePaneContListener; + protected FocusListener lizziePaneFocusListener; + private Handler handler; + + protected String constraintBeforeFloating; + + private static String FOCUSED_COMP_INDEX = "LizziePane.focusedCompIndex"; + + public static ComponentUI createUI(JComponent c) { + return new BasicLizziePaneUI(); + } + + public void installUI(JComponent c) { + lizziePane = (LizziePane) c; + + // Set defaults + installDefaults(); + installComponents(); + // Default disabled drag + // installListeners(); + // installKeyboardActions(); + + // Initialize instance vars + floating = false; + floatingX = floatingY = 0; + floatingLizziePane = null; + + LookAndFeel.installProperty(c, "opaque", Boolean.TRUE); + + if (c.getClientProperty(FOCUSED_COMP_INDEX) != null) { + focusedCompIndex = ((Integer) (c.getClientProperty(FOCUSED_COMP_INDEX))).intValue(); + } + } + + public void uninstallUI(JComponent c) { + + // Clear defaults + uninstallDefaults(); + uninstallComponents(); + uninstallListeners(); + // uninstallKeyboardActions(); + + // Clear instance vars + if (isFloating()) setFloating(false, null); + + floatingLizziePane = null; + dragWindow = null; + dockingSource = null; + + c.putClientProperty(FOCUSED_COMP_INDEX, Integer.valueOf(focusedCompIndex)); + } + + protected void installDefaults() { + LookAndFeel.installBorder(lizziePane, "LizziePane.border"); + LookAndFeel.installColorsAndFont( + lizziePane, "LizziePane.background", "LizziePane.foreground", "LizziePane.font"); + } + + protected void uninstallDefaults() { + LookAndFeel.uninstallBorder(lizziePane); + } + + protected void installComponents() {} + + protected void uninstallComponents() {} + + public void installListeners() { + dockingListener = createDockingListener(); + + if (dockingListener != null) { + lizziePane.addMouseMotionListener(dockingListener); + lizziePane.addMouseListener(dockingListener); + } + + propertyListener = createPropertyListener(); // added in setFloating + if (propertyListener != null) { + lizziePane.addPropertyChangeListener(propertyListener); + } + + lizziePaneContListener = createLizziePaneContListener(); + if (lizziePaneContListener != null) { + lizziePane.addContainerListener(lizziePaneContListener); + } + + lizziePaneFocusListener = createLizziePaneFocusListener(); + + if (lizziePaneFocusListener != null) { + // Put focus listener on all components in lizziePane + Component[] components = lizziePane.getComponents(); + + for (Component component : components) { + component.addFocusListener(lizziePaneFocusListener); + } + } + } + + public void uninstallListeners() { + if (dockingListener != null) { + lizziePane.removeMouseMotionListener(dockingListener); + lizziePane.removeMouseListener(dockingListener); + + dockingListener = null; + } + + if (propertyListener != null) { + lizziePane.removePropertyChangeListener(propertyListener); + propertyListener = null; // removed in setFloating + } + + if (lizziePaneContListener != null) { + lizziePane.removeContainerListener(lizziePaneContListener); + lizziePaneContListener = null; + } + + if (lizziePaneFocusListener != null) { + // Remove focus listener from all components in lizziePane + Component[] components = lizziePane.getComponents(); + + for (Component component : components) { + component.removeFocusListener(lizziePaneFocusListener); + } + + lizziePaneFocusListener = null; + } + handler = null; + } + + protected void installKeyboardActions() { + InputMap km = getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); + + SwingUtilities.replaceUIInputMap(lizziePane, JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT, km); + } + + InputMap getInputMap(int condition) { + if (condition == JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT) { + return (InputMap) UIManager.get("LizziePane.ancestorInputMap", lizziePane.getLocale()); + } + return null; + } + + protected void uninstallKeyboardActions() { + SwingUtilities.replaceUIActionMap(lizziePane, null); + SwingUtilities.replaceUIInputMap( + lizziePane, JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT, null); + } + + /** + * Creates a window which contains the lizziePane after it has been dragged out from its container + * + * @return a RootPaneContainer object, containing the lizziePane. + */ + protected RootPaneContainer createFloatingWindow(LizziePane lizziePane) { + class LizziePaneDialog extends JDialog { + public LizziePaneDialog(Frame owner, String title, boolean modal) { + super(owner, title, modal); + } + + public LizziePaneDialog(Dialog owner, String title, boolean modal) { + super(owner, title, modal); + } + + protected JRootPane createRootPane() { + JRootPane rootPane = new JRootPane(); + rootPane.setOpaque(false); + + rootPane.registerKeyboardAction( + e -> { + if (Lizzie.frame.isDesignMode()) { + Lizzie.frame.toggleDesignMode(); + } + }, + KeyStroke.getKeyStroke(KeyEvent.VK_W, KeyEvent.ALT_DOWN_MASK), + JComponent.WHEN_IN_FOCUSED_WINDOW); + return rootPane; + } + } + + JDialog dialog; + Window window = SwingUtilities.getWindowAncestor(lizziePane); + if (window instanceof Frame) { + dialog = new LizziePaneDialog((Frame) window, lizziePane.getName(), false); + } else if (window instanceof Dialog) { + dialog = new LizziePaneDialog((Dialog) window, lizziePane.getName(), false); + } else { + dialog = new LizziePaneDialog((Frame) null, lizziePane.getName(), false); + } + + dialog.getRootPane().setName("LizziePane.FloatingWindow"); + dialog.setTitle(lizziePane.getName()); + dialog.setResizable(true); + // dialog.setSize(lizziePane.getSize()); + WindowListener wl = createFrameListener(); + dialog.addWindowListener(wl); + return dialog; + } + + protected DragWindow createDragWindow(LizziePane lizziePane) { + Window frame = null; + if (lizziePane != null) { + Container p; + for (p = lizziePane.getParent(); p != null && !(p instanceof Window); p = p.getParent()) ; + if (p != null && p instanceof Window) frame = (Window) p; + } + if (floatingLizziePane == null) { + floatingLizziePane = createFloatingWindow(lizziePane); + } + if (floatingLizziePane instanceof Window) frame = (Window) floatingLizziePane; + DragWindow dragWindow = new DragWindow(frame); + return dragWindow; + } + + public void setFloatingLocation(int x, int y) { + floatingX = x; + floatingY = y; + } + + public boolean isFloating() { + return floating; + } + + public void setFloating(boolean b, Point p) { + if (lizziePane.isFloatable()) { + boolean visible = false; + Window ancestor = SwingUtilities.getWindowAncestor(lizziePane); + if (ancestor != null) { + visible = ancestor.isVisible(); + } + if (dragWindow != null) dragWindow.setVisible(false); + this.floating = b; + if (floatingLizziePane == null) { + floatingLizziePane = createFloatingWindow(lizziePane); + } + if (b == true) { + if (dockingSource == null) { + dockingSource = lizziePane.getParent(); + dockingSource.remove(lizziePane); + } + constraintBeforeFloating = calculateConstraint(); + if (propertyListener != null) UIManager.addPropertyChangeListener(propertyListener); + floatingLizziePane.getContentPane().add(lizziePane, BorderLayout.CENTER); + if (floatingLizziePane instanceof Window) { + ((Window) floatingLizziePane).pack(); + ((Window) floatingLizziePane).setLocation(floatingX, floatingY); + Insets insets = ((Window) floatingLizziePane).getInsets(); + Dimension d = + new Dimension( + originSize.width + insets.left + insets.right, + originSize.height + insets.top + insets.bottom); + ((Window) floatingLizziePane).setSize(d); + if (visible) { + ((Window) floatingLizziePane).setVisible(true); + } else { + ancestor.addWindowListener( + new WindowAdapter() { + public void windowOpened(WindowEvent e) { + ((Window) floatingLizziePane).setVisible(true); + } + }); + } + } + } else { + if (floatingLizziePane == null) floatingLizziePane = createFloatingWindow(lizziePane); + if (floatingLizziePane instanceof Window) ((Window) floatingLizziePane).setVisible(false); + floatingLizziePane.getContentPane().remove(lizziePane); + String constraint = getDockingConstraint(dockingSource, p); + if (constraint != null) { + if (dockingSource == null) dockingSource = lizziePane.getParent(); + if (propertyListener != null) UIManager.removePropertyChangeListener(propertyListener); + dockingSource.add(constraint, lizziePane); + } + } + dockingSource.invalidate(); + Container dockingSourceParent = dockingSource.getParent(); + if (dockingSourceParent != null) dockingSourceParent.validate(); + dockingSource.repaint(); + } + } + + public boolean canDock(Component c, Point p) { + return (p != null && getDockingConstraint(c, p) != null); + } + + private String calculateConstraint() { + String constraint = null; + LayoutManager lm = dockingSource.getLayout(); + if (lm instanceof LizzieLayout) { + constraint = (String) ((LizzieLayout) lm).getConstraints(lizziePane); + } + return (constraint != null) ? constraint : constraintBeforeFloating; + } + + private String getDockingConstraint(Component c, Point p) { + if (p == null) return constraintBeforeFloating; + return null; + } + + protected void dragTo(Point position, Point origin) { + originSize = lizziePane.getSize(); + if (lizziePane.isFloatable()) { + try { + if (dragWindow == null) dragWindow = createDragWindow(lizziePane); + Point offset = dragWindow.getOffset(); + if (offset == null) { + Dimension size = lizziePane.getSize(); + offset = new Point(size.width / 2, size.height / 2); + dragWindow.setOffset(offset); + } + Point global = new Point(origin.x + position.x, origin.y + position.y); + Point dragPoint = new Point(global.x - offset.x, global.y - offset.y); + if (dockingSource == null) dockingSource = lizziePane.getParent(); + constraintBeforeFloating = calculateConstraint(); + dragWindow.setLocation(dragPoint.x, dragPoint.y); + if (dragWindow.isVisible() == false) { + Dimension size = lizziePane.getSize(); + dragWindow.setSize(size.width, size.height); + dragWindow.setVisible(true); + } + } catch (IllegalComponentStateException e) { + } + } + } + + protected void floatAt(Point position, Point origin) { + if (lizziePane.isFloatable()) { + try { + Point offset = dragWindow.getOffset(); + if (offset == null) { + offset = position; + dragWindow.setOffset(offset); + } + Point global = new Point(origin.x + position.x, origin.y + position.y); + setFloatingLocation(global.x - offset.x, global.y - offset.y); + if (dockingSource != null) { + Point dockingPosition = dockingSource.getLocationOnScreen(); + Point comparisonPoint = + new Point(global.x - dockingPosition.x, global.y - dockingPosition.y); + if (canDock(dockingSource, comparisonPoint)) { + setFloating(false, comparisonPoint); + } else { + setFloating(true, null); + } + } else { + setFloating(true, null); + } + dragWindow.setOffset(null); + } catch (IllegalComponentStateException e) { + } + } + } + + public void toWindow(Point position, Dimension size) { + if (lizziePane.isFloatable()) { + try { + originSize = size; + if (dragWindow == null) dragWindow = createDragWindow(lizziePane); + if (dockingSource == null) dockingSource = lizziePane.getParent(); + constraintBeforeFloating = calculateConstraint(); + setFloatingLocation(position.x, position.y); + setFloating(true, null); + } catch (IllegalComponentStateException e) { + } + } + } + + private Handler getHandler() { + if (handler == null) { + handler = new Handler(); + } + return handler; + } + + protected ContainerListener createLizziePaneContListener() { + return getHandler(); + } + + protected FocusListener createLizziePaneFocusListener() { + return getHandler(); + } + + protected PropertyChangeListener createPropertyListener() { + return getHandler(); + } + + protected MouseInputListener createDockingListener() { + getHandler().lp = lizziePane; + return getHandler(); + } + + protected WindowListener createFrameListener() { + return new FrameListener(); + } + + /** + * Paints the contents of the window used for dragging. + * + * @param g Graphics to paint to. + * @throws NullPointerException is g is null + */ + protected void paintDragWindow(Graphics g) { + g.setColor(dragWindow.getBackground()); + int w = dragWindow.getWidth(); + int h = dragWindow.getHeight(); + g.fillRect(0, 0, w, h); + g.setColor(dragWindow.getBorderColor()); + g.drawRect(0, 0, w - 1, h - 1); + } + + private class Handler + implements ContainerListener, FocusListener, MouseInputListener, PropertyChangeListener { + + // + // ContainerListener + // + public void componentAdded(ContainerEvent evt) { + Component c = evt.getChild(); + + if (lizziePaneFocusListener != null) { + c.addFocusListener(lizziePaneFocusListener); + } + } + + public void componentRemoved(ContainerEvent evt) { + Component c = evt.getChild(); + + if (lizziePaneFocusListener != null) { + c.removeFocusListener(lizziePaneFocusListener); + } + } + + public void focusGained(FocusEvent evt) { + Component c = evt.getComponent(); + focusedCompIndex = lizziePane.getComponentIndex(c); + } + + public void focusLost(FocusEvent evt) {} + + LizziePane lp; + boolean isDragging = false; + Point origin = null; + + public void mousePressed(MouseEvent evt) { + if (!lp.isEnabled()) { + return; + } + isDragging = false; + } + + public void mouseReleased(MouseEvent evt) { + if (!lp.isEnabled()) { + return; + } + if (isDragging) { + Point position = evt.getPoint(); + if (origin == null) origin = evt.getComponent().getLocationOnScreen(); + floatAt(position, origin); + } + origin = null; + isDragging = false; + } + + public void mouseDragged(MouseEvent evt) { + if (!lp.isEnabled()) { + return; + } + isDragging = true; + Point position = evt.getPoint(); + if (origin == null) { + origin = evt.getComponent().getLocationOnScreen(); + } + dragTo(position, origin); + } + + public void mouseClicked(MouseEvent evt) {} + + public void mouseEntered(MouseEvent evt) {} + + public void mouseExited(MouseEvent evt) {} + + public void mouseMoved(MouseEvent evt) {} + + public void propertyChange(PropertyChangeEvent evt) { + String propertyName = evt.getPropertyName(); + if (propertyName == "lookAndFeel") { + lizziePane.updateUI(); + } + } + } + + protected class FrameListener extends WindowAdapter { + public void windowClosing(WindowEvent w) { + if (lizziePane.isFloatable()) { + if (dragWindow != null) dragWindow.setVisible(false); + floating = false; + if (floatingLizziePane == null) floatingLizziePane = createFloatingWindow(lizziePane); + if (floatingLizziePane instanceof Window) ((Window) floatingLizziePane).setVisible(false); + floatingLizziePane.getContentPane().remove(lizziePane); + String constraint = constraintBeforeFloating; + if (dockingSource == null) dockingSource = lizziePane.getParent(); + if (propertyListener != null) UIManager.removePropertyChangeListener(propertyListener); + dockingSource.add(lizziePane, constraint); + dockingSource.invalidate(); + Container dockingSourceParent = dockingSource.getParent(); + if (dockingSourceParent != null) dockingSourceParent.validate(); + dockingSource.repaint(); + } + } + } + + protected class LizziePaneContListener implements ContainerListener { + public void componentAdded(ContainerEvent e) { + getHandler().componentAdded(e); + } + + public void componentRemoved(ContainerEvent e) { + getHandler().componentRemoved(e); + } + } + + protected class LizziePaneFocusListener implements FocusListener { + public void focusGained(FocusEvent e) { + getHandler().focusGained(e); + } + + public void focusLost(FocusEvent e) { + getHandler().focusLost(e); + } + } + + protected class PropertyListener implements PropertyChangeListener { + public void propertyChange(PropertyChangeEvent e) { + getHandler().propertyChange(e); + } + } + + /** + * This class should be treated as a "protected" inner class. Instantiate it only within + * subclasses of LizziePaneUI. + */ + public class DockingListener implements MouseInputListener { + protected LizziePane lizziePane; + protected boolean isDragging = false; + protected Point origin = null; + + public DockingListener(LizziePane t) { + this.lizziePane = t; + getHandler().lp = t; + } + + public void mouseClicked(MouseEvent e) { + getHandler().mouseClicked(e); + } + + public void mousePressed(MouseEvent e) { + getHandler().lp = lizziePane; + getHandler().mousePressed(e); + isDragging = getHandler().isDragging; + } + + public void mouseReleased(MouseEvent e) { + getHandler().lp = lizziePane; + getHandler().isDragging = isDragging; + getHandler().origin = origin; + getHandler().mouseReleased(e); + isDragging = getHandler().isDragging; + origin = getHandler().origin; + } + + public void mouseEntered(MouseEvent e) { + getHandler().mouseEntered(e); + } + + public void mouseExited(MouseEvent e) { + getHandler().mouseExited(e); + } + + public void mouseDragged(MouseEvent e) { + getHandler().lp = lizziePane; + getHandler().origin = origin; + getHandler().mouseDragged(e); + isDragging = getHandler().isDragging; + origin = getHandler().origin; + } + + public void mouseMoved(MouseEvent e) { + getHandler().mouseMoved(e); + } + } + + protected class DragWindow extends Window { + Color borderColor = Color.gray; + Point offset; // offset of the mouse cursor inside the DragWindow + + DragWindow(Window w) { + super(w); + } + + public Point getOffset() { + return offset; + } + + public void setOffset(Point p) { + this.offset = p; + } + + public void setBorderColor(Color c) { + if (this.borderColor == c) return; + this.borderColor = c; + repaint(); + } + + public Color getBorderColor() { + return this.borderColor; + } + + public void paint(Graphics g) { + paintDragWindow(g); + // Paint the children + super.paint(g); + } + + public Insets getInsets() { + return new Insets(1, 1, 1, 1); + } + } +} diff --git a/src/main/java/featurecat/lizzie/gui/BoardPane.java b/src/main/java/featurecat/lizzie/gui/BoardPane.java new file mode 100644 index 000000000..7649d99a5 --- /dev/null +++ b/src/main/java/featurecat/lizzie/gui/BoardPane.java @@ -0,0 +1,468 @@ +package featurecat.lizzie.gui; + +import static java.awt.image.BufferedImage.TYPE_INT_ARGB; +import static java.lang.Math.max; +import static java.lang.Math.min; + +import com.jhlabs.image.GaussianFilter; +import featurecat.lizzie.Lizzie; +import featurecat.lizzie.rules.Board; +import featurecat.lizzie.rules.SGFParser; +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Font; +import java.awt.FontMetrics; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.RenderingHints; +import java.awt.Toolkit; +import java.awt.datatransfer.Clipboard; +import java.awt.datatransfer.DataFlavor; +import java.awt.datatransfer.StringSelection; +import java.awt.datatransfer.Transferable; +import java.awt.datatransfer.UnsupportedFlavorException; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.event.MouseMotionListener; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.ResourceBundle; +import java.util.function.Consumer; + +/** The window used to display the game. */ +public class BoardPane extends LizziePane { + private static final ResourceBundle resourceBundle = + ResourceBundle.getBundle("l10n.DisplayStrings"); + + private static final String[] commands = { + resourceBundle.getString("LizzieFrame.commands.keyN"), + resourceBundle.getString("LizzieFrame.commands.keyEnter"), + resourceBundle.getString("LizzieFrame.commands.keySpace"), + resourceBundle.getString("LizzieFrame.commands.keyUpArrow"), + resourceBundle.getString("LizzieFrame.commands.keyDownArrow"), + resourceBundle.getString("LizzieFrame.commands.rightClick"), + resourceBundle.getString("LizzieFrame.commands.mouseWheelScroll"), + resourceBundle.getString("LizzieFrame.commands.keyC"), + resourceBundle.getString("LizzieFrame.commands.keyP"), + resourceBundle.getString("LizzieFrame.commands.keyPeriod"), + resourceBundle.getString("LizzieFrame.commands.keyA"), + resourceBundle.getString("LizzieFrame.commands.keyM"), + resourceBundle.getString("LizzieFrame.commands.keyI"), + resourceBundle.getString("LizzieFrame.commands.keyO"), + resourceBundle.getString("LizzieFrame.commands.keyS"), + resourceBundle.getString("LizzieFrame.commands.keyAltC"), + resourceBundle.getString("LizzieFrame.commands.keyAltV"), + resourceBundle.getString("LizzieFrame.commands.keyF"), + resourceBundle.getString("LizzieFrame.commands.keyV"), + resourceBundle.getString("LizzieFrame.commands.keyW"), + resourceBundle.getString("LizzieFrame.commands.keyCtrlW"), + resourceBundle.getString("LizzieFrame.commands.keyG"), + resourceBundle.getString("LizzieFrame.commands.keyR"), + resourceBundle.getString("LizzieFrame.commands.keyBracket"), + resourceBundle.getString("LizzieFrame.commands.keyT"), + resourceBundle.getString("LizzieFrame.commands.keyCtrlT"), + resourceBundle.getString("LizzieFrame.commands.keyY"), + resourceBundle.getString("LizzieFrame.commands.keyZ"), + resourceBundle.getString("LizzieFrame.commands.keyShiftZ"), + resourceBundle.getString("LizzieFrame.commands.keyHome"), + resourceBundle.getString("LizzieFrame.commands.keyEnd"), + resourceBundle.getString("LizzieFrame.commands.keyControl"), + resourceBundle.getString("LizzieFrame.commands.keyDelete"), + resourceBundle.getString("LizzieFrame.commands.keyBackspace"), + resourceBundle.getString("LizzieFrame.commands.keyE"), + }; + + private static BoardRenderer boardRenderer; + + // private final BufferStrategy bs; + private static boolean started = false; + + private static final int[] outOfBoundCoordinate = new int[] {-1, -1}; + public int[] mouseOverCoordinate = outOfBoundCoordinate; + + private long lastAutosaveTime = System.currentTimeMillis(); + private boolean isReplayVariation = false; + + LizzieMain owner; + /** Creates a window */ + public BoardPane(LizzieMain owner) { + super(owner); + this.owner = owner; + + boardRenderer = new BoardRenderer(true); + + // createBufferStrategy(2); + // bs = getBufferStrategy(); + + addMouseListener( + new MouseAdapter() { + @Override + public void mousePressed(MouseEvent e) { + if (e.getButton() == MouseEvent.BUTTON1) { // left click + if (e.getClickCount() == 2) { // TODO: Maybe need to delay check + onDoubleClicked(e.getX(), e.getY()); + } else { + onClicked(e.getX(), e.getY()); + } + } else if (e.getButton() == MouseEvent.BUTTON3) { // right click + Input.undo(); + } + } + }); + addMouseMotionListener( + new MouseMotionListener() { + @Override + public void mouseMoved(MouseEvent e) { + onMouseMoved(e.getX(), e.getY()); + } + + @Override + public void mouseDragged(MouseEvent e) {} + }); + } + + /** Clears related status from empty board. */ + public void clear() { + if (LizzieMain.winratePane != null) { + LizzieMain.winratePane.clear(); + } + started = false; + owner.updateStatus(); + } + + private BufferedImage cachedImage; + + /** + * Draws the game board and interface + * + * @param g0 not used + */ + @Override + protected void paintComponent(Graphics g0) { + super.paintComponent(g0); + autosaveMaybe(); + + int width = getWidth(); + int height = getHeight(); + + if (!owner.showControls) { + // layout parameters + // initialize + cachedImage = new BufferedImage(width, height, TYPE_INT_ARGB); + Graphics2D g = (Graphics2D) cachedImage.getGraphics(); + g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); + + boardRenderer.setLocation(0, 0); + boardRenderer.setBoardLength(width); + boardRenderer.draw(g); + + owner.repaintSub(); + + if (Lizzie.leelaz != null && Lizzie.leelaz.isLoaded() && !started) { + started = true; + if (Lizzie.config.showVariationGraph || Lizzie.config.showComment) { + owner.updateStatus(); + } + } + + // cleanup + g.dispose(); + } + + // draw the image + Graphics2D bsGraphics = (Graphics2D) g0; // bs.getDrawGraphics(); + bsGraphics.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); + // bsGraphics.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, + // RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY); + bsGraphics.drawImage(cachedImage, 0, 0, null); + + // cleanup + bsGraphics.dispose(); + // bs.show(); + } + + private GaussianFilter filter10 = new GaussianFilter(10); + + /** Display the controls */ + void drawControls() { + userAlreadyKnowsAboutCommandString = true; + + cachedImage = new BufferedImage(getWidth(), getHeight(), TYPE_INT_ARGB); + + // redraw background + // createBackground(); + + List commandsToShow = new ArrayList<>(Arrays.asList(commands)); + if (Lizzie.leelaz.getDynamicKomi().isPresent()) { + commandsToShow.add(resourceBundle.getString("LizzieFrame.commands.keyD")); + } + + Graphics2D g = cachedImage.createGraphics(); + + int maxSize = min(getWidth(), getHeight()); + int fontSize = (int) (maxSize * min(0.034, 0.80 / commandsToShow.size())); + Font font = new Font(Lizzie.config.fontName, Font.PLAIN, fontSize); + g.setFont(font); + + FontMetrics metrics = g.getFontMetrics(font); + int maxCmdWidth = commandsToShow.stream().mapToInt(c -> metrics.stringWidth(c)).max().orElse(0); + int lineHeight = (int) (font.getSize() * 1.15); + + int boxWidth = min((int) (maxCmdWidth * 1.4), getWidth()); + int boxHeight = + min(commandsToShow.size() * lineHeight, getHeight() - getInsets().top - getInsets().bottom); + + int commandsX = min(getWidth() / 2 - boxWidth / 2, getWidth()); + int top = this.getInsets().top; + int commandsY = top + min((getHeight() - top) / 2 - boxHeight / 2, getHeight() - top); + + BufferedImage result = new BufferedImage(boxWidth, boxHeight, TYPE_INT_ARGB); + filter10.filter( + owner.cachedBackground.getSubimage(commandsX, commandsY, boxWidth, boxHeight), result); + g.drawImage(result, commandsX, commandsY, null); + + g.setColor(new Color(0, 0, 0, 130)); + g.fillRect(commandsX, commandsY, boxWidth, boxHeight); + int strokeRadius = Lizzie.config.showBorder ? 2 : 1; + g.setStroke(new BasicStroke(strokeRadius == 1 ? strokeRadius : 2 * strokeRadius)); + if (Lizzie.config.showBorder) { + g.setColor(new Color(0, 0, 0, 60)); + g.drawRect( + commandsX + strokeRadius, + commandsY + strokeRadius, + boxWidth - 2 * strokeRadius, + boxHeight - 2 * strokeRadius); + } + int verticalLineX = (int) (commandsX + boxWidth * 0.3); + g.setColor(new Color(0, 0, 0, 60)); + g.drawLine( + verticalLineX, + commandsY + 2 * strokeRadius, + verticalLineX, + commandsY + boxHeight - 2 * strokeRadius); + + g.setStroke(new BasicStroke(1)); + + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + + g.setColor(Color.WHITE); + int lineOffset = commandsY; + for (String command : commandsToShow) { + String[] split = command.split("\\|"); + g.drawString( + split[0], + verticalLineX - metrics.stringWidth(split[0]) - strokeRadius * 4, + font.getSize() + lineOffset); + g.drawString(split[1], verticalLineX + strokeRadius * 4, font.getSize() + lineOffset); + lineOffset += lineHeight; + } + + // refreshBackground(); + } + + private boolean userAlreadyKnowsAboutCommandString = false; + + private void drawCommandString(Graphics2D g) { + if (userAlreadyKnowsAboutCommandString) return; + + int maxSize = (int) (min(getWidth(), getHeight()) * 0.98); + + Font font = new Font(Lizzie.config.fontName, Font.PLAIN, (int) (maxSize * 0.03)); + String commandString = resourceBundle.getString("LizzieFrame.prompt.showControlsHint"); + int strokeRadius = Lizzie.config.showBorder ? 2 : 0; + + int showCommandsHeight = (int) (font.getSize() * 1.1); + int showCommandsWidth = g.getFontMetrics(font).stringWidth(commandString) + 4 * strokeRadius; + int showCommandsX = this.getInsets().left; + int showCommandsY = getHeight() - showCommandsHeight - this.getInsets().bottom; + g.setColor(new Color(0, 0, 0, 130)); + g.fillRect(showCommandsX, showCommandsY, showCommandsWidth, showCommandsHeight); + if (Lizzie.config.showBorder) { + g.setStroke(new BasicStroke(2 * strokeRadius)); + g.setColor(new Color(0, 0, 0, 60)); + g.drawRect( + showCommandsX + strokeRadius, + showCommandsY + strokeRadius, + showCommandsWidth - 2 * strokeRadius, + showCommandsHeight - 2 * strokeRadius); + } + g.setStroke(new BasicStroke(1)); + + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g.setColor(Color.WHITE); + g.setFont(font); + g.drawString(commandString, showCommandsX + 2 * strokeRadius, showCommandsY + font.getSize()); + } + + /** + * Checks whether or not something was clicked and performs the appropriate action + * + * @param x x coordinate + * @param y y coordinate + */ + public void onClicked(int x, int y) { + // Check for board click + Optional boardCoordinates = boardRenderer.convertScreenToCoordinates(x, y); + if (boardCoordinates.isPresent()) { + int[] coords = boardCoordinates.get(); + if (Lizzie.board.inAnalysisMode()) Lizzie.board.toggleAnalysis(); + if (!owner.isPlayingAgainstLeelaz + || (owner.playerIsBlack == Lizzie.board.getData().blackToPlay)) + Lizzie.board.place(coords[0], coords[1]); + // repaint(); + // owner.updateStatus(); + } + } + + public void onDoubleClicked(int x, int y) { + // Check for board double click + Optional boardCoordinates = boardRenderer.convertScreenToCoordinates(x, y); + if (boardCoordinates.isPresent()) { + int[] coords = boardCoordinates.get(); + if (!owner.isPlayingAgainstLeelaz) { + int moveNumber = Lizzie.board.moveNumberByCoord(coords); + if (moveNumber > 0) { + Lizzie.board.goToMoveNumberBeyondBranch(moveNumber); + } + } + } + } + + public void onMouseMoved(int x, int y) { + mouseOverCoordinate = outOfBoundCoordinate; + Optional coords = boardRenderer.convertScreenToCoordinates(x, y); + coords.filter(c -> !isMouseOver(c[0], c[1])).ifPresent(c -> repaint()); + coords.ifPresent( + c -> { + mouseOverCoordinate = c; + isReplayVariation = false; + }); + if (!coords.isPresent() && boardRenderer.isShowingBranch()) { + repaint(); + } + } + + private final Consumer placeVariation = + v -> Board.asCoordinates(v).ifPresent(c -> Lizzie.board.place(c[0], c[1])); + + public boolean playCurrentVariation() { + boardRenderer.variationOpt.ifPresent(vs -> vs.forEach(placeVariation)); + return boardRenderer.variationOpt.isPresent(); + } + + public void playBestMove() { + boardRenderer.bestMoveCoordinateName().ifPresent(placeVariation); + } + + public boolean isMouseOver(int x, int y) { + return mouseOverCoordinate[0] == x && mouseOverCoordinate[1] == y; + } + + private void autosaveMaybe() { + int interval = + Lizzie.config.config.getJSONObject("ui").getInt("autosave-interval-seconds") * 1000; + long currentTime = System.currentTimeMillis(); + if (interval > 0 && currentTime - lastAutosaveTime >= interval) { + Lizzie.board.autosave(); + lastAutosaveTime = currentTime; + } + } + + private void setDisplayedBranchLength(int n) { + boardRenderer.setDisplayedBranchLength(n); + } + + public void startRawBoard() { + boolean onBranch = boardRenderer.isShowingBranch(); + int n = (onBranch ? 1 : BoardRenderer.SHOW_RAW_BOARD); + boardRenderer.setDisplayedBranchLength(n); + } + + public void stopRawBoard() { + boardRenderer.setDisplayedBranchLength(BoardRenderer.SHOW_NORMAL_BOARD); + } + + public boolean incrementDisplayedBranchLength(int n) { + return boardRenderer.incrementDisplayedBranchLength(n); + } + + public void copySgf() { + try { + // Get sgf content from game + String sgfContent = SGFParser.saveToString(); + + // Save to clipboard + Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + Transferable transferableString = new StringSelection(sgfContent); + clipboard.setContents(transferableString, null); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public void pasteSgf() { + // Get string from clipboard + String sgfContent = + Optional.ofNullable(Toolkit.getDefaultToolkit().getSystemClipboard().getContents(null)) + .filter(cc -> cc.isDataFlavorSupported(DataFlavor.stringFlavor)) + .flatMap( + cc -> { + try { + return Optional.of((String) cc.getTransferData(DataFlavor.stringFlavor)); + } catch (UnsupportedFlavorException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + return Optional.empty(); + }) + .orElse(""); + + // Load game contents from sgf string + if (!sgfContent.isEmpty()) { + SGFParser.loadFromString(sgfContent); + } + } + + public void increaseMaxAlpha(int k) { + boardRenderer.increaseMaxAlpha(k); + } + + public void replayBranch() { + if (isReplayVariation) return; + int replaySteps = boardRenderer.getReplayBranch(); + if (replaySteps <= 0) return; // Bad steps or no branch + int oriBranchLength = boardRenderer.getDisplayedBranchLength(); + isReplayVariation = true; + if (Lizzie.leelaz.isPondering()) Lizzie.leelaz.togglePonder(); + Runnable runnable = + new Runnable() { + public void run() { + int secs = (int) (Lizzie.config.replayBranchIntervalSeconds * 1000); + for (int i = 1; i < replaySteps + 1; i++) { + if (!isReplayVariation) break; + setDisplayedBranchLength(i); + repaint(); + try { + Thread.sleep(secs); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + boardRenderer.setDisplayedBranchLength(oriBranchLength); + isReplayVariation = false; + if (!Lizzie.leelaz.isPondering()) Lizzie.leelaz.togglePonder(); + } + }; + Thread thread = new Thread(runnable); + thread.start(); + } + + public void updateStatus() { + owner.updateStatus(); + } +} diff --git a/src/main/java/featurecat/lizzie/gui/BoardRenderer.java b/src/main/java/featurecat/lizzie/gui/BoardRenderer.java index 58ad1db11..2980055a6 100644 --- a/src/main/java/featurecat/lizzie/gui/BoardRenderer.java +++ b/src/main/java/featurecat/lizzie/gui/BoardRenderer.java @@ -1,6 +1,10 @@ package featurecat.lizzie.gui; -import static java.awt.RenderingHints.*; +import static java.awt.RenderingHints.KEY_ANTIALIASING; +import static java.awt.RenderingHints.KEY_INTERPOLATION; +import static java.awt.RenderingHints.VALUE_ANTIALIAS_OFF; +import static java.awt.RenderingHints.VALUE_ANTIALIAS_ON; +import static java.awt.RenderingHints.VALUE_INTERPOLATION_BILINEAR; import static java.awt.image.BufferedImage.TYPE_INT_ARGB; import static java.lang.Math.log; import static java.lang.Math.max; @@ -16,7 +20,19 @@ import featurecat.lizzie.rules.SGFParser; import featurecat.lizzie.rules.Stone; import featurecat.lizzie.rules.Zobrist; -import java.awt.*; +import featurecat.lizzie.util.Utils; +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Font; +import java.awt.FontMetrics; +import java.awt.Graphics2D; +import java.awt.Image; +import java.awt.Paint; +import java.awt.Point; +import java.awt.RadialGradientPaint; +import java.awt.Rectangle; +import java.awt.RenderingHints; +import java.awt.TexturePaint; import java.awt.font.TextAttribute; import java.awt.geom.Point2D; import java.awt.image.BufferedImage; @@ -49,6 +65,7 @@ public class BoardRenderer { private boolean cachedBackgroundImageHasCoordinatesEnabled = false; private int cachedX, cachedY; + private int cachedBoardLength = 0; private BufferedImage cachedStonesImage = emptyImage; private BufferedImage cachedBoardImage = emptyImage; private BufferedImage cachedWallpaperImage = emptyImage; @@ -95,7 +112,7 @@ public void draw(Graphics2D g) { // timer.lap("background"); drawStones(); // timer.lap("stones"); - if (Lizzie.board.inScoreMode() && isMainBoard) { + if (Lizzie.board != null && Lizzie.board.inScoreMode() && isMainBoard) { drawScore(g); } else { drawBranch(); @@ -137,6 +154,14 @@ public Optional bestMoveCoordinateName() { return bestMoves.isEmpty() ? Optional.empty() : Optional.of(bestMoves.get(0).coordinate); } + /** Calculate good values for boardLength, scaledMargin, availableLength, and squareLength */ + public static int availableLength(int boardLength, boolean showCoordinates) { + int[] calculatedPixelMargins = calculatePixelMargins(boardLength, showCoordinates); + return (calculatedPixelMargins != null && calculatedPixelMargins.length >= 1) + ? calculatedPixelMargins[0] + : boardLength; + } + /** Calculate good values for boardLength, scaledMargin, availableLength, and squareLength */ private void setupSizeParameters() { int boardLength0 = boardLength; @@ -163,12 +188,14 @@ private void drawGoban(Graphics2D g0) { // Draw the cached background image if frame size changes if (cachedBackgroundImage.getWidth() != width || cachedBackgroundImage.getHeight() != height + || cachedBoardLength != boardLength || cachedX != x || cachedY != y || cachedBackgroundImageHasCoordinatesEnabled != showCoordinates() - || Lizzie.board.isForceRefresh()) { + || Lizzie.frame.isForceRefresh()) { - Lizzie.board.setForceRefresh(false); + cachedBoardLength = boardLength; + Lizzie.frame.setForceRefresh(false); cachedBackgroundImage = new BufferedImage(width, height, TYPE_INT_ARGB); Graphics2D g = cachedBackgroundImage.createGraphics(); @@ -204,16 +231,16 @@ private void drawGoban(Graphics2D g0) { drawString( g, x + scaledMargin + squareLength * i, - y + scaledMargin / 2, - LizzieFrame.uiFont, + y + scaledMargin / 3, + MainFrame.uiFont, Board.asName(i), stoneRadius * 4 / 5, stoneRadius); drawString( g, x + scaledMargin + squareLength * i, - y - scaledMargin / 2 + boardLength, - LizzieFrame.uiFont, + y - scaledMargin / 3 + boardLength, + MainFrame.uiFont, Board.asName(i), stoneRadius * 4 / 5, stoneRadius); @@ -221,17 +248,17 @@ private void drawGoban(Graphics2D g0) { for (int i = 0; i < Board.boardSize; i++) { drawString( g, - x + scaledMargin / 2, + x + scaledMargin / 3, y + scaledMargin + squareLength * i, - LizzieFrame.uiFont, + MainFrame.uiFont, "" + (Board.boardSize - i), stoneRadius * 4 / 5, stoneRadius); drawString( g, - x - scaledMargin / 2 + +boardLength, + x - scaledMargin / 3 + boardLength, y + scaledMargin + squareLength * i, - LizzieFrame.uiFont, + MainFrame.uiFont, "" + (Board.boardSize - i), stoneRadius * 4 / 5, stoneRadius); @@ -453,6 +480,7 @@ private void renderImages(Graphics2D g) { /** Draw move numbers and/or mark the last played move */ private void drawMoveNumbers(Graphics2D g) { g.setRenderingHint(KEY_ANTIALIASING, VALUE_ANTIALIAS_ON); + if (Lizzie.board == null) return; Board board = Lizzie.board; Optional lastMoveOpt = branchOpt.map(b -> b.data.lastMove).orElse(board.getLastMove()); if (Lizzie.config.allowMoveNumber == 0 && !branchOpt.isPresent()) { @@ -488,7 +516,7 @@ private void drawMoveNumbers(Graphics2D g) { g, x + boardLength / 2, y + boardLength / 2, - LizzieFrame.uiFont, + MainFrame.uiFont, "pass", stoneRadius * 4, stoneRadius * 6); @@ -541,7 +569,7 @@ private void drawMoveNumbers(Graphics2D g) { g, stoneX, stoneY, - LizzieFrame.uiFont, + MainFrame.uiFont, moveNumberString, (float) (stoneRadius * 1.4), (int) (stoneRadius * 1.4)); @@ -619,7 +647,8 @@ private void drawLeelazSuggestions(Graphics2D g) { int suggestionY = y + scaledMargin + squareLength * coords[1]; float hue; - if (isBestMove && !Lizzie.config.colorByWinrateInsteadOfVisits) { + if (isBestMove && !Lizzie.config.colorByWinrateInsteadOfVisits + || hasMaxWinrate && Lizzie.config.colorByWinrateInsteadOfVisits) { hue = cyanHue; } else { double fraction; @@ -717,7 +746,7 @@ private void drawLeelazSuggestions(Graphics2D g) { g, suggestionX, suggestionY, - LizzieFrame.winrateFont, + MainFrame.winrateFont, Font.PLAIN, text, stoneRadius, @@ -728,8 +757,8 @@ private void drawLeelazSuggestions(Graphics2D g) { g, suggestionX, suggestionY + stoneRadius * 2 / 5, - LizzieFrame.uiFont, - Lizzie.frame.getPlayoutsString(move.playouts), + MainFrame.uiFont, + Utils.getPlayoutsString(move.playouts), (float) (stoneRadius * 0.8), stoneRadius * 1.4); } @@ -739,6 +768,7 @@ private void drawLeelazSuggestions(Graphics2D g) { } private void drawNextMoves(Graphics2D g) { + if (Lizzie.board == null) return; g.setColor(Lizzie.board.getData().blackToPlay ? Color.BLACK : Color.WHITE); List nexts = Lizzie.board.getHistory().getNexts(); @@ -802,7 +832,7 @@ private void drawWoodenBoard(Graphics2D g) { * @param boardLength go board's length in pixels; must be boardLength >= BOARD_SIZE - 1 * @return an array containing the three outputs: new boardLength, scaledMargin, availableLength */ - private int[] calculatePixelMargins(int boardLength) { + private static int[] calculatePixelMargins(int boardLength, boolean showCoordinates) { // boardLength -= boardLength*MARGIN/3; // account for the shadows we will draw around the edge // of the board // if (boardLength < Board.BOARD_SIZE - 1) @@ -814,7 +844,7 @@ private int[] calculatePixelMargins(int boardLength) { // decrease boardLength until the availableLength will result in square board intersections double margin = - (showCoordinates() ? 1.5 / (Board.boardSize + 2) : 1.0d / (Board.boardSize + 1)); + (showCoordinates ? (Board.boardSize > 3 ? 0.06 : 0.04) : 0.03) / Board.boardSize * 19.0; boardLength++; do { boardLength--; @@ -1015,7 +1045,7 @@ public void drawTextureImage( * @param g */ private void drawStoneMarkup(Graphics2D g) { - + if (Lizzie.board == null) return; BoardData data = Lizzie.board.getHistory().getData(); data.getProperties() @@ -1044,7 +1074,7 @@ private void drawStoneMarkup(Graphics2D g) { g, moveX, moveY, - LizzieFrame.uiFont, + MainFrame.uiFont, moves[1], (float) labelRadius, labelRadius); @@ -1156,7 +1186,7 @@ private Font makeFont(Font fontBase, int style) { } private int[] calculatePixelMargins() { - return calculatePixelMargins(boardLength); + return calculatePixelMargins(boardLength, showCoordinates()); } /** @@ -1214,8 +1244,8 @@ public Optional convertScreenToCoordinates(int x, int y) { int squareSize = calculateSquareLength(boardLengthWithoutMargins); // transform the pixel coordinates to board coordinates - x = Math.floorDiv(x - this.x - marginLength + squareSize / 2, squareSize); - y = Math.floorDiv(y - this.y - marginLength + squareSize / 2, squareSize); + x = squareSize == 0 ? 0 : Math.floorDiv(x - this.x - marginLength + squareSize / 2, squareSize); + y = squareSize == 0 ? 0 : Math.floorDiv(y - this.y - marginLength + squareSize / 2, squareSize); // return these values if they are valid board coordinates return Board.isValid(x, y) ? Optional.of(new int[] {x, y}) : Optional.empty(); diff --git a/src/main/java/featurecat/lizzie/gui/CommentPane.java b/src/main/java/featurecat/lizzie/gui/CommentPane.java new file mode 100644 index 000000000..1cf7444f2 --- /dev/null +++ b/src/main/java/featurecat/lizzie/gui/CommentPane.java @@ -0,0 +1,158 @@ +package featurecat.lizzie.gui; + +import static java.lang.Math.min; + +import featurecat.lizzie.Lizzie; +import java.awt.BorderLayout; +import java.awt.Font; +import java.awt.event.MouseMotionAdapter; +import java.awt.event.MouseMotionListener; +import java.io.IOException; +import javax.swing.BorderFactory; +import javax.swing.JLabel; +import javax.swing.JScrollPane; +import javax.swing.JTextPane; +import javax.swing.text.BadLocationException; +import javax.swing.text.html.HTMLDocument; +import javax.swing.text.html.StyleSheet; + +/** The window used to display the game. */ +public class CommentPane extends LizziePane { + + // private final BufferStrategy bs; + + // Display Comment + private HTMLDocument htmlDoc; + private LizziePane.HtmlKit htmlKit; + private StyleSheet htmlStyle; + public JScrollPane scrollPane; + private JTextPane commentPane; + private JLabel dragPane = new JLabel("Drag out"); + private MouseMotionListener[] mouseMotionListeners; + private MouseMotionAdapter mouseMotionAdapter; + + /** Creates a window */ + public CommentPane(LizzieMain owner) { + super(owner); + setLayout(new BorderLayout(0, 0)); + + htmlKit = new LizziePane.HtmlKit(); + htmlDoc = (HTMLDocument) htmlKit.createDefaultDocument(); + htmlStyle = htmlKit.getStyleSheet(); + String style = + "body {background:#" + + String.format( + "%02x%02x%02x", + Lizzie.config.commentBackgroundColor.getRed(), + Lizzie.config.commentBackgroundColor.getGreen(), + Lizzie.config.commentBackgroundColor.getBlue()) + + "; color:#" + + String.format( + "%02x%02x%02x", + Lizzie.config.commentFontColor.getRed(), + Lizzie.config.commentFontColor.getGreen(), + Lizzie.config.commentFontColor.getBlue()) + + "; font-family:" + + Lizzie.config.fontName + + ", Consolas, Menlo, Monaco, 'Ubuntu Mono', monospace;" + + (Lizzie.config.commentFontSize > 0 + ? "font-size:" + Lizzie.config.commentFontSize + : "") + + "}"; + htmlStyle.addRule(style); + + commentPane = new JTextPane(); + commentPane.setBorder(BorderFactory.createEmptyBorder()); + commentPane.setEditorKit(htmlKit); + commentPane.setDocument(htmlDoc); + commentPane.setText(""); + commentPane.setEditable(false); + commentPane.setFocusable(false); + scrollPane = new JScrollPane(); + scrollPane.setBorder(BorderFactory.createEmptyBorder()); + scrollPane.setVerticalScrollBarPolicy( + javax.swing.ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED); + add(scrollPane); + scrollPane.setViewportView(commentPane); + setVisible(false); + + // mouseMotionAdapter = new MouseMotionAdapter() { + // @Override + // public void mouseDragged(MouseEvent e) { + // System.out.println("Mouse Dragged"); + // owner.dispatchEvent(e); + // } + // }; + // commentPane.addMouseMotionListener(mouseMotionAdapter); + } + + /** Draw the Comment of the Sgf file */ + public void drawComment() { + if (Lizzie.leelaz != null && Lizzie.leelaz.isLoaded()) { + if (Lizzie.config.showComment) { + setVisible(true); + String comment = Lizzie.board.getHistory().getData().comment; + int fontSize = (int) (min(getWidth(), getHeight()) * 0.0294); + if (Lizzie.config.commentFontSize > 0) { + fontSize = Lizzie.config.commentFontSize; + } else if (fontSize < 16) { + fontSize = 16; + } + Font font = new Font(Lizzie.config.fontName, Font.PLAIN, fontSize); + commentPane.setFont(font); + comment = comment.replaceAll("(\r\n)|(\n)", "
").replaceAll(" ", " "); + addText("" + comment + ""); + } + } + } + + private void addText(String text) { + try { + htmlDoc.remove(0, htmlDoc.getLength()); + htmlKit.insertHTML(htmlDoc, htmlDoc.getLength(), text, 0, 0, null); + commentPane.setCaretPosition(htmlDoc.getLength()); + } catch (BadLocationException | IOException e) { + e.printStackTrace(); + } + } + + public void setDesignMode(boolean mode) { + // if (mode) { + // mouseMotionListeners = commentPane.getMouseMotionListeners(); + // if (mouseMotionListeners != null) { + // for (MouseMotionListener l : mouseMotionListeners) { + // commentPane.removeMouseMotionListener(l); + // } + // } + // } else { + // if (mouseMotionListeners != null) { + // for (MouseMotionListener l : mouseMotionListeners) { + // commentPane.addMouseMotionListener(l); + // } + // } + // } + if (mode) { + remove(scrollPane); + add(dragPane); + commentPane.setVisible(false); + scrollPane.setVisible(false); + dragPane.setVisible(true); + } else { + remove(dragPane); + add(scrollPane); + commentPane.setVisible(true); + scrollPane.setVisible(true); + dragPane.setVisible(false); + } + super.setDesignMode(mode); + revalidate(); + repaint(); + } + + public void setCommentBounds(int x, int y, int width, int height) { + this.setBounds(x, y, width, height); + if (scrollPane != null) { + scrollPane.setSize(width, height); + } + } +} diff --git a/src/main/java/featurecat/lizzie/gui/ConfigDialog.java b/src/main/java/featurecat/lizzie/gui/ConfigDialog.java index df089ffd1..11acac6eb 100644 --- a/src/main/java/featurecat/lizzie/gui/ConfigDialog.java +++ b/src/main/java/featurecat/lizzie/gui/ConfigDialog.java @@ -37,7 +37,6 @@ import javax.swing.text.AttributeSet; import javax.swing.text.BadLocationException; import javax.swing.text.DocumentFilter; -import javax.swing.text.DocumentFilter.FilterBypass; import javax.swing.text.InternationalFormatter; import org.json.JSONArray; import org.json.JSONObject; diff --git a/src/main/java/featurecat/lizzie/gui/GtpConsolePane.java b/src/main/java/featurecat/lizzie/gui/GtpConsolePane.java index 4515a319b..293305e64 100644 --- a/src/main/java/featurecat/lizzie/gui/GtpConsolePane.java +++ b/src/main/java/featurecat/lizzie/gui/GtpConsolePane.java @@ -1,6 +1,7 @@ package featurecat.lizzie.gui; import featurecat.lizzie.Lizzie; +import featurecat.lizzie.util.WindowPosition; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Font; @@ -19,7 +20,6 @@ import javax.swing.JTextPane; import javax.swing.text.BadLocationException; import javax.swing.text.html.HTMLDocument; -import javax.swing.text.html.HTMLEditorKit; import javax.swing.text.html.StyleSheet; import org.json.JSONArray; @@ -29,7 +29,7 @@ public class GtpConsolePane extends JDialog { // Display Comment private HTMLDocument htmlDoc; - private HTMLEditorKit htmlKit; + private LizziePane.HtmlKit htmlKit; private StyleSheet htmlStyle; private JScrollPane scrollPane; private JTextPane console; @@ -44,12 +44,8 @@ public GtpConsolePane(Window owner) { super(owner); setTitle("Gtp Console"); - boolean persisted = - Lizzie.config.persistedUi != null - && Lizzie.config.persistedUi.optJSONArray("gtp-console-position") != null - && Lizzie.config.persistedUi.optJSONArray("gtp-console-position").length() == 4; - if (persisted) { - JSONArray pos = Lizzie.config.persistedUi.getJSONArray("gtp-console-position"); + JSONArray pos = WindowPosition.gtpWindowPos(); + if (pos != null) { this.setBounds(pos.getInt(0), pos.getInt(1), pos.getInt(2), pos.getInt(3)); } else { Insets oi = owner.getInsets(); @@ -60,7 +56,7 @@ public GtpConsolePane(Window owner) { Math.max(owner.getHeight() + oi.top + oi.bottom, 300)); } - htmlKit = new HTMLEditorKit(); + htmlKit = new LizziePane.HtmlKit(); htmlDoc = (HTMLDocument) htmlKit.createDefaultDocument(); htmlStyle = htmlKit.getStyleSheet(); htmlStyle.addRule(Lizzie.config.gtpConsoleStyle); diff --git a/src/main/java/featurecat/lizzie/gui/Input.java b/src/main/java/featurecat/lizzie/gui/Input.java index 636f9f320..cb22f4d7c 100644 --- a/src/main/java/featurecat/lizzie/gui/Input.java +++ b/src/main/java/featurecat/lizzie/gui/Input.java @@ -3,8 +3,13 @@ import static java.awt.event.KeyEvent.*; import featurecat.lizzie.Lizzie; -import java.awt.event.*; -import javax.swing.*; +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.awt.event.MouseMotionListener; +import java.awt.event.MouseWheelEvent; +import java.awt.event.MouseWheelListener; public class Input implements MouseListener, KeyListener, MouseWheelListener, MouseMotionListener { @Override @@ -12,9 +17,13 @@ public void mouseClicked(MouseEvent e) {} @Override public void mousePressed(MouseEvent e) { - if (e.getButton() == MouseEvent.BUTTON1) // left click - Lizzie.frame.onClicked(e.getX(), e.getY()); - else if (e.getButton() == MouseEvent.BUTTON3) // right click + if (e.getButton() == MouseEvent.BUTTON1) { // left click + if (e.getClickCount() == 2) { // TODO: Maybe need to delay check + Lizzie.frame.onDoubleClicked(e.getX(), e.getY()); + } else { + Lizzie.frame.onClicked(e.getX(), e.getY()); + } + } else if (e.getButton() == MouseEvent.BUTTON3) // right click undo(); } @@ -171,6 +180,7 @@ public void keyPressed(KeyEvent e) { // If any controls key is pressed, let's disable analysis mode. // This is probably the user attempting to exit analysis mode. boolean shouldDisableAnalysis = true; + int refreshType = 1; switch (e.getKeyCode()) { case VK_E: @@ -227,7 +237,7 @@ public void keyPressed(KeyEvent e) { case VK_N: // stop the ponder if (Lizzie.leelaz.isPondering()) Lizzie.leelaz.togglePonder(); - LizzieFrame.startNewGame(); + Lizzie.frame.startGame(); break; case VK_SPACE: if (Lizzie.frame.isPlayingAgainstLeelaz) { @@ -277,12 +287,12 @@ public void keyPressed(KeyEvent e) { case VK_S: // stop the ponder if (Lizzie.leelaz.isPondering()) Lizzie.leelaz.togglePonder(); - LizzieFrame.saveFile(); + Lizzie.frame.saveFile(); break; case VK_O: if (Lizzie.leelaz.isPondering()) Lizzie.leelaz.togglePonder(); - LizzieFrame.openFile(); + Lizzie.frame.openFile(); break; case VK_V: @@ -325,8 +335,12 @@ public void keyPressed(KeyEvent e) { case VK_W: if (controlIsPressed(e)) { Lizzie.config.toggleLargeWinrate(); + refreshType = 2; + } else if (e.isAltDown()) { + Lizzie.frame.toggleDesignMode(); } else { Lizzie.config.toggleShowWinrate(); + refreshType = 2; } break; @@ -336,6 +350,7 @@ public void keyPressed(KeyEvent e) { case VK_G: Lizzie.config.toggleShowVariationGraph(); + refreshType = 2; break; case VK_T: @@ -343,6 +358,7 @@ public void keyPressed(KeyEvent e) { Lizzie.config.toggleShowCommentNodeColor(); } else { Lizzie.config.toggleShowComment(); + refreshType = 2; } break; @@ -355,6 +371,7 @@ public void keyPressed(KeyEvent e) { Lizzie.frame.copySgf(); } else { Lizzie.config.toggleCoordinates(); + refreshType = 2; } break; @@ -410,11 +427,17 @@ public void keyPressed(KeyEvent e) { break; case VK_OPEN_BRACKET: - if (Lizzie.frame.BoardPositionProportion > 0) Lizzie.frame.BoardPositionProportion--; + if (Lizzie.frame.boardPositionProportion > 0) { + Lizzie.frame.boardPositionProportion--; + refreshType = 2; + } break; case VK_CLOSE_BRACKET: - if (Lizzie.frame.BoardPositionProportion < 8) Lizzie.frame.BoardPositionProportion++; + if (Lizzie.frame.boardPositionProportion < 8) { + Lizzie.frame.boardPositionProportion++; + refreshType = 2; + } break; case VK_K: @@ -434,6 +457,7 @@ public void keyPressed(KeyEvent e) { case VK_9: if (controlIsPressed(e)) { Lizzie.switchEngine(e.getKeyCode() - VK_0); + refreshType = 0; } break; default: @@ -442,7 +466,7 @@ public void keyPressed(KeyEvent e) { if (shouldDisableAnalysis && Lizzie.board.inAnalysisMode()) Lizzie.board.toggleAnalysis(); - Lizzie.frame.repaint(); + Lizzie.frame.refresh(refreshType); } private boolean wasPonderingWhenControlsShown = false; @@ -453,29 +477,34 @@ public void keyReleased(KeyEvent e) { case VK_X: if (wasPonderingWhenControlsShown) Lizzie.leelaz.togglePonder(); Lizzie.frame.showControls = false; - Lizzie.frame.repaint(); + Lizzie.frame.refresh(1); break; case VK_Z: stopTemporaryBoard(); - Lizzie.frame.repaint(); + Lizzie.frame.refresh(1); break; default: } } + private long wheelWhen; + @Override public void mouseWheelMoved(MouseWheelEvent e) { if (Lizzie.frame.processCommentMouseWheelMoved(e)) { return; } - if (Lizzie.board.inAnalysisMode()) Lizzie.board.toggleAnalysis(); - if (e.getWheelRotation() > 0) { - redo(); - } else if (e.getWheelRotation() < 0) { - undo(); + if (e.getWhen() - wheelWhen > 0) { + wheelWhen = e.getWhen(); + if (Lizzie.board.inAnalysisMode()) Lizzie.board.toggleAnalysis(); + if (e.getWheelRotation() > 0) { + redo(); + } else if (e.getWheelRotation() < 0) { + undo(); + } + Lizzie.frame.refresh(); } - Lizzie.frame.repaint(); } } diff --git a/src/main/java/featurecat/lizzie/gui/LizzieFrame.java b/src/main/java/featurecat/lizzie/gui/LizzieFrame.java index 8a9daf535..5721f1507 100644 --- a/src/main/java/featurecat/lizzie/gui/LizzieFrame.java +++ b/src/main/java/featurecat/lizzie/gui/LizzieFrame.java @@ -4,7 +4,6 @@ import static java.awt.image.BufferedImage.TYPE_INT_RGB; import static java.lang.Math.max; import static java.lang.Math.min; -import static java.lang.Math.round; import com.jhlabs.image.GaussianFilter; import featurecat.lizzie.Lizzie; @@ -16,6 +15,7 @@ import featurecat.lizzie.rules.BoardHistoryNode; import featurecat.lizzie.rules.GIBParser; import featurecat.lizzie.rules.SGFParser; +import featurecat.lizzie.util.Utils; import java.awt.*; import java.awt.BasicStroke; import java.awt.Color; @@ -46,11 +46,13 @@ import javax.imageio.ImageIO; import javax.swing.*; import javax.swing.filechooser.FileNameExtensionFilter; +import javax.swing.text.html.HTMLDocument; +import javax.swing.text.html.StyleSheet; import org.json.JSONArray; import org.json.JSONObject; /** The window used to display the game. */ -public class LizzieFrame extends JFrame { +public class LizzieFrame extends MainFrame { private static final ResourceBundle resourceBundle = ResourceBundle.getBundle("l10n.DisplayStrings"); @@ -97,18 +99,10 @@ public class LizzieFrame extends JFrame { private static VariationTree variationTree; private static WinrateGraph winrateGraph; - public static Font uiFont; - public static Font winrateFont; - private final BufferStrategy bs; private static final int[] outOfBoundCoordinate = new int[] {-1, -1}; public int[] mouseOverCoordinate = outOfBoundCoordinate; - public boolean showControls = false; - public boolean isPlayingAgainstLeelaz = false; - public boolean playerIsBlack = true; - public int winRateGridLines = 3; - public int BoardPositionProportion = Lizzie.config.boardPositionProportion; private long lastAutosaveTime = System.currentTimeMillis(); private boolean isReplayVariation = false; @@ -117,6 +111,9 @@ public class LizzieFrame extends JFrame { private String playerTitle = ""; // Display Comment + private HTMLDocument htmlDoc; + private LizziePane.HtmlKit htmlKit; + private StyleSheet htmlStyle; private JScrollPane scrollPane; private JTextPane commentPane; private BufferedImage cachedCommentImage = new BufferedImage(1, 1, TYPE_INT_ARGB); @@ -165,8 +162,9 @@ public LizzieFrame() { && Lizzie.config.persistedUi.optJSONArray("main-window-position").length() == 4) { JSONArray pos = Lizzie.config.persistedUi.getJSONArray("main-window-position"); this.setBounds(pos.getInt(0), pos.getInt(1), pos.getInt(2), pos.getInt(3)); - this.BoardPositionProportion = - Lizzie.config.persistedUi.optInt("board-postion-propotion", this.BoardPositionProportion); + this.boardPositionProportion = + Lizzie.config.persistedUi.optInt( + "board-position-proportion", this.boardPositionProportion); } else { setSize(960, 600); setLocationRelativeTo(null); // Start centered, needs to be called *after* setSize... @@ -186,11 +184,35 @@ public LizzieFrame() { setExtendedState(Frame.MAXIMIZED_BOTH); } + htmlKit = new LizziePane.HtmlKit(); + htmlDoc = (HTMLDocument) htmlKit.createDefaultDocument(); + htmlStyle = htmlKit.getStyleSheet(); + String style = + "body {background:#" + + String.format( + "%02x%02x%02x", + Lizzie.config.commentBackgroundColor.getRed(), + Lizzie.config.commentBackgroundColor.getGreen(), + Lizzie.config.commentBackgroundColor.getBlue()) + + "; color:#" + + String.format( + "%02x%02x%02x", + Lizzie.config.commentFontColor.getRed(), + Lizzie.config.commentFontColor.getGreen(), + Lizzie.config.commentFontColor.getBlue()) + + "; font-family:" + + Lizzie.config.fontName + + ", Consolas, Menlo, Monaco, 'Ubuntu Mono', monospace;" + + (Lizzie.config.commentFontSize > 0 + ? "font-size:" + Lizzie.config.commentFontSize + : "") + + "}"; + htmlStyle.addRule(style); commentPane = new JTextPane(); + commentPane.setBorder(BorderFactory.createEmptyBorder()); + commentPane.setEditorKit(htmlKit); + commentPane.setDocument(htmlDoc); commentPane.setEditable(false); - commentPane.setMargin(new Insets(5, 5, 5, 5)); - commentPane.setBackground(Lizzie.config.commentBackgroundColor); - commentPane.setForeground(Lizzie.config.commentFontColor); scrollPane = new JScrollPane(); scrollPane.setViewportView(commentPane); scrollPane.setBorder(null); @@ -263,12 +285,12 @@ public void clear() { } } - public static void openConfigDialog() { + public void openConfigDialog() { ConfigDialog configDialog = new ConfigDialog(); configDialog.setVisible(true); } - public static void openChangeMoveDialog() { + public void openChangeMoveDialog() { ChangeMoveDialog changeMoveDialog = new ChangeMoveDialog(); changeMoveDialog.setVisible(true); } @@ -283,38 +305,49 @@ public void toggleGtpConsole() { } } - public static void startNewGame() { + @Override + public void startGame() { GameInfo gameInfo = Lizzie.board.getHistory().getGameInfo(); - NewGameDialog newGameDialog = new NewGameDialog(); - newGameDialog.setGameInfo(gameInfo); - newGameDialog.setVisible(true); - boolean playerIsBlack = newGameDialog.playerIsBlack(); - newGameDialog.dispose(); - if (newGameDialog.isCancelled()) return; + NewGameDialog gameDialog = new NewGameDialog(); + gameDialog.setGameInfo(gameInfo); + gameDialog.setVisible(true); + boolean playerIsBlack = gameDialog.playerIsBlack(); + boolean isNewGame = gameDialog.isNewGame(); + // gameDialog.dispose(); + if (gameDialog.isCancelled()) return; - Lizzie.board.clear(); - Lizzie.board.getHistory().setGameInfo(gameInfo); + if (isNewGame) { + Lizzie.board.clear(); + } Lizzie.leelaz.sendCommand("komi " + gameInfo.getKomi()); - Lizzie.leelaz.sendCommand( - "time_settings 0 " - + Lizzie.config.config.getJSONObject("leelaz").getInt("max-game-thinking-time-seconds") - + " 1"); + Lizzie.leelaz.time_settings(); Lizzie.frame.playerIsBlack = playerIsBlack; + Lizzie.frame.isNewGame = isNewGame; Lizzie.frame.isPlayingAgainstLeelaz = true; boolean isHandicapGame = gameInfo.getHandicap() != 0; - if (isHandicapGame) { - Lizzie.board.getHistory().getData().blackToPlay = false; - Lizzie.leelaz.sendCommand("fixed_handicap " + gameInfo.getHandicap()); - if (playerIsBlack) Lizzie.leelaz.genmove("W"); - } else if (!playerIsBlack) { - Lizzie.leelaz.genmove("B"); + if (isNewGame) { + Lizzie.board.getHistory().setGameInfo(gameInfo); + if (isHandicapGame) { + Lizzie.board.getHistory().getData().blackToPlay = false; + Lizzie.leelaz.sendCommand("fixed_handicap " + gameInfo.getHandicap()); + if (playerIsBlack) Lizzie.leelaz.genmove("W"); + } else if (!playerIsBlack) { + Lizzie.leelaz.genmove("B"); + } + } else { + Lizzie.board.getHistory().setGameInfo(gameInfo); + if (Lizzie.frame.playerIsBlack != Lizzie.board.getData().blackToPlay) { + if (!Lizzie.leelaz.isThinking) { + Lizzie.leelaz.genmove((Lizzie.board.getData().blackToPlay ? "B" : "W")); + } + } } } - public static void editGameInfo() { + public void editGameInfo() { GameInfo gameInfo = Lizzie.board.getHistory().getGameInfo(); GameInfoDialog gameInfoDialog = new GameInfoDialog(); @@ -324,7 +357,7 @@ public static void editGameInfo() { gameInfoDialog.dispose(); } - public static void saveFile() { + public void saveFile() { FileNameExtensionFilter filter = new FileNameExtensionFilter("*.sgf", "SGF"); JSONObject filesystem = Lizzie.config.persisted.getJSONObject("filesystem"); JFileChooser chooser = new JFileChooser(filesystem.getString("last-folder")); @@ -360,7 +393,7 @@ public static void saveFile() { } } - public static void openFile() { + public void openFile() { FileNameExtensionFilter filter = new FileNameExtensionFilter("*.sgf or *.gib", "SGF", "GIB"); JSONObject filesystem = Lizzie.config.persisted.getJSONObject("filesystem"); JFileChooser chooser = new JFileChooser(filesystem.getString("last-folder")); @@ -371,7 +404,7 @@ public static void openFile() { if (result == JFileChooser.APPROVE_OPTION) loadFile(chooser.getSelectedFile()); } - public static void loadFile(File file) { + public void loadFile(File file) { JSONObject filesystem = Lizzie.config.persisted.getJSONObject("filesystem"); if (!(file.getPath().endsWith(".sgf") || file.getPath().endsWith(".gib"))) { file = new File(file.getPath() + ".sgf"); @@ -404,7 +437,7 @@ public static void loadFile(File file) { private boolean cachedLargeWinrate = true; private boolean cachedShowComment = true; private boolean redrawBackgroundAnyway = false; - private int cachedBoardPositionProportion = BoardPositionProportion; + private int cachedBoardPositionProportion = boardPositionProportion; /** * Draws the game board and interface @@ -420,6 +453,7 @@ public void paint(Graphics g0) { Optional backgroundG; if (cachedBackgroundWidth != width || cachedBackgroundHeight != height + || cachedBoardPositionProportion != boardPositionProportion || redrawBackgroundAnyway) { backgroundG = Optional.of(createBackground()); } else { @@ -435,10 +469,20 @@ public void paint(Graphics g0) { int bottomInset = this.getInsets().bottom; int maxBound = Math.max(width, height); + boolean noWinrate = !Lizzie.config.showWinrate; + boolean noVariation = !Lizzie.config.showVariationGraph; + boolean noBasic = !Lizzie.config.showCaptured; + boolean noSubBoard = !Lizzie.config.showSubBoard; + boolean noComment = !Lizzie.config.showComment; // board int maxSize = (int) (min(width - leftInset - rightInset, height - topInset - bottomInset)); maxSize = max(maxSize, Board.boardSize + 5); // don't let maxWidth become too small - int boardX = (width - maxSize) / 8 * BoardPositionProportion; + int boardX = (width - maxSize) / 8 * boardPositionProportion; + if (noBasic && noWinrate && noSubBoard) { + boardX = leftInset; + } else if (noVariation && noComment) { + boardX = (width - maxSize); + } int boardY = topInset + (height - topInset - bottomInset - maxSize) / 2; int panelMargin = (int) (maxSize * 0.02); @@ -495,7 +539,7 @@ public void paint(Graphics g0) { if (width >= height) { // Landscape mode - if (Lizzie.config.showLargeSubBoard()) { + if (Lizzie.config.showLargeSubBoard() && !noSubBoard) { boardX = width - maxSize - panelMargin; int spaceW = boardX - panelMargin - leftInset; int spaceH = height - topInset - bottomInset; @@ -503,7 +547,7 @@ public void paint(Graphics g0) { int panelH = spaceH / 4; // captured stones - capw = panelW; + capw = (noVariation && noComment) ? spaceW : panelW; caph = (int) (panelH * 0.2); // move statistics (winrate bar) staty = capy + caph; @@ -522,7 +566,7 @@ public void paint(Graphics g0) { subBoardWidth = spaceW; subBoardHeight = ponderingY - subBoardY; subBoardLength = Math.min(subBoardWidth, subBoardHeight); - subBoardX = statx + (statw + vw - subBoardLength) / 2; + subBoardX = statx + (spaceW - subBoardLength) / 2; } else if (Lizzie.config.showLargeWinrate()) { boardX = width - maxSize - panelMargin; int spaceW = boardX - panelMargin - leftInset; @@ -555,7 +599,7 @@ public void paint(Graphics g0) { } } else { // Portrait mode - if (Lizzie.config.showLargeSubBoard()) { + if (Lizzie.config.showLargeSubBoard() && !noSubBoard) { // board maxSize = (int) (maxSize * 0.8); boardY = height - maxSize - bottomInset; @@ -588,7 +632,7 @@ public void paint(Graphics g0) { subBoardY = capy + (gry + grh - capy - subBoardLength) / 2; // pondering message ponderingY = height; - } else if (Lizzie.config.showLargeWinrate()) { + } else if (Lizzie.config.showLargeWinrate() && !noWinrate) { // board maxSize = (int) (maxSize * 0.8); boardY = height - maxSize - bottomInset; @@ -809,9 +853,9 @@ private Graphics2D createBackground() { cachedShowLargeSubBoard = Lizzie.config.showLargeSubBoard(); cachedLargeWinrate = Lizzie.config.showLargeWinrate(); cachedShowComment = Lizzie.config.showComment; - cachedBoardPositionProportion = BoardPositionProportion; + cachedBoardPositionProportion = boardPositionProportion; - redrawBackgroundAnyway = false; + // redrawBackgroundAnyway = false; Graphics2D g = cachedBackground.createGraphics(); g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); @@ -834,7 +878,7 @@ private void drawContainer(Graphics g, int vx, int vy, int vw, int vh) { || vy + vh > cachedBackground.getMinY() + cachedBackground.getHeight()) { return; } - + redrawBackgroundAnyway = false; BufferedImage result = new BufferedImage(vw, vh, TYPE_INT_ARGB); filter20.filter(cachedBackground.getSubimage(vx, vy, vw, vh), result); g.drawImage(result, vx, vy, null); @@ -849,7 +893,7 @@ private void drawPonderingState(Graphics2D g, String text, int x, int y, double if (Lizzie.leelaz != null && Lizzie.leelaz.isLoaded()) { int mainBoardX = boardRenderer.getLocation().x; if (getWidth() > getHeight() && (mainBoardX > x) && stringWidth > (mainBoardX - x)) { - text = truncateStringByWidth(text, fm, mainBoardX - x); + text = Utils.truncateStringByWidth(text, fm, mainBoardX - x); stringWidth = fm.stringWidth(text); } } @@ -878,62 +922,11 @@ private void drawPonderingState(Graphics2D g, String text, int x, int y, double text, x + (width - stringWidth) / 2, y + stringHeight + (height - stringHeight) / 2); } - /** - * @return a shorter, rounded string version of playouts. e.g. 345 -> 345, 1265 -> 1.3k, 44556 -> - * 45k, 133523 -> 134k, 1234567 -> 1.2m - */ - public String getPlayoutsString(int playouts) { - if (playouts >= 1_000_000) { - double playoutsDouble = (double) playouts / 100_000; // 1234567 -> 12.34567 - return round(playoutsDouble) / 10.0 + "m"; - } else if (playouts >= 10_000) { - double playoutsDouble = (double) playouts / 1_000; // 13265 -> 13.265 - return round(playoutsDouble) + "k"; - } else if (playouts >= 1_000) { - double playoutsDouble = (double) playouts / 100; // 1265 -> 12.65 - return round(playoutsDouble) / 10.0 + "k"; - } else { - return String.valueOf(playouts); - } - } - - /** - * Truncate text that is too long for the given width - * - * @param line - * @param fm - * @param fitWidth - * @return fitted - */ - private static String truncateStringByWidth(String line, FontMetrics fm, int fitWidth) { - if (line.isEmpty()) { - return ""; - } - int width = fm.stringWidth(line); - if (width > fitWidth) { - int guess = line.length() * fitWidth / width; - String before = line.substring(0, guess).trim(); - width = fm.stringWidth(before); - if (width > fitWidth) { - int diff = width - fitWidth; - int i = 0; - for (; (diff > 0 && i < 5); i++) { - diff = diff - fm.stringWidth(line.substring(guess - i - 1, guess - i)); - } - return line.substring(0, guess - i).trim(); - } else { - return before; - } - } else { - return line; - } - } - private GaussianFilter filter20 = new GaussianFilter(20); private GaussianFilter filter10 = new GaussianFilter(10); /** Display the controls */ - void drawControls() { + public void drawControls() { userAlreadyKnowsAboutCommandString = true; cachedImage = new BufferedImage(getWidth(), getHeight(), TYPE_INT_ARGB); @@ -1210,10 +1203,14 @@ private void drawCaptured(Graphics2D g, int posX, int posY, int width, int heigh int diam = height / 3; int smallDiam = diam / 2; int bdiam = diam, wdiam = diam; - if (Lizzie.board.inScoreMode()) { - // do nothing - } else if (Lizzie.board.getHistory().isBlacksTurn()) { - wdiam = smallDiam; + if (Lizzie.board != null) { + if (Lizzie.board.inScoreMode()) { + // do nothing + } else if (Lizzie.board.getHistory().isBlacksTurn()) { + wdiam = smallDiam; + } else { + bdiam = smallDiam; + } } else { bdiam = smallDiam; } @@ -1228,6 +1225,9 @@ private void drawCaptured(Graphics2D g, int posX, int posY, int width, int heigh // Draw captures String bval, wval; setPanelFont(g, (float) (height * 0.18)); + if (Lizzie.board == null) { + return; + } if (Lizzie.board.inScoreMode()) { double score[] = Lizzie.board.getScore(Lizzie.board.scoreStones()); bval = String.format("%.0f", score[0]); @@ -1283,6 +1283,20 @@ public void onClicked(int x, int y) { repaint(); } + public void onDoubleClicked(int x, int y) { + // Check for board double click + Optional boardCoordinates = boardRenderer.convertScreenToCoordinates(x, y); + if (boardCoordinates.isPresent()) { + int[] coords = boardCoordinates.get(); + if (!isPlayingAgainstLeelaz) { + int moveNumber = Lizzie.board.moveNumberByCoord(coords); + if (moveNumber > 0) { + Lizzie.board.goToMoveNumberBeyondBranch(moveNumber); + } + } + } + } + private final Consumer placeVariation = v -> Board.asCoordinates(v).ifPresent(c -> Lizzie.board.place(c[0], c[1])); @@ -1327,6 +1341,7 @@ public void onMouseDragged(int x, int y) { * * @return true when the scroll event was processed by this method */ + @Override public boolean processCommentMouseWheelMoved(MouseWheelEvent e) { if (Lizzie.config.showComment && commentRect.contains(e.getX(), e.getY())) { scrollPane.dispatchEvent(e); @@ -1475,6 +1490,7 @@ private void drawComment(Graphics2D g, int x, int y, int w, int h) { } Font font = new Font(Lizzie.config.fontName, Font.PLAIN, fontSize); commentPane.setFont(font); + comment = comment.replaceAll("(\r\n)|(\n)", "
").replaceAll(" ", " "); commentPane.setText(comment); commentPane.setSize(w, h); createCommentImage(!comment.equals(this.cachedComment), w, h); @@ -1489,57 +1505,6 @@ private void drawComment(Graphics2D g, int x, int y, int w, int h) { cachedComment = comment; } - public double lastWinrateDiff(BoardHistoryNode node) { - - // Last winrate - Optional lastNode = node.previous().flatMap(n -> Optional.of(n.getData())); - boolean validLastWinrate = lastNode.map(d -> d.getPlayouts() > 0).orElse(false); - double lastWR = validLastWinrate ? lastNode.get().winrate : 50; - - // Current winrate - BoardData data = node.getData(); - boolean validWinrate = false; - double curWR = 50; - if (data == Lizzie.board.getHistory().getData()) { - Leelaz.WinrateStats stats = Lizzie.leelaz.getWinrateStats(); - curWR = stats.maxWinrate; - validWinrate = (stats.totalPlayouts > 0); - if (isPlayingAgainstLeelaz - && playerIsBlack == !Lizzie.board.getHistory().getData().blackToPlay) { - validWinrate = false; - } - } else { - validWinrate = (data.getPlayouts() > 0); - curWR = validWinrate ? data.winrate : 100 - lastWR; - } - - // Last move difference winrate - if (validLastWinrate && validWinrate) { - return 100 - lastWR - curWR; - } else { - return 0; - } - } - - public Color getBlunderNodeColor(BoardHistoryNode node) { - if (Lizzie.config.nodeColorMode == 1 && node.getData().blackToPlay - || Lizzie.config.nodeColorMode == 2 && !node.getData().blackToPlay) { - return Color.WHITE; - } - double diffWinrate = lastWinrateDiff(node); - Optional st = - diffWinrate >= 0 - ? Lizzie.config.blunderWinrateThresholds.flatMap( - l -> l.stream().filter(t -> (t > 0 && t <= diffWinrate)).reduce((f, s) -> s)) - : Lizzie.config.blunderWinrateThresholds.flatMap( - l -> l.stream().filter(t -> (t < 0 && t >= diffWinrate)).reduce((f, s) -> f)); - if (st.isPresent()) { - return Lizzie.config.blunderNodeColors.map(m -> m.get(st.get())).get(); - } else { - return Color.WHITE; - } - } - public void replayBranch() { if (isReplayVariation) return; int replaySteps = boardRenderer.getReplayBranch(); diff --git a/src/main/java/featurecat/lizzie/gui/LizzieLayout.java b/src/main/java/featurecat/lizzie/gui/LizzieLayout.java new file mode 100644 index 000000000..9a5f03d5b --- /dev/null +++ b/src/main/java/featurecat/lizzie/gui/LizzieLayout.java @@ -0,0 +1,642 @@ +package featurecat.lizzie.gui; + +import static java.lang.Math.max; +import static java.lang.Math.min; + +import featurecat.lizzie.Lizzie; +import featurecat.lizzie.rules.Board; +import java.awt.Component; +import java.awt.Container; +import java.awt.Dimension; +import java.awt.Insets; +import java.awt.LayoutManager2; + +public class LizzieLayout implements LayoutManager2, java.io.Serializable { + int hgap; + int vgap; + + private Component mainBoard; + private Component subBoard; + private Component winratePane; + private Component variationPane; + private Component basicInfoPane; + private Component commentPane; + private Component consolePane; + + public static final String MAIN_BOARD = "mainBoard"; + public static final String SUB_BOARD = "subBoard"; + public static final String WINRATE = "winratePane"; + public static final String VARIATION = "variationPane"; + public static final String BASIC_INFO = "basicInfoPane"; + public static final String COMMENT = "commentPane"; + public static final String CONSOLE = "consolePane"; + + public static final int MODE_FUSION = 0; + public static final int MODE_SEPARATION = 1; + + private int mode = 0; + + public LizzieLayout() { + this(3, 0); + } + + public LizzieLayout(int hgap, int vgap) { + this.hgap = hgap; + this.vgap = vgap; + } + + public int getHgap() { + return hgap; + } + + public void setHgap(int hgap) { + this.hgap = hgap; + } + + public int getVgap() { + return vgap; + } + + public void setVgap(int vgap) { + this.vgap = vgap; + } + + public void addLayoutComponent(Component comp, Object constraints) { + synchronized (comp.getTreeLock()) { + if ((constraints == null) || (constraints instanceof String)) { + addLayoutComponent((String) constraints, comp); + } else { + throw new IllegalArgumentException( + "cannot add to layout: constraint must be a string (or null)"); + } + } + } + + /** @deprecated replaced by addLayoutComponent(Component, Object). */ + @Deprecated + public void addLayoutComponent(String name, Component comp) { + synchronized (comp.getTreeLock()) { + if (name == null) { + name = MAIN_BOARD; + } + + if (BASIC_INFO.equals(name)) { + basicInfoPane = comp; + } else if (MAIN_BOARD.equals(name)) { + mainBoard = comp; + } else if (VARIATION.equals(name)) { + variationPane = comp; + } else if (WINRATE.equals(name)) { + winratePane = comp; + } else if (SUB_BOARD.equals(name)) { + subBoard = comp; + } else if (COMMENT.equals(name)) { + commentPane = comp; + } else if (CONSOLE.equals(name)) { + consolePane = comp; + } else { + throw new IllegalArgumentException("cannot add to layout: unknown constraint: " + name); + } + } + } + + public void removeLayoutComponent(Component comp) { + synchronized (comp.getTreeLock()) { + if (comp == basicInfoPane) { + basicInfoPane = null; + } else if (comp == mainBoard) { + mainBoard = null; + } else if (comp == variationPane) { + variationPane = null; + } else if (comp == winratePane) { + winratePane = null; + } else if (comp == subBoard) { + subBoard = null; + } + if (comp == commentPane) { + commentPane = null; + } else if (comp == consolePane) { + consolePane = null; + } + } + } + + public Component getLayoutComponent(Object constraints) { + if (BASIC_INFO.equals(constraints)) { + return basicInfoPane; + } else if (MAIN_BOARD.equals(constraints)) { + return mainBoard; + } else if (SUB_BOARD.equals(constraints)) { + return subBoard; + } else if (VARIATION.equals(constraints)) { + return variationPane; + } else if (WINRATE.equals(constraints)) { + return winratePane; + } else if (COMMENT.equals(constraints)) { + return commentPane; + } else if (CONSOLE.equals(constraints)) { + return consolePane; + } else { + throw new IllegalArgumentException( + "cannot get component: unknown constraint: " + constraints); + } + } + + public Component getLayoutComponent(Container target, Object constraints) { + boolean ltr = target.getComponentOrientation().isLeftToRight(); + Component result = null; + + if (MAIN_BOARD.equals(constraints)) { + result = mainBoard; + } else if (SUB_BOARD.equals(constraints)) { + result = subBoard; + } else if (COMMENT.equals(constraints)) { + result = commentPane; + } else if (VARIATION.equals(constraints)) { + result = variationPane; + } else if (WINRATE.equals(constraints)) { + result = winratePane; + } else if (BASIC_INFO.equals(constraints)) { + result = basicInfoPane; + } else { + throw new IllegalArgumentException( + "cannot get component: invalid constraint: " + constraints); + } + + return result; + } + + public Object getConstraints(Component comp) { + if (comp == null) { + return null; + } + if (comp == basicInfoPane) { + return BASIC_INFO; + } else if (comp == mainBoard) { + return MAIN_BOARD; + } else if (comp == subBoard) { + return SUB_BOARD; + } else if (comp == variationPane) { + return VARIATION; + } else if (comp == winratePane) { + return WINRATE; + } else if (comp == commentPane) { + return COMMENT; + } else if (comp == consolePane) { + return CONSOLE; + } + return null; + } + + public Dimension minimumLayoutSize(Container target) { + synchronized (target.getTreeLock()) { + Dimension dim = new Dimension(0, 0); + + boolean ltr = target.getComponentOrientation().isLeftToRight(); + Component c = null; + + if ((c = getChild(WINRATE, ltr)) != null) { + Dimension d = c.getMinimumSize(); + dim.width += d.width + hgap; + dim.height = Math.max(d.height, dim.height); + } + if ((c = getChild(VARIATION, ltr)) != null) { + Dimension d = c.getMinimumSize(); + dim.width += d.width + hgap; + dim.height = Math.max(d.height, dim.height); + } + if ((c = getChild(BASIC_INFO, ltr)) != null) { + Dimension d = c.getMinimumSize(); + dim.width += d.width; + dim.height = Math.max(d.height, dim.height); + } + if ((c = getChild(MAIN_BOARD, ltr)) != null) { + Dimension d = c.getMinimumSize(); + dim.width = Math.max(d.width, dim.width); + dim.height += d.height + vgap; + } + if ((c = getChild(SUB_BOARD, ltr)) != null) { + Dimension d = c.getMinimumSize(); + dim.width = Math.max(d.width, dim.width); + dim.height += d.height + vgap; + } + + Insets insets = target.getInsets(); + dim.width += insets.left + insets.right; + dim.height += insets.top + insets.bottom; + + return dim; + } + } + + public Dimension preferredLayoutSize(Container target) { + synchronized (target.getTreeLock()) { + Dimension dim = new Dimension(0, 0); + + boolean ltr = target.getComponentOrientation().isLeftToRight(); + Component c = null; + + if ((c = getChild(WINRATE, ltr)) != null) { + Dimension d = c.getPreferredSize(); + dim.width += d.width + hgap; + dim.height = Math.max(d.height, dim.height); + } + if ((c = getChild(VARIATION, ltr)) != null) { + Dimension d = c.getPreferredSize(); + dim.width += d.width + hgap; + dim.height = Math.max(d.height, dim.height); + } + if ((c = getChild(BASIC_INFO, ltr)) != null) { + Dimension d = c.getPreferredSize(); + dim.width += d.width; + dim.height = Math.max(d.height, dim.height); + } + if ((c = getChild(MAIN_BOARD, ltr)) != null) { + Dimension d = c.getPreferredSize(); + dim.width = Math.max(d.width, dim.width); + dim.height += d.height + vgap; + } + if ((c = getChild(SUB_BOARD, ltr)) != null) { + Dimension d = c.getPreferredSize(); + dim.width = Math.max(d.width, dim.width); + dim.height += d.height + vgap; + } + + Insets insets = target.getInsets(); + dim.width += insets.left + insets.right; + dim.height += insets.top + insets.bottom; + + return dim; + } + } + + public Dimension maximumLayoutSize(Container target) { + return new Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE); + } + + public float getLayoutAlignmentX(Container parent) { + return 0.5f; + } + + public float getLayoutAlignmentY(Container parent) { + return 0.5f; + } + + public void invalidateLayout(Container target) {} + + public void layoutContainer(Container target) { + if (mode != MODE_FUSION) { + return; + } + synchronized (target.getTreeLock()) { + Container main = getMain(target); + Insets insets = target.getInsets(); + + int x = target.getX(); + int y = target.getY(); + int width = target.getWidth(); + int height = target.getHeight(); + + // layout parameters + + int topInset = insets.top; + int leftInset = insets.left; + int rightInset = insets.right; + int bottomInset = insets.bottom; + int maxBound = Math.max(width, height); + + boolean ltr = target.getComponentOrientation().isLeftToRight(); + Component c = null; + boolean noWinrate = (getChild(WINRATE, ltr) == null || !Lizzie.config.showWinrate); + boolean noVariation = (getChild(VARIATION, ltr) == null || !Lizzie.config.showVariationGraph); + boolean noBasic = (getChild(BASIC_INFO, ltr) == null || !Lizzie.config.showCaptured); + boolean noSubBoard = (getChild(SUB_BOARD, ltr) == null || !Lizzie.config.showSubBoard); + boolean noComment = (getChild(COMMENT, ltr) == null || !Lizzie.config.showComment); + // boolean onlyMainBoard = noWinrate && noVariation && noBasic && noSubBoard && + // noComment; + + // board + int maxSize = (int) (min(width - leftInset - rightInset, height - topInset - bottomInset)); + maxSize = + BoardRenderer.availableLength( + max(maxSize, Board.boardSize + 5), + Lizzie.config.showCoordinates); // don't let maxWidth become too small + int boardX = + (width - maxSize) + / 8 + * ((main == null || !(main instanceof LizzieMain)) + ? Lizzie.config.boardPositionProportion + : ((LizzieMain) main).boardPositionProportion); + if (noBasic && noWinrate && noSubBoard) { + boardX = leftInset; + } else if (noVariation && noComment) { + boardX = (width - maxSize); + } + int boardY = topInset + (height - topInset - bottomInset - maxSize) / 2; + + int panelMargin = (int) (maxSize * 0.02); + + // captured stones + int capx = leftInset; + int capy = topInset; + int capw = boardX - panelMargin - leftInset; + int caph = maxSize / 8; + + // move statistics (winrate bar) + // boardX equals width of space on each side + int statx = capx; + int staty = capy + caph; + int statw = capw; + int stath = maxSize / 10; + + // winrate graph + int grx = statx; + int gry = staty + stath; + int grw = statw; + int grh = maxSize / 3; + + // variation tree container + int vx = boardX + maxSize + panelMargin; + int vy = capy; + int vw = width - vx - rightInset; + int vh = height - vy - bottomInset; + + // pondering message + double ponderingSize = .02; + int ponderingY = + height - bottomInset - (int) (maxSize * 0.033) - (int) (maxBound * ponderingSize); + + // subboard + int subBoardY = gry + grh + 1; + int subBoardWidth = grw; + int subBoardHeight = ponderingY - subBoardY; + int subBoardLength = BoardRenderer.availableLength(min(subBoardWidth, subBoardHeight), false); + int subBoardX = statx + (statw - subBoardLength) / 2; + + if (width >= height) { + // Landscape mode + if (Lizzie.config.showLargeSubBoard() && !noSubBoard) { + boardX = width - maxSize - panelMargin; + int spaceW = boardX - panelMargin - leftInset; + int spaceH = height - topInset - bottomInset; + int panelW = spaceW / 2; + int panelH = spaceH / 4; + + // captured stones + capw = (noVariation && noComment) ? spaceW : panelW; + caph = (int) (panelH * 0.2); + // move statistics (winrate bar) + staty = capy + caph; + statw = capw; + stath = (int) (panelH * 0.4); + // winrate graph + gry = staty + stath; + grw = statw; + grh = panelH - caph - stath; + // variation tree container + vx = statx + statw; + vw = panelW; + vh = panelH; + // subboard + subBoardY = gry + grh; + subBoardWidth = spaceW; + subBoardHeight = ponderingY - subBoardY; + subBoardLength = + BoardRenderer.availableLength(Math.min(subBoardWidth, subBoardHeight), false); + subBoardX = statx + (spaceW - subBoardLength) / 2; + } else if (Lizzie.config.showLargeWinrate() && !noWinrate) { + boardX = width - maxSize - panelMargin; + int spaceW = boardX - panelMargin - leftInset; + int spaceH = height - topInset - bottomInset; + int panelW = spaceW / 2; + int panelH = spaceH / 4; + + // captured stones + capy = topInset + panelH + 1; + capw = spaceW; + caph = (int) ((ponderingY - topInset - panelH) * 0.15); + // move statistics (winrate bar) + staty = capy + caph; + statw = capw; + stath = caph; + // winrate graph + gry = staty + stath; + grw = statw; + grh = ponderingY - gry; + // variation tree container + vx = leftInset + panelW; + vw = panelW; + vh = panelH; + // subboard + subBoardY = topInset; + subBoardWidth = panelW - leftInset; + subBoardHeight = panelH; + subBoardLength = + BoardRenderer.availableLength(Math.min(subBoardWidth, subBoardHeight), false); + subBoardX = statx + (vw - subBoardLength) / 2; + } + } else { + // Portrait mode + if (Lizzie.config.showLargeSubBoard() && !noSubBoard) { + // board + maxSize = + BoardRenderer.availableLength((int) (maxSize * 0.8), Lizzie.config.showCoordinates); + boardY = height - maxSize - bottomInset; + int spaceW = width - leftInset - rightInset; + int spaceH = boardY - panelMargin - topInset; + int panelW = spaceW / 2; + int panelH = spaceH / 2; + boardX = (spaceW - maxSize) / 2 + leftInset; + + // captured stones + capw = panelW / 2; + caph = panelH / 2; + // move statistics (winrate bar) + staty = capy + caph; + statw = capw; + stath = caph; + // winrate graph + gry = staty + stath; + grw = statw; + grh = spaceH - caph - stath; + // variation tree container + vx = capx + capw; + vw = panelW / 2; + vh = spaceH; + // subboard + subBoardX = vx + vw; + subBoardWidth = panelW; + subBoardHeight = boardY - topInset; + subBoardLength = + BoardRenderer.availableLength(Math.min(subBoardWidth, subBoardHeight), false); + subBoardY = capy + (gry + grh - capy - subBoardLength) / 2; + // pondering message + ponderingY = height; + } else if (Lizzie.config.showLargeWinrate() && !noWinrate) { + // board + maxSize = + BoardRenderer.availableLength((int) (maxSize * 0.8), Lizzie.config.showCoordinates); + boardY = height - maxSize - bottomInset; + int spaceW = width - leftInset - rightInset; + int spaceH = boardY - panelMargin - topInset; + int panelW = spaceW / 2; + int panelH = spaceH / 2; + boardX = (spaceW - maxSize) / 2 + leftInset; + + // captured stones + capw = panelW / 2; + caph = panelH / 4; + // move statistics (winrate bar) + statx = capx + capw; + staty = capy; + statw = capw; + stath = caph; + // winrate graph + gry = staty + stath; + grw = spaceW; + grh = boardY - gry - 1; + // variation tree container + vx = statx + statw; + vy = capy; + vw = panelW / 2; + vh = caph; + // subboard + subBoardY = topInset; + subBoardWidth = panelW / 2; + subBoardHeight = gry - topInset; + subBoardLength = + BoardRenderer.availableLength(Math.min(subBoardWidth, subBoardHeight), false); + subBoardX = vx + vw; + // pondering message + ponderingY = height; + } else { + // Normal + // board + boardY = (height - maxSize + topInset - bottomInset) / 2; + int spaceW = width - leftInset - rightInset; + int spaceH = boardY - panelMargin - topInset; + int panelW = spaceW / 2; + int panelH = spaceH / 2; + + // captured stones + capw = panelW * 3 / 4; + caph = panelH / 2; + // move statistics (winrate bar) + statx = capx + capw; + staty = capy; + statw = capw; + stath = caph; + // winrate graph + grx = capx; + gry = staty + stath; + grw = capw + statw; + grh = boardY - gry; + // subboard + subBoardX = grx + grw; + subBoardWidth = panelW / 2; + subBoardHeight = boardY - topInset; + subBoardLength = + BoardRenderer.availableLength(Math.min(subBoardWidth, subBoardHeight), false); + subBoardY = capy + (boardY - topInset - subBoardLength) / 2; + // variation tree container + vx = leftInset + panelW; + vy = boardY + maxSize; + vw = panelW; + vh = height - vy - bottomInset; + } + } + + // variation tree + int treex = vx; + int treey = vy; + int treew = vw; + int treeh = vh; + + // comment panel + int cx = vx, cy = vy, cw = vw, ch = vh; + if (Lizzie.config.showComment) { + if (width >= height) { + if (Lizzie.config.showVariationGraph) { + treeh = vh / 2; + cy = vy + treeh; + ch = treeh; + } + } else { + if (Lizzie.config.showVariationGraph) { + if (Lizzie.config.showLargeSubBoard()) { + treeh = vh / 2; + cy = vy + treeh; + ch = treeh; + } else { + treew = vw / 2; + cx = vx + treew; + cw = treew; + } + } + } + } + + if ((c = getChild(MAIN_BOARD, ltr)) != null) { + c.setBounds(x + boardX, y + boardY, maxSize, maxSize); + // c.repaint(); + } + if ((c = getChild(SUB_BOARD, ltr)) != null) { + c.setBounds(x + subBoardX, y + subBoardY, subBoardLength, subBoardLength); + // c.repaint(); + } + if ((c = getChild(BASIC_INFO, ltr)) != null) { + c.setBounds(x + capx, y + capy, capw, caph); + } + if ((c = getChild(WINRATE, ltr)) != null) { + c.setBounds(x + statx, y + staty, statw, stath + grh); + } + if ((c = getChild(VARIATION, ltr)) != null) { + c.setBounds(x + treex, y + treey, treew, treeh); + } + if ((c = getChild(COMMENT, ltr)) != null) { + // ((CommentPane)c).setCommentBounds(x + cx, y + cy, cw, ch); + c.setBounds(x + cx, y + cy, cw, ch); + c.repaint(); + } + } + } + + private Component getChild(String key, boolean ltr) { + Component result = null; + + if (key == MAIN_BOARD) { + result = mainBoard; + } else if (key == SUB_BOARD) { + result = subBoard; + } else if (key == VARIATION) { + result = variationPane; + } else if (key == WINRATE) { + result = winratePane; + } else if (key == COMMENT) { + result = commentPane; + } else if (key == BASIC_INFO) { + result = basicInfoPane; + } + return result; + } + + public String toString() { + return getClass().getName() + "[hgap=" + hgap + ",vgap=" + vgap + "]"; + } + + private Container getMain(Container target) { + Container p = (target != null) ? target.getParent() : null; + while (p != null && !(p instanceof LizzieMain)) { + p = p.getParent(); + } + return p; + } + + public int getMode() { + return mode; + } + + public void setMode(int mode) { + this.mode = mode; + } +} diff --git a/src/main/java/featurecat/lizzie/gui/LizzieMain.java b/src/main/java/featurecat/lizzie/gui/LizzieMain.java new file mode 100644 index 000000000..72a70afde --- /dev/null +++ b/src/main/java/featurecat/lizzie/gui/LizzieMain.java @@ -0,0 +1,746 @@ +package featurecat.lizzie.gui; + +import static java.awt.image.BufferedImage.TYPE_INT_ARGB; +import static java.awt.image.BufferedImage.TYPE_INT_RGB; +import static java.lang.Math.max; + +import com.jhlabs.image.GaussianFilter; +import featurecat.lizzie.Lizzie; +import featurecat.lizzie.analysis.GameInfo; +import featurecat.lizzie.analysis.MoveData; +import featurecat.lizzie.rules.GIBParser; +import featurecat.lizzie.rules.SGFParser; +import featurecat.lizzie.util.Utils; +import featurecat.lizzie.util.WindowPosition; +import java.awt.Color; +import java.awt.Font; +import java.awt.FontFormatException; +import java.awt.FontMetrics; +import java.awt.Frame; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Rectangle; +import java.awt.RenderingHints; +import java.awt.TexturePaint; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.util.Optional; +import java.util.ResourceBundle; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import javax.imageio.ImageIO; +import javax.swing.JFileChooser; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.filechooser.FileNameExtensionFilter; +import org.json.JSONArray; +import org.json.JSONObject; + +public class LizzieMain extends MainFrame { + public static final ResourceBundle resourceBundle = + ResourceBundle.getBundle("l10n.DisplayStrings"); + + public static Input input; + public static BasicInfoPane basicInfoPane; + private static final String DEFAULT_TITLE = resourceBundle.getString("LizzieFrame.title"); + + public static BoardPane boardPane; + public static SubBoardPane subBoardPane; + public static WinratePane winratePane; + public static VariationTreePane variationTreePane; + public static CommentPane commentPane; + public static boolean designMode; + private LizzieLayout layout; + + private static final BufferedImage emptyImage = new BufferedImage(1, 1, TYPE_INT_ARGB); + + public BufferedImage cachedBackground; + private BufferedImage cachedBasicInfoContainer = emptyImage; + private BufferedImage cachedWinrateContainer = emptyImage; + private BufferedImage cachedVariationContainer = emptyImage; + + private BufferedImage cachedWallpaperImage = emptyImage; + private int cachedBackgroundWidth = 0, cachedBackgroundHeight = 0; + private boolean redrawBackgroundAnyway = false; + + private static final int[] outOfBoundCoordinate = new int[] {-1, -1}; + public int[] mouseOverCoordinate = outOfBoundCoordinate; + + // Save the player title + private String playerTitle = ""; + + // Show the playouts in the title + private ScheduledExecutorService showPlayouts = Executors.newScheduledThreadPool(1); + private long lastPlayouts = 0; + private String visitsString = ""; + public boolean isDrawVisitsInTitle = true; + + static { + // load fonts + try { + uiFont = new Font("SansSerif", Font.TRUETYPE_FONT, 12); + // Font.createFont( + // Font.TRUETYPE_FONT, + // Thread.currentThread() + // .getContextClassLoader() + // .getResourceAsStream("fonts/OpenSans-Regular.ttf")); + winrateFont = + Font.createFont( + Font.TRUETYPE_FONT, + Thread.currentThread() + .getContextClassLoader() + .getResourceAsStream("fonts/OpenSans-Semibold.ttf")); + } catch (IOException | FontFormatException e) { + e.printStackTrace(); + } + } + + /** Creates a window */ + public LizzieMain() { + super(DEFAULT_TITLE); + + // TODO + // setMinimumSize(new Dimension(640, 400)); + boolean persisted = Lizzie.config.persistedUi != null; + if (persisted) + boardPositionProportion = + Lizzie.config.persistedUi.optInt("board-position-proportion", boardPositionProportion); + JSONArray pos = WindowPosition.mainWindowPos(); + if (pos != null) { + this.setBounds(pos.getInt(0), pos.getInt(1), pos.getInt(2), pos.getInt(3)); + } else { + setSize(960, 600); + setLocationRelativeTo(null); // Start centered, needs to be called *after* setSize... + } + + // Allow change font in the config + if (Lizzie.config.uiFontName != null) { + uiFont = new Font(Lizzie.config.uiFontName, Font.PLAIN, 12); + } + if (Lizzie.config.winrateFontName != null) { + winrateFont = new Font(Lizzie.config.winrateFontName, Font.BOLD, 12); + } + + if (Lizzie.config.startMaximized && !persisted) { + setExtendedState(Frame.MAXIMIZED_BOTH); + } else if (persisted && Lizzie.config.persistedUi.getBoolean("window-maximized")) { + setExtendedState(Frame.MAXIMIZED_BOTH); + } + + JPanel panel = + new JPanel() { + @Override + protected void paintComponent(Graphics g) { + if (g instanceof Graphics2D) { + int width = getWidth(); + int height = getHeight(); + Optional backgroundG; + if (cachedBackgroundWidth != width + || cachedBackgroundHeight != height + || redrawBackgroundAnyway) { + backgroundG = Optional.of(createBackground(width, height)); + } else { + backgroundG = Optional.empty(); + } + // draw the image + Graphics2D bsGraphics = (Graphics2D) g; // bs.getDrawGraphics(); + bsGraphics.setRenderingHint( + RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); + bsGraphics.setRenderingHint( + RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + bsGraphics.drawImage(cachedBackground, 0, 0, null); + + // pondering message + int maxBound = Math.max(width, height); + double ponderingSize = .02; + int ponderingX = 0; + int ponderingY = height - (int) (maxBound * ponderingSize); + + // dynamic komi + double dynamicKomiSize = .02; + int dynamicKomiX = 0; + int dynamicKomiY = ponderingY - (int) (maxBound * dynamicKomiSize); + int dynamicKomiLabelX = 0; + int dynamicKomiLabelY = dynamicKomiY - (int) (maxBound * dynamicKomiSize); + + // loading message; + double loadingSize = 0.03; + int loadingX = ponderingX; + int loadingY = ponderingY - (int) (maxBound * (loadingSize - ponderingSize)); + + if (Lizzie.leelaz != null && Lizzie.leelaz.isLoaded()) { + if (Lizzie.config.showStatus) { + String statusKey = + "LizzieFrame.display." + (Lizzie.leelaz.isPondering() ? "on" : "off"); + String statusText = resourceBundle.getString(statusKey); + String ponderingText = resourceBundle.getString("LizzieFrame.display.pondering"); + String switching = resourceBundle.getString("LizzieFrame.prompt.switching"); + String switchingText = Lizzie.leelaz.switching() ? switching : ""; + String weightText = Lizzie.leelaz.currentWeight(); + String text = + ponderingText + " " + statusText + " " + weightText + " " + switchingText; + drawPonderingState(bsGraphics, text, ponderingX, ponderingY, ponderingSize); + } + + Optional dynamicKomi = Lizzie.leelaz.getDynamicKomi(); + if (Lizzie.config.showDynamicKomi && dynamicKomi.isPresent()) { + String text = resourceBundle.getString("LizzieFrame.display.dynamic-komi"); + drawPonderingState( + bsGraphics, text, dynamicKomiLabelX, dynamicKomiLabelY, dynamicKomiSize); + drawPonderingState( + bsGraphics, dynamicKomi.get(), dynamicKomiX, dynamicKomiY, dynamicKomiSize); + } + } else if (Lizzie.config.showStatus) { + String loadingText = resourceBundle.getString("LizzieFrame.display.loading"); + drawPonderingState(bsGraphics, loadingText, loadingX, loadingY, loadingSize); + } + } + } + }; + setContentPane(panel); + + layout = new LizzieLayout(); + getContentPane().setLayout(layout); + basicInfoPane = new BasicInfoPane(this); + boardPane = new BoardPane(this); + subBoardPane = new SubBoardPane(this); + winratePane = new WinratePane(this); + variationTreePane = new VariationTreePane(this); + commentPane = new CommentPane(this); + getContentPane().add(boardPane, LizzieLayout.MAIN_BOARD); + getContentPane().add(basicInfoPane, LizzieLayout.BASIC_INFO); + getContentPane().add(winratePane, LizzieLayout.WINRATE); + getContentPane().add(subBoardPane, LizzieLayout.SUB_BOARD); + getContentPane().add(variationTreePane, LizzieLayout.VARIATION); + getContentPane().add(commentPane, LizzieLayout.COMMENT); + WindowPosition.restorePane(Lizzie.config.persistedUi, boardPane); + WindowPosition.restorePane(Lizzie.config.persistedUi, basicInfoPane); + WindowPosition.restorePane(Lizzie.config.persistedUi, winratePane); + WindowPosition.restorePane(Lizzie.config.persistedUi, subBoardPane); + WindowPosition.restorePane(Lizzie.config.persistedUi, variationTreePane); + WindowPosition.restorePane(Lizzie.config.persistedUi, commentPane); + + try { + this.setIconImage(ImageIO.read(getClass().getResourceAsStream("/assets/logo.png"))); + } catch (IOException e) { + e.printStackTrace(); + } + + setVisible(true); + + input = new Input(); + // addMouseListener(input); + addKeyListener(input); + addMouseWheelListener(input); + // addMouseMotionListener(input); + + // When the window is closed: save the SGF file, then run shutdown() + this.addWindowListener( + new WindowAdapter() { + public void windowClosing(WindowEvent e) { + Lizzie.shutdown(); + } + }); + + // Show the playouts in the title + showPlayouts.scheduleAtFixedRate( + new Runnable() { + @Override + public void run() { + if (!isDrawVisitsInTitle) { + visitsString = ""; + return; + } + if (Lizzie.leelaz == null) return; + try { + int totalPlayouts = MoveData.getPlayouts(Lizzie.leelaz.getBestMoves()); + if (totalPlayouts <= 0) return; + visitsString = + String.format( + " %d visits/second", + (totalPlayouts > lastPlayouts) ? totalPlayouts - lastPlayouts : 0); + updateTitle(); + lastPlayouts = totalPlayouts; + } catch (Exception e) { + } + } + }, + 1, + 1, + TimeUnit.SECONDS); + + setFocusable(true); + setFocusTraversalKeysEnabled(false); + } + + /** + * temporary measure to refresh background. ideally we shouldn't need this (but we want to release + * Lizzie 0.5 today, not tomorrow!). Refactor me out please! (you need to get blurring to work + * properly on startup). + */ + public void refreshBackground() { + redrawBackgroundAnyway = true; + } + + public BufferedImage getWallpaper() { + if (cachedWallpaperImage == emptyImage) { + cachedWallpaperImage = Lizzie.config.theme.background(); + } + return cachedWallpaperImage; + } + + private Graphics2D createBackground(int width, int height) { + cachedBackground = new BufferedImage(width, height, TYPE_INT_RGB); + cachedBackgroundWidth = cachedBackground.getWidth(); + cachedBackgroundHeight = cachedBackground.getHeight(); + + redrawBackgroundAnyway = false; + + Graphics2D g = cachedBackground.createGraphics(); + g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); + + BufferedImage wallpaper = getWallpaper(); + int drawWidth = max(wallpaper.getWidth(), width); + int drawHeight = max(wallpaper.getHeight(), height); + // Support seamless texture + drawTextureImage(g, wallpaper, 0, 0, drawWidth, drawHeight); + + return g; + } + + private void drawPonderingState(Graphics2D g, String text, int x, int y, double size) { + int fontSize = (int) (max(getWidth(), getHeight()) * size); + Font font = new Font(Lizzie.config.fontName, Font.PLAIN, fontSize); + FontMetrics fm = g.getFontMetrics(font); + int stringWidth = fm.stringWidth(text); + // Truncate too long text when display switching prompt + if (Lizzie.leelaz != null && Lizzie.leelaz.isLoaded()) { + int mainBoardX = boardPane.getLocation().x; + if (getWidth() > getHeight() && (mainBoardX > x) && stringWidth > (mainBoardX - x)) { + text = Utils.truncateStringByWidth(text, fm, mainBoardX - x); + stringWidth = fm.stringWidth(text); + } + } + // Do nothing when no text + if (stringWidth <= 0) { + return; + } + int stringHeight = fm.getAscent() - fm.getDescent(); + int width = max(stringWidth, 1); + int height = max((int) (stringHeight * 1.2), 1); + + BufferedImage result = new BufferedImage(width, height, TYPE_INT_ARGB); + // commenting this out for now... always causing an exception on startup. will fix in the + // upcoming refactoring + // filter20.filter(cachedBackground.getSubimage(x, y, result.getWidth(), + // result.getHeight()), result); + g.drawImage(result, x, y, null); + + g.setColor(new Color(0, 0, 0, 130)); + g.fillRect(x, y, width, height); + g.drawRect(x, y, width, height); + + g.setColor(Color.white); + g.setFont(font); + g.drawString( + text, x + (width - stringWidth) / 2, y + stringHeight + (height - stringHeight) / 2); + } + + private GaussianFilter filter20 = new GaussianFilter(20); + + public BufferedImage getBasicInfoContainer(LizziePane pane) { + if (cachedBackground == null + || (cachedBasicInfoContainer != null + && cachedBasicInfoContainer.getWidth() == pane.getWidth() + && cachedBasicInfoContainer.getHeight() == pane.getHeight())) { + return cachedBasicInfoContainer; + } + int vx = pane.getX(); + int vy = pane.getY(); + int vw = pane.getWidth(); + int vh = pane.getHeight(); + BufferedImage result = cachedBackground.getSubimage(vx, vy, vw, vh); + // BufferedImage result = new BufferedImage(vw, vh, TYPE_INT_ARGB); + // filter10.filter(cachedBackground.getSubimage(vx, vy, vw, vh), result); + cachedBasicInfoContainer = result; + return result; + } + + public BufferedImage getWinrateContainer(LizziePane pane) { + if (cachedBackground == null + || (cachedWinrateContainer != null + && cachedWinrateContainer.getWidth() == pane.getWidth() + && cachedWinrateContainer.getHeight() == pane.getHeight())) { + return cachedWinrateContainer; + } + int vx = pane.getX(); + int vy = pane.getY(); + int vw = pane.getWidth(); + int vh = pane.getHeight(); + BufferedImage result = new BufferedImage(vw, vh, TYPE_INT_ARGB); + filter20.filter(cachedBackground.getSubimage(vx, vy, vw, vh), result); + cachedWinrateContainer = result; + return result; + } + + public BufferedImage getVariationContainer(LizziePane pane) { + if (cachedBackground == null + || (cachedVariationContainer != null + && cachedVariationContainer.getWidth() == pane.getWidth() + && cachedVariationContainer.getHeight() == pane.getHeight())) { + return cachedVariationContainer; + } + int vx = pane.getX(); + int vy = pane.getY(); + int vw = pane.getWidth(); + int vh = pane.getHeight(); + BufferedImage result = new BufferedImage(vw, vh, TYPE_INT_ARGB); + filter20.filter(cachedBackground.getSubimage(vx, vy, vw, vh), result); + cachedVariationContainer = result; + return result; + } + + public void drawContainer(Graphics g, int vx, int vy, int vw, int vh) { + if (vw <= 0 + || vh <= 0 + || vx < cachedBackground.getMinX() + || vx + vw > cachedBackground.getMinX() + cachedBackground.getWidth() + || vy < cachedBackground.getMinY() + || vy + vh > cachedBackground.getMinY() + cachedBackground.getHeight()) { + return; + } + redrawBackgroundAnyway = false; + BufferedImage result = new BufferedImage(vw, vh, TYPE_INT_ARGB); + filter20.filter(cachedBackground.getSubimage(vx, vy, vw, vh), result); + g.drawImage(result, vx, vy, null); + } + + /** Draw texture image */ + public void drawTextureImage( + Graphics2D g, BufferedImage img, int x, int y, int width, int height) { + TexturePaint paint = + new TexturePaint(img, new Rectangle(0, 0, img.getWidth(), img.getHeight())); + g.setPaint(paint); + g.fill(new Rectangle(x, y, width, height)); + } + + @Override + public boolean isDesignMode() { + return designMode; + } + + @Override + public void toggleDesignMode() { + this.designMode = !this.designMode; + // boardPane.setDesignMode(designMode); + basicInfoPane.setDesignMode(designMode); + winratePane.setDesignMode(designMode); + subBoardPane.setDesignMode(designMode); + variationTreePane.setDesignMode(designMode); + commentPane.setDesignMode(designMode); + } + + @Override + public void updateBasicInfo() { + if (basicInfoPane != null) { + basicInfoPane.repaint(); + } + } + + public void invalidLayout() { + // TODO + layout.layoutContainer(getContentPane()); + layout.invalidateLayout(getContentPane()); + repaint(); + } + + @Override + public void refresh() { + refresh(0); + } + + @Override + public void refresh(int type) { + if (type == 2) { + invalidLayout(); + } else { + boardPane.repaint(); + if (type != 1) { + updateStatus(); + } + } + } + + public void repaintSub() { + if (Lizzie.leelaz != null && Lizzie.leelaz.isLoaded()) { + if (Lizzie.config.showSubBoard && !subBoardPane.isVisible()) { + subBoardPane.setVisible(true); + } + if (Lizzie.config.showWinrate && !winratePane.isVisible()) { + winratePane.setVisible(true); + } + } + subBoardPane.repaint(); + winratePane.repaint(); + } + + public void updateStatus() { + // basicInfoPane.revalidate(); + basicInfoPane.repaint(); + if (Lizzie.leelaz != null && Lizzie.leelaz.isLoaded()) { + if (Lizzie.config.showVariationGraph && !variationTreePane.isVisible()) { + variationTreePane.setVisible(true); + } + } + // variationTreePane.revalidate(); + variationTreePane.repaint(); + commentPane.drawComment(); + // commentPane.revalidate(); + commentPane.repaint(); + invalidLayout(); + } + + public void openConfigDialog() { + ConfigDialog configDialog = new ConfigDialog(); + configDialog.setVisible(true); + // configDialog.dispose(); + } + + public void openChangeMoveDialog() { + ChangeMoveDialog changeMoveDialog = new ChangeMoveDialog(); + changeMoveDialog.setVisible(true); + } + + public void toggleGtpConsole() { + Lizzie.leelaz.toggleGtpConsole(); + if (Lizzie.gtpConsole != null) { + Lizzie.gtpConsole.setVisible(!Lizzie.gtpConsole.isVisible()); + } else { + Lizzie.gtpConsole = new GtpConsolePane(this); + Lizzie.gtpConsole.setVisible(true); + } + } + + @Override + public void startGame() { + GameInfo gameInfo = Lizzie.board.getHistory().getGameInfo(); + + NewGameDialog gameDialog = new NewGameDialog(); + gameDialog.setGameInfo(gameInfo); + gameDialog.setVisible(true); + boolean playerIsBlack = gameDialog.playerIsBlack(); + boolean isNewGame = gameDialog.isNewGame(); + // gameDialog.dispose(); + if (gameDialog.isCancelled()) return; + + if (isNewGame) { + Lizzie.board.clear(); + } + Lizzie.leelaz.sendCommand("komi " + gameInfo.getKomi()); + + Lizzie.leelaz.time_settings(); + Lizzie.frame.playerIsBlack = playerIsBlack; + Lizzie.frame.isNewGame = isNewGame; + Lizzie.frame.isPlayingAgainstLeelaz = true; + + boolean isHandicapGame = gameInfo.getHandicap() != 0; + if (isNewGame) { + Lizzie.board.getHistory().setGameInfo(gameInfo); + if (isHandicapGame) { + Lizzie.board.getHistory().getData().blackToPlay = false; + Lizzie.leelaz.sendCommand("fixed_handicap " + gameInfo.getHandicap()); + if (playerIsBlack) Lizzie.leelaz.genmove("W"); + } else if (!playerIsBlack) { + Lizzie.leelaz.genmove("B"); + } + } else { + Lizzie.board.getHistory().setGameInfo(gameInfo); + if (Lizzie.frame.playerIsBlack != Lizzie.board.getData().blackToPlay) { + if (!Lizzie.leelaz.isThinking) { + Lizzie.leelaz.genmove((Lizzie.board.getData().blackToPlay ? "B" : "W")); + } + } + } + } + + public void editGameInfo() { + GameInfo gameInfo = Lizzie.board.getHistory().getGameInfo(); + + GameInfoDialog gameInfoDialog = new GameInfoDialog(); + gameInfoDialog.setGameInfo(gameInfo); + gameInfoDialog.setVisible(true); + + gameInfoDialog.dispose(); + } + + public void saveFile() { + FileNameExtensionFilter filter = new FileNameExtensionFilter("*.sgf", "SGF"); + JSONObject filesystem = Lizzie.config.persisted.getJSONObject("filesystem"); + JFileChooser chooser = new JFileChooser(filesystem.getString("last-folder")); + chooser.setFileFilter(filter); + chooser.setMultiSelectionEnabled(false); + int result = chooser.showSaveDialog(null); + if (result == JFileChooser.APPROVE_OPTION) { + File file = chooser.getSelectedFile(); + if (file.exists()) { + int ret = + JOptionPane.showConfirmDialog( + null, + resourceBundle.getString("LizzieFrame.prompt.sgfExists"), + "Warning", + JOptionPane.OK_CANCEL_OPTION); + if (ret == JOptionPane.CANCEL_OPTION) { + return; + } + } + if (!file.getPath().endsWith(".sgf")) { + file = new File(file.getPath() + ".sgf"); + } + try { + SGFParser.save(Lizzie.board, file.getPath()); + filesystem.put("last-folder", file.getParent()); + } catch (IOException err) { + JOptionPane.showConfirmDialog( + null, + resourceBundle.getString("LizzieFrame.prompt.failedTosaveFile"), + "Error", + JOptionPane.ERROR); + } + } + } + + public void openFile() { + FileNameExtensionFilter filter = new FileNameExtensionFilter("*.sgf or *.gib", "SGF", "GIB"); + JSONObject filesystem = Lizzie.config.persisted.getJSONObject("filesystem"); + JFileChooser chooser = new JFileChooser(filesystem.getString("last-folder")); + + chooser.setFileFilter(filter); + chooser.setMultiSelectionEnabled(false); + int result = chooser.showOpenDialog(null); + if (result == JFileChooser.APPROVE_OPTION) loadFile(chooser.getSelectedFile()); + } + + public void loadFile(File file) { + JSONObject filesystem = Lizzie.config.persisted.getJSONObject("filesystem"); + if (!(file.getPath().endsWith(".sgf") || file.getPath().endsWith(".gib"))) { + file = new File(file.getPath() + ".sgf"); + } + try { + System.out.println(file.getPath()); + if (file.getPath().endsWith(".sgf")) { + SGFParser.load(file.getPath()); + } else { + GIBParser.load(file.getPath()); + } + filesystem.put("last-folder", file.getParent()); + } catch (IOException err) { + JOptionPane.showConfirmDialog( + null, + resourceBundle.getString("LizzieFrame.prompt.failedToOpenFile"), + "Error", + JOptionPane.ERROR); + } + } + + public void setPlayers(String whitePlayer, String blackPlayer) { + playerTitle = String.format("(%s [W] vs %s [B])", whitePlayer, blackPlayer); + updateTitle(); + } + + public void updateTitle() { + StringBuilder sb = new StringBuilder(DEFAULT_TITLE); + sb.append(playerTitle); + sb.append(" [" + Lizzie.leelaz.engineCommand() + "]"); + sb.append(visitsString); + setTitle(sb.toString()); + } + + public void resetTitle() { + playerTitle = ""; + updateTitle(); + } + + @Override + public void drawControls() { + boardPane.drawControls(); + } + + @Override + public void replayBranch() { + boardPane.replayBranch(); + } + + @Override + public boolean isMouseOver(int x, int y) { + return boardPane.isMouseOver(x, y); + } + + @Override + public void onClicked(int x, int y) { + boardPane.onClicked(x, y); + } + + @Override + public void onDoubleClicked(int x, int y) { + boardPane.onDoubleClicked(x, y); + } + + @Override + public void onMouseDragged(int x, int y) { + winratePane.onMouseDragged(x, y); + } + + @Override + public void onMouseMoved(int x, int y) { + boardPane.onMouseMoved(x, y); + } + + @Override + public void startRawBoard() { + boardPane.startRawBoard(); + } + + @Override + public void stopRawBoard() { + boardPane.stopRawBoard(); + } + + @Override + public boolean incrementDisplayedBranchLength(int n) { + return boardPane.incrementDisplayedBranchLength(n); + } + + @Override + public void increaseMaxAlpha(int k) { + boardPane.increaseMaxAlpha(k); + } + + @Override + public void copySgf() { + boardPane.copySgf(); + } + + @Override + public void pasteSgf() { + boardPane.pasteSgf(); + } + + @Override + public boolean playCurrentVariation() { + return boardPane.playCurrentVariation(); + } + + @Override + public void playBestMove() { + boardPane.playBestMove(); + } + + @Override + public void clear() { + boardPane.clear(); + } +} diff --git a/src/main/java/featurecat/lizzie/gui/LizziePane.java b/src/main/java/featurecat/lizzie/gui/LizziePane.java new file mode 100644 index 000000000..5d47e12f8 --- /dev/null +++ b/src/main/java/featurecat/lizzie/gui/LizziePane.java @@ -0,0 +1,537 @@ +package featurecat.lizzie.gui; + +import java.awt.Component; +import java.awt.Container; +import java.awt.Cursor; +import java.awt.Dimension; +import java.awt.Insets; +import java.awt.LayoutManager; +import java.awt.LayoutManager2; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.Toolkit; +import java.awt.Window; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.io.IOException; +import java.io.ObjectOutputStream; +import java.io.Serializable; +import javax.swing.BorderFactory; +import javax.swing.JDialog; +import javax.swing.JPanel; +import javax.swing.JWindow; +import javax.swing.UIManager; +import javax.swing.plaf.UIResource; +import javax.swing.text.html.HTMLEditorKit; +import javax.swing.text.html.StyleSheet; + +/** The window used to display the game. */ +public class LizziePane extends JPanel { + + private static final String uiClassID = "LizziePaneUI"; + + static { + UIManager.put(uiClassID, BasicLizziePaneUI.class.getName()); + } + + private boolean floatable = true; + + /** Keys to lookup borders in defaults table. */ + private static final int[] cursorMapping = + new int[] { + Cursor.NW_RESIZE_CURSOR, + Cursor.NW_RESIZE_CURSOR, + Cursor.N_RESIZE_CURSOR, + Cursor.NE_RESIZE_CURSOR, + Cursor.NE_RESIZE_CURSOR, + Cursor.NW_RESIZE_CURSOR, + 0, + 0, + 0, + Cursor.NE_RESIZE_CURSOR, + Cursor.W_RESIZE_CURSOR, + 0, + 0, + 0, + Cursor.E_RESIZE_CURSOR, + Cursor.SW_RESIZE_CURSOR, + 0, + 0, + 0, + Cursor.SE_RESIZE_CURSOR, + Cursor.SW_RESIZE_CURSOR, + Cursor.SW_RESIZE_CURSOR, + Cursor.S_RESIZE_CURSOR, + Cursor.SE_RESIZE_CURSOR, + Cursor.SE_RESIZE_CURSOR + }; + + /** The amount of space (in pixels) that the cursor is changed on. */ + private static final int CORNER_DRAG_WIDTH = 16; + + /** Region from edges that dragging is active from. */ + private static final int BORDER_DRAG_THICKNESS = 5; + + /** + * Cursor used to track the cursor set by the user. This is initially + * Cursor.DEFAULT_CURSOR. + */ + private Cursor lastCursor = Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR); + + protected PaneDragListener dragListener; + protected Input input; + + public LizziePane() { + super(); + } + + /** Creates a window */ + public LizziePane(LizzieMain owner) { + // super(owner); + // initCompotents(); + // input = owner.input; + // installInputListeners(); + setOpaque(false); + } + + @Override + public LizziePaneUI getUI() { + return (LizziePaneUI) ui; + } + + public void setUI(LizziePaneUI ui) { + super.setUI(ui); + } + + public void updateUI() { + setUI((LizziePaneUI) UIManager.getUI(this)); + if (getLayout() == null) { + setLayout(new DefaultLizziePaneLayout()); + } + invalidate(); + } + + public String getUIClassID() { + return uiClassID; + } + + public void toWindow(Point position, Dimension size) { + if (getUI() != null) { + getUI().toWindow(position, size); + } + } + + public int getComponentIndex(Component c) { + int ncomponents = this.getComponentCount(); + Component[] component = this.getComponents(); + for (int i = 0; i < ncomponents; i++) { + Component comp = component[i]; + if (comp == c) return i; + } + return -1; + } + + public Component getComponentAtIndex(int i) { + int ncomponents = this.getComponentCount(); + if (i >= 0 && i < ncomponents) { + Component[] component = this.getComponents(); + return component[i]; + } + return null; + } + + public boolean isFloatable() { + return floatable; + } + + public void setFloatable(boolean b) { + if (floatable != b) { + boolean old = floatable; + floatable = b; + + firePropertyChange("floatable", old, b); + revalidate(); + repaint(); + } + } + + private void initCompotents() { + setBorder(BorderFactory.createEmptyBorder()); + setVisible(true); + } + + private class PaneDragListener extends MouseAdapter { + + /** Set to true if the drag operation is moving the window. */ + private boolean isMovingWindow; + + /** Used to determine the corner the resize is occurring from. */ + private int dragCursor; + + /** X location the mouse went down on for a drag operation. */ + private int dragOffsetX; + + /** Y location the mouse went down on for a drag operation. */ + private int dragOffsetY; + + /** Width of the window when the drag started. */ + private int dragWidth; + + /** Height of the window when the drag started. */ + private int dragHeight; + + /** Window the JRootPane is in. */ + private Window window; + + public PaneDragListener(Window window) { + this.window = window; + } + + public void mouseMoved(MouseEvent e) { + Window w = (Window) e.getSource(); + + JWindow f = null; + JDialog d = null; + + if (w instanceof JWindow) { + f = (JWindow) w; + } else if (w instanceof JDialog) { + d = (JDialog) w; + } + + // Update the cursor + int cursor = getCursor(calculateCorner(w, e.getX(), e.getY())); + + if (cursor != 0 + && ((f != null) // && (f.isResizable() && (f.getExtendedState() & Frame.MAXIMIZED_BOTH) + // == 0)) + || (d != null && d.isResizable()))) { + w.setCursor(Cursor.getPredefinedCursor(cursor)); + } else { + w.setCursor(lastCursor); + } + } + + public void mouseReleased(MouseEvent e) { + if (dragCursor != 0 && window != null && !window.isValid()) { + // Some Window systems validate as you resize, others won't, + // thus the check for validity before repainting. + window.validate(); + getRootPane().repaint(); + } + isMovingWindow = false; + dragCursor = 0; + } + + public void mousePressed(MouseEvent e) { + Point dragWindowOffset = e.getPoint(); + Window w = (Window) e.getSource(); + if (w != null) { + w.toFront(); + } + + JWindow f = null; + JDialog d = null; + + if (w instanceof JWindow) { + f = (JWindow) w; + } else if (w instanceof JDialog) { + d = (JDialog) w; + } + + // int frameState = (f != null) ? f.getExtendedState() : 0; + + if (((f != null) // && ((frameState & Frame.MAXIMIZED_BOTH) == 0) + || (d != null)) + && dragWindowOffset.y >= BORDER_DRAG_THICKNESS + && dragWindowOffset.x >= BORDER_DRAG_THICKNESS + && dragWindowOffset.x < w.getWidth() - BORDER_DRAG_THICKNESS) { + isMovingWindow = true; + dragOffsetX = dragWindowOffset.x; + dragOffsetY = dragWindowOffset.y; + } else if (f != null // && f.isResizable() && ((frameState & Frame.MAXIMIZED_BOTH) == 0) + || (d != null && d.isResizable())) { + dragOffsetX = dragWindowOffset.x; + dragOffsetY = dragWindowOffset.y; + dragWidth = w.getWidth(); + dragHeight = w.getHeight(); + dragCursor = getCursor(calculateCorner(w, dragWindowOffset.x, dragWindowOffset.y)); + } + } + + public void mouseDragged(MouseEvent e) { + Window w = (Window) e.getSource(); + Point pt = e.getPoint(); + + if (isMovingWindow) { + Point eventLocationOnScreen = e.getLocationOnScreen(); + w.setLocation(eventLocationOnScreen.x - dragOffsetX, eventLocationOnScreen.y - dragOffsetY); + } else if (dragCursor != 0) { + Rectangle r = w.getBounds(); + Rectangle startBounds = new Rectangle(r); + Dimension min = w.getMinimumSize(); + + switch (dragCursor) { + case Cursor.E_RESIZE_CURSOR: + adjust(r, min, 0, 0, pt.x + (dragWidth - dragOffsetX) - r.width, 0); + break; + case Cursor.S_RESIZE_CURSOR: + adjust(r, min, 0, 0, 0, pt.y + (dragHeight - dragOffsetY) - r.height); + break; + case Cursor.N_RESIZE_CURSOR: + adjust(r, min, 0, pt.y - dragOffsetY, 0, -(pt.y - dragOffsetY)); + break; + case Cursor.W_RESIZE_CURSOR: + adjust(r, min, pt.x - dragOffsetX, 0, -(pt.x - dragOffsetX), 0); + break; + case Cursor.NE_RESIZE_CURSOR: + adjust( + r, + min, + 0, + pt.y - dragOffsetY, + pt.x + (dragWidth - dragOffsetX) - r.width, + -(pt.y - dragOffsetY)); + break; + case Cursor.SE_RESIZE_CURSOR: + adjust( + r, + min, + 0, + 0, + pt.x + (dragWidth - dragOffsetX) - r.width, + pt.y + (dragHeight - dragOffsetY) - r.height); + break; + case Cursor.NW_RESIZE_CURSOR: + adjust( + r, + min, + pt.x - dragOffsetX, + pt.y - dragOffsetY, + -(pt.x - dragOffsetX), + -(pt.y - dragOffsetY)); + break; + case Cursor.SW_RESIZE_CURSOR: + adjust( + r, + min, + pt.x - dragOffsetX, + 0, + -(pt.x - dragOffsetX), + pt.y + (dragHeight - dragOffsetY) - r.height); + break; + default: + break; + } + if (!r.equals(startBounds)) { + w.setBounds(r); + // Defer repaint/validate on mouseReleased unless dynamic + // layout is active. + if (Toolkit.getDefaultToolkit().isDynamicLayoutActive()) { + w.validate(); + getRootPane().repaint(); + } + } + } + } + + private int calculateCorner(Window c, int x, int y) { + Insets insets = c.getInsets(); + int xPosition = calculatePosition(x - insets.left, c.getWidth() - insets.left - insets.right); + int yPosition = calculatePosition(y - insets.top, c.getHeight() - insets.top - insets.bottom); + + if (xPosition == -1 || yPosition == -1) { + return -1; + } + return yPosition * 5 + xPosition; + } + + private int getCursor(int corner) { + if (corner == -1) { + return 0; + } + return cursorMapping[corner]; + } + + private int calculatePosition(int spot, int width) { + if (spot < BORDER_DRAG_THICKNESS) { + return 0; + } + if (spot < CORNER_DRAG_WIDTH) { + return 1; + } + if (spot >= (width - BORDER_DRAG_THICKNESS)) { + return 4; + } + if (spot >= (width - CORNER_DRAG_WIDTH)) { + return 3; + } + return 2; + } + + private void adjust( + Rectangle bounds, Dimension min, int deltaX, int deltaY, int deltaWidth, int deltaHeight) { + bounds.x += deltaX; + bounds.y += deltaY; + bounds.width += deltaWidth; + bounds.height += deltaHeight; + if (min != null) { + if (bounds.width < min.width) { + int correction = min.width - bounds.width; + if (deltaX != 0) { + bounds.x -= correction; + } + bounds.width = min.width; + } + if (bounds.height < min.height) { + int correction = min.height - bounds.height; + if (deltaY != 0) { + bounds.y -= correction; + } + bounds.height = min.height; + } + } + } + } + + protected void installDesignListeners() { + LizziePaneUI ui = getUI(); + if (ui != null && ui instanceof BasicLizziePaneUI) { + ((BasicLizziePaneUI) ui).installListeners(); + } + } + + protected void uninstallDesignListeners() { + LizziePaneUI ui = getUI(); + if (ui != null && ui instanceof BasicLizziePaneUI) { + ((BasicLizziePaneUI) ui).uninstallListeners(); + } + } + + protected void installInputListeners() { + // addMouseListener(input); + // addKeyListener(input); + // addMouseWheelListener(input); + // addMouseMotionListener(input); + } + + protected void uninstallInputListeners() { + // removeMouseListener(input); + // removeKeyListener(input); + // removeMouseWheelListener(input); + // removeMouseMotionListener(input); + } + + public void setDesignMode(boolean mode) { + if (mode) { + uninstallInputListeners(); + installDesignListeners(); + } else { + uninstallDesignListeners(); + installInputListeners(); + } + } + + private class DefaultLizziePaneLayout + implements LayoutManager2, Serializable, PropertyChangeListener, UIResource { + + LizzieLayout lm; + + DefaultLizziePaneLayout() { + lm = new LizzieLayout(); + } + + /** @deprecated replaced by addLayoutComponent(Component, Object). */ + @Deprecated + public void addLayoutComponent(String name, Component comp) { + lm.addLayoutComponent(name, comp); + } + + public void addLayoutComponent(Component comp, Object constraints) { + lm.addLayoutComponent(comp, constraints); + } + + public void removeLayoutComponent(Component comp) { + lm.removeLayoutComponent(comp); + } + + public Dimension preferredLayoutSize(Container target) { + return lm.preferredLayoutSize(target); + } + + public Dimension minimumLayoutSize(Container target) { + return lm.minimumLayoutSize(target); + } + + public Dimension maximumLayoutSize(Container target) { + return lm.maximumLayoutSize(target); + } + + public void layoutContainer(Container target) { + lm.layoutContainer(target); + } + + public float getLayoutAlignmentX(Container target) { + return lm.getLayoutAlignmentX(target); + } + + public float getLayoutAlignmentY(Container target) { + return lm.getLayoutAlignmentY(target); + } + + public void invalidateLayout(Container target) { + lm.invalidateLayout(target); + } + + public void propertyChange(PropertyChangeEvent e) { + // TODO + // String name = e.getPropertyName(); + // if (name.equals("orientation")) { + // int o = ((Integer) e.getNewValue()).intValue(); + // if (o == LizziePane.VERTICAL) + // lm = new LizzieLayout(LizziePane.this, LizzieLayout.PAGE_AXIS); + // else { + // lm = new LizzieLayout(LizziePane.this, LizzieLayout.LINE_AXIS); + // } + // } + } + } + + public void setLayout(LayoutManager mgr) { + LayoutManager oldMgr = getLayout(); + if (oldMgr instanceof PropertyChangeListener) { + removePropertyChangeListener((PropertyChangeListener) oldMgr); + } + super.setLayout(mgr); + } + + private void writeObject(ObjectOutputStream s) throws IOException { + s.defaultWriteObject(); + if (getUIClassID().equals(uiClassID)) { + // byte count = JComponent.getWriteObjCounter(this); + // JComponent.setWriteObjCounter(this, --count); + // if (count == 0 && ui != null) { + ui.installUI(this); + // } + } + } + + public static class HtmlKit extends HTMLEditorKit { + private StyleSheet style = new StyleSheet(); + + @Override + public void setStyleSheet(StyleSheet styleSheet) { + style = styleSheet; + } + + @Override + public StyleSheet getStyleSheet() { + if (style == null) { + style = super.getStyleSheet(); + } + return style; + } + } +} diff --git a/src/main/java/featurecat/lizzie/gui/LizziePaneUI.java b/src/main/java/featurecat/lizzie/gui/LizziePaneUI.java new file mode 100644 index 000000000..8386785c1 --- /dev/null +++ b/src/main/java/featurecat/lizzie/gui/LizziePaneUI.java @@ -0,0 +1,10 @@ +package featurecat.lizzie.gui; + +import java.awt.Dimension; +import java.awt.Point; +import javax.swing.plaf.PanelUI; + +public abstract class LizziePaneUI extends PanelUI { + + public abstract void toWindow(Point position, Dimension size); +} diff --git a/src/main/java/featurecat/lizzie/gui/MainFrame.java b/src/main/java/featurecat/lizzie/gui/MainFrame.java new file mode 100644 index 000000000..a7ceab40d --- /dev/null +++ b/src/main/java/featurecat/lizzie/gui/MainFrame.java @@ -0,0 +1,115 @@ +package featurecat.lizzie.gui; + +import featurecat.lizzie.Lizzie; +import java.awt.Font; +import java.awt.HeadlessException; +import java.awt.event.MouseWheelEvent; +import java.io.File; +import javax.swing.JFrame; + +public abstract class MainFrame extends JFrame { + + public boolean isPlayingAgainstLeelaz = false; + public boolean playerIsBlack = true; + public boolean isNewGame = false; + public int boardPositionProportion = Lizzie.config.boardPositionProportion; + public int winRateGridLines = 3; + public boolean showControls = false; + public static Font uiFont; + public static Font winrateFont; + // Force refresh board + private boolean forceRefresh; + + public MainFrame(String title) throws HeadlessException { + super(title); + } + + public boolean isDesignMode() { + return false; + } + + public void toggleDesignMode() {} + + public void updateBasicInfo() {} + + public void refresh() { + repaint(); + } + + /** + * Refresh + * + * @param type: 0-All, 1-Only Board, 2-Invalid Layout + */ + public void refresh(int type) { + repaint(); + } + + public boolean isForceRefresh() { + return forceRefresh; + } + + public void setForceRefresh(boolean forceRefresh) { + this.forceRefresh = forceRefresh; + } + + public boolean processCommentMouseWheelMoved(MouseWheelEvent e) { + return false; + } + + public abstract void drawControls(); + + public abstract void replayBranch(); + + public abstract void refreshBackground(); + + public abstract void updateTitle(); + + public abstract void setPlayers(String whitePlayer, String blackPlayer); + + public abstract void resetTitle(); + + public abstract void clear(); + + public abstract boolean isMouseOver(int x, int y); + + public abstract void onClicked(int x, int y); + + public abstract void onDoubleClicked(int x, int y); + + public abstract void onMouseDragged(int x, int y); + + public abstract void onMouseMoved(int x, int y); + + public abstract void startRawBoard(); + + public abstract void stopRawBoard(); + + public abstract boolean incrementDisplayedBranchLength(int n); + + public abstract void increaseMaxAlpha(int k); + + public abstract void loadFile(File file); + + public abstract void openFile(); + + public abstract void saveFile(); + + public abstract void copySgf(); + + public abstract void pasteSgf(); + + public abstract void openConfigDialog(); + + public abstract void toggleGtpConsole(); + + public abstract void startGame(); + + public abstract void editGameInfo(); + + public abstract void openChangeMoveDialog(); + + public abstract boolean playCurrentVariation(); + + public abstract void playBestMove(); +} diff --git a/src/main/java/featurecat/lizzie/gui/NewGameDialog.java b/src/main/java/featurecat/lizzie/gui/NewGameDialog.java index 395800aa6..c26455a26 100644 --- a/src/main/java/featurecat/lizzie/gui/NewGameDialog.java +++ b/src/main/java/featurecat/lizzie/gui/NewGameDialog.java @@ -5,19 +5,32 @@ package featurecat.lizzie.gui; import featurecat.lizzie.analysis.GameInfo; -import java.awt.*; +import java.awt.BorderLayout; +import java.awt.Container; +import java.awt.Dimension; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.GridLayout; +import java.awt.Insets; import java.text.DecimalFormat; import java.text.ParseException; import java.util.ResourceBundle; -import javax.swing.*; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JDialog; +import javax.swing.JFormattedTextField; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JTextField; import javax.swing.border.EmptyBorder; /** @author unknown */ public class NewGameDialog extends JDialog { + private static final ResourceBundle resourceBundle = + ResourceBundle.getBundle("l10n.DisplayStrings"); // create formatters public static final DecimalFormat FORMAT_KOMI = new DecimalFormat("#0.0"); public static final DecimalFormat FORMAT_HANDICAP = new DecimalFormat("0"); - public static final JLabel PLACEHOLDER = new JLabel(""); static { FORMAT_HANDICAP.setMaximumIntegerDigits(1); @@ -36,14 +49,12 @@ public class NewGameDialog extends JDialog { private boolean cancelled = true; private GameInfo gameInfo; + private JCheckBox chkNewGame; public NewGameDialog() { initComponents(); } - private static final ResourceBundle resourceBundle = - ResourceBundle.getBundle("l10n.DisplayStrings"); - private void initComponents() { setMinimumSize(new Dimension(100, 100)); setResizable(false); @@ -80,10 +91,14 @@ private void initContentPanel() { textFieldBlack = new JTextField(); textFieldKomi = new JFormattedTextField(FORMAT_KOMI); textFieldHandicap = new JFormattedTextField(FORMAT_HANDICAP); + textFieldHandicap.setEnabled(false); textFieldHandicap.addPropertyChangeListener(evt -> modifyHandicap()); contentPanel.add(checkBoxPlayerIsBlack); - contentPanel.add(PLACEHOLDER); + + chkNewGame = new JCheckBox(resourceBundle.getString("NewGameDialog.NewGame"), false); + chkNewGame.addChangeListener(evt -> toggleNewGame()); + contentPanel.add(chkNewGame); contentPanel.add(new JLabel(resourceBundle.getString("NewGameDialog.Black"))); contentPanel.add(textFieldBlack); contentPanel.add(new JLabel(resourceBundle.getString("NewGameDialog.White"))); @@ -93,7 +108,7 @@ private void initContentPanel() { contentPanel.add(new JLabel(resourceBundle.getString("NewGameDialog.Handicap"))); contentPanel.add(textFieldHandicap); - textFieldKomi.setEnabled(false); + textFieldKomi.setEnabled(true); dialogPane.add(contentPanel, BorderLayout.CENTER); } @@ -126,7 +141,7 @@ private void initButtonBar() { ((GridBagLayout) buttonBar.getLayout()).columnWeights = new double[] {1.0, 0.0}; // ---- okButton ---- - okButton.setText("OK"); + okButton.setText(resourceBundle.getString("NewGameDialog.OK")); okButton.addActionListener(e -> apply()); int center = GridBagConstraints.CENTER; @@ -172,6 +187,14 @@ public void setGameInfo(GameInfo gameInfo) { togglePlayerIsBlack(); } + private void toggleNewGame() { + textFieldHandicap.setEnabled(chkNewGame.isSelected()); + } + + public boolean isNewGame() { + return chkNewGame.isSelected(); + } + public boolean playerIsBlack() { return checkBoxPlayerIsBlack.isSelected(); } @@ -180,15 +203,15 @@ public boolean isCancelled() { return cancelled; } - public static void main(String[] args) { - EventQueue.invokeLater( - () -> { - try { - NewGameDialog window = new NewGameDialog(); - window.setVisible(true); - } catch (Exception e) { - e.printStackTrace(); - } - }); - } + // public static void main(String[] args) { + // EventQueue.invokeLater( + // () -> { + // try { + // NewGameDialog window = new NewGameDialog(); + // window.setVisible(true); + // } catch (Exception e) { + // e.printStackTrace(); + // } + // }); + // } } diff --git a/src/main/java/featurecat/lizzie/gui/SubBoardPane.java b/src/main/java/featurecat/lizzie/gui/SubBoardPane.java new file mode 100644 index 000000000..28dcc8d89 --- /dev/null +++ b/src/main/java/featurecat/lizzie/gui/SubBoardPane.java @@ -0,0 +1,112 @@ +package featurecat.lizzie.gui; + +import static java.awt.image.BufferedImage.TYPE_INT_ARGB; + +import featurecat.lizzie.Lizzie; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.RenderingHints; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.image.BufferedImage; + +/** The window used to display the game. */ +public class SubBoardPane extends LizziePane { + + private static BoardRenderer subBoardRenderer; + private BufferedImage cachedImage; + + // private final BufferStrategy bs; + + /** Creates a window */ + public SubBoardPane(LizzieMain owner) { + super(owner); + + subBoardRenderer = new BoardRenderer(false); + + setVisible(false); + + // TODO BufferStrategy does not support transparent background? + // createBufferStrategy(2); + // bs = getBufferStrategy(); + + addMouseListener( + new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + if (e.getButton() == MouseEvent.BUTTON1) { // left click + if (Lizzie.config.showSubBoard) { + Lizzie.config.toggleLargeSubBoard(); + owner.invalidLayout(); + } + } + } + }); + } + + /** + * Draws the game board and interface + * + * @param g0 not used + */ + @Override + protected void paintComponent(Graphics g0) { + super.paintComponent(g0); + + int x = 0; // getX(); + int y = 0; // getY(); + int width = getWidth(); + int height = getHeight(); + // layout parameters + + // initialize + cachedImage = new BufferedImage(width, height, TYPE_INT_ARGB); + Graphics2D g = (Graphics2D) cachedImage.getGraphics(); + g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); + + if (Lizzie.leelaz != null) { // && Lizzie.leelaz.isLoaded()) { + + if (Lizzie.config.showSubBoard) { + try { + subBoardRenderer.setLocation(x, y); + subBoardRenderer.setBoardLength(width); + subBoardRenderer.draw(g); + } catch (Exception e) { + // This can happen when no space is left for subboard. + } + } + } + + // cleanup + g.dispose(); + + // draw the image + // TODO BufferStrategy does not support transparent background? + Graphics2D bsGraphics = (Graphics2D) g0; // bs.getDrawGraphics(); + bsGraphics.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); + bsGraphics.drawImage(cachedImage, 0, 0, null); + + // cleanup + bsGraphics.dispose(); + // TODO BufferStrategy does not support transparent background? + // bs.show(); + } + + /** + * Checks whether or not something was clicked and performs the appropriate action + * + * @param x x coordinate + * @param y y coordinate + */ + public void onClicked(int x, int y) { + + if (Lizzie.config.showSubBoard && subBoardRenderer.isInside(x, y)) { + Lizzie.config.toggleLargeSubBoard(); + } + repaint(); + } + + public boolean isInside(int x1, int y1) { + return subBoardRenderer.isInside(x1, y1); + } +} diff --git a/src/main/java/featurecat/lizzie/gui/VariationTree.java b/src/main/java/featurecat/lizzie/gui/VariationTree.java index 09f4fee0e..3a6789017 100644 --- a/src/main/java/featurecat/lizzie/gui/VariationTree.java +++ b/src/main/java/featurecat/lizzie/gui/VariationTree.java @@ -2,7 +2,13 @@ import featurecat.lizzie.Lizzie; import featurecat.lizzie.rules.BoardHistoryNode; -import java.awt.*; +import featurecat.lizzie.util.Utils; +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.Rectangle; import java.util.ArrayList; import java.util.Optional; @@ -124,7 +130,7 @@ public Optional drawTree( RING_DIAM, RING_DIAM); } - g.setColor(Lizzie.frame.getBlunderNodeColor(cur)); + g.setColor(Utils.getBlunderNodeColor(cur)); g.fillOval(curposx + diff, posy + diff, diam, diam); if (startNode == curMove) { g.setColor(Color.BLACK); @@ -144,9 +150,12 @@ public Optional drawTree( } // Draw main line - while (cur.next().isPresent() && posy + YSPACING < maxposy) { + while (cur.next(true).isPresent() && posy + YSPACING < maxposy) { posy += YSPACING; - cur = cur.next().get(); + cur = cur.next(true).get(); + if (cur.isEndDummay()) { + continue; + } if (calc) { if (inNode(curposx + dotoffset, posy + dotoffset)) { return Optional.of(cur); @@ -168,7 +177,7 @@ public Optional drawTree( RING_DIAM, RING_DIAM); } - g.setColor(Lizzie.frame.getBlunderNodeColor(cur)); + g.setColor(Utils.getBlunderNodeColor(cur)); g.fillOval(curposx + diff, posy + diff, diam, diam); if (cur == curMove) { g.setColor(Color.BLACK); diff --git a/src/main/java/featurecat/lizzie/gui/VariationTreePane.java b/src/main/java/featurecat/lizzie/gui/VariationTreePane.java new file mode 100644 index 000000000..bd1495275 --- /dev/null +++ b/src/main/java/featurecat/lizzie/gui/VariationTreePane.java @@ -0,0 +1,101 @@ +package featurecat.lizzie.gui; + +import static java.awt.image.BufferedImage.TYPE_INT_ARGB; + +import featurecat.lizzie.Lizzie; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.RenderingHints; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.image.BufferedImage; + +/** The window used to display the game. */ +public class VariationTreePane extends LizziePane { + private static VariationTree variationTree; + + private LizzieMain owner; + + // private final BufferStrategy bs; + + /** Creates a window */ + public VariationTreePane(LizzieMain owner) { + super(owner); + this.owner = owner; + + variationTree = new VariationTree(); + + setVisible(false); + + // createBufferStrategy(2); + // bs = getBufferStrategy(); + + addMouseListener( + new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + if (e.getButton() == MouseEvent.BUTTON1) { // left click + onClicked(e.getX(), e.getY()); + } + } + }); + } + + private BufferedImage cachedImage; + + /** + * Draws the game board and interface + * + * @param g0 not used + */ + @Override + protected void paintComponent(Graphics g0) { + super.paintComponent(g0); + + int x = 0; // getX(); + int y = 0; // getY(); + int width = getWidth(); + int height = getHeight(); + + // layout parameters + // initialize + + cachedImage = new BufferedImage(width, height, TYPE_INT_ARGB); + Graphics2D g = (Graphics2D) cachedImage.getGraphics(); + g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + + if (Lizzie.leelaz != null) { // && Lizzie.leelaz.isLoaded()) { + if (Lizzie.config.showVariationGraph) { + g.drawImage(owner.getVariationContainer(this), x, y, null); + if (Lizzie.config.showVariationGraph) { + variationTree.draw(g, x, y, width, height); + } + } + } + + // cleanup + g.dispose(); + + // draw the image + Graphics2D bsGraphics = (Graphics2D) g0; // bs.getDrawGraphics(); + bsGraphics.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); + bsGraphics.drawImage(cachedImage, 0, 0, null); + + // cleanup + bsGraphics.dispose(); + // bs.show(); + } + + /** + * Checks whether or not something was clicked and performs the appropriate action + * + * @param x x coordinate + * @param y y coordinate + */ + public void onClicked(int x, int y) { + if (Lizzie.config.showVariationGraph) { + variationTree.onClicked(x, y); + } + } +} diff --git a/src/main/java/featurecat/lizzie/gui/WinrateGraph.java b/src/main/java/featurecat/lizzie/gui/WinrateGraph.java index 057feaed3..9a8bc0711 100644 --- a/src/main/java/featurecat/lizzie/gui/WinrateGraph.java +++ b/src/main/java/featurecat/lizzie/gui/WinrateGraph.java @@ -3,7 +3,12 @@ import featurecat.lizzie.Lizzie; import featurecat.lizzie.analysis.Leelaz; import featurecat.lizzie.rules.BoardHistoryNode; -import java.awt.*; +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.GradientPaint; +import java.awt.Graphics2D; +import java.awt.Paint; +import java.awt.Stroke; import java.awt.geom.Point2D; import java.util.Optional; diff --git a/src/main/java/featurecat/lizzie/gui/WinratePane.java b/src/main/java/featurecat/lizzie/gui/WinratePane.java new file mode 100644 index 000000000..297d350e7 --- /dev/null +++ b/src/main/java/featurecat/lizzie/gui/WinratePane.java @@ -0,0 +1,269 @@ +package featurecat.lizzie.gui; + +import static java.awt.image.BufferedImage.TYPE_INT_ARGB; +import static java.lang.Math.min; + +import featurecat.lizzie.Lizzie; +import featurecat.lizzie.analysis.Leelaz; +import featurecat.lizzie.rules.BoardData; +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Font; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.RenderingHints; +import java.awt.Stroke; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.event.MouseMotionAdapter; +import java.awt.image.BufferedImage; +import java.util.Optional; + +/** The window used to display the game. */ +public class WinratePane extends LizziePane { + + private LizzieMain owner; + private static WinrateGraph winrateGraph; + private BufferedImage cachedImage; + public int winRateGridLines = 3; + + // private final BufferStrategy bs; + + /** Creates a window */ + public WinratePane(LizzieMain owner) { + super(owner); + this.owner = owner; + + winrateGraph = new WinrateGraph(); + + setVisible(false); + + // createBufferStrategy(2); + // bs = getBufferStrategy(); + + addMouseListener( + new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + if (e.getButton() == MouseEvent.BUTTON1) { // left click + onClicked(e.getX(), e.getY()); + } + } + }); + + addMouseMotionListener( + new MouseMotionAdapter() { + @Override + public void mouseDragged(MouseEvent e) { + onMouseDragged(e.getX(), e.getY()); + } + }); + } + + /** Clears related status from empty board. */ + public void clear() { + if (winrateGraph != null) { + winrateGraph.clear(); + } + } + + /** + * Draws the game board and interface + * + * @param g0 not used + */ + @Override + protected void paintComponent(Graphics g0) { + super.paintComponent(g0); + + int x = 0; // getX(); + int y = 0; // getY(); + int width = getWidth(); + int height = getHeight(); + + // initialize + + cachedImage = new BufferedImage(width, height, TYPE_INT_ARGB); + Graphics2D g = (Graphics2D) cachedImage.getGraphics(); + g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); + + if (Lizzie.leelaz != null) { // && Lizzie.leelaz.isLoaded()) { + if (Lizzie.config.showWinrate) { + g.drawImage(owner.getWinrateContainer(this), x, y, null); + int hh = height * 3 / 13; + drawMoveStatistics(g, x, y, width, hh); + winrateGraph.draw(g, x, y + hh, width, height - hh); + } + } + + // cleanup + g.dispose(); + + // draw the image + Graphics2D bsGraphics = (Graphics2D) g0; // bs.getDrawGraphics(); + bsGraphics.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); + bsGraphics.drawImage(cachedImage, 0, 0, null); + + // cleanup + bsGraphics.dispose(); + // bs.show(); + } + + private void drawMoveStatistics(Graphics2D g, int posX, int posY, int width, int height) { + if (width < 0 || height < 0) return; // we don't have enough space + + double lastWR = 50; // winrate the previous move + boolean validLastWinrate = false; // whether it was actually calculated + Optional previous = Lizzie.board.getHistory().getPrevious(); + if (previous.isPresent() && previous.get().getPlayouts() > 0) { + lastWR = previous.get().winrate; + validLastWinrate = true; + } + + Leelaz.WinrateStats stats = Lizzie.leelaz.getWinrateStats(); + double curWR = stats.maxWinrate; // winrate on this move + boolean validWinrate = (stats.totalPlayouts > 0); // and whether it was actually calculated + if (Lizzie.frame.isPlayingAgainstLeelaz + && Lizzie.frame.playerIsBlack == !Lizzie.board.getHistory().getData().blackToPlay) { + validWinrate = false; + } + + if (!validWinrate) { + curWR = 100 - lastWR; // display last move's winrate for now (with color difference) + } + double whiteWR, blackWR; + if (Lizzie.board.getData().blackToPlay) { + blackWR = curWR; + } else { + blackWR = 100 - curWR; + } + + whiteWR = 100 - blackWR; + + // Background rectangle + g.setColor(new Color(0, 0, 0, 130)); + g.fillRect(posX, posY, width, height); + + // border. does not include bottom edge + int strokeRadius = Lizzie.config.showBorder ? 3 : 1; + g.setStroke(new BasicStroke(strokeRadius == 1 ? strokeRadius : 2 * strokeRadius)); + g.drawLine( + posX + strokeRadius, posY + strokeRadius, + posX - strokeRadius + width, posY + strokeRadius); + if (Lizzie.config.showBorder) { + g.drawLine( + posX + strokeRadius, posY + 3 * strokeRadius, + posX + strokeRadius, posY - strokeRadius + height); + g.drawLine( + posX - strokeRadius + width, posY + 3 * strokeRadius, + posX - strokeRadius + width, posY - strokeRadius + height); + } + + // resize the box now so it's inside the border + posX += 2 * strokeRadius; + posY += 2 * strokeRadius; + width -= 4 * strokeRadius; + height -= 4 * strokeRadius; + + // Title + strokeRadius = 2; + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g.setColor(Color.WHITE); + setPanelFont(g, (int) (min(width, height) * 0.2)); + + // Last move + if (validLastWinrate && validWinrate) { + String text; + if (Lizzie.config.handicapInsteadOfWinrate) { + double currHandicapedWR = Lizzie.leelaz.winrateToHandicap(100 - curWR); + double lastHandicapedWR = Lizzie.leelaz.winrateToHandicap(lastWR); + text = String.format(": %.2f", currHandicapedWR - lastHandicapedWR); + } else { + text = String.format(": %.1f%%", 100 - lastWR - curWR); + } + + g.drawString( + LizzieMain.resourceBundle.getString("LizzieFrame.display.lastMove") + text, + posX + 2 * strokeRadius, + posY + height - 2 * strokeRadius); // - font.getSize()); + } else { + // I think it's more elegant to just not display anything when we don't have + // valid data --dfannius + // g.drawString(resourceBundle.getString("LizzieFrame.display.lastMove") + ": ?%", + // posX + 2 * strokeRadius, posY + height - 2 * strokeRadius); + } + + if (validWinrate || validLastWinrate) { + int maxBarwidth = (int) (width); + int barWidthB = (int) (blackWR * maxBarwidth / 100); + int barWidthW = (int) (whiteWR * maxBarwidth / 100); + int barPosY = posY + height / 3; + int barPosxB = (int) (posX); + int barPosxW = barPosxB + barWidthB; + int barHeight = height / 3; + + // Draw winrate bars + g.fillRect(barPosxW, barPosY, barWidthW, barHeight); + g.setColor(Color.BLACK); + g.fillRect(barPosxB, barPosY, barWidthB, barHeight); + + // Show percentage above bars + g.setColor(Color.WHITE); + g.drawString( + String.format("%.1f%%", blackWR), + barPosxB + 2 * strokeRadius, + posY + barHeight - 2 * strokeRadius); + String winString = String.format("%.1f%%", whiteWR); + int sw = g.getFontMetrics().stringWidth(winString); + g.drawString( + winString, + barPosxB + maxBarwidth - sw - 2 * strokeRadius, + posY + barHeight - 2 * strokeRadius); + + g.setColor(Color.GRAY); + Stroke oldstroke = g.getStroke(); + Stroke dashed = + new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, 0, new float[] {4}, 0); + g.setStroke(dashed); + + for (int i = 1; i <= winRateGridLines; i++) { + int x = barPosxB + (int) (i * (maxBarwidth / (winRateGridLines + 1))); + g.drawLine(x, barPosY, x, barPosY + barHeight); + } + g.setStroke(oldstroke); + } + } + + private void setPanelFont(Graphics2D g, float size) { + Font font = new Font(Lizzie.config.fontName, Font.PLAIN, (int) size); + g.setFont(font); + } + + /** + * Checks whether or not something was clicked and performs the appropriate action + * + * @param x x coordinate + * @param y y coordinate + */ + public void onClicked(int x, int y) { + int moveNumber = winrateGraph.moveNumber(x, y); + if (Lizzie.config.showWinrate && moveNumber >= 0) { + Lizzie.frame.isPlayingAgainstLeelaz = false; + Lizzie.board.goToMoveNumberBeyondBranch(moveNumber); + repaint(); + } + } + + public void onMouseDragged(int x, int y) { + int moveNumber = winrateGraph.moveNumber(x, y); + if (Lizzie.config.showWinrate && moveNumber >= 0) { + if (Lizzie.board.goToMoveNumberWithinBranch(moveNumber)) { + repaint(); + } + } + } + + public int moveNumber(int x, int y) { + return winrateGraph.moveNumber(x, y); + } +} diff --git a/src/main/java/featurecat/lizzie/rules/Board.java b/src/main/java/featurecat/lizzie/rules/Board.java index b4adc0b73..5f91d6f5f 100644 --- a/src/main/java/featurecat/lizzie/rules/Board.java +++ b/src/main/java/featurecat/lizzie/rules/Board.java @@ -21,7 +21,7 @@ import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; -import javax.swing.*; +import javax.swing.JOptionPane; import org.json.JSONException; public class Board implements LeelazListener { @@ -37,9 +37,6 @@ public class Board implements LeelazListener { // Save the node for restore move when in the branch private Optional saveNode; - // Force refresh board - private boolean forceRefresh; - public Board() { initialize(); } @@ -51,7 +48,7 @@ private void initialize() { analysisMode = false; playoutsAnalysis = 100; saveNode = Optional.empty(); - forceRefresh = false; + Lizzie.frame.setForceRefresh(false); history = new BoardHistoryList(BoardData.empty(boardSize)); } @@ -166,18 +163,10 @@ public void reopen(int size) { Zobrist.init(); clear(); Lizzie.leelaz.sendCommand("boardsize " + boardSize); - forceRefresh = true; + Lizzie.frame.setForceRefresh(true); } } - public boolean isForceRefresh() { - return forceRefresh; - } - - public void setForceRefresh(boolean forceRefresh) { - this.forceRefresh = forceRefresh; - } - /** * The comment. Thread safe * @@ -214,6 +203,37 @@ public void moveNumber(int moveNumber) { } } + public int moveNumberByCoord(int[] coord) { + int moveNumber = 0; + if (Lizzie.board.isValid(coord)) { + int index = Lizzie.board.getIndex(coord[0], coord[1]); + if (Lizzie.board.getHistory().getStones()[index] != Stone.EMPTY) { + BoardHistoryNode cur = Lizzie.board.getHistory().getCurrentHistoryNode(); + moveNumber = cur.getData().moveNumberList[index]; + if (!cur.isMainTrunk()) { + if (moveNumber > 0) { + moveNumber = cur.getData().moveNumber - cur.getData().moveMNNumber + moveNumber; + } else { + BoardHistoryNode p = cur.firstParentWithVariations().orElse(cur); + while (p != cur && moveNumber == 0) { + moveNumber = p.getData().moveNumberList[index]; + if (moveNumber > 0) { + BoardHistoryNode topOfTop = p.firstParentWithVariations().orElse(p); + if (topOfTop != p) { + moveNumber = p.getData().moveNumber - p.getData().moveMNNumber + moveNumber; + } + } else { + cur = p; + p = cur.firstParentWithVariations().orElse(cur); + } + } + } + } + } + } + return moveNumber; + } + /** * Add a stone to the board representation. Thread safe * @@ -232,7 +252,7 @@ public void addStone(int x, int y, Stone color) { stones[getIndex(x, y)] = color; zobrist.toggleStone(x, y, color); - Lizzie.frame.repaint(); + Lizzie.frame.refresh(); } } @@ -257,7 +277,7 @@ public void removeStone(int x, int y, Stone color) { zobrist.toggleStone(x, y, oriColor); data.moveNumberList[Board.getIndex(x, y)] = 0; - Lizzie.frame.repaint(); + Lizzie.frame.refresh(); } } @@ -329,7 +349,7 @@ public void pass(Stone color, boolean newBranch, boolean dummy, boolean changeMo Zobrist zobrist = history.getZobrist(); int moveNumber = history.getMoveNumber() + 1; int[] moveNumberList = - newBranch && history.getNext().isPresent() + newBranch && history.getNext(true).isPresent() ? new int[Board.boardSize * Board.boardSize] : history.getMoveNumberList().clone(); @@ -357,7 +377,7 @@ public void pass(Stone color, boolean newBranch, boolean dummy, boolean changeMo // update history with pass history.addOrGoto(newState, newBranch, changeMove); - Lizzie.frame.repaint(); + Lizzie.frame.refresh(); } } @@ -378,7 +398,7 @@ public void place(int x, int y, Stone color) { } public void place(int x, int y, Stone color, boolean newBranch) { - place(x, y, color, false, false); + place(x, y, color, newBranch, false); } /** @@ -434,7 +454,7 @@ public void place(int x, int y, Stone color, boolean newBranch, boolean changeMo int moveMNNumber = history.getMoveMNNumber() > -1 && !newBranch ? history.getMoveMNNumber() + 1 : -1; int[] moveNumberList = - newBranch && history.getNext().isPresent() + newBranch && history.getNext(true).isPresent() ? new int[Board.boardSize * Board.boardSize] : history.getMoveNumberList().clone(); @@ -478,6 +498,7 @@ public void place(int x, int y, Stone color, boolean newBranch, boolean changeMo nextWinrate, 0); newState.moveMNNumber = moveMNNumber; + newState.dummy = false; // don't make this coordinate if it is suicidal or violates superko if (isSuicidal > 0 || history.violatesKoRule(newState)) return; @@ -494,7 +515,7 @@ public void place(int x, int y, Stone color, boolean newBranch, boolean changeMo // update history with this coordinate history.addOrGoto(newState, newBranch, changeMove); - Lizzie.frame.repaint(); + Lizzie.frame.refresh(); } } @@ -556,7 +577,7 @@ public void flatten() { * @param zobrist the zobrist object to modify * @return number of removed stones */ - private int removeDeadChain(int x, int y, Stone color, Stone[] stones, Zobrist zobrist) { + public static int removeDeadChain(int x, int y, Stone color, Stone[] stones, Zobrist zobrist) { if (!isValid(x, y) || stones[getIndex(x, y)] != color) return 0; boolean hasLiberties = hasLibertiesHelper(x, y, color, stones); @@ -575,7 +596,7 @@ private int removeDeadChain(int x, int y, Stone color, Stone[] stones, Zobrist z * @param stones the stones array to modify * @return whether or not this chain has liberties */ - private boolean hasLibertiesHelper(int x, int y, Stone color, Stone[] stones) { + private static boolean hasLibertiesHelper(int x, int y, Stone color, Stone[] stones) { if (!isValid(x, y)) return false; if (stones[getIndex(x, y)] == Stone.EMPTY) return true; // a liberty was found @@ -607,7 +628,7 @@ else if (stones[getIndex(x, y)] != color) * their unrecursed version * @return number of removed stones */ - private int cleanupHasLibertiesHelper( + private static int cleanupHasLibertiesHelper( int x, int y, Stone color, Stone[] stones, Zobrist zobrist, boolean removeStones) { int removed = 0; if (!isValid(x, y) || stones[getIndex(x, y)] != color) return 0; @@ -676,7 +697,7 @@ public boolean nextMove() { } else { Lizzie.leelaz.playMove(history.getLastMoveColor(), "pass"); } - Lizzie.frame.repaint(); + Lizzie.frame.refresh(); return true; } return false; @@ -807,7 +828,7 @@ public boolean nextVariation(int idx) { } else { Lizzie.leelaz.playMove(history.getLastMoveColor(), "pass"); } - Lizzie.frame.repaint(); + Lizzie.frame.refresh(); return true; } return false; @@ -963,7 +984,7 @@ public void moveBranchDown() { public void deleteMove() { synchronized (this) { BoardHistoryNode currentNode = history.getCurrentHistoryNode(); - if (currentNode.next().isPresent()) { + if (currentNode.next(true).isPresent()) { // Will delete more than one move, ask for confirmation int ret = JOptionPane.showConfirmDialog( @@ -1020,7 +1041,7 @@ public boolean previousMove() { updateWinrate(); if (history.previous().isPresent()) { Lizzie.leelaz.undo(); - Lizzie.frame.repaint(); + Lizzie.frame.refresh(); return true; } return false; @@ -1115,7 +1136,7 @@ private void toggleLiveStatus(Stone[] stones, int stonex, int stoney) { x++; } } - Lizzie.frame.repaint(); + Lizzie.frame.refresh(); } /* diff --git a/src/main/java/featurecat/lizzie/rules/BoardData.java b/src/main/java/featurecat/lizzie/rules/BoardData.java index 0f757a57d..6b9a092e8 100644 --- a/src/main/java/featurecat/lizzie/rules/BoardData.java +++ b/src/main/java/featurecat/lizzie/rules/BoardData.java @@ -2,7 +2,11 @@ import featurecat.lizzie.Lizzie; import featurecat.lizzie.analysis.MoveData; -import java.util.*; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; public class BoardData { public int moveNumber; @@ -188,4 +192,33 @@ public void setPlayouts(int playouts) { public int getPlayouts() { return playouts; } + + public void sync(BoardData data) { + this.moveMNNumber = data.moveMNNumber; + this.moveNumber = data.moveNumber; + this.lastMove = data.lastMove; + this.moveNumberList = data.moveNumberList; + this.blackToPlay = data.blackToPlay; + this.dummy = data.dummy; + this.lastMoveColor = data.lastMoveColor; + this.stones = data.stones; + this.zobrist = data.zobrist; + this.verify = data.verify; + this.blackCaptures = data.blackCaptures; + this.whiteCaptures = data.whiteCaptures; + this.comment = data.comment; + } + + public BoardData clone() { + BoardData data = BoardData.empty(19); + data.sync(this); + return data; + } + + public boolean isSameCoord(int[] coord) { + if (coord == null || coord.length < 2 || !this.lastMove.isPresent()) { + return false; + } + return this.lastMove.map(m -> (m[0] == coord[0] && m[1] == coord[1])).orElse(false); + } } diff --git a/src/main/java/featurecat/lizzie/rules/BoardHistoryList.java b/src/main/java/featurecat/lizzie/rules/BoardHistoryList.java index ce54886d3..bbf8598f4 100644 --- a/src/main/java/featurecat/lizzie/rules/BoardHistoryList.java +++ b/src/main/java/featurecat/lizzie/rules/BoardHistoryList.java @@ -76,13 +76,29 @@ public void toStart() { while (previous().isPresent()) ; } + public void toBranchTop() { + BoardHistoryNode start = head; + while (start.previous().isPresent()) { + BoardHistoryNode pre = start.previous().get(); + if (pre.next(true).isPresent() && pre.next(true).get() != start) { + previous(); + break; + } + previous(); + start = pre; + } + } /** * moves the pointer to the right, returns the data stored there * * @return the data of next node, Optional.empty if there is no next node */ public Optional next() { - Optional n = head.next(); + return next(false); + } + + public Optional next(boolean includeDummay) { + Optional n = head.next(includeDummay); n.ifPresent(x -> head = x); return n.map(x -> x.getData()); } @@ -104,7 +120,11 @@ public Optional nextVariation(int idx) { * @return the data stored at the next index, if any, Optional.empty otherwise. */ public Optional getNext() { - return head.next().map(x -> x.getData()); + return getNext(false); + } + + public Optional getNext(boolean includeDummy) { + return head.next(includeDummy).map(x -> x.getData()); } /** @return nexts for display */ @@ -240,4 +260,285 @@ public BoardHistoryNode getEnd() { } return e; } + + public void pass(Stone color) { + pass(color, false, false, false); + } + + public void pass(Stone color, boolean newBranch) { + pass(color, newBranch, false, false); + } + + public void pass(Stone color, boolean newBranch, boolean dummy) { + pass(color, newBranch, dummy, false); + } + + public void pass(Stone color, boolean newBranch, boolean dummy, boolean changeMove) { + synchronized (this) { + + // check to see if this move is being replayed in history + if (this.getNext().map(n -> !n.lastMove.isPresent()).orElse(false) && !newBranch) { + // this is the next move in history. Just increment history so that we don't erase the + // redo's + this.next(); + return; + } + + Stone[] stones = this.getStones().clone(); + Zobrist zobrist = this.getZobrist(); + int moveNumber = this.getMoveNumber() + 1; + int[] moveNumberList = + newBranch && this.getNext(true).isPresent() + ? new int[Board.boardSize * Board.boardSize] + : this.getMoveNumberList().clone(); + + // build the new game state + BoardData newState = + new BoardData( + stones, + Optional.empty(), + color, + color.equals(Stone.WHITE), + zobrist, + moveNumber, + moveNumberList, + this.getData().blackCaptures, + this.getData().whiteCaptures, + 0, + 0); + newState.dummy = dummy; + + // update history with pass + this.addOrGoto(newState, newBranch, changeMove); + } + } + + public void place(int x, int y, Stone color) { + place(x, y, color, false); + } + + public void place(int x, int y, Stone color, boolean newBranch) { + place(x, y, color, newBranch, false); + } + + public void place(int x, int y, Stone color, boolean newBranch, boolean changeMove) { + synchronized (this) { + if (!Board.isValid(x, y) + || (this.getStones()[Board.getIndex(x, y)] != Stone.EMPTY && !newBranch)) return; + + double nextWinrate = -100; + if (this.getData().winrate >= 0) nextWinrate = 100 - this.getData().winrate; + + // check to see if this coordinate is being replayed in history + Optional nextLast = this.getNext().flatMap(n -> n.lastMove); + if (nextLast.isPresent() + && nextLast.get()[0] == x + && nextLast.get()[1] == y + && !newBranch + && !changeMove) { + // this is the next coordinate in history. Just increment history so that we don't erase the + // redo's + this.next(); + return; + } + + // load a copy of the data at the current node of history + Stone[] stones = this.getStones().clone(); + Zobrist zobrist = this.getZobrist(); + Optional lastMove = Optional.of(new int[] {x, y}); + int moveNumber = this.getMoveNumber() + 1; + int moveMNNumber = + this.getMoveMNNumber() > -1 && !newBranch ? this.getMoveMNNumber() + 1 : -1; + int[] moveNumberList = + newBranch && this.getNext(true).isPresent() + ? new int[Board.boardSize * Board.boardSize] + : this.getMoveNumberList().clone(); + + moveNumberList[Board.getIndex(x, y)] = moveMNNumber > -1 ? moveMNNumber : moveNumber; + + // set the stone at (x, y) to color + stones[Board.getIndex(x, y)] = color; + zobrist.toggleStone(x, y, color); + + // remove enemy stones + int capturedStones = 0; + capturedStones += Board.removeDeadChain(x + 1, y, color.opposite(), stones, zobrist); + capturedStones += Board.removeDeadChain(x, y + 1, color.opposite(), stones, zobrist); + capturedStones += Board.removeDeadChain(x - 1, y, color.opposite(), stones, zobrist); + capturedStones += Board.removeDeadChain(x, y - 1, color.opposite(), stones, zobrist); + + // check to see if the player made a suicidal coordinate + int isSuicidal = Board.removeDeadChain(x, y, color, stones, zobrist); + + for (int i = 0; i < Board.boardSize * Board.boardSize; i++) { + if (stones[i].equals(Stone.EMPTY)) { + moveNumberList[i] = 0; + } + } + + int bc = this.getData().blackCaptures; + int wc = this.getData().whiteCaptures; + if (color.isBlack()) bc += capturedStones; + else wc += capturedStones; + BoardData newState = + new BoardData( + stones, + lastMove, + color, + color.equals(Stone.WHITE), + zobrist, + moveNumber, + moveNumberList, + bc, + wc, + nextWinrate, + 0); + newState.moveMNNumber = moveMNNumber; + + // don't make this coordinate if it is suicidal or violates superko + if (isSuicidal > 0 || this.violatesKoRule(newState)) return; + + // update history with this coordinate + this.addOrGoto(newState, newBranch, changeMove); + } + } + + public void addNodeProperty(String key, String value) { + synchronized (this) { + this.getData().addProperty(key, value); + if ("MN".equals(key)) { + moveNumber(Integer.parseInt(value)); + } + } + } + + public void moveNumber(int moveNumber) { + synchronized (this) { + BoardData data = this.getData(); + if (data.lastMove.isPresent()) { + int[] moveNumberList = this.getMoveNumberList(); + moveNumberList[Board.getIndex(data.lastMove.get()[0], data.lastMove.get()[1])] = moveNumber; + Optional node = this.getCurrentHistoryNode().previous(); + while (node.isPresent() && node.get().numberOfChildren() <= 1) { + BoardData nodeData = node.get().getData(); + if (nodeData.lastMove.isPresent() && nodeData.moveNumber >= moveNumber) { + moveNumber = (moveNumber > 1) ? moveNumber - 1 : 0; + moveNumberList[Board.getIndex(nodeData.lastMove.get()[0], nodeData.lastMove.get()[1])] = + moveNumber; + } + node = node.get().previous(); + } + } + } + } + + public void addStone(int x, int y, Stone color) { + synchronized (this) { + if (!Board.isValid(x, y) || this.getStones()[Board.getIndex(x, y)] != Stone.EMPTY) return; + + Stone[] stones = this.getData().stones; + Zobrist zobrist = this.getData().zobrist; + + // set the stone at (x, y) to color + stones[Board.getIndex(x, y)] = color; + zobrist.toggleStone(x, y, color); + } + } + + public void removeStone(int x, int y, Stone color) { + synchronized (this) { + if (!Board.isValid(x, y) || this.getStones()[Board.getIndex(x, y)] == Stone.EMPTY) return; + + BoardData data = this.getData(); + Stone[] stones = data.stones; + Zobrist zobrist = data.zobrist; + + // set the stone at (x, y) to empty + Stone oriColor = stones[Board.getIndex(x, y)]; + stones[Board.getIndex(x, y)] = Stone.EMPTY; + zobrist.toggleStone(x, y, oriColor); + data.moveNumberList[Board.getIndex(x, y)] = 0; + } + } + + public void flatten() { + Stone[] stones = this.getStones(); + boolean blackToPlay = this.isBlacksTurn(); + Zobrist zobrist = this.getZobrist().clone(); + BoardHistoryList oldHistory = this; + + head = + new BoardHistoryNode( + new BoardData( + stones, + Optional.empty(), + Stone.EMPTY, + blackToPlay, + zobrist, + 0, + new int[Board.boardSize * Board.boardSize], + 0, + 0, + 0.0, + 0)); + this.setGameInfo(oldHistory.getGameInfo()); + } + + public boolean goToMoveNumber(int moveNumber, boolean withinBranch) { + int delta = moveNumber - this.getMoveNumber(); + boolean moved = false; + for (int i = 0; i < Math.abs(delta); i++) { + if (withinBranch && delta < 0) { + BoardHistoryNode currentNode = this.getCurrentHistoryNode(); + if (!currentNode.isFirstChild()) { + break; + } + } + if (!(delta > 0 ? next().isPresent() : previous().isPresent())) { + break; + } + moved = true; + } + return moved; + } + + public int sync(BoardHistoryList newList) { + int diffMoveNo = -1; + + BoardHistoryNode node = this.getCurrentHistoryNode(); + BoardHistoryNode prev = node.previous().map(p -> p).orElse(null); + // From begin + while (prev != null) { + node = prev; + prev = node.previous().map(p -> p).orElse(null); + } + // Compare + BoardHistoryNode newNode = newList.getCurrentHistoryNode(); + + while (newNode != null) { + if (node == null) { + // Add + prev.addOrGoto(newNode.getData().clone()); + node = prev.next().map(n -> n).orElse(null); + if (diffMoveNo < 0) { + diffMoveNo = newNode.getData().moveNumber; + } + node.sync(newNode); + break; + } else { + if (!node.compare(newNode)) { + if (diffMoveNo < 0) { + diffMoveNo = newNode.getData().moveNumber; + } + node.sync(newNode); + break; + } + } + prev = node; + node = node.next(true).map(n -> n).orElse(null); + newNode = newNode.next(true).map(n -> n).orElse(null); + } + + return diffMoveNo; + } } diff --git a/src/main/java/featurecat/lizzie/rules/BoardHistoryNode.java b/src/main/java/featurecat/lizzie/rules/BoardHistoryNode.java index dd7c778d4..6a8058f37 100644 --- a/src/main/java/featurecat/lizzie/rules/BoardHistoryNode.java +++ b/src/main/java/featurecat/lizzie/rules/BoardHistoryNode.java @@ -53,7 +53,7 @@ public BoardHistoryNode addOrGoto(BoardData data) { } public BoardHistoryNode addOrGoto(BoardData data, boolean newBranch) { - return addOrGoto(data, false, false); + return addOrGoto(data, newBranch, false); } /** @@ -89,6 +89,9 @@ public BoardHistoryNode addOrGoto(BoardData data, boolean newBranch, boolean cha // variations.set(i, variations.get(0)); // variations.set(0, currentNext); // } + if (i != 0 && changeMove) { + break; + } return variations.get(i); } } @@ -109,7 +112,7 @@ public BoardHistoryNode addOrGoto(BoardData data, boolean newBranch, boolean cha } BoardHistoryNode node = new BoardHistoryNode(data); if (changeMove) { - Optional next = next(); + Optional next = next(true); next.ifPresent( n -> { node.variations = n.variations; @@ -141,7 +144,17 @@ public Optional previous() { } public Optional next() { - return variations.isEmpty() ? Optional.empty() : Optional.of(variations.get(0)); + return next(false); + } + + public Optional next(boolean includeDummy) { + return variations.isEmpty() || (!includeDummy && variations.get(0).isEndDummay()) + ? Optional.empty() + : Optional.of(variations.get(0)); + } + + public boolean isEndDummay() { + return this.data.dummy && variations.isEmpty(); } public BoardHistoryNode topOfBranch() { @@ -368,7 +381,7 @@ public Optional findChildOfPreviousWithVariation() { * @return index of child node, -1 if child node not a child of parent */ public int indexOfNode(BoardHistoryNode childNode) { - if (!next().isPresent()) { + if (!next(true).isPresent()) { return -1; } for (int i = 0; i < numberOfChildren(); i++) { @@ -459,6 +472,40 @@ public boolean isMainTrunk() { return true; } + /** + * Finds the next node with the comment. + * + * @return the first next node with comment, if any, Optional.empty otherwise + */ + public Optional findNextNodeWithComment() { + BoardHistoryNode node = this; + while (node.next().isPresent()) { + BoardHistoryNode next = node.next().get(); + if (!next.getData().comment.isEmpty()) { + return Optional.ofNullable(next); + } + node = next; + } + return Optional.empty(); + } + + /** + * Finds the previous node with the comment. + * + * @return the first previous node with comment, if any, Optional.empty otherwise + */ + public Optional findPreviousNodeWithComment() { + BoardHistoryNode node = this; + while (node.previous().isPresent()) { + BoardHistoryNode previous = node.previous().get(); + if (!previous.getData().comment.isEmpty()) { + return Optional.ofNullable(previous); + } + node = previous; + } + return Optional.empty(); + } + /** * Go to the next node with the comment. * @@ -496,4 +543,55 @@ public int goToPreviousNodeWithComment() { } return moves; } + + public boolean compare(BoardHistoryNode node) { + BoardData sData = this.getData(); + BoardData dData = node.getData(); + + boolean dMove = + sData.lastMove.isPresent() && dData.lastMove.isPresent() + || !sData.lastMove.isPresent() && !dData.lastMove.isPresent(); + if (dMove && sData.lastMove.isPresent()) { + int[] sM = sData.lastMove.get(); + int[] dM = dData.lastMove.get(); + dMove = + (sM != null + && sM.length == 2 + && dM != null + && dM.length == 2 + && sM[0] == dM[0] + && sM[1] == dM[1]); + } + return dMove + && sData.comment != null + && sData.comment.equals(dData.comment) + && this.numberOfChildren() == node.numberOfChildren(); + } + + public void sync(BoardHistoryNode node) { + if (node == null) return; + + BoardHistoryNode cur = this; + // Compare + while (node != null) { + if (!cur.compare(node)) { + BoardData sData = cur.getData(); + sData.sync(node.getData()); + if (node.numberOfChildren() > 0) { + for (int i = 0; i < node.numberOfChildren(); i++) { + if (node.getVariation(i).isPresent()) { + if (cur.variations.size() <= i) { + cur.addOrGoto(node.getVariation(i).get().getData().clone(), (i > 0)); + } + if (i > 0) { + cur.variations.get(i).sync(node.getVariation(i).get()); + } + } + } + } + } + cur = cur.next(true).map(n -> n).orElse(null); + node = node.next(true).map(n -> n).orElse(null); + } + } } diff --git a/src/main/java/featurecat/lizzie/rules/SGFParser.java b/src/main/java/featurecat/lizzie/rules/SGFParser.java index d15a3feab..4226d5e53 100644 --- a/src/main/java/featurecat/lizzie/rules/SGFParser.java +++ b/src/main/java/featurecat/lizzie/rules/SGFParser.java @@ -6,7 +6,15 @@ import featurecat.lizzie.analysis.GameInfo; import featurecat.lizzie.analysis.Leelaz; import featurecat.lizzie.util.EncodingDetector; -import java.io.*; +import featurecat.lizzie.util.Utils; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.StringWriter; +import java.io.Writer; import java.text.SimpleDateFormat; import java.util.HashMap; import java.util.Map; @@ -190,7 +198,7 @@ private static boolean parse(String value) { Lizzie.board.place(move[0], move[1], color, newBranch); } if (newBranch) { - processPendingPros(pendingProps); + processPendingPros(Lizzie.board.getHistory(), pendingProps); } } else if (tag.equals("C")) { // Support comment @@ -217,7 +225,7 @@ private static boolean parse(String value) { .replaceAll("[^0-9]", "")); Lizzie.board.getData().setPlayouts(numPlayouts); if (numPlayouts > 0 && !line2.isEmpty()) { - Lizzie.board.getData().bestMoves = Leelaz.parseInfo(line2); + Lizzie.board.getData().bestMoves = Lizzie.leelaz.parseInfo(line2); } } else if (tag.equals("AB") || tag.equals("AW")) { int[] move = convertSgfPosToCoord(tagContent); @@ -231,7 +239,7 @@ private static boolean parse(String value) { boolean newBranch = (subTreeStepMap.get(subTreeDepth) == 1); Lizzie.board.pass(color, newBranch, true); if (newBranch) { - processPendingPros(pendingProps); + processPendingPros(Lizzie.board.getHistory(), pendingProps); } addPassForMove = false; } @@ -275,7 +283,7 @@ private static boolean parse(String value) { boolean newBranch = (subTreeStepMap.get(subTreeDepth) == 1); Lizzie.board.pass(color, newBranch, true); if (newBranch) { - processPendingPros(pendingProps); + processPendingPros(Lizzie.board.getHistory(), pendingProps); } addPassForMove = false; } @@ -326,6 +334,7 @@ private static boolean parse(String value) { // Set AW/AB Comment if (!headComment.isEmpty()) { Lizzie.board.comment(headComment); + Lizzie.frame.refresh(); } if (gameProperties.size() > 0) { Lizzie.board.addNodeProperties(gameProperties); @@ -504,7 +513,7 @@ private static String formatComment(BoardHistoryNode node) { String engine = Lizzie.leelaz.currentWeight(); // Playouts - String playouts = Lizzie.frame.getPlayoutsString(data.getPlayouts()); + String playouts = Utils.getPlayoutsString(data.getPlayouts()); // Last winrate Optional lastNode = node.previous().flatMap(n -> Optional.of(n.getData())); @@ -569,7 +578,7 @@ private static String formatNodeData(BoardHistoryNode node) { BoardData data = node.getData(); // Playouts - String playouts = Lizzie.frame.getPlayoutsString(data.getPlayouts()); + String playouts = Utils.getPlayoutsString(data.getPlayouts()); // Last winrate Optional lastNode = node.previous().flatMap(n -> Optional.of(n.getData())); @@ -726,8 +735,8 @@ public static String nodeString(String key, String value) { return sb.toString(); } - private static void processPendingPros(Map props) { - props.forEach((key, value) -> Lizzie.board.addNodeProperty(key, value)); + private static void processPendingPros(BoardHistoryList history, Map props) { + props.forEach((key, value) -> history.addNodeProperty(key, value)); props = new HashMap(); } diff --git a/src/main/java/featurecat/lizzie/util/Utils.java b/src/main/java/featurecat/lizzie/util/Utils.java new file mode 100644 index 000000000..b2ab56f25 --- /dev/null +++ b/src/main/java/featurecat/lizzie/util/Utils.java @@ -0,0 +1,162 @@ +package featurecat.lizzie.util; + +import static java.lang.Math.round; + +import featurecat.lizzie.Lizzie; +import featurecat.lizzie.analysis.Leelaz; +import featurecat.lizzie.rules.BoardData; +import featurecat.lizzie.rules.BoardHistoryNode; +import java.awt.Color; +import java.awt.FontMetrics; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import javax.swing.JTextField; + +public class Utils { + + public static boolean isBlank(String str) { + return str == null || str.trim().isEmpty(); + } + + /** + * @return a shorter, rounded string version of playouts. e.g. 345 -> 345, 1265 -> 1.3k, 44556 -> + * 45k, 133523 -> 134k, 1234567 -> 1.2m + */ + public static String getPlayoutsString(int playouts) { + if (playouts >= 1_000_000) { + double playoutsDouble = (double) playouts / 100_000; // 1234567 -> 12.34567 + return round(playoutsDouble) / 10.0 + "m"; + } else if (playouts >= 10_000) { + double playoutsDouble = (double) playouts / 1_000; // 13265 -> 13.265 + return round(playoutsDouble) + "k"; + } else if (playouts >= 1_000) { + double playoutsDouble = (double) playouts / 100; // 1265 -> 12.65 + return round(playoutsDouble) / 10.0 + "k"; + } else { + return String.valueOf(playouts); + } + } + + /** + * Truncate text that is too long for the given width + * + * @param line + * @param fm + * @param fitWidth + * @return fitted + */ + public static String truncateStringByWidth(String line, FontMetrics fm, int fitWidth) { + if (line.isEmpty()) { + return ""; + } + int width = fm.stringWidth(line); + if (width > fitWidth) { + int guess = line.length() * fitWidth / width; + String before = line.substring(0, guess).trim(); + width = fm.stringWidth(before); + if (width > fitWidth) { + int diff = width - fitWidth; + int i = 0; + for (; (diff > 0 && i < 5); i++) { + diff = diff - fm.stringWidth(line.substring(guess - i - 1, guess - i)); + } + return line.substring(0, guess - i).trim(); + } else { + return before; + } + } else { + return line; + } + } + + public static double lastWinrateDiff(BoardHistoryNode node) { + + // Last winrate + Optional lastNode = node.previous().flatMap(n -> Optional.of(n.getData())); + boolean validLastWinrate = lastNode.map(d -> d.getPlayouts() > 0).orElse(false); + double lastWR = validLastWinrate ? lastNode.get().winrate : 50; + + // Current winrate + BoardData data = node.getData(); + boolean validWinrate = false; + double curWR = 50; + if (data == Lizzie.board.getHistory().getData()) { + Leelaz.WinrateStats stats = Lizzie.leelaz.getWinrateStats(); + curWR = stats.maxWinrate; + validWinrate = (stats.totalPlayouts > 0); + if (Lizzie.frame.isPlayingAgainstLeelaz + && Lizzie.frame.playerIsBlack == !Lizzie.board.getHistory().getData().blackToPlay) { + validWinrate = false; + } + } else { + validWinrate = (data.getPlayouts() > 0); + curWR = validWinrate ? data.winrate : 100 - lastWR; + } + + // Last move difference winrate + if (validLastWinrate && validWinrate) { + return 100 - lastWR - curWR; + } else { + return 0; + } + } + + public static Color getBlunderNodeColor(BoardHistoryNode node) { + if (Lizzie.config.nodeColorMode == 1 && node.getData().blackToPlay + || Lizzie.config.nodeColorMode == 2 && !node.getData().blackToPlay) { + return Color.WHITE; + } + double diffWinrate = lastWinrateDiff(node); + Optional st = + diffWinrate >= 0 + ? Lizzie.config.blunderWinrateThresholds.flatMap( + l -> l.stream().filter(t -> (t > 0 && t <= diffWinrate)).reduce((f, s) -> s)) + : Lizzie.config.blunderWinrateThresholds.flatMap( + l -> l.stream().filter(t -> (t < 0 && t >= diffWinrate)).reduce((f, s) -> f)); + if (st.isPresent()) { + return Lizzie.config.blunderNodeColors.map(m -> m.get(st.get())).get(); + } else { + return Color.WHITE; + } + } + + public static Integer txtFieldValue(JTextField txt) { + if (txt.getText().trim().isEmpty() + || txt.getText().trim().length() >= String.valueOf(Integer.MAX_VALUE).length()) { + return 0; + } else { + return Integer.parseInt(txt.getText().trim()); + } + } + + public static int intOfMap(Map map, String key) { + if (map == null) { + return 0; + } + List s = (List) map.get(key); + if (s == null || s.size() <= 0) { + return 0; + } + try { + return Integer.parseInt((String) s.get(0)); + } catch (NumberFormatException e) { + return 0; + } + } + + public static String stringOfMap(Map map, String key) { + if (map == null) { + return ""; + } + List s = (List) map.get(key); + if (s == null || s.size() <= 0) { + return ""; + } + try { + return (String) s.get(0); + } catch (NumberFormatException e) { + return ""; + } + } +} diff --git a/src/main/java/featurecat/lizzie/util/WindowPosition.java b/src/main/java/featurecat/lizzie/util/WindowPosition.java new file mode 100644 index 000000000..ca37a91f3 --- /dev/null +++ b/src/main/java/featurecat/lizzie/util/WindowPosition.java @@ -0,0 +1,156 @@ +package featurecat.lizzie.util; + +import featurecat.lizzie.Lizzie; +import featurecat.lizzie.gui.BasicInfoPane; +import featurecat.lizzie.gui.BoardPane; +import featurecat.lizzie.gui.CommentPane; +import featurecat.lizzie.gui.LizzieMain; +import featurecat.lizzie.gui.LizziePane; +import featurecat.lizzie.gui.SubBoardPane; +import featurecat.lizzie.gui.VariationTreePane; +import featurecat.lizzie.gui.WinratePane; +import java.awt.Dimension; +import java.awt.Insets; +import java.awt.Point; +import java.awt.Window; +import javax.swing.JFrame; +import javax.swing.SwingUtilities; +import org.json.JSONArray; +import org.json.JSONObject; + +public class WindowPosition { + + public static JSONObject create(JSONObject ui) { + if (ui == null) { + ui = new JSONObject(); + } + + // Main Window Position & Size + ui.put("main-window-position", new JSONArray("[]")); + ui.put("gtp-console-position", new JSONArray("[]")); + ui.put("board-position-proportion", 4); + // Panes + ui.put("main-board-position", new JSONArray("[]")); + ui.put("sub-board-position", new JSONArray("[]")); + ui.put("basic-info-position", new JSONArray("[]")); + ui.put("winrate-position", new JSONArray("[]")); + ui.put("variation-tree-position", new JSONArray("[]")); + ui.put("comment-position", new JSONArray("[]")); + + ui.put("window-maximized", false); + + return ui; + } + + public static JSONObject save(JSONObject ui) { + if (ui == null) { + ui = new JSONObject(); + } + + boolean windowIsMaximized = Lizzie.frame.getExtendedState() == JFrame.MAXIMIZED_BOTH; + ui.put("window-maximized", windowIsMaximized); + ui.put("board-position-proportion", Lizzie.frame.boardPositionProportion); + + JSONArray mainPos = new JSONArray(); + if (!windowIsMaximized) { + mainPos.put(Lizzie.frame.getX()); + mainPos.put(Lizzie.frame.getY()); + mainPos.put(Lizzie.frame.getWidth()); + mainPos.put(Lizzie.frame.getHeight()); + } + ui.put("main-window-position", mainPos); + + JSONArray gtpPos = new JSONArray(); + gtpPos.put(Lizzie.gtpConsole.getX()); + gtpPos.put(Lizzie.gtpConsole.getY()); + gtpPos.put(Lizzie.gtpConsole.getWidth()); + gtpPos.put(Lizzie.gtpConsole.getHeight()); + ui.put("gtp-console-position", gtpPos); + + // Panes + if (Lizzie.frame instanceof LizzieMain) { + LizzieMain main = (LizzieMain) Lizzie.frame; + ui.put("main-board-position", getWindowPos(main.boardPane)); + ui.put("sub-board-position", getWindowPos(main.subBoardPane)); + ui.put("basic-info-position", getWindowPos(main.basicInfoPane)); + ui.put("winrate-position", getWindowPos(main.winratePane)); + ui.put("variation-tree-position", getWindowPos(main.variationTreePane)); + ui.put("comment-position", getWindowPos(main.commentPane)); + } + + return ui; + } + + public static JSONArray mainWindowPos() { + + // Main + boolean persisted = (Lizzie.config.persistedUi != null); + if (persisted + && Lizzie.config.persistedUi.optJSONArray("main-window-position") != null + && Lizzie.config.persistedUi.optJSONArray("main-window-position").length() == 4) { + return Lizzie.config.persistedUi.getJSONArray("main-window-position"); + } else { + return null; + } + } + + public static JSONArray gtpWindowPos() { + + boolean persisted = Lizzie.config.persistedUi != null; + if (persisted + && Lizzie.config.persistedUi.optJSONArray("gtp-console-position") != null + && Lizzie.config.persistedUi.optJSONArray("gtp-console-position").length() == 4) { + return Lizzie.config.persistedUi.getJSONArray("gtp-console-position"); + } else { + return null; + } + } + + public static void restorePane(JSONObject ui, LizziePane pane) { + if (ui == null) { + return; + } + JSONArray pos = getPersistedPanePos(pane, ui); + if (pos != null) { + pane.toWindow( + new Point(pos.getInt(0), pos.getInt(1)), new Dimension(pos.getInt(2), pos.getInt(3))); + } + } + + public static JSONArray getWindowPos(LizziePane pane) { + JSONArray panePos = new JSONArray("[]"); + Window paneWindow = SwingUtilities.getWindowAncestor(pane); + if (!(paneWindow instanceof LizzieMain)) { + Insets insets = paneWindow.getInsets(); + panePos.put(paneWindow.getX()); + panePos.put(paneWindow.getY()); + panePos.put(paneWindow.getWidth() - insets.left - insets.right); + panePos.put(paneWindow.getHeight() - insets.top - insets.bottom); + } + return panePos; + } + + public static JSONArray getPersistedPanePos(LizziePane pane, JSONObject ui) { + String key = ""; + if (pane instanceof BoardPane) { + key = "main-board-position"; + } else if (pane instanceof SubBoardPane) { + key = "sub-board-position"; + } else if (pane instanceof BasicInfoPane) { + key = "basic-info-position"; + } else if (pane instanceof WinratePane) { + key = "winrate-position"; + } else if (pane instanceof VariationTreePane) { + key = "variation-tree-position"; + } else if (pane instanceof CommentPane) { + key = "comment-position"; + } + JSONArray pos = null; + if (!key.isEmpty()) { + if (ui.optJSONArray(key) != null && ui.optJSONArray(key).length() == 4) { + pos = ui.getJSONArray(key); + } + } + return pos; + } +} diff --git a/src/main/resources/l10n/DisplayStrings.properties b/src/main/resources/l10n/DisplayStrings.properties index cec15df04..c6ae04c26 100644 --- a/src/main/resources/l10n/DisplayStrings.properties +++ b/src/main/resources/l10n/DisplayStrings.properties @@ -15,7 +15,7 @@ # # You can directly use the original display texts as the key, but it is recommended to name the key properly. -LizzieFrame.title=Lizzie - Leela Zero Interface +LizzieFrame.title=Lizzie - AI Interface LizzieFrame.commands.keyAltC=ctrl-c|copy SGF to clipboard LizzieFrame.commands.keyAltV=ctrl-v|paste SGF from clipboard LizzieFrame.commands.keyC=c|toggle coordinates @@ -24,7 +24,7 @@ LizzieFrame.commands.keyD=d|show/hide dynamic komi LizzieFrame.commands.keyDownArrow=down arrow|redo LizzieFrame.commands.keyE=e|toggle evaluation coloring LizzieFrame.commands.keyEnd=end|go to end -LizzieFrame.commands.keyEnter=enter|force Leela Zero move +LizzieFrame.commands.keyEnter=enter|force AI move LizzieFrame.commands.keyF=f|toggle next move display LizzieFrame.commands.keyG=g|toggle variation graph LizzieFrame.commands.keyT=t|toggle comment display @@ -33,7 +33,7 @@ LizzieFrame.commands.keyHome=home|go to start LizzieFrame.commands.keyI=i|edit game info LizzieFrame.commands.keyA=a|run automatic analysis of game LizzieFrame.commands.keyM=m|show/hide move number -LizzieFrame.commands.keyN=n|start game against Leela Zero +LizzieFrame.commands.keyN=n|start game against AI LizzieFrame.commands.keyO=o|open SGF LizzieFrame.commands.keyP=p|pass LizzieFrame.commands.keyS=s|save SGF @@ -61,7 +61,7 @@ LizzieFrame.display.lastMove=Last move LizzieFrame.display.pondering=Pondering LizzieFrame.display.on=on LizzieFrame.display.off=off -LizzieFrame.display.loading=Leela Zero is loading... +LizzieFrame.display.loading=Engine is loading... LizzieFrame.display.leelaz-missing=Did not find Leela Zero, update config.txt or download from Leela Zero homepage LizzieFrame.display.network-missing=Did not find network weights.\nUpdate config.txt (network-file) or download from Leela Zero homepage LizzieFrame.display.dynamic-komi=dyn. komi: @@ -97,9 +97,11 @@ LizzieChangeMove.txtMoveNumber.error=Invalid move number! LizzieChangeMove.txtChangeCoord.error=Invalid move coordinate! LizzieChangeMove.button.ok=OK LizzieChangeMove.button.cancel=Cancel -NewGameDialog.title=New Game +NewGameDialog.title=Play against AI NewGameDialog.PlayBlack=Play black? NewGameDialog.Black=Black NewGameDialog.White=White NewGameDialog.Komi=Komi NewGameDialog.Handicap=Handicap +NewGameDialog.NewGame=New game? +NewGameDialog.OK=OK diff --git a/src/main/resources/l10n/DisplayStrings_zh_CN.properties b/src/main/resources/l10n/DisplayStrings_zh_CN.properties index 02e60e999..465341c52 100644 --- a/src/main/resources/l10n/DisplayStrings_zh_CN.properties +++ b/src/main/resources/l10n/DisplayStrings_zh_CN.properties @@ -3,6 +3,7 @@ # to encode native translations in raw unicode codes, such as u001 u002 if the native strings # cannot be encoded in ascii. Typically east Asian translations, such as Chinese(both simplified and traditional), # Korean or Japanese, need the encoding process. +LizzieFrame.title=Lizzie-AI\u63A5\u53E3 LizzieFrame.commands.keyAltC=ctrl-c|\u62F7\u8D1DSGF\u5230\u526A\u8D34\u677F LizzieFrame.commands.keyAltV=ctrl-v|\u4ECE\u526A\u8D34\u677F\u7C98\u8D34SGF\u5C40\u9762 LizzieFrame.commands.keyC=c|\u663E\u793A/\u9690\u85CF\u5750\u6807 @@ -10,7 +11,7 @@ LizzieFrame.commands.keyD=d|show/hide dynamic komi LizzieFrame.commands.keyControl=ctrl|\u5411\u524D/\u5411\u540E10\u6B65 LizzieFrame.commands.keyDownArrow=\u4E0B\u65B9\u5411\u952E|\u5411\u540E\u4E00\u6B65 LizzieFrame.commands.keyEnd=end|\u8DF3\u5230\u68CB\u8C31\u672B\u5C3E -LizzieFrame.commands.keyEnter=enter|\u8BA9Leela Zero\u843D\u5B50 +LizzieFrame.commands.keyEnter=enter|\u8BA9AI\u843D\u5B50 LizzieFrame.commands.keyF=f|\u663E\u793A/\u9690\u85CF\u4E0B\u4E00\u624B LizzieFrame.commands.keyG=g|\u663E\u793A/\u9690\u85CF\u5206\u652F\u56FE LizzieFrame.commands.keyT=t|\u663E\u793A/\u9690\u85CF\u8BC4\u8BBA @@ -19,7 +20,7 @@ LizzieFrame.commands.keyHome=home|\u8DF3\u8F6C\u5230\u68CB\u8C31\u5F00\u5934 LizzieFrame.commands.keyI=i|\u7F16\u8F91\u68CB\u5C40\u4FE1\u606F LizzieFrame.commands.keyA=a|\u8FD0\u884C\u7B80\u5355\u7684\u81EA\u52A8\u5168\u76D8\u5206\u6790 LizzieFrame.commands.keyM=m|\u663E\u793A/\u9690\u85CF\u624B\u6570 -LizzieFrame.commands.keyN=n|\u5F00\u59CB\u4E0ELeela Zero\u7684\u5BF9\u5F08 +LizzieFrame.commands.keyN=n|\u5F00\u59CB\u4E0EAI\u7684\u5BF9\u5F08 LizzieFrame.commands.keyO=o|\u6253\u5F00SGF\u6587\u4EF6 LizzieFrame.commands.keyP=p|\u505C\u4E00\u624B LizzieFrame.commands.keyS=s|\u4FDD\u5B58SGF @@ -43,7 +44,7 @@ LizzieFrame.display.lastMove=\u6700\u540E\u4E00\u624B LizzieFrame.display.pondering=\u5206\u6790 LizzieFrame.display.on=\u5F00\u542F LizzieFrame.display.off=\u6682\u505C -LizzieFrame.display.loading=Leela Zero\u6B63\u5728\u8F7D\u5165\u4E2D... +LizzieFrame.display.loading=\u5F15\u64CE\u8F7D\u5165\u4E2D... LizzieFrame.display.leelaz-missing=Did not find Leela Zero, update config.txt or download from Leela Zero homepage LizzieFrame.display.network-missing=Did not find network weights.\nUpdate config.txt (network-file) or download from Leela Zero homepage LizzieFrame.display.dynamic-komi=dyn. komi: @@ -79,3 +80,11 @@ LizzieChangeMove.txtMoveNumber.error=\u65E0\u6548\u7684\u624B\u6570\uFF01 LizzieChangeMove.txtChangeCoord.error=\u65E0\u6548\u7684\u5750\u6807\uFF01 LizzieChangeMove.button.ok=\u786E\u5B9A LizzieChangeMove.button.cancel=\u53D6\u6D88 +NewGameDialog.title=\u548CAI\u5BF9\u6218 +NewGameDialog.PlayBlack=\u6267\u9ED1\uFF1F +NewGameDialog.Black=\u9ED1\u65B9 +NewGameDialog.White=\u767D\u65B9 +NewGameDialog.Komi=\u8D34\u76EE +NewGameDialog.Handicap=\u8BA9\u5B50 +NewGameDialog.NewGame=\u65B0\u5C40\uFF1F +NewGameDialog.OK=\u786E\u5B9A diff --git a/src/test/java/featurecat/lizzie/rules/SGFParserTest.java b/src/test/java/featurecat/lizzie/rules/SGFParserTest.java index d42bab753..91f3f59ab 100644 --- a/src/test/java/featurecat/lizzie/rules/SGFParserTest.java +++ b/src/test/java/featurecat/lizzie/rules/SGFParserTest.java @@ -8,7 +8,6 @@ import featurecat.lizzie.Config; import featurecat.lizzie.Lizzie; import featurecat.lizzie.analysis.Leelaz; -import featurecat.lizzie.gui.LizzieFrame; import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -23,7 +22,7 @@ public void run() throws IOException { lizzie = new Lizzie(); lizzie.config = new Config(); lizzie.board = new Board(); - lizzie.frame = new LizzieFrame(); + // lizzie.frame = new LizzieFrame(); // new Thread( () -> { lizzie.leelaz = new Leelaz(); // }).start();