diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c0805e12..c199ea00c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,7 +22,7 @@ All notable changes to this project will be documented in this file. - Option to enter custom zoom level by clicking on zoom percentage label - Show in Simple editor context menu item for timelined items (sprites, buttons, swfs) - Simple editor - change background color -- Simple editor - filters (without editing gradients, convolution, colormatrix) +- Simple editor - filters (without editing convolution, colormatrix) ### Fixed - [#2424] DefineEditText handling of letterSpacing, font size on incorrect values diff --git a/src/com/jpexs/decompiler/flash/easygui/FiltersTreeTable.java b/src/com/jpexs/decompiler/flash/easygui/FiltersTreeTable.java index 2f41aa2a3..a3a667b4a 100644 --- a/src/com/jpexs/decompiler/flash/easygui/FiltersTreeTable.java +++ b/src/com/jpexs/decompiler/flash/easygui/FiltersTreeTable.java @@ -16,6 +16,8 @@ */ package com.jpexs.decompiler.flash.easygui; +import com.jpexs.decompiler.flash.easygui.properties.GradientEditor; +import com.jpexs.decompiler.flash.easygui.properties.PropertyEditor; import com.jpexs.decompiler.flash.ecma.EcmaNumberToString; import com.jpexs.decompiler.flash.ecma.EcmaScript; import com.jpexs.decompiler.flash.gui.AppStrings; @@ -278,7 +280,7 @@ public class FiltersTreeTable extends JTreeTable { private static class FiltersValueCellEditor implements TableCellEditor { private Object value; - private GenericTagEditor editor; + private PropertyEditor editor; private List listeners = new ArrayList<>(); private final FiltersTreeTable filtersTable; @@ -302,11 +304,10 @@ public class FiltersTreeTable extends JTreeTable { FilterField filterField = filterValue.filterField; - if (editor != null) { - //((Component) editor).setVisible(false); - } editor = null; - if (realValue.getClass() == Boolean.class) { + if ("gradientColors".equals(filterField.field.getName())) { + editor = new GradientEditor(filterField.filter); + } else if (realValue.getClass() == Boolean.class) { editor = new BooleanEditor(filterField.toString(), filterField.filter, filterField.field, -1, Boolean.class); /*editor.addChangeListener(new ChangeListener() { @Override @@ -319,7 +320,7 @@ public class FiltersTreeTable extends JTreeTable { editor = new FloatEditor(filterField.toString(), filterField.filter, filterField.field, -1, realValue.getClass()); //editor = new NumberEditor(filterField.toString(), filterField.filter, filterField.field, -1, realValue.getClass(), filterField.field.getAnnotation(SWFType.class)); if ("angle".equals(filterField.field.getName())) { - editor.setValueNormalizer(new ValueNormalizer() { + ((FloatEditor) editor).setValueNormalizer(new ValueNormalizer() { @Override public Object toFieldValue(Object viewValue) { return Math.toRadians((double) viewValue); @@ -341,7 +342,7 @@ public class FiltersTreeTable extends JTreeTable { editor.addChangeListener(new ChangeListener() { @Override - public void change(GenericTagEditor editor) { + public void change(PropertyEditor editor) { editor.save(); filtersTable.fireFilterChanged(); } @@ -452,9 +453,12 @@ public class FiltersTreeTable extends JTreeTable { case "distance": units = " px"; } - label.setText(value.toString() + units); + label.setText(value.toString() + units); Object fieldValue = filterValue.getValue(); - if (fieldValue != null) { + if (fieldValue != null) { + if ("gradientColors".equals(filterValue.filterField.field.getName())) { + component = new GradientEditor(filterValue.filterField.filter); + } if (fieldValue.getClass() == Boolean.class) { JPanel panel = new JPanel(new BorderLayout()); JCheckBox checkBox = new JCheckBox(); @@ -552,6 +556,9 @@ public class FiltersTreeTable extends JTreeTable { @Override public String toString() { + if ("gradientColors".equals(field.getName())) { + return "gradient"; + } return field.getName(); } @@ -674,6 +681,9 @@ public class FiltersTreeTable extends JTreeTable { if ("id".equals(field.getName())) { continue; } + if ("gradientRatio".equals(field.getName())) { + continue; + } Reserved reserved = field.getAnnotation(Reserved.class); if (reserved != null) { continue; diff --git a/src/com/jpexs/decompiler/flash/easygui/properties/GradientEditor.java b/src/com/jpexs/decompiler/flash/easygui/properties/GradientEditor.java new file mode 100644 index 000000000..f1bc18e10 --- /dev/null +++ b/src/com/jpexs/decompiler/flash/easygui/properties/GradientEditor.java @@ -0,0 +1,488 @@ +/* + * Copyright (C) 2025 JPEXS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.jpexs.decompiler.flash.easygui.properties; + +import com.jpexs.decompiler.flash.gui.ViewMessages; +import com.jpexs.decompiler.flash.gui.generictageditors.ChangeListener; +import com.jpexs.decompiler.flash.types.RGBA; +import com.jpexs.decompiler.flash.types.filters.FILTER; +import com.jpexs.decompiler.flash.types.filters.GRADIENTBEVELFILTER; +import com.jpexs.decompiler.flash.types.filters.GRADIENTGLOWFILTER; +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.LinearGradientPaint; +import java.awt.Point; +import java.awt.Polygon; +import java.awt.Rectangle; +import java.awt.Window; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.util.ArrayList; +import java.util.List; +import javax.swing.JDialog; +import javax.swing.JPanel; +import javax.swing.JRootPane; +import javax.swing.SwingUtilities; + +/** + * + * @author JPEXS + */ +public class GradientEditor extends JPanel implements PropertyEditor { + + private List colors = new ArrayList<>(); + private List ratios = new ArrayList<>(); + private boolean colorDialogDisplayed = false; + + private List listeners = new ArrayList<>(); + private final FILTER filter; + + private static void drawCheckerboard(Graphics g, int tileSize, Rectangle rect) { + drawCheckerboard(g, tileSize, rect.x, rect.y, rect.width, rect.height); + } + + private static void drawCheckerboard(Graphics g, int tileSize, int startX, int startY, int width, int height) { + Color lightGray = new Color(238, 238, 238); // light gray + Color darkGray = new Color(189, 189, 189); // darker gray + + for (int y = 0; y < height; y += tileSize) { + for (int x = 0; x < width; x += tileSize) { + boolean isLight = ((x / tileSize) + (y / tileSize)) % 2 == 0; + g.setColor(isLight ? lightGray : darkGray); + int tileW = tileSize; + int tileH = tileSize; + if (x + tileSize > width) { + tileW = width - x; + } + if (y + tileSize > height) { + tileH = height - y; + } + g.fillRect(startX + x, startY + y, tileW, tileH); + } + } + } + + public GradientEditor(FILTER filter) { + this.filter = filter; + setLayout(new BorderLayout()); + JPanel miniGradientPanel = new JPanel() { + + @Override + protected void paintComponent(Graphics g) { + Graphics2D g2d = (Graphics2D) g; + drawCheckerboard(g, 4, 0, 0, getWidth(), getHeight()); + + float[] ratiosArr = new float[ratios.size()]; + for (int i = 0; i < ratios.size(); i++) { + ratiosArr[i] = ratios.get(i); + } + Color[] colorsArr = colors.toArray(new Color[colors.size()]); + g2d.setPaint(new LinearGradientPaint(0, 0, getWidth(), 0, ratiosArr, colorsArr)); + g2d.fill(new Rectangle(getWidth(), getHeight())); + g2d.setPaint(Color.black); + g2d.draw(new Rectangle(getWidth(), getHeight())); + } + }; + miniGradientPanel.setPreferredSize(new Dimension(100, 16)); + + miniGradientPanel.addMouseListener(new MouseAdapter() { + @Override + public void mousePressed(MouseEvent e) { + if (SwingUtilities.isLeftMouseButton(e)) { + JDialog dialog = new JDialog(); + dialog.setUndecorated(true); + dialog.setResizable(false); + dialog.getRootPane().setWindowDecorationStyle(JRootPane.NONE); + dialog.setContentPane(new GradientEditorPanel()); + dialog.pack(); + + Window window = SwingUtilities.getWindowAncestor(GradientEditor.this); + + dialog.setLocationRelativeTo(window); + Point loc = SwingUtilities.convertPoint(miniGradientPanel, 0, 0, window); + if (loc.x + dialog.getWidth() > window.getWidth()) { + loc.x -= loc.x + dialog.getWidth() - window.getWidth(); + } + if (loc.y + dialog.getHeight() > window.getHeight()) { + loc.y -= loc.y + dialog.getHeight() - window.getHeight(); + } + SwingUtilities.convertPointToScreen(loc, window); + + dialog.setLocation(loc); + dialog.addWindowListener(new WindowAdapter() { + @Override + public void windowDeactivated(WindowEvent e) { + if (!colorDialogDisplayed) { + dialog.setVisible(false); + List listeners2 = new ArrayList<>(listeners); + for (ChangeListener l : listeners2) { + l.change(GradientEditor.this); + } + } + } + }); + dialog.setVisible(true); + } + } + }); + + add(miniGradientPanel, BorderLayout.WEST); + reset(); + } + + private void loadParams(RGBA[] rgbaColors, int[] intRatios) { + colors = new ArrayList<>(); + ratios = new ArrayList<>(); + int lastRatio = -1; + for (int i = 0; i < rgbaColors.length; i++) { + if ((i > 0) && (intRatios[i - 1] == intRatios[i])) { + if (lastRatio < 255) { + lastRatio++; + } + } else { + if (intRatios[i] > lastRatio) { + lastRatio = intRatios[i]; + } else if (lastRatio < 255) { + lastRatio++; + } + } + ratios.add(lastRatio / 255f); + colors.add(rgbaColors[i].toColor()); + if (lastRatio == 255) { + break; + } + } + + if (colors.size() == 1) { + colors.add(colors.get(0)); + ratios.set(0, 0f); + ratios.add(1f); + } + + } + + @Override + public boolean save() { + int[] gradientRatio = new int[ratios.size()]; + RGBA[] gradientColors = new RGBA[ratios.size()]; + + for (int i = 0; i < ratios.size(); i++) { + gradientRatio[i] = Math.round(ratios.get(i) * 255); + gradientColors[i] = new RGBA(colors.get(i)); + } + + if (filter instanceof GRADIENTBEVELFILTER) { + GRADIENTBEVELFILTER bevel = (GRADIENTBEVELFILTER) filter; + bevel.gradientColors = gradientColors; + bevel.gradientRatio = gradientRatio; + } else if (filter instanceof GRADIENTGLOWFILTER) { + GRADIENTGLOWFILTER glow = (GRADIENTGLOWFILTER) filter; + glow.gradientColors = gradientColors; + glow.gradientRatio = gradientRatio; + } + return true; + } + + @Override + public void reset() { + if (filter instanceof GRADIENTBEVELFILTER) { + GRADIENTBEVELFILTER bevel = (GRADIENTBEVELFILTER) filter; + loadParams(bevel.gradientColors, bevel.gradientRatio); + } else if (filter instanceof GRADIENTGLOWFILTER) { + GRADIENTGLOWFILTER glow = (GRADIENTGLOWFILTER) filter; + loadParams(glow.gradientColors, glow.gradientRatio); + } else { + throw new RuntimeException("Invalid FILTER"); + } + } + + @Override + public void addChangeListener(ChangeListener l) { + listeners.add(l); + } + + private class GradientEditorPanel extends JPanel { + + int dragIndex = -1; + boolean draggedOut = false; + boolean newlySelected = true; + + private Color interpolate(Color c1, Color c2, float t) { + int r = (int) (c1.getRed() + t * (c2.getRed() - c1.getRed())); + int g = (int) (c1.getGreen() + t * (c2.getGreen() - c1.getGreen())); + int b = (int) (c1.getBlue() + t * (c2.getBlue() - c1.getBlue())); + int a = (int) (c1.getAlpha() + t * (c2.getAlpha() - c1.getAlpha())); + return new Color(r, g, b, a); + } + + public GradientEditorPanel() { + Dimension dim = new Dimension(300, 50); + setMinimumSize(dim); + setPreferredSize(dim); + addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + if (SwingUtilities.isLeftMouseButton(e)) { + if (newlySelected) { + return; + } + List rects = getColorRects(); + for (int r = 0; r < rects.size(); r++) { + Rectangle rect = rects.get(r); + if (rect.contains(e.getPoint())) { + if (selectedColorIndex == r) { + colorDialogDisplayed = true; + Color newColor = ViewMessages.showColorDialog(GradientEditorPanel.this, colors.get(selectedColorIndex), true); + colorDialogDisplayed = false; + if (newColor != null) { + if (isFixedZeroAlphaIndex(selectedColorIndex)) { + newColor = new Color(newColor.getRed(), newColor.getGreen(), newColor.getBlue(), 0); + } + colors.set(selectedColorIndex, newColor); + repaint(); + } + } + return; + } + } + } + + } + + @Override + public void mousePressed(MouseEvent e) { + if (SwingUtilities.isLeftMouseButton(e)) { + List rects = getColorRects(); + for (int r = 0; r < rects.size(); r++) { + Rectangle rect = rects.get(r); + if (rect.contains(e.getPoint())) { + newlySelected = selectedColorIndex != r; + selectedColorIndex = r; + dragIndex = r; + repaint(); + return; + } + } + + float newRatio = (e.getX() - COLOR_SIZE / 2) / (float) (getWidth() - COLOR_SIZE); + if (newRatio >= 0 && newRatio <= 1) { + synchronized (GradientEditorPanel.this) { + int newIndex = -1; + Color color1 = null; + Color color2 = null; + Float ratio1 = null; + Float ratio2 = null; + for (int i = 0; i < ratios.size(); i++) { + float ratio = ratios.get(i); + if (ratio >= newRatio) { + if (newRatio == ratio) { + if (Float.compare(newRatio, 0f) > 0) { + newRatio -= 0.0000001; + } else { + newRatio += 0.0000001; + i++; + } + } + ratio2 = ratio; + newIndex = i; + color2 = colors.get(i); + break; + } + color1 = colors.get(i); + ratio1 = ratio; + } + if (newIndex == -1) { + newIndex = ratios.size(); + color2 = color1; + ratio2 = 1f; + } + ratios.add(newIndex, newRatio); + float ratioDelta = (newRatio - ratio1) / (ratio2 - ratio1); + Color newColor = interpolate(color1, color2, ratioDelta); + + colors.add(newIndex, newColor); + + dragIndex = newIndex; + selectedColorIndex = newIndex; + + newlySelected = true; + repaint(); + } + } + + } + } + + @Override + public void mouseReleased(MouseEvent e) { + if (draggedOut && dragIndex > -1) { + synchronized (GradientEditorPanel.this) { + colors.remove(dragIndex); + ratios.remove(dragIndex); + } + repaint(); + } + + dragIndex = -1; + draggedOut = false; + } + }); + + addMouseMotionListener(new MouseAdapter() { + @Override + public void mouseDragged(MouseEvent e) { + float newRatio = (e.getX() - COLOR_SIZE / 2) / (float) (getWidth() - COLOR_SIZE); + newRatio = Math.round(newRatio * 255) / 255f; + if (newRatio < 0 || newRatio > 1) { + return; + } + if (dragIndex > -1) { + + if (isFixedZeroAlphaIndex(dragIndex)) { + return; + } + + + draggedOut = colors.size() > 2 && e.getY() > getHeight(); + + synchronized (GradientEditorPanel.this) { + int newIndex = -1; + for (int i = 0; i < ratios.size(); i++) { + if (i == dragIndex) { + continue; + } + float ratio = ratios.get(i); + if (ratio >= newRatio) { + if (newRatio == ratio) { + if (Float.compare(newRatio, 0f) > 0) { + newRatio -= 0.0000001; + } else { + newRatio += 0.0000001; + i++; + } + } + newIndex = i; + break; + } + } + ratios.set(dragIndex, newRatio); + if (newIndex == -1) { + newIndex = ratios.size(); + } + ratios.add(newIndex, newRatio); + colors.add(newIndex, colors.get(dragIndex)); + + if (newIndex <= dragIndex) { + dragIndex++; + } else { + newIndex--; + } + + ratios.remove(dragIndex); + colors.remove(dragIndex); + + dragIndex = newIndex; + } + repaint(); + } + } + }); + } + + final int BOTTOM_HEIGHT = 15; + final int COLOR_SIZE = 10; + final int ARROW_HEIGHT = 4; + + int selectedColorIndex = 0; + + private boolean isFixedZeroAlphaIndex(int index) { + if (filter instanceof GRADIENTGLOWFILTER && index == 0 && colors.get(0).getAlpha() == 0) { + return true; + } + if (filter instanceof GRADIENTBEVELFILTER && ratios.get(index) == 128f/255f && colors.get(index).getAlpha() == 0) { + return true; + } + return false; + } + + + private List getColorRects() { + List rects = new ArrayList<>(); + + for (int i = 0; i < ratios.size(); i++) { + rects.add(new Rectangle(Math.round((getWidth() - COLOR_SIZE) * ratios.get(i)), getHeight() - BOTTOM_HEIGHT + ARROW_HEIGHT, COLOR_SIZE, COLOR_SIZE)); + } + return rects; + } + + @Override + protected void paintComponent(Graphics g) { + super.paintComponent(g); + Graphics2D g2d = (Graphics2D) g; + + drawCheckerboard(g, 5, COLOR_SIZE / 2, 0, getWidth() - COLOR_SIZE, getHeight() - BOTTOM_HEIGHT); + + float[] ratiosArr; + Color[] colorsArr; + + synchronized (GradientEditorPanel.this) { + ratiosArr = new float[ratios.size()]; + for (int i = 0; i < ratios.size(); i++) { + ratiosArr[i] = ratios.get(i); + } + + colorsArr = colors.toArray(new Color[colors.size()]); + } + g2d.setPaint(new LinearGradientPaint(COLOR_SIZE / 2, 0, getWidth() - COLOR_SIZE, 0, ratiosArr, colorsArr)); + g2d.fill(new Rectangle(COLOR_SIZE / 2, 0, getWidth() - COLOR_SIZE, getHeight() - BOTTOM_HEIGHT)); + g2d.setPaint(Color.black); + g2d.draw(new Rectangle(COLOR_SIZE / 2, 0, getWidth() - COLOR_SIZE, getHeight() - BOTTOM_HEIGHT)); + + List rects = getColorRects(); + + for (int i = 0; i < ratios.size(); i++) { + if (draggedOut && i == dragIndex) { + continue; + } + Rectangle rect = rects.get(i); + drawCheckerboard(g, 3, rect); + g2d.setPaint(colorsArr[i]); + g2d.fill(rect); + g2d.setColor(Color.black); + g2d.draw(rect); + g2d.setColor(selectedColorIndex == i ? Color.black : Color.white); + Polygon arrow = new Polygon(new int[]{ + Math.round((getWidth() - COLOR_SIZE) * ratios.get(i)), + Math.round((getWidth() - COLOR_SIZE) * ratios.get(i)) + COLOR_SIZE / 2, + Math.round((getWidth() - COLOR_SIZE) * ratios.get(i)) + COLOR_SIZE + }, new int[]{ + getHeight() - BOTTOM_HEIGHT + ARROW_HEIGHT, + getHeight() - BOTTOM_HEIGHT, + getHeight() - BOTTOM_HEIGHT + ARROW_HEIGHT + }, 3); + g2d.fill(arrow); + g2d.setColor(Color.black); + g2d.draw(arrow); + } + } + } +} diff --git a/src/com/jpexs/decompiler/flash/easygui/properties/PropertyEditor.java b/src/com/jpexs/decompiler/flash/easygui/properties/PropertyEditor.java new file mode 100644 index 000000000..817884a2a --- /dev/null +++ b/src/com/jpexs/decompiler/flash/easygui/properties/PropertyEditor.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2025 JPEXS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.jpexs.decompiler.flash.easygui.properties; + +import com.jpexs.decompiler.flash.gui.generictageditors.ChangeListener; + +/** + * + * @author JPEXS + */ +public interface PropertyEditor { + public boolean save(); + + public void reset(); + + public void addChangeListener(ChangeListener l); +} diff --git a/src/com/jpexs/decompiler/flash/gui/generictageditors/ChangeListener.java b/src/com/jpexs/decompiler/flash/gui/generictageditors/ChangeListener.java index 3ca4b63bb..5ec0e471a 100644 --- a/src/com/jpexs/decompiler/flash/gui/generictageditors/ChangeListener.java +++ b/src/com/jpexs/decompiler/flash/gui/generictageditors/ChangeListener.java @@ -16,11 +16,13 @@ */ package com.jpexs.decompiler.flash.gui.generictageditors; +import com.jpexs.decompiler.flash.easygui.properties.PropertyEditor; + /** * @author JPEXS */ @FunctionalInterface public interface ChangeListener { - public void change(GenericTagEditor editor); + public void change(PropertyEditor editor); } diff --git a/src/com/jpexs/decompiler/flash/gui/generictageditors/FloatEditor.java b/src/com/jpexs/decompiler/flash/gui/generictageditors/FloatEditor.java index 71edb3365..b38836f09 100644 --- a/src/com/jpexs/decompiler/flash/gui/generictageditors/FloatEditor.java +++ b/src/com/jpexs/decompiler/flash/gui/generictageditors/FloatEditor.java @@ -16,6 +16,7 @@ */ package com.jpexs.decompiler.flash.gui.generictageditors; +import com.jpexs.decompiler.flash.easygui.properties.PropertyEditor; import com.jpexs.decompiler.flash.ecma.EcmaScript; import com.jpexs.helpers.Helper; import com.jpexs.helpers.ReflectionTools; @@ -24,8 +25,6 @@ import java.awt.Dimension; import java.awt.event.FocusAdapter; import java.awt.event.FocusEvent; import java.lang.reflect.Field; -import java.util.ArrayList; -import java.util.List; import java.util.Objects; import javax.swing.JTextField; @@ -120,7 +119,7 @@ public class FloatEditor extends JTextField implements GenericTagEditor { @Override public void addChangeListener(final ChangeListener l) { - final GenericTagEditor t = this; + final PropertyEditor t = this; addFocusListener(new FocusAdapter() { @Override diff --git a/src/com/jpexs/decompiler/flash/gui/generictageditors/GenericTagEditor.java b/src/com/jpexs/decompiler/flash/gui/generictageditors/GenericTagEditor.java index c6926c8ed..3a07b98b8 100644 --- a/src/com/jpexs/decompiler/flash/gui/generictageditors/GenericTagEditor.java +++ b/src/com/jpexs/decompiler/flash/gui/generictageditors/GenericTagEditor.java @@ -16,20 +16,15 @@ */ package com.jpexs.decompiler.flash.gui.generictageditors; +import com.jpexs.decompiler.flash.easygui.properties.PropertyEditor; import java.lang.reflect.Field; /** * @author JPEXS */ -public interface GenericTagEditor { +public interface GenericTagEditor extends PropertyEditor { - public void added(); - - public void reset(); - - public boolean save(); - - public void addChangeListener(ChangeListener l); + public void added(); public Object getChangedValue();