├── .gitignore ├── .metadata ├── CHANGELOG.md ├── LICENSE ├── README.md ├── example ├── .gitignore ├── .metadata ├── README.md ├── lib │ └── main.dart ├── pubspec.lock └── pubspec.yaml ├── lib ├── anchored_popup_region.dart └── anchored_popups.dart ├── pubspec.lock └── pubspec.yaml /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | .dart_tool/ 26 | .flutter-plugins 27 | .flutter-plugins-dependencies 28 | .packages 29 | .pub-cache/ 30 | .pub/ 31 | build/ 32 | 33 | # Android related 34 | **/android/**/gradle-wrapper.jar 35 | **/android/.gradle 36 | **/android/captures/ 37 | **/android/gradlew 38 | **/android/gradlew.bat 39 | **/android/local.properties 40 | **/android/**/GeneratedPluginRegistrant.java 41 | 42 | # iOS/XCode related 43 | **/ios/**/*.mode1v3 44 | **/ios/**/*.mode2v3 45 | **/ios/**/*.moved-aside 46 | **/ios/**/*.pbxuser 47 | **/ios/**/*.perspectivev3 48 | **/ios/**/*sync/ 49 | **/ios/**/.sconsign.dblite 50 | **/ios/**/.tags* 51 | **/ios/**/.vagrant/ 52 | **/ios/**/DerivedData/ 53 | **/ios/**/Icon? 54 | **/ios/**/Pods/ 55 | **/ios/**/.symlinks/ 56 | **/ios/**/profile 57 | **/ios/**/xcuserdata 58 | **/ios/.generated/ 59 | **/ios/Flutter/App.framework 60 | **/ios/Flutter/Flutter.framework 61 | **/ios/Flutter/Flutter.podspec 62 | **/ios/Flutter/Generated.xcconfig 63 | **/ios/Flutter/ephemeral 64 | **/ios/Flutter/app.flx 65 | **/ios/Flutter/app.zip 66 | **/ios/Flutter/flutter_assets/ 67 | **/ios/Flutter/flutter_export_environment.sh 68 | **/ios/ServiceDefinitions.json 69 | **/ios/Runner/GeneratedPluginRegistrant.* 70 | 71 | # Exceptions to above rules. 72 | !**/ios/**/default.mode1v3 73 | !**/ios/**/default.mode2v3 74 | !**/ios/**/default.pbxuser 75 | !**/ios/**/default.perspectivev3 76 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 8264cb3e8a797eef39cbcd32bb56fd07790efb7f 8 | channel: dev 9 | 10 | project_type: package 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.0.1 2 | 3 | * TODO: Describe initial release. 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 gskinner.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # anchored_popups 2 | A package to show context menus on right-click or long-press. 3 | 4 | [ Add Video ] 5 | 6 | ## 🔨 Installation 7 | ```yaml 8 | dependencies: 9 | anchored_popups: ^0.1.0 10 | ``` 11 | 12 | ### ⚙ Import 13 | 14 | ```dart 15 | import 'package:anchored_popups/anchored_popups.dart'; 16 | ``` 17 | 18 | ## 🕹️ Usage 19 | 20 | [ TODO ] 21 | 22 | ## 🐞 Bugs/Requests 23 | 24 | If you encounter any problems please open an issue. If you feel the library is missing a feature, please raise a ticket on Github and we'll look into it. Pull request are welcome. 25 | 26 | ## 📃 License 27 | 28 | MIT License 29 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | **/ios/Flutter/.last_build_id 26 | .dart_tool/ 27 | .flutter-plugins 28 | .flutter-plugins-dependencies 29 | .packages 30 | .pub-cache/ 31 | .pub/ 32 | /build/ 33 | /windows/ 34 | 35 | # Web related 36 | lib/generated_plugin_registrant.dart 37 | 38 | # Symbolication related 39 | app.*.symbols 40 | 41 | # Obfuscation related 42 | app.*.map.json 43 | 44 | # Android Studio will place build artifacts here 45 | /android/app/debug 46 | /android/app/profile 47 | /android/app/release 48 | -------------------------------------------------------------------------------- /example/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 8264cb3e8a797eef39cbcd32bb56fd07790efb7f 8 | channel: dev 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # flutter_app 2 | 3 | A new Flutter application. 4 | 5 | ## Getting Started 6 | 7 | This project is a starting point for a Flutter application. 8 | 9 | A few resources to get you started if this is your first Flutter project: 10 | 11 | - [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) 12 | - [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) 13 | 14 | For help getting started with Flutter, view our 15 | [online documentation](https://flutter.dev/docs), which offers tutorials, 16 | samples, guidance on mobile development, and a full API reference. 17 | -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:anchored_popups/anchored_popup_region.dart'; 2 | import 'package:anchored_popups/anchored_popups.dart'; 3 | import 'package:flextras/flextras.dart'; 4 | import 'package:flutter/material.dart'; 5 | 6 | void main() { 7 | runApp(MyApp()); 8 | } 9 | 10 | class MyApp extends StatelessWidget { 11 | // This widget is the root of your application. 12 | @override 13 | Widget build(BuildContext context) { 14 | return AnchoredPopups( 15 | child: MaterialApp( 16 | home: PopupTests(), 17 | ), 18 | ); 19 | } 20 | } 21 | 22 | class PopupTests extends StatelessWidget { 23 | @override 24 | Widget build(BuildContext context) { 25 | return Scaffold( 26 | body: Stack( 27 | children: [ 28 | Positioned( 29 | bottom: 0, 30 | right: 0, 31 | child: AnchoredPopUpRegion( 32 | anchor: Alignment.centerLeft, 33 | popAnchor: Alignment.centerRight, 34 | popChild: Card(child: Text("centerLeft to centerRight")), 35 | child: Container(width: 50, height: 50, child: Placeholder())), 36 | ), 37 | Center( 38 | child: SeparatedColumn( 39 | mainAxisAlignment: MainAxisAlignment.center, 40 | separatorBuilder: () => SizedBox(height: 10), 41 | children: [ 42 | Text("Hover over the boxes to show different behaviors."), 43 | AnchoredPopUpRegion( 44 | anchor: Alignment.bottomLeft, 45 | popAnchor: Alignment.bottomRight, 46 | popChild: Card(child: Text("BottomLeft to BottomRight")), 47 | child: Container(width: 50, height: 50, child: Placeholder()), 48 | ), 49 | 50 | AnchoredPopUpRegion( 51 | anchor: Alignment.center, 52 | popAnchor: Alignment.center, 53 | popChild: Card(child: Text("Center to Center")), 54 | child: Container(width: 50, height: 50, child: Placeholder())), 55 | 56 | /// Example of a card that, when clicked shows a form. 57 | AnchoredPopUpRegion( 58 | mode: PopUpMode.clickToToggle, 59 | anchor: Alignment.centerRight, 60 | popAnchor: Alignment.centerLeft, 61 | // This is what is shown when the popup is opened: 62 | popChild: Card( 63 | child: Column( 64 | mainAxisSize: MainAxisSize.min, 65 | children: [ 66 | Text("CenterRight to CenterLeft"), 67 | TextButton( 68 | onPressed: () { 69 | AnchoredPopups.of(context).hide(); 70 | }, 71 | child: Text("Close Popup")) 72 | ], 73 | )), 74 | // The region that will activate the popup 75 | child: Container( 76 | child: Card( 77 | child: Padding( 78 | padding: EdgeInsets.all(16), 79 | child: Text("Click me for more info..."), 80 | ), 81 | ), 82 | ), 83 | ), 84 | ], 85 | ), 86 | ) 87 | ], 88 | ), 89 | ); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /example/pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | anchored_popups: 5 | dependency: "direct main" 6 | description: 7 | path: ".." 8 | relative: true 9 | source: path 10 | version: "0.0.1" 11 | async: 12 | dependency: transitive 13 | description: 14 | name: async 15 | url: "https://pub.dartlang.org" 16 | source: hosted 17 | version: "2.5.0" 18 | boolean_selector: 19 | dependency: transitive 20 | description: 21 | name: boolean_selector 22 | url: "https://pub.dartlang.org" 23 | source: hosted 24 | version: "2.1.0" 25 | characters: 26 | dependency: transitive 27 | description: 28 | name: characters 29 | url: "https://pub.dartlang.org" 30 | source: hosted 31 | version: "1.1.0" 32 | charcode: 33 | dependency: transitive 34 | description: 35 | name: charcode 36 | url: "https://pub.dartlang.org" 37 | source: hosted 38 | version: "1.2.0" 39 | clock: 40 | dependency: transitive 41 | description: 42 | name: clock 43 | url: "https://pub.dartlang.org" 44 | source: hosted 45 | version: "1.1.0" 46 | collection: 47 | dependency: transitive 48 | description: 49 | name: collection 50 | url: "https://pub.dartlang.org" 51 | source: hosted 52 | version: "1.15.0" 53 | cupertino_icons: 54 | dependency: "direct main" 55 | description: 56 | name: cupertino_icons 57 | url: "https://pub.dartlang.org" 58 | source: hosted 59 | version: "1.0.3" 60 | fake_async: 61 | dependency: transitive 62 | description: 63 | name: fake_async 64 | url: "https://pub.dartlang.org" 65 | source: hosted 66 | version: "1.2.0" 67 | flextras: 68 | dependency: "direct main" 69 | description: 70 | name: flextras 71 | url: "https://pub.dartlang.org" 72 | source: hosted 73 | version: "0.0.1+5" 74 | flutter: 75 | dependency: "direct main" 76 | description: flutter 77 | source: sdk 78 | version: "0.0.0" 79 | flutter_test: 80 | dependency: "direct dev" 81 | description: flutter 82 | source: sdk 83 | version: "0.0.0" 84 | matcher: 85 | dependency: transitive 86 | description: 87 | name: matcher 88 | url: "https://pub.dartlang.org" 89 | source: hosted 90 | version: "0.12.10" 91 | meta: 92 | dependency: transitive 93 | description: 94 | name: meta 95 | url: "https://pub.dartlang.org" 96 | source: hosted 97 | version: "1.3.0" 98 | path: 99 | dependency: transitive 100 | description: 101 | name: path 102 | url: "https://pub.dartlang.org" 103 | source: hosted 104 | version: "1.8.0" 105 | sky_engine: 106 | dependency: transitive 107 | description: flutter 108 | source: sdk 109 | version: "0.0.99" 110 | source_span: 111 | dependency: transitive 112 | description: 113 | name: source_span 114 | url: "https://pub.dartlang.org" 115 | source: hosted 116 | version: "1.8.1" 117 | stack_trace: 118 | dependency: transitive 119 | description: 120 | name: stack_trace 121 | url: "https://pub.dartlang.org" 122 | source: hosted 123 | version: "1.10.0" 124 | stream_channel: 125 | dependency: transitive 126 | description: 127 | name: stream_channel 128 | url: "https://pub.dartlang.org" 129 | source: hosted 130 | version: "2.1.0" 131 | string_scanner: 132 | dependency: transitive 133 | description: 134 | name: string_scanner 135 | url: "https://pub.dartlang.org" 136 | source: hosted 137 | version: "1.1.0" 138 | term_glyph: 139 | dependency: transitive 140 | description: 141 | name: term_glyph 142 | url: "https://pub.dartlang.org" 143 | source: hosted 144 | version: "1.2.0" 145 | test_api: 146 | dependency: transitive 147 | description: 148 | name: test_api 149 | url: "https://pub.dartlang.org" 150 | source: hosted 151 | version: "0.2.19" 152 | typed_data: 153 | dependency: transitive 154 | description: 155 | name: typed_data 156 | url: "https://pub.dartlang.org" 157 | source: hosted 158 | version: "1.3.0" 159 | vector_math: 160 | dependency: transitive 161 | description: 162 | name: vector_math 163 | url: "https://pub.dartlang.org" 164 | source: hosted 165 | version: "2.1.0" 166 | sdks: 167 | dart: ">=2.12.0 <3.0.0" 168 | flutter: ">=1.17.0" 169 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: example 2 | description: A new Flutter application. 3 | 4 | # The following line prevents the package from being accidentally published to 5 | # pub.dev using `pub publish`. This is preferred for private packages. 6 | publish_to: 'none' # Remove this line if you wish to publish to pub.dev 7 | 8 | # The following defines the version and build number for your application. 9 | # A version number is three numbers separated by dots, like 1.2.43 10 | # followed by an optional build number separated by a +. 11 | # Both the version and the builder number may be overridden in flutter 12 | # build by specifying --build-name and --build-number, respectively. 13 | # In Android, build-name is used as versionName while build-number used as versionCode. 14 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 15 | # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. 16 | # Read more about iOS versioning at 17 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 18 | version: 1.0.0+1 19 | 20 | environment: 21 | sdk: ">=2.7.0 <3.0.0" 22 | 23 | dependencies: 24 | flutter: 25 | sdk: flutter 26 | 27 | anchored_popups: 28 | path: ../ 29 | cupertino_icons: ^1.0.2 30 | flextras: 31 | 32 | dev_dependencies: 33 | flutter_test: 34 | sdk: flutter 35 | 36 | # For information on the generic Dart part of this file, see the 37 | # following page: https://dart.dev/tools/pub/pubspec 38 | 39 | # The following section is specific to Flutter. 40 | flutter: 41 | 42 | # The following line ensures that the Material Icons font is 43 | # included with your application, so that you can use the icons in 44 | # the material Icons class. 45 | uses-material-design: true 46 | 47 | # To add assets to your application, add an assets section, like this: 48 | # assets: 49 | # - images/a_dot_burr.jpeg 50 | # - images/a_dot_ham.jpeg 51 | 52 | # An image asset can refer to one or more resolution-specific "variants", see 53 | # https://flutter.dev/assets-and-images/#resolution-aware. 54 | 55 | # For details regarding adding assets from package dependencies, see 56 | # https://flutter.dev/assets-and-images/#from-packages 57 | 58 | # To add custom fonts to your application, add a fonts section here, 59 | # in this "flutter" section. Each entry in this list should have a 60 | # "family" key with the font family name, and a "fonts" key with a 61 | # list giving the asset and other descriptors for the font. For 62 | # example: 63 | # fonts: 64 | # - family: Schyler 65 | # fonts: 66 | # - asset: fonts/Schyler-Regular.ttf 67 | # - asset: fonts/Schyler-Italic.ttf 68 | # style: italic 69 | # - family: Trajan Pro 70 | # fonts: 71 | # - asset: fonts/TrajanPro.ttf 72 | # - asset: fonts/TrajanPro_Bold.ttf 73 | # weight: 700 74 | # 75 | # For details regarding fonts from package dependencies, 76 | # see https://flutter.dev/custom-fonts/#from-packages 77 | -------------------------------------------------------------------------------- /lib/anchored_popup_region.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/material.dart'; 4 | 5 | import 'anchored_popups.dart'; 6 | 7 | enum PopUpMode { 8 | clickToToggle, // Click a region to open PopOver, click barrier to close. 9 | hover, // Open on hoverIn (slightly delayed), close on hoverOut 10 | } 11 | 12 | class AnchoredPopUpRegion extends StatefulWidget { 13 | AnchoredPopUpRegion( 14 | {Key? key, 15 | required this.child, 16 | required this.popChild, 17 | this.anchor, 18 | this.popAnchor, 19 | this.barrierDismissable, 20 | this.barrierColor, 21 | this.mode = PopUpMode.hover}) 22 | : super(key: key); 23 | final Widget child; 24 | final Widget popChild; 25 | final bool? barrierDismissable; 26 | final Color? barrierColor; 27 | final Alignment? anchor; 28 | final Alignment? popAnchor; 29 | final PopUpMode mode; 30 | @override 31 | AnchoredPopUpRegionState createState() => AnchoredPopUpRegionState(); 32 | 33 | // Non-interactive tool-tips, triggered on a delayed hover. Auto-close when you roll-out of the PopOverRegion 34 | static AnchoredPopUpRegion hover( 35 | {Key? key, required Widget child, required Widget popChild, Alignment? anchor, Alignment? popAnchor}) { 36 | return AnchoredPopUpRegion( 37 | key: key, child: child, popChild: popChild, anchor: anchor, popAnchor: popAnchor, mode: PopUpMode.hover); 38 | } 39 | 40 | // Click to open/close. Use for interactive panels, or other elements that should close themselves 41 | static AnchoredPopUpRegion click( 42 | {Key? key, 43 | required Widget child, 44 | required Widget popChild, 45 | Alignment? anchor, 46 | Alignment? popAnchor, 47 | bool? barrierDismissable, 48 | Color? barrierColor}) { 49 | return AnchoredPopUpRegion( 50 | key: key, 51 | child: child, 52 | popChild: popChild, 53 | anchor: anchor, 54 | popAnchor: popAnchor, 55 | mode: PopUpMode.clickToToggle, 56 | barrierColor: barrierColor, 57 | barrierDismissable: barrierDismissable, 58 | ); 59 | } 60 | 61 | static AnchoredPopUpRegion hoverWithClick({ 62 | Key? key, 63 | required Widget child, 64 | required Widget hoverPopChild, 65 | required Widget clickPopChild, 66 | bool barrierDismissable = true, 67 | Color? barrierColor, 68 | Alignment? hoverAnchor, 69 | Alignment? hoverPopAnchor, 70 | Alignment? clickAnchor, 71 | Alignment? clickPopAnchor, 72 | }) { 73 | return click( 74 | key: key, 75 | anchor: clickAnchor, 76 | barrierColor: barrierColor, 77 | barrierDismissable: barrierDismissable, 78 | popChild: clickPopChild, 79 | popAnchor: clickPopAnchor, 80 | child: hover(popAnchor: hoverPopAnchor, popChild: hoverPopChild, anchor: hoverAnchor, child: child)); 81 | } 82 | } 83 | 84 | // TODO: Support for some sort of dualMode widget would be nice, has both a tooltip and a panel. 85 | class AnchoredPopUpRegionState extends State { 86 | Timer? _timer; 87 | AnchoredPopupsController? _popups; 88 | 89 | @override 90 | Widget build(BuildContext context) { 91 | Widget content; 92 | // If Hover, add a MouseRegion 93 | if (widget.mode == PopUpMode.hover) { 94 | content = MouseRegion( 95 | opaque: true, 96 | onEnter: (_) => _handleHoverStart(), 97 | onExit: (_) => _handleHoverEnd(), 98 | child: widget.child, 99 | ); 100 | } else { 101 | //TODO: A button builder would be nice here. 102 | content = TextButton(onPressed: show, child: widget.child); 103 | } 104 | return content; 105 | } 106 | 107 | @override 108 | void dispose() { 109 | if (widget.mode == PopUpMode.hover) { 110 | _handleHoverEnd(); 111 | } 112 | super.dispose(); 113 | } 114 | 115 | void show() { 116 | if (mounted == false) { 117 | print("PopoverRegion: Exiting early not mounted anymore"); 118 | return; 119 | } 120 | _popups = AnchoredPopups.of(context); 121 | _popups?.show(context, 122 | popUpMode: widget.mode, 123 | popContent: widget.popChild, 124 | anchor: widget.anchor ?? Alignment.bottomCenter, 125 | popAnchor: widget.popAnchor ?? Alignment.topCenter, 126 | useBarrier: widget.mode != PopUpMode.hover, 127 | barrierColor: widget.barrierColor ?? Colors.transparent, 128 | dismissOnBarrierClick: widget.barrierDismissable ?? true); 129 | } 130 | 131 | void _handleHoverStart() { 132 | _timer?.cancel(); 133 | _timer = Timer.periodic(Duration(milliseconds: 400), (_) { 134 | show(); 135 | _timer?.cancel(); 136 | }); 137 | } 138 | 139 | void _handleHoverEnd() { 140 | _timer?.cancel(); 141 | bool isStillOpen = _popups?.currentPopup?.context == context; 142 | if (isStillOpen) _popups?.hide(); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /lib/anchored_popups.dart: -------------------------------------------------------------------------------- 1 | import 'package:anchored_popups/anchored_popup_region.dart'; 2 | import 'package:flutter/foundation.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter/rendering.dart'; 5 | 6 | /// ////////////////////////////////// 7 | /// POPOVER CONTEXT (ROOT) 8 | class AnchoredPopups extends StatefulWidget { 9 | const AnchoredPopups({Key? key, required this.child}) : super(key: key); 10 | final Widget child; 11 | 12 | @override 13 | AnchoredPopupsController createState() => AnchoredPopupsController(); 14 | 15 | static AnchoredPopupsController? of(BuildContext context) { 16 | final w = context.dependOnInheritedWidgetOfExactType<_InheritedPopupOverlay>(); 17 | if (w == null) print("[AnchoredPopups] WARNING: No AnchoredPopup was found."); 18 | return w == null ? null : w.state; 19 | } 20 | } 21 | 22 | class AnchoredPopupsController extends State { 23 | OverlayEntry? barrierOverlay; 24 | OverlayEntry? mainContentOverlay; 25 | ValueNotifier _sizeNotifier = ValueNotifier(Size.zero); 26 | PopupConfig? _currentPopupConfig; 27 | PopupConfig? get currentPopup => _currentPopupConfig; 28 | Size? _prevSize; 29 | @override 30 | Widget build(BuildContext context) { 31 | _closeHoverOnScreenSizeChange(); 32 | final config = _currentPopupConfig; 33 | // Get the size and position of the region that triggered this popup, 34 | // then calculate the global offset for the popup content 35 | Size anchorSize = Size.zero; 36 | Offset anchoredRegionPos = Offset.zero; 37 | Offset popUpFractionalOffset = Offset.zero; 38 | 39 | if (config != null) { 40 | RenderBox? rb = config.context.findRenderObject() as RenderBox?; 41 | if (rb != null) { 42 | anchorSize = rb.size; 43 | anchoredRegionPos = rb.localToGlobal(Offset( 44 | anchorSize.width / 2 + (config.anchor.x) * anchorSize.width / 2, 45 | anchorSize.height / 2 + (config.anchor.y) * anchorSize.height / 2, 46 | )); 47 | } 48 | // Work out the fractional offset for the popUp content based on the incoming popupAnchor. 49 | // For anchor of -1,-1 (top left), we want an offset of (0, 0), for anchor of 1, 1 (bottom right), we want an offset of (-1, -1) 50 | // Formula is: offset = .5 - align/2 - 1; 51 | popUpFractionalOffset = Offset(.5 - (config.popUpAnchor.x / 2) - 1, .5 - (config.popUpAnchor.y / 2) - 1); 52 | } 53 | return _InheritedPopupOverlay( 54 | state: this, 55 | child: Directionality( 56 | textDirection: TextDirection.ltr, 57 | child: Stack( 58 | alignment: Alignment.topLeft, 59 | children: [ 60 | widget.child, 61 | if (config != null) ...[ 62 | // Barrier 63 | if (config.useBarrier) ...[ 64 | GestureDetector( 65 | onTap: config.dismissOnBarrierClick ? () => hide() : null, 66 | child: Container(color: config.barrierColor)), 67 | ], 68 | // Pop child 69 | Transform.translate( 70 | offset: anchoredRegionPos, 71 | child: FractionalTranslation( 72 | translation: (((popUpFractionalOffset))), 73 | child: IgnorePointer( 74 | ignoring: config.popUpMode == PopUpMode.hover, 75 | child: config.popUpContent, 76 | ), 77 | ), 78 | ), 79 | ], 80 | ], 81 | ), 82 | )); 83 | } 84 | 85 | bool get isBarrierOpen => barrierOverlay != null; 86 | 87 | void hide() { 88 | print("Close current"); 89 | setState(() => _currentPopupConfig = null); 90 | _sizeNotifier.value = null; 91 | barrierOverlay?.remove(); 92 | mainContentOverlay?.remove(); 93 | barrierOverlay = mainContentOverlay = null; 94 | } 95 | 96 | void show( 97 | BuildContext context, { 98 | bool useBarrier = true, 99 | bool dismissOnBarrierClick = true, 100 | Color barrierColor = Colors.transparent, 101 | required PopUpMode popUpMode, 102 | required Alignment anchor, 103 | required Alignment popAnchor, 104 | required Widget popContent, 105 | }) { 106 | setState(() { 107 | _currentPopupConfig = PopupConfig(context, popUpMode, 108 | anchor: anchor, 109 | popUpAnchor: popAnchor, 110 | popUpContent: popContent, 111 | useBarrier: useBarrier, 112 | barrierColor: barrierColor, 113 | dismissOnBarrierClick: dismissOnBarrierClick); 114 | }); 115 | } 116 | 117 | void _closeHoverOnScreenSizeChange() { 118 | Size screenSize = MediaQueryData.fromWindow(WidgetsBinding.instance!.window).size; 119 | if (screenSize != _prevSize) { 120 | _currentPopupConfig = null; 121 | } 122 | _prevSize = screenSize; 123 | } 124 | } 125 | 126 | /// InheritedWidget boilerplate 127 | class _InheritedPopupOverlay extends InheritedWidget { 128 | _InheritedPopupOverlay({Key? key, required Widget child, required this.state}) : super(key: key, child: child); 129 | 130 | final AnchoredPopupsController state; 131 | 132 | @override 133 | bool updateShouldNotify(covariant InheritedWidget oldWidget) => true; 134 | } 135 | 136 | class PopupConfig { 137 | PopupConfig( 138 | this.context, 139 | this.popUpMode, { 140 | this.useBarrier = true, 141 | this.dismissOnBarrierClick = true, 142 | this.barrierColor = Colors.transparent, 143 | required this.anchor, 144 | required this.popUpAnchor, 145 | required this.popUpContent, 146 | }); 147 | 148 | final BuildContext context; 149 | final PopUpMode popUpMode; 150 | final bool useBarrier; 151 | final bool dismissOnBarrierClick; 152 | final Color barrierColor; 153 | final Alignment anchor; 154 | final Alignment popUpAnchor; 155 | final Widget popUpContent; 156 | } 157 | -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | async: 5 | dependency: transitive 6 | description: 7 | name: async 8 | url: "https://pub.dartlang.org" 9 | source: hosted 10 | version: "2.5.0" 11 | boolean_selector: 12 | dependency: transitive 13 | description: 14 | name: boolean_selector 15 | url: "https://pub.dartlang.org" 16 | source: hosted 17 | version: "2.1.0" 18 | characters: 19 | dependency: transitive 20 | description: 21 | name: characters 22 | url: "https://pub.dartlang.org" 23 | source: hosted 24 | version: "1.1.0" 25 | charcode: 26 | dependency: transitive 27 | description: 28 | name: charcode 29 | url: "https://pub.dartlang.org" 30 | source: hosted 31 | version: "1.2.0" 32 | clock: 33 | dependency: transitive 34 | description: 35 | name: clock 36 | url: "https://pub.dartlang.org" 37 | source: hosted 38 | version: "1.1.0" 39 | collection: 40 | dependency: transitive 41 | description: 42 | name: collection 43 | url: "https://pub.dartlang.org" 44 | source: hosted 45 | version: "1.15.0" 46 | fake_async: 47 | dependency: transitive 48 | description: 49 | name: fake_async 50 | url: "https://pub.dartlang.org" 51 | source: hosted 52 | version: "1.2.0" 53 | flutter: 54 | dependency: "direct main" 55 | description: flutter 56 | source: sdk 57 | version: "0.0.0" 58 | flutter_test: 59 | dependency: "direct dev" 60 | description: flutter 61 | source: sdk 62 | version: "0.0.0" 63 | matcher: 64 | dependency: transitive 65 | description: 66 | name: matcher 67 | url: "https://pub.dartlang.org" 68 | source: hosted 69 | version: "0.12.10" 70 | meta: 71 | dependency: transitive 72 | description: 73 | name: meta 74 | url: "https://pub.dartlang.org" 75 | source: hosted 76 | version: "1.3.0" 77 | path: 78 | dependency: transitive 79 | description: 80 | name: path 81 | url: "https://pub.dartlang.org" 82 | source: hosted 83 | version: "1.8.0" 84 | sky_engine: 85 | dependency: transitive 86 | description: flutter 87 | source: sdk 88 | version: "0.0.99" 89 | source_span: 90 | dependency: transitive 91 | description: 92 | name: source_span 93 | url: "https://pub.dartlang.org" 94 | source: hosted 95 | version: "1.8.1" 96 | stack_trace: 97 | dependency: transitive 98 | description: 99 | name: stack_trace 100 | url: "https://pub.dartlang.org" 101 | source: hosted 102 | version: "1.10.0" 103 | stream_channel: 104 | dependency: transitive 105 | description: 106 | name: stream_channel 107 | url: "https://pub.dartlang.org" 108 | source: hosted 109 | version: "2.1.0" 110 | string_scanner: 111 | dependency: transitive 112 | description: 113 | name: string_scanner 114 | url: "https://pub.dartlang.org" 115 | source: hosted 116 | version: "1.1.0" 117 | term_glyph: 118 | dependency: transitive 119 | description: 120 | name: term_glyph 121 | url: "https://pub.dartlang.org" 122 | source: hosted 123 | version: "1.2.0" 124 | test_api: 125 | dependency: transitive 126 | description: 127 | name: test_api 128 | url: "https://pub.dartlang.org" 129 | source: hosted 130 | version: "0.2.19" 131 | typed_data: 132 | dependency: transitive 133 | description: 134 | name: typed_data 135 | url: "https://pub.dartlang.org" 136 | source: hosted 137 | version: "1.3.0" 138 | vector_math: 139 | dependency: transitive 140 | description: 141 | name: vector_math 142 | url: "https://pub.dartlang.org" 143 | source: hosted 144 | version: "2.1.0" 145 | sdks: 146 | dart: ">=2.12.0 <3.0.0" 147 | flutter: ">=1.17.0" 148 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: anchored_popups 2 | description: Shows popup panels and custom tooltips that are anchored to a Widget in the tree. 3 | version: 0.0.1+2 4 | homepage: https://github.com/gskinnerTeam/flutter_anchored_popups 5 | 6 | environment: 7 | sdk: ">=2.12.0 <3.0.0" 8 | flutter: ">=1.17.0" 9 | 10 | dependencies: 11 | flutter: 12 | sdk: flutter 13 | 14 | dev_dependencies: 15 | flutter_test: 16 | sdk: flutter 17 | 18 | flutter: 19 | --------------------------------------------------------------------------------