diff --git a/Documents/Changelog/Changelog.md b/Documents/Changelog/Changelog.md
index 6654f05a61..f6b5bf3cb5 100644
--- a/Documents/Changelog/Changelog.md
+++ b/Documents/Changelog/Changelog.md
@@ -3,6 +3,7 @@
====
## 2025-11-xx - Build 2511 (V10 - alpha) - November 2025
+* Resolved [#2461](https://github.com/Krypton-Suite/Standard-Toolkit/issues/2461), (feat) Add `KryptonCalcInput` edit+dropdown control.
* Resolved [#2480](https://github.com/Krypton-Suite/Standard-Toolkit/issues/2480). `KryptonForm`'s 'InternalPanel' designer issues
* Resolved [#2512](https://github.com/Krypton-Suite/Standard-Toolkit/issues/2512), Added borders with straight corners (`LinearBorder2`) and adjusted the colors in the `KryptonRibbon` in the `Microsoft365` themes. Adjusted the design of the `RibbonQATButton`
* Implemented [#2503](https://github.com/Krypton-Suite/Standard-Toolkit/issues/2503), Add the ability to create zip files for binaries
diff --git a/Scripts/make_calc_icon.py b/Scripts/make_calc_icon.py
new file mode 100644
index 0000000000..1b02e6ef1f
--- /dev/null
+++ b/Scripts/make_calc_icon.py
@@ -0,0 +1,160 @@
+# Python 3.10+
+# Creates a 16x16 24-bit BMP depicting a simple calculator.
+# Default output: Source\Krypton Components\Krypton.Toolkit\Resources\KryptonCalcInput.bmp
+
+from __future__ import annotations
+
+from pathlib import Path
+from typing import List, Tuple
+import sys
+
+# Image size
+WIDTH = 16
+HEIGHT = 16
+
+# Colors (B, G, R) for BMP storage convenience
+BGR = Tuple[int, int, int]
+
+MAGENTA: BGR = (255, 0, 255) # transparent key background for legacy use
+GRAY: BGR = (200, 200, 200) # body
+DARK: BGR = (30, 30, 30) # outline
+DISPLAY: BGR = (170, 220, 180) # display area
+BTN: BGR = (230, 230, 230) # buttons
+BTN_EDGE: BGR = (60, 60, 60) # button outline
+EQUALS: BGR = (200, 210, 250) # equals button
+
+
+def clamp8(v: int) -> int:
+ if v < 0:
+ return 0
+ if v > 255:
+ return 255
+ return v
+
+
+def new_canvas(color: BGR) -> List[List[BGR]]:
+ return [[color for _ in range(WIDTH)] for _ in range(HEIGHT)]
+
+
+def set_px(img: List[List[BGR]], x: int, y: int, color: BGR) -> None:
+ if 0 <= x < WIDTH and 0 <= y < HEIGHT:
+ b, g, r = color
+ img[y][x] = (clamp8(b), clamp8(g), clamp8(r))
+
+
+def fill_rect(img: List[List[BGR]], x0: int, y0: int, x1: int, y1: int, color: BGR) -> None:
+ if x1 < x0:
+ x0, x1 = x1, x0
+ if y1 < y0:
+ y0, y1 = y1, y0
+ for y in range(max(0, y0), min(HEIGHT - 1, y1) + 1):
+ for x in range(max(0, x0), min(WIDTH - 1, x1) + 1):
+ set_px(img, x, y, color)
+
+
+def draw_rect(img: List[List[BGR]], x0: int, y0: int, x1: int, y1: int, color: BGR) -> None:
+ if x1 < x0:
+ x0, x1 = x1, x0
+ if y1 < y0:
+ y0, y1 = y1, y0
+ for x in range(x0, x1 + 1):
+ set_px(img, x, y0, color)
+ set_px(img, x, y1, color)
+ for y in range(y0, y1 + 1):
+ set_px(img, x0, y, color)
+ set_px(img, x1, y, color)
+
+
+def draw_calculator() -> List[List[BGR]]:
+ img = new_canvas(MAGENTA)
+
+ # Body
+ fill_rect(img, 1, 1, 14, 14, GRAY)
+ draw_rect(img, 1, 1, 14, 14, DARK)
+
+ # Display
+ fill_rect(img, 3, 3, 12, 6, DISPLAY)
+ draw_rect(img, 3, 3, 12, 6, DARK)
+
+ # Buttons 2 rows of 3 + wide equals bottom
+ buttons = [
+ (3, 7, 5, 9), (7, 7, 9, 9), (11, 7, 13, 9),
+ (3, 10, 5, 12), (7, 10, 9, 12), (11, 10, 13, 12),
+ ]
+ for (x0, y0, x1, y1) in buttons:
+ fill_rect(img, x0, y0, x1, y1, BTN)
+ draw_rect(img, x0, y0, x1, y1, BTN_EDGE)
+
+ # Equals wide
+ fill_rect(img, 3, 13, 13, 14, EQUALS)
+ draw_rect(img, 3, 13, 13, 14, BTN_EDGE)
+
+ return img
+
+
+def to_24bit_bottom_up(img: List[List[BGR]]) -> bytes:
+ # Each row is WIDTH*3 bytes; for width=16 -> 48 bytes (already multiple of 4, so no padding).
+ out = bytearray()
+ for y in range(HEIGHT - 1, -1, -1):
+ for x in range(WIDTH):
+ b, g, r = img[y][x]
+ out.extend((b, g, r))
+ return bytes(out)
+
+
+def write_bmp_24(path: Path, img: List[List[BGR]]) -> None:
+ pixel_data = to_24bit_bottom_up(img)
+ file_header_size = 14
+ info_header_size = 40
+ off_bits = file_header_size + info_header_size
+ size_image = len(pixel_data)
+ file_size = off_bits + size_image
+
+ # BITMAPFILEHEADER
+ bfType = b"BM"
+ bfSize = file_size.to_bytes(4, "little")
+ bfReserved1 = (0).to_bytes(2, "little")
+ bfReserved2 = (0).to_bytes(2, "little")
+ bfOffBits = off_bits.to_bytes(4, "little")
+ file_header = bfType + bfSize + bfReserved1 + bfReserved2 + bfOffBits
+
+ # BITMAPINFOHEADER
+ biSize = info_header_size.to_bytes(4, "little")
+ biWidth = WIDTH.to_bytes(4, "little", signed=True)
+ biHeight = HEIGHT.to_bytes(4, "little", signed=True)
+ biPlanes = (1).to_bytes(2, "little")
+ biBitCount = (24).to_bytes(2, "little")
+ biCompression = (0).to_bytes(4, "little") # BI_RGB
+ biSizeImage = size_image.to_bytes(4, "little")
+ biXPelsPerMeter = (0).to_bytes(4, "little")
+ biYPelsPerMeter = (0).to_bytes(4, "little")
+ biClrUsed = (0).to_bytes(4, "little")
+ biClrImportant = (0).to_bytes(4, "little")
+ info_header = (
+ biSize + biWidth + biHeight + biPlanes + biBitCount + biCompression +
+ biSizeImage + biXPelsPerMeter + biYPelsPerMeter + biClrUsed + biClrImportant
+ )
+
+ path.parent.mkdir(parents=True, exist_ok=True)
+ with path.open("wb") as f:
+ f.write(file_header)
+ f.write(info_header)
+ f.write(pixel_data)
+
+
+def default_output_path() -> Path:
+ # Scripts/make_calc_icon.py -> repo root -> Source/.../Resources/KryptonCalcInput.bmp
+ repo_root = Path(__file__).resolve().parents[1]
+ return repo_root / "Source" / "Krypton Components" / "Krypton.Toolkit" / "Resources" / "KryptonCalcInput.bmp"
+
+
+def main(argv: list[str]) -> int:
+ out_path = Path(argv[1]) if len(argv) > 1 else default_output_path()
+ img = draw_calculator()
+ write_bmp_24(out_path, img)
+ print(f"Wrote 16x16 BMP to {out_path}")
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main(sys.argv))
diff --git a/Source/Krypton Components/Krypton.Toolkit/Controls Toolkit/KryptonCalcInput.cs b/Source/Krypton Components/Krypton.Toolkit/Controls Toolkit/KryptonCalcInput.cs
new file mode 100644
index 0000000000..e29793531b
--- /dev/null
+++ b/Source/Krypton Components/Krypton.Toolkit/Controls Toolkit/KryptonCalcInput.cs
@@ -0,0 +1,1715 @@
+#region BSD License
+/*
+ *
+ * New BSD 3-Clause License (https://github.com/Krypton-Suite/Standard-Toolkit/blob/master/LICENSE)
+ * Modifications by Peter Wagner (aka Wagnerp), Simon Coghlan (aka Smurf-IV), tobitege et al. 2025 - 2025. All rights reserved.
+ *
+ */
+#endregion
+
+namespace Krypton.Toolkit;
+
+///
+/// Provides a numeric input control with a calculator dropdown.
+///
+[ToolboxItem(true)]
+[ToolboxBitmap(typeof(KryptonCalcInput), "ToolboxBitmaps.KryptonCalcInput.bmp")]
+[DefaultEvent(nameof(ValueChanged))]
+[DefaultProperty(nameof(Value))]
+[DefaultBindingProperty(nameof(Value))]
+[Designer(typeof(KryptonCalcInputDesigner))]
+[DesignerCategory(@"code")]
+[Description(@"Represents a numeric input control with integrated calculator dropdown.")]
+public class KryptonCalcInput : VisualControlBase, IContainedInputControl
+{
+ #region Instance Fields
+ private VisualPopupToolTip? _visualPopupToolTip;
+ private readonly ButtonSpecManagerLayout? _buttonManager;
+ private readonly ViewLayoutDocker _drawDockerInner;
+ private readonly ViewDrawDocker _drawDockerOuter;
+ private readonly ViewLayoutFill _layoutFill;
+ private readonly KryptonTextBox _textBox;
+ private readonly ViewDrawDropDownButton _dropDownGlyph;
+ private readonly ButtonController _buttonController;
+ private InputControlStyle _inputControlStyle;
+ private decimal _value;
+ private bool? _fixedActive;
+ private bool _alwaysActive;
+ private bool _mouseOver;
+ private bool _trackingMouseEnter;
+ private bool _allowDecimals;
+ private int _decimalPlaces;
+ private bool _trailingZeroes;
+ private bool _autoSize;
+ private bool _forcedLayout;
+ private bool _thousandsSeparator;
+ private Padding _contentPadding;
+ private int _dropDownWidth;
+ private VisualOrientation _popupSide;
+ #endregion
+
+ #region Events
+ ///
+ /// Occurs when the value of the Value property changes.
+ ///
+ [Description(@"Occurs when the value of the Value property changes.")]
+ [Category(@"Action")]
+ public event EventHandler? ValueChanged;
+
+ ///
+ /// Occurs when the value of the TextChanged property changes.
+ ///
+ [Browsable(false)]
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public new event EventHandler? TextChanged;
+
+ ///
+ /// Occurs when the mouse enters the control.
+ ///
+ [Description(@"Raises the TrackMouseEnter event in the wrapped control.")]
+ [Category(@"Mouse")]
+ [EditorBrowsable(EditorBrowsableState.Advanced)]
+ public event EventHandler? TrackMouseEnter;
+
+ ///
+ /// Occurs when the mouse leaves the control.
+ ///
+ [Description(@"Raises the TrackMouseLeave event in the wrapped control.")]
+ [Category(@"Mouse")]
+ [EditorBrowsable(EditorBrowsableState.Advanced)]
+ public event EventHandler? TrackMouseLeave;
+
+ ///
+ /// Occurs when the value of the BackColor property changes.
+ ///
+ [Browsable(false)]
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public new event EventHandler? BackColorChanged;
+
+ ///
+ /// Occurs when the value of the BackgroundImage property changes.
+ ///
+ [Browsable(false)]
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public new event EventHandler? BackgroundImageChanged;
+
+ ///
+ /// Occurs when the value of the BackgroundImageLayout property changes.
+ ///
+ [Browsable(false)]
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public new event EventHandler? BackgroundImageLayoutChanged;
+
+ ///
+ /// Occurs when the value of the ForeColor property changes.
+ ///
+ [Browsable(false)]
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public new event EventHandler? ForeColorChanged;
+
+ ///
+ /// Occurs when the value of the PaddingChanged property changes.
+ ///
+ [Browsable(false)]
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public new event EventHandler? PaddingChanged;
+
+ ///
+ /// Occurs when any ButtonSpec in is clicked.
+ ///
+ [Category(@"Action")]
+ [Description(@"Raised when any ButtonSpec is clicked.")]
+ public event EventHandler? ButtonSpecClicked;
+ #endregion
+
+ #region Identity
+ ///
+ /// Initialize a new instance of the KryptonCalcInput class.
+ ///
+ public KryptonCalcInput()
+ {
+ // Contains another control and needs marking as such for validation to work
+ SetStyle(ControlStyles.ContainerControl, true);
+
+ // By default we are not multiline and so the height is fixed
+ SetStyle(ControlStyles.FixedHeight, true);
+
+ // Cannot select this control, only the child TextBox
+ SetStyle(ControlStyles.Selectable, false);
+
+ // Defaults
+ _inputControlStyle = InputControlStyle.Standalone;
+ _alwaysActive = true;
+ _decimalPlaces = 0;
+ _allowDecimals = false;
+ _trailingZeroes = true;
+ _autoSize = false;
+ _contentPadding = Padding.Empty;
+ _dropDownWidth = 0;
+ _popupSide = VisualOrientation.Bottom;
+
+ // Create storage properties
+ ButtonSpecs = new CalcButtonSpecCollection(this);
+
+ // Monitor ButtonSpec additions/removals so we can surface click events
+ ButtonSpecs.Inserted += OnButtonSpecInserted;
+ ButtonSpecs.Removed += OnButtonSpecRemoved;
+
+ // Create the palette storage
+ StateCommon = new PaletteInputControlTripleRedirect(Redirector, PaletteBackStyle.InputControlStandalone, PaletteBorderStyle.InputControlStandalone, PaletteContentStyle.InputControlStandalone, NeedPaintDelegate);
+ StateDisabled = new PaletteInputControlTripleStates(StateCommon, NeedPaintDelegate);
+ StateNormal = new PaletteInputControlTripleStates(StateCommon, NeedPaintDelegate);
+ StateActive = new PaletteInputControlTripleStates(StateCommon, NeedPaintDelegate);
+
+ // Create the internal textbox used for containing content
+ _textBox = new KryptonTextBox();
+ _textBox.StateCommon.Border.Draw = InheritBool.False;
+ _textBox.StateCommon.Border.Width = 0;
+ _textBox.TextChanged += OnTextBoxTextChanged;
+ _textBox.GotFocus += OnTextBoxGotFocus;
+ _textBox.LostFocus += OnTextBoxLostFocus;
+ _textBox.KeyDown += OnTextBoxKeyDown;
+ _textBox.KeyUp += OnTextBoxKeyUp;
+ _textBox.KeyPress += OnTextBoxKeyPress;
+ _textBox.PreviewKeyDown += OnTextBoxPreviewKeyDown;
+ _textBox.Validating += OnTextBoxValidating;
+ _textBox.Validated += OnTextBoxValidated;
+ _textBox.MouseEnter += OnTextBoxMouseEnter;
+ _textBox.MouseLeave += OnTextBoxMouseLeave;
+
+ // Create the element that fills the remainder space and remembers fill rectangle
+ _layoutFill = new ViewLayoutFill(_textBox)
+ {
+ DisplayPadding = new Padding(1, 1, 1, 0)
+ };
+
+ // Create inner view for placing inside the drawing docker
+ _drawDockerInner = new ViewLayoutDocker
+ {
+ { _layoutFill, ViewDockStyle.Fill }
+ };
+
+ // Add dropdown glyph holder (inserted after construction below)
+
+ // Create view for the control border and background
+ _drawDockerOuter = new ViewDrawDocker(StateNormal.Back, StateNormal.Border)
+ {
+ { _drawDockerInner, ViewDockStyle.Fill }
+ };
+
+ // Create the view manager instance
+ ViewManager = new ViewManager(this, _drawDockerOuter);
+
+ // Create button specification collection manager
+ _buttonManager = new ButtonSpecManagerLayout(this, Redirector, ButtonSpecs, null,
+ [_drawDockerInner],
+ [StateCommon],
+ [PaletteMetricInt.HeaderButtonEdgeInsetInputControl],
+ [PaletteMetricPadding.HeaderButtonPaddingInputControl],
+ CreateToolStripRenderer,
+ NeedPaintDelegate);
+
+ // Create the manager for handling tooltips
+ ToolTipManager = new ToolTipManager(ToolTipValues);
+ ToolTipManager.ShowToolTip += OnShowToolTip;
+ ToolTipManager.CancelToolTip += OnCancelToolTip;
+ _buttonManager.ToolTipManager = ToolTipManager;
+
+ // Create the dropdown glyph view (renderer draws an arrow or custom glyph)
+ _dropDownGlyph = new ViewDrawDropDownButton(StateCommon.Content)
+ {
+ Orientation = VisualOrientation.Top
+ };
+
+ // Set calculator icon for dropdown glyph
+ try
+ {
+ _dropDownGlyph.CustomGlyph = ResourceFiles.Generic.GenericKryptonImageResources.KryptonCalcInput;
+ }
+ catch
+ {
+ }
+
+ // Constrain glyph size using a fixed-size decorator and center it
+ var glyphFixed = new ViewDecoratorFixedSize(_dropDownGlyph, new Size(20, 20));
+ var glyphCenter = new ViewLayoutCenter(1);
+ glyphCenter.Add(glyphFixed);
+
+ // Add the glyph to the right side of the inner docker
+ _drawDockerInner.Add(glyphCenter, ViewDockStyle.Right);
+
+ // Create dropdown controller
+ _buttonController = new ButtonController(glyphCenter, NeedPaintDelegate)
+ {
+ BecomesFixed = true,
+ ClickOnDown = false
+ };
+
+ // Assign the controller to the view element
+ glyphCenter.MouseController = _buttonController;
+ glyphCenter.KeyController = _buttonController;
+ glyphCenter.SourceController = _buttonController;
+
+ // Handle dropdown button click
+ _buttonController.Click += OnDropDownClick;
+
+ // Add textbox to the controls collection
+ ((KryptonReadOnlyControls)Controls).AddInternal(_textBox);
+
+ // Defaults for calculator value
+ _value = 0m;
+ UpdateTextBoxValue();
+ }
+
+ ///
+ /// Clean up any resources being used.
+ ///
+ /// true if managed resources should be disposed; otherwise, false.
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ // Remove any showing tooltip
+ OnCancelToolTip(this, EventArgs.Empty);
+
+ // Tell the buttons class to cleanup resources
+ _buttonManager?.Destruct();
+ }
+
+ base.Dispose(disposing);
+ }
+ #endregion
+
+ #region Type Definitions
+ ///
+ /// Collection for managing ButtonSpecAny instances.
+ ///
+ public class CalcButtonSpecCollection : ButtonSpecCollection
+ {
+ #region Identity
+ ///
+ /// Initialize a new instance of the CalcButtonSpecCollection class.
+ ///
+ /// Reference to owning object.
+ public CalcButtonSpecCollection(KryptonCalcInput owner)
+ : base(owner)
+ {
+ }
+ #endregion
+ }
+ #endregion
+
+ #region Public
+ ///
+ /// Gets and sets the automatic resize of the control to fit contents.
+ ///
+ [Browsable(true)]
+ [DefaultValue(false)]
+ [Description("Autosizes the control based on the maximum value possible.")]
+ [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
+ public new bool AutoSize
+ {
+ get => _autoSize;
+ set
+ {
+ if (_autoSize != value)
+ {
+ _autoSize = value;
+ UpdateAutoSizing();
+ }
+ }
+ }
+
+ ///
+ /// Gets and sets the internal padding space.
+ ///
+ [Browsable(false)]
+ [Localizable(false)]
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
+ public new Padding Padding
+ {
+ get => base.Padding;
+ set => base.Padding = value;
+ }
+
+ ///
+ /// Gets or sets extra padding inside the text area. When empty, palette padding is used.
+ ///
+ [Category(@"Layout")]
+ [Description(@"Extra padding inside the text area. When empty, palette padding is used.")]
+ [DefaultValue(typeof(Padding), "0,0,0,0")]
+ public Padding ContentPadding
+ {
+ get => _contentPadding;
+ set
+ {
+ if (_contentPadding != value)
+ {
+ _contentPadding = value;
+ if (_contentPadding != Padding.Empty)
+ {
+ _layoutFill.DisplayPadding = _contentPadding;
+ }
+ PerformNeedPaint(true);
+ }
+ }
+ }
+
+ ///
+ /// Gets or sets the text associated with this control.
+ ///
+ [Browsable(false)]
+ [Localizable(false)]
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
+ [Bindable(false)]
+ [AllowNull]
+ public override string Text
+ {
+ get => _textBox.Text;
+ set => _textBox.Text = value;
+ }
+
+ ///
+ /// Gets or sets horizontal alignment of the text in the edit area.
+ ///
+ [Category(@"Appearance")]
+ [Description(@"Horizontal alignment of the text in the edit area.")]
+ [DefaultValue(HorizontalAlignment.Left)]
+ public HorizontalAlignment TextAlign
+ {
+ get => _textBox.TextAlign;
+ set
+ {
+ if (_textBox.TextAlign != value)
+ {
+ _textBox.TextAlign = value;
+ PerformNeedPaint(true);
+ }
+ }
+ }
+
+ ///
+ /// Gets and sets the associated context menu strip.
+ ///
+ [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
+ public override ContextMenuStrip? ContextMenuStrip
+ {
+ get => base.ContextMenuStrip;
+ set
+ {
+ base.ContextMenuStrip = value;
+ _textBox.ContextMenuStrip = value;
+ }
+ }
+
+ ///
+ /// Gets or sets the number of decimal places to display.
+ ///
+ [Category(@"Data")]
+ [Description(@"Indicates the number of decimal places to display.")]
+ [DefaultValue(0)]
+ [Browsable(true)]
+ public int DecimalPlaces
+ {
+ get => _decimalPlaces;
+ set
+ {
+ if (_decimalPlaces != value)
+ {
+ _decimalPlaces = value;
+ UpdateTextBoxValue();
+ }
+ }
+ }
+
+ ///
+ /// Gets or sets whether the control accepts decimal values.
+ ///
+ [Category(@"Behavior")]
+ [Description(@"Indicates whether the control can accept decimal values, rather than integer values only.")]
+ [DefaultValue(false)]
+ public bool AllowDecimals
+ {
+ get => _allowDecimals;
+ set
+ {
+ if (_allowDecimals != value)
+ {
+ _allowDecimals = value;
+ UpdateTextBoxValue();
+ }
+ }
+ }
+
+ ///
+ /// Gets or sets whether the control displays trailing zeroes.
+ ///
+ [Category(@"Behavior")]
+ [Description(@"Indicates whether the control will display trailing zeroes, when decimals are in play")]
+ [DefaultValue(true)]
+ public bool TrailingZeroes
+ {
+ get => _trailingZeroes;
+ set
+ {
+ if (_trailingZeroes != value)
+ {
+ _trailingZeroes = value;
+ UpdateTextBoxValue();
+ }
+ }
+ }
+
+ ///
+ /// Gets or sets whether the thousands separator will be inserted between each three decimal digits.
+ ///
+ [Category(@"Data")]
+ [Description(@"Indicates whether the thousands separator will be inserted between each three decimal digits.")]
+ [DefaultValue(false)]
+ [Localizable(true)]
+ public bool ThousandsSeparator
+ {
+ get => _thousandsSeparator;
+ set
+ {
+ if (_thousandsSeparator != value)
+ {
+ _thousandsSeparator = value;
+ UpdateTextBoxValue();
+ }
+ }
+ }
+
+ ///
+ /// Gets or sets the default width of the calculator dropdown. At runtime a sensible minimum is enforced.
+ ///
+ [Category(@"Layout")]
+ [Description(@"Default width of the calculator popup. A sensible minimum is enforced at runtime.")]
+ [DefaultValue(0)]
+ [Localizable(true)]
+ public int DropDownWidth
+ {
+ get => _dropDownWidth;
+ set
+ {
+ if (_dropDownWidth != value)
+ {
+ _dropDownWidth = value;
+ }
+ }
+ }
+
+ ///
+ /// Gets or sets the side of the control where the calculator popup appears.
+ ///
+ [Category(@"Visuals")]
+ [Description(@"Side of the control where the calculator popup appears (Top/Bottom/Left/Right).")]
+ [DefaultValue(VisualOrientation.Bottom)]
+ public VisualOrientation PopupSide
+ {
+ get => _popupSide;
+ set
+ {
+ if (_popupSide != value)
+ {
+ _popupSide = value;
+ PerformNeedPaint(true);
+ }
+ }
+ }
+
+ ///
+ /// Gets or sets the current value of the numeric up-down control.
+ ///
+ [Category(@"Appearance")]
+ [Description(@"The current value of the numeric up-down control.")]
+ [DefaultValue(0.0d)]
+ [Bindable(true)]
+ public decimal Value
+ {
+ get => _value;
+ set
+ {
+ if (_value != value)
+ {
+ _value = value;
+ UpdateTextBoxValue();
+ OnValueChanged(EventArgs.Empty);
+ PerformNeedPaint(true);
+ }
+ }
+ }
+
+ ///
+ /// Gets and sets Determines if the control is always active or only when the mouse is over the control or has focus.
+ ///
+ [Category(@"Visuals")]
+ [Description(@"Determines if the control is always active or only when the mouse is over the control or has focus.")]
+ [DefaultValue(true)]
+ public bool AlwaysActive
+ {
+ get => _alwaysActive;
+ set
+ {
+ if (_alwaysActive != value)
+ {
+ _alwaysActive = value;
+ PerformNeedPaint(true);
+ }
+ }
+ }
+
+ ///
+ /// Gets and sets the input control style.
+ ///
+ [Category(@"Visuals")]
+ [Description(@"Input control style.")]
+ public InputControlStyle InputControlStyle
+ {
+ get => _inputControlStyle;
+ set
+ {
+ if (_inputControlStyle != value)
+ {
+ _inputControlStyle = value;
+ StateCommon.SetStyles(value);
+ PerformNeedPaint(true);
+ }
+ }
+ }
+
+ private void ResetInputControlStyle() => InputControlStyle = InputControlStyle.Standalone;
+
+ private bool ShouldSerializeInputControlStyle() => InputControlStyle != InputControlStyle.Standalone;
+
+ ///
+ /// Gets and sets a value indicating if tooltips should be displayed for button specs.
+ ///
+ [Category(@"Visuals")]
+ [Description(@"Should tooltips be displayed for button specs.")]
+ [DefaultValue(false)]
+ public bool AllowButtonSpecToolTips { get; set; }
+
+ ///
+ /// Gets and sets a value indicating if button spec tooltips should remove the parent tooltip.
+ ///
+ [Category(@"Visuals")]
+ [Description(@"Should button spec tooltips should remove the parent tooltip")]
+ [DefaultValue(false)]
+ public bool AllowButtonSpecToolTipPriority { get; set; }
+
+ ///
+ /// Gets the collection of button specifications.
+ ///
+ [Category(@"Visuals")]
+ [Description(@"Collection of button specifications.")]
+ [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
+ public CalcButtonSpecCollection ButtonSpecs { get; }
+
+ ///
+ /// Gets access to the common textbox appearance entries that other states can override.
+ ///
+ [Category(@"Visuals")]
+ [Description(@"Overrides for defining common textbox appearance that other states can override.")]
+ [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
+ public PaletteInputControlTripleRedirect StateCommon { get; }
+
+ private bool ShouldSerializeStateCommon() => !StateCommon.IsDefault;
+
+ ///
+ /// Gets access to the disabled textbox appearance entries.
+ ///
+ [Category(@"Visuals")]
+ [Description(@"Overrides for defining disabled textbox appearance.")]
+ [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
+ public PaletteInputControlTripleStates StateDisabled { get; }
+
+ private bool ShouldSerializeStateDisabled() => !StateDisabled.IsDefault;
+
+ ///
+ /// Gets access to the normal textbox appearance entries.
+ ///
+ [Category(@"Visuals")]
+ [Description(@"Overrides for defining normal textbox appearance.")]
+ [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
+ public PaletteInputControlTripleStates StateNormal { get; }
+
+ private bool ShouldSerializeStateNormal() => !StateNormal.IsDefault;
+
+ ///
+ /// Gets access to the active textbox appearance entries.
+ ///
+ [Category(@"Visuals")]
+ [Description(@"Overrides for defining active textbox appearance.")]
+ [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
+ public PaletteInputControlTripleStates StateActive { get; }
+
+ private bool ShouldSerializeStateActive() => !StateActive.IsDefault;
+
+ ///
+ /// Gets access to the contained input control.
+ ///
+ [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
+ [EditorBrowsable(EditorBrowsableState.Always)]
+ [Browsable(false)]
+ public Control ContainedControl => _textBox;
+
+ ///
+ /// Gets a value indicating whether the control has input focus.
+ ///
+ [Browsable(false)]
+ public override bool Focused => _textBox.Focused;
+
+ ///
+ /// Selects a range of text in the control.
+ ///
+ /// The position of the first character in the current text selection within the text box.
+ /// The number of characters to select.
+ public void Select(int start, int length) => _textBox?.Select(start, length);
+
+ ///
+ /// Sets the fixed state of the control.
+ ///
+ /// Should the control be fixed as active.
+ public void SetFixedState(bool active) => _fixedActive = active;
+
+ ///
+ /// Gets access to the ToolTipManager used for displaying tool tips.
+ ///
+ [Browsable(false)]
+ [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
+ public ToolTipManager ToolTipManager { get; }
+
+ ///
+ /// Gets a value indicating if the input control is active.
+ ///
+ [Browsable(false)]
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public bool IsActive =>
+ _fixedActive ?? DesignMode || AlwaysActive ||
+ ContainsFocus || _mouseOver;
+
+ ///
+ /// Sets input focus to the control.
+ ///
+ /// true if the input focus request was successful; otherwise, false.
+ public new bool Focus() => _textBox != null && _textBox.Focus();
+
+ ///
+ /// Activates the control.
+ ///
+ public new void Select() => _textBox?.Select();
+
+ ///
+ /// Get the preferred size of the control based on a proposed size.
+ ///
+ /// Starting size proposed by the caller.
+ /// Calculated preferred size.
+ public override Size GetPreferredSize(Size proposedSize)
+ {
+ // Do we have a manager to ask for a preferred size?
+ if (ViewManager != null)
+ {
+ // Ask the view to perform a layout
+ Size retSize = ViewManager.GetPreferredSize(Renderer, proposedSize);
+
+ // Apply the maximum sizing
+ if (MaximumSize.Width > 0)
+ {
+ retSize.Width = Math.Min(MaximumSize.Width, retSize.Width);
+ }
+
+ if (MaximumSize.Height > 0)
+ {
+ retSize.Height = Math.Min(MaximumSize.Height, retSize.Height);
+ }
+
+ // Apply the minimum sizing
+ if (MinimumSize.Width > 0)
+ {
+ retSize.Width = Math.Max(MinimumSize.Width, retSize.Width);
+ }
+
+ if (MinimumSize.Height > 0)
+ {
+ retSize.Height = Math.Max(MinimumSize.Height, retSize.Height);
+ }
+
+ return retSize;
+ }
+
+ // Fall back on default control processing
+ return base.GetPreferredSize(proposedSize);
+ }
+
+ ///
+ /// Gets or sets a value indicating whether an ampersand is included in the text of the control.
+ ///
+ [Category(@"Appearance")]
+ [Description(@"When true the first character after an ampersand will be used as a mnemonic.")]
+ [DefaultValue(true)]
+ public bool UseMnemonic
+ {
+ get => _buttonManager!.UseMnemonic;
+
+ set
+ {
+ if (_buttonManager!.UseMnemonic != value)
+ {
+ _buttonManager.UseMnemonic = value;
+ PerformNeedPaint(true);
+ }
+ }
+ }
+
+ ///
+ /// Gets and sets if the control is in the ribbon design mode.
+ ///
+ [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ [Browsable(false)]
+ public bool InRibbonDesignMode { get; set; }
+ #endregion
+
+ #region Protected
+ ///
+ /// Force the layout logic to size and position the controls.
+ ///
+ protected void ForceControlLayout()
+ {
+ if (!IsHandleCreated)
+ {
+ _forcedLayout = true;
+ OnLayout(new LayoutEventArgs(null, null));
+ _forcedLayout = false;
+ }
+ }
+
+ ///
+ /// Gets or sets a value indicating whether a value has been entered by the user.
+ ///
+ protected bool UserEdit { get; set; }
+ #endregion
+
+ #region Protected Virtual
+ ///
+ /// Raises the ValueChanged event.
+ ///
+ /// An EventArgs containing the event data.
+ protected virtual void OnValueChanged(EventArgs e) => ValueChanged?.Invoke(this, e);
+
+ ///
+ /// Raises the TrackMouseEnter event.
+ ///
+ /// An EventArgs containing the event data.
+ protected virtual void OnTrackMouseEnter(EventArgs e) => TrackMouseEnter?.Invoke(this, e);
+
+ ///
+ /// Raises the TrackMouseLeave event.
+ ///
+ /// An EventArgs containing the event data.
+ protected virtual void OnTrackMouseLeave(EventArgs e) => TrackMouseLeave?.Invoke(this, e);
+ #endregion
+
+ #region Protected Overrides
+ ///
+ /// Creates a new instance of the control collection for the KryptonTextBox.
+ ///
+ /// A new instance of Control.ControlCollection assigned to the control.
+ [EditorBrowsable(EditorBrowsableState.Advanced)]
+ protected override ControlCollection CreateControlsInstance() => new KryptonReadOnlyControls(this);
+
+ ///
+ /// Raises the HandleCreated event.
+ ///
+ /// An EventArgs containing the event data.
+ protected override void OnHandleCreated(EventArgs e)
+ {
+ // Let base class do standard stuff
+ base.OnHandleCreated(e);
+
+ // Ensure the child textbox reflects any explicitly set Font immediately
+ _textBox.Font = Font;
+ PerformNeedPaint(false);
+
+ // We need a layout to occur before any painting
+ InvokeLayout();
+
+ // We need to recalculate the correct height
+ Height = PreferredHeight;
+ }
+
+ ///
+ /// Raises the EnabledChanged event.
+ ///
+ /// An EventArgs that contains the event data.
+ protected override void OnEnabledChanged(EventArgs e)
+ {
+ // Change in enabled state requires a layout and repaint
+ UpdateStateAndPalettes();
+
+ // Update view elements
+ _drawDockerInner.Enabled = Enabled;
+ _drawDockerOuter.Enabled = Enabled;
+
+ // Update state to reflect change in enabled state
+ _buttonManager?.RefreshButtons();
+
+ PerformNeedPaint(true);
+
+ // Let base class fire standard event
+ base.OnEnabledChanged(e);
+ }
+
+ ///
+ /// Raises the PaletteChanged event.
+ ///
+ /// An EventArgs containing the event data.
+ protected override void OnPaletteChanged(EventArgs e)
+ {
+ InvalidateChildren();
+ // If no explicit font set, update textbox font from palette
+ var fontProp = System.ComponentModel.TypeDescriptor.GetProperties(this)[nameof(Font)];
+ bool explicitFont = fontProp?.ShouldSerializeValue(this) == true;
+ if (!explicitFont)
+ {
+ IPaletteTriple triple = GetTripleState();
+ Font? font = triple.PaletteContent!.GetContentShortTextFont(_drawDockerOuter.State);
+ if ((_textBox.Handle != IntPtr.Zero) && font != null && !_textBox.Font.Equals(font))
+ {
+ _textBox.Font = font;
+ }
+ }
+ base.OnPaletteChanged(e);
+ }
+
+ ///
+ /// Processes a notification from palette of a paint and optional layout required.
+ ///
+ /// Source of notification.
+ /// An NeedLayoutEventArgs containing event data.
+ protected override void OnPaletteNeedPaint(object? sender, NeedLayoutEventArgs e)
+ {
+ InvalidateChildren();
+ base.OnPaletteChanged(e);
+ }
+
+ ///
+ /// Raises the BackColorChanged event.
+ ///
+ /// An EventArgs that contains the event data.
+ protected override void OnBackColorChanged(EventArgs e) => BackColorChanged?.Invoke(this, e);
+
+ ///
+ /// Raises the BackgroundImageChanged event.
+ ///
+ /// An EventArgs that contains the event data.
+ protected override void OnBackgroundImageChanged(EventArgs e) => BackgroundImageChanged?.Invoke(this, e);
+
+ ///
+ /// Raises the BackgroundImageLayoutChanged event.
+ ///
+ /// An EventArgs that contains the event data.
+ protected override void OnBackgroundImageLayoutChanged(EventArgs e) => BackgroundImageLayoutChanged?.Invoke(this, e);
+
+ ///
+ /// Raises the ForeColorChanged event.
+ ///
+ /// An EventArgs that contains the event data.
+ protected override void OnForeColorChanged(EventArgs e) => ForeColorChanged?.Invoke(this, e);
+
+ ///
+ /// Raises the Resize event.
+ ///
+ /// An EventArgs that contains the event data.
+ protected override void OnResize(EventArgs e)
+ {
+ // Let base class raise events
+ base.OnResize(e);
+
+ // We must have a layout calculation
+ ForceControlLayout();
+ }
+
+ ///
+ /// Raises the Layout event.
+ ///
+ /// An EventArgs that contains the event data.
+ protected override void OnLayout(LayoutEventArgs levent)
+ {
+ if (!IsDisposed && !Disposing)
+ {
+ // Update to match the new palette settings
+ Height = PreferredHeight;
+
+ // Apply current content padding visually by adjusting our fill padding
+ if (_contentPadding != Padding.Empty)
+ {
+ _layoutFill.DisplayPadding = _contentPadding;
+ }
+
+ // Let base class calculate fill rectangle
+ base.OnLayout(levent);
+
+ // Only use layout logic if control is fully initialized or if being forced
+ // to allow a relayout or if in design mode.
+ if (IsHandleCreated || _forcedLayout || (DesignMode && (_textBox != null)))
+ {
+ Rectangle fillRect = _layoutFill.FillRect;
+ _textBox?.SetBounds(fillRect.X, fillRect.Y, fillRect.Width, fillRect.Height);
+
+ // In the designer, ensure the control Width reflects the full visual width
+ // (not just the inner textbox width), so the property grid reports the true size.
+ if (DesignMode && ViewManager != null)
+ {
+ Size pref = ViewManager.GetPreferredSize(Renderer, new Size(int.MaxValue, int.MaxValue));
+ if (Width < pref.Width)
+ {
+ Width = pref.Width;
+ }
+ }
+ }
+ }
+ }
+
+ ///
+ /// Raises the MouseEnter event.
+ ///
+ /// An EventArgs that contains the event data.
+ protected override void OnMouseEnter(EventArgs e)
+ {
+ _mouseOver = true;
+ PerformNeedPaint(true);
+ InvalidateChildren();
+ base.OnMouseEnter(e);
+ }
+
+ ///
+ /// Raises the MouseLeave event.
+ ///
+ /// An EventArgs that contains the event data.
+ protected override void OnMouseLeave(EventArgs e)
+ {
+ _mouseOver = false;
+ PerformNeedPaint(true);
+ InvalidateChildren();
+ base.OnMouseLeave(e);
+ }
+
+ ///
+ /// Raises the GotFocus event.
+ ///
+ /// An EventArgs that contains the event data.
+ protected override void OnGotFocus(EventArgs e)
+ {
+ base.OnGotFocus(e);
+ _textBox?.Focus();
+ }
+
+ ///
+ /// Performs the work of setting the specified bounds of this control.
+ ///
+ /// The new Left property value of the control.
+ /// The new Top property value of the control.
+ /// The new Width property value of the control.
+ /// The new Height property value of the control.
+ /// A bitwise combination of the BoundsSpecified values.
+ protected override void SetBoundsCore(int x, int y,
+ int width, int height,
+ BoundsSpecified specified)
+ {
+ // Get the preferred size of the entire control
+ Size preferredSize = GetPreferredSize(new Size(int.MaxValue, int.MaxValue));
+
+ // If setting the actual height
+ if (specified.HasFlag(BoundsSpecified.Height))
+ {
+ // Override the actual height used
+ height = preferredSize.Height;
+ }
+
+ base.SetBoundsCore(x, y, width, height, specified);
+ }
+
+ ///
+ /// Gets the default size of the control.
+ ///
+ protected override Size DefaultSize => new Size(120, PreferredHeight);
+
+ ///
+ /// Processes a notification from palette storage of a paint and optional layout required.
+ ///
+ /// Source of notification.
+ /// An NeedLayoutEventArgs containing event data.
+ protected override void OnNeedPaint(object? sender, NeedLayoutEventArgs e)
+ {
+ if (IsHandleCreated && !e.NeedLayout)
+ {
+ InvalidateChildren();
+ }
+ else
+ {
+ ForceControlLayout();
+ }
+
+ if (!IsDisposed && !Disposing)
+ {
+ // Update the back/fore/font from the palette settings
+ UpdateStateAndPalettes();
+ IPaletteTriple triple = GetTripleState();
+ _textBox.BackColor = triple.PaletteBack.GetBackColor1(_drawDockerOuter.State);
+ _textBox.ForeColor = triple.PaletteContent!.GetContentShortTextColor1(_drawDockerOuter.State);
+
+ // Apply font: honor an explicit Font override set on this control; otherwise use palette font
+ var fontProp = System.ComponentModel.TypeDescriptor.GetProperties(this)[nameof(Font)];
+ bool explicitFont = fontProp?.ShouldSerializeValue(this) == true;
+ Font? desiredFont = explicitFont
+ ? Font
+ : triple.PaletteContent.GetContentShortTextFont(_drawDockerOuter.State);
+ if ((_textBox.Handle != IntPtr.Zero) && desiredFont != null && !_textBox.Font.Equals(desiredFont))
+ {
+ _textBox.Font = desiredFont;
+ }
+
+ // Alignment mapping from palette
+ var hAlign = triple.PaletteContent.GetContentShortTextH(_drawDockerOuter.State);
+ _textBox.TextAlign = hAlign switch
+ {
+ PaletteRelativeAlign.Center => HorizontalAlignment.Center,
+ PaletteRelativeAlign.Far => HorizontalAlignment.Right,
+ _ => HorizontalAlignment.Left
+ };
+ }
+
+ base.OnNeedPaint(sender, e);
+ }
+
+ ///
+ /// Raises the PaddingChanged event.
+ ///
+ /// An NeedLayoutEventArgs containing event data.
+ protected override void OnPaddingChanged(EventArgs e) => PaddingChanged?.Invoke(this, e);
+
+ ///
+ /// Raises the TabStop event.
+ ///
+ /// An EventArgs that contains the event data.
+ protected override void OnTabStopChanged(EventArgs e)
+ {
+ _textBox.TabStop = TabStop;
+ base.OnTabStopChanged(e);
+ }
+
+ ///
+ /// Raises the CausesValidationChanged event.
+ ///
+ /// An EventArgs that contains the event data.
+ protected override void OnCausesValidationChanged(EventArgs e)
+ {
+ _textBox.CausesValidation = CausesValidation;
+ base.OnCausesValidationChanged(e);
+ }
+
+ ///
+ /// Raises the TextChanged event.
+ ///
+ /// An NeedLayoutEventArgs containing event data.
+ protected override void OnTextChanged(EventArgs e) => TextChanged?.Invoke(this, e);
+
+ ///
+ /// Process Windows-based messages.
+ ///
+ /// A Windows-based message.
+ protected override void WndProc(ref Message m)
+ {
+ switch (m.Msg)
+ {
+ case PI.WM_.NCHITTEST:
+ // Treat clicks as client area to avoid click-through on transparent pixels
+ base.WndProc(ref m);
+ m.Result = (IntPtr)PI.HT.CLIENT;
+ return;
+ default:
+ base.WndProc(ref m);
+ return;
+ }
+ }
+ #endregion
+
+ #region Internal
+ internal bool InTransparentDesignMode => InRibbonDesignMode;
+
+ internal bool IsFixedActive => _fixedActive != null;
+ #endregion
+
+ #region Implementation
+ private void UpdateAutoSizing()
+ {
+ if (AutoSize)
+ {
+ var graphics = Graphics.FromHwnd(Handle);
+ var newWidth = (int)Math.Ceiling(graphics.MeasureString(Value.ToString(), Font).Width);
+ newWidth += 60; // Add space for dropdown button and padding
+ newWidth = Math.Max(newWidth, MinimumSize.Width);
+ newWidth = Math.Min(newWidth, MaximumSize.Width);
+
+ if (newWidth > 0)
+ {
+ Width = newWidth;
+ PerformNeedPaint(true);
+ }
+ }
+ }
+
+ private void InvalidateChildren()
+ {
+ if (_textBox != null)
+ {
+ _textBox.Invalidate();
+
+ if (!IsDisposed && !Disposing)
+ {
+ PI.RedrawWindow(Handle, IntPtr.Zero, IntPtr.Zero, 0x85);
+ }
+ }
+ }
+
+ private void UpdateStateAndPalettes()
+ {
+ // Get the correct palette settings to use
+ IPaletteTriple tripleState = GetTripleState();
+ _drawDockerOuter.SetPalettes(tripleState.PaletteBack, tripleState.PaletteBorder!);
+
+ // Update enabled state
+ _drawDockerOuter.Enabled = Enabled;
+
+ // Find the new state of the main view element
+ PaletteState state = Enabled ? (IsActive ? PaletteState.Tracking : PaletteState.Normal) : PaletteState.Disabled;
+
+ _drawDockerOuter.ElementState = state;
+ }
+
+ internal PaletteInputControlTripleStates GetTripleState() => Enabled ? (IsActive ? StateActive : StateNormal) : StateDisabled;
+
+ private int PreferredHeight
+ {
+ get
+ {
+ // Get the preferred size of the entire control
+ Size preferredSize = GetPreferredSize(new Size(int.MaxValue, int.MaxValue));
+
+ // We only need the height
+ return preferredSize.Height;
+ }
+ }
+
+ private void UpdateTextBoxValue()
+ {
+ if (_textBox != null)
+ {
+ var numberFormat = (System.Globalization.NumberFormatInfo)System.Globalization.CultureInfo.CurrentCulture.NumberFormat.Clone();
+
+ if (ThousandsSeparator)
+ {
+ // Respect current culture separators and digits
+ numberFormat.NumberDecimalDigits = _decimalPlaces;
+ }
+
+ string formatString = AllowDecimals
+ ? $"F{_decimalPlaces.ToString(System.Globalization.CultureInfo.InvariantCulture)}"
+ : "F0";
+
+ if (TrailingZeroes || !AllowDecimals)
+ {
+ // Simple culture-aware format when zero trimming is not requested
+ _textBox.Text = _value.ToString(formatString, System.Globalization.CultureInfo.CurrentCulture);
+ }
+ else
+ {
+ // Mirror KryptonNumericUpDown: compute a culture formatted string with requested places,
+ // and an invariant trimmed string; remove the delta to keep grouping while trimming zeros.
+ var culture = System.Globalization.CultureInfo.CurrentCulture;
+ string textAsRequested = _value.ToString(formatString, culture);
+ string textTrimmedInvariant = _value.ToString(@"0.#########################",
+ System.Globalization.CultureInfo.InvariantCulture);
+ int lengthToRemove = textAsRequested.Length - textTrimmedInvariant.Length;
+ if (lengthToRemove > 0 && lengthToRemove <= textAsRequested.Length)
+ {
+ _textBox.Text = textAsRequested.Substring(0, textAsRequested.Length - lengthToRemove);
+ }
+ else
+ {
+ _textBox.Text = textAsRequested;
+ }
+ }
+ }
+ }
+
+ private void OnTextBoxTextChanged(object? sender, EventArgs e)
+ {
+ if (_textBox != null && decimal.TryParse(_textBox.Text, out decimal newValue))
+ {
+ if (_value != newValue)
+ {
+ _value = newValue;
+ OnValueChanged(EventArgs.Empty);
+ }
+ }
+ OnTextChanged(e);
+ }
+
+ private void OnTextBoxGotFocus(object? sender, EventArgs e)
+ {
+ UpdateStateAndPalettes();
+ PerformNeedPaint(true);
+ InvalidateChildren();
+ base.OnGotFocus(e);
+ }
+
+ private void OnTextBoxLostFocus(object? sender, EventArgs e)
+ {
+ UpdateStateAndPalettes();
+ PerformNeedPaint(true);
+ InvalidateChildren();
+ base.OnLostFocus(e);
+ }
+
+ private void OnTextBoxKeyDown(object? sender, KeyEventArgs e) => OnKeyDown(e);
+
+ private void OnTextBoxKeyUp(object? sender, KeyEventArgs e) => OnKeyUp(e);
+
+ private void OnTextBoxKeyPress(object? sender, KeyPressEventArgs e)
+ {
+ // Handle numeric input validation
+ if (!char.IsControl(e.KeyChar) && !char.IsDigit(e.KeyChar))
+ {
+ if (AllowDecimals && (e.KeyChar == '.' || e.KeyChar == ','))
+ {
+ // Allow decimal separator only if not already present
+ if (_textBox.Text.Contains('.') || _textBox.Text.Contains(','))
+ {
+ e.Handled = true;
+ }
+ }
+ else if (e.KeyChar == '-')
+ {
+ // Allow negative sign only at the beginning
+ if (_textBox.SelectionStart != 0 || _textBox.Text.Contains('-'))
+ {
+ e.Handled = true;
+ }
+ }
+ else
+ {
+ e.Handled = true;
+ }
+ }
+
+ if (!e.Handled)
+ {
+ OnKeyPress(e);
+ }
+ }
+
+ private void OnTextBoxPreviewKeyDown(object? sender, PreviewKeyDownEventArgs e) => OnPreviewKeyDown(e);
+
+ private void OnTextBoxValidating(object? sender, CancelEventArgs e) => OnValidating(e);
+
+ private void OnTextBoxValidated(object? sender, EventArgs e) => OnValidated(e);
+
+ private void OnTextBoxMouseEnter(object? sender, EventArgs e)
+ {
+ if (!_trackingMouseEnter)
+ {
+ _trackingMouseEnter = true;
+ InvalidateChildren();
+ OnTrackMouseEnter(EventArgs.Empty);
+ OnMouseEnter(e);
+ }
+ }
+
+ private void OnTextBoxMouseLeave(object? sender, EventArgs e)
+ {
+ if (_trackingMouseEnter)
+ {
+ _trackingMouseEnter = false;
+ InvalidateChildren();
+ OnTrackMouseLeave(EventArgs.Empty);
+ OnMouseLeave(e);
+ }
+ }
+
+ private void OnDropDownClick(object? sender, EventArgs e)
+ {
+ ShowCalculator();
+ }
+
+ private void ShowCalculator()
+ {
+ // Ensure owner form is active and focus is correct
+ var ownerForm = FindForm();
+ ownerForm?.Activate();
+ Focus();
+ _textBox.Focus();
+
+ // Create a new KryptonContextMenu each time
+ Rectangle dropScreenRect = RectangleToScreen(ClientRectangle);
+ var kcm = new CalcContextMenu(dropScreenRect);
+
+ // Honor palette settings
+ if (PaletteMode != PaletteMode.Custom)
+ {
+ kcm.PaletteMode = PaletteMode;
+ }
+ else
+ {
+ kcm.LocalCustomPalette = LocalCustomPalette;
+ }
+
+ // Preview row using a disabled menu item (ensures reliable repaint on item clicks)
+ var culture = System.Globalization.CultureInfo.CurrentCulture;
+ string expr = Value.ToString(culture);
+ bool selectAllPrimed = true;
+ var displayItem = new KryptonContextMenuItem(expr)
+ {
+ AutoClose = false,
+ Enabled = false
+ };
+ displayItem.StateDisabled.ItemTextStandard.ShortText.Font = Font;
+ var palette = KryptonManager.CurrentGlobalPalette;
+ if (palette != null)
+ {
+ // Use a palette-derived text color that contrasts with context menu surfaces
+ var c = palette.GetContentShortTextColor1(PaletteContentStyle.ContextMenuItemTextStandard, PaletteState.Normal);
+ if (c != GlobalStaticValues.EMPTY_COLOR && !c.IsEmpty)
+ {
+ displayItem.StateDisabled.ItemTextStandard.ShortText.Color1 = c;
+ displayItem.StateDisabled.ItemTextStandard.ShortText.Color2 = c;
+ }
+ }
+ kcm.Items.Add(displayItem);
+
+ // Build a 5x4 grid as a single items block with 4 columns
+ var items = new KryptonContextMenuItems
+ {
+ ImageColumn = false
+ };
+
+ // Column definitions (top to bottom)
+ string[][] columns =
+ [
+ ["7","4","1","0","<-"] ,
+ ["8","5","2",".","(" ] ,
+ ["9","6","3","C",")" ] ,
+ ["/","*","-","+","=" ]
+ ];
+
+ for (int c = 0; c < columns.Length; c++)
+ {
+ if (c > 0)
+ {
+ items.Items.Add(new KryptonContextMenuSeparator { Horizontal = false });
+ }
+
+ for (int r = 0; r < columns[c].Length; r++)
+ {
+ string token = columns[c][r];
+ var mi = new KryptonContextMenuItem(token, (_, __) =>
+ {
+ switch (token)
+ {
+ case "C":
+ expr = string.Empty;
+ selectAllPrimed = false;
+ displayItem.Text = expr;
+ break;
+ case "<-":
+ if (!string.IsNullOrEmpty(expr))
+ {
+ expr = expr.Substring(0, expr.Length - 1);
+ }
+ selectAllPrimed = false;
+ displayItem.Text = expr;
+ break;
+ case "=":
+ {
+ try
+ {
+ var dt = new System.Data.DataTable();
+ string normalized = NormalizeExpressionForCompute(expr, culture);
+ object? v = dt.Compute(normalized, null);
+ if (v != null)
+ {
+ decimal dec = Convert.ToDecimal(v, System.Globalization.CultureInfo.InvariantCulture);
+ Value = dec;
+ }
+ }
+ catch { }
+ break;
+ }
+ default:
+ {
+ if (selectAllPrimed)
+ {
+ if (IsDigitToken(token))
+ {
+ expr = token;
+ }
+ else if (IsOperatorToken(token))
+ {
+ expr += token;
+ }
+ else
+ {
+ expr = token;
+ }
+ selectAllPrimed = false;
+ }
+ else
+ {
+ expr += token;
+ }
+ displayItem.Text = expr;
+ break;
+ }
+ }
+ });
+ // Keep the menu open for all inputs except '=' which should compute and close
+ mi.AutoClose = token == "=";
+ items.Items.Add(mi);
+ }
+ }
+
+ kcm.Items.Add(items);
+
+ // Show relative to control with side preference and 1px gap like DTP
+ Rectangle screenRect = RectangleToScreen(ClientRectangle);
+ var posH = KryptonContextMenuPositionH.Left;
+ var posV = KryptonContextMenuPositionV.Below;
+ switch (_popupSide)
+ {
+ case VisualOrientation.Top:
+ posV = KryptonContextMenuPositionV.Above;
+ break;
+ case VisualOrientation.Left:
+ posH = KryptonContextMenuPositionH.Before;
+ break;
+ case VisualOrientation.Right:
+ posH = KryptonContextMenuPositionH.After;
+ break;
+ }
+
+ if (posV == KryptonContextMenuPositionV.Above)
+ {
+ screenRect.Y -= 1;
+ }
+ else if (posV == KryptonContextMenuPositionV.Below)
+ {
+ screenRect.Height += 1;
+ }
+
+ if (posH == KryptonContextMenuPositionH.Before)
+ {
+ screenRect.X -= 1;
+ }
+ else if (posH == KryptonContextMenuPositionH.After)
+ {
+ screenRect.Width += 1;
+ }
+
+ // Respect DropDownWidth as a minimum width
+ if (_dropDownWidth > 0)
+ {
+ // KryptonContextMenu measures itself; we nudge via an invisible padding item if needed
+ // Add a spacer heading with non-breaking spaces to reach desired width
+ int target = _dropDownWidth;
+ using (var g = CreateGraphics())
+ {
+ string pad = "\u00A0\u00A0\u00A0\u00A0";
+ while (TextRenderer.MeasureText(g, displayItem.Text + pad, Font).Width < target)
+ {
+ pad += "\u00A0\u00A0";
+ }
+ // Temporarily extend heading to influence width
+ displayItem.Text = expr + pad;
+ }
+ }
+
+ kcm.Closed += OnKryptonCalcContextMenuClosed;
+ kcm.Show(this, screenRect, posH, posV);
+ }
+
+ private void OnShowToolTip(object? sender, ToolTipEventArgs e)
+ {
+ if (!IsDisposed && !Disposing)
+ {
+ // Do not show tooltips when the form we are in does not have focus
+ Form? topForm = FindForm();
+ if (topForm is { ContainsFocus: false })
+ {
+ return;
+ }
+
+ // Never show tooltips at design time
+ if (!DesignMode)
+ {
+ IContentValues? sourceContent = null;
+ var toolTipStyle = LabelStyle.ToolTip;
+
+ var shadow = true;
+
+ // Find the button spec associated with the tooltip request
+ ButtonSpec? buttonSpec = _buttonManager?.ButtonSpecFromView(e.Target);
+
+ // If the tooltip is for a button spec
+ if (buttonSpec != null)
+ {
+ // Are we allowed to show page related tooltips
+ if (AllowButtonSpecToolTips)
+ {
+ // Create a helper object to provide tooltip values
+ var buttonSpecMapping = new ButtonSpecToContent(Redirector, buttonSpec);
+
+ // Is there actually anything to show for the tooltip
+ if (buttonSpecMapping.HasContent)
+ {
+ sourceContent = buttonSpecMapping;
+ toolTipStyle = buttonSpec.ToolTipStyle;
+ shadow = buttonSpec.ToolTipShadow;
+ }
+ }
+ }
+
+ if (sourceContent != null)
+ {
+ // Remove any currently showing tooltip
+ _visualPopupToolTip?.Dispose();
+
+ // Create the actual tooltip popup object
+ _visualPopupToolTip = new VisualPopupToolTip(Redirector,
+ sourceContent,
+ Renderer,
+ PaletteBackStyle.ControlToolTip,
+ PaletteBorderStyle.ControlToolTip,
+ CommonHelper.ContentStyleFromLabelStyle(toolTipStyle),
+ shadow);
+
+ _visualPopupToolTip.Disposed += OnVisualPopupToolTipDisposed;
+ _visualPopupToolTip.ShowRelativeTo(e.Target, e.ControlMousePosition);
+ }
+ }
+ }
+ }
+
+ private void OnCancelToolTip(object? sender, EventArgs e) =>
+ // Remove any currently showing tooltip
+ _visualPopupToolTip?.Dispose();
+
+ private void OnVisualPopupToolTipDisposed(object? sender, EventArgs e)
+ {
+ // Unhook events from the specific instance that generated event
+ var popupToolTip = sender as VisualPopupToolTip ?? throw new ArgumentNullException(nameof(sender));
+ popupToolTip.Disposed -= OnVisualPopupToolTipDisposed;
+
+ // Not showing a popup page anymore
+ _visualPopupToolTip = null;
+ }
+
+ private void OnKryptonCalcContextMenuClosed(object? sender, EventArgs e)
+ {
+ if (sender is KryptonContextMenu kcm)
+ {
+ kcm.Closed -= OnKryptonCalcContextMenuClosed;
+ _buttonController.RemoveFixed();
+ kcm.Dispose();
+ }
+ }
+
+ private static string NormalizeExpressionForCompute(string expr, System.Globalization.CultureInfo culture)
+ {
+ var nf = culture.NumberFormat;
+ string decimalSep = nf.NumberDecimalSeparator;
+ string groupSep = nf.NumberGroupSeparator;
+
+ string s = expr.Replace("\u00A0", string.Empty).Replace(" ", string.Empty);
+
+ if (!string.IsNullOrEmpty(groupSep))
+ {
+ s = s.Replace(groupSep, string.Empty);
+ }
+
+ if (!string.IsNullOrEmpty(decimalSep) && decimalSep != ".")
+ {
+ s = s.Replace(decimalSep, ".");
+ }
+
+ return s;
+ }
+
+ private static bool IsDigitToken(string t) => t.Length == 1 && (char.IsDigit(t[0]) || t[0] == '.');
+
+ private static bool IsOperatorToken(string t)
+ {
+ if (t.Length != 1)
+ {
+ return false;
+ }
+ char ch = t[0];
+ return ch == '+' || ch == '-' || ch == '*' || ch == '/' || ch == '(' || ch == ')';
+ }
+ #endregion
+
+ [ToolboxItem(false)]
+ [DesignerCategory(@"code")]
+ private sealed class CalcContextMenu : KryptonContextMenu
+ {
+ private readonly Rectangle _dropScreenRect;
+
+ public CalcContextMenu(Rectangle dropScreenRect) => _dropScreenRect = dropScreenRect;
+
+ protected override VisualContextMenu CreateContextMenu(KryptonContextMenu kcm,
+ PaletteBase? palette,
+ PaletteMode paletteMode,
+ PaletteRedirect redirector,
+ PaletteRedirectContextMenu redirectorImages,
+ KryptonContextMenuCollection items,
+ bool enabled,
+ bool keyboardActivated) =>
+ new VisualContextMenuDTP(kcm, palette, paletteMode, redirector, redirectorImages, items, enabled, keyboardActivated, _dropScreenRect);
+ }
+
+ #region ButtonSpec wiring
+ private void OnButtonSpecInserted(object? sender, ButtonSpecEventArgs e)
+ {
+ if (e.ButtonSpec != null)
+ {
+ e.ButtonSpec.Click += OnAnyButtonSpecClick;
+ }
+ }
+
+ private void OnButtonSpecRemoved(object? sender, ButtonSpecEventArgs e)
+ {
+ if (e.ButtonSpec != null)
+ {
+ e.ButtonSpec.Click -= OnAnyButtonSpecClick;
+ }
+ }
+
+ private void OnAnyButtonSpecClick(object? sender, EventArgs e)
+ {
+ if (sender is ButtonSpec spec)
+ {
+ var index = ButtonSpecs.IndexOf(spec);
+ OnButtonSpecClicked(new ButtonSpecEventArgs(spec, index));
+ }
+ }
+
+ ///
+ /// Raises the ButtonSpecClicked event.
+ ///
+ /// Event args containing the clicked spec.
+ protected virtual void OnButtonSpecClicked(ButtonSpecEventArgs e) => ButtonSpecClicked?.Invoke(this, e);
+ #endregion
+}
\ No newline at end of file
diff --git a/Source/Krypton Components/Krypton.Toolkit/Designers/Action Lists/KryptonCalcButtonActionList.cs b/Source/Krypton Components/Krypton.Toolkit/Designers/Action Lists/KryptonCalcButtonActionList.cs
new file mode 100644
index 0000000000..7af1f94cc2
--- /dev/null
+++ b/Source/Krypton Components/Krypton.Toolkit/Designers/Action Lists/KryptonCalcButtonActionList.cs
@@ -0,0 +1,170 @@
+#region BSD License
+/*
+ *
+ * New BSD 3-Clause License (https://github.com/Krypton-Suite/Standard-Toolkit/blob/master/LICENSE)
+ * Modifications by Peter Wagner (aka Wagnerp), Simon Coghlan (aka Smurf-IV), tobitege 2025 - 2025. All rights reserved.
+ *
+ */
+#endregion
+
+namespace Krypton.Toolkit;
+
+internal class KryptonCalcInputActionList : DesignerActionList
+{
+ #region Instance Fields
+ private readonly KryptonCalcInput _calcButton;
+ private readonly IComponentChangeService? _service;
+ #endregion
+
+ #region Identity
+ ///
+ /// Initialize a new instance of the KryptonCalcInputActionList class.
+ ///
+ /// Designer that owns this action list instance.
+ public KryptonCalcInputActionList(KryptonCalcInputDesigner owner)
+ : base(owner.Component)
+ {
+ _calcButton = (owner.Component as KryptonCalcInput)!;
+ _service = GetService(typeof(IComponentChangeService)) as IComponentChangeService;
+ }
+ #endregion
+
+ #region Public
+ public InputControlStyle InputControlStyle
+ {
+ get => _calcButton.InputControlStyle;
+
+ set
+ {
+ if (_calcButton.InputControlStyle != value)
+ {
+ _service?.OnComponentChanged(_calcButton, null, _calcButton.InputControlStyle, value);
+ _calcButton.InputControlStyle = value;
+ }
+ }
+ }
+
+ public decimal Value
+ {
+ get => _calcButton.Value;
+
+ set
+ {
+ if (_calcButton.Value != value)
+ {
+ _service?.OnComponentChanged(_calcButton, null, _calcButton.Value, value);
+ _calcButton.Value = value;
+ }
+ }
+ }
+
+ public int DecimalPlaces
+ {
+ get => _calcButton.DecimalPlaces;
+
+ set
+ {
+ if (_calcButton.DecimalPlaces != value)
+ {
+ _service?.OnComponentChanged(_calcButton, null, _calcButton.DecimalPlaces, value);
+ _calcButton.DecimalPlaces = value;
+ }
+ }
+ }
+
+ public bool AllowDecimals
+ {
+ get => _calcButton.AllowDecimals;
+
+ set
+ {
+ if (_calcButton.AllowDecimals != value)
+ {
+ _service?.OnComponentChanged(_calcButton, null, _calcButton.AllowDecimals, value);
+ _calcButton.AllowDecimals = value;
+ }
+ }
+ }
+
+ public int DropDownWidth
+ {
+ get => _calcButton.DropDownWidth;
+
+ set
+ {
+ if (_calcButton.DropDownWidth != value)
+ {
+ _service?.OnComponentChanged(_calcButton, null, _calcButton.DropDownWidth, value);
+ _calcButton.DropDownWidth = value;
+ }
+ }
+ }
+
+ public VisualOrientation PopupSide
+ {
+ get => _calcButton.PopupSide;
+
+ set
+ {
+ if (_calcButton.PopupSide != value)
+ {
+ _service?.OnComponentChanged(_calcButton, null, _calcButton.PopupSide, value);
+ _calcButton.PopupSide = value;
+ }
+ }
+ }
+
+ public string Text
+ {
+ get => _calcButton.Text;
+
+ set
+ {
+ if (_calcButton.Text != value)
+ {
+ _service?.OnComponentChanged(_calcButton, null, _calcButton.Text, value);
+ _calcButton.Text = value;
+ }
+ }
+ }
+
+ public PaletteMode PaletteMode
+ {
+ get => _calcButton.PaletteMode;
+
+ set
+ {
+ if (_calcButton.PaletteMode != value)
+ {
+ _service?.OnComponentChanged(_calcButton, null, _calcButton.PaletteMode, value);
+ _calcButton.PaletteMode = value;
+ }
+ }
+ }
+ #endregion
+
+ #region Public Override
+ public override DesignerActionItemCollection GetSortedActionItems()
+ {
+ var actions = new DesignerActionItemCollection();
+
+ if (_calcButton != null)
+ {
+ actions.Add(new DesignerActionHeaderItem(nameof(Appearance)));
+ actions.Add(new DesignerActionPropertyItem(nameof(InputControlStyle), nameof(InputControlStyle), nameof(Appearance), @"Input control style"));
+ actions.Add(new DesignerActionPropertyItem(nameof(DecimalPlaces), nameof(DecimalPlaces), nameof(Appearance), @"Number of decimal places"));
+ actions.Add(new DesignerActionPropertyItem(nameof(AllowDecimals), nameof(AllowDecimals), nameof(Appearance), @"Allow decimal values"));
+ actions.Add(new DesignerActionHeaderItem(@"Layout"));
+ actions.Add(new DesignerActionPropertyItem(nameof(DropDownWidth), nameof(DropDownWidth), @"Layout", @"Default width of popup"));
+ actions.Add(new DesignerActionPropertyItem(nameof(PopupSide), nameof(PopupSide), @"Layout", @"Side where popup appears"));
+ actions.Add(new DesignerActionHeaderItem(@"Values"));
+ actions.Add(new DesignerActionPropertyItem(nameof(Text), nameof(Text), @"Values", @"Input text"));
+ actions.Add(new DesignerActionPropertyItem(nameof(Value), nameof(Value), @"Values", @"Numeric value"));
+ actions.Add(new DesignerActionHeaderItem(@"Visuals"));
+ actions.Add(new DesignerActionPropertyItem(nameof(PaletteMode), @"Palette", @"Visuals", @"Palette applied to drawing"));
+ }
+
+ return actions;
+ }
+ #endregion
+}
diff --git a/Source/Krypton Components/Krypton.Toolkit/Designers/Designers/KryptonCalcButtonDesigner.cs b/Source/Krypton Components/Krypton.Toolkit/Designers/Designers/KryptonCalcButtonDesigner.cs
new file mode 100644
index 0000000000..473738ef30
--- /dev/null
+++ b/Source/Krypton Components/Krypton.Toolkit/Designers/Designers/KryptonCalcButtonDesigner.cs
@@ -0,0 +1,45 @@
+#region BSD License
+/*
+ *
+ * New BSD 3-Clause License (https://github.com/Krypton-Suite/Standard-Toolkit/blob/master/LICENSE)
+ * Modifications by Peter Wagner (aka Wagnerp), Simon Coghlan (aka Smurf-IV), tobitege 2025 - 2025. All rights reserved.
+ *
+ */
+#endregion
+
+namespace Krypton.Toolkit;
+
+internal class KryptonCalcInputDesigner : ControlDesigner
+{
+ #region Identity
+ ///
+ /// Initialize a new instance of the KryptonCalcInputDesigner class.
+ ///
+ public KryptonCalcInputDesigner() =>
+ // The resizing handles around the control need to change depending on the
+ // value of the AutoSize and AutoSizeMode properties. When in AutoSize you
+ // do not get the resizing handles, otherwise you do.
+ AutoResizeHandles = true;
+
+ #endregion
+
+ #region Public Overrides
+ ///
+ /// Gets the design-time action lists supported by the component associated with the designer.
+ ///
+ public override DesignerActionListCollection ActionLists
+ {
+ get
+ {
+ // Create a collection of action lists
+ var actionLists = new DesignerActionListCollection
+ {
+ // Add the calculator button specific list
+ new KryptonCalcInputActionList(this)
+ };
+
+ return actionLists;
+ }
+ }
+ #endregion
+}
diff --git a/Source/Krypton Components/Krypton.Toolkit/Krypton.Toolkit 2022.csproj b/Source/Krypton Components/Krypton.Toolkit/Krypton.Toolkit 2022.csproj
index 70a40caf48..c64288fdaf 100644
--- a/Source/Krypton Components/Krypton.Toolkit/Krypton.Toolkit 2022.csproj
+++ b/Source/Krypton Components/Krypton.Toolkit/Krypton.Toolkit 2022.csproj
@@ -533,6 +533,7 @@
+
diff --git a/Source/Krypton Components/Krypton.Toolkit/ResourceFiles/Generic/GenericKryptonImageResources.Designer.cs b/Source/Krypton Components/Krypton.Toolkit/ResourceFiles/Generic/GenericKryptonImageResources.Designer.cs
index b56af09704..5d36d0dec3 100644
--- a/Source/Krypton Components/Krypton.Toolkit/ResourceFiles/Generic/GenericKryptonImageResources.Designer.cs
+++ b/Source/Krypton Components/Krypton.Toolkit/ResourceFiles/Generic/GenericKryptonImageResources.Designer.cs
@@ -110,6 +110,16 @@ internal static System.Drawing.Bitmap KryptonButtonSpec {
}
}
+ ///
+ /// Looks up a localized resource of type System.Drawing.Bitmap.
+ ///
+ internal static System.Drawing.Bitmap KryptonCalcInput {
+ get {
+ object obj = ResourceManager.GetObject("KryptonCalcInput", resourceCulture);
+ return ((System.Drawing.Bitmap)(obj));
+ }
+ }
+
///
/// Looks up a localized resource of type System.Drawing.Bitmap.
///
diff --git a/Source/Krypton Components/Krypton.Toolkit/ResourceFiles/Generic/GenericKryptonImageResources.resx b/Source/Krypton Components/Krypton.Toolkit/ResourceFiles/Generic/GenericKryptonImageResources.resx
index 1969f73937..fc49884d02 100644
--- a/Source/Krypton Components/Krypton.Toolkit/ResourceFiles/Generic/GenericKryptonImageResources.resx
+++ b/Source/Krypton Components/Krypton.Toolkit/ResourceFiles/Generic/GenericKryptonImageResources.resx
@@ -187,6 +187,9 @@
..\..\Resources\KryptonDropButton.bmp;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
+
+ ..\..\Resources\KryptonCalcInput.bmp;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
+
..\..\Resources\KryptonForm.bmp;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
diff --git a/Source/Krypton Components/Krypton.Toolkit/Resources/KryptonCalcInput.bmp b/Source/Krypton Components/Krypton.Toolkit/Resources/KryptonCalcInput.bmp
new file mode 100644
index 0000000000..2e2cba20a5
Binary files /dev/null and b/Source/Krypton Components/Krypton.Toolkit/Resources/KryptonCalcInput.bmp differ
diff --git a/Source/Krypton Components/Krypton.Toolkit/ToolboxBitmaps/KryptonCalcInput.bmp b/Source/Krypton Components/Krypton.Toolkit/ToolboxBitmaps/KryptonCalcInput.bmp
new file mode 100644
index 0000000000..2e2cba20a5
Binary files /dev/null and b/Source/Krypton Components/Krypton.Toolkit/ToolboxBitmaps/KryptonCalcInput.bmp differ
diff --git a/Source/Krypton Components/Krypton.Toolkit/View Draw/ViewDrawDropDownButton.cs b/Source/Krypton Components/Krypton.Toolkit/View Draw/ViewDrawDropDownButton.cs
index 999ec23f14..f39b9ab7e8 100644
--- a/Source/Krypton Components/Krypton.Toolkit/View Draw/ViewDrawDropDownButton.cs
+++ b/Source/Krypton Components/Krypton.Toolkit/View Draw/ViewDrawDropDownButton.cs
@@ -5,7 +5,7 @@
* © Component Factory Pty Ltd, 2006 - 2016, (Version 4.5.0.0) All rights reserved.
*
* New BSD 3-Clause License (https://github.com/Krypton-Suite/Standard-Toolkit/blob/master/LICENSE)
- * Modifications by Peter Wagner (aka Wagnerp), Simon Coghlan (aka Smurf-IV), Giduac & Ahmed Abdelhameed et al. 2017 - 2025. All rights reserved.
+ * Modifications by Peter Wagner (aka Wagnerp), Simon Coghlan (aka Smurf-IV), Giduac, Ahmed Abdelhameed, tobitege et al. 2017 - 2025. All rights reserved.
*
*/
#endregion
@@ -17,6 +17,10 @@ namespace Krypton.Toolkit;
///
public class ViewDrawDropDownButton : ViewLeaf
{
+ #region Instance Fields
+ private Image? _customGlyph;
+ #endregion
+
#region Identity
///
/// Initialize a new instance of the ViewDrawDropDownButton class.
@@ -51,6 +55,16 @@ public override string ToString() =>
///
public VisualOrientation Orientation { get; set; }
+ ///
+ /// Gets or sets a custom image to render instead of the default arrow glyph.
+ /// When null, the renderer-drawn arrow is used.
+ ///
+ public Image? CustomGlyph
+ {
+ get => _customGlyph;
+ set => _customGlyph = value;
+ }
+
#endregion
#region Layout
@@ -72,6 +86,12 @@ public override Size GetPreferredSize([DisallowNull] ViewLayoutContext context)
throw new ArgumentNullException(nameof(context.Renderer));
}
+ // If a custom glyph is provided, prefer its size
+ if (_customGlyph != null)
+ {
+ return _customGlyph.Size;
+ }
+
// Ask the renderer for the required size of the drop-down button
return context.Renderer.RenderGlyph.GetDropDownButtonPreferredSize(context, Palette, State, Orientation);
}
@@ -99,6 +119,7 @@ public override void Layout([DisallowNull] ViewLayoutContext context)
#region Paint
///
/// Perform rendering before child elements are rendered.
+ /// Allows to render a custom glyph, if set.
///
/// Rendering context.
public override void RenderBefore( [DisallowNull] RenderContext context)
@@ -108,11 +129,21 @@ public override void RenderBefore( [DisallowNull] RenderContext context)
throw new ArgumentNullException(nameof(context.Renderer));
}
- context.Renderer.RenderGlyph.DrawDropDownButton(context,
- ClientRectangle,
- Palette,
- State,
- Orientation);
+ if (_customGlyph != null)
+ {
+ Size gsz = _customGlyph.Size;
+ int x = ClientRectangle.X + (ClientRectangle.Width - gsz.Width) / 2;
+ int y = ClientRectangle.Y + (ClientRectangle.Height - gsz.Height) / 2;
+ context.Graphics.DrawImage(_customGlyph, new Rectangle(new Point(x, y), gsz));
+ }
+ else
+ {
+ context.Renderer.RenderGlyph.DrawDropDownButton(context,
+ ClientRectangle,
+ Palette,
+ State,
+ Orientation);
+ }
}
#endregion
diff --git a/Source/Krypton Components/TestForm/ButtonsTest.Designer.cs b/Source/Krypton Components/TestForm/ButtonsTest.Designer.cs
index 1f998fdfd8..5196bc31f9 100644
--- a/Source/Krypton Components/TestForm/ButtonsTest.Designer.cs
+++ b/Source/Krypton Components/TestForm/ButtonsTest.Designer.cs
@@ -39,6 +39,8 @@ private void InitializeComponent()
{
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(ButtonsTest));
this.kryptonPanel1 = new Krypton.Toolkit.KryptonPanel();
+ this.KryptonCalcInput1 = new Krypton.Toolkit.KryptonCalcInput();
+ this.buttonSpecAny2 = new Krypton.Toolkit.ButtonSpecAny();
this.kryptonLabel2 = new Krypton.Toolkit.KryptonLabel();
this.kcbColorScheme = new Krypton.Toolkit.KryptonComboBox();
this.kryptonLabel1 = new Krypton.Toolkit.KryptonLabel();
@@ -72,6 +74,7 @@ private void InitializeComponent()
//
// kryptonPanel1
//
+ this.kryptonPanel1.Controls.Add(this.KryptonCalcInput1);
this.kryptonPanel1.Controls.Add(this.kryptonLabel2);
this.kryptonPanel1.Controls.Add(this.kcbColorScheme);
this.kryptonPanel1.Controls.Add(this.kryptonLabel1);
@@ -91,9 +94,33 @@ private void InitializeComponent()
this.kryptonPanel1.Dock = System.Windows.Forms.DockStyle.Fill;
this.kryptonPanel1.Location = new System.Drawing.Point(0, 0);
this.kryptonPanel1.Name = "kryptonPanel1";
- this.kryptonPanel1.Size = new System.Drawing.Size(559, 305);
+ this.kryptonPanel1.Size = new System.Drawing.Size(562, 318);
this.kryptonPanel1.TabIndex = 0;
//
+ // KryptonCalcInput1
+ //
+ this.KryptonCalcInput1.AllowDecimals = true;
+ this.KryptonCalcInput1.ButtonSpecs.Add(this.buttonSpecAny2);
+ this.KryptonCalcInput1.DecimalPlaces = 2;
+ this.KryptonCalcInput1.DropDownWidth = 0;
+ this.KryptonCalcInput1.Font = new System.Drawing.Font("Microsoft Sans Serif", 9.75F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
+ this.KryptonCalcInput1.Location = new System.Drawing.Point(120, 270);
+ this.KryptonCalcInput1.Name = "KryptonCalcInput1";
+ this.KryptonCalcInput1.Size = new System.Drawing.Size(141, 24);
+ this.KryptonCalcInput1.TabIndex = 16;
+ this.KryptonCalcInput1.Value = new decimal(new int[] {
+ 0,
+ 0,
+ 0,
+ 0});
+ this.KryptonCalcInput1.ButtonSpecClicked += new System.EventHandler(this.KryptonCalcInput1_ButtonSpecClicked);
+ //
+ // buttonSpecAny2
+ //
+ this.buttonSpecAny2.Image = ((System.Drawing.Image)(resources.GetObject("buttonSpecAny2.Image")));
+ this.buttonSpecAny2.Type = Krypton.Toolkit.PaletteButtonSpecStyle.Close;
+ this.buttonSpecAny2.UniqueName = "f71d42b1cecb4ded8a08ee132c851ffe";
+ //
// kryptonLabel2
//
this.kryptonLabel2.Location = new System.Drawing.Point(18, 173);
@@ -149,7 +176,7 @@ private void InitializeComponent()
//
this.kbtnButtonStyles.Location = new System.Drawing.Point(278, 270);
this.kbtnButtonStyles.Name = "kbtnButtonStyles";
- this.kbtnButtonStyles.Size = new System.Drawing.Size(243, 25);
+ this.kbtnButtonStyles.Size = new System.Drawing.Size(243, 24);
this.kbtnButtonStyles.TabIndex = 10;
this.kbtnButtonStyles.Values.DropDownArrowColor = System.Drawing.Color.Empty;
this.kbtnButtonStyles.Values.Text = "Button Styles";
@@ -332,7 +359,7 @@ private void InitializeComponent()
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ButtonSpecs.Add(this.buttonSpecAny1);
- this.ClientSize = new System.Drawing.Size(559, 305);
+ this.ClientSize = new System.Drawing.Size(562, 318);
this.Controls.Add(this.kryptonPanel1);
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.Fixed3D;
this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon")));
@@ -379,5 +406,7 @@ private void InitializeComponent()
private Krypton.Toolkit.KryptonLabel kryptonLabel1;
private Krypton.Toolkit.KryptonComboBox kcbColorScheme;
private Krypton.Toolkit.KryptonLabel kryptonLabel2;
+ private KryptonCalcInput KryptonCalcInput1;
+ private ButtonSpecAny buttonSpecAny2;
}
}
diff --git a/Source/Krypton Components/TestForm/ButtonsTest.cs b/Source/Krypton Components/TestForm/ButtonsTest.cs
index 127c39b422..5d5dfcde23 100644
--- a/Source/Krypton Components/TestForm/ButtonsTest.cs
+++ b/Source/Krypton Components/TestForm/ButtonsTest.cs
@@ -64,4 +64,12 @@ private void kcbColorScheme_SelectedIndexChanged(object sender, EventArgs e)
kcbtnDropDown.SchemeThemes = scheme;
kcbSortMode.Enabled = scheme == Krypton.Toolkit.ColorScheme.PaletteColors;
}
+
+ private void KryptonCalcInput1_ButtonSpecClicked(object sender, ButtonSpecEventArgs e)
+ {
+ if (e.ButtonSpec is ButtonSpecAny any && any.Type == PaletteButtonSpecStyle.Close)
+ {
+ KryptonCalcInput1.Value = 0m;
+ }
+ }
}
\ No newline at end of file
diff --git a/Source/Krypton Components/TestForm/ButtonsTest.resx b/Source/Krypton Components/TestForm/ButtonsTest.resx
index 21eb1fe54c..f36669bdc6 100644
--- a/Source/Krypton Components/TestForm/ButtonsTest.resx
+++ b/Source/Krypton Components/TestForm/ButtonsTest.resx
@@ -118,6 +118,24 @@
System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAL
+ DQAACw0B7QfALAAAAAd0SU1FB9gEDw0NJWkouy0AAAK4SURBVDhPdVNbSFRRFN3jYxwHxbcovt9+KOiH
+ KAgiZlg6VpqO3pEczBgbRdR8IBGk0EOs7z6DCOrDyNBSC7FmHEctfGQPK4iIKKuv7CMwwdW6M02g5IbD
+ 3Nlnr7XXWfsc8cRBkeoDIpuHRBr4V+PO7o4qkVqDyK9jIvV/U+4oE6lTdLqd6b4+NAcEoFrEzPQuErIq
+ LVrtzp2SErT4+aFOpNG1oXau1+t3ZgcGsDI0BEd/P6x6PdjiHwk/lHaCJsrLMV5aipHCQpz29UUt80LZ
+ Px62tWFlcBALPT1wdnTAbrGgS6eDQqwK7tZqMV1WhgfFxRjNy8NITg5upaaqBNuqfGMj5dusVji47E1N
+ cJrNmDMa0csuPQQ72NVG6Y/y8zGWnY3RlBT0enurDZpUhXKcXSwsfKIocJpMmK+pwbOqKhdwrqgI87m5
+ mImLw0RwMO75++OcCE6KWAj1chGowTMr7ezoqKjAUmUlVnneJUp9GhXlWguRkbCT4IKXF6x7wZ5Qne70
+ 8YGzoADLSUl4EROD9fh4rCckYCk8HMMEt+4HVuMECc5Q3nRgIF5R8ofkZHxOT8entDSskeCKRoPu/4zY
+ FarbHSyYJHg1NhbvCf6WkYGfNG0zKwsb/H5Nkqus6dtLQjeVVm7c5fznIiKwRoKPdPpNdDQ2EhOxRRO/
+ Z2a6SJdDQnCJtV0eEl5L4ymRnZu8KJNBQZhll+ckWCXRRZ55mMVf6MNXKnhHX1boi411g8zTjAY5IrJ1
+ jTO9zTGO8xo/DguDLTQUA27DzPTEoMp+SzUvSbTIicxw/z7VsvG2HBUxNdO46xzhKJNjJOl3j8pzTg3P
+ bLjMJotUZ+eaok9nSUqcSbVA1IfRwsQNqujkL6/XXqc1PLPhPImn2EAFU537MXmihmy8279p6H7PWUPJ
+ h7m2uNydReQPiLFB/uRDeL0AAAAASUVORK5CYII=
+
+
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAL