diff --git a/diylc/diylc-core/src/main/java/org/diylc/common/EventType.java b/diylc/diylc-core/src/main/java/org/diylc/common/EventType.java index cfdcb2702..22e61de2b 100644 --- a/diylc/diylc-core/src/main/java/org/diylc/common/EventType.java +++ b/diylc/diylc-core/src/main/java/org/diylc/common/EventType.java @@ -21,7 +21,6 @@ */ package org.diylc.common; -import java.awt.Rectangle; import java.awt.geom.Point2D; import org.diylc.core.Project; diff --git a/diylc/diylc-core/src/main/java/org/diylc/common/IPlugInPort.java b/diylc/diylc-core/src/main/java/org/diylc/common/IPlugInPort.java index 53b55416a..cdb94cf87 100644 --- a/diylc/diylc-core/src/main/java/org/diylc/common/IPlugInPort.java +++ b/diylc/diylc-core/src/main/java/org/diylc/common/IPlugInPort.java @@ -41,6 +41,7 @@ import org.diylc.core.IDIYComponent; import org.diylc.core.Project; import org.diylc.core.Template; import org.diylc.core.Theme; +import org.diylc.core.measures.ResizeDimensions; import org.diylc.core.measures.Size; import org.diylc.filter.IComponentFilter; import org.diylc.test.ITestProcessor; @@ -49,9 +50,9 @@ import org.diylc.test.ITestProcessor; * Interface for communication between plug-ins and the application. Plug-ins may acquire * information or make changes through this interface. Application events are dispatched to plug-ins * through {@link MessageDispatcher} - * + * * @author Branislav Stojkovic - * + * * @see IPlugIn * @see MessageDispatcher * @see IMessageListener @@ -86,18 +87,19 @@ public interface IPlugInPort extends ISelectionProcessor, IMouseProcessor, IKeyP public static final String RULER_IN_SUBDIVISION_KEY = "rulerInSubdivision"; public static final String CACHING_ENABLED_KEY = "cachingEnabled"; public static final String FONT_SCALE_FACTOR_KEY = "fontScaleFactor"; - + public static final String SHOW_RESIZE_DIMENSIONS_TOOLTIP_KEY = "showResizeDimensionsTooltip"; + public static final String RULER_IN_SUBDIVISION_2 = "base of 2"; public static final String RULER_IN_SUBDIVISION_10 = "base of 10"; public static final String RULER_IN_SUBDIVISION_DEFAULT = RULER_IN_SUBDIVISION_10; - + public static final String SNAP_TO_NONE = "None"; public static final String SNAP_TO_GRID = "Grid"; public static final String SNAP_TO_COMPONENTS = "Components"; public static final String SNAP_TO_DEFAULT = SNAP_TO_GRID; - + public static final String LOCKED_ALPHA = "lockedAlpha"; - + public static final String LANGUAGE = "language"; public static final String LANGUAGE_DEFAULT = "English"; @@ -110,25 +112,25 @@ public interface IPlugInPort extends ISelectionProcessor, IMouseProcessor, IKeyP public static final int NOBUTTON = 0; public static final int BUTTON1 = 1; public static final int BUTTON2 = 2; - public static final int BUTTON3 = 3; + public static final int BUTTON3 = 3; /** * Returns size of the canvas that takes project dimensions into account as well as zoom level. * Each dimension is calculated as the product of the actual size and the number of pixels per * unit. If useZoom is set to true, the result is scaled by zoom factor. - * + * * @param useZoom * @param includeExtraSpace - * + * * @return canvas dimensions */ Dimension getCanvasDimensions(boolean useZoom, boolean includeExtraSpace); /** * Returns an instance of {@link Cursor} that should be used at the specified location. - * + * * Note: point coordinates are scaled for zoom factor. - * + * * @param point * @param ctrlDown * @param shiftDown @@ -139,14 +141,14 @@ public interface IPlugInPort extends ISelectionProcessor, IMouseProcessor, IKeyP /** * Returns an instance of currently loaded project. - * + * * @return */ Project getCurrentProject(); /** * Loads specified {@link Project}. - * + * * @param project * @param freshStart * @param filename file name when loading from a file @@ -160,22 +162,22 @@ public interface IPlugInPort extends ISelectionProcessor, IMouseProcessor, IKeyP /** * Loads a project from the specified file. - * + * * @param fileName */ void loadProjectFromFile(String fileName); /** * Saves the current project into the specified file. - * + * * @param fileName * @param isBackup */ void saveProjectToFile(String fileName, boolean isBackup); - + /** * Exports the current project into one or more files - one per layer and potentially drill layer. - * + * * @param fileNameBase * @param g2d */ @@ -193,7 +195,7 @@ public interface IPlugInPort extends ISelectionProcessor, IMouseProcessor, IKeyP /** * Shows a user dialog if there are changes to confirm that it's safe to proceed. - * + * * @return true, if file actions (new, open, close) can be taken */ boolean allowFileAction(); @@ -202,7 +204,7 @@ public interface IPlugInPort extends ISelectionProcessor, IMouseProcessor, IKeyP * Returns all available {@link ComponentType}s classified by category. Result is a {@link Map} * between category name to a {@link List} of all {@link ComponentType}s that share that category * name. - * + * * @return */ Map> getComponentTypes(); @@ -210,7 +212,7 @@ public interface IPlugInPort extends ISelectionProcessor, IMouseProcessor, IKeyP /** * Draws project on the provided {@link Graphics2D}. If the provided filter is not null, it will * be used to filter the components that are shown. - * + * * @param g2d * @param drawOptions specific drawing options * @param filter @@ -224,35 +226,35 @@ public interface IPlugInPort extends ISelectionProcessor, IMouseProcessor, IKeyP /** * Returns current zoom level where zoomLevel = 1.0d means 100%. - * + * * @return current zoom level */ double getZoomLevel(); /** * Changes current zoom level where zoomLevel = 1.0d means 100% - * + * * @param zoomLevel new zoom leve */ void setZoomLevel(double zoomLevel); /** * Returns current version number. - * + * * @return */ VersionNumber getCurrentVersionNumber(); - + /** - * Returns few most recent versions. - * + * Returns few most recent versions. + * * @return */ List getRecentUpdates(); /** * Adds a list of components to the project. - * + * * @param componentTransferable * @param autoGroup * @param assignNewNames @@ -271,7 +273,7 @@ public interface IPlugInPort extends ISelectionProcessor, IMouseProcessor, IKeyP /** * Sets default value for the specified property name for currently selected component types. - * + * * @param propertyName display name for property * @param value new default value, must not be null */ @@ -279,7 +281,7 @@ public interface IPlugInPort extends ISelectionProcessor, IMouseProcessor, IKeyP /** * Sets default value for the specified property name for projects. - * + * * @param clazz class to set defaults to * @param propertyName display name for property * @param value new default value, must not be null @@ -293,6 +295,15 @@ public interface IPlugInPort extends ISelectionProcessor, IMouseProcessor, IKeyP */ List getMutualSelectionProperties(); + /** + * Available only during resizing, returns the dimensions of the component being resized. + * Otherwise returns null. + * + * @return ResizeDimensions with width, height (in mm or in depending on configuration), + * and length (if component implements IHaveLength), or null if not resizing + */ + ResizeDimensions getResizeDimensions(); + void editSelection(); /** diff --git a/diylc/diylc-core/src/main/java/org/diylc/components/AbstractCurvedComponent.java b/diylc/diylc-core/src/main/java/org/diylc/components/AbstractCurvedComponent.java index 8a3b80630..1a1fbb25b 100644 --- a/diylc/diylc-core/src/main/java/org/diylc/components/AbstractCurvedComponent.java +++ b/diylc/diylc-core/src/main/java/org/diylc/components/AbstractCurvedComponent.java @@ -37,16 +37,14 @@ import com.thoughtworks.xstream.annotations.XStreamOmitField; import org.diylc.common.IPlugInPort; import org.diylc.common.LineStyle; import org.diylc.common.ObjectCache; -import org.diylc.core.ComponentState; -import org.diylc.core.IDrawingObserver; -import org.diylc.core.Project; -import org.diylc.core.VisibilityPolicy; +import org.diylc.core.*; import org.diylc.core.annotations.EditableProperty; import org.diylc.core.measures.Size; import org.diylc.core.measures.SizeUnit; import org.diylc.utils.Constants; -public abstract class AbstractCurvedComponent extends AbstractTransparentComponent { +public abstract class AbstractCurvedComponent extends AbstractTransparentComponent implements + IHaveLength { private static final long serialVersionUID = 1L; @@ -337,8 +335,8 @@ public abstract class AbstractCurvedComponent extends AbstractTransparentComp // } } - @EditableProperty - public Size getLength() { + @EditableProperty(name = "Length") + public Size calculateLength() { double d = 0; try { Point2D[] p = getControlPoints(); diff --git a/diylc/diylc-core/src/main/java/org/diylc/core/IHaveLength.java b/diylc/diylc-core/src/main/java/org/diylc/core/IHaveLength.java new file mode 100644 index 000000000..07372b766 --- /dev/null +++ b/diylc/diylc-core/src/main/java/org/diylc/core/IHaveLength.java @@ -0,0 +1,29 @@ +/* + + DIY Layout Creator (DIYLC). + Copyright (c) 2009-2025 held jointly by the individual authors. + + This file is part of DIYLC. + + DIYLC 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. + + DIYLC 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 DIYLC. If not, see . + +*/ +package org.diylc.core; + +import org.diylc.core.measures.Size; + +public interface IHaveLength { + + Size calculateLength(); +} diff --git a/diylc/diylc-core/src/main/java/org/diylc/core/measures/ResizeDimensions.java b/diylc/diylc-core/src/main/java/org/diylc/core/measures/ResizeDimensions.java new file mode 100644 index 000000000..8ca5e488c --- /dev/null +++ b/diylc/diylc-core/src/main/java/org/diylc/core/measures/ResizeDimensions.java @@ -0,0 +1,33 @@ +/* + * + * DIY Layout Creator (DIYLC). + * Copyright (c) 2009-2025 held jointly by the individual authors. + * + * This file is part of DIYLC. + * + * DIYLC 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. + * + * DIYLC 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 DIYLC. If not, see . + * + */ +package org.diylc.core.measures; + +/** + * Record containing resize dimensions for a component being resized. + * + * @param width the width of the component (in mm or in, depending on configuration) + * @param height the height of the component (in mm or in, depending on configuration) + * @param length the length of the component (if it implements IHaveLength), null otherwise + */ +public record ResizeDimensions(Size width, Size height, Size length) { +} + diff --git a/diylc/diylc-core/src/main/java/org/diylc/presenter/Presenter.java b/diylc/diylc-core/src/main/java/org/diylc/presenter/Presenter.java index 7a16225f6..6da611fda 100644 --- a/diylc/diylc-core/src/main/java/org/diylc/presenter/Presenter.java +++ b/diylc/diylc-core/src/main/java/org/diylc/presenter/Presenter.java @@ -59,16 +59,10 @@ import com.thoughtworks.xstream.security.AnyTypePermission; import org.diylc.clipboard.ComponentTransferable; import org.diylc.common.*; -import org.diylc.core.ExpansionMode; -import org.diylc.core.IDIYComponent; -import org.diylc.core.IDatasheetSupport; -import org.diylc.core.IView; -import org.diylc.core.Project; -import org.diylc.core.Template; -import org.diylc.core.Theme; -import org.diylc.core.VisibilityPolicy; +import org.diylc.core.*; import org.diylc.core.annotations.IAutoCreator; import org.diylc.core.gerber.GerberExporter; +import org.diylc.core.measures.ResizeDimensions; import org.diylc.core.measures.Size; import org.diylc.core.measures.SizeUnit; import org.diylc.filter.IComponentFilter; @@ -928,29 +922,36 @@ public class Presenter implements IPlugInPort { messageDispatcher.dispatchMessage(EventType.REPAINT); } - @Override public Rectangle2D getSelectionBounds(boolean applyZoom) { - if (selectedComponents == null || selectedComponents.isEmpty()) + return getComponentBounds(selectedComponents, applyZoom); + } + + private Rectangle2D getComponentBounds(Collection> components, boolean applyZoom) { + if (components == null || components.isEmpty()) return null; int minX = Integer.MAX_VALUE; int maxX = Integer.MIN_VALUE; int minY = Integer.MAX_VALUE; int maxY = Integer.MIN_VALUE; - for (IDIYComponent c : selectedComponents) { + + for (IDIYComponent c : components) { + ComponentArea compArea = drawingManager.getComponentArea(c); - if (compArea != null && compArea.getOutlineArea() != null) { - Rectangle rect = compArea.getOutlineArea().getBounds(); - if (rect.x < minX) - minX = rect.x; - if (rect.x + rect.width > maxX) - maxX = rect.x + rect.width; - if (rect.y < minY) - minY = rect.y; - if (rect.y + rect.height > maxY) - maxY = rect.y + rect.height; - } else if (currentProject.getComponents().contains(c)) + if (compArea == null || compArea.getOutlineArea() == null) { LOG.debug("Area is null for " + c.getName() + " of type " + c.getClass().getName()); + return null; + } + + Rectangle rect = compArea.getOutlineArea().getBounds(); + if (rect.x < minX) + minX = rect.x; + if (rect.x + rect.width > maxX) + maxX = rect.x + rect.width; + if (rect.y < minY) + minY = rect.y; + if (rect.y + rect.height > maxY) + maxY = rect.y + rect.height; } if (configManager.readBoolean(EXTRA_SPACE_KEY, true)) { @@ -1019,8 +1020,9 @@ public class Presenter implements IPlugInPort { } updateSelection(matching); messageDispatcher.dispatchMessage(EventType.REPAINT, true); - if (matching.size() > 0) + if (!matching.isEmpty()) { messageDispatcher.dispatchMessage(EventType.SCROLL_TO, getSelectionBounds(true)); + } } @Override @@ -1237,11 +1239,14 @@ public class Presenter implements IPlugInPort { Map params = new HashMap(); params.put("point", point); test.addStep(DIYTest.DRAG_OVER, params); - } - + } + if (point == null || operationMode == OperationMode.HIGHLIGHT_CONNECTED_AREAS) { return false; } + + boolean resizeInProgress = false; + Point2D scaledPoint = scalePoint(point); if (controlPointMap != null && !controlPointMap.isEmpty()) { // We're dragging control point(s). @@ -1252,6 +1257,13 @@ public class Presenter implements IPlugInPort { if (actualD == null) return true; + // if there's at least one component with only one control point being dragged it means that we are resizing + if (controlPointMap.size() == 1 && controlPointMap.entrySet().stream() + .anyMatch(entry -> entry.getValue().size() == 1 + && entry.getKey().getControlPointCount() > 1)) { + resizeInProgress = true; + } + previousDragPoint.setLocation(previousDragPoint.getX() + actualD.getX(), previousDragPoint.getY() + actualD.getY()); } else if (selectedComponents.isEmpty() && instantiationManager.getComponentTypeSlot() == null && previousDragPoint != null) { @@ -1269,10 +1281,50 @@ public class Presenter implements IPlugInPort { this.previousScaledPoint = scalePoint(point); instantiationManager.updateSingleClick(previousScaledPoint, isSnapToGrid(), currentProject.getGridSpacing()); } - messageDispatcher.dispatchMessage(EventType.REPAINT); + + messageDispatcher.dispatchMessage(EventType.REPAINT, false, resizeInProgress); return true; } + public ResizeDimensions getResizeDimensions() { + + if (controlPointMap == null || controlPointMap.isEmpty()) { + return null; + } + + // Find the component being resized (one with a single control point being dragged) + IDIYComponent resizingComponent = null; + for (Map.Entry, Set> entry : controlPointMap.entrySet()) { + if (entry.getValue().size() == 1 && entry.getKey().getControlPointCount() > 1) { + resizingComponent = entry.getKey(); + break; + } + } + + if (resizingComponent == null) { + return null; + } + + Rectangle2D componentBounds = getComponentBounds(controlPointMap.keySet(), false); + if (componentBounds == null) { + return null; + } + + // Convert pixel dimensions to Size (mm or in based on configuration) + boolean metric = configManager.readBoolean(METRIC_KEY, true); + SizeUnit unit = metric ? SizeUnit.mm : SizeUnit.in; + Size width = Size.fromPixels(componentBounds.getWidth(), unit); + Size height = Size.fromPixels(componentBounds.getHeight(), unit); + + // Get length if component implements IHaveLength + Size length = null; + if (resizingComponent instanceof IHaveLength iHaveLength) { + length = iHaveLength.calculateLength(); + } + + return new ResizeDimensions(width, height, length); + } + private Point2D moveComponents(Map, Set> controlPointMap, int dx, int dy, boolean snapToGrid, boolean snapToObjects) { // After we make the transfer and snap to grid, calculate actual dx // and dy. We'll use them to translate the previous drag point. @@ -2171,6 +2223,10 @@ public class Presenter implements IPlugInPort { } Rectangle2D rect = getSelectionBounds(false); + if (rect == null) { + return null; + } + double width = rect.getWidth(); double height = rect.getHeight(); diff --git a/diylc/diylc-core/src/main/resources/update.xml b/diylc/diylc-core/src/main/resources/update.xml index 5fedfa418..6aab193ed 100644 --- a/diylc/diylc-core/src/main/resources/update.xml +++ b/diylc/diylc-core/src/main/resources/update.xml @@ -484,5 +484,21 @@ https://github.com/bancika/diy-layout-creator/releases + + + 5 + 9 + 0 + + 2025-12-30 00:00:00.000 CET + + + + NEW_FEATURE + Added balloon tooltip next to cursor showing component width, height and length (where available) while resizing; can be disabled from Config menu + + + https://github.com/bancika/diy-layout-creator/releases + diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/AbstractLeadedComponent.java b/diylc/diylc-library/src/main/java/org/diylc/components/AbstractLeadedComponent.java index 7d23c65c1..f91468ae9 100644 --- a/diylc/diylc-library/src/main/java/org/diylc/components/AbstractLeadedComponent.java +++ b/diylc/diylc-library/src/main/java/org/diylc/components/AbstractLeadedComponent.java @@ -43,11 +43,7 @@ import org.diylc.common.IPlugInPort; import org.diylc.common.LineStyle; import org.diylc.common.ObjectCache; import org.diylc.common.VerticalAlignment; -import org.diylc.core.ComponentState; -import org.diylc.core.IDrawingObserver; -import org.diylc.core.Project; -import org.diylc.core.Theme; -import org.diylc.core.VisibilityPolicy; +import org.diylc.core.*; import org.diylc.core.annotations.EditableProperty; import org.diylc.core.annotations.PositiveNonZeroMeasureValidator; import org.diylc.core.measures.Size; @@ -62,7 +58,8 @@ import org.diylc.utils.Constants; * * @author Branislav Stojkovic */ -public abstract class AbstractLeadedComponent extends AbstractLabeledComponent { +public abstract class AbstractLeadedComponent extends AbstractLabeledComponent implements + IHaveLength { private static final long serialVersionUID = 1L; @@ -640,6 +637,32 @@ public abstract class AbstractLeadedComponent extends AbstractLabeledComponen this.length = length; } + @Override + public Size calculateLength() { + + try { + Point2D[] points = getPoints(); + if (points.length < 2) { + return null; + } + + // Calculate distance between ending points + double distanceInPixels = points[0].distance(points[1]); + + // Convert to Size with appropriate unit (mm or in based on configuration) + SizeUnit unit; + if (ConfigurationManager.getInstance().readBoolean(IPlugInPort.METRIC_KEY, true)) { + unit = SizeUnit.mm; + } else { + unit = SizeUnit.in; + } + + return Size.fromPixels(distanceInPixels, unit); + } catch (Exception e) { + return null; + } + } + @EditableProperty(name = "Width", defaultable = true, validatorClass = PositiveNonZeroMeasureValidator.class, sortOrder = 102) public Size getWidth() { return width; diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/boards/BlankBoard.java b/diylc/diylc-library/src/main/java/org/diylc/components/boards/BlankBoard.java index 89ceddbac..7ee0a538c 100644 --- a/diylc/diylc-library/src/main/java/org/diylc/components/boards/BlankBoard.java +++ b/diylc/diylc-library/src/main/java/org/diylc/components/boards/BlankBoard.java @@ -69,18 +69,18 @@ public class BlankBoard extends AbstractBoard { } g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1)); - if (componentState != ComponentState.DRAGGING) { - Composite oldComposite = g2d.getComposite(); - if (alpha < MAX_ALPHA) { - g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1f * alpha / MAX_ALPHA)); - } - g2d.setColor(boardColor); - if (getType() == Type.SQUARE) - g2d.fillRect((int)firstPoint.getX(), (int)firstPoint.getY(), (int)(finalSecondPoint.getX() - firstPoint.getX()), (int)(finalSecondPoint.getY() - firstPoint.getY())); - else - g2d.fillOval((int)firstPoint.getX(), (int)firstPoint.getY(), (int)(finalSecondPoint.getX() - firstPoint.getX()), (int)(finalSecondPoint.getY() - firstPoint.getY())); - g2d.setComposite(oldComposite); +// if (componentState != ComponentState.DRAGGING) { + Composite oldComposite = g2d.getComposite(); + if (alpha < MAX_ALPHA) { + g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1f * alpha / MAX_ALPHA)); } + g2d.setColor(boardColor); + if (getType() == Type.SQUARE) + g2d.fillRect((int)firstPoint.getX(), (int)firstPoint.getY(), (int)(finalSecondPoint.getX() - firstPoint.getX()), (int)(finalSecondPoint.getY() - firstPoint.getY())); + else + g2d.fillOval((int)firstPoint.getX(), (int)firstPoint.getY(), (int)(finalSecondPoint.getX() - firstPoint.getX()), (int)(finalSecondPoint.getY() - firstPoint.getY())); + g2d.setComposite(oldComposite); +// } // Do not track any changes that follow because the whole board has been // tracked so far. drawingObserver.stopTracking(); diff --git a/diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/canvas/CanvasPlugin.java b/diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/canvas/CanvasPlugin.java index cf6a32c9c..947d861b5 100644 --- a/diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/canvas/CanvasPlugin.java +++ b/diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/canvas/CanvasPlugin.java @@ -41,6 +41,7 @@ import org.apache.log4j.Logger; import org.diylc.appframework.miscutils.IConfigListener; import org.diylc.appframework.miscutils.IConfigurationManager; import org.diylc.appframework.miscutils.Utils; +import org.diylc.core.measures.ResizeDimensions; import org.diylc.swingframework.ruler.IRulerListener; import org.diylc.swingframework.ruler.Ruler.InchSubdivision; import org.diylc.swingframework.ruler.RulerScrollPane; @@ -53,7 +54,6 @@ import org.diylc.common.BadPositionException; import org.diylc.common.EventType; import org.diylc.common.IPlugIn; import org.diylc.common.IPlugInPort; -import org.diylc.core.IDIYComponent; import org.diylc.core.measures.Size; import org.diylc.core.measures.SizeUnit; import org.diylc.presenter.Presenter; @@ -68,6 +68,8 @@ public class CanvasPlugin implements IPlugIn{ private RulerScrollPane scrollPane; private CanvasPanel canvasPanel; private ComponentPopupMenu popupMenu; + private ResizeInfoTooltip resizeInfoTooltip; + private boolean resizingInProgress = false; private IPlugInPort plugInPort; private ISwingUI swingUI; @@ -193,6 +195,7 @@ public class CanvasPlugin implements IPlugIn{ public CanvasPanel getCanvasPanel() { if (canvasPanel == null) { canvasPanel = new CanvasPanel(plugInPort, configManager); + resizeInfoTooltip = new ResizeInfoTooltip(canvasPanel, configManager); canvasPanel.addMouseListener(new MouseAdapter() { private MouseEvent pressedEvent; @@ -277,6 +280,15 @@ public class CanvasPlugin implements IPlugIn{ e.isShiftDown(), e.isAltDown())); plugInPort.mouseMoved(e.getPoint(), Utils.isMac() ? e.isMetaDown() : e.isControlDown(), e.isShiftDown(), e.isAltDown()); + + // Update tooltip position if resizing is in progress and tooltip is enabled, otherwise hide it + if (resizeInfoTooltip != null) { + if (resizingInProgress && configManager.readBoolean(IPlugInPort.SHOW_RESIZE_DIMENSIONS_TOOLTIP_KEY, true)) { + resizeInfoTooltip.updatePosition(e.getPoint()); + } else { + resizeInfoTooltip.hideTooltip(); + } + } } }); @@ -505,11 +517,13 @@ public class CanvasPlugin implements IPlugIn{ canvasPanel.repaint(); // Refresh selection bounds after we're done with painting to ensure we have traced the // component areas + boolean panToSelection = params.length > 0 && Boolean.TRUE.equals(params[0]); + this.resizingInProgress = params.length > 1 && Boolean.TRUE.equals(params[1]); SwingUtilities.invokeLater(new Runnable() { @Override public void run() { - if (params.length > 0 && Boolean.TRUE.equals(params[0])) { + if (panToSelection) { Rectangle2D selectionBounds = plugInPort.getSelectionBounds(true); if (selectionBounds != null) { Rectangle visibleRect = scrollPane.getVisibleRect(); @@ -519,8 +533,28 @@ public class CanvasPlugin implements IPlugIn{ ); } } - if (configManager.readBoolean(IPlugInPort.SHOW_RULERS_KEY, true)) + if (configManager.readBoolean(IPlugInPort.SHOW_RULERS_KEY, true)) { scrollPane.setSelectionRectangle(plugInPort.getSelectionBounds(true)); + } + if (CanvasPlugin.this.resizingInProgress && getCanvasPanel().getMousePosition() != null) { + // Only show tooltip if enabled in configuration + if (configManager.readBoolean(IPlugInPort.SHOW_RESIZE_DIMENSIONS_TOOLTIP_KEY, true)) { + ResizeDimensions resizeDimensions = plugInPort.getResizeDimensions(); + if (resizeInfoTooltip != null) { + resizeInfoTooltip.update(resizeDimensions, getCanvasPanel().getMousePosition()); + } + } else { + // Hide tooltip if disabled + if (resizeInfoTooltip != null) { + resizeInfoTooltip.hideTooltip(); + } + } + } else { + // Hide tooltip when not resizing + if (resizeInfoTooltip != null) { + resizeInfoTooltip.hideTooltip(); + } + } } }); break; @@ -563,25 +597,4 @@ public class CanvasPlugin implements IPlugIn{ double extraSpace = CanvasPlugin.this.plugInPort.getExtraSpace(); getScrollPane().setZeroLocation(new Point2D.Double(extraSpace, extraSpace)); } - - class LockAction extends AbstractAction { - - private static final long serialVersionUID = 1L; - - private IDIYComponent component; - private boolean locked; - - public LockAction(IDIYComponent component, boolean locked) { - super(); - this.locked = locked; - this.component = component; - putValue(AbstractAction.NAME, component.getName()); - } - - @Override - public void actionPerformed(ActionEvent e) { - plugInPort.lockComponent(component, locked); - } - - } } diff --git a/diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/canvas/ResizeInfoTooltip.java b/diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/canvas/ResizeInfoTooltip.java new file mode 100644 index 000000000..6d55b224f --- /dev/null +++ b/diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/canvas/ResizeInfoTooltip.java @@ -0,0 +1,225 @@ +/* + * + * DIY Layout Creator (DIYLC). Copyright (c) 2009-2025 held jointly by the individual authors. + * + * This file is part of DIYLC. + * + * DIYLC 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. + * + * DIYLC 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 DIYLC. If not, see + * . + */ +package org.diylc.swing.plugins.canvas; + +import java.awt.Color; +import java.awt.Component; +import java.awt.Dimension; +import java.awt.Font; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.RenderingHints; +import java.awt.Window; +import java.text.DecimalFormat; +import javax.swing.BorderFactory; +import javax.swing.JLabel; +import javax.swing.JWindow; +import javax.swing.SwingUtilities; +import javax.swing.UIManager; +import org.diylc.appframework.miscutils.IConfigurationManager; +import org.diylc.core.measures.ResizeDimensions; +import org.diylc.core.measures.Size; + +/** + * A lightweight tooltip window that displays resize dimensions and tracks the mouse cursor. + */ +public class ResizeInfoTooltip { + + private static final int OFFSET_X = 16; + private static final int OFFSET_Y = 16; + private static final DecimalFormat SIZE_FORMAT = new DecimalFormat("0.00"); + + private JWindow tooltipWindow; + private JLabel tooltipLabel; + private Component parentComponent; + private ResizeDimensions currentDimensions; + + public ResizeInfoTooltip(Component parentComponent, IConfigurationManager configManager) { + this.parentComponent = parentComponent; + initializeTooltip(); + } + + private void initializeTooltip() { + Window parentWindow = SwingUtilities.getWindowAncestor(parentComponent); + if (parentWindow == null) { + return; + } + + tooltipWindow = new JWindow(parentWindow); + tooltipLabel = new JLabel() { + private static final long serialVersionUID = 1L; + + @Override + protected void paintComponent(Graphics g) { + Graphics2D g2d = (Graphics2D) g.create(); + g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + super.paintComponent(g2d); + g2d.dispose(); + } + }; + + // Use system tooltip font and colors + Font tooltipFont = UIManager.getFont("ToolTip.font"); + if (tooltipFont != null) { + tooltipLabel.setFont(tooltipFont); + } + Color tooltipBackground = UIManager.getColor("ToolTip.background"); + Color tooltipForeground = UIManager.getColor("ToolTip.foreground"); + Color tooltipBorderColor = UIManager.getColor("ToolTip.border"); + if (tooltipBorderColor == null) { + tooltipBorderColor = Color.GRAY; + } + + tooltipLabel.setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createLineBorder(tooltipBorderColor, 1), + BorderFactory.createEmptyBorder(4, 6, 4, 6))); + tooltipLabel.setOpaque(true); + tooltipLabel.setBackground(tooltipBackground != null ? tooltipBackground : new Color(255, 255, 220)); + tooltipLabel.setForeground(tooltipForeground != null ? tooltipForeground : Color.BLACK); + + tooltipWindow.add(tooltipLabel); + tooltipWindow.setFocusableWindowState(false); + tooltipWindow.setAlwaysOnTop(true); + } + + /** + * Updates the tooltip with new dimensions and mouse position. + */ + public void update(ResizeDimensions dimensions, Point mousePosition) { + this.currentDimensions = dimensions; + + if (dimensions == null || mousePosition == null) { + hideTooltip(); + return; + } + + if (tooltipWindow == null) { + initializeTooltip(); + if (tooltipWindow == null) { + return; + } + } + + // Format the dimensions + String text = formatDimensions(dimensions); + tooltipLabel.setText(text); + tooltipLabel.setPreferredSize(null); // Reset to calculate preferred size + tooltipWindow.pack(); + + // Position the tooltip near the mouse cursor + updateTooltipPosition(mousePosition); + tooltipWindow.setVisible(true); + } + + /** + * Updates the tooltip position based on the current mouse position. + */ + public void updatePosition(Point mousePosition) { + if (currentDimensions != null && mousePosition != null && tooltipWindow != null && tooltipWindow.isVisible()) { + updateTooltipPosition(mousePosition); + } + } + + private void updateTooltipPosition(Point mousePosition) { + if (tooltipWindow == null || parentComponent == null) { + return; + } + + // Convert mouse position to screen coordinates + Point screenPoint = new Point(mousePosition); + SwingUtilities.convertPointToScreen(screenPoint, parentComponent); + + // Position tooltip 16 pixels down and right from cursor + int x = screenPoint.x + OFFSET_X; + int y = screenPoint.y + OFFSET_Y; + + // Ensure tooltip stays on screen + Dimension tooltipSize = tooltipWindow.getSize(); + if (tooltipSize == null) { + tooltipWindow.pack(); + tooltipSize = tooltipWindow.getSize(); + } + + if (tooltipSize != null) { + java.awt.Toolkit toolkit = java.awt.Toolkit.getDefaultToolkit(); + Dimension screenSize = toolkit.getScreenSize(); + + // Adjust if tooltip would go off screen + if (x + tooltipSize.width > screenSize.width) { + x = screenPoint.x - tooltipSize.width - OFFSET_X; + } + if (y + tooltipSize.height > screenSize.height) { + y = screenPoint.y - tooltipSize.height - OFFSET_Y; + } + if (x < 0) { + x = 0; + } + if (y < 0) { + y = 0; + } + } + + tooltipWindow.setLocation(x, y); + } + + private String formatDimensions(ResizeDimensions dimensions) { + StringBuilder sb = new StringBuilder(""); + + // Format width + Size width = dimensions.width(); + String widthUnit = width.getUnit().toString(); + sb.append(String.format("W: %s %s", SIZE_FORMAT.format(width.getValue()), widthUnit)); + + // Format height + Size height = dimensions.height(); + String heightUnit = height.getUnit().toString(); + sb.append(String.format("
H: %s %s", SIZE_FORMAT.format(height.getValue()), heightUnit)); + + // Format length if available + Size length = dimensions.length(); + if (length != null) { + String lengthUnit = length.getUnit().toString(); + sb.append(String.format("
L: %s %s", SIZE_FORMAT.format(length.getValue()), lengthUnit)); + } + + sb.append(""); + return sb.toString(); + } + + /** + * Hides the tooltip. + */ + public void hideTooltip() { + if (tooltipWindow != null) { + tooltipWindow.setVisible(false); + } + currentDimensions = null; + } + + /** + * Disposes the tooltip window. + */ + public void dispose() { + if (tooltipWindow != null) { + tooltipWindow.dispose(); + tooltipWindow = null; + } + } +} + diff --git a/diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/config/ConfigPlugin.java b/diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/config/ConfigPlugin.java index 114cd1b4c..9bd3222cf 100644 --- a/diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/config/ConfigPlugin.java +++ b/diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/config/ConfigPlugin.java @@ -158,6 +158,10 @@ public class ConfigPlugin implements IPlugIn { ActionFactory.getInstance().createConfigAction(plugInPort, "Show Rulers", IPlugInPort.SHOW_RULERS_KEY, true), CONFIG_MENU); + swingUI.injectMenuAction( + ActionFactory.getInstance().createConfigAction(plugInPort, "Show Resize Dimensions", + IPlugInPort.SHOW_RESIZE_DIMENSIONS_TOOLTIP_KEY, true), CONFIG_MENU); + swingUI.injectSubmenu(SNAP_MENU, IconLoader.GraphEdgeDirected.getIcon(), CONFIG_MENU); swingUI.injectMenuAction( ActionFactory.getInstance().createToggleAction(IPlugInPort.SNAP_TO_NONE, IPlugInPort.SNAP_TO_KEY, SNAP_MENU, IPlugInPort.SNAP_TO_DEFAULT), diff --git a/diylc/diylc-swing/src/main/resources/lang/Deutsch.txt b/diylc/diylc-swing/src/main/resources/lang/Deutsch.txt index 78c188a4b..adc6cde0b 100644 --- a/diylc/diylc-swing/src/main/resources/lang/Deutsch.txt +++ b/diylc/diylc-swing/src/main/resources/lang/Deutsch.txt @@ -363,6 +363,7 @@ Shaft Up|Achse nach oben Shape|Form Shapes|Formen Show Grid|Raster anzeigen +Show Resize Dimensions|Größenänderungsdimensionen anzeigen Show Rulers|Lineale anzeigen Shroud|Wanne Shrouded|Abgeschirmt diff --git a/diylc/diylc-swing/src/main/resources/lang/English.txt b/diylc/diylc-swing/src/main/resources/lang/English.txt index 271f5b339..c14a7b124 100644 --- a/diylc/diylc-swing/src/main/resources/lang/English.txt +++ b/diylc/diylc-swing/src/main/resources/lang/English.txt @@ -352,6 +352,7 @@ Shape Shapes Show Bracket Show Grid +Show Resize Dimensions Show Rulers Shroud Shrouded diff --git a/diylc/diylc-swing/src/main/resources/lang/Español.txt b/diylc/diylc-swing/src/main/resources/lang/Español.txt index 45ff7d88b..dcda6accf 100644 --- a/diylc/diylc-swing/src/main/resources/lang/Español.txt +++ b/diylc/diylc-swing/src/main/resources/lang/Español.txt @@ -351,6 +351,7 @@ Shaft Up|Eje Arriba Shape|Forma Shapes|Formas Show Grid|Mostrar Cuadrícula +Show Resize Dimensions|Mostrar Dimensiones de Redimensionamiento Show Rulers|Mostrar Reglas Shroud|Envoltura Shrouded|Con Envoltura diff --git a/diylc/diylc-swing/src/main/resources/lang/Français.txt b/diylc/diylc-swing/src/main/resources/lang/Français.txt index 3c25e3d00..47e68b6d8 100644 --- a/diylc/diylc-swing/src/main/resources/lang/Français.txt +++ b/diylc/diylc-swing/src/main/resources/lang/Français.txt @@ -348,6 +348,7 @@ Shaft Up|Tige vers le haut Shape|Forme Shapes|Formes Show Grid|Afficher la grille +Show Resize Dimensions|Afficher les dimensions de redimensionnement Show Rulers|Afficher les règles Shroud|Enveloppe Shrouded|Enveloppé diff --git a/diylc/diylc-swing/src/main/resources/lang/Italiano.txt b/diylc/diylc-swing/src/main/resources/lang/Italiano.txt index b108068c6..7134450e9 100644 --- a/diylc/diylc-swing/src/main/resources/lang/Italiano.txt +++ b/diylc/diylc-swing/src/main/resources/lang/Italiano.txt @@ -363,6 +363,7 @@ Shaft Up|Shaft Up Shape|forma Shapes|forme Show Grid|Mostra griglia +Show Resize Dimensions|Mostra dimensioni ridimensionamento Show Rulers|Mostra righelli Shroud|Copri Shrouded|Coperto diff --git a/diylc/diylc-swing/src/main/resources/lang/Nederlands.txt b/diylc/diylc-swing/src/main/resources/lang/Nederlands.txt index 5c0425f66..35bd6fb6a 100644 --- a/diylc/diylc-swing/src/main/resources/lang/Nederlands.txt +++ b/diylc/diylc-swing/src/main/resources/lang/Nederlands.txt @@ -363,6 +363,7 @@ Shaft Up|As omhoog Shape|Vorm Shapes|Vormen Show Grid|Raster weergeven +Show Resize Dimensions|Grootte-aanpassingsafmetingen weergeven Show Rulers|Linialen weergeven Shroud|Beschermrand Shrouded|Afgeschermd diff --git a/diylc/diylc-swing/src/main/resources/lang/Português.txt b/diylc/diylc-swing/src/main/resources/lang/Português.txt index 76ae999d5..fc4140afe 100644 --- a/diylc/diylc-swing/src/main/resources/lang/Português.txt +++ b/diylc/diylc-swing/src/main/resources/lang/Português.txt @@ -351,6 +351,7 @@ Shaft Up|Jogar para cima Shape|Forma Shapes|Formas Show Grid|Mostrar Grid +Show Resize Dimensions|Mostrar Dimensões de Redimensionamento Show Rulers|Mostrar Réguas Shroud|Envoltura Shrouded|Envolto diff --git a/diylc/diylc-swing/src/main/resources/lang/Srpski.txt b/diylc/diylc-swing/src/main/resources/lang/Srpski.txt index 37e095056..7cb6b5456 100644 --- a/diylc/diylc-swing/src/main/resources/lang/Srpski.txt +++ b/diylc/diylc-swing/src/main/resources/lang/Srpski.txt @@ -344,6 +344,7 @@ Shaft Up|Osovina Gore Shape|Oblik Shapes|Oblici Show Grid|Prikaz Rešetke +Show Resize Dimensions|Prikaz Dimenzija Promene Veličine Show Rulers|Prikaz Lenjira Shroud|Kućište Shrouded|Sa Kućištem diff --git a/diylc/diylc-swing/src/main/resources/lang/हिंदी.txt b/diylc/diylc-swing/src/main/resources/lang/हिंदी.txt index 8d2b0bc0a..29fdc2bbe 100644 --- a/diylc/diylc-swing/src/main/resources/lang/हिंदी.txt +++ b/diylc/diylc-swing/src/main/resources/lang/हिंदी.txt @@ -352,6 +352,7 @@ Shape|आकार Shapes|आकार Show Bracket|ब्रैकेट दिखाएं Show Grid|ग्रिड दिखाएं +Show Resize Dimensions|आकार बदलने के आयाम दिखाएं Show Rulers|रूलर दिखाएं Shroud|श्राउड Shrouded|श्राउडेड diff --git a/diylc/diylc-swing/src/main/resources/lang/简体中文.txt b/diylc/diylc-swing/src/main/resources/lang/简体中文.txt index afd6ad673..228e616d5 100644 --- a/diylc/diylc-swing/src/main/resources/lang/简体中文.txt +++ b/diylc/diylc-swing/src/main/resources/lang/简体中文.txt @@ -352,6 +352,7 @@ Shape|形状 Shapes|形状 Show Bracket|显示支架 Show Grid|显示网格 +Show Resize Dimensions|显示调整大小尺寸 Show Rulers|显示标尺 Shroud|护罩 Shrouded|带护罩