Skip to content

Commit

Permalink
feat: added absorbpointer for duplicated views
Browse files Browse the repository at this point in the history
  • Loading branch information
squidrye committed Sep 22, 2023
1 parent 45566b0 commit 72b03dd
Show file tree
Hide file tree
Showing 6 changed files with 284 additions and 4 deletions.
6 changes: 6 additions & 0 deletions frontend/appflowy_flutter/lib/core/config/kv_keys.dart
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,10 @@ class KVKeys {
/// The value is a boolean string.
static const String showRenameDialogWhenCreatingNewFile =
'showRenameDialogWhenCreatingNewFile';

///The key for saving list of open views when using multi-pane feature
///
///The value is a json string with following format:
/// openedPlugins: {'pluginId': true, 'pluginId':false}
static const String openedPlugins = 'openedPlugins';
}
5 changes: 5 additions & 0 deletions frontend/appflowy_flutter/lib/startup/startup.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import 'dart:io';

import 'package:appflowy/core/config/kv.dart';
import 'package:appflowy/core/config/kv_keys.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy_backend/appflowy_backend.dart';
import 'package:flutter/foundation.dart';
Expand Down Expand Up @@ -50,6 +52,9 @@ class FlowyRunner {
(value) => Directory(value),
);

// remove panes shared preference
await getIt<KeyValueStorage>().remove(KVKeys.openedPlugins);

// add task
final launcher = getIt<AppLauncher>();
launcher.addTasks(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import 'package:appflowy/plugins/util.dart';
import 'package:appflowy/startup/plugin/plugin.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/tabs/tabs_service.dart';
import 'package:appflowy/workspace/presentation/home/home_stack.dart';
import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart';
import 'package:appflowy/workspace/presentation/home/tabs/draggable_tab_item.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:flutter/material.dart';

class TabsController extends ChangeNotifier {
int currentIndex;
List<PageManager> pageManagers;
int get pages => pageManagers.length;
PageManager get currentPageManager => pageManagers[currentIndex];
final MenuSharedState menuSharedState;

TabsController({int? currentIndex, List<PageManager>? pageManagers})
: pageManagers = pageManagers ?? [PageManager()],
menuSharedState = getIt<MenuSharedState>(),
currentIndex = currentIndex ?? 0;

Future<void> closeAllViews() async {
for (final page in pageManagers) {
closeView(page.plugin.id, closePaneSubRoutine: true);
}
}

void openView(Plugin plugin) async {
final selectExistingPlugin = _selectPluginIfOpen(plugin.id);

if (!selectExistingPlugin) {
final readOnly = await TabsService.setPluginOpenedInCache(plugin);
pageManagers.add(PageManager()
..setPlugin(plugin)
..setReadOnlyStatus(readOnly));
}
currentIndex = pageManagers.length - 1;

setLatestOpenView();
notifyListeners();
}

void closeView(String pluginId, {bool? closePaneSubRoutine}) async {
// Avoid closing the only open tab
if (pageManagers.length == 1) {
if (closePaneSubRoutine ?? false) {
await TabsService.setPluginClosedInCache(pluginId);
}
return;
}
await TabsService.setPluginClosedInCache(pluginId);
pageManagers.removeWhere((pm) => pm.plugin.id == pluginId);

/// If currentIndex is greater than the amount of allowed indices
/// And the current selected tab isn't the first (index 0)
/// as currentIndex cannot be -1
/// Then decrease currentIndex by 1
currentIndex = currentIndex > pageManagers.length - 1 && currentIndex > 0
? currentIndex - 1
: currentIndex;

setLatestOpenView();
notifyListeners();
}

/// Checks if a [Plugin.id] is already associated with an open tab.
/// Returns a [TabState] with new index if there is a match.
///
/// If no match it returns null
///
bool _selectPluginIfOpen(String id) {
final index = pageManagers.indexWhere((pm) => pm.plugin.id == id);
if (index == -1) {
return false;
}
currentIndex = index;
notifyListeners();
return true;
}

/// This opens a plugin in the current selected tab,
/// due to how Document currently works, only one tab
/// per plugin can currently be active.
///
/// If the plugin is already open in a tab, then that tab
/// will become selected.
///
void openPlugin({required Plugin plugin}) async {
final selectExistingPlugin = _selectPluginIfOpen(plugin.id);

if (!selectExistingPlugin) {
await TabsService.setPluginClosedInCache(currentPageManager.plugin.id);
final readOnly = await TabsService.setPluginOpenedInCache(plugin);
pageManagers[currentIndex]
..setPlugin(plugin)
..setReadOnlyStatus(readOnly);
}
setLatestOpenView();
notifyListeners();
}

void selectTab({required int index}) {
if (index != currentIndex && index >= 0 && index < pages) {
currentIndex = index;
setLatestOpenView();
notifyListeners();
}
}

void move({
required PageManager from,
required PageManager to,
required TabDraggableHoverPosition position,
}) async {
final selectExistingPlugin = _selectPluginIfOpen(from.plugin.id);

if (!selectExistingPlugin) {
final readOnly = await TabsService.setPluginOpenedInCache(from.plugin);
final newPm = PageManager()
..setPlugin(from.plugin)
..setReadOnlyStatus(readOnly);
switch (position) {
case TabDraggableHoverPosition.none:
return;
case TabDraggableHoverPosition.left:
{
final index = pageManagers.indexOf(to);
pageManagers.insert(index, newPm);
currentIndex = index;
break;
}
case TabDraggableHoverPosition.right:
{
final index = pageManagers.indexOf(to);
if (index + 1 == pageManagers.length) {
pageManagers.add(newPm);
} else {
pageManagers.insert(index + 1, newPm);
}
currentIndex = index + 1;
break;
}
}
}
setLatestOpenView();
notifyListeners();
}

void setLatestOpenView([ViewPB? view]) {
if (view != null) {
menuSharedState.latestOpenView = view;
} else {
final notifier = currentPageManager.plugin.notifier;
if (notifier is ViewPluginNotifier) {
menuSharedState.latestOpenView = notifier.view;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import 'dart:convert';

import 'package:appflowy/core/config/kv.dart';
import 'package:appflowy/core/config/kv_keys.dart';
import 'package:appflowy/startup/plugin/plugin.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy_backend/log.dart';

class TabsService {
const TabsService();

static Future<void> setPluginClosedInCache(String pluginId) async {
final result = await getIt<KeyValueStorage>().get(KVKeys.openedPlugins);
final map = result.fold(
(l) => {},
(r) => jsonDecode(r),
);
if (map[pluginId] != null) {
map[pluginId] -= 1;

if (map[pluginId] <= 0) {
map.remove(pluginId);
}
}
await getIt<KeyValueStorage>().set(KVKeys.openedPlugins, jsonEncode(map));
}

static Future<bool> setPluginOpenedInCache(Plugin plugin) async {
final result = await getIt<KeyValueStorage>().get(KVKeys.openedPlugins);
final map = result.fold(
(l) => {},
(r) => jsonDecode(r),
);
// Log.warn("Result Map $map ${map[plugin.id]} ${plugin.id}");
if (map[plugin.id] != null) {
map[plugin.id] += 1;
return true;
}

map[plugin.id] = 1;
Log.warn(map);
await getIt<KeyValueStorage>().set(KVKeys.openedPlugins, jsonEncode(map));
return false;
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'package:appflowy/core/frameless_window.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/blank/blank.dart';
import 'package:appflowy/startup/plugin/plugin.dart';
import 'package:appflowy/startup/startup.dart';
Expand All @@ -11,6 +12,7 @@ import 'package:appflowy/workspace/presentation/home/panes/flowy_pane_group.dart
import 'package:appflowy/workspace/presentation/home/toast.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/extension.dart';
import 'package:flutter/material.dart';
Expand Down Expand Up @@ -41,6 +43,7 @@ class HomeStack extends StatelessWidget {
Widget build(BuildContext context) {
return BlocBuilder<PanesCubit, PanesState>(
builder: (context, state) {
_printTree(state.root);
return BlocBuilder<HomeSettingBloc, HomeSettingState>(
builder: (context, homeState) {
return FlowyPaneGroup(
Expand All @@ -55,6 +58,13 @@ class HomeStack extends StatelessWidget {
},
);
}

void _printTree(PaneNode node, [String prefix = '']) {
print('$prefix${node.tabs.hashCode}');
for (var child in node.children) {
_printTree(child, '$prefix └─ ');
}
}
}

class PageStack extends StatefulWidget {
Expand All @@ -77,7 +87,23 @@ class _PageStackState extends State<PageStack>
@override
Widget build(BuildContext context) {
super.build(context);
if (widget.pageManager.readOnly) {
return Stack(
children: [
AbsorbPointer(
child: Opacity(
opacity: 0.5,
child: _buildWidgetStack(context),
),
),
Positioned(child: _buildReadOnlyBanner())
],
);
}
return _buildWidgetStack(context);
}

Widget _buildWidgetStack(BuildContext context) {
return Container(
color: Theme.of(context).colorScheme.surface,
child: FocusTraversalGroup(
Expand All @@ -90,6 +116,29 @@ class _PageStackState extends State<PageStack>
);
}

Widget _buildReadOnlyBanner() {
final colorScheme = Theme.of(context).colorScheme;
return ConstrainedBox(
constraints: const BoxConstraints(minHeight: 20),
child: Container(
width: double.infinity,
color: colorScheme.primary,
child: FittedBox(
alignment: Alignment.center,
fit: BoxFit.scaleDown,
child: Row(
children: [
FlowyText.medium(
LocaleKeys.readOnlyViewText.tr(),
fontSize: 14,
),
],
),
),
),
);
}

@override
bool get wantKeepAlive => true;
}
Expand Down Expand Up @@ -152,14 +201,16 @@ abstract mixin class NavigationItem {

class PageNotifier extends ChangeNotifier {
Plugin _plugin;
bool _readOnly;

Widget get titleWidget => _plugin.widgetBuilder.leftBarItem;

Widget tabBarWidget(String pluginId) =>
_plugin.widgetBuilder.tabBarItem(pluginId);

PageNotifier({Plugin? plugin})
: _plugin = plugin ?? makePlugin(pluginType: PluginType.blank);
PageNotifier({Plugin? plugin, bool? readOnly})
: _plugin = plugin ?? makePlugin(pluginType: PluginType.blank),
_readOnly = readOnly ?? false;

/// This is the only place where the plugin is set.
/// No need compare the old plugin with the new plugin. Just set it.
Expand All @@ -173,7 +224,14 @@ class PageNotifier extends ChangeNotifier {
notifyListeners();
}

set readOnlyStatus(bool status) {
_readOnly = status;
notifyListeners();
}

Plugin get plugin => _plugin;

bool get readOnly => _readOnly;
}

// PageManager manages the view for one Tab
Expand All @@ -190,10 +248,16 @@ class PageManager {

Plugin get plugin => _notifier.plugin;

bool get readOnly => _notifier.readOnly;

void setPlugin(Plugin newPlugin) {
_notifier.plugin = newPlugin;
}

void setReadOnlyStatus(bool status) {
_notifier.readOnlyStatus = status;
}

void setStackWithId(String id) {
// Navigate to the page with id
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import 'package:appflowy/workspace/application/panes/panes.dart';
import 'package:appflowy/workspace/application/panes/panes_cubit/panes_cubit.dart';
import 'package:appflowy/workspace/application/tabs/tabs.dart';
import 'package:appflowy/workspace/application/tabs/tabs_controller.dart';
import 'package:appflowy/workspace/presentation/home/home_sizes.dart';
import 'package:appflowy/workspace/presentation/home/tabs/draggable_tab_item.dart';
import 'package:appflowy/workspace/presentation/home/tabs/flowy_tab.dart';
Expand All @@ -11,7 +11,7 @@ import 'package:provider/provider.dart';

class TabsManager extends StatefulWidget {
final PageController pageController;
final Tabs tabs;
final TabsController tabs;
final PaneNode pane;
const TabsManager({
super.key,
Expand Down

0 comments on commit 72b03dd

Please sign in to comment.