[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));
final theme = AppTheme.fromType(ThemeType.light);
FlowyOverlayConfig config = FlowyOverlayConfig(barrierColor: Colors.transparent);
return Provider.value(
value: theme,
child: MaterialApp(
builder: overlayManagerBuilder(config: config),
builder: overlayManagerBuilder(),
debugShowCheckedModeBanner: false,
theme: theme.themeData,
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/domain/page_stack/page_stack.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:flowy_infra/image.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/icon_button.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/presentation/widgets/menu/widget/app/menu_app.dart';
import 'action.dart';
// ignore: must_be_immutable
class ViewSectionItem extends StatelessWidget {
final bool isSelected;
@ -89,8 +91,12 @@ class ViewSectionItem extends StatelessWidget {
action.foldRight({}, (action, previous) {
switch (action) {
case ViewAction.rename:
// TODO: Handle this case.
FlowyPoppuWindow.show(
context,
child: ViewRenamePannel(renameCallback: (name) {
context.read<ViewBloc>().add(ViewEvent.rename(name));
}),
);
break;
case ViewAction.delete:
context.read<ViewBloc>().add(const ViewEvent.delete());
@ -128,69 +134,12 @@ class ViewDisclosureButton extends StatelessWidget {
}
}
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);
class ViewRenamePannel extends StatelessWidget {
final void Function(String) renameCallback;
const ViewRenamePannel({Key? key, required this.renameCallback}) : 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,
),
);
},
);
return SizedBox(width: 100, height: 200, child: Container(color: Colors.black));
}
}

View File

@ -3,6 +3,7 @@ import 'package:flutter/foundation.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 'item.dart';
class ViewListNotifier extends ChangeNotifier {
@ -47,10 +48,7 @@ class ViewSection extends StatelessWidget {
// The ViewListNotifier will be updated after ViewListData changed passed by parent widget
return ChangeNotifierProxyProvider<ViewListNotifier, ViewSectionNotifier>(
create: (_) {
final views = Provider.of<ViewListNotifier>(
context,
listen: false,
).items;
final views = Provider.of<ViewListNotifier>(context, listen: false).items;
return ViewSectionNotifier(views);
},
update: (_, notifier, controller) => controller!..update(notifier),
@ -61,20 +59,15 @@ class ViewSection extends StatelessWidget {
}
Widget _renderSectionItems(BuildContext context, List<View> views) {
var viewWidgets = views.map((view) {
final item = ViewSectionItem(
var viewWidgets = views.map(
(view) => ViewSectionItem(
view: view,
isSelected: _isViewSelected(context, view.id),
onSelected: (view) => context.read<ViewSectionNotifier>().setSelectedView(view),
);
).padding(vertical: 4),
);
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: item,
);
}).toList(growable: false);
return Column(children: viewWidgets);
return Column(children: viewWidgets.toList(growable: false));
}
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:flowy_infra_ui/src/flowy_overlay/layout.dart';
import 'package:flutter/material.dart';
import 'dart:ui';
/// Specifies how overlay are anchored to the SourceWidget
enum AnchorDirection {
@ -11,6 +12,7 @@ enum AnchorDirection {
topRight,
bottomLeft,
bottomRight,
center,
// Edge aligned with a edge of the SourceWidget
topWithLeftAligned,
@ -55,21 +57,20 @@ enum OnBackBehavior {
dismiss,
}
class FlowyOverlayConfig {
class FlowyOverlayStyle {
final Color barrierColor;
bool blur;
FlowyOverlayConfig({required this.barrierColor});
const FlowyOverlayConfig.defualt() : barrierColor = Colors.transparent;
FlowyOverlayStyle({this.barrierColor = Colors.transparent, this.blur = false});
}
final GlobalKey<FlowyOverlayState> _key = GlobalKey<FlowyOverlayState>();
/// Invoke this method in app generation process
TransitionBuilder overlayManagerBuilder({FlowyOverlayConfig config = const FlowyOverlayConfig.defualt()}) {
TransitionBuilder overlayManagerBuilder() {
return (context, child) {
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 {
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 FlowyOverlayConfig config;
static FlowyOverlayState of(BuildContext context, {bool rootOverlay = false}) {
FlowyOverlayState? state = maybeOf(context, rootOverlay: rootOverlay);
assert(() {
@ -113,6 +112,7 @@ class FlowyOverlay extends StatefulWidget {
class FlowyOverlayState extends State<FlowyOverlay> {
List<Tuple3<Widget, String, FlowyOverlayDelegate?>> _overlayList = [];
FlowyOverlayStyle style = FlowyOverlayStyle();
/// 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`)
@ -137,7 +137,12 @@ class FlowyOverlayState extends State<FlowyOverlay> {
AnchorDirection? anchorDirection,
FlowyOverlayDelegate? delegate,
OverlapBehaviour? overlapBehaviour,
FlowyOverlayStyle? style,
}) {
if (style != null) {
this.style = style;
}
_showOverlay(
widget: widget,
identifier: identifier,
@ -157,7 +162,10 @@ class FlowyOverlayState extends State<FlowyOverlay> {
AnchorDirection? anchorDirection,
FlowyOverlayDelegate? delegate,
OverlapBehaviour? overlapBehaviour,
FlowyOverlayStyle? style,
}) {
this.style = style ?? FlowyOverlayStyle();
_showOverlay(
widget: widget,
identifier: identifier,
@ -245,17 +253,30 @@ class FlowyOverlayState extends State<FlowyOverlay> {
@override
Widget build(BuildContext context) {
final overlays = _overlayList.map((ele) => ele.value1);
final children = <Widget>[
widget.child,
if (overlays.isNotEmpty)
Container(
color: widget.config.barrierColor,
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: _handleTapOnBackground,
),
List<Widget> children = <Widget>[widget.child];
Widget? child;
if (overlays.isNotEmpty) {
child = Container(
color: style.barrierColor,
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: _handleTapOnBackground,
),
];
);
if (style.blur) {
child = BackdropFilter(
child: child,
filter: ImageFilter.blur(sigmaX: 4, sigmaY: 4),
);
}
}
if (child != null) {
children.add(child);
}
return Stack(
children: children..addAll(overlays),
);

View File

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

View File

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