control work fixes
This commit is contained in:
39
Controls/EntryExtensions.cs
Normal file
39
Controls/EntryExtensions.cs
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
// Licensed to the .NET Foundation under one or more agreements.
|
||||||
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||||
|
|
||||||
|
using Microsoft.Maui.Controls;
|
||||||
|
|
||||||
|
namespace Microsoft.Maui.Controls;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provides attached properties for Entry controls.
|
||||||
|
/// </summary>
|
||||||
|
public static class EntryExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Attached property for SelectAllOnDoubleClick behavior.
|
||||||
|
/// When true, double-clicking the entry selects all text instead of just the word.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly BindableProperty SelectAllOnDoubleClickProperty =
|
||||||
|
BindableProperty.CreateAttached(
|
||||||
|
"SelectAllOnDoubleClick",
|
||||||
|
typeof(bool),
|
||||||
|
typeof(EntryExtensions),
|
||||||
|
false);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the SelectAllOnDoubleClick value for the specified entry.
|
||||||
|
/// </summary>
|
||||||
|
public static bool GetSelectAllOnDoubleClick(BindableObject view)
|
||||||
|
{
|
||||||
|
return (bool)view.GetValue(SelectAllOnDoubleClickProperty);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the SelectAllOnDoubleClick value for the specified entry.
|
||||||
|
/// </summary>
|
||||||
|
public static void SetSelectAllOnDoubleClick(BindableObject view, bool value)
|
||||||
|
{
|
||||||
|
view.SetValue(SelectAllOnDoubleClickProperty, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -35,6 +35,7 @@ public partial class EntryHandler : ViewHandler<IEntry, SkiaEntry>
|
|||||||
[nameof(ITextAlignment.VerticalTextAlignment)] = MapVerticalTextAlignment,
|
[nameof(ITextAlignment.VerticalTextAlignment)] = MapVerticalTextAlignment,
|
||||||
[nameof(IView.Background)] = MapBackground,
|
[nameof(IView.Background)] = MapBackground,
|
||||||
["BackgroundColor"] = MapBackgroundColor,
|
["BackgroundColor"] = MapBackgroundColor,
|
||||||
|
["SelectAllOnDoubleClick"] = MapSelectAllOnDoubleClick,
|
||||||
};
|
};
|
||||||
|
|
||||||
public static CommandMapper<IEntry, EntryHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
|
public static CommandMapper<IEntry, EntryHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
|
||||||
@@ -245,4 +246,14 @@ public partial class EntryHandler : ViewHandler<IEntry, SkiaEntry>
|
|||||||
handler.PlatformView.BackgroundColor = ve.BackgroundColor;
|
handler.PlatformView.BackgroundColor = ve.BackgroundColor;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void MapSelectAllOnDoubleClick(EntryHandler handler, IEntry entry)
|
||||||
|
{
|
||||||
|
if (handler.PlatformView is null) return;
|
||||||
|
|
||||||
|
if (entry is BindableObject bindable)
|
||||||
|
{
|
||||||
|
handler.PlatformView.SelectAllOnDoubleClick = EntryExtensions.GetSelectAllOnDoubleClick(bindable);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ public partial class ProgressBarHandler : ViewHandler<IProgress, SkiaProgressBar
|
|||||||
[nameof(IView.IsEnabled)] = MapIsEnabled,
|
[nameof(IView.IsEnabled)] = MapIsEnabled,
|
||||||
[nameof(IView.Background)] = MapBackground,
|
[nameof(IView.Background)] = MapBackground,
|
||||||
["BackgroundColor"] = MapBackgroundColor,
|
["BackgroundColor"] = MapBackgroundColor,
|
||||||
|
[nameof(IView.Height)] = MapHeight,
|
||||||
|
[nameof(IView.Width)] = MapWidth,
|
||||||
|
["VerticalOptions"] = MapVerticalOptions,
|
||||||
};
|
};
|
||||||
|
|
||||||
public static CommandMapper<IProgress, ProgressBarHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
|
public static CommandMapper<IProgress, ProgressBarHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
|
||||||
@@ -131,4 +134,34 @@ public partial class ProgressBarHandler : ViewHandler<IProgress, SkiaProgressBar
|
|||||||
handler.PlatformView.Invalidate();
|
handler.PlatformView.Invalidate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void MapHeight(ProgressBarHandler handler, IProgress progress)
|
||||||
|
{
|
||||||
|
if (handler.PlatformView is null) return;
|
||||||
|
|
||||||
|
if (progress is VisualElement visualElement && visualElement.HeightRequest >= 0)
|
||||||
|
{
|
||||||
|
handler.PlatformView.HeightRequest = visualElement.HeightRequest;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void MapWidth(ProgressBarHandler handler, IProgress progress)
|
||||||
|
{
|
||||||
|
if (handler.PlatformView is null) return;
|
||||||
|
|
||||||
|
if (progress is VisualElement visualElement && visualElement.WidthRequest >= 0)
|
||||||
|
{
|
||||||
|
handler.PlatformView.WidthRequest = visualElement.WidthRequest;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void MapVerticalOptions(ProgressBarHandler handler, IProgress progress)
|
||||||
|
{
|
||||||
|
if (handler.PlatformView is null) return;
|
||||||
|
|
||||||
|
if (progress is Microsoft.Maui.Controls.View view)
|
||||||
|
{
|
||||||
|
handler.PlatformView.VerticalOptions = view.VerticalOptions;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ public class LinuxApplication : IDisposable
|
|||||||
private static int _requestRedrawCount;
|
private static int _requestRedrawCount;
|
||||||
private static int _drawCount;
|
private static int _drawCount;
|
||||||
private static int _gtkThreadId;
|
private static int _gtkThreadId;
|
||||||
|
public static int GtkThreadId => _gtkThreadId;
|
||||||
private static DateTime _lastCounterReset = DateTime.Now;
|
private static DateTime _lastCounterReset = DateTime.Now;
|
||||||
private static bool _isRedrawing;
|
private static bool _isRedrawing;
|
||||||
private static int _loopCounter = 0;
|
private static int _loopCounter = 0;
|
||||||
|
|||||||
@@ -250,15 +250,21 @@ public sealed class GtkSkiaSurfaceWidget : IDisposable
|
|||||||
private bool OnButtonPress(IntPtr widget, IntPtr eventData, IntPtr userData)
|
private bool OnButtonPress(IntPtr widget, IntPtr eventData, IntPtr userData)
|
||||||
{
|
{
|
||||||
GtkNative.gtk_widget_grab_focus(_widget);
|
GtkNative.gtk_widget_grab_focus(_widget);
|
||||||
var (x, y, button) = ParseButtonEvent(eventData);
|
var (x, y, button, eventType) = ParseButtonEvent(eventData);
|
||||||
Console.WriteLine($"[GtkSkiaSurfaceWidget] ButtonPress at ({x}, {y}), button={button}");
|
|
||||||
|
// GTK event types: GDK_BUTTON_PRESS=4, GDK_2BUTTON_PRESS=5, GDK_3BUTTON_PRESS=6
|
||||||
|
// Only process single button press events. GTK sends 2BUTTON_PRESS and 3BUTTON_PRESS
|
||||||
|
// events after detecting double/triple clicks, but we handle that ourselves in SkiaEntry.
|
||||||
|
if (eventType != 4) // GDK_BUTTON_PRESS
|
||||||
|
return true;
|
||||||
|
|
||||||
PointerPressed?.Invoke(this, (x, y, button));
|
PointerPressed?.Invoke(this, (x, y, button));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool OnButtonRelease(IntPtr widget, IntPtr eventData, IntPtr userData)
|
private bool OnButtonRelease(IntPtr widget, IntPtr eventData, IntPtr userData)
|
||||||
{
|
{
|
||||||
var (x, y, button) = ParseButtonEvent(eventData);
|
var (x, y, button, _) = ParseButtonEvent(eventData);
|
||||||
PointerReleased?.Invoke(this, (x, y, button));
|
PointerReleased?.Invoke(this, (x, y, button));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -272,7 +278,6 @@ public sealed class GtkSkiaSurfaceWidget : IDisposable
|
|||||||
|
|
||||||
public void RaisePointerPressed(double x, double y, int button)
|
public void RaisePointerPressed(double x, double y, int button)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"[GtkSkiaSurfaceWidget] RaisePointerPressed at ({x}, {y}), button={button}");
|
|
||||||
PointerPressed?.Invoke(this, (x, y, button));
|
PointerPressed?.Invoke(this, (x, y, button));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -319,10 +324,10 @@ public sealed class GtkSkiaSurfaceWidget : IDisposable
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static (double x, double y, int button) ParseButtonEvent(IntPtr eventData)
|
private static (double x, double y, int button, int eventType) ParseButtonEvent(IntPtr eventData)
|
||||||
{
|
{
|
||||||
var evt = Marshal.PtrToStructure<GdkEventButton>(eventData);
|
var evt = Marshal.PtrToStructure<GdkEventButton>(eventData);
|
||||||
return (evt.x, evt.y, (int)evt.button);
|
return (evt.x, evt.y, (int)evt.button, evt.type);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static (double x, double y) ParseMotionEvent(IntPtr eventData)
|
private static (double x, double y) ParseMotionEvent(IntPtr eventData)
|
||||||
|
|||||||
@@ -209,6 +209,17 @@ public class SkiaEntry : SkiaView, IInputContext
|
|||||||
typeof(SkiaEntry),
|
typeof(SkiaEntry),
|
||||||
0);
|
0);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bindable property for SelectAllOnDoubleClick.
|
||||||
|
/// When true, double-clicking selects all text instead of just the word.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly BindableProperty SelectAllOnDoubleClickProperty =
|
||||||
|
BindableProperty.Create(
|
||||||
|
nameof(SelectAllOnDoubleClick),
|
||||||
|
typeof(bool),
|
||||||
|
typeof(SkiaEntry),
|
||||||
|
false);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Bindable property for IsReadOnly.
|
/// Bindable property for IsReadOnly.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -504,6 +515,16 @@ public class SkiaEntry : SkiaView, IInputContext
|
|||||||
set => SetValue(MaxLengthProperty, value);
|
set => SetValue(MaxLengthProperty, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets whether double-clicking selects all text instead of just the word.
|
||||||
|
/// Useful for URL bars and similar inputs.
|
||||||
|
/// </summary>
|
||||||
|
public bool SelectAllOnDoubleClick
|
||||||
|
{
|
||||||
|
get => (bool)GetValue(SelectAllOnDoubleClickProperty);
|
||||||
|
set => SetValue(SelectAllOnDoubleClickProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets whether the entry is read-only.
|
/// Gets or sets whether the entry is read-only.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -1372,13 +1393,11 @@ public class SkiaEntry : SkiaView, IInputContext
|
|||||||
|
|
||||||
public override void OnPointerPressed(PointerEventArgs e)
|
public override void OnPointerPressed(PointerEventArgs e)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"[SkiaEntry] OnPointerPressed Button={e.Button} at ({e.X}, {e.Y})");
|
|
||||||
if (!IsEnabled) return;
|
if (!IsEnabled) return;
|
||||||
|
|
||||||
// Handle right-click context menu
|
// Handle right-click context menu
|
||||||
if (e.Button == PointerButton.Right)
|
if (e.Button == PointerButton.Right)
|
||||||
{
|
{
|
||||||
Console.WriteLine("[SkiaEntry] Right-click detected, showing context menu");
|
|
||||||
ShowContextMenu(e.X, e.Y);
|
ShowContextMenu(e.X, e.Y);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1409,15 +1428,22 @@ public class SkiaEntry : SkiaView, IInputContext
|
|||||||
var clickX = e.X - (float)screenBounds.Left - (float)Padding.Left + _scrollOffset;
|
var clickX = e.X - (float)screenBounds.Left - (float)Padding.Left + _scrollOffset;
|
||||||
_cursorPosition = GetCharacterIndexAtX(clickX);
|
_cursorPosition = GetCharacterIndexAtX(clickX);
|
||||||
|
|
||||||
// Check for double-click (select word)
|
// Check for double-click (select word or select all)
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
var timeSinceLastClick = (now - _lastClickTime).TotalMilliseconds;
|
var timeSinceLastClick = (now - _lastClickTime).TotalMilliseconds;
|
||||||
var distanceFromLastClick = Math.Abs(e.X - _lastClickX);
|
var distanceFromLastClick = Math.Abs(e.X - _lastClickX);
|
||||||
|
|
||||||
if (timeSinceLastClick < DoubleClickThresholdMs && distanceFromLastClick < 10)
|
if (timeSinceLastClick < DoubleClickThresholdMs && distanceFromLastClick < 10)
|
||||||
{
|
{
|
||||||
// Double-click: select the word at cursor
|
// Double-click: select all or select word based on property
|
||||||
|
if (SelectAllOnDoubleClick)
|
||||||
|
{
|
||||||
|
SelectAll();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
SelectWordAtCursor();
|
SelectWordAtCursor();
|
||||||
|
}
|
||||||
_lastClickTime = DateTime.MinValue; // Reset to prevent triple-click issues
|
_lastClickTime = DateTime.MinValue; // Reset to prevent triple-click issues
|
||||||
_isSelecting = false;
|
_isSelecting = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -805,12 +805,9 @@ public class SkiaImageButton : SkiaView
|
|||||||
}
|
}
|
||||||
// Fill (3) and Start (0) both use y = bounds.Top
|
// Fill (3) and Start (0) both use y = bounds.Top
|
||||||
|
|
||||||
var result1 = new Rect(x, y, finalWidth, finalHeight);
|
return new Rect(x, y, finalWidth, finalHeight);
|
||||||
Console.WriteLine($"[SkiaImageButton] ArrangeOverride output (aligned): Y={result1.Y}, Height={result1.Height}");
|
|
||||||
return result1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Console.WriteLine($"[SkiaImageButton] ArrangeOverride output (unchanged): Y={bounds.Y}, Height={bounds.Height}");
|
|
||||||
return bounds;
|
return bounds;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -248,13 +248,7 @@ public abstract class SkiaLayoutView : SkiaView
|
|||||||
public override SkiaView? HitTest(float x, float y)
|
public override SkiaView? HitTest(float x, float y)
|
||||||
{
|
{
|
||||||
if (!IsVisible || !IsEnabled || !Bounds.Contains(x, y))
|
if (!IsVisible || !IsEnabled || !Bounds.Contains(x, y))
|
||||||
{
|
|
||||||
if (this is SkiaBorder)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"[SkiaBorder.HitTest] Miss - x={x}, y={y}, Bounds={Bounds}, IsVisible={IsVisible}, IsEnabled={IsEnabled}");
|
|
||||||
}
|
|
||||||
return null;
|
return null;
|
||||||
}
|
|
||||||
|
|
||||||
// Hit test children in reverse order (top-most first)
|
// Hit test children in reverse order (top-most first)
|
||||||
for (int i = _children.Count - 1; i >= 0; i--)
|
for (int i = _children.Count - 1; i >= 0; i--)
|
||||||
@@ -262,19 +256,9 @@ public abstract class SkiaLayoutView : SkiaView
|
|||||||
var child = _children[i];
|
var child = _children[i];
|
||||||
var hit = child.HitTest(x, y);
|
var hit = child.HitTest(x, y);
|
||||||
if (hit != null)
|
if (hit != null)
|
||||||
{
|
|
||||||
if (this is SkiaBorder)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"[SkiaBorder.HitTest] Hit child - x={x}, y={y}, Bounds={Bounds}, child={hit.GetType().Name}");
|
|
||||||
}
|
|
||||||
return hit;
|
return hit;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (this is SkiaBorder)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"[SkiaBorder.HitTest] Hit self - x={x}, y={y}, Bounds={Bounds}, children={_children.Count}");
|
|
||||||
}
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -187,9 +187,22 @@ public class SkiaProgressBar : SkiaView
|
|||||||
var barHeight = (float)BarHeight;
|
var barHeight = (float)BarHeight;
|
||||||
var cornerRadius = (float)CornerRadius;
|
var cornerRadius = (float)CornerRadius;
|
||||||
|
|
||||||
|
// If bounds height is small (HeightRequest was set), use the full bounds height
|
||||||
|
// Otherwise, center the bar vertically within the bounds
|
||||||
|
float trackTop, trackBottom;
|
||||||
|
if (bounds.Height <= barHeight + 4)
|
||||||
|
{
|
||||||
|
// Small bounds - fill the height
|
||||||
|
trackTop = bounds.Top;
|
||||||
|
trackBottom = bounds.Bottom;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Large bounds - center the bar
|
||||||
float midY = bounds.MidY;
|
float midY = bounds.MidY;
|
||||||
float trackTop = midY - barHeight / 2f;
|
trackTop = midY - barHeight / 2f;
|
||||||
float trackBottom = midY + barHeight / 2f;
|
trackBottom = midY + barHeight / 2f;
|
||||||
|
}
|
||||||
|
|
||||||
// Get colors
|
// Get colors
|
||||||
var trackColorSK = ToSKColor(TrackColor);
|
var trackColorSK = ToSKColor(TrackColor);
|
||||||
@@ -246,8 +259,11 @@ public class SkiaProgressBar : SkiaView
|
|||||||
|
|
||||||
protected override Size MeasureOverride(Size availableSize)
|
protected override Size MeasureOverride(Size availableSize)
|
||||||
{
|
{
|
||||||
var barHeight = BarHeight;
|
// Respect HeightRequest if set, otherwise use BarHeight with padding
|
||||||
return new Size(200, barHeight + 8);
|
var height = HeightRequest >= 0 ? HeightRequest : BarHeight + 8;
|
||||||
|
// Respect WidthRequest if set, otherwise use available width or default
|
||||||
|
var width = WidthRequest >= 0 ? WidthRequest : (availableSize.Width > 0 ? availableSize.Width : 200);
|
||||||
|
return new Size(width, height);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using Microsoft.Maui.Controls.Shapes;
|
|||||||
using Microsoft.Maui.Graphics;
|
using Microsoft.Maui.Graphics;
|
||||||
using Microsoft.Maui.Platform.Linux;
|
using Microsoft.Maui.Platform.Linux;
|
||||||
using Microsoft.Maui.Platform.Linux.Handlers;
|
using Microsoft.Maui.Platform.Linux.Handlers;
|
||||||
|
using Microsoft.Maui.Platform.Linux.Native;
|
||||||
using Microsoft.Maui.Platform.Linux.Rendering;
|
using Microsoft.Maui.Platform.Linux.Rendering;
|
||||||
using Microsoft.Maui.Platform.Linux.Services;
|
using Microsoft.Maui.Platform.Linux.Services;
|
||||||
using Microsoft.Maui.Platform.Linux.Window;
|
using Microsoft.Maui.Platform.Linux.Window;
|
||||||
@@ -1270,8 +1271,27 @@ public abstract class SkiaView : BindableObject, IDisposable, IAccessible
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Requests that this view be redrawn.
|
/// Requests that this view be redrawn.
|
||||||
|
/// Thread-safe - will marshal to GTK thread if needed.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void Invalidate()
|
public void Invalidate()
|
||||||
|
{
|
||||||
|
// Check if we're on the GTK thread - if not, marshal the entire call
|
||||||
|
int currentThread = Environment.CurrentManagedThreadId;
|
||||||
|
int gtkThread = LinuxApplication.GtkThreadId;
|
||||||
|
if (gtkThread != 0 && currentThread != gtkThread)
|
||||||
|
{
|
||||||
|
GLibNative.IdleAdd(() =>
|
||||||
|
{
|
||||||
|
InvalidateInternal();
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
InvalidateInternal();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InvalidateInternal()
|
||||||
{
|
{
|
||||||
LinuxApplication.LogInvalidate(GetType().Name);
|
LinuxApplication.LogInvalidate(GetType().Name);
|
||||||
Invalidated?.Invoke(this, EventArgs.Empty);
|
Invalidated?.Invoke(this, EventArgs.Empty);
|
||||||
@@ -1286,7 +1306,7 @@ public abstract class SkiaView : BindableObject, IDisposable, IAccessible
|
|||||||
|
|
||||||
if (_parent != null)
|
if (_parent != null)
|
||||||
{
|
{
|
||||||
_parent.Invalidate();
|
_parent.InvalidateInternal();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user