From 027d7f948bccc0a54179d0aacd7e79fb8c3dd7f3 Mon Sep 17 00:00:00 2001 From: Laszlo Balazs-Csiki Date: Sat, 4 Jan 2025 17:19:31 +0100 Subject: [PATCH] multiline/path text alignment --- .../filters/painters/TextSettings.java | 46 ++++- .../filters/painters/TextSettingsPanel.java | 94 ++++++---- .../painters/TransformedTextPainter.java | 161 +++++++++++++----- .../gui/utils/AlignmentSelector.java | 157 +++++++++++++++++ .../{TextAlignment.java => BoxAlignment.java} | 14 +- .../pixelitor/gui/utils/GridBagHelper.java | 9 +- .../java/pixelitor/gui/utils/VectorIcon.java | 16 +- src/main/java/pixelitor/layers/TextLayer.java | 16 +- .../pixelitor/menus/help/AboutDialog.java | 16 +- .../java/pixelitor/tools/gui/ToolButton.java | 6 +- .../tools/gui/ToolSettingsPanel.java | 6 +- .../java/pixelitor/utils/AppPreferences.java | 22 +-- src/main/java/pixelitor/utils/Shapes.java | 33 +++- .../java/pixelitor/utils/debug/Debug.java | 10 ++ .../utils/test/SplashImageCreator.java | 5 +- 15 files changed, 478 insertions(+), 133 deletions(-) create mode 100644 src/main/java/pixelitor/gui/utils/AlignmentSelector.java rename src/main/java/pixelitor/gui/utils/{TextAlignment.java => BoxAlignment.java} (86%) diff --git a/src/main/java/pixelitor/filters/painters/TextSettings.java b/src/main/java/pixelitor/filters/painters/TextSettings.java index 8c5d17f32..b62810943 100644 --- a/src/main/java/pixelitor/filters/painters/TextSettings.java +++ b/src/main/java/pixelitor/filters/painters/TextSettings.java @@ -23,7 +23,8 @@ import pixelitor.Composition; import pixelitor.Views; import pixelitor.filters.gui.UserPreset; -import pixelitor.gui.utils.TextAlignment; +import pixelitor.gui.utils.AlignmentSelector; +import pixelitor.gui.utils.BoxAlignment; import pixelitor.layers.TextLayer; import pixelitor.utils.Messages; import pixelitor.utils.Rnd; @@ -59,6 +60,7 @@ public class TextSettings implements Serializable, Debuggable { private static final String PRESET_KEY_COLOR = "color"; private static final String PRESET_KEY_ROTATION = "rotation"; private static final String PRESET_KEY_ALIGN = "align"; + private static final String PRESET_KEY_MLP_ALIGN = "mlp_align"; private static final String PRESET_KEY_WATERMARK = "watermark"; private static final String PRESET_KEY_REL_LINE_HEIGHT = "rel_line_height"; private static final String PRESET_KEY_SX = "sx"; @@ -77,6 +79,7 @@ public class TextSettings implements Serializable, Debuggable { private VerticalAlignment verticalAlignment; private HorizontalAlignment horizontalAlignment; private boolean watermark; + private int mlpAlignment; private double rotation; private double sx; @@ -95,6 +98,7 @@ public TextSettings(String text, Font font, Color color, AreaEffects effects, HorizontalAlignment horizontalAlignment, VerticalAlignment verticalAlignment, + int mlpAlignment, boolean watermark, double rotation, double relLineHeight, double sx, double sy, @@ -102,12 +106,13 @@ public TextSettings(String text, Font font, Color color, Consumer guiUpdateCallback) { assert effects != null; + this.text = text; this.areaEffects = effects; this.color = color; this.font = font; this.horizontalAlignment = horizontalAlignment; - this.text = text; this.verticalAlignment = verticalAlignment; + this.mlpAlignment = mlpAlignment; this.watermark = watermark; this.rotation = rotation; this.relLineHeight = relLineHeight; @@ -129,6 +134,8 @@ public TextSettings() { text = DEFAULT_TEXT; verticalAlignment = VerticalAlignment.CENTER; watermark = false; + mlpAlignment = AlignmentSelector.LEFT; + rotation = 0; relLineHeight = 1.0; sx = 1.0; @@ -149,6 +156,7 @@ private TextSettings(TextSettings other) { color = other.color; verticalAlignment = other.verticalAlignment; horizontalAlignment = other.horizontalAlignment; + mlpAlignment = other.mlpAlignment; watermark = other.watermark; rotation = other.rotation; relLineHeight = other.relLineHeight; @@ -170,6 +178,10 @@ private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundE shx = 0.0; shy = 0.0; } + if (mlpAlignment == 0) { + // field not found in old pxc files + mlpAlignment = AlignmentSelector.LEFT; + } } public TextSettings copy() { @@ -181,6 +193,7 @@ public void configurePainter(TransformedTextPainter painter) { painter.setFont(font); painter.setEffects(areaEffects); painter.setAlignment(horizontalAlignment, verticalAlignment); + painter.setMLPAlignment(mlpAlignment); painter.setRotation(rotation); painter.setAdvancedSettings(relLineHeight, sx, sy, shx, shy); } @@ -212,6 +225,12 @@ public void randomize() { color = Rnd.createRandomColor(); horizontalAlignment = Rnd.chooseFrom(HorizontalAlignment.values()); verticalAlignment = Rnd.chooseFrom(VerticalAlignment.values()); + mlpAlignment = Rnd.chooseFrom(new int[]{ + AlignmentSelector.LEFT, + AlignmentSelector.CENTER, + AlignmentSelector.RIGHT + }); + watermark = Rnd.nextBoolean(); rotation = Rnd.nextDouble() * Math.PI * 2; relLineHeight = 0.5 + Rnd.nextDouble(); @@ -226,6 +245,7 @@ public void saveStateTo(UserPreset preset) { preset.putColor(PRESET_KEY_COLOR, color); preset.putFloat(PRESET_KEY_ROTATION, (float) rotation); preset.putInt(PRESET_KEY_ALIGN, getAlignment().ordinal()); + preset.putInt(PRESET_KEY_MLP_ALIGN, mlpAlignment); new FontInfo(font).saveStateTo(preset); @@ -250,13 +270,14 @@ public void loadUserPreset(UserPreset preset) { horizontalAlignment = HorizontalAlignment.values()[preset.getInt(PRESET_KEY_HOR_ALIGN)]; verticalAlignment = VerticalAlignment.values()[preset.getInt(PRESET_KEY_VER_ALIGN)]; } else { - TextAlignment alignment = TextAlignment.values()[alignIndex]; - if (alignment == TextAlignment.PATH && !Views.getActiveComp().hasActivePath()) { - alignment = TextAlignment.CENTER_CENTER; + BoxAlignment alignment = BoxAlignment.values()[alignIndex]; + if (alignment == BoxAlignment.PATH && !Views.getActiveComp().hasActivePath()) { + alignment = BoxAlignment.CENTER_CENTER; } horizontalAlignment = alignment.getHorizontal(); verticalAlignment = alignment.getVertical(); } + mlpAlignment = preset.getInt(PRESET_KEY_MLP_ALIGN); font = new FontInfo(preset).createFont(); @@ -312,15 +333,23 @@ public void setFont(Font font) { this.font = font; } - public TextAlignment getAlignment() { - return TextAlignment.from(horizontalAlignment, verticalAlignment); + public BoxAlignment getAlignment() { + return BoxAlignment.from(horizontalAlignment, verticalAlignment); } - public void setAlignment(TextAlignment newAlignment) { + public void setAlignment(BoxAlignment newAlignment) { this.horizontalAlignment = newAlignment.getHorizontal(); this.verticalAlignment = newAlignment.getVertical(); } + public int getMLPAlignment() { + return mlpAlignment; + } + + public void setMLPAlignment(int mlpAlignment) { + this.mlpAlignment = mlpAlignment; + } + public String getText() { return text; } @@ -379,6 +408,7 @@ public DebugNode createDebugNode(String key) { node.add(areaEffects.createDebugNode("effects")); node.addDouble("rotation", rotation); node.addBoolean("watermark", watermark); + node.addInt("multiline/path alignment", mlpAlignment); return node; } diff --git a/src/main/java/pixelitor/filters/painters/TextSettingsPanel.java b/src/main/java/pixelitor/filters/painters/TextSettingsPanel.java index e486b1433..a8cb34645 100644 --- a/src/main/java/pixelitor/filters/painters/TextSettingsPanel.java +++ b/src/main/java/pixelitor/filters/painters/TextSettingsPanel.java @@ -1,5 +1,5 @@ /* - * Copyright 2024 Laszlo Balazs-Csiki and Contributors + * Copyright 2025 Laszlo Balazs-Csiki and Contributors * * This file is part of Pixelitor. Pixelitor is free software: you * can redistribute it and/or modify it under the terms of the GNU @@ -69,14 +69,19 @@ public class TextSettingsPanel extends FilterGUI private JCheckBox italicCB; private ColorParam color; private EffectsPanel effectsPanel; - private JComboBox alignmentCB; + private JComboBox boxAlignmentCB; private JCheckBox watermarkCB; private JDialog advancedSettingsDialog; private AdvancedTextSettingsPanel advancedSettingsPanel; private boolean suppressGuiUpdates = false; - private TextAlignment lastAlignment; + private BoxAlignment lastBoxAlignment; + + // multiline/path alignment settings + private boolean hasMLPAlign = true; + private AlignmentSelector mlpAlignmentSelector; + private JLabel mlpLabel; /** * Used for the text filter on images @@ -134,34 +139,46 @@ private JPanel createTextPanel(TextSettings settings, Composition comp) { gbh.addLabel("Color:", 0, 1); color = new ColorParam("Color", settings.getColor(), USER_ONLY_TRANSPARENCY); - gbh.addControl(color.createGUI()); color.setAdjustmentListener(this); - gbh.addLabel("Rotation:", 2, 1); + JPanel colorRotPanel = new JPanel(new FlowLayout(LEFT)); + colorRotPanel.add(color.createGUI()); + colorRotPanel.add(new JLabel(" Rotation:")); rotationParam = new AngleParam("", settings.getRotation()); rotationParam.setAdjustmentListener(this); - gbh.addControl(rotationParam.createGUI()); - - alignmentCB = new JComboBox<>(TextAlignment.values()); - alignmentCB.setName("alignmentCB"); - TextAlignment alignment = settings.getAlignment(); - lastAlignment = alignment; - alignmentCB.setSelectedItem(alignment); - - alignmentCB.addActionListener(e -> { - TextAlignment selectedAlignment = getSelectedAlignment(); - if (selectedAlignment == TextAlignment.PATH && !comp.hasActivePath()) { + colorRotPanel.add(rotationParam.createGUI()); + gbh.addControl(colorRotPanel); + + boxAlignmentCB = new JComboBox<>(BoxAlignment.values()); + boxAlignmentCB.setName("alignmentCB"); + BoxAlignment alignment = settings.getAlignment(); + lastBoxAlignment = alignment; + boxAlignmentCB.setSelectedItem(alignment); + + boxAlignmentCB.addActionListener(e -> { + BoxAlignment selectedAlignment = getSelectedBoxAlignment(); + if (selectedAlignment == BoxAlignment.PATH && !comp.hasActivePath()) { String msg = "There's no path in \"" + comp.getName() + "\".
You can have text along a path after creating a path with the Pen Tool."; Messages.showError("No Path", msg, this); - alignmentCB.setSelectedItem(lastAlignment); + boxAlignmentCB.setSelectedItem(lastBoxAlignment); return; } - lastAlignment = selectedAlignment; + lastBoxAlignment = selectedAlignment; + updateMLPAlignEnabled(); actionPerformed(e); }); - gbh.addLabel("Alignment:", 0, 2); - gbh.addControl(alignmentCB); + gbh.addLabel("Box Alignment:", 0, 2); + + JPanel alignPanel = new JPanel(new FlowLayout(LEFT)); + alignPanel.add(boxAlignmentCB); + mlpLabel = new JLabel(" Multiline/Path Alignment:"); + alignPanel.add(mlpLabel); + mlpAlignmentSelector = new AlignmentSelector(settings.getMLPAlignment(), this); + alignPanel.add(mlpAlignmentSelector); + updateMLPAlignEnabled(); + + gbh.addControl(alignPanel); return textPanel; } @@ -173,21 +190,35 @@ private void createTextArea(TextSettings settings) { textArea.getDocument().addDocumentListener(new DocumentListener() { @Override public void insertUpdate(DocumentEvent e) { - paramAdjusted(); + textChanged(); } @Override public void removeUpdate(DocumentEvent e) { - paramAdjusted(); + textChanged(); } @Override public void changedUpdate(DocumentEvent e) { - paramAdjusted(); + textChanged(); } }); } + private void textChanged() { + updateMLPAlignEnabled(); + paramAdjusted(); + } + + private void updateMLPAlignEnabled() { + boolean hasMLPAlignNow = getSelectedBoxAlignment() == BoxAlignment.PATH || textArea.getText().contains("\n"); + if (hasMLPAlignNow != hasMLPAlign) { + hasMLPAlign = hasMLPAlignNow; + mlpLabel.setEnabled(hasMLPAlign); + mlpAlignmentSelector.setEnabled(hasMLPAlign); + } + } + private JPanel createFontPanel(TextSettings settings) { JPanel fontPanel = new JPanel(new GridBagLayout()); fontPanel.setBorder(createTitledBorder("Font")); @@ -323,12 +354,12 @@ private void createEffectsPanel(TextSettings settings) { } private JPanel createBottomPanel(TextSettings settings) { - watermarkCB = new JCheckBox("Watermarking", settings.hasWatermark()); + JPanel p = new JPanel(new FlowLayout(LEFT, 5, 5)); + watermarkCB = new JCheckBox("Watermarking", settings.hasWatermark()); watermarkCB.addActionListener(this); - - JPanel p = new JPanel(new FlowLayout(LEFT, 5, 5)); p.add(watermarkCB); + return p; } @@ -352,7 +383,7 @@ private TextSettings createSettingsFromGui() { effects = effectsPanel.getEffects(); } - TextAlignment alignment = getSelectedAlignment(); + BoxAlignment alignment = getSelectedBoxAlignment(); return new TextSettings( textArea.getText(), @@ -361,6 +392,7 @@ private TextSettings createSettingsFromGui() { effects, alignment.getHorizontal(), alignment.getVertical(), + mlpAlignmentSelector.getSelectedAlignment(), watermarkCB.isSelected(), rotationParam.getValueInRadians(), getRelLineHeight(), @@ -368,8 +400,8 @@ private TextSettings createSettingsFromGui() { getShearX(), getShearY(), this); } - private TextAlignment getSelectedAlignment() { - return (TextAlignment) alignmentCB.getSelectedItem(); + private BoxAlignment getSelectedBoxAlignment() { + return (BoxAlignment) boxAlignmentCB.getSelectedItem(); } private void updateApp(TextSettings settings) { @@ -403,7 +435,9 @@ private void setUIValues(TextSettings settings) { textArea.setText(settings.getText()); color.setColor(settings.getColor(), false); rotationParam.setValue(settings.getRotation(), false); - alignmentCB.setSelectedItem(settings.getAlignment()); + boxAlignmentCB.setSelectedItem(settings.getAlignment()); + mlpAlignmentSelector.setSelected(settings.getMLPAlignment()); + updateMLPAlignEnabled(); Font font = settings.getFont(); fontSizeSlider.setValue(font.getSize()); diff --git a/src/main/java/pixelitor/filters/painters/TransformedTextPainter.java b/src/main/java/pixelitor/filters/painters/TransformedTextPainter.java index 45caf59c3..2d1b9c746 100644 --- a/src/main/java/pixelitor/filters/painters/TransformedTextPainter.java +++ b/src/main/java/pixelitor/filters/painters/TransformedTextPainter.java @@ -1,5 +1,5 @@ /* - * Copyright 2024 Laszlo Balazs-Csiki and Contributors + * Copyright 2025 Laszlo Balazs-Csiki and Contributors * * This file is part of Pixelitor. Pixelitor is free software: you * can redistribute it and/or modify it under the terms of the GNU @@ -26,7 +26,8 @@ import pixelitor.Views; import pixelitor.colors.Colors; import pixelitor.compactions.Flip; -import pixelitor.gui.utils.TextAlignment; +import pixelitor.gui.utils.AlignmentSelector; +import pixelitor.gui.utils.BoxAlignment; import pixelitor.utils.ImageUtils; import pixelitor.utils.QuadrantAngle; import pixelitor.utils.Shapes; @@ -76,6 +77,7 @@ public class TransformedTextPainter implements Debuggable { private HorizontalAlignment horizontalAlignment = HorizontalAlignment.CENTER; private VerticalAlignment verticalAlignment = VerticalAlignment.CENTER; + private int mlpAlignment = AlignmentSelector.LEFT; private Font font = null; private Color color; @@ -102,6 +104,8 @@ public class TransformedTextPainter implements Debuggable { private float lineHeight; private double relLineHeight; + private int origTextWidth; // max width before rotation + private SoftReference renderCache; private boolean invalidLayout = true; @@ -293,7 +297,15 @@ private void doPaint(Graphics2D g, AffineTransform origTransform) { g.drawString(text, effectsWidth, drawY); } else { for (String line : textLines) { - g.drawString(line, effectsWidth, drawY); + float drawX = switch (mlpAlignment) { + case AlignmentSelector.LEFT -> effectsWidth; + case AlignmentSelector.CENTER -> + (origTextWidth + 2 * effectsWidth) / 2.0f - metrics.stringWidth(line) / 2.0f; + case AlignmentSelector.RIGHT -> + (origTextWidth + 2 * effectsWidth) - metrics.stringWidth(line) - effectsWidth; + default -> throw new IllegalStateException("alignment: " + mlpAlignment); + }; + g.drawString(line, drawX, drawY); drawY += lineHeight; } } @@ -308,7 +320,7 @@ private void doPaint(Graphics2D g, AffineTransform origTransform) { if (effects != null && effects.hasEnabledEffects()) { if (invalidShape) { - baseShape = provideShape(g); + baseShape = calcUntransformedTextShape(g); invalidShape = false; invalidShapeTransform = true; // implied } @@ -389,6 +401,8 @@ private void updateLayout(int width, int height, Graphics2D g, Composition comp) } boundingBox = calcBoundingBox(textWidth, textHeight, width, height, g); invalidLayout = false; + + origTextWidth = textWidth; } private void renderOnPath(Path2D path, Graphics2D g2) { @@ -408,10 +422,55 @@ private void renderOnPath(Path2D path, Graphics2D g2) { transformedRect = null; } + private double calcTextLength(GlyphVector glyphVector, double tracking) { + double totalTextLength = 0; + int numGlyphs = glyphVector.getNumGlyphs(); + + for (int i = 0; i < numGlyphs; i++) { + totalTextLength += glyphVector.getGlyphMetrics(i).getAdvance() * Math.abs(scaleX); + if (i < numGlyphs - 1) { + totalTextLength += tracking; + } + } + return totalTextLength; + } + private Path2D distributeGlyphsAlongPath(GlyphVector glyphVector, Path2D path) { + double pathLength = Shapes.calcPathLength(path); + double tracking = calcTracking(); + double textLength = calcTextLength(glyphVector, tracking); + + // calculate the initial offset based on alignment + int startGlyphIndex = 0; + int numGlyphs = glyphVector.getNumGlyphs(); + + double initialOffset = switch (mlpAlignment) { + case AlignmentSelector.LEFT -> 0; + case AlignmentSelector.CENTER -> (pathLength - textLength) / 2; + case AlignmentSelector.RIGHT -> pathLength - textLength; + default -> throw new IllegalStateException("Unexpected value: " + mlpAlignment); + }; + + // handle text overflow for right or center alignment + if (initialOffset < 0 && (mlpAlignment == AlignmentSelector.RIGHT || mlpAlignment == AlignmentSelector.CENTER)) { + // calculate how many glyphs we need to skip + double accumulatedWidth = 0; + double overflow = -initialOffset; + if (mlpAlignment == AlignmentSelector.CENTER) { + overflow = overflow / 2; + } + while (startGlyphIndex < numGlyphs && accumulatedWidth < overflow) { + accumulatedWidth += glyphVector.getGlyphMetrics(startGlyphIndex).getAdvance() * Math.abs(scaleX); + if (startGlyphIndex < numGlyphs - 1) { + accumulatedWidth += tracking; + } + startGlyphIndex++; + } + initialOffset = 0; + } + Path2D result = new Path2D.Float(); PathIterator it = new FlatteningPathIterator(path.getPathIterator(null), 1); - double[] points = new double[6]; // the coordinates of the starting point of a path segment @@ -424,24 +483,12 @@ private Path2D distributeGlyphsAlongPath(GlyphVector glyphVector, Path2D path) { double thisX, thisY; int type; + double currentDist = 0; + double thresholdDist = initialOffset; - // the distance to be covered before the next shape is placed - double thresholdDist = 0; - - int glyphIndex = 0; - int numGlyphs = glyphVector.getNumGlyphs(); + int glyphIndex = startGlyphIndex; AffineTransform at = new AffineTransform(); - double nextAdvance = 0; - double sxa = Math.abs(scaleX); - - double tracking = 0.0; - var map = font.getAttributes(); - Float trackingValue = (Float) map.get(TRACKING); - if (trackingValue != null) { - tracking = trackingValue * font.getSize(); - } - while (glyphIndex < numGlyphs && !it.isDone()) { type = it.currentSegment(points); switch (type) { @@ -449,8 +496,6 @@ private Path2D distributeGlyphsAlongPath(GlyphVector glyphVector, Path2D path) { moveX = lastX = points[0]; moveY = lastY = points[1]; result.moveTo(moveX, moveY); - nextAdvance = tracking + glyphVector.getGlyphMetrics(glyphIndex).getAdvance() * 0.5f; - thresholdDist = nextAdvance; break; case PathIterator.SEG_CLOSE: @@ -464,15 +509,17 @@ private Path2D distributeGlyphsAlongPath(GlyphVector glyphVector, Path2D path) { double dx = thisX - lastX; double dy = thisY - lastY; double distance = Math.sqrt(dx * dx + dy * dy); - if (distance >= thresholdDist) { + currentDist += distance; + + if (currentDist >= thresholdDist) { double angle = Math.atan2(dy, dx); - while (glyphIndex < numGlyphs && distance >= thresholdDist) { + while (glyphIndex < numGlyphs && currentDist >= thresholdDist) { Shape glyph = glyphVector.getGlyphOutline(glyphIndex); Point2D origGlyphPos = glyphVector.getGlyphPosition(glyphIndex); - double x = lastX + thresholdDist * dx / distance; - double y = lastY + thresholdDist * dy / distance; - double advance = nextAdvance; - nextAdvance = glyphIndex < numGlyphs - 1 ? tracking + glyphVector.getGlyphMetrics(glyphIndex + 1).getAdvance() * 0.5f : 0; + double ratio = (thresholdDist - (currentDist - distance)) / distance; + double x = lastX + ratio * dx; + double y = lastY + ratio * dy; + at.setToTranslation(x, y); at.rotate(angle + rotation); if (scaleX != 1.0 || scaleY != 1.0) { @@ -481,13 +528,18 @@ private Path2D distributeGlyphsAlongPath(GlyphVector glyphVector, Path2D path) { if (shearX != 0 || shearY != 0) { at.shear(-shearX, -shearY); } - at.translate(-origGlyphPos.getX() - advance, -origGlyphPos.getY()); + at.translate(-origGlyphPos.getX(), -origGlyphPos.getY()); result.append(at.createTransformedShape(glyph), false); - thresholdDist += sxa * (advance + nextAdvance); + + // calculate next threshold + double advance = glyphVector.getGlyphMetrics(glyphIndex).getAdvance() * Math.abs(scaleX); + if (glyphIndex < numGlyphs - 1) { + advance += tracking; + } + thresholdDist += advance; glyphIndex++; } } - thresholdDist -= distance; lastX = thisX; lastY = thisY; break; @@ -497,6 +549,13 @@ private Path2D distributeGlyphsAlongPath(GlyphVector glyphVector, Path2D path) { return result; } + private double calcTracking() { + Float trackingValue = (Float) font.getAttributes().get(TRACKING); + return trackingValue == null + ? 0.0 + : trackingValue * font.getSize() * Math.abs(scaleX); + } + public Shape getTextShape() { if (isOnPath()) { return textShape; @@ -510,7 +569,7 @@ public Shape getTextShape() { transformGraphics(g2); var at = g2.getTransform(); g2.setTransform(imgOrigTransform); // provideShape must be called with untransformed Graphics - Shape shape = provideShape(g2); + Shape shape = calcUntransformedTextShape(g2); g2.dispose(); tmp.flush(); @@ -596,7 +655,7 @@ public TransformedTextPainter copy(TextSettings settings) { } // Return the text shape relative to (0, 0) - private Shape provideShape(Graphics2D g2) { + private Shape calcUntransformedTextShape(Graphics2D g2) { FontMetrics metrics = g2.getFontMetrics(font); FontRenderContext frc = g2.getFontRenderContext(); @@ -611,17 +670,27 @@ private Shape provideShape(Graphics2D g2) { } assert textLines.length > 1; - Area retVal = null; + Area fullShape = null; for (int i = 0; i < textLines.length; i++) { - Shape lineShape = getLineShape(textLines[i], frc, metrics, hasKerning, hasLigatures, hasUnderline, hasStrikeThrough); + String line = textLines[i]; + Shape lineShape = getLineShape(line, frc, metrics, hasKerning, hasLigatures, hasUnderline, hasStrikeThrough); + double tx = switch (mlpAlignment) { + case AlignmentSelector.LEFT -> 0; + case AlignmentSelector.CENTER -> origTextWidth / 2.0 - metrics.stringWidth(line) / 2.0; + case AlignmentSelector.RIGHT -> origTextWidth - metrics.stringWidth(line); + default -> throw new IllegalStateException("alignment: " + mlpAlignment); + }; if (i == 0) { - retVal = new Area(lineShape); + if (tx != 0) { + lineShape = Shapes.translate(lineShape, tx, 0); + } + fullShape = new Area(lineShape); } else { - lineShape = Shapes.translate(lineShape, 0, i * lineHeight); - retVal.add(new Area(lineShape)); + lineShape = Shapes.translate(lineShape, tx, i * lineHeight); + fullShape.add(new Area(lineShape)); } } - return retVal; + return fullShape; } private Shape getLineShape(String line, FontRenderContext frc, FontMetrics metrics, boolean hasKerning, boolean hasLigatures, boolean hasUnderline, boolean hasStrikeThrough) { @@ -686,7 +755,7 @@ private static Area createStrikeThroughShape(LineMetrics lineMetrics, float asce return new Area(strikethroughShape); } - public void setAlignment(TextAlignment newAlignment) { + public void setAlignment(BoxAlignment newAlignment) { setAlignment(newAlignment.getHorizontal(), newAlignment.getVertical()); } @@ -702,6 +771,15 @@ public void setAlignment(HorizontalAlignment newHorAlignment, VerticalAlignment } } + public void setMLPAlignment(int mlpAlignment) { + boolean change = this.mlpAlignment != mlpAlignment; + if (change) { + this.mlpAlignment = mlpAlignment; + clearCache(); + invalidShape = true; + } + } + public void setColor(Color newColor) { boolean change = !newColor.equals(this.color); this.color = newColor; @@ -715,6 +793,9 @@ public void setText(String newText) { if (change) { this.text = newText == null ? "" : newText; textLines = newText.split("\n"); + for (int i = 0; i < textLines.length; i++) { + textLines[i] = textLines[i].trim(); + } clearCache(); invalidLayout = true; invalidShape = true; diff --git a/src/main/java/pixelitor/gui/utils/AlignmentSelector.java b/src/main/java/pixelitor/gui/utils/AlignmentSelector.java new file mode 100644 index 000000000..0e1abcb7c --- /dev/null +++ b/src/main/java/pixelitor/gui/utils/AlignmentSelector.java @@ -0,0 +1,157 @@ +/* + * Copyright 2025 Laszlo Balazs-Csiki and Contributors + * + * This file is part of Pixelitor. Pixelitor is free software: you + * can redistribute it and/or modify it under the terms of the GNU + * General Public License, version 3 as published by the Free + * Software Foundation. + * + * Pixelitor 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 Pixelitor. If not, see . + */ + +package pixelitor.gui.utils; + +import pixelitor.filters.gui.ParamAdjustmentListener; + +import javax.swing.*; +import java.awt.*; +import java.awt.geom.Path2D; + +import static pixelitor.gui.utils.VectorIcon.LIGHT_FG; +import static pixelitor.tools.gui.ToolButton.darkThemeSelectedColor; + +public class AlignmentSelector extends JPanel { + private final JToggleButton leftAlign; + private final JToggleButton centerAlign; + private final JToggleButton rightAlign; + + public static final int LEFT = 1; + public static final int CENTER = 2; + public static final int RIGHT = 3; + + private static final int ICON_SIZE = 18; + private static final int PADDING = 4; + private static final Dimension BUTTON_SIZE = new Dimension(30, 30); + + private static Shape[] iconShapes; + + public AlignmentSelector(int defaultAlignment, ParamAdjustmentListener adjustmentListener) { + setLayout(new GridLayout(1, 3, 0, 0)); + + boolean darkTheme = Themes.getActive().isDark(); + ButtonGroup group = new ButtonGroup(); + + if (iconShapes == null) { + createIconShapes(); + } + + leftAlign = addButton(0, group, darkTheme); + centerAlign = addButton(1, group, darkTheme); + rightAlign = addButton(2, group, darkTheme); + + setSelected(defaultAlignment); + + // bind to the listener only after setting the default selection + leftAlign.addActionListener(e -> adjustmentListener.paramAdjusted()); + centerAlign.addActionListener(e -> adjustmentListener.paramAdjusted()); + rightAlign.addActionListener(e -> adjustmentListener.paramAdjusted()); + } + + private static void createIconShapes() { + iconShapes = new Shape[3]; + iconShapes[0] = createIconShape(LEFT); + iconShapes[1] = createIconShape(CENTER); + iconShapes[2] = createIconShape(RIGHT); + } + + private JToggleButton addButton(int shapeIndex, ButtonGroup group, boolean darkTheme) { + VectorIcon icon = createAlignmentIcon(shapeIndex, darkTheme); + JToggleButton button = new JToggleButton(icon); + button.setPreferredSize(BUTTON_SIZE); + + VectorIcon disabledIcon = icon.copyWithColor(Color.GRAY); + button.setDisabledIcon(disabledIcon); + + if (darkTheme) { + button.setSelectedIcon(icon.copyWithColor(darkThemeSelectedColor)); + button.setDisabledSelectedIcon(disabledIcon); + } + + button.setFocusPainted(false); + button.setMargin(new Insets(PADDING, PADDING, PADDING, PADDING)); + group.add(button); + add(button); + + return button; + } + + private static VectorIcon createAlignmentIcon(int shapeIndex, boolean darkTheme) { + Shape shape = iconShapes[shapeIndex]; + + Color color = darkTheme ? Themes.LIGHT_ICON_COLOR : LIGHT_FG; + return VectorIcon.createFilled(shape, color, ICON_SIZE, ICON_SIZE); + } + + private static Path2D createIconShape(int alignment) { + int lineHeight = 2; + int lineGap = 2; + + // 5 lines, and 4 gaps between them + assert (5 * lineHeight) + (4 * lineGap) == ICON_SIZE; + + // TODO the 3 shapes could be cached + Path2D shape = new Path2D.Double(); + for (int i = 0; i < 5; i++) { + int y = i * (lineHeight + lineGap); + int lineWidth = (i % 2 == 0) ? ICON_SIZE : (int) (ICON_SIZE * 0.6); + + double x = switch (alignment) { + case CENTER -> (ICON_SIZE - lineWidth) / 2.0; // center all lines + case RIGHT -> ICON_SIZE - lineWidth; // align to right + case LEFT -> 0; // align to left + default -> throw new IllegalStateException("Unexpected value: " + alignment); + }; + + // create a horizontal line as a rectangle that can be filled + shape.moveTo(x, y); + shape.lineTo(x, y + lineHeight); + shape.lineTo(x + lineWidth, y + lineHeight); + shape.lineTo(x + lineWidth, y); + shape.closePath(); + } + return shape; + } + + public int getSelectedAlignment() { + if (centerAlign.isSelected()) { + return CENTER; + } + if (rightAlign.isSelected()) { + return RIGHT; + } + return LEFT; + } + + public void setSelected(int alignment) { + JToggleButton selectedButton = switch (alignment) { + case LEFT -> leftAlign; + case CENTER -> centerAlign; + case RIGHT -> rightAlign; + default -> throw new IllegalStateException("alignment: " + alignment); + }; + selectedButton.setSelected(true); + } + + @Override + public void setEnabled(boolean enabled) { + leftAlign.setEnabled(enabled); + centerAlign.setEnabled(enabled); + rightAlign.setEnabled(enabled); + } +} diff --git a/src/main/java/pixelitor/gui/utils/TextAlignment.java b/src/main/java/pixelitor/gui/utils/BoxAlignment.java similarity index 86% rename from src/main/java/pixelitor/gui/utils/TextAlignment.java rename to src/main/java/pixelitor/gui/utils/BoxAlignment.java index 75ca858c3..97eb04b1e 100644 --- a/src/main/java/pixelitor/gui/utils/TextAlignment.java +++ b/src/main/java/pixelitor/gui/utils/BoxAlignment.java @@ -1,5 +1,5 @@ /* - * Copyright 2024 Laszlo Balazs-Csiki and Contributors + * Copyright 2025 Laszlo Balazs-Csiki and Contributors * * This file is part of Pixelitor. Pixelitor is free software: you * can redistribute it and/or modify it under the terms of the GNU @@ -21,9 +21,9 @@ import static org.jdesktop.swingx.painter.AbstractLayoutPainter.VerticalAlignment; /** - * A two-dimensional text alignment combining horizontal and vertical positioning. + * A two-dimensional text box alignment combining horizontal and vertical positioning. */ -public enum TextAlignment { +public enum BoxAlignment { CENTER_CENTER("Center", HorizontalAlignment.CENTER, VerticalAlignment.CENTER), TOP_CENTER("Top", HorizontalAlignment.CENTER, VerticalAlignment.TOP), CENTER_LEFT("Left", HorizontalAlignment.LEFT, VerticalAlignment.CENTER), @@ -41,7 +41,7 @@ public enum TextAlignment { private final HorizontalAlignment horizontalAlignment; private final VerticalAlignment verticalAlignment; - TextAlignment(String displayName, HorizontalAlignment horizontalAlignment, VerticalAlignment verticalAlignment) { + BoxAlignment(String displayName, HorizontalAlignment horizontalAlignment, VerticalAlignment verticalAlignment) { this.displayName = displayName; this.horizontalAlignment = horizontalAlignment; this.verticalAlignment = verticalAlignment; @@ -61,11 +61,11 @@ public String toString() { } /** - * Returns the corresponding {@link TextAlignment} based + * Returns the corresponding {@link BoxAlignment} based * on the given horizontal and vertical alignments. */ - public static TextAlignment from(HorizontalAlignment horizontal, - VerticalAlignment vertical) { + public static BoxAlignment from(HorizontalAlignment horizontal, + VerticalAlignment vertical) { if (horizontal == null || vertical == null) { return PATH; } diff --git a/src/main/java/pixelitor/gui/utils/GridBagHelper.java b/src/main/java/pixelitor/gui/utils/GridBagHelper.java index 09a71f35f..9a50b60da 100644 --- a/src/main/java/pixelitor/gui/utils/GridBagHelper.java +++ b/src/main/java/pixelitor/gui/utils/GridBagHelper.java @@ -1,5 +1,5 @@ /* - * Copyright 2024 Laszlo Balazs-Csiki and Contributors + * Copyright 2025 Laszlo Balazs-Csiki and Contributors * * This file is part of Pixelitor. Pixelitor is free software: you * can redistribute it and/or modify it under the terms of the GNU @@ -33,7 +33,6 @@ import static java.awt.GridBagConstraints.NONE; import static java.awt.GridBagConstraints.REMAINDER; import static java.awt.GridBagConstraints.WEST; -import static javax.swing.SwingConstants.RIGHT; /** * Helper object for GridBagLayout @@ -80,7 +79,7 @@ private static void initConstraints() { } public void addLabel(String labelText, int column, int row) { - JLabel label = new JLabel(labelText, RIGHT); + JLabel label = new JLabel(labelText, SwingConstants.RIGHT); addLabel(label, column, row); } @@ -129,7 +128,7 @@ public void addLabelAndControl(String labelText, Component c) { } public void addVerticallyStretchable(String labelText, Component c, double verticalWeight) { - JLabel label = new JLabel(labelText, RIGHT); + JLabel label = new JLabel(labelText, SwingConstants.RIGHT); LABEL_CONSTRAINTS.gridx = 0; LABEL_CONSTRAINTS.gridy = currentRow; container.add(label, LABEL_CONSTRAINTS); @@ -153,7 +152,7 @@ public void addParam(FilterParam param, String lookupName) { } public void addLabelAndControl(String labelText, Component c, int row) { - JLabel label = new JLabel(labelText, RIGHT); + JLabel label = new JLabel(labelText, SwingConstants.RIGHT); addTwoControls(label, c, row); } diff --git a/src/main/java/pixelitor/gui/utils/VectorIcon.java b/src/main/java/pixelitor/gui/utils/VectorIcon.java index 8a8b0f3c1..d61ea62a8 100644 --- a/src/main/java/pixelitor/gui/utils/VectorIcon.java +++ b/src/main/java/pixelitor/gui/utils/VectorIcon.java @@ -1,5 +1,5 @@ /* - * Copyright 2024 Laszlo Balazs-Csiki and Contributors + * Copyright 2025 Laszlo Balazs-Csiki and Contributors * * This file is part of Pixelitor. Pixelitor is free software: you * can redistribute it and/or modify it under the terms of the GNU @@ -36,7 +36,7 @@ public abstract class VectorIcon implements Icon, Cloneable { private static final Color LIGHT_BG = new Color(214, 217, 223); private static final Color DARK_BG = new Color(42, 42, 42); - private static final Color LIGHT_FG = new Color(19, 30, 43); + public static final Color LIGHT_FG = new Color(19, 30, 43); protected VectorIcon(Color color, int width, int height) { this.color = color; @@ -68,7 +68,7 @@ public final int getIconHeight() { return height; } - public VectorIcon copy(Color color) { + public VectorIcon copyWithColor(Color color) { VectorIcon copy = clone(); copy.color = color; return copy; @@ -97,4 +97,14 @@ protected void paintIcon(Graphics2D g) { } }; } + + public static VectorIcon createFilled(Shape shape, Color color, int width, int height) { + return new VectorIcon(color, width, height) { + @Override + protected void paintIcon(Graphics2D g) { + g.setColor(color); + g.fill(shape); + } + }; + } } diff --git a/src/main/java/pixelitor/layers/TextLayer.java b/src/main/java/pixelitor/layers/TextLayer.java index a25061d02..a935a6bee 100644 --- a/src/main/java/pixelitor/layers/TextLayer.java +++ b/src/main/java/pixelitor/layers/TextLayer.java @@ -27,9 +27,9 @@ import pixelitor.filters.painters.TextSettings; import pixelitor.filters.painters.TextSettingsPanel; import pixelitor.filters.painters.TransformedTextPainter; +import pixelitor.gui.utils.BoxAlignment; import pixelitor.gui.utils.DialogBuilder; import pixelitor.gui.utils.TaskAction; -import pixelitor.gui.utils.TextAlignment; import pixelitor.history.*; import pixelitor.io.TranslatedImage; import pixelitor.tools.Tools; @@ -229,7 +229,7 @@ public int getPixelAtPoint(Point p) { @Override public void paint(Graphics2D g, boolean firstVisibleLayer) { - painter.setColor(settings.getColor()); + painter.setColor(settings.getColor()); // TODO is this already set? painter.paint(g, comp.getCanvasWidth(), comp.getCanvasHeight(), comp); } @@ -307,8 +307,8 @@ public TranslatedImage getTranslatedImage() { @Override public void enlargeCanvas(Outsets out) { - TextAlignment alignment = settings.getAlignment(); - if (alignment == TextAlignment.PATH) { + BoxAlignment alignment = settings.getAlignment(); + if (alignment == BoxAlignment.PATH) { return; } @@ -433,15 +433,15 @@ public void pathChanged(boolean deleted) { holder.invalidateImageCache(); if (deleted) { - settings.setAlignment(TextAlignment.CENTER_CENTER); - painter.setAlignment(TextAlignment.CENTER_CENTER); + settings.setAlignment(BoxAlignment.CENTER_CENTER); + painter.setAlignment(BoxAlignment.CENTER_CENTER); } } } public void usePathEditing() { - settings.setAlignment(TextAlignment.PATH); - painter.setAlignment(TextAlignment.PATH); + settings.setAlignment(BoxAlignment.PATH); + painter.setAlignment(BoxAlignment.PATH); painter.pathChanged(); holder.invalidateImageCache(); diff --git a/src/main/java/pixelitor/menus/help/AboutDialog.java b/src/main/java/pixelitor/menus/help/AboutDialog.java index 195264df2..fbbc1b790 100644 --- a/src/main/java/pixelitor/menus/help/AboutDialog.java +++ b/src/main/java/pixelitor/menus/help/AboutDialog.java @@ -23,12 +23,10 @@ import pixelitor.utils.OpenInBrowserAction; import javax.swing.*; +import java.awt.Component; import java.awt.Dimension; import java.net.URL; -import static java.awt.Component.CENTER_ALIGNMENT; -import static javax.swing.SwingConstants.CENTER; - /** * The "About" dialog of the app. */ @@ -97,12 +95,12 @@ private static JButton createLinkButton(JComponent aboutPanel) { String linkButtonText = "" + WEBSITE_URL + ""; var linkButton = new JButton(linkButtonText); - linkButton.setHorizontalAlignment(CENTER); + linkButton.setHorizontalAlignment(SwingConstants.CENTER); linkButton.setBorderPainted(false); linkButton.setFocusPainted(false); linkButton.setOpaque(false); linkButton.setBackground(aboutPanel.getBackground()); - linkButton.setAlignmentX(CENTER_ALIGNMENT); + linkButton.setAlignmentX(Component.CENTER_ALIGNMENT); linkButton.addActionListener(new OpenInBrowserAction(null, WEBSITE_URL)); return linkButton; @@ -110,14 +108,14 @@ private static JButton createLinkButton(JComponent aboutPanel) { private static void addLabel(JComponent p, URL imageURL) { var imageIcon = new ImageIcon(imageURL); - var label = new JLabel(imageIcon, CENTER); - label.setAlignmentX(CENTER_ALIGNMENT); + var label = new JLabel(imageIcon, SwingConstants.CENTER); + label.setAlignmentX(Component.CENTER_ALIGNMENT); p.add(label); } private static void addLabel(JComponent p, String text) { - var label = new JLabel(text, CENTER); - label.setAlignmentX(CENTER_ALIGNMENT); + var label = new JLabel(text, SwingConstants.CENTER); + label.setAlignmentX(Component.CENTER_ALIGNMENT); p.add(label); } } diff --git a/src/main/java/pixelitor/tools/gui/ToolButton.java b/src/main/java/pixelitor/tools/gui/ToolButton.java index 844023d06..4e1b74a06 100644 --- a/src/main/java/pixelitor/tools/gui/ToolButton.java +++ b/src/main/java/pixelitor/tools/gui/ToolButton.java @@ -1,5 +1,5 @@ /* - * Copyright 2024 Laszlo Balazs-Csiki and Contributors + * Copyright 2025 Laszlo Balazs-Csiki and Contributors * * This file is part of Pixelitor. Pixelitor is free software: you * can redistribute it and/or modify it under the terms of the GNU @@ -38,7 +38,7 @@ */ public class ToolButton extends JToggleButton { public static final int ICON_SIZE = 28; - private static Color darkThemeSelectedColor = Themes.DEFAULT_ACCENT_COLOR.asColor(); + public static Color darkThemeSelectedColor = Themes.DEFAULT_ACCENT_COLOR.asColor(); private final Tool tool; @@ -86,7 +86,7 @@ private void setupIcons(Tool tool) { setIcon(toolIcon); VectorIcon selectedIcon = Themes.getActive().isDark() - ? toolIcon.copy(darkThemeSelectedColor) + ? toolIcon.copyWithColor(darkThemeSelectedColor) : toolIcon; setSelectedIcon(selectedIcon); } diff --git a/src/main/java/pixelitor/tools/gui/ToolSettingsPanel.java b/src/main/java/pixelitor/tools/gui/ToolSettingsPanel.java index 3f32cea22..a4146c4e4 100644 --- a/src/main/java/pixelitor/tools/gui/ToolSettingsPanel.java +++ b/src/main/java/pixelitor/tools/gui/ToolSettingsPanel.java @@ -1,5 +1,5 @@ /* - * Copyright 2022 Laszlo Balazs-Csiki and Contributors + * Copyright 2025 Laszlo Balazs-Csiki and Contributors * * This file is part of Pixelitor. Pixelitor is free software: you * can redistribute it and/or modify it under the terms of the GNU @@ -28,8 +28,6 @@ import java.awt.event.ActionListener; import java.util.function.Consumer; -import static javax.swing.SwingConstants.VERTICAL; - /** * The upper horizontal panel with the settings of the active tool. */ @@ -39,7 +37,7 @@ public ToolSettingsPanel() { } public void addSeparator() { - JSeparator separator = new JSeparator(VERTICAL); + JSeparator separator = new JSeparator(SwingConstants.VERTICAL); separator.setPreferredSize(new Dimension( separator.getPreferredSize().width, 26)); diff --git a/src/main/java/pixelitor/utils/AppPreferences.java b/src/main/java/pixelitor/utils/AppPreferences.java index 0fc3ac4e7..e5db46c85 100644 --- a/src/main/java/pixelitor/utils/AppPreferences.java +++ b/src/main/java/pixelitor/utils/AppPreferences.java @@ -51,10 +51,6 @@ import java.util.Locale; import java.util.prefs.Preferences; -import static javax.swing.SwingConstants.BOTTOM; -import static javax.swing.SwingConstants.LEFT; -import static javax.swing.SwingConstants.RIGHT; -import static javax.swing.SwingConstants.TOP; import static pixelitor.gui.ImageArea.Mode.FRAMES; import static pixelitor.gui.ImageArea.Mode.TABS; import static pixelitor.menus.file.RecentFilesMenu.MAX_RECENT_FILES; @@ -461,16 +457,16 @@ public static ImageAreaConfig loadDesktopMode() { } else { // return TOP tab placement so that if the user // changes the UI via preferences, this will be set - return new ImageAreaConfig(FRAMES, TOP); + return new ImageAreaConfig(FRAMES, SwingConstants.TOP); } } private static ImageAreaConfig loadSavedTabsInfo(String value) { int tabPlacement = switch (value) { - case "TabsL" -> LEFT; - case "TabsR" -> RIGHT; - case "TabsB" -> BOTTOM; - default -> TOP; + case "TabsL" -> SwingConstants.LEFT; + case "TabsR" -> SwingConstants.RIGHT; + case "TabsB" -> SwingConstants.BOTTOM; + default -> SwingConstants.TOP; }; return new ImageAreaConfig(TABS, tabPlacement); } @@ -483,10 +479,10 @@ private static void saveDesktopMode() { } else { int tabPlacement = ImageArea.getTabPlacement(); savedString = switch (tabPlacement) { - case TOP -> "TabsT"; - case LEFT -> "TabsL"; - case RIGHT -> "TabsR"; - case BOTTOM -> "TabsB"; + case SwingConstants.TOP -> "TabsT"; + case SwingConstants.LEFT -> "TabsL"; + case SwingConstants.RIGHT -> "TabsR"; + case SwingConstants.BOTTOM -> "TabsB"; default -> throw new IllegalStateException("tabPlacement = " + tabPlacement); }; } diff --git a/src/main/java/pixelitor/utils/Shapes.java b/src/main/java/pixelitor/utils/Shapes.java index e35159e1e..b3c860cf9 100644 --- a/src/main/java/pixelitor/utils/Shapes.java +++ b/src/main/java/pixelitor/utils/Shapes.java @@ -1,5 +1,5 @@ /* - * Copyright 2024 Laszlo Balazs-Csiki and Contributors + * Copyright 2025 Laszlo Balazs-Csiki and Contributors * * This file is part of Pixelitor. Pixelitor is free software: you * can redistribute it and/or modify it under the terms of the GNU @@ -2140,4 +2140,35 @@ public static List getAnchorPoints(Shape shape) { } return points; } + + public static double calcPathLength(Path2D path) { + double pathLength = 0; + double[] points = new double[6]; + PathIterator pathIt = new FlatteningPathIterator(path.getPathIterator(null), 1); + double moveX = 0, moveY = 0; + double lastX = 0, lastY = 0; + + while (!pathIt.isDone()) { + int type = pathIt.currentSegment(points); + switch (type) { + case SEG_MOVETO: + moveX = lastX = points[0]; + moveY = lastY = points[1]; + break; + case SEG_CLOSE: + points[0] = moveX; + points[1] = moveY; + // fall through + case SEG_LINETO: + double dx = points[0] - lastX; + double dy = points[1] - lastY; + pathLength += Math.sqrt(dx * dx + dy * dy); + lastX = points[0]; + lastY = points[1]; + break; + } + pathIt.next(); + } + return pathLength; + } } diff --git a/src/main/java/pixelitor/utils/debug/Debug.java b/src/main/java/pixelitor/utils/debug/Debug.java index 2da28f596..319351130 100644 --- a/src/main/java/pixelitor/utils/debug/Debug.java +++ b/src/main/java/pixelitor/utils/debug/Debug.java @@ -26,6 +26,7 @@ import pixelitor.filters.util.Filters; import pixelitor.gui.PixelitorWindow; import pixelitor.gui.View; +import pixelitor.gui.utils.AlignmentSelector; import pixelitor.gui.utils.DialogBuilder; import pixelitor.gui.utils.GUIUtils; import pixelitor.io.FileUtils; @@ -552,4 +553,13 @@ private static String debugComponent(Component c) { return descr; } + + public static String mlpAlignmentToString(int alignment) { + return switch (alignment) { + case AlignmentSelector.LEFT -> "left"; + case AlignmentSelector.CENTER -> "center"; + case AlignmentSelector.RIGHT -> "right"; + default -> throw new IllegalStateException("Unexpected value: " + alignment); + }; + } } diff --git a/src/main/java/pixelitor/utils/test/SplashImageCreator.java b/src/main/java/pixelitor/utils/test/SplashImageCreator.java index 43d23a999..dd42c8fc2 100644 --- a/src/main/java/pixelitor/utils/test/SplashImageCreator.java +++ b/src/main/java/pixelitor/utils/test/SplashImageCreator.java @@ -1,5 +1,5 @@ /* - * Copyright 2024 Laszlo Balazs-Csiki and Contributors + * Copyright 2025 Laszlo Balazs-Csiki and Contributors * * This file is part of Pixelitor. Pixelitor is free software: you * can redistribute it and/or modify it under the terms of the GNU @@ -30,6 +30,7 @@ import pixelitor.filters.AbstractLights; import pixelitor.filters.painters.AreaEffects; import pixelitor.filters.painters.TextSettings; +import pixelitor.gui.utils.AlignmentSelector; import pixelitor.io.Dirs; import pixelitor.io.FileFormat; import pixelitor.io.FileUtils; @@ -186,7 +187,7 @@ private static void addTextLayer(Composition comp, String text, Font font, int translationY) { AreaEffects effects = createDropShadowEffect(); TextSettings settings = new TextSettings(text, font, WHITE, effects, - HorizontalAlignment.CENTER, VerticalAlignment.CENTER, + HorizontalAlignment.CENTER, VerticalAlignment.CENTER, AlignmentSelector.CENTER, false, 0, 1.0, 1.0, 1.0, 0.0, 0.0, null); addNewTextLayer(comp, text, settings, translationY); }