[flutter]: config popup window

This commit is contained in:
appflowy
2021-10-19 22:16:37 +08:00
parent ce5cccd670
commit 7a214ba3f4
8 changed files with 179 additions and 99 deletions

View File

@ -36,11 +36,10 @@ class ApplicationWidget extends StatelessWidget {
// setWindowFrame(const Rect.fromLTWH(0, 0, launchWidth, launchWidth / ratio)); // setWindowFrame(const Rect.fromLTWH(0, 0, launchWidth, launchWidth / ratio));
final theme = AppTheme.fromType(ThemeType.light); final theme = AppTheme.fromType(ThemeType.light);
FlowyOverlayConfig config = FlowyOverlayConfig(barrierColor: Colors.transparent);
return Provider.value( return Provider.value(
value: theme, value: theme,
child: MaterialApp( child: MaterialApp(
builder: overlayManagerBuilder(config: config), builder: overlayManagerBuilder(),
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
theme: theme.themeData, theme: theme.themeData,
navigatorKey: AppGlobals.rootNavKey, navigatorKey: AppGlobals.rootNavKey,

View File

@ -0,0 +1,77 @@
import 'package:dartz/dartz.dart' as dartz;
import 'package:flowy_infra/theme.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:app_flowy/workspace/domain/view_edit.dart';
class ViewActionList implements FlowyOverlayDelegate {
final Function(dartz.Option<ViewAction>) onSelected;
final BuildContext anchorContext;
final String _identifier = 'ViewActionList';
const ViewActionList({required this.anchorContext, required this.onSelected});
void show(BuildContext buildContext) {
final items = ViewAction.values
.map((action) => ActionItem(
action: action,
onSelected: (action) {
FlowyOverlay.of(buildContext).remove(_identifier);
onSelected(dartz.some(action));
}))
.toList();
ListOverlay.showWithAnchor(
buildContext,
identifier: _identifier,
itemCount: items.length,
itemBuilder: (context, index) => items[index],
anchorContext: anchorContext,
anchorDirection: AnchorDirection.bottomRight,
maxWidth: 120,
maxHeight: 80,
delegate: this,
);
}
@override
void didRemove() {
onSelected(dartz.none());
}
}
class ActionItem extends StatelessWidget {
final ViewAction action;
final Function(ViewAction) onSelected;
const ActionItem({
Key? key,
required this.action,
required this.onSelected,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final theme = context.watch<AppTheme>();
return FlowyHover(
config: HoverDisplayConfig(hoverColor: theme.hover),
builder: (context, onHover) {
return GestureDetector(
onTap: () => onSelected(action),
child: FlowyText.medium(
action.name,
fontSize: 12,
).padding(
horizontal: 10,
vertical: 6,
),
);
},
);
}
}

View File

@ -2,10 +2,10 @@ import 'package:app_flowy/startup/startup.dart';
import 'package:app_flowy/workspace/application/view/view_bloc.dart'; import 'package:app_flowy/workspace/application/view/view_bloc.dart';
import 'package:app_flowy/workspace/domain/page_stack/page_stack.dart'; import 'package:app_flowy/workspace/domain/page_stack/page_stack.dart';
import 'package:app_flowy/workspace/domain/view_ext.dart'; import 'package:app_flowy/workspace/domain/view_ext.dart';
import 'package:app_flowy/workspace/presentation/widgets/pop_up_window.dart';
import 'package:dartz/dartz.dart' as dartz; import 'package:dartz/dartz.dart' as dartz;
import 'package:flowy_infra/image.dart'; import 'package:flowy_infra/image.dart';
import 'package:flowy_infra/theme.dart'; import 'package:flowy_infra/theme.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flowy_infra_ui/style_widget/icon_button.dart'; import 'package:flowy_infra_ui/style_widget/icon_button.dart';
import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/style_widget/text.dart';
@ -20,6 +20,8 @@ import 'package:app_flowy/workspace/domain/image.dart';
import 'package:app_flowy/workspace/domain/view_edit.dart'; import 'package:app_flowy/workspace/domain/view_edit.dart';
import 'package:app_flowy/workspace/presentation/widgets/menu/widget/app/menu_app.dart'; import 'package:app_flowy/workspace/presentation/widgets/menu/widget/app/menu_app.dart';
import 'action.dart';
// ignore: must_be_immutable // ignore: must_be_immutable
class ViewSectionItem extends StatelessWidget { class ViewSectionItem extends StatelessWidget {
final bool isSelected; final bool isSelected;
@ -89,8 +91,12 @@ class ViewSectionItem extends StatelessWidget {
action.foldRight({}, (action, previous) { action.foldRight({}, (action, previous) {
switch (action) { switch (action) {
case ViewAction.rename: case ViewAction.rename:
FlowyPoppuWindow.show(
// TODO: Handle this case. context,
child: ViewRenamePannel(renameCallback: (name) {
context.read<ViewBloc>().add(ViewEvent.rename(name));
}),
);
break; break;
case ViewAction.delete: case ViewAction.delete:
context.read<ViewBloc>().add(const ViewEvent.delete()); context.read<ViewBloc>().add(const ViewEvent.delete());
@ -128,69 +134,12 @@ class ViewDisclosureButton extends StatelessWidget {
} }
} }
class ViewActionList implements FlowyOverlayDelegate { class ViewRenamePannel extends StatelessWidget {
final Function(dartz.Option<ViewAction>) onSelected; final void Function(String) renameCallback;
final BuildContext anchorContext; const ViewRenamePannel({Key? key, required this.renameCallback}) : super(key: key);
final String _identifier = 'ViewActionList';
const ViewActionList({required this.anchorContext, required this.onSelected});
void show(BuildContext buildContext) {
final items = ViewAction.values
.map((action) => ActionItem(
action: action,
onSelected: (action) {
FlowyOverlay.of(buildContext).remove(_identifier);
onSelected(dartz.some(action));
}))
.toList();
ListOverlay.showWithAnchor(
buildContext,
identifier: _identifier,
itemCount: items.length,
itemBuilder: (context, index) => items[index],
anchorContext: anchorContext,
anchorDirection: AnchorDirection.bottomRight,
maxWidth: 120,
maxHeight: 80,
delegate: this,
);
}
@override
void didRemove() {
onSelected(dartz.none());
}
}
class ActionItem extends StatelessWidget {
final ViewAction action;
final Function(ViewAction) onSelected;
const ActionItem({
Key? key,
required this.action,
required this.onSelected,
}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = context.watch<AppTheme>(); return SizedBox(width: 100, height: 200, child: Container(color: Colors.black));
return FlowyHover(
config: HoverDisplayConfig(hoverColor: theme.hover),
builder: (context, onHover) {
return GestureDetector(
onTap: () => onSelected(action),
child: FlowyText.medium(
action.name,
fontSize: 12,
).padding(
horizontal: 10,
vertical: 6,
),
);
},
);
} }
} }

View File

@ -3,6 +3,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart';
import 'item.dart'; import 'item.dart';
class ViewListNotifier extends ChangeNotifier { class ViewListNotifier extends ChangeNotifier {
@ -47,10 +48,7 @@ class ViewSection extends StatelessWidget {
// The ViewListNotifier will be updated after ViewListData changed passed by parent widget // The ViewListNotifier will be updated after ViewListData changed passed by parent widget
return ChangeNotifierProxyProvider<ViewListNotifier, ViewSectionNotifier>( return ChangeNotifierProxyProvider<ViewListNotifier, ViewSectionNotifier>(
create: (_) { create: (_) {
final views = Provider.of<ViewListNotifier>( final views = Provider.of<ViewListNotifier>(context, listen: false).items;
context,
listen: false,
).items;
return ViewSectionNotifier(views); return ViewSectionNotifier(views);
}, },
update: (_, notifier, controller) => controller!..update(notifier), update: (_, notifier, controller) => controller!..update(notifier),
@ -61,20 +59,15 @@ class ViewSection extends StatelessWidget {
} }
Widget _renderSectionItems(BuildContext context, List<View> views) { Widget _renderSectionItems(BuildContext context, List<View> views) {
var viewWidgets = views.map((view) { var viewWidgets = views.map(
final item = ViewSectionItem( (view) => ViewSectionItem(
view: view, view: view,
isSelected: _isViewSelected(context, view.id), isSelected: _isViewSelected(context, view.id),
onSelected: (view) => context.read<ViewSectionNotifier>().setSelectedView(view), onSelected: (view) => context.read<ViewSectionNotifier>().setSelectedView(view),
).padding(vertical: 4),
); );
return Padding( return Column(children: viewWidgets.toList(growable: false));
padding: const EdgeInsets.symmetric(vertical: 4),
child: item,
);
}).toList(growable: false);
return Column(children: viewWidgets);
} }
bool _isViewSelected(BuildContext context, String viewId) { bool _isViewSelected(BuildContext context, String viewId) {

View File

@ -0,0 +1,28 @@
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:window_size/window_size.dart';
class FlowyPoppuWindow extends StatelessWidget {
final Widget child;
const FlowyPoppuWindow({Key? key, required this.child}) : super(key: key);
@override
Widget build(BuildContext context) {
return child;
}
static Future<void> show(
BuildContext context, {
required Widget child,
}) async {
final window = await getWindowInfo();
FlowyOverlay.of(context).insertWithRect(
widget: FlowyPoppuWindow(child: child),
identifier: 'FlowyPoppuWindow',
anchorPosition: Offset.zero,
anchorSize: window.frame.size,
anchorDirection: AnchorDirection.center,
style: FlowyOverlayStyle(blur: true),
);
}
}

View File

@ -3,6 +3,7 @@
import 'package:dartz/dartz.dart' show Tuple3; import 'package:dartz/dartz.dart' show Tuple3;
import 'package:flowy_infra_ui/src/flowy_overlay/layout.dart'; import 'package:flowy_infra_ui/src/flowy_overlay/layout.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'dart:ui';
/// Specifies how overlay are anchored to the SourceWidget /// Specifies how overlay are anchored to the SourceWidget
enum AnchorDirection { enum AnchorDirection {
@ -11,6 +12,7 @@ enum AnchorDirection {
topRight, topRight,
bottomLeft, bottomLeft,
bottomRight, bottomRight,
center,
// Edge aligned with a edge of the SourceWidget // Edge aligned with a edge of the SourceWidget
topWithLeftAligned, topWithLeftAligned,
@ -55,21 +57,20 @@ enum OnBackBehavior {
dismiss, dismiss,
} }
class FlowyOverlayConfig { class FlowyOverlayStyle {
final Color barrierColor; final Color barrierColor;
bool blur;
FlowyOverlayConfig({required this.barrierColor}); FlowyOverlayStyle({this.barrierColor = Colors.transparent, this.blur = false});
const FlowyOverlayConfig.defualt() : barrierColor = Colors.transparent;
} }
final GlobalKey<FlowyOverlayState> _key = GlobalKey<FlowyOverlayState>(); final GlobalKey<FlowyOverlayState> _key = GlobalKey<FlowyOverlayState>();
/// Invoke this method in app generation process /// Invoke this method in app generation process
TransitionBuilder overlayManagerBuilder({FlowyOverlayConfig config = const FlowyOverlayConfig.defualt()}) { TransitionBuilder overlayManagerBuilder() {
return (context, child) { return (context, child) {
assert(child != null, 'Child can\'t be null.'); assert(child != null, 'Child can\'t be null.');
return FlowyOverlay(key: _key, child: child!, config: config); return FlowyOverlay(key: _key, child: child!);
}; };
} }
@ -78,12 +79,10 @@ abstract class FlowyOverlayDelegate {
} }
class FlowyOverlay extends StatefulWidget { class FlowyOverlay extends StatefulWidget {
const FlowyOverlay({Key? key, required this.child, required this.config}) : super(key: key); const FlowyOverlay({Key? key, required this.child}) : super(key: key);
final Widget child; final Widget child;
final FlowyOverlayConfig config;
static FlowyOverlayState of(BuildContext context, {bool rootOverlay = false}) { static FlowyOverlayState of(BuildContext context, {bool rootOverlay = false}) {
FlowyOverlayState? state = maybeOf(context, rootOverlay: rootOverlay); FlowyOverlayState? state = maybeOf(context, rootOverlay: rootOverlay);
assert(() { assert(() {
@ -113,6 +112,7 @@ class FlowyOverlay extends StatefulWidget {
class FlowyOverlayState extends State<FlowyOverlay> { class FlowyOverlayState extends State<FlowyOverlay> {
List<Tuple3<Widget, String, FlowyOverlayDelegate?>> _overlayList = []; List<Tuple3<Widget, String, FlowyOverlayDelegate?>> _overlayList = [];
FlowyOverlayStyle style = FlowyOverlayStyle();
/// Insert a overlay widget which frame is set by the widget, not the component. /// Insert a overlay widget which frame is set by the widget, not the component.
/// Be sure to specify the offset and size using a anchorable widget (like `Postition`, `CompositedTransformFollower`) /// Be sure to specify the offset and size using a anchorable widget (like `Postition`, `CompositedTransformFollower`)
@ -137,7 +137,12 @@ class FlowyOverlayState extends State<FlowyOverlay> {
AnchorDirection? anchorDirection, AnchorDirection? anchorDirection,
FlowyOverlayDelegate? delegate, FlowyOverlayDelegate? delegate,
OverlapBehaviour? overlapBehaviour, OverlapBehaviour? overlapBehaviour,
FlowyOverlayStyle? style,
}) { }) {
if (style != null) {
this.style = style;
}
_showOverlay( _showOverlay(
widget: widget, widget: widget,
identifier: identifier, identifier: identifier,
@ -157,7 +162,10 @@ class FlowyOverlayState extends State<FlowyOverlay> {
AnchorDirection? anchorDirection, AnchorDirection? anchorDirection,
FlowyOverlayDelegate? delegate, FlowyOverlayDelegate? delegate,
OverlapBehaviour? overlapBehaviour, OverlapBehaviour? overlapBehaviour,
FlowyOverlayStyle? style,
}) { }) {
this.style = style ?? FlowyOverlayStyle();
_showOverlay( _showOverlay(
widget: widget, widget: widget,
identifier: identifier, identifier: identifier,
@ -245,17 +253,30 @@ class FlowyOverlayState extends State<FlowyOverlay> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final overlays = _overlayList.map((ele) => ele.value1); final overlays = _overlayList.map((ele) => ele.value1);
final children = <Widget>[ List<Widget> children = <Widget>[widget.child];
widget.child,
if (overlays.isNotEmpty) Widget? child;
Container( if (overlays.isNotEmpty) {
color: widget.config.barrierColor, child = Container(
color: style.barrierColor,
child: GestureDetector( child: GestureDetector(
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
onTap: _handleTapOnBackground, onTap: _handleTapOnBackground,
), ),
), );
];
if (style.blur) {
child = BackdropFilter(
child: child,
filter: ImageFilter.blur(sigmaX: 4, sigmaY: 4),
);
}
}
if (child != null) {
children.add(child);
}
return Stack( return Stack(
children: children..addAll(overlays), children: children..addAll(overlays),
); );

View File

@ -55,6 +55,12 @@ class OverlayLayoutDelegate extends SingleChildLayoutDelegate {
constraints.maxHeight - anchorRect.bottom, constraints.maxHeight - anchorRect.bottom,
)); ));
break; break;
case AnchorDirection.center:
childConstraints = BoxConstraints.loose(Size(
constraints.maxWidth,
constraints.maxHeight,
));
break;
case AnchorDirection.topWithLeftAligned: case AnchorDirection.topWithLeftAligned:
childConstraints = BoxConstraints.loose(Size( childConstraints = BoxConstraints.loose(Size(
constraints.maxWidth - anchorRect.left, constraints.maxWidth - anchorRect.left,
@ -165,6 +171,9 @@ class OverlayLayoutDelegate extends SingleChildLayoutDelegate {
anchorRect.bottom, anchorRect.bottom,
); );
break; break;
case AnchorDirection.center:
position = anchorRect.center;
break;
case AnchorDirection.topWithLeftAligned: case AnchorDirection.topWithLeftAligned:
position = Offset( position = Offset(
anchorRect.left, anchorRect.left,

View File

@ -55,6 +55,7 @@ class ListOverlay extends StatelessWidget {
AnchorDirection? anchorDirection, AnchorDirection? anchorDirection,
FlowyOverlayDelegate? delegate, FlowyOverlayDelegate? delegate,
OverlapBehaviour? overlapBehaviour, OverlapBehaviour? overlapBehaviour,
FlowyOverlayStyle? style,
}) { }) {
FlowyOverlay.of(context).insertWithAnchor( FlowyOverlay.of(context).insertWithAnchor(
widget: ListOverlay( widget: ListOverlay(
@ -69,6 +70,7 @@ class ListOverlay extends StatelessWidget {
anchorDirection: anchorDirection, anchorDirection: anchorDirection,
delegate: delegate, delegate: delegate,
overlapBehaviour: overlapBehaviour, overlapBehaviour: overlapBehaviour,
style: style,
); );
} }
@ -86,6 +88,7 @@ class ListOverlay extends StatelessWidget {
AnchorDirection? anchorDirection, AnchorDirection? anchorDirection,
FlowyOverlayDelegate? delegate, FlowyOverlayDelegate? delegate,
OverlapBehaviour? overlapBehaviour, OverlapBehaviour? overlapBehaviour,
FlowyOverlayStyle? style,
}) { }) {
FlowyOverlay.of(context).insertWithRect( FlowyOverlay.of(context).insertWithRect(
widget: ListOverlay( widget: ListOverlay(
@ -101,6 +104,7 @@ class ListOverlay extends StatelessWidget {
anchorDirection: anchorDirection, anchorDirection: anchorDirection,
delegate: delegate, delegate: delegate,
overlapBehaviour: overlapBehaviour, overlapBehaviour: overlapBehaviour,
style: style,
); );
} }
} }