diff --git a/Controls/EntryExtensions.cs b/Controls/EntryExtensions.cs new file mode 100644 index 0000000..36f1d1b --- /dev/null +++ b/Controls/EntryExtensions.cs @@ -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; + +/// +/// Provides attached properties for Entry controls. +/// +public static class EntryExtensions +{ + /// + /// Attached property for SelectAllOnDoubleClick behavior. + /// When true, double-clicking the entry selects all text instead of just the word. + /// + public static readonly BindableProperty SelectAllOnDoubleClickProperty = + BindableProperty.CreateAttached( + "SelectAllOnDoubleClick", + typeof(bool), + typeof(EntryExtensions), + false); + + /// + /// Gets the SelectAllOnDoubleClick value for the specified entry. + /// + public static bool GetSelectAllOnDoubleClick(BindableObject view) + { + return (bool)view.GetValue(SelectAllOnDoubleClickProperty); + } + + /// + /// Sets the SelectAllOnDoubleClick value for the specified entry. + /// + public static void SetSelectAllOnDoubleClick(BindableObject view, bool value) + { + view.SetValue(SelectAllOnDoubleClickProperty, value); + } +} diff --git a/Handlers/EntryHandler.cs b/Handlers/EntryHandler.cs index 64af349..9184cce 100644 --- a/Handlers/EntryHandler.cs +++ b/Handlers/EntryHandler.cs @@ -35,6 +35,7 @@ public partial class EntryHandler : ViewHandler [nameof(ITextAlignment.VerticalTextAlignment)] = MapVerticalTextAlignment, [nameof(IView.Background)] = MapBackground, ["BackgroundColor"] = MapBackgroundColor, + ["SelectAllOnDoubleClick"] = MapSelectAllOnDoubleClick, }; public static CommandMapper CommandMapper = new(ViewHandler.ViewCommandMapper) @@ -245,4 +246,14 @@ public partial class EntryHandler : ViewHandler 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); + } + } } diff --git a/Handlers/ProgressBarHandler.cs b/Handlers/ProgressBarHandler.cs index b5c406c..69896ae 100644 --- a/Handlers/ProgressBarHandler.cs +++ b/Handlers/ProgressBarHandler.cs @@ -23,6 +23,9 @@ public partial class ProgressBarHandler : ViewHandler CommandMapper = new(ViewHandler.ViewCommandMapper) @@ -131,4 +134,34 @@ public partial class ProgressBarHandler : ViewHandler= 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; + } + } } diff --git a/LinuxApplication.cs b/LinuxApplication.cs index 97de89b..9f2f99f 100644 --- a/LinuxApplication.cs +++ b/LinuxApplication.cs @@ -31,6 +31,7 @@ public class LinuxApplication : IDisposable private static int _requestRedrawCount; private static int _drawCount; private static int _gtkThreadId; + public static int GtkThreadId => _gtkThreadId; private static DateTime _lastCounterReset = DateTime.Now; private static bool _isRedrawing; private static int _loopCounter = 0; diff --git a/Rendering/GtkSkiaSurfaceWidget.cs b/Rendering/GtkSkiaSurfaceWidget.cs index 0f05f8f..d2b16be 100644 --- a/Rendering/GtkSkiaSurfaceWidget.cs +++ b/Rendering/GtkSkiaSurfaceWidget.cs @@ -250,15 +250,21 @@ public sealed class GtkSkiaSurfaceWidget : IDisposable private bool OnButtonPress(IntPtr widget, IntPtr eventData, IntPtr userData) { GtkNative.gtk_widget_grab_focus(_widget); - var (x, y, button) = ParseButtonEvent(eventData); - Console.WriteLine($"[GtkSkiaSurfaceWidget] ButtonPress at ({x}, {y}), button={button}"); + var (x, y, button, eventType) = ParseButtonEvent(eventData); + + // 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)); return true; } 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)); return true; } @@ -272,7 +278,6 @@ public sealed class GtkSkiaSurfaceWidget : IDisposable public void RaisePointerPressed(double x, double y, int button) { - Console.WriteLine($"[GtkSkiaSurfaceWidget] RaisePointerPressed at ({x}, {y}), button={button}"); PointerPressed?.Invoke(this, (x, y, button)); } @@ -319,10 +324,10 @@ public sealed class GtkSkiaSurfaceWidget : IDisposable 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(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) diff --git a/Views/SkiaEntry.cs b/Views/SkiaEntry.cs index 50357b8..c96aad9 100644 --- a/Views/SkiaEntry.cs +++ b/Views/SkiaEntry.cs @@ -209,6 +209,17 @@ public class SkiaEntry : SkiaView, IInputContext typeof(SkiaEntry), 0); + /// + /// Bindable property for SelectAllOnDoubleClick. + /// When true, double-clicking selects all text instead of just the word. + /// + public static readonly BindableProperty SelectAllOnDoubleClickProperty = + BindableProperty.Create( + nameof(SelectAllOnDoubleClick), + typeof(bool), + typeof(SkiaEntry), + false); + /// /// Bindable property for IsReadOnly. /// @@ -504,6 +515,16 @@ public class SkiaEntry : SkiaView, IInputContext set => SetValue(MaxLengthProperty, value); } + /// + /// Gets or sets whether double-clicking selects all text instead of just the word. + /// Useful for URL bars and similar inputs. + /// + public bool SelectAllOnDoubleClick + { + get => (bool)GetValue(SelectAllOnDoubleClickProperty); + set => SetValue(SelectAllOnDoubleClickProperty, value); + } + /// /// Gets or sets whether the entry is read-only. /// @@ -1372,13 +1393,11 @@ public class SkiaEntry : SkiaView, IInputContext public override void OnPointerPressed(PointerEventArgs e) { - Console.WriteLine($"[SkiaEntry] OnPointerPressed Button={e.Button} at ({e.X}, {e.Y})"); if (!IsEnabled) return; // Handle right-click context menu if (e.Button == PointerButton.Right) { - Console.WriteLine("[SkiaEntry] Right-click detected, showing context menu"); ShowContextMenu(e.X, e.Y); return; } @@ -1409,15 +1428,22 @@ public class SkiaEntry : SkiaView, IInputContext var clickX = e.X - (float)screenBounds.Left - (float)Padding.Left + _scrollOffset; _cursorPosition = GetCharacterIndexAtX(clickX); - // Check for double-click (select word) + // Check for double-click (select word or select all) var now = DateTime.UtcNow; var timeSinceLastClick = (now - _lastClickTime).TotalMilliseconds; var distanceFromLastClick = Math.Abs(e.X - _lastClickX); if (timeSinceLastClick < DoubleClickThresholdMs && distanceFromLastClick < 10) { - // Double-click: select the word at cursor - SelectWordAtCursor(); + // Double-click: select all or select word based on property + if (SelectAllOnDoubleClick) + { + SelectAll(); + } + else + { + SelectWordAtCursor(); + } _lastClickTime = DateTime.MinValue; // Reset to prevent triple-click issues _isSelecting = false; } diff --git a/Views/SkiaImageButton.cs b/Views/SkiaImageButton.cs index ccbf614..d1c7c71 100644 --- a/Views/SkiaImageButton.cs +++ b/Views/SkiaImageButton.cs @@ -805,12 +805,9 @@ public class SkiaImageButton : SkiaView } // Fill (3) and Start (0) both use y = bounds.Top - var result1 = new Rect(x, y, finalWidth, finalHeight); - Console.WriteLine($"[SkiaImageButton] ArrangeOverride output (aligned): Y={result1.Y}, Height={result1.Height}"); - return result1; + return new Rect(x, y, finalWidth, finalHeight); } - Console.WriteLine($"[SkiaImageButton] ArrangeOverride output (unchanged): Y={bounds.Y}, Height={bounds.Height}"); return bounds; } diff --git a/Views/SkiaLayoutView.cs b/Views/SkiaLayoutView.cs index 78b4e28..2b1ce69 100644 --- a/Views/SkiaLayoutView.cs +++ b/Views/SkiaLayoutView.cs @@ -248,13 +248,7 @@ public abstract class SkiaLayoutView : SkiaView public override SkiaView? HitTest(float x, float 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; - } // Hit test children in reverse order (top-most first) for (int i = _children.Count - 1; i >= 0; i--) @@ -262,19 +256,9 @@ public abstract class SkiaLayoutView : SkiaView var child = _children[i]; var hit = child.HitTest(x, y); 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; - } } - if (this is SkiaBorder) - { - Console.WriteLine($"[SkiaBorder.HitTest] Hit self - x={x}, y={y}, Bounds={Bounds}, children={_children.Count}"); - } return this; } diff --git a/Views/SkiaProgressBar.cs b/Views/SkiaProgressBar.cs index 850cd07..b3ce4d4 100644 --- a/Views/SkiaProgressBar.cs +++ b/Views/SkiaProgressBar.cs @@ -187,9 +187,22 @@ public class SkiaProgressBar : SkiaView var barHeight = (float)BarHeight; var cornerRadius = (float)CornerRadius; - float midY = bounds.MidY; - float trackTop = midY - barHeight / 2f; - float trackBottom = midY + barHeight / 2f; + // 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; + trackTop = midY - barHeight / 2f; + trackBottom = midY + barHeight / 2f; + } // Get colors var trackColorSK = ToSKColor(TrackColor); @@ -246,8 +259,11 @@ public class SkiaProgressBar : SkiaView protected override Size MeasureOverride(Size availableSize) { - var barHeight = BarHeight; - return new Size(200, barHeight + 8); + // Respect HeightRequest if set, otherwise use BarHeight with padding + 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 diff --git a/Views/SkiaView.cs b/Views/SkiaView.cs index ee1079c..7461fad 100644 --- a/Views/SkiaView.cs +++ b/Views/SkiaView.cs @@ -6,6 +6,7 @@ using Microsoft.Maui.Controls.Shapes; using Microsoft.Maui.Graphics; using Microsoft.Maui.Platform.Linux; using Microsoft.Maui.Platform.Linux.Handlers; +using Microsoft.Maui.Platform.Linux.Native; using Microsoft.Maui.Platform.Linux.Rendering; using Microsoft.Maui.Platform.Linux.Services; using Microsoft.Maui.Platform.Linux.Window; @@ -1270,8 +1271,27 @@ public abstract class SkiaView : BindableObject, IDisposable, IAccessible /// /// Requests that this view be redrawn. + /// Thread-safe - will marshal to GTK thread if needed. /// 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); Invalidated?.Invoke(this, EventArgs.Empty); @@ -1286,7 +1306,7 @@ public abstract class SkiaView : BindableObject, IDisposable, IAccessible if (_parent != null) { - _parent.Invalidate(); + _parent.InvalidateInternal(); } else {