diff --git a/android/local.properties b/android/local.properties index 52acb9e..d0145e8 100644 --- a/android/local.properties +++ b/android/local.properties @@ -1,2 +1,2 @@ sdk.dir=/Users/bensonarafat/Library/Android/sdk -flutter.sdk=/Users/bensonarafat/fvm/versions/3.39.0-0.1.pre \ No newline at end of file +flutter.sdk=/Users/bensonarafat/fvm/versions/3.39.0-0.1.pre diff --git a/example/.metadata b/example/.metadata index 68ebd18..b95fa4d 100644 --- a/example/.metadata +++ b/example/.metadata @@ -1,10 +1,30 @@ -# This file tracks properties of this Flutter project. -# Used by Flutter tool to assess capabilities and perform upgrades etc. -# -# This file should be version controlled and should not be manually edited. - -version: - revision: adc9dde3ba8563eebb824feb689f95eb947ab745 - channel: master - -project_type: app +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "adc901062556672b4138e18a4dc62a4be8f4b3c2" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 + base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 + - platform: web + create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 + base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/example/analysis_options.yaml @@ -0,0 +1 @@ + diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index b49fc3a..6f5ae27 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -1,8 +1,9 @@ plugins { id "com.android.application" - id "kotlin-android" + id "org.jetbrains.kotlin.android" id "dev.flutter.flutter-gradle-plugin" } + def localProperties = new Properties() def localPropertiesFile = rootProject.file('local.properties') if (localPropertiesFile.exists()) { @@ -10,6 +11,7 @@ if (localPropertiesFile.exists()) { localProperties.load(reader) } } + def flutterVersionCode = localProperties.getProperty('flutter.versionCode') if (flutterVersionCode == null) { flutterVersionCode = '1' @@ -20,12 +22,9 @@ if (flutterVersionName == null) { flutterVersionName = '1.0' } -// apply plugin: 'com.android.application' -// apply plugin: 'kotlin-android' -// apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" - android { - compileSdkVersion flutter.compileSdkVersion + compileSdkVersion 34 + namespace "com.example.super_tooltip_example" sourceSets { main.java.srcDirs += 'src/main/kotlin' @@ -39,7 +38,7 @@ android { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "com.example.super_tooltip_example" minSdkVersion flutter.minSdkVersion - targetSdkVersion 28 + targetSdkVersion 34 versionCode flutterVersionCode.toInteger() versionName flutterVersionName } @@ -51,12 +50,17 @@ android { signingConfig signingConfigs.debug } } + + kotlinOptions { + jvmTarget = "1.8" + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } } flutter { source '../..' -} - -dependencies { - // implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" } \ No newline at end of file diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index d8447f2..93d126e 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -15,7 +15,9 @@ android:theme="@style/LaunchTheme" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:hardwareAccelerated="true" - android:windowSoftInputMode="adjustResize"> + android:exported="true" + android:windowSoftInputMode="adjustResize" + > + + + + + + + + + + + + + + + + super_tooltip_example + + + + + + diff --git a/example/web/manifest.json b/example/web/manifest.json new file mode 100644 index 0000000..7a59db4 --- /dev/null +++ b/example/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "super_tooltip_example", + "short_name": "super_tooltip_example", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/lib/src/super_tooltip.dart b/lib/src/super_tooltip.dart index 0f7bc9b..477d84d 100644 --- a/lib/src/super_tooltip.dart +++ b/lib/src/super_tooltip.dart @@ -2,6 +2,7 @@ import 'dart:ui'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:super_tooltip/src/utils.dart'; @@ -325,6 +326,25 @@ class SuperTooltip extends StatefulWidget { /// Defaults to `false`. final bool clickThrough; + /// Whether to automatically show the tooltip when the mouse pointer hovers over the [child]. + /// + /// This feature utilizes [MouseRegion] and is primarily intended for Web and Desktop platforms. + /// On touch-based mobile devices, this parameter is generally ignored unless a mouse is connected. + /// + /// Defaults to `false`. + final bool showOnHover; + + /// Whether to automatically hide the tooltip when the mouse pointer leaves the [child]'s bounds. + /// + /// This is primarily intended for Web and Desktop platforms. + /// + /// **Note:** On Web/Desktop, enabling this will automatically disable the modal barrier + /// (regardless of [showBarrier]) to ensure the mouse can exit the widget area without + /// being blocked by the overlay. + /// + /// Defaults to `false`. + final bool hideOnHoverExit; + SuperTooltip({ Key? key, required this.content, @@ -399,6 +419,8 @@ class SuperTooltip extends StatefulWidget { this.showOnTap = true, this.boxShadows, this.clickThrough = false, + this.showOnHover = false, + this.hideOnHoverExit = false, }) : assert(showDropBoxFilter ? showBarrier ?? false : true, 'showDropBoxFilter or showBarrier can\'t be false | null'), super(key: key); @@ -486,7 +508,6 @@ class _SuperTooltipState extends State closeButtonType = widget.closeButtonType; closeButtonColor = widget.closeButtonColor ?? Colors.black; closeButtonSize = widget.closeButtonSize ?? 30.0; - showBarrier = widget.showBarrier ?? true; barrierColor = widget.barrierColor ?? Colors.black54; hasShadow = widget.hasShadow ?? true; shadowColor = widget.shadowColor ?? Colors.black54; @@ -495,20 +516,55 @@ class _SuperTooltipState extends State shadowOffset = widget.shadowOffset ?? Offset.zero; showBlur = widget.showDropBoxFilter; - return CompositedTransformTarget( - link: _layerLink, - child: GestureDetector( - onTap: () { - if (widget.toggleOnTap && _superTooltipController!.isVisible) { + /// On native mobile platforms, this parameter is ignored as hover events are not supported. + /// The widget reverts to standard barrier behavior (tap-to-dismiss) to prevent the + /// tooltip from becoming unresponsive. + var isNativeMobile = !kIsWeb && + (defaultTargetPlatform == TargetPlatform.iOS || + defaultTargetPlatform == TargetPlatform.android); + + if (isNativeMobile) { + /// On native mobile platforms, this parameter is ignored as hover events are not supported. + /// The widget reverts to standard barrier behavior (tap-to-dismiss) to prevent the + /// tooltip from becoming unresponsive. + showBarrier = widget.showBarrier ?? true; + } else { + /// On Web and Desktop, if [hideOnHoverExit] is true, the barrier is + /// automatically disabled regardless of this value. This ensures that + /// the barrier does not obstruct the mouse cursor from triggering the exit event. + showBarrier = widget.hideOnHoverExit ? false : widget.showBarrier ?? true; + } + return MouseRegion( + hitTestBehavior: HitTestBehavior.translucent, + onEnter: (_) { + if (widget.showOnHover) { + if (!_superTooltipController!.isVisible) { + _superTooltipController!.showTooltip(); + } + } + }, + onExit: (_) { + if (widget.hideOnHoverExit) { + if (_superTooltipController!.isVisible) { _superTooltipController!.hideTooltip(); - } else { - if (widget.showOnTap) { - _superTooltipController!.showTooltip(); - } } - }, - onLongPress: widget.onLongPress, - child: widget.child, + } + }, + child: CompositedTransformTarget( + link: _layerLink, + child: GestureDetector( + onTap: () { + if (widget.toggleOnTap && _superTooltipController!.isVisible) { + _superTooltipController!.hideTooltip(); + } else { + if (widget.showOnTap) { + _superTooltipController!.showTooltip(); + } + } + }, + onLongPress: widget.onLongPress, + child: widget.child, + ), ), ); } diff --git a/lib/src/tooltip_position_delegate.dart b/lib/src/tooltip_position_delegate.dart index 5987a69..2770d5c 100644 --- a/lib/src/tooltip_position_delegate.dart +++ b/lib/src/tooltip_position_delegate.dart @@ -34,15 +34,15 @@ class ToolTipPositionDelegate extends SingleChildLayoutDelegate { @override BoxConstraints getConstraintsForChild(BoxConstraints constraints) { - // TD: when margin is EdgeInsets, look into - // constraints.deflate(margin); - var newConstraints = this.constraints; + // We use the INCOMING constraints (screen size) to calculate available space. + // We do NOT start with this.constraints (user prefs) because that leads to negative math. + var availableConstraints = constraints; switch (preferredDirection) { case TooltipDirection.up: case TooltipDirection.down: - newConstraints = SuperUtils.verticalConstraints( - constraints: newConstraints, + availableConstraints = SuperUtils.verticalConstraints( + constraints: availableConstraints, margin: margin, bottom: bottom, isUp: preferredDirection == TooltipDirection.up, @@ -54,8 +54,8 @@ class ToolTipPositionDelegate extends SingleChildLayoutDelegate { break; case TooltipDirection.right: case TooltipDirection.left: - newConstraints = SuperUtils.horizontalConstraints( - constraints: newConstraints, + availableConstraints = SuperUtils.horizontalConstraints( + constraints: availableConstraints, margin: margin, bottom: bottom, isRight: preferredDirection == TooltipDirection.right, @@ -67,21 +67,24 @@ class ToolTipPositionDelegate extends SingleChildLayoutDelegate { break; } - // TD: This scenerio should likely be avoided in the initial functions - // Ensure constraints are valid - no negiative values - final validatedConstraints = newConstraints.copyWith( - minHeight: math.max( - 0, - newConstraints.minHeight > newConstraints.maxHeight - ? newConstraints.maxHeight - : newConstraints.minHeight), - minWidth: math.max( - 0, - newConstraints.minWidth > newConstraints.maxWidth - ? newConstraints.maxWidth - : newConstraints.minWidth), - maxHeight: math.max(0, newConstraints.maxHeight), - maxWidth: math.max(0, newConstraints.maxWidth), + // Now we merge the calculated "Available Space" with the User's "Desired Constraints". + // We take the smaller of the two max widths/heights to ensure we fit in both. + // We respect the user's min sizes unless they exceed available space. + + double finalMaxWidth = + math.min(availableConstraints.maxWidth, this.constraints.maxWidth); + double finalMaxHeight = + math.min(availableConstraints.maxHeight, this.constraints.maxHeight); + + // Ensure final max is not negative + finalMaxWidth = math.max(0.0, finalMaxWidth); + finalMaxHeight = math.max(0.0, finalMaxHeight); + + final validatedConstraints = BoxConstraints( + minWidth: math.min(this.constraints.minWidth, finalMaxWidth), + maxWidth: finalMaxWidth, + minHeight: math.min(this.constraints.minHeight, finalMaxHeight), + maxHeight: finalMaxHeight, ); return validatedConstraints; @@ -89,18 +92,6 @@ class ToolTipPositionDelegate extends SingleChildLayoutDelegate { @override Offset getPositionForChild(Size size, Size childSize) { - // TD: If there isn't enough space for the child on the preferredDirection - // use the opposite dirrection - // - // See: - // return positionDependentBox( - // size: size, - // childSize: childSize, - // target: target, - // verticalOffset: verticalOffset, - // preferBelow: preferBelow, - // ); - switch (preferredDirection) { case TooltipDirection.up: case TooltipDirection.down: diff --git a/lib/src/utils.dart b/lib/src/utils.dart index 5b43890..aae2dee 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -153,18 +153,25 @@ class SuperUtils { if (isRight) { if (right != null) { - minWidth = maxWidth = maxWidth - right - target.dx; + // Calculate available width based on margin + maxWidth = maxWidth - right - target.dx; + // Don't force minWidth to be equal to maxWidth (allows shrinking) } else { - maxWidth = min(maxWidth, maxWidth - target.dx) - margin; + maxWidth = (maxWidth - target.dx) - margin; } } else { if (left != null) { - minWidth = maxWidth = target.dx - left; + // Calculate available width based on margin + maxWidth = target.dx - left; } else { maxWidth = min(maxWidth, target.dx) - margin; } } + // Robustness check to prevent negative constraints + if (maxWidth < 0) maxWidth = 0; + if (minWidth > maxWidth) minWidth = maxWidth; + return constraints.copyWith( maxHeight: maxHeight, minWidth: minWidth, @@ -190,7 +197,6 @@ class SuperUtils { maxWidth = maxWidth - (left + right); } else if ((left != null && right == null) || (left == null && right != null)) { - // make sure that the sum of left, right + maxwidth isn't bigger than the screen width. final sideDelta = (left ?? 0.0) + (right ?? 0.0) + margin; if (maxWidth > maxWidth - sideDelta) { @@ -204,20 +210,22 @@ class SuperUtils { if (isUp) { if (top != null) { - minHeight = maxHeight = target.dy - top; + maxHeight = target.dy - top; } else { maxHeight = min(maxHeight, target.dy) - margin; - // TD: clamp minheight } } else { if (bottom != null) { - minHeight = maxHeight = maxHeight - bottom - target.dy; + maxHeight = maxHeight - bottom - target.dy; } else { maxHeight = min(maxHeight, maxHeight - target.dy) - margin; - // TD: clamp minheight } } + // Robustness check + if (maxHeight < 0) maxHeight = 0; + if (minHeight > maxHeight) minHeight = maxHeight; + return constraints.copyWith( minHeight: minHeight, maxHeight: maxHeight,