mirror of
https://github.com/bancika/diy-layout-creator
synced 2026-11-13 02:32:09 +10:00
Added 'Value Notation' to the Config menu
This commit is contained in:
@@ -90,6 +90,7 @@ public interface IPlugInPort extends ISelectionProcessor, IMouseProcessor, IKeyP
|
|||||||
public static final String FONT_SCALE_FACTOR_KEY = "fontScaleFactor";
|
public static final String FONT_SCALE_FACTOR_KEY = "fontScaleFactor";
|
||||||
public static final String SHOW_RESIZE_DIMENSIONS_TOOLTIP_KEY = "showResizeDimensionsTooltip";
|
public static final String SHOW_RESIZE_DIMENSIONS_TOOLTIP_KEY = "showResizeDimensionsTooltip";
|
||||||
public static final String AUTO_UNIT_CONVERSION_KEY = "autoUnitConversion";
|
public static final String AUTO_UNIT_CONVERSION_KEY = "autoUnitConversion";
|
||||||
|
public static final String VALUE_NOTATION_KEY = "valueNotation";
|
||||||
|
|
||||||
|
|
||||||
public static final String RULER_IN_SUBDIVISION_2 = "base of 2";
|
public static final String RULER_IN_SUBDIVISION_2 = "base of 2";
|
||||||
@@ -101,6 +102,10 @@ public interface IPlugInPort extends ISelectionProcessor, IMouseProcessor, IKeyP
|
|||||||
public static final String SNAP_TO_COMPONENTS = "Components";
|
public static final String SNAP_TO_COMPONENTS = "Components";
|
||||||
public static final String SNAP_TO_DEFAULT = SNAP_TO_GRID;
|
public static final String SNAP_TO_DEFAULT = SNAP_TO_GRID;
|
||||||
|
|
||||||
|
public static final String VALUE_NOTATION_DECIMAL = "Decimal (4.7K, 0.22uF)";
|
||||||
|
public static final String VALUE_NOTATION_RKM = "RKM (4K7, 0u22)";
|
||||||
|
public static final String VALUE_NOTATION_DEFAULT = VALUE_NOTATION_DECIMAL;
|
||||||
|
|
||||||
public static final String SHOW_NODE_NAME_TOOLTIPS_KEY = "showNodeNameTooltips";
|
public static final String SHOW_NODE_NAME_TOOLTIPS_KEY = "showNodeNameTooltips";
|
||||||
|
|
||||||
public static final String LOCKED_ALPHA = "lockedAlpha";
|
public static final String LOCKED_ALPHA = "lockedAlpha";
|
||||||
|
|||||||
@@ -41,14 +41,16 @@ public class Capacitance extends AbstractMeasure<CapacitanceUnit> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static Capacitance parseCapacitance(String value) {
|
public static Capacitance parseCapacitance(String value) {
|
||||||
value = value.replace("*", "");
|
ValueNotation.ParsedMeasure<CapacitanceUnit> parsed;
|
||||||
for (CapacitanceUnit unit : CapacitanceUnit.values()) {
|
try {
|
||||||
if (value.toLowerCase().endsWith(unit.toString().toLowerCase())) {
|
parsed = ValueNotation.parse(value, CapacitanceUnit::fromSymbol);
|
||||||
value = value.substring(0, value.length() - unit.toString().length()).trim();
|
} catch (IllegalArgumentException e) {
|
||||||
return new Capacitance(parse(value), unit);
|
throw new IllegalArgumentException("Could not parse capacitance: " + value, e);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
throw new IllegalArgumentException("Could not parse capacitance: " + value);
|
if (parsed.unit() == null) {
|
||||||
|
throw new IllegalArgumentException("Could not parse capacitance: " + value);
|
||||||
|
}
|
||||||
|
return new Capacitance(parsed.value(), parsed.unit());
|
||||||
}
|
}
|
||||||
|
|
||||||
public Double getNormalizedValue() {
|
public Double getNormalizedValue() {
|
||||||
@@ -56,4 +58,12 @@ public class Capacitance extends AbstractMeasure<CapacitanceUnit> {
|
|||||||
return null;
|
return null;
|
||||||
return value * unit.getFactor();
|
return value * unit.getFactor();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
if (ValueNotation.getCurrent() == ValueNotation.RKM && value != null && unit != null) {
|
||||||
|
return ValueNotation.formatRkm(value, unit.getRkmSymbol());
|
||||||
|
}
|
||||||
|
return super.toString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,14 +23,16 @@ package org.diylc.core.measures;
|
|||||||
|
|
||||||
public enum CapacitanceUnit implements Unit {
|
public enum CapacitanceUnit implements Unit {
|
||||||
|
|
||||||
pF(1e-6, "pF"), nF(1e-3, "nF"), uF(1, "uF"), mF(1e3, "mF"), F(1e6, "F");
|
pF(1e-6, "pF", "p"), nF(1e-3, "nF", "n"), uF(1, "uF", "u"), mF(1e3, "mF", "m"), F(1e6, "F", "F");
|
||||||
|
|
||||||
double factor;
|
double factor;
|
||||||
String display;
|
String display;
|
||||||
|
String rkmSymbol;
|
||||||
|
|
||||||
private CapacitanceUnit(double factor, String display) {
|
private CapacitanceUnit(double factor, String display, String rkmSymbol) {
|
||||||
this.factor = factor;
|
this.factor = factor;
|
||||||
this.display = display;
|
this.display = display;
|
||||||
|
this.rkmSymbol = rkmSymbol;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -38,8 +40,49 @@ public enum CapacitanceUnit implements Unit {
|
|||||||
return factor;
|
return factor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getRkmSymbol() {
|
||||||
|
return rkmSymbol;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return display;
|
return display;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the unit written after a capacitance value, e.g. "nF", "n", "u", "\u00B5F" or "F".
|
||||||
|
* Case does not matter when the trailing "F" is present, which is how values have always been
|
||||||
|
* parsed. Without it, "M" is rejected so that it is never mistaken for mega.
|
||||||
|
*
|
||||||
|
* @param symbol unit text with no surrounding whitespace
|
||||||
|
* @return the unit, or null when the text is not a capacitance unit
|
||||||
|
*/
|
||||||
|
public static CapacitanceUnit fromSymbol(String symbol) {
|
||||||
|
if (symbol.equalsIgnoreCase("F")) {
|
||||||
|
return F;
|
||||||
|
}
|
||||||
|
String prefix = symbol;
|
||||||
|
if (symbol.length() == 2 && (symbol.endsWith("F") || symbol.endsWith("f"))) {
|
||||||
|
prefix = symbol.substring(0, 1).toLowerCase();
|
||||||
|
} else if (symbol.equals("M")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
switch (prefix) {
|
||||||
|
case "p":
|
||||||
|
case "P":
|
||||||
|
return pF;
|
||||||
|
case "n":
|
||||||
|
case "N":
|
||||||
|
return nF;
|
||||||
|
case "u":
|
||||||
|
case "U":
|
||||||
|
case "\u00B5":
|
||||||
|
case "\u03BC":
|
||||||
|
return uF;
|
||||||
|
case "m":
|
||||||
|
return mF;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,14 +109,16 @@ public class Resistance extends AbstractMeasure<ResistanceUnit> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static Resistance parseResistance(String value) {
|
public static Resistance parseResistance(String value) {
|
||||||
value = value.replace("*", "").replace("R", "\u2126");
|
ValueNotation.ParsedMeasure<ResistanceUnit> parsed;
|
||||||
for (ResistanceUnit unit : ResistanceUnit.values()) {
|
try {
|
||||||
if (value.toLowerCase().endsWith(unit.toString().toLowerCase())) {
|
parsed = ValueNotation.parse(value, ResistanceUnit::fromSymbol);
|
||||||
value = value.substring(0, value.length() - unit.toString().length()).trim();
|
} catch (IllegalArgumentException e) {
|
||||||
return new Resistance(parse(value), unit);
|
throw new IllegalArgumentException("Could not parse resistance: " + value, e);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
throw new IllegalArgumentException("Could not parse resistance: " + value);
|
if (parsed.unit() == null) {
|
||||||
|
throw new IllegalArgumentException("Could not parse resistance: " + value);
|
||||||
|
}
|
||||||
|
return new Resistance(parsed.value(), parsed.unit());
|
||||||
}
|
}
|
||||||
|
|
||||||
public Double getNormalizedValue() {
|
public Double getNormalizedValue() {
|
||||||
@@ -124,4 +126,12 @@ public class Resistance extends AbstractMeasure<ResistanceUnit> {
|
|||||||
return null;
|
return null;
|
||||||
return value * unit.getFactor();
|
return value * unit.getFactor();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
if (ValueNotation.getCurrent() == ValueNotation.RKM && value != null && unit != null) {
|
||||||
|
return ValueNotation.formatRkm(value, unit.getRkmSymbol());
|
||||||
|
}
|
||||||
|
return super.toString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,14 +23,16 @@ package org.diylc.core.measures;
|
|||||||
|
|
||||||
public enum ResistanceUnit implements Unit {
|
public enum ResistanceUnit implements Unit {
|
||||||
|
|
||||||
R(1, "\u03A9"), K(1e3, "K"), M(1e6, "M");
|
R(1, "\u03A9", "R"), K(1e3, "K", "K"), M(1e6, "M", "M");
|
||||||
|
|
||||||
double factor;
|
double factor;
|
||||||
String display;
|
String display;
|
||||||
|
String rkmSymbol;
|
||||||
|
|
||||||
private ResistanceUnit(double factor, String display) {
|
private ResistanceUnit(double factor, String display, String rkmSymbol) {
|
||||||
this.factor = factor;
|
this.factor = factor;
|
||||||
this.display = display;
|
this.display = display;
|
||||||
|
this.rkmSymbol = rkmSymbol;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -39,8 +41,45 @@ public enum ResistanceUnit implements Unit {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getRkmSymbol() {
|
||||||
|
return rkmSymbol;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return display;
|
return display;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the unit written after a resistance value, e.g. "K", "kohm", "R", "\u03A9" or "Meg".
|
||||||
|
* A lower-case "m" means mega, because there is no milliohm unit and parts lists commonly write
|
||||||
|
* "1m" for one megohm.
|
||||||
|
*
|
||||||
|
* @param symbol unit text with no surrounding whitespace
|
||||||
|
* @return the unit, or null when the text is not a resistance unit
|
||||||
|
*/
|
||||||
|
public static ResistanceUnit fromSymbol(String symbol) {
|
||||||
|
String prefix = symbol;
|
||||||
|
String lower = symbol.toLowerCase();
|
||||||
|
for (String ohm : new String[] {"ohms", "ohm", "\u03A9", "\u2126"}) {
|
||||||
|
if (lower.endsWith(ohm.toLowerCase())) {
|
||||||
|
prefix = symbol.substring(0, symbol.length() - ohm.length());
|
||||||
|
if (prefix.isEmpty()) {
|
||||||
|
return R;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch (prefix.toLowerCase()) {
|
||||||
|
case "r":
|
||||||
|
return R;
|
||||||
|
case "k":
|
||||||
|
return K;
|
||||||
|
case "m":
|
||||||
|
case "meg":
|
||||||
|
return M;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,128 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
*/
|
||||||
|
package org.diylc.core.measures;
|
||||||
|
|
||||||
|
import java.text.DecimalFormat;
|
||||||
|
import java.text.DecimalFormatSymbols;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notation used to write resistance and capacitance values: plain decimal ("4.7K", "0.22uF") or
|
||||||
|
* RKM as defined by IEC 60062, where the unit symbol takes the place of the decimal point ("4K7",
|
||||||
|
* "0u22"). Parsing accepts both regardless of the selected notation.
|
||||||
|
*
|
||||||
|
* @author Branislav Stojkovic
|
||||||
|
*/
|
||||||
|
public enum ValueNotation {
|
||||||
|
|
||||||
|
DECIMAL, RKM;
|
||||||
|
|
||||||
|
private static final DecimalFormat RKM_FORMAT =
|
||||||
|
new DecimalFormat("0.####", DecimalFormatSymbols.getInstance(Locale.US));
|
||||||
|
|
||||||
|
private static final Pattern RKM_PATTERN = Pattern.compile("^(-?)(\\d*)([^\\d.\\-]+)(\\d+)([^\\d.]*)$");
|
||||||
|
|
||||||
|
// read on every repaint through toString(), so it is kept here rather than looked up in the
|
||||||
|
// configuration
|
||||||
|
private static volatile ValueNotation current = DECIMAL;
|
||||||
|
|
||||||
|
public static ValueNotation getCurrent() {
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setCurrent(ValueNotation notation) {
|
||||||
|
current = notation == null ? DECIMAL : notation;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes a value in RKM notation, e.g. 4.7 with "K" becomes "4K7" and 100 with "R" becomes
|
||||||
|
* "100R". The value is kept in the unit it is stored in and never renormalized.
|
||||||
|
*
|
||||||
|
* @param value value expressed in the unit
|
||||||
|
* @param rkmSymbol symbol that stands in for the decimal point
|
||||||
|
* @return RKM code
|
||||||
|
*/
|
||||||
|
public static String formatRkm(double value, String rkmSymbol) {
|
||||||
|
String text;
|
||||||
|
synchronized (RKM_FORMAT) {
|
||||||
|
text = RKM_FORMAT.format(value);
|
||||||
|
}
|
||||||
|
int dot = text.indexOf('.');
|
||||||
|
if (dot < 0) {
|
||||||
|
return text + rkmSymbol;
|
||||||
|
}
|
||||||
|
return text.substring(0, dot) + rkmSymbol + text.substring(dot + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a value written either in decimal notation with an optional unit ("4.7", "4.7k",
|
||||||
|
* "0.22 uF") or in RKM notation ("4K7", "R22", "2n2F").
|
||||||
|
*
|
||||||
|
* @param text text to parse
|
||||||
|
* @param unitResolver maps the unit text to a unit, returning null when it is not recognized
|
||||||
|
* @return parsed value; its unit is null when the text carries no unit
|
||||||
|
* @throws IllegalArgumentException when the text cannot be parsed
|
||||||
|
*/
|
||||||
|
public static <U extends Unit> ParsedMeasure<U> parse(String text,
|
||||||
|
Function<String, U> unitResolver) {
|
||||||
|
if (text == null) {
|
||||||
|
throw new IllegalArgumentException("Nothing to parse");
|
||||||
|
}
|
||||||
|
String value = text.replace("*", "").replace(",", ".").replaceAll("\\s", "");
|
||||||
|
if (value.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("Nothing to parse");
|
||||||
|
}
|
||||||
|
|
||||||
|
int unitStart = value.length();
|
||||||
|
while (unitStart > 0 && !Character.isDigit(value.charAt(unitStart - 1))
|
||||||
|
&& value.charAt(unitStart - 1) != '.') {
|
||||||
|
unitStart--;
|
||||||
|
}
|
||||||
|
String number = value.substring(0, unitStart);
|
||||||
|
String unitText = value.substring(unitStart);
|
||||||
|
U unit = unitText.isEmpty() ? null : unitResolver.apply(unitText);
|
||||||
|
if (unitText.isEmpty() || unit != null) {
|
||||||
|
try {
|
||||||
|
return new ParsedMeasure<U>(Double.parseDouble(number), unit);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
// not a decimal number, try RKM below
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Matcher matcher = RKM_PATTERN.matcher(value);
|
||||||
|
if (matcher.matches()) {
|
||||||
|
unit = unitResolver.apply(matcher.group(3) + matcher.group(5));
|
||||||
|
if (unit != null) {
|
||||||
|
String integerPart = matcher.group(2).isEmpty() ? "0" : matcher.group(2);
|
||||||
|
double parsed = Double.parseDouble(integerPart + "." + matcher.group(4));
|
||||||
|
return new ParsedMeasure<U>(matcher.group(1).isEmpty() ? parsed : -parsed, unit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new IllegalArgumentException("Could not parse value: " + text);
|
||||||
|
}
|
||||||
|
|
||||||
|
public record ParsedMeasure<U extends Unit>(double value, U unit) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
*/
|
||||||
|
package org.diylc.presenter;
|
||||||
|
|
||||||
|
import org.diylc.appframework.miscutils.IConfigurationManager;
|
||||||
|
|
||||||
|
import org.diylc.common.IPlugInPort;
|
||||||
|
import org.diylc.core.measures.ValueNotation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keeps {@link ValueNotation#getCurrent()} in sync with the value notation the user picked in the
|
||||||
|
* configuration, and redraws the project when it changes. The notation is global, so only the
|
||||||
|
* main window binds it; secondary presenters (previews, thumbnails, batch conversion, regression
|
||||||
|
* runs) run on in-memory configurations and must not reset it.
|
||||||
|
*
|
||||||
|
* @author Branislav Stojkovic
|
||||||
|
*/
|
||||||
|
public class ValueNotationBinder {
|
||||||
|
|
||||||
|
public static void bind(IConfigurationManager<?> configManager, Runnable refresh) {
|
||||||
|
apply(configManager.readString(IPlugInPort.VALUE_NOTATION_KEY,
|
||||||
|
IPlugInPort.VALUE_NOTATION_DEFAULT));
|
||||||
|
configManager.addConfigListener(IPlugInPort.VALUE_NOTATION_KEY, (key, value) -> {
|
||||||
|
apply(value == null ? null : value.toString());
|
||||||
|
// cached component images still carry the labels drawn in the previous notation
|
||||||
|
DrawingCache.Instance.clear();
|
||||||
|
refresh.run();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static void apply(String configValue) {
|
||||||
|
ValueNotation.setCurrent(IPlugInPort.VALUE_NOTATION_RKM.equalsIgnoreCase(configValue)
|
||||||
|
? ValueNotation.RKM : ValueNotation.DECIMAL);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -629,6 +629,10 @@
|
|||||||
<releaseDate>2026-11-07 00:00:00.000 CET</releaseDate>
|
<releaseDate>2026-11-07 00:00:00.000 CET</releaseDate>
|
||||||
<name></name>
|
<name></name>
|
||||||
<changes>
|
<changes>
|
||||||
|
<org.diylc.appframework.update.Change>
|
||||||
|
<changeType>NEW_FEATURE</changeType>
|
||||||
|
<description>Added 'Value Notation' to the Config menu to show resistance and capacitance values in RKM notation, e.g. 4K7, 0R22, 2n2 or 0u22, instead of decimal; values can be typed in either notation, and typing the unit together with the value, e.g. 4K7 or 2.2nF, sets the unit at the same time</description>
|
||||||
|
</org.diylc.appframework.update.Change>
|
||||||
<org.diylc.appframework.update.Change>
|
<org.diylc.appframework.update.Change>
|
||||||
<changeType>BUG_FIX</changeType>
|
<changeType>BUG_FIX</changeType>
|
||||||
<description>Rotated labels in pasted components and building blocks were drawn in the wrong place and at the wrong angle while following the mouse, until the components were placed</description>
|
<description>Rotated labels in pasted components and building blocks were drawn in the wrong place and at the wrong angle while following the mouse, until the components were placed</description>
|
||||||
|
|||||||
@@ -0,0 +1,203 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
*/
|
||||||
|
package org.diylc.core.measures;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertNull;
|
||||||
|
import static org.junit.Assert.fail;
|
||||||
|
|
||||||
|
import org.junit.After;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
public class ValueNotationTest {
|
||||||
|
|
||||||
|
private static final double DELTA = 1e-9;
|
||||||
|
|
||||||
|
@After
|
||||||
|
public void resetNotation() {
|
||||||
|
ValueNotation.setCurrent(ValueNotation.DECIMAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- formatting ---
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void decimalResistanceIsUnchanged() {
|
||||||
|
assertEquals("4.7\u03A9", new Resistance(4.7, ResistanceUnit.R).toString());
|
||||||
|
assertEquals("0.22\u03A9", new Resistance(0.22, ResistanceUnit.R).toString());
|
||||||
|
assertEquals("4.7K", new Resistance(4.7, ResistanceUnit.K).toString());
|
||||||
|
assertEquals("1M", new Resistance(1d, ResistanceUnit.M).toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void decimalCapacitanceIsUnchanged() {
|
||||||
|
assertEquals("0.22uF", new Capacitance(0.22, CapacitanceUnit.uF).toString());
|
||||||
|
assertEquals("100nF", new Capacitance(100d, CapacitanceUnit.nF).toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void rkmResistance() {
|
||||||
|
ValueNotation.setCurrent(ValueNotation.RKM);
|
||||||
|
assertEquals("4R7", new Resistance(4.7, ResistanceUnit.R).toString());
|
||||||
|
assertEquals("0R22", new Resistance(0.22, ResistanceUnit.R).toString());
|
||||||
|
assertEquals("100R", new Resistance(100d, ResistanceUnit.R).toString());
|
||||||
|
assertEquals("4K7", new Resistance(4.7, ResistanceUnit.K).toString());
|
||||||
|
assertEquals("47K", new Resistance(47d, ResistanceUnit.K).toString());
|
||||||
|
assertEquals("1M", new Resistance(1d, ResistanceUnit.M).toString());
|
||||||
|
assertEquals("4700R", new Resistance(4700d, ResistanceUnit.R).toString());
|
||||||
|
assertEquals("0R", new Resistance(0d, ResistanceUnit.R).toString());
|
||||||
|
assertEquals("4K7501", new Resistance(4.75012, ResistanceUnit.K).toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void rkmCapacitance() {
|
||||||
|
ValueNotation.setCurrent(ValueNotation.RKM);
|
||||||
|
assertEquals("0u22", new Capacitance(0.22, CapacitanceUnit.uF).toString());
|
||||||
|
assertEquals("2n2", new Capacitance(2.2, CapacitanceUnit.nF).toString());
|
||||||
|
assertEquals("100n", new Capacitance(100d, CapacitanceUnit.nF).toString());
|
||||||
|
assertEquals("4p7", new Capacitance(4.7, CapacitanceUnit.pF).toString());
|
||||||
|
assertEquals("1000u", new Capacitance(1000d, CapacitanceUnit.uF).toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- parsing ---
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void parsesDecimalResistance() {
|
||||||
|
assertResistance(4.7, ResistanceUnit.K, "4.7K");
|
||||||
|
assertResistance(4.7, ResistanceUnit.K, "4.7k");
|
||||||
|
assertResistance(4.7, ResistanceUnit.K, "4.7 K");
|
||||||
|
assertResistance(4.7, ResistanceUnit.K, "4,7K");
|
||||||
|
assertResistance(4.7, ResistanceUnit.K, "4.7kohm");
|
||||||
|
assertResistance(4.7, ResistanceUnit.R, "4.7ohm");
|
||||||
|
assertResistance(4.7, ResistanceUnit.R, "4.7 ohms");
|
||||||
|
assertResistance(4.7, ResistanceUnit.R, "4.7\u03A9");
|
||||||
|
assertResistance(4.7, ResistanceUnit.R, "4.7\u2126");
|
||||||
|
assertResistance(4.7, ResistanceUnit.K, "4.7K\u03A9");
|
||||||
|
assertResistance(1, ResistanceUnit.M, "1M");
|
||||||
|
assertResistance(1, ResistanceUnit.M, "1m");
|
||||||
|
assertResistance(2.2, ResistanceUnit.M, "2.2Meg");
|
||||||
|
assertResistance(100, ResistanceUnit.R, "100R");
|
||||||
|
assertResistance(100, ResistanceUnit.R, "100r");
|
||||||
|
assertResistance(0.5, ResistanceUnit.K, ".5K");
|
||||||
|
assertResistance(10, ResistanceUnit.K, "10K*");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void parsesRkmResistance() {
|
||||||
|
assertResistance(4.7, ResistanceUnit.R, "4R7");
|
||||||
|
assertResistance(0.22, ResistanceUnit.R, "0R22");
|
||||||
|
assertResistance(0.22, ResistanceUnit.R, "R22");
|
||||||
|
assertResistance(4.7, ResistanceUnit.K, "4K7");
|
||||||
|
assertResistance(4.7, ResistanceUnit.K, "4k7");
|
||||||
|
assertResistance(4.75, ResistanceUnit.K, "4K75");
|
||||||
|
assertResistance(2.2, ResistanceUnit.M, "2M2");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void parsesDecimalCapacitance() {
|
||||||
|
assertCapacitance(0.22, CapacitanceUnit.uF, "0.22uF");
|
||||||
|
assertCapacitance(0.22, CapacitanceUnit.uF, "0.22UF");
|
||||||
|
assertCapacitance(0.22, CapacitanceUnit.uF, "0.22u");
|
||||||
|
assertCapacitance(220, CapacitanceUnit.nF, "220 nF");
|
||||||
|
assertCapacitance(220, CapacitanceUnit.nF, "220n");
|
||||||
|
assertCapacitance(47, CapacitanceUnit.pF, "47pf");
|
||||||
|
assertCapacitance(4.7, CapacitanceUnit.uF, "4.7\u00B5F");
|
||||||
|
assertCapacitance(1, CapacitanceUnit.F, "1F");
|
||||||
|
assertCapacitance(1, CapacitanceUnit.mF, "1mF");
|
||||||
|
assertCapacitance(1, CapacitanceUnit.mF, "1MF");
|
||||||
|
assertCapacitance(1, CapacitanceUnit.mF, "1m");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void parsesRkmCapacitance() {
|
||||||
|
assertCapacitance(0.22, CapacitanceUnit.uF, "0u22");
|
||||||
|
assertCapacitance(0.22, CapacitanceUnit.uF, "u22");
|
||||||
|
assertCapacitance(2.2, CapacitanceUnit.nF, "2n2");
|
||||||
|
assertCapacitance(2.2, CapacitanceUnit.nF, "2n2F");
|
||||||
|
assertCapacitance(4.7, CapacitanceUnit.uF, "4u7");
|
||||||
|
assertCapacitance(4.7, CapacitanceUnit.uF, "4\u00B57");
|
||||||
|
assertCapacitance(4.7, CapacitanceUnit.uF, "4\u03BC7");
|
||||||
|
assertCapacitance(4.7, CapacitanceUnit.pF, "4p7");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void bareNumberHasNoUnit() {
|
||||||
|
ValueNotation.ParsedMeasure<ResistanceUnit> parsed =
|
||||||
|
ValueNotation.parse("4.7", ResistanceUnit::fromSymbol);
|
||||||
|
assertEquals(4.7, parsed.value(), DELTA);
|
||||||
|
assertNull(parsed.unit());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void rejectsInvalidText() {
|
||||||
|
assertRejected("4u7", ResistanceUnit.class);
|
||||||
|
assertRejected("4K7K", ResistanceUnit.class);
|
||||||
|
assertRejected("K", ResistanceUnit.class);
|
||||||
|
assertRejected("", ResistanceUnit.class);
|
||||||
|
assertRejected("100", ResistanceUnit.class);
|
||||||
|
assertRejected("4R7", CapacitanceUnit.class);
|
||||||
|
assertRejected("1M", CapacitanceUnit.class);
|
||||||
|
assertRejected("4M7", CapacitanceUnit.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void roundTripsInBothNotations() {
|
||||||
|
double[] values = {0.22, 1, 4.7, 47, 100, 4700, 0.0047};
|
||||||
|
for (ValueNotation notation : ValueNotation.values()) {
|
||||||
|
ValueNotation.setCurrent(notation);
|
||||||
|
for (double value : values) {
|
||||||
|
for (ResistanceUnit unit : ResistanceUnit.values()) {
|
||||||
|
Resistance resistance = new Resistance(value, unit);
|
||||||
|
assertEquals(resistance, Resistance.parseResistance(resistance.toString()));
|
||||||
|
}
|
||||||
|
for (CapacitanceUnit unit : CapacitanceUnit.values()) {
|
||||||
|
Capacitance capacitance = new Capacitance(value, unit);
|
||||||
|
assertEquals(capacitance, Capacitance.parseCapacitance(capacitance.toString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void assertResistance(double value, ResistanceUnit unit, String text) {
|
||||||
|
Resistance parsed = Resistance.parseResistance(text);
|
||||||
|
assertEquals(text, value, parsed.getValue(), DELTA);
|
||||||
|
assertEquals(text, unit, parsed.getUnit());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void assertCapacitance(double value, CapacitanceUnit unit, String text) {
|
||||||
|
Capacitance parsed = Capacitance.parseCapacitance(text);
|
||||||
|
assertEquals(text, value, parsed.getValue(), DELTA);
|
||||||
|
assertEquals(text, unit, parsed.getUnit());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void assertRejected(String text, Class<?> unitType) {
|
||||||
|
try {
|
||||||
|
if (unitType == ResistanceUnit.class) {
|
||||||
|
Resistance.parseResistance(text);
|
||||||
|
} else {
|
||||||
|
Capacitance.parseCapacitance(text);
|
||||||
|
}
|
||||||
|
fail("Expected " + text + " to be rejected");
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
// expected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+157
-57
@@ -26,6 +26,8 @@ import java.awt.Color;
|
|||||||
import java.awt.Container;
|
import java.awt.Container;
|
||||||
import java.awt.event.ActionEvent;
|
import java.awt.event.ActionEvent;
|
||||||
import java.awt.event.ActionListener;
|
import java.awt.event.ActionListener;
|
||||||
|
import java.awt.event.FocusAdapter;
|
||||||
|
import java.awt.event.FocusEvent;
|
||||||
import java.awt.event.KeyListener;
|
import java.awt.event.KeyListener;
|
||||||
import java.beans.PropertyChangeEvent;
|
import java.beans.PropertyChangeEvent;
|
||||||
import java.beans.PropertyChangeListener;
|
import java.beans.PropertyChangeListener;
|
||||||
@@ -34,12 +36,19 @@ import java.lang.reflect.Constructor;
|
|||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.lang.reflect.ParameterizedType;
|
import java.lang.reflect.ParameterizedType;
|
||||||
import java.lang.reflect.Type;
|
import java.lang.reflect.Type;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Function;
|
||||||
import javax.swing.JComboBox;
|
import javax.swing.JComboBox;
|
||||||
|
import javax.swing.JTextField;
|
||||||
import org.apache.log4j.Logger;
|
import org.apache.log4j.Logger;
|
||||||
import org.diylc.swingframework.DoubleArrayTextField;
|
import org.diylc.swingframework.DoubleArrayTextField;
|
||||||
|
|
||||||
import org.diylc.common.PropertyWrapper;
|
import org.diylc.common.PropertyWrapper;
|
||||||
import org.diylc.core.measures.AbstractMeasure;
|
import org.diylc.core.measures.AbstractMeasure;
|
||||||
|
import org.diylc.core.measures.Unit;
|
||||||
|
import org.diylc.core.measures.ValueNotation;
|
||||||
|
import org.diylc.core.measures.ValueNotation.ParsedMeasure;
|
||||||
import org.diylc.utils.Constants;
|
import org.diylc.utils.Constants;
|
||||||
|
|
||||||
public class MeasureArrayEditor extends Container {
|
public class MeasureArrayEditor extends Container {
|
||||||
@@ -50,52 +59,59 @@ public class MeasureArrayEditor extends Container {
|
|||||||
.getLogger(MeasureArrayEditor.class);
|
.getLogger(MeasureArrayEditor.class);
|
||||||
|
|
||||||
private Color oldBg;
|
private Color oldBg;
|
||||||
private DoubleArrayTextField valueField;
|
private JTextField valueField;
|
||||||
private JComboBox<Object> unitBox;
|
private JComboBox<Object> unitBox;
|
||||||
|
private boolean ignoreUnitChanges = false;
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
public MeasureArrayEditor(final PropertyWrapper property) {
|
public MeasureArrayEditor(final PropertyWrapper property) {
|
||||||
setLayout(new BorderLayout());
|
setLayout(new BorderLayout());
|
||||||
final AbstractMeasure<?>[] measure = ((AbstractMeasure<?>[]) property
|
final AbstractMeasure<?>[] measure = ((AbstractMeasure<?>[]) property
|
||||||
.getValue());
|
.getValue());
|
||||||
Double[] values = new Double[measure.length];
|
final Function<String, Unit> unitResolver = MeasureEditor
|
||||||
for (int i = 0; i < measure.length; i++) {
|
.getUnitResolver(property.getType().getComponentType());
|
||||||
values[i] = measure[i] == null ? null : measure[i].getValue();
|
if (unitResolver == null) {
|
||||||
}
|
valueField = createDoubleField(property, measure);
|
||||||
valueField = new DoubleArrayTextField(measure == null ? null : values);
|
} else {
|
||||||
if (property.isReadOnly())
|
final MeasureTextField<List<ParsedMeasure<Unit>>> measureField = new MeasureTextField<List<ParsedMeasure<Unit>>>(
|
||||||
valueField.setEnabled(false);
|
formatForField(measure), text -> parseSections(text, unitResolver));
|
||||||
oldBg = valueField.getBackground();
|
measureField.addPropertyChangeListener(MeasureTextField.VALUE_PROPERTY,
|
||||||
valueField.addPropertyChangeListener(
|
new PropertyChangeListener() {
|
||||||
DoubleArrayTextField.VALUE_PROPERTY,
|
|
||||||
new PropertyChangeListener() {
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void propertyChange(PropertyChangeEvent evt) {
|
public void propertyChange(PropertyChangeEvent evt) {
|
||||||
try {
|
List<ParsedMeasure<Unit>> parsed = (List<ParsedMeasure<Unit>>) evt
|
||||||
Constructor<?> ctor = property.getType().getComponentType()
|
.getNewValue();
|
||||||
.getConstructors()[0];
|
// the unit box shows the unit of the first section
|
||||||
Double[] newValues = (Double[]) evt.getNewValue();
|
if (parsed != null && !parsed.isEmpty() && parsed.get(0) != null
|
||||||
AbstractMeasure<?>[] newMeasure;
|
&& parsed.get(0).unit() != null
|
||||||
if (newValues == null)
|
&& parsed.get(0).unit() != unitBox.getSelectedItem()) {
|
||||||
newMeasure = null;
|
ignoreUnitChanges = true;
|
||||||
else {
|
try {
|
||||||
newMeasure = (AbstractMeasure<?>[]) Array.newInstance(property.getType().getComponentType(), newValues.length);
|
unitBox.setSelectedItem(parsed.get(0).unit());
|
||||||
for (int i = 0; i < newValues.length; i++) {
|
} finally {
|
||||||
newMeasure[i] = (AbstractMeasure<?>) ctor
|
ignoreUnitChanges = false;
|
||||||
.newInstance(newValues[i],
|
|
||||||
unitBox.getSelectedItem());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
applySections(property, parsed, false);
|
||||||
property.setValue(newMeasure);
|
|
||||||
property.setChanged(true);
|
|
||||||
valueField.setBackground(oldBg);
|
|
||||||
unitBox.setBackground(oldBg);
|
|
||||||
} catch (Exception e) {
|
|
||||||
LOG.error("Error while updating property value", e);
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
measureField.addFocusListener(new FocusAdapter() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void focusLost(FocusEvent e) {
|
||||||
|
if (measureField.getValue() != null) {
|
||||||
|
measureField.setDisplayText(
|
||||||
|
formatForField((AbstractMeasure<?>[]) property.getValue()));
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
valueField = measureField;
|
||||||
|
}
|
||||||
|
if (property.isReadOnly()) {
|
||||||
|
valueField.setEnabled(false);
|
||||||
|
}
|
||||||
|
oldBg = valueField.getBackground();
|
||||||
add(valueField, BorderLayout.CENTER);
|
add(valueField, BorderLayout.CENTER);
|
||||||
try {
|
try {
|
||||||
Type type = ((ParameterizedType) property.getType().getComponentType()
|
Type type = ((ParameterizedType) property.getType().getComponentType()
|
||||||
@@ -108,29 +124,18 @@ public class MeasureArrayEditor extends Container {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void actionPerformed(ActionEvent evt) {
|
public void actionPerformed(ActionEvent evt) {
|
||||||
try {
|
if (ignoreUnitChanges) {
|
||||||
Constructor<?> ctor = property.getType().getComponentType()
|
return;
|
||||||
.getConstructors()[0];
|
}
|
||||||
|
if (valueField instanceof DoubleArrayTextField doubleField) {
|
||||||
AbstractMeasure<?>[] newMeasure;
|
applyValues(property, doubleField.getValue());
|
||||||
Double[] newValues = valueField.getValue();
|
} else {
|
||||||
if (newValues == null)
|
MeasureTextField<List<ParsedMeasure<Unit>>> measureField = (MeasureTextField<List<ParsedMeasure<Unit>>>) valueField;
|
||||||
newMeasure = null;
|
if (measureField.getValue() != null) {
|
||||||
else {
|
applySections(property, measureField.getValue(), true);
|
||||||
newMeasure = (AbstractMeasure<?>[]) Array.newInstance(property.getType().getComponentType(), newValues.length);
|
measureField.setDisplayText(
|
||||||
for (int i = 0; i < newValues.length; i++) {
|
formatForField((AbstractMeasure<?>[]) property.getValue()));
|
||||||
newMeasure[i] = (AbstractMeasure<?>) ctor
|
|
||||||
.newInstance(newValues[i],
|
|
||||||
unitBox.getSelectedItem());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
property.setValue(newMeasure);
|
|
||||||
property.setChanged(true);
|
|
||||||
valueField.setBackground(oldBg);
|
|
||||||
unitBox.setBackground(oldBg);
|
|
||||||
} catch (Exception e) {
|
|
||||||
LOG.error("Error while updating property units", e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -145,6 +150,101 @@ public class MeasureArrayEditor extends Container {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private DoubleArrayTextField createDoubleField(final PropertyWrapper property,
|
||||||
|
AbstractMeasure<?>[] measure) {
|
||||||
|
Double[] values = new Double[measure.length];
|
||||||
|
for (int i = 0; i < measure.length; i++) {
|
||||||
|
values[i] = measure[i] == null ? null : measure[i].getValue();
|
||||||
|
}
|
||||||
|
DoubleArrayTextField valueField = new DoubleArrayTextField(measure == null ? null : values);
|
||||||
|
valueField.addPropertyChangeListener(
|
||||||
|
DoubleArrayTextField.VALUE_PROPERTY,
|
||||||
|
new PropertyChangeListener() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void propertyChange(PropertyChangeEvent evt) {
|
||||||
|
applyValues(property, (Double[]) evt.getNewValue());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return valueField;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyValues(PropertyWrapper property, Double[] newValues) {
|
||||||
|
try {
|
||||||
|
Constructor<?> ctor = property.getType().getComponentType()
|
||||||
|
.getConstructors()[0];
|
||||||
|
AbstractMeasure<?>[] newMeasure;
|
||||||
|
if (newValues == null)
|
||||||
|
newMeasure = null;
|
||||||
|
else {
|
||||||
|
newMeasure = (AbstractMeasure<?>[]) Array.newInstance(property.getType().getComponentType(), newValues.length);
|
||||||
|
for (int i = 0; i < newValues.length; i++) {
|
||||||
|
newMeasure[i] = (AbstractMeasure<?>) ctor
|
||||||
|
.newInstance(newValues[i],
|
||||||
|
unitBox.getSelectedItem());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
property.setValue(newMeasure);
|
||||||
|
property.setChanged(true);
|
||||||
|
valueField.setBackground(oldBg);
|
||||||
|
unitBox.setBackground(oldBg);
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOG.error("Error while updating property value", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sections typed with their own unit keep it, the others take the unit selected in the box.
|
||||||
|
* When the box itself changed, its unit is applied to every section.
|
||||||
|
*/
|
||||||
|
private void applySections(PropertyWrapper property, List<ParsedMeasure<Unit>> sections,
|
||||||
|
boolean forceBoxUnit) {
|
||||||
|
try {
|
||||||
|
Constructor<?> ctor = property.getType().getComponentType()
|
||||||
|
.getConstructors()[0];
|
||||||
|
AbstractMeasure<?>[] newMeasure = null;
|
||||||
|
if (sections != null) {
|
||||||
|
newMeasure = (AbstractMeasure<?>[]) Array.newInstance(property.getType().getComponentType(), sections.size());
|
||||||
|
for (int i = 0; i < sections.size(); i++) {
|
||||||
|
ParsedMeasure<Unit> section = sections.get(i);
|
||||||
|
Object unit = section == null || section.unit() == null || forceBoxUnit
|
||||||
|
? unitBox.getSelectedItem() : section.unit();
|
||||||
|
newMeasure[i] = (AbstractMeasure<?>) ctor
|
||||||
|
.newInstance(section == null ? null : section.value(), unit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
property.setValue(newMeasure);
|
||||||
|
property.setChanged(true);
|
||||||
|
valueField.setBackground(oldBg);
|
||||||
|
unitBox.setBackground(oldBg);
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOG.error("Error while updating property value", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<ParsedMeasure<Unit>> parseSections(String text,
|
||||||
|
Function<String, Unit> unitResolver) {
|
||||||
|
List<ParsedMeasure<Unit>> sections = new ArrayList<ParsedMeasure<Unit>>();
|
||||||
|
for (String part : text.split("/", -1)) {
|
||||||
|
sections.add(part.trim().isEmpty() ? null : ValueNotation.parse(part, unitResolver));
|
||||||
|
}
|
||||||
|
return sections;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String formatForField(AbstractMeasure<?>[] measure) {
|
||||||
|
if (measure == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
Object boxUnit = measure.length == 0 || measure[0] == null ? null : measure[0].getUnit();
|
||||||
|
List<String> parts = new ArrayList<String>();
|
||||||
|
for (AbstractMeasure<?> section : measure) {
|
||||||
|
parts.add(MeasureEditor.formatForField(section,
|
||||||
|
section == null || section.getUnit() == boxUnit));
|
||||||
|
}
|
||||||
|
return String.join(" / ", parts);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void requestFocus() {
|
public void requestFocus() {
|
||||||
this.valueField.requestFocus();
|
this.valueField.requestFocus();
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ import java.awt.Color;
|
|||||||
import java.awt.Container;
|
import java.awt.Container;
|
||||||
import java.awt.event.ActionEvent;
|
import java.awt.event.ActionEvent;
|
||||||
import java.awt.event.ActionListener;
|
import java.awt.event.ActionListener;
|
||||||
|
import java.awt.event.FocusAdapter;
|
||||||
|
import java.awt.event.FocusEvent;
|
||||||
import java.awt.event.KeyListener;
|
import java.awt.event.KeyListener;
|
||||||
import java.beans.PropertyChangeEvent;
|
import java.beans.PropertyChangeEvent;
|
||||||
import java.beans.PropertyChangeListener;
|
import java.beans.PropertyChangeListener;
|
||||||
@@ -33,12 +35,22 @@ import java.lang.reflect.Constructor;
|
|||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.lang.reflect.ParameterizedType;
|
import java.lang.reflect.ParameterizedType;
|
||||||
import java.lang.reflect.Type;
|
import java.lang.reflect.Type;
|
||||||
|
import java.text.DecimalFormat;
|
||||||
|
import java.text.Format;
|
||||||
|
import java.util.function.Function;
|
||||||
import javax.swing.JComboBox;
|
import javax.swing.JComboBox;
|
||||||
|
import javax.swing.JTextField;
|
||||||
import org.apache.log4j.Logger;
|
import org.apache.log4j.Logger;
|
||||||
import org.diylc.appframework.miscutils.ConfigurationManager;
|
import org.diylc.appframework.miscutils.ConfigurationManager;
|
||||||
import org.diylc.common.IPlugInPort;
|
import org.diylc.common.IPlugInPort;
|
||||||
|
import org.diylc.core.measures.Capacitance;
|
||||||
|
import org.diylc.core.measures.CapacitanceUnit;
|
||||||
|
import org.diylc.core.measures.Resistance;
|
||||||
|
import org.diylc.core.measures.ResistanceUnit;
|
||||||
import org.diylc.core.measures.Size;
|
import org.diylc.core.measures.Size;
|
||||||
import org.diylc.core.measures.SizeUnit;
|
import org.diylc.core.measures.SizeUnit;
|
||||||
|
import org.diylc.core.measures.ValueNotation;
|
||||||
|
import org.diylc.core.measures.ValueNotation.ParsedMeasure;
|
||||||
import org.diylc.swingframework.DoubleTextField;
|
import org.diylc.swingframework.DoubleTextField;
|
||||||
|
|
||||||
import org.diylc.common.PropertyWrapper;
|
import org.diylc.common.PropertyWrapper;
|
||||||
@@ -52,39 +64,70 @@ public class MeasureEditor extends Container {
|
|||||||
|
|
||||||
private final static Logger LOG = Logger.getLogger(MeasureEditor.class);
|
private final static Logger LOG = Logger.getLogger(MeasureEditor.class);
|
||||||
|
|
||||||
|
private static final Format DECIMAL_FORMAT = new DecimalFormat("0.#####");
|
||||||
|
|
||||||
private Color oldBg;
|
private Color oldBg;
|
||||||
private DoubleTextField valueField;
|
private JTextField valueField;
|
||||||
private JComboBox<Object> unitBox;
|
private JComboBox<Object> unitBox;
|
||||||
|
private boolean ignoreUnitChanges = false;
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
public MeasureEditor(final PropertyWrapper property) {
|
public MeasureEditor(final PropertyWrapper property) {
|
||||||
setLayout(new BorderLayout());
|
setLayout(new BorderLayout());
|
||||||
AbstractMeasure<?> measure = ((AbstractMeasure<?>) property.getValue());
|
AbstractMeasure<?> measure = ((AbstractMeasure<?>) property.getValue());
|
||||||
measure = convertUnitsIfNeeded(measure);
|
measure = convertUnitsIfNeeded(measure);
|
||||||
valueField = new DoubleTextField(measure == null ? null : measure.getValue());
|
final Function<String, Unit> unitResolver = getUnitResolver(property.getType());
|
||||||
oldBg = valueField.getBackground();
|
if (unitResolver == null) {
|
||||||
valueField.addPropertyChangeListener(DoubleTextField.VALUE_PROPERTY, new PropertyChangeListener() {
|
final DoubleTextField doubleField =
|
||||||
|
new DoubleTextField(measure == null ? null : measure.getValue());
|
||||||
|
doubleField.addPropertyChangeListener(DoubleTextField.VALUE_PROPERTY,
|
||||||
|
new PropertyChangeListener() {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void propertyChange(PropertyChangeEvent evt) {
|
public void propertyChange(PropertyChangeEvent evt) {
|
||||||
try {
|
applyValue(property, (Double) evt.getNewValue(), unitBox.getSelectedItem());
|
||||||
Double newValue = (Double) evt.getNewValue();
|
}
|
||||||
if (newValue == null) {
|
});
|
||||||
property.setValue(null);
|
valueField = doubleField;
|
||||||
} else {
|
} else {
|
||||||
Constructor<?> ctor = property.getType().getConstructors()[0];
|
final MeasureTextField<ParsedMeasure<Unit>> measureField =
|
||||||
AbstractMeasure<?> newMeasure =
|
new MeasureTextField<ParsedMeasure<Unit>>(formatForField(measure, true),
|
||||||
(AbstractMeasure<?>) ctor.newInstance(newValue, unitBox.getSelectedItem());
|
text -> ValueNotation.parse(text, unitResolver));
|
||||||
property.setValue(newMeasure);
|
measureField.addPropertyChangeListener(MeasureTextField.VALUE_PROPERTY,
|
||||||
|
new PropertyChangeListener() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void propertyChange(PropertyChangeEvent evt) {
|
||||||
|
ParsedMeasure<Unit> parsed = (ParsedMeasure<Unit>) evt.getNewValue();
|
||||||
|
if (parsed == null) {
|
||||||
|
applyValue(property, null, unitBox.getSelectedItem());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// a unit typed together with the value, e.g. 4K7, also switches the unit box
|
||||||
|
if (parsed.unit() != null && parsed.unit() != unitBox.getSelectedItem()) {
|
||||||
|
ignoreUnitChanges = true;
|
||||||
|
try {
|
||||||
|
unitBox.setSelectedItem(parsed.unit());
|
||||||
|
} finally {
|
||||||
|
ignoreUnitChanges = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
applyValue(property, parsed.value(), unitBox.getSelectedItem());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
measureField.addFocusListener(new FocusAdapter() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void focusLost(FocusEvent e) {
|
||||||
|
if (measureField.getValue() != null) {
|
||||||
|
measureField.setDisplayText(
|
||||||
|
formatForField((AbstractMeasure<?>) property.getValue(), true));
|
||||||
}
|
}
|
||||||
property.setChanged(true);
|
|
||||||
valueField.setBackground(oldBg);
|
|
||||||
unitBox.setBackground(oldBg);
|
|
||||||
} catch (Exception e) {
|
|
||||||
LOG.error("Error while updating property value", e);
|
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
valueField = measureField;
|
||||||
|
}
|
||||||
|
oldBg = valueField.getBackground();
|
||||||
add(valueField, BorderLayout.CENTER);
|
add(valueField, BorderLayout.CENTER);
|
||||||
try {
|
try {
|
||||||
Type type = ((ParameterizedType) property.getType().getGenericSuperclass()).getActualTypeArguments()[0];
|
Type type = ((ParameterizedType) property.getType().getGenericSuperclass()).getActualTypeArguments()[0];
|
||||||
@@ -95,33 +138,27 @@ public class MeasureEditor extends Container {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void actionPerformed(ActionEvent evt) {
|
public void actionPerformed(ActionEvent evt) {
|
||||||
|
if (ignoreUnitChanges) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
Double newValue = valueField.getValue();
|
Double newValue = getFieldValue();
|
||||||
if (newValue == null) {
|
if (newValue != null && property.isReadOnly()) {
|
||||||
property.setValue(null);
|
double oldFactor = ((Unit)((AbstractMeasure<?>)property.getValue()).getUnit()).getFactor();
|
||||||
} else {
|
double newFactor = ((Unit)unitBox.getSelectedItem()).getFactor();
|
||||||
Constructor<?> ctor = property.getType().getConstructors()[0];
|
newValue = newValue * oldFactor / newFactor;
|
||||||
if (property.isReadOnly()) {
|
}
|
||||||
double oldFactor = ((Unit)((AbstractMeasure<?>)property.getValue()).getUnit()).getFactor();
|
applyValue(property, newValue, unitBox.getSelectedItem());
|
||||||
double newFactor = ((Unit)unitBox.getSelectedItem()).getFactor();
|
if (newValue != null && (property.isReadOnly() || unitResolver != null)) {
|
||||||
newValue = newValue * oldFactor / newFactor;
|
setFieldValue((AbstractMeasure<?>) property.getValue());
|
||||||
valueField.setValue(newValue);
|
|
||||||
}
|
|
||||||
AbstractMeasure<?> newMeasure =
|
|
||||||
(AbstractMeasure<?>) ctor.newInstance(newValue,
|
|
||||||
(Enum<? extends Unit>) unitBox.getSelectedItem());
|
|
||||||
property.setValue(newMeasure);
|
|
||||||
}
|
}
|
||||||
property.setChanged(true);
|
|
||||||
valueField.setBackground(oldBg);
|
|
||||||
unitBox.setBackground(oldBg);
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
LOG.error("Error while updating property units", e);
|
LOG.error("Error while updating property units", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
add(unitBox, BorderLayout.EAST);
|
add(unitBox, BorderLayout.EAST);
|
||||||
|
|
||||||
if (property.isReadOnly()) {
|
if (property.isReadOnly()) {
|
||||||
valueField.setEnabled(false);
|
valueField.setEnabled(false);
|
||||||
unitBox.setEnabled(true);
|
unitBox.setEnabled(true);
|
||||||
@@ -136,6 +173,70 @@ public class MeasureEditor extends Container {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the function that resolves unit symbols typed together with the value, or null when
|
||||||
|
* the measure only accepts a plain number.
|
||||||
|
*/
|
||||||
|
static Function<String, Unit> getUnitResolver(Class<?> measureType) {
|
||||||
|
if (Resistance.class.equals(measureType)) {
|
||||||
|
return ResistanceUnit::fromSymbol;
|
||||||
|
}
|
||||||
|
if (Capacitance.class.equals(measureType)) {
|
||||||
|
return CapacitanceUnit::fromSymbol;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Text shown in the value field. It is always decimal, whatever the selected value notation,
|
||||||
|
* because the unit box beside it already shows the unit; the unit is written out only when it
|
||||||
|
* differs from the one in the box.
|
||||||
|
*/
|
||||||
|
static String formatForField(AbstractMeasure<?> measure, boolean unitShownInBox) {
|
||||||
|
if (measure == null || measure.getValue() == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
String number;
|
||||||
|
synchronized (DECIMAL_FORMAT) {
|
||||||
|
number = DECIMAL_FORMAT.format(measure.getValue());
|
||||||
|
}
|
||||||
|
return unitShownInBox || measure.getUnit() == null ? number : number + measure.getUnit();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyValue(PropertyWrapper property, Double newValue, Object unit) {
|
||||||
|
try {
|
||||||
|
if (newValue == null) {
|
||||||
|
property.setValue(null);
|
||||||
|
} else {
|
||||||
|
Constructor<?> ctor = property.getType().getConstructors()[0];
|
||||||
|
property.setValue((AbstractMeasure<?>) ctor.newInstance(newValue, unit));
|
||||||
|
}
|
||||||
|
property.setChanged(true);
|
||||||
|
valueField.setBackground(oldBg);
|
||||||
|
unitBox.setBackground(oldBg);
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOG.error("Error while updating property value", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private Double getFieldValue() {
|
||||||
|
if (valueField instanceof DoubleTextField doubleField) {
|
||||||
|
return doubleField.getValue();
|
||||||
|
}
|
||||||
|
ParsedMeasure<Unit> parsed = ((MeasureTextField<ParsedMeasure<Unit>>) valueField).getValue();
|
||||||
|
return parsed == null ? null : parsed.value();
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private void setFieldValue(AbstractMeasure<?> measure) {
|
||||||
|
if (valueField instanceof DoubleTextField doubleField) {
|
||||||
|
doubleField.setValue(measure == null ? null : measure.getValue());
|
||||||
|
} else {
|
||||||
|
((MeasureTextField<ParsedMeasure<Unit>>) valueField).setDisplayText(formatForField(measure, true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private AbstractMeasure<?> convertUnitsIfNeeded(AbstractMeasure<?> value) {
|
private AbstractMeasure<?> convertUnitsIfNeeded(AbstractMeasure<?> value) {
|
||||||
if (ConfigurationManager.getInstance().readBoolean(IPlugInPort.AUTO_UNIT_CONVERSION_KEY, true)
|
if (ConfigurationManager.getInstance().readBoolean(IPlugInPort.AUTO_UNIT_CONVERSION_KEY, true)
|
||||||
&& value instanceof Size sizeValue) {
|
&& value instanceof Size sizeValue) {
|
||||||
|
|||||||
@@ -0,0 +1,131 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
*/
|
||||||
|
package org.diylc.swing.gui.editor;
|
||||||
|
|
||||||
|
import java.awt.BorderLayout;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import javax.swing.BorderFactory;
|
||||||
|
import javax.swing.JLabel;
|
||||||
|
import javax.swing.JTextField;
|
||||||
|
import javax.swing.event.DocumentEvent;
|
||||||
|
import javax.swing.event.DocumentListener;
|
||||||
|
|
||||||
|
import org.diylc.utils.IconLoader;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link JTextField} that parses its text on every change and fires {@link #VALUE_PROPERTY} with
|
||||||
|
* the result, showing a warning icon while the text cannot be parsed. Unlike
|
||||||
|
* {@code DoubleTextField} the text is never rewritten while the user types; call
|
||||||
|
* {@link #setDisplayText(String)} to reformat it.
|
||||||
|
*
|
||||||
|
* @author Branislav Stojkovic
|
||||||
|
*
|
||||||
|
* @param <T> type of the parsed value
|
||||||
|
*/
|
||||||
|
public class MeasureTextField<T> extends JTextField {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
public static final String VALUE_PROPERTY = "MeasureValue";
|
||||||
|
|
||||||
|
private final Function<String, T> parser;
|
||||||
|
private final JLabel errorLabel;
|
||||||
|
|
||||||
|
private T value;
|
||||||
|
private boolean ignoreChanges = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param text initial text
|
||||||
|
* @param parser turns non-empty text into a value, throwing {@link IllegalArgumentException} when
|
||||||
|
* it cannot
|
||||||
|
*/
|
||||||
|
public MeasureTextField(String text, Function<String, T> parser) {
|
||||||
|
super();
|
||||||
|
this.parser = parser;
|
||||||
|
setLayout(new BorderLayout());
|
||||||
|
errorLabel = new JLabel(IconLoader.Warning.getIcon());
|
||||||
|
errorLabel.setBorder(BorderFactory.createEmptyBorder(3, 0, 0, 0));
|
||||||
|
add(errorLabel, BorderLayout.EAST);
|
||||||
|
setDisplayText(text);
|
||||||
|
getDocument().addDocumentListener(new DocumentListener() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void changedUpdate(DocumentEvent e) {
|
||||||
|
textChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void insertUpdate(DocumentEvent e) {
|
||||||
|
textChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void removeUpdate(DocumentEvent e) {
|
||||||
|
textChanged();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public T getValue() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replaces the text without firing {@link #VALUE_PROPERTY}.
|
||||||
|
*/
|
||||||
|
public void setDisplayText(String text) {
|
||||||
|
ignoreChanges = true;
|
||||||
|
try {
|
||||||
|
setText(text);
|
||||||
|
value = parseQuietly(text);
|
||||||
|
errorLabel.setVisible(value == null);
|
||||||
|
} finally {
|
||||||
|
ignoreChanges = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private T parseQuietly(String text) {
|
||||||
|
try {
|
||||||
|
return text == null || text.trim().isEmpty() ? null : parser.apply(text);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void textChanged() {
|
||||||
|
if (ignoreChanges) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
T newValue;
|
||||||
|
try {
|
||||||
|
newValue = getText().trim().isEmpty() ? null : parser.apply(getText());
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
value = null;
|
||||||
|
errorLabel.setVisible(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
T oldValue = value;
|
||||||
|
value = newValue;
|
||||||
|
errorLabel.setVisible(false);
|
||||||
|
firePropertyChange(VALUE_PROPERTY, oldValue, newValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -40,6 +40,7 @@ import org.diylc.core.Theme;
|
|||||||
import org.diylc.utils.FlagLoader;
|
import org.diylc.utils.FlagLoader;
|
||||||
import org.diylc.lang.LangUtil;
|
import org.diylc.lang.LangUtil;
|
||||||
import org.diylc.lang.Language;
|
import org.diylc.lang.Language;
|
||||||
|
import org.diylc.presenter.ValueNotationBinder;
|
||||||
import org.diylc.swing.ActionFactory;
|
import org.diylc.swing.ActionFactory;
|
||||||
import org.diylc.swing.ISwingUI;
|
import org.diylc.swing.ISwingUI;
|
||||||
import org.diylc.utils.IconLoader;
|
import org.diylc.utils.IconLoader;
|
||||||
@@ -58,6 +59,7 @@ public class ConfigPlugin implements IPlugIn {
|
|||||||
private static final String THEME_MENU = "Theme";
|
private static final String THEME_MENU = "Theme";
|
||||||
private static final String RULER_MENU = "Ruler Inch Subdivision...";
|
private static final String RULER_MENU = "Ruler Inch Subdivision...";
|
||||||
private static final String SNAP_MENU = "Snap To";
|
private static final String SNAP_MENU = "Snap To";
|
||||||
|
private static final String VALUE_NOTATION_MENU = "Value Notation";
|
||||||
private static final String CATEGORIES_MENU = "Component Categories";
|
private static final String CATEGORIES_MENU = "Component Categories";
|
||||||
private static final String COMPONENT_BROWSER_MENU = "Toolbox";
|
private static final String COMPONENT_BROWSER_MENU = "Toolbox";
|
||||||
public static final String COMPONENT_BROWSER = "componentBrowser";
|
public static final String COMPONENT_BROWSER = "componentBrowser";
|
||||||
@@ -196,6 +198,17 @@ public class ConfigPlugin implements IPlugIn {
|
|||||||
ActionFactory.getInstance()
|
ActionFactory.getInstance()
|
||||||
.createConfigAction(plugInPort, "Sticky Points", IPlugInPort.STICKY_POINTS_KEY, true), CONFIG_MENU);
|
.createConfigAction(plugInPort, "Sticky Points", IPlugInPort.STICKY_POINTS_KEY, true), CONFIG_MENU);
|
||||||
|
|
||||||
|
swingUI.injectSubmenu(VALUE_NOTATION_MENU, IconLoader.Decimal.getIcon(), CONFIG_MENU);
|
||||||
|
swingUI.injectMenuAction(
|
||||||
|
ActionFactory.getInstance().createToggleAction(IPlugInPort.VALUE_NOTATION_DECIMAL,
|
||||||
|
IPlugInPort.VALUE_NOTATION_KEY, VALUE_NOTATION_MENU, IPlugInPort.VALUE_NOTATION_DEFAULT),
|
||||||
|
VALUE_NOTATION_MENU);
|
||||||
|
swingUI.injectMenuAction(
|
||||||
|
ActionFactory.getInstance().createToggleAction(IPlugInPort.VALUE_NOTATION_RKM,
|
||||||
|
IPlugInPort.VALUE_NOTATION_KEY, VALUE_NOTATION_MENU, IPlugInPort.VALUE_NOTATION_DEFAULT),
|
||||||
|
VALUE_NOTATION_MENU);
|
||||||
|
ValueNotationBinder.bind(ConfigurationManager.getInstance(), plugInPort::refresh);
|
||||||
|
|
||||||
Map<String, String> themeContents = ResourceUtils.getResourceContents(THEMES_DIR);
|
Map<String, String> themeContents = ResourceUtils.getResourceContents(THEMES_DIR);
|
||||||
if (!themeContents.isEmpty()) {
|
if (!themeContents.isEmpty()) {
|
||||||
XStream xStream = new XStream(new DomDriver());
|
XStream xStream = new XStream(new DomDriver());
|
||||||
|
|||||||
@@ -51,8 +51,8 @@ public enum IconLoader {
|
|||||||
"coffeebean_edit.png"), Component("component.png"), ComponentAdd(
|
"coffeebean_edit.png"), Component("component.png"), ComponentAdd(
|
||||||
"component_add.png"), ComponentLarge("component_large.png"), ComponentPreferences(
|
"component_add.png"), ComponentLarge("component_large.png"), ComponentPreferences(
|
||||||
"component_preferences.png"), ComponentReplace("component_replace.png"), Copy(
|
"component_preferences.png"), ComponentReplace("component_replace.png"), Copy(
|
||||||
"copy.png"), Cut("cut.png"), Dashboard("dashboard.png"), DataFind("data_find.png"), Delete(
|
"copy.png"), Cut("cut.png"), Dashboard("dashboard.png"), DataFind("data_find.png"), Decimal(
|
||||||
"delete.png"), DiskBlue("disk_blue.png"), DistributeCentersHorizontal(
|
"decimal.png"), Delete("delete.png"), DiskBlue("disk_blue.png"), DistributeCentersHorizontal(
|
||||||
"distribute_centers_horizontal.png"), DistributeCentersVertical(
|
"distribute_centers_horizontal.png"), DistributeCentersVertical(
|
||||||
"distribute_centers_vertical.png"), DistributeSpacingHorizontal(
|
"distribute_centers_vertical.png"), DistributeSpacingHorizontal(
|
||||||
"distribute_spacing_horizontal.png"), DistributeSpacingVertical(
|
"distribute_spacing_horizontal.png"), DistributeSpacingVertical(
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 769 B |
@@ -729,3 +729,6 @@ Are you sure you want to delete variant "%s"?|Möchten Sie die Variante "%s" wir
|
|||||||
Delete|Löschen
|
Delete|Löschen
|
||||||
Set default model|Als Standardmodell festlegen
|
Set default model|Als Standardmodell festlegen
|
||||||
Remove default model|Standardmodell entfernen
|
Remove default model|Standardmodell entfernen
|
||||||
|
Value Notation|Wertnotation
|
||||||
|
Decimal (4.7K, 0.22uF)|Dezimal (4.7K, 0.22uF)
|
||||||
|
RKM (4K7, 0u22)|RKM (4K7, 0u22)
|
||||||
|
|||||||
@@ -722,3 +722,6 @@ Are you sure you want to delete variant "%s"?
|
|||||||
Delete
|
Delete
|
||||||
Set default model
|
Set default model
|
||||||
Remove default model
|
Remove default model
|
||||||
|
Value Notation
|
||||||
|
Decimal (4.7K, 0.22uF)
|
||||||
|
RKM (4K7, 0u22)
|
||||||
|
|||||||
@@ -725,3 +725,6 @@ Are you sure you want to delete variant "%s"?|¿Seguro que desea eliminar la var
|
|||||||
Delete|Eliminar
|
Delete|Eliminar
|
||||||
Set default model|Establecer como modelo predeterminado
|
Set default model|Establecer como modelo predeterminado
|
||||||
Remove default model|Quitar modelo predeterminado
|
Remove default model|Quitar modelo predeterminado
|
||||||
|
Value Notation|Notación de valores
|
||||||
|
Decimal (4.7K, 0.22uF)|Decimal (4.7K, 0.22uF)
|
||||||
|
RKM (4K7, 0u22)|RKM (4K7, 0u22)
|
||||||
|
|||||||
@@ -724,3 +724,6 @@ Are you sure you want to delete variant "%s"?|Voulez-vous vraiment supprimer la
|
|||||||
Delete|Supprimer
|
Delete|Supprimer
|
||||||
Set default model|Définir comme modèle par défaut
|
Set default model|Définir comme modèle par défaut
|
||||||
Remove default model|Retirer le modèle par défaut
|
Remove default model|Retirer le modèle par défaut
|
||||||
|
Value Notation|Notation des valeurs
|
||||||
|
Decimal (4.7K, 0.22uF)|Décimale (4.7K, 0.22uF)
|
||||||
|
RKM (4K7, 0u22)|RKM (4K7, 0u22)
|
||||||
|
|||||||
@@ -726,3 +726,6 @@ Are you sure you want to delete variant "%s"?|Eliminare davvero la variante "%s"
|
|||||||
Delete|Elimina
|
Delete|Elimina
|
||||||
Set default model|Imposta come modello predefinito
|
Set default model|Imposta come modello predefinito
|
||||||
Remove default model|Rimuovi modello predefinito
|
Remove default model|Rimuovi modello predefinito
|
||||||
|
Value Notation|Notazione dei valori
|
||||||
|
Decimal (4.7K, 0.22uF)|Decimale (4.7K, 0.22uF)
|
||||||
|
RKM (4K7, 0u22)|RKM (4K7, 0u22)
|
||||||
|
|||||||
@@ -730,3 +730,6 @@ Are you sure you want to delete variant "%s"?|Weet u zeker dat u variant "%s" wi
|
|||||||
Delete|Verwijderen
|
Delete|Verwijderen
|
||||||
Set default model|Instellen als standaardmodel
|
Set default model|Instellen als standaardmodel
|
||||||
Remove default model|Standaardmodel verwijderen
|
Remove default model|Standaardmodel verwijderen
|
||||||
|
Value Notation|Waardenotatie
|
||||||
|
Decimal (4.7K, 0.22uF)|Decimaal (4.7K, 0.22uF)
|
||||||
|
RKM (4K7, 0u22)|RKM (4K7, 0u22)
|
||||||
|
|||||||
@@ -725,3 +725,6 @@ Are you sure you want to delete variant "%s"?|Tem certeza de que deseja excluir
|
|||||||
Delete|Excluir
|
Delete|Excluir
|
||||||
Set default model|Definir como modelo padrão
|
Set default model|Definir como modelo padrão
|
||||||
Remove default model|Remover modelo padrão
|
Remove default model|Remover modelo padrão
|
||||||
|
Value Notation|Notação de valores
|
||||||
|
Decimal (4.7K, 0.22uF)|Decimal (4.7K, 0.22uF)
|
||||||
|
RKM (4K7, 0u22)|RKM (4K7, 0u22)
|
||||||
|
|||||||
@@ -721,3 +721,6 @@ Are you sure you want to delete variant "%s"?|Da li ste sigurni da želite da ob
|
|||||||
Delete|Obriši
|
Delete|Obriši
|
||||||
Set default model|Postavi kao podrazumevani model
|
Set default model|Postavi kao podrazumevani model
|
||||||
Remove default model|Ukloni podrazumevani model
|
Remove default model|Ukloni podrazumevani model
|
||||||
|
Value Notation|Zapis vrednosti
|
||||||
|
Decimal (4.7K, 0.22uF)|Decimalni (4.7K, 0.22uF)
|
||||||
|
RKM (4K7, 0u22)|RKM (4K7, 0u22)
|
||||||
|
|||||||
@@ -723,3 +723,6 @@ Are you sure you want to delete variant "%s"?|क्या आप वाकई
|
|||||||
Delete|हटाएँ
|
Delete|हटाएँ
|
||||||
Set default model|डिफ़ॉल्ट मॉडल सेट करें
|
Set default model|डिफ़ॉल्ट मॉडल सेट करें
|
||||||
Remove default model|डिफ़ॉल्ट मॉडल हटाएँ
|
Remove default model|डिफ़ॉल्ट मॉडल हटाएँ
|
||||||
|
Value Notation|मान संकेतन
|
||||||
|
Decimal (4.7K, 0.22uF)|दशमलव (4.7K, 0.22uF)
|
||||||
|
RKM (4K7, 0u22)|RKM (4K7, 0u22)
|
||||||
|
|||||||
@@ -723,3 +723,6 @@ Are you sure you want to delete variant "%s"?|确定要删除变体“%s”吗
|
|||||||
Delete|删除
|
Delete|删除
|
||||||
Set default model|设为默认型号
|
Set default model|设为默认型号
|
||||||
Remove default model|取消默认型号
|
Remove default model|取消默认型号
|
||||||
|
Value Notation|数值表示法
|
||||||
|
Decimal (4.7K, 0.22uF)|小数 (4.7K, 0.22uF)
|
||||||
|
RKM (4K7, 0u22)|RKM (4K7, 0u22)
|
||||||
|
|||||||
@@ -0,0 +1,185 @@
|
|||||||
|
# RKM Value Notation for Resistance and Capacitance
|
||||||
|
|
||||||
|
Let users read and type resistor and capacitor values in RKM (IEC 60062) notation, where the unit
|
||||||
|
letter replaces the decimal point (`4K7`, `0R22`, `2n2`, `0u22`), as well as in today's decimal
|
||||||
|
form (`4.7K`, `0.22uF`). Typing a value with a unit letter in it, in either notation, also sets the
|
||||||
|
unit, so `4K7` typed into a field whose unit is `Ω` becomes 4.7 K.
|
||||||
|
|
||||||
|
## 1. User-facing behaviour
|
||||||
|
|
||||||
|
### Config menu
|
||||||
|
|
||||||
|
One radio submenu in `ConfigPlugin` covering both resistance and capacitance, built the same way
|
||||||
|
as `Snap To` and `Ruler Inch Subdivision...` (`ToggleAction`, string config key, title stored as
|
||||||
|
the value):
|
||||||
|
|
||||||
|
```
|
||||||
|
Config
|
||||||
|
...
|
||||||
|
Value Notation > (after Sticky Points, alphabetical like the rest)
|
||||||
|
(o) Decimal (4.7K, 0.22uF)
|
||||||
|
( ) RKM (4K7, 0u22)
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
- Key `IPlugInPort.VALUE_NOTATION_KEY = "valueNotation"`, values `VALUE_NOTATION_DECIMAL` /
|
||||||
|
`VALUE_NOTATION_RKM`, default decimal.
|
||||||
|
- Switching notation repaints the canvas immediately, so labels on the board change at once.
|
||||||
|
|
||||||
|
### What the notation controls (display)
|
||||||
|
|
||||||
|
The notation governs `Resistance.toString()` and `Capacitance.toString()` and therefore everything
|
||||||
|
built on them: labels drawn on the canvas, `getValueForDisplay()` (search, BOM, netlist text,
|
||||||
|
project explorer, AI assistant context). The edit dialog's value field is the exception: it always
|
||||||
|
shows the bare decimal number, because the unit box beside it already carries the unit and an RKM
|
||||||
|
code there would repeat it.
|
||||||
|
|
||||||
|
| Value | Decimal (unchanged) | RKM |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 4.7 Ω | `4.7Ω` | `4R7` |
|
||||||
|
| 0.22 Ω | `0.22Ω` | `0R22` |
|
||||||
|
| 100 Ω | `100Ω` | `100R` |
|
||||||
|
| 4.7 K | `4.7K` | `4K7` |
|
||||||
|
| 47 K | `47K` | `47K` |
|
||||||
|
| 1 M | `1M` | `1M` |
|
||||||
|
| 0.22 uF | `0.22uF` | `0u22` |
|
||||||
|
| 2.2 nF | `2.2nF` | `2n2` |
|
||||||
|
| 100 nF | `100nF` | `100n` |
|
||||||
|
| 4.7 pF | `4.7pF` | `4p7` |
|
||||||
|
| 1000 uF | `1000uF` | `1000u` |
|
||||||
|
|
||||||
|
Rules: RKM keeps the stored unit (it never renormalises 4700 Ω to 4K7), uses `R` for ohms, drops
|
||||||
|
the trailing `F` on capacitance, puts the unit letter where the decimal point was, and writes it
|
||||||
|
as a suffix for whole numbers. The digits after the letter are rounded the same way as today's
|
||||||
|
`0.####` decimal format.
|
||||||
|
|
||||||
|
### What the user can type (input)
|
||||||
|
|
||||||
|
The value field accepts, in **both** notations:
|
||||||
|
|
||||||
|
- a bare number (`4.7`, `0,22`) — the unit is unchanged, as today;
|
||||||
|
- a decimal number with a unit (`4.7k`, `0.22uF`, `220 nF`, `4.7ohm`, `4.7Ω`);
|
||||||
|
- an RKM code (`4K7`, `4k7`, `0R22`, `R22`, `2n2`, `0u22`, `4µ7`, `1M`, `100R`).
|
||||||
|
|
||||||
|
When the text carries a unit, the unit combo switches to it and the property gets the new value
|
||||||
|
and unit together. Accepted unit letters, case-insensitive except where noted:
|
||||||
|
|
||||||
|
- Resistance: `R`, `Ω`, `ohm`/`ohms` → Ω; `K`/`k` → K; `M`/`m`/`Meg` → M (there is no milliohm
|
||||||
|
unit, so a lower-case `m` on a resistor means mega, as it does on many parts lists).
|
||||||
|
- Capacitance: `p` → pF; `n` → nF; `u`/`µ` → uF; `m` → mF (**case-sensitive**: `M` is not
|
||||||
|
accepted, to avoid reading "mega"); `F` alone → F. A trailing `F` after any of these is
|
||||||
|
optional (`4u7F`, `2.2nF`).
|
||||||
|
|
||||||
|
A unit letter that does not belong to the measure (`4u7` on a resistor) shows the existing warning
|
||||||
|
icon, as any unparseable text does today.
|
||||||
|
|
||||||
|
**Commit behaviour.** The property is updated on every keystroke, as today — the edit dialog's
|
||||||
|
Enter key commits straight away, so it cannot wait for focus loss. When the field loses focus,
|
||||||
|
the text is rewritten as the bare decimal number (`4K7` typed → `4.7` with the combo showing `K`),
|
||||||
|
whichever notation is selected. The text is not reformatted while the user is typing, so the
|
||||||
|
caret never jumps.
|
||||||
|
|
||||||
|
## 2. Design
|
||||||
|
|
||||||
|
### Core (`diylc-core`, `org.diylc.core.measures`)
|
||||||
|
|
||||||
|
- **`ValueNotation`** — new enum `DECIMAL, RKM` holding the formatting and parsing logic, so the
|
||||||
|
two measure classes stay thin:
|
||||||
|
- `String format(double value, Unit unit, String rkmSymbol)` — decimal falls through to the
|
||||||
|
existing `0.####` + `unit.toString()`; RKM implements the rules in §1.
|
||||||
|
- `static ParsedMeasure parse(String text, ...)` — returns value + unit (unit `null` for a bare
|
||||||
|
number), or throws `IllegalArgumentException`. One small record `ParsedMeasure<U>` at the
|
||||||
|
bottom of the file.
|
||||||
|
- **Current notation.** One `private static volatile ValueNotation current` on `ValueNotation`
|
||||||
|
with static `getCurrent()` / `setCurrent()`, shared by `Resistance` and `Capacitance`.
|
||||||
|
`toString()` reads the field, never
|
||||||
|
`ConfigurationManager`: it runs from `draw()` on every repaint. Default `DECIMAL`, so headless
|
||||||
|
rendering, the regression suite and unit tests are unchanged unless they opt in.
|
||||||
|
- **Unit display symbol for RKM.** `ResistanceUnit` and `CapacitanceUnit` get an `rkmSymbol`
|
||||||
|
constructor argument (`R`/`K`/`M`, `p`/`n`/`u`/`m`/`F`). Enum constant names and `toString()`
|
||||||
|
are untouched — XStream stores the constant name, so files are unaffected.
|
||||||
|
- **Parsing.** `Resistance.parseResistance` / `Capacitance.parseCapacitance` delegate to
|
||||||
|
`ValueNotation.parse`, so RKM is accepted everywhere a string is turned into a value: the
|
||||||
|
editor, `V1FileParser`, `TangoNetlistParser`, `CapacitorDatasheetService` and — important once
|
||||||
|
`toString()` can emit `4K7` — the AI assistant, which receives values as text and writes them
|
||||||
|
back through these parsers. Existing accepted inputs must keep parsing identically.
|
||||||
|
- `Inductance` and `Power` are out of scope; their names are left as they are.
|
||||||
|
|
||||||
|
### Wiring the setting
|
||||||
|
|
||||||
|
`org.diylc.presenter.ValueNotationBinder.bind(configManager, refresh)` reads the key once,
|
||||||
|
pushes it into `ValueNotation.setCurrent()`, and on change updates the static, calls
|
||||||
|
`DrawingCache.Instance.clear()` (cached component images contain the old labels) and refreshes
|
||||||
|
the canvas. It is called from `ConfigPlugin.connect()`, not from the `Presenter` constructor:
|
||||||
|
the notation is global, and the many secondary presenters (project preview, cloud thumbnails,
|
||||||
|
batch converter, regression runner) are built on in-memory configurations and would reset it to
|
||||||
|
decimal.
|
||||||
|
|
||||||
|
### Swing (`diylc-swing`)
|
||||||
|
|
||||||
|
- **`MeasureTextField`** — new field in `org.diylc.swing.gui.editor`, beside `MeasureEditor`.
|
||||||
|
Same look as `DoubleTextField` (warning icon on the right) but it parses through
|
||||||
|
`ValueNotation.parse` for the measure type and fires a `MEASURE_PROPERTY` change carrying
|
||||||
|
value + optional unit; on `focusLost` it reformats its text. It is generic over the unit enum
|
||||||
|
so it can serve `Inductance` later without changes.
|
||||||
|
|
||||||
|
Recommendation: keep it in DIYLC rather than extending `DoubleTextField` in
|
||||||
|
`bancika-gui-framework`. The framework is consumed as the vendored
|
||||||
|
`lib/swing-framework-1.1.jar`, so a change there means rebuilding and re-vendoring the jar for a
|
||||||
|
widget whose parsing rules are DIYLC domain knowledge. `DoubleTextField` keeps serving
|
||||||
|
`PercentageEditor`, `LoadlineEditorFrame` and every other plain-number measure.
|
||||||
|
|
||||||
|
One `DoubleTextField` bug is worth fixing in the framework separately, and the new field must
|
||||||
|
not copy it: `DecimalFormat.parseObject(String)` accepts a valid prefix, so `4R7` silently
|
||||||
|
parses as `4` today, with no warning.
|
||||||
|
- **`MeasureEditor`** — uses `MeasureTextField` when the property type is `Resistance` or
|
||||||
|
`Capacitance`, `DoubleTextField` otherwise. On a value + unit event it sets the combo's
|
||||||
|
selected unit behind a guard flag, so the combo's own `ActionListener` does not fire a second
|
||||||
|
`setValue`. Read-only properties keep today's behaviour (disabled field, combo converts).
|
||||||
|
- **`MeasureArrayEditor`** (`MultiSectionCapacitor`) — each section value is parsed with the same
|
||||||
|
rules. Units travel per section in the text (`47u / 22u / 10u`) and a section without a unit
|
||||||
|
takes the combo's unit, as today. The combo shows the first section's unit; in decimal notation a
|
||||||
|
section in a different unit is written with its unit (`47 / 100nF`). Changing the combo applies
|
||||||
|
its unit to every section, as before.
|
||||||
|
- **`ConfigPlugin`** — the `Value Notation` submenu (`IconLoader.Decimal`), and the call to
|
||||||
|
`ValueNotationBinder.bind`.
|
||||||
|
|
||||||
|
### Localization
|
||||||
|
|
||||||
|
New keys in `English.txt` and every other file in `lang/`: `Value Notation`,
|
||||||
|
`Decimal (4.7K, 0.22uF)`, `RKM (4K7, 0u22)`.
|
||||||
|
Because `ToggleAction` stores its title as the config value, the stored values are the English
|
||||||
|
titles and `MainFrame` translates only what is shown.
|
||||||
|
|
||||||
|
## 3. Compatibility
|
||||||
|
|
||||||
|
- `.diy` files store `value` and the unit enum name; neither changes. No serialization impact.
|
||||||
|
- With the default (decimal) every string the app produces is byte-identical to today, so
|
||||||
|
regression references do not move. A regression run is still worth doing after implementation,
|
||||||
|
since `parseResistance` / `parseCapacitance` change.
|
||||||
|
- A user who switches to RKM gets RKM in BOM and netlist exports as well. That is the intended
|
||||||
|
"everywhere" behaviour; anything that re-imports those files goes through the RKM-aware
|
||||||
|
parsers.
|
||||||
|
|
||||||
|
## 4. Tests (`diylc-core`, JUnit 4)
|
||||||
|
|
||||||
|
`ValueNotationTest`:
|
||||||
|
|
||||||
|
- formatting: every row of the §1 table, both notations; zero, null, values ≥ 1000 in their unit,
|
||||||
|
rounding at the fourth decimal;
|
||||||
|
- parsing: every input form in §1, per measure; case rules (`m` vs `M` for capacitance); comma as
|
||||||
|
decimal separator; rejects `4u7` on a resistor, `4K7K`, `K`, empty text;
|
||||||
|
- round trip: `parse(format(x))` returns `x` for both notations;
|
||||||
|
- regression guard: every string currently accepted by `parseResistance` / `parseCapacitance`
|
||||||
|
(the inputs in `V1FileParser`, `TangoNetlistParser` and `CapacitorDatasheetService` tests and
|
||||||
|
`AiEditScriptEditorTest`) still parses to the same value.
|
||||||
|
|
||||||
|
## 5. Status and next steps
|
||||||
|
|
||||||
|
Implemented as described; `mvn -q test` passes, including `ValueNotationTest`.
|
||||||
|
|
||||||
|
1. Try it in the running app: the edit dialog (single value and `MultiSectionCapacitor`), canvas
|
||||||
|
labels after switching notation, BOM export.
|
||||||
|
2. Run the regression suite to confirm decimal output is unchanged.
|
||||||
|
3. Optional, in `bancika-gui-framework`: stop `DoubleTextField` / `DoubleArrayTextField`
|
||||||
|
accepting a valid prefix (`4R7` read as `4`).
|
||||||
Reference in New Issue
Block a user