Skip to content

Commit

Permalink
Core,Controls(Gtk): implement tap/click gestures (lytico#14)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
webwarrior-ws authored and aarani committed Apr 24, 2023
1 parent bb36698 commit 0c835f8
Show file tree
Hide file tree
Showing 2 changed files with 254 additions and 3 deletions.
251 changes: 251 additions & 0 deletions src/Controls/src/Core/Platform/GestureManager/GestureManager.Gtk.cs
Original file line number Diff line number Diff line change
@@ -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<IGestureRecognizer>)view.GestureRecognizers;
oldRecognizers.CollectionChanged -= _collectionChangedHandler;
}
}

_element = value;

if (_element != null)
{
var view = _element as View;
if (view != null)
{
var newRecognizers = (ObservableCollection<IGestureRecognizer>)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<IGestureRecognizer>)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<TapGestureRecognizer>().Any() || gestures.GetGesturesFor<ClickGestureRecognizer>().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<TapGestureRecognizer> tapGestures = view.GestureRecognizers
.GetGesturesFor<TapGestureRecognizer>(recognizer => recognizer.NumberOfTapsRequired == numClicks);

foreach (TapGestureRecognizer recognizer in tapGestures)
recognizer.SendTapped(view);

IEnumerable<ClickGestureRecognizer> clickGestures = view.GestureRecognizers
.GetGesturesFor<ClickGestureRecognizer>(recognizer => recognizer.NumberOfClicksRequired == numClicks &&
recognizer.Buttons == ButtonsMask.Primary);

foreach (ClickGestureRecognizer recognizer in clickGestures)
recognizer.SendClicked(view, ButtonsMask.Primary);
}
else
{
// right click
IEnumerable<ClickGestureRecognizer> rightClickGestures = view.GestureRecognizers
.GetGesturesFor<ClickGestureRecognizer>(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;
}

}
}
6 changes: 3 additions & 3 deletions src/Core/src/Platform/Gtk/ContentView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<double, double, Size>? CrossPlatformMeasure { get; set; }

Expand All @@ -28,7 +28,7 @@ public Widget? Content
}
else if (value != null)
{
PackStart(value, true, true, 0);
Add(value);
}

_content = value;
Expand Down

0 comments on commit 0c835f8

Please sign in to comment.