diff --git a/CHANGELOG.md b/CHANGELOG.md index 3cab0103f..db7863ec6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ All notable changes to this project will be documented in this file. - Objects dragging - show touch point and snap it to 9 important points around object rectangle - [#2370] Snap to guides, objects and pixels, Snap align, toggle with magnet icon - [#2370] Show/Hide guides, lock guides, clear guides actions from icon menu +- Display grid, snap to grid ### Fixed - [#2424] DefineEditText handling of letterSpacing, font size on incorrect values diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/configuration/Configuration.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/configuration/Configuration.java index 8be0600b9..501d45e7f 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/configuration/Configuration.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/configuration/Configuration.java @@ -1081,6 +1081,26 @@ public final class Configuration { @ConfigurationCategory("display") public static ConfigurationItem lockGuides = null; + @ConfigurationDefaultBoolean(false) + @ConfigurationCategory("display") + public static ConfigurationItem showGrid = null; + + @ConfigurationDefaultInt(10) + @ConfigurationCategory("display") + public static ConfigurationItem gridVerticalSpace = null; + + @ConfigurationDefaultInt(10) + @ConfigurationCategory("display") + public static ConfigurationItem gridHorizontalSpace = null; + + @ConfigurationDefaultBoolean(false) + @ConfigurationCategory("display") + public static ConfigurationItem snapToGrid = null; + + @ConfigurationDefaultBoolean(false) + @ConfigurationCategory("display") + public static ConfigurationItem gridOverObjects = null; + private enum OSId { WINDOWS, OSX, UNIX } diff --git a/src/com/jpexs/decompiler/flash/gui/ImagePanel.java b/src/com/jpexs/decompiler/flash/gui/ImagePanel.java index 5222943ce..e93070ec9 100644 --- a/src/com/jpexs/decompiler/flash/gui/ImagePanel.java +++ b/src/com/jpexs/decompiler/flash/gui/ImagePanel.java @@ -910,6 +910,54 @@ public final class ImagePanel extends JPanel implements MediaDisplay { public void setResample(boolean resample) { this.resample = resample; } + + private static void drawGridSwf(Graphics2D g, Rectangle realRect, double zoom) { + g.setColor(new Color(0x94, 0x94, 0x94)); + double x; + double y; + int ix; + int iy; + int minIx = 0; + int minIy = 0; + int maxIx; + int maxIy; + + ix = 0; + while ((double) realRect.x + ix * Configuration.gridHorizontalSpace.get() * zoom < realRect.getMaxX()) { + ix++; + } + maxIx = ix; + + iy = 0; + while ((double) realRect.y + iy * Configuration.gridVerticalSpace.get() * zoom < realRect.getMaxY()) { + iy++; + } + maxIy = iy; + + for (ix = minIx; ix <= maxIx; ix++) { + x = realRect.x + ix * Configuration.gridHorizontalSpace.get() * zoom; + Point2D p1 = new Point2D.Double(x, realRect.y + minIy * Configuration.gridVerticalSpace.get() * zoom); + Point2D p2 = new Point2D.Double(x, realRect.y + maxIy * Configuration.gridVerticalSpace.get() * zoom); + g.drawLine( + (int) Math.round(p1.getX()), + (int) Math.round(p1.getY()), + (int) Math.round(p2.getX()), + (int) Math.round(p2.getY()) + ); + } + + for (iy = minIy; iy <= maxIy; iy++) { + y = realRect.y + iy * Configuration.gridVerticalSpace.get() * zoom; + Point2D p1 = new Point2D.Double(realRect.x + minIx * Configuration.gridHorizontalSpace.get() * zoom, y); + Point2D p2 = new Point2D.Double(realRect.x + maxIx * Configuration.gridHorizontalSpace.get() * zoom, y); + g.drawLine( + (int) Math.round(p1.getX()), + (int) Math.round(p1.getY()), + (int) Math.round(p2.getX()), + (int) Math.round(p2.getY()) + ); + } + } private class IconPanel extends JPanel { @@ -959,7 +1007,69 @@ public final class ImagePanel extends JPanel implements MediaDisplay { return allowMove; } - VolatileImage renderImage; + VolatileImage renderImage; + + private void drawGridNoSwf(Graphics2D g2, int x, int y) { + double zoomDouble = getRealZoom(); + g2.setColor(new Color(0x94, 0x94, 0x94)); + double gx; + double gy; + int ix = 0; + int iy = 0; + int minIx; + int minIy; + int maxIx; + int maxIy; + double sx = x + offsetPoint.getX(); + double sy = y + offsetPoint.getY(); + + while (sx + ix * Configuration.gridHorizontalSpace.get() * zoomDouble > 0) { + ix--; + } + minIx = ix; + ix = 0; + while (sx + ix * Configuration.gridHorizontalSpace.get() * zoomDouble < getWidth()) { + ix++; + } + maxIx = ix; + + while (sy + iy * Configuration.gridVerticalSpace.get() * zoomDouble > 0) { + iy--; + } + minIy = iy; + + iy = 0; + while (sy + iy * Configuration.gridVerticalSpace.get() * zoomDouble < getHeight()) { + iy++; + } + maxIy = iy; + + for (ix = minIx; ix <= maxIx; ix++) { + gx = sx + ix * Configuration.gridHorizontalSpace.get() * zoomDouble; + + Point2D p1 = new Point2D.Double(gx, sy + minIy * Configuration.gridVerticalSpace.get() * zoomDouble); + Point2D p2 = new Point2D.Double(gx, sy + maxIy * Configuration.gridVerticalSpace.get() * zoomDouble); + g2.drawLine( + (int) Math.round(p1.getX()), + (int) Math.round(p1.getY()), + (int) Math.round(p2.getX()), + (int) Math.round(p2.getY()) + ); + } + + for (iy = minIy; iy <= maxIy; iy++) { + gy = sy + iy * Configuration.gridVerticalSpace.get() * zoomDouble; + + Point2D p1 = new Point2D.Double(sx + minIx * Configuration.gridHorizontalSpace.get() * zoomDouble, gy); + Point2D p2 = new Point2D.Double(sx + maxIx * Configuration.gridHorizontalSpace.get() * zoomDouble, gy); + g2.drawLine( + (int) Math.round(p1.getX()), + (int) Math.round(p1.getY()), + (int) Math.round(p2.getX()), + (int) Math.round(p2.getY()) + ); + } + } public void render() { SerializableImage img = getImg(); @@ -1001,8 +1111,18 @@ public final class ImagePanel extends JPanel implements MediaDisplay { y = (int) offsetPoint.getY(); } + double zoomDouble = zoom.fit ? getZoomToFit() : zoom.value; + + if (Configuration.showGrid.get() && !(timelined instanceof SWF) && !Configuration.gridOverObjects.get()) { + drawGridNoSwf(g2, x, y); + } + g2.drawImage(img.getBufferedImage(), x, y, x + img.getWidth(), y + img.getHeight(), 0, 0, img.getWidth(), img.getHeight(), null); + if (Configuration.showGrid.get() && !(timelined instanceof SWF) && Configuration.gridOverObjects.get()) { + drawGridNoSwf(g2, x, y); + } + if (hilightedEdge != null || hilightedPoints != null) { hilightEdgeColor += hilightEdgeColorStep; if (hilightEdgeColor < 100 || hilightEdgeColor > 255) { @@ -1010,7 +1130,6 @@ public final class ImagePanel extends JPanel implements MediaDisplay { hilightEdgeColor += hilightEdgeColorStep * 2; } RECT timRect = timelined.getRect(); - double zoomDouble = zoom.fit ? getZoomToFit() : zoom.value; AffineTransform trans = new AffineTransform(); trans.translate(offsetPoint.getX(), offsetPoint.getY()); trans.scale(1 / SWF.unitDivisor, 1 / SWF.unitDivisor); @@ -1104,7 +1223,6 @@ public final class ImagePanel extends JPanel implements MediaDisplay { if (!(timelined instanceof SWF) && (doFreeTransform || hilightedPoints != null)) { int axisX = 0; int axisY = 0; - double zoomDouble = zoom.fit ? getZoomToFit() : zoom.value; RECT timRect = timelined.getRect(); axisX = (int) Math.round(offsetPoint.getX()); axisY = (int) Math.round(offsetPoint.getY()); @@ -2174,6 +2292,27 @@ public final class ImagePanel extends JPanel implements MediaDisplay { } } + if (Configuration.showGrid.get() && Configuration.snapToGrid.get()) { + if (snapOffsetX == null) { + int positionPxX = (int) Math.round((touchPointPos.getX() - offsetPoint.getX()) / zoomDouble); + int d = (positionPxX / Configuration.gridHorizontalSpace.get()) * Configuration.gridHorizontalSpace.get(); + if ((positionPxX - d) * zoomDouble < SNAP_DISTANCE) { + snapOffsetX = d * zoomDouble - touchPointPos.getX() + offsetPoint.getX(); + } else if ((d + Configuration.gridHorizontalSpace.get() - positionPxX) * zoomDouble < SNAP_DISTANCE) { + snapOffsetX = (d + Configuration.gridHorizontalSpace.get()) * zoomDouble - touchPointPos.getX() + offsetPoint.getX(); + } + } + if (snapOffsetY == null) { + int positionPxY = (int) Math.round((touchPointPos.getY() - offsetPoint.getY()) / zoomDouble); + int d = (positionPxY / Configuration.gridVerticalSpace.get()) * Configuration.gridVerticalSpace.get(); + if ((positionPxY - d) * zoomDouble < SNAP_DISTANCE) { + snapOffsetY = d * zoomDouble - touchPointPos.getY() + offsetPoint.getY(); + } else if ((d + Configuration.gridVerticalSpace.get() - positionPxY) * zoomDouble < SNAP_DISTANCE) { + snapOffsetY = (d + Configuration.gridVerticalSpace.get()) * zoomDouble - touchPointPos.getY() + offsetPoint.getY(); + } + } + } + if (Configuration.snapToPixels.get()) { if (snapOffsetX == null) { int positionPxX = (int) Math.round((touchPointPos.getX() - offsetPoint.getX()) / zoomDouble); @@ -4633,6 +4772,11 @@ public final class ImagePanel extends JPanel implements MediaDisplay { g.fillRect(realRect.x, realRect.y, realRect.width, realRect.height); } + if (Configuration.showGrid.get() && (drawable instanceof SWF) && !Configuration.gridOverObjects.get()) { + Graphics2D g = (Graphics2D) image.getBufferedImage().getGraphics(); + drawGridSwf(g, realRect, zoom); + } + parentMatrix = new Matrix(); List ignoreDepths = new ArrayList<>(); for (int i = 0; i < parentTimelineds.size(); i++) { @@ -4774,6 +4918,13 @@ public final class ImagePanel extends JPanel implements MediaDisplay { } } } + + + if (Configuration.showGrid.get() && (drawable instanceof SWF) && Configuration.gridOverObjects.get()) { + Graphics2D g = (Graphics2D) image.getBufferedImage().getGraphics(); + drawGridSwf(g, realRect, zoom); + } + img = image; /*if (shouldCache) { diff --git a/src/com/jpexs/decompiler/flash/gui/abc/SnapOptionsButton.java b/src/com/jpexs/decompiler/flash/gui/abc/SnapOptionsButton.java index 640304546..d3d9934f3 100644 --- a/src/com/jpexs/decompiler/flash/gui/abc/SnapOptionsButton.java +++ b/src/com/jpexs/decompiler/flash/gui/abc/SnapOptionsButton.java @@ -50,6 +50,11 @@ public class SnapOptionsButton extends PopupButton { snapAlignMenuItem.addActionListener(this::snapAlignMenuItemActionPerformed); popupMenu.add(snapAlignMenuItem); + JCheckBox snapToGridMenuItem = new JCheckBox(AppStrings.translate("snap_options.snap_to_grid")); + snapToGridMenuItem.setSelected(Configuration.snapToGrid.get()); + snapToGridMenuItem.addActionListener(this::snapToGridMenuItemActionPerformed); + popupMenu.add(snapToGridMenuItem); + JCheckBox snapToGuidesMenuItem = new JCheckBox(AppStrings.translate("snap_options.snap_to_guides")); snapToGuidesMenuItem.setSelected(Configuration.snapToGuides.get()); snapToGuidesMenuItem.addActionListener(this::snapToGuidesMenuItemActionPerformed); @@ -73,6 +78,11 @@ public class SnapOptionsButton extends PopupButton { Configuration.snapAlign.set(menuItem.isSelected()); } + private void snapToGridMenuItemActionPerformed(ActionEvent evt) { + JCheckBox menuItem = (JCheckBox) evt.getSource(); + Configuration.snapToGrid.set(menuItem.isSelected()); + } + private void snapToGuidesMenuItemActionPerformed(ActionEvent evt) { JCheckBox menuItem = (JCheckBox) evt.getSource(); Configuration.snapToGuides.set(menuItem.isSelected()); diff --git a/src/com/jpexs/decompiler/flash/gui/graphics/grid16.png b/src/com/jpexs/decompiler/flash/gui/graphics/grid16.png new file mode 100644 index 000000000..5ab106b3c Binary files /dev/null and b/src/com/jpexs/decompiler/flash/gui/graphics/grid16.png differ diff --git a/src/com/jpexs/decompiler/flash/gui/locales/AdvancedSettingsDialog.properties b/src/com/jpexs/decompiler/flash/gui/locales/AdvancedSettingsDialog.properties index 3f0880a46..81b78c621 100644 --- a/src/com/jpexs/decompiler/flash/gui/locales/AdvancedSettingsDialog.properties +++ b/src/com/jpexs/decompiler/flash/gui/locales/AdvancedSettingsDialog.properties @@ -577,3 +577,19 @@ config.description.showGuides = Set this to false to hide guides. config.name.lockGuides = Lock guides config.description.lockGuides = Disables moving of guides so they stay in position and cannot be moved. + +config.name.showGrid = Show grid +config.description.showGrid = Shows grid on stage + +config.name.gridVerticalSpace = Grid vertical spacing (px) +config.description.gridVerticalSpace = Vertical space between grid lines in pixels. + +config.name.gridHorizontalSpace = Grid horizontal spacing (px) +config.description.gridHorizontalSpace = Horizontal space between grid lines in pixels. + +config.name.snapToGrid = Snap to grid +config.description.snapToGrid = Enables snapping cursor to grid while moving objects. + +config.name.gridOverObjects = Show grid over objects +config.description.gridOverObjects = When the grid is displayed, it is shown over objects on stage. + diff --git a/src/com/jpexs/decompiler/flash/gui/locales/MainFrame.properties b/src/com/jpexs/decompiler/flash/gui/locales/MainFrame.properties index 283e83843..12b805b00 100644 --- a/src/com/jpexs/decompiler/flash/gui/locales/MainFrame.properties +++ b/src/com/jpexs/decompiler/flash/gui/locales/MainFrame.properties @@ -1025,6 +1025,7 @@ filter.swf_spl = SWF files (*.swf, *.spl) #after 22.0.2 button.snap_options = Snap options snap_options.snap_align = Snap Align +snap_options.snap_to_grid = Snap to Grid snap_options.snap_to_guides = Snap to Guides snap_options.snap_to_pixels = Snap to Pixels snap_options.snap_to_objects = Snap to Objects @@ -1034,4 +1035,6 @@ button.ruler.hint = Toggle ruler display button.guides_options.hint = Guides options guides_options.show = Show guides guides_options.lock = Lock guides -guides_options.clear = Clear guides \ No newline at end of file +guides_options.clear = Clear guides + +button.grid.hint = Toggle grid display \ No newline at end of file diff --git a/src/com/jpexs/decompiler/flash/gui/player/ZoomPanel.java b/src/com/jpexs/decompiler/flash/gui/player/ZoomPanel.java index b15990bb3..34651897e 100644 --- a/src/com/jpexs/decompiler/flash/gui/player/ZoomPanel.java +++ b/src/com/jpexs/decompiler/flash/gui/player/ZoomPanel.java @@ -105,6 +105,18 @@ public class ZoomPanel extends JPanel implements MediaDisplayListener { guidesOptionsButton.setToolTipText(AppStrings.translate("button.guides_options.hint")); snapOptionsButton = new SnapOptionsButton(); + + JToggleButton gridButton = new JToggleButton(View.getIcon("grid16")); + gridButton.addActionListener(this::gridButtonActionPerformed); + gridButton.setToolTipText(AppStrings.translate("button.grid.hint")); + gridButton.setSelected(Configuration.showGrid.get()); + Configuration.showGrid.addListener(new ConfigurationItemChangeListener() { + @Override + public void configurationItemChanged(Boolean newValue) { + gridButton.setSelected(newValue); + } + }); + setLayout(new FlowLayout()); add(percentLabel); @@ -113,11 +125,17 @@ public class ZoomPanel extends JPanel implements MediaDisplayListener { add(zoomNoneButton); add(zoomFitButton); add(rulerButton); + add(gridButton); add(guidesOptionsButton); add(snapOptionsButton); display.addEventListener(this); } + + private void gridButtonActionPerformed(ActionEvent evt) { + JToggleButton source = (JToggleButton) evt.getSource(); + Configuration.showGrid.set(source.isSelected()); + } private void guidesShowActionPerformed(ActionEvent evt) { JCheckBoxMenuItem source = (JCheckBoxMenuItem) evt.getSource();