From 0c835f86e566d6f5925f31fcb26c5cce2041ce08 Mon Sep 17 00:00:00 2001 From: webwarrior-ws Date: Mon, 3 Apr 2023 14:53:23 +0200 Subject: [PATCH] Core,Controls(Gtk): implement tap/click gestures (#14) Implement tap/click gestures for Gtk. Note that due to the way Gtk works only certain widgets can respond to click events. I changed ContentView to inherit from EventBox, so many elements have this capability, including all layouts. Still many widgets such as label for which adding tap gesture recognizer will have no effect. See https://discourse.gnome.org/t/gtk-widget-mouse-down-event-does-not-seem-to-work/1699. --- .../GestureManager/GestureManager.Gtk.cs | 251 ++++++++++++++++++ src/Core/src/Platform/Gtk/ContentView.cs | 6 +- 2 files changed, 254 insertions(+), 3 deletions(-) diff --git a/src/Controls/src/Core/Platform/GestureManager/GestureManager.Gtk.cs b/src/Controls/src/Core/Platform/GestureManager/GestureManager.Gtk.cs index 6275ef1b866d..b1220230187c 100644 --- a/src/Controls/src/Core/Platform/GestureManager/GestureManager.Gtk.cs +++ b/src/Controls/src/Core/Platform/GestureManager/GestureManager.Gtk.cs @@ -1,17 +1,268 @@ #nullable enable using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.Linq; +using Gtk; +using Microsoft.Maui.Controls.Internals; namespace Microsoft.Maui.Controls.Platform { + // ported from https://github.com/xamarin/Xamarin.Forms/blob/5.0.0/Xamarin.Forms.Platform.GTK/VisualElementTracker.cs class GestureManager : IDisposable { + readonly IPlatformViewHandler _handler; + readonly NotifyCollectionChangedEventHandler _collectionChangedHandler; + Widget? _container; + Widget? _control; + VisualElement? _element; + + bool _isDisposed; + public GestureManager(IViewHandler handler) { + _handler = (IPlatformViewHandler)handler; + _collectionChangedHandler = ModelGestureRecognizersOnCollectionChanged; + + if (_handler.VirtualView == null) + throw new ArgumentNullException(nameof(handler.VirtualView)); + + if (_handler.PlatformView == null) + throw new ArgumentNullException(nameof(handler.PlatformView)); + + Element = (VisualElement)_handler.VirtualView; + Control = _handler.PlatformView; + + if (_handler.ContainerView != null) + Container = _handler.ContainerView; + else + Container = _handler.PlatformView; + } + + public Widget? Container + { + get { return _container; } + set + { + if (_container == value) + return; + + if (_container != null) + { + _container.ButtonPressEvent -= OnContainerButtonPressEvent; + } + + _container = value; + + UpdatingGestureRecognizers(); + } + } + + public Widget? Control + { + get { return _control; } + set + { + if (_control == value) + return; + + if (_control != null) + { + _control.ButtonPressEvent -= OnControlButtonPressEvent; + } + + _control = value; + + if (PreventGestureBubbling) + { + UpdatingGestureRecognizers(); + } + } + } + + public VisualElement? Element + { + get { return _element; } + set + { + if (_element == value) + return; + + if (_element != null) + { + var view = _element as View; + if (view != null) + { + var oldRecognizers = (ObservableCollection)view.GestureRecognizers; + oldRecognizers.CollectionChanged -= _collectionChangedHandler; + } + } + + _element = value; + + if (_element != null) + { + var view = _element as View; + if (view != null) + { + var newRecognizers = (ObservableCollection)view.GestureRecognizers; + newRecognizers.CollectionChanged += _collectionChangedHandler; + } + } + } + } + + private bool PreventGestureBubbling + { + get + { + return Element switch + { + Button => true, + CheckBox => true, + DatePicker => true, + Stepper => true, + Slider => true, + Switch => true, + TimePicker => true, + ImageButton => true, + RadioButton => true, + _ => false, + }; + } } public void Dispose() { + Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (_isDisposed) + return; + + _isDisposed = true; + + if (!disposing) + return; + + if (_container != null) + { + _container.ButtonPressEvent -= OnContainerButtonPressEvent; + } + + if (_element != null) + { + var view = _element as View; + if (view != null) + { + var oldRecognizers = (ObservableCollection)view.GestureRecognizers; + oldRecognizers.CollectionChanged -= _collectionChangedHandler; + } + } + + if (_control != null) + { + _control.ButtonPressEvent -= OnControlButtonPressEvent; + } + + Container?.Destroy(); + Container = null; + } + + private void ModelGestureRecognizersOnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs notifyCollectionChangedEventArgs) + { + UpdatingGestureRecognizers(); + } + + private void UpdatingGestureRecognizers() + { + var view = Element as View; + var gestures = view?.GestureRecognizers; + + if (_container == null || gestures == null) + return; + + _container.ButtonPressEvent -= OnContainerButtonPressEvent; + + if (gestures.GetGesturesFor().Any() || gestures.GetGesturesFor().Any()) + { + _container.ButtonPressEvent += OnContainerButtonPressEvent; + } + else + { + if (_control != null && PreventGestureBubbling) + { + _control.ButtonPressEvent += OnControlButtonPressEvent; + } + } } + + private void OnContainerButtonPressEvent(object sender, ButtonPressEventArgs args) + { + var button = args.Event.Button; + if (button != 1 && button != 3) + { + return; + } + + var view = Element as View; + + if (view == null) + return; + + int numClicks = 0; + switch (args.Event.Type) + { + case Gdk.EventType.ThreeButtonPress: + numClicks = 3; + break; + case Gdk.EventType.TwoButtonPress: + numClicks = 2; + break; + case Gdk.EventType.ButtonPress: + numClicks = 1; + break; + default: + return; + } + + // Taps or Clicks + if (button == (uint)ButtonsMask.Primary) + { + IEnumerable tapGestures = view.GestureRecognizers + .GetGesturesFor(recognizer => recognizer.NumberOfTapsRequired == numClicks); + + foreach (TapGestureRecognizer recognizer in tapGestures) + recognizer.SendTapped(view); + + IEnumerable clickGestures = view.GestureRecognizers + .GetGesturesFor(recognizer => recognizer.NumberOfClicksRequired == numClicks && + recognizer.Buttons == ButtonsMask.Primary); + + foreach (ClickGestureRecognizer recognizer in clickGestures) + recognizer.SendClicked(view, ButtonsMask.Primary); + } + else + { + // right click + IEnumerable rightClickGestures = view.GestureRecognizers + .GetGesturesFor(recognizer => recognizer.NumberOfClicksRequired == numClicks && + recognizer.Buttons == ButtonsMask.Secondary); + + foreach (ClickGestureRecognizer recognizer in rightClickGestures) + recognizer.SendClicked(view, ButtonsMask.Secondary); + } + } + + private void OnControlButtonPressEvent(object sender, ButtonPressEventArgs args) + { + args.RetVal = true; + } + } } diff --git a/src/Core/src/Platform/Gtk/ContentView.cs b/src/Core/src/Platform/Gtk/ContentView.cs index 1af8361a3309..a5a998f62d4f 100644 --- a/src/Core/src/Platform/Gtk/ContentView.cs +++ b/src/Core/src/Platform/Gtk/ContentView.cs @@ -5,10 +5,10 @@ namespace Microsoft.Maui.Platform { - public class ContentView : Gtk.Box + public class ContentView : Gtk.EventBox { - public ContentView() : base(Orientation.Horizontal, 0) { } + public ContentView() : base() { } internal Func? CrossPlatformMeasure { get; set; } @@ -28,7 +28,7 @@ public Widget? Content } else if (value != null) { - PackStart(value, true, true, 0); + Add(value); } _content = value;