Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions lib/src/model/analysis/analysis_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,11 @@ class AnalysisController extends _$AnalysisController
@override
void expandVariations(UciPath path) {
final node = _root.nodeAt(path);
for (final child in node.children) {

final childrenToHide =
_root.isOnMainline(path) ? node.children.skip(1) : node.children;

for (final child in childrenToHide) {
child.isHidden = false;
for (final grandChild in child.children) {
grandChild.isHidden = false;
Expand All @@ -283,7 +287,6 @@ class AnalysisController extends _$AnalysisController
@override
void collapseVariations(UciPath path) {
final node = _root.nodeAt(path);

for (final child in node.children) {
child.isHidden = true;
}
Expand Down
131 changes: 90 additions & 41 deletions lib/src/widgets/pgn.dart
Original file line number Diff line number Diff line change
Expand Up @@ -615,6 +615,13 @@ class _SideLinePart extends ConsumerWidget {
return moves;
},
).flattened,
if (nodes.last.children.any((node) => !node.isHidden))
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: _CollapseVariationsButton(
onTap: () => params.notifier.collapseVariations(path),
),
),
];

return Text.rich(
Expand All @@ -625,6 +632,26 @@ class _SideLinePart extends ConsumerWidget {
}
}

class _CollapseVariationsButton extends StatelessWidget {
const _CollapseVariationsButton({
required this.onTap,
});

final VoidCallback onTap;

@override
Widget build(BuildContext context) {
return AdaptiveInkWell(
onTap: onTap,
child: Icon(
Icons.indeterminate_check_box,
color: _textColor(context, 0.6),
size: _baseTextStyle.fontSize! + 5,
),
);
}
}

/// A widget that renders part of the mainline.
///
/// A part of the mainline is rendered on a single line. See [_mainlineParts].
Expand All @@ -650,41 +677,47 @@ class _MainLinePart extends ConsumerWidget {
var path = initialPath;
return Text.rich(
TextSpan(
children: nodes
.takeWhile((node) => node.children.isNotEmpty)
.mapIndexed(
(i, node) {
final mainlineNode = node.children.first;
final moves = [
_moveWithComment(
mainlineNode,
lineInfo: (
type: _LineType.mainline,
startLine: i == 0 || (node as ViewBranch).hasTextComment,
pathToLine: initialPath,
),
pathToNode: path,
children: [
...nodes.takeWhile((node) => node.children.isNotEmpty).mapIndexed(
(i, node) {
final mainlineNode = node.children.first;
final moves = [
_moveWithComment(
mainlineNode,
lineInfo: (
type: _LineType.mainline,
startLine: i == 0 || (node as ViewBranch).hasTextComment,
pathToLine: initialPath,
),
pathToNode: path,
textStyle: textStyle,
params: params,
),
if (node.children.length == 2 &&
_displaySideLineAsInline(node.children[1])) ...[
_buildInlineSideLine(
followsComment: mainlineNode.hasTextComment,
firstNode: node.children[1],
parent: node,
initialPath: path,
textStyle: textStyle,
params: params,
),
if (node.children.length == 2 &&
_displaySideLineAsInline(node.children[1])) ...[
_buildInlineSideLine(
followsComment: mainlineNode.hasTextComment,
firstNode: node.children[1],
parent: node,
initialPath: path,
textStyle: textStyle,
params: params,
),
],
];
path = path + mainlineNode.id;
return moves.flattened;
},
)
.flattened
.toList(growable: false),
],
];
path = path + mainlineNode.id;
return moves.flattened;
},
).flattened,
if (nodes.last.children.skip(1).any((node) => !node.isHidden))
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: _CollapseVariationsButton(
onTap: () =>
params.notifier.collapseVariations(path.penultimate),
),
),
],
),
);
}
Expand Down Expand Up @@ -917,16 +950,11 @@ class _IndentedSideLinesState extends State<_IndentedSideLines> {
children: [
...sideLineWidgets,
if (_hasHiddenLines)
GestureDetector(
child: Icon(
Icons.add_box,
color: _textColor(context, 0.6),
key: _sideLinesStartKeys.last,
size: _baseTextStyle.fontSize! + 5,
_ExpandVariationsButton(
key: _sideLinesStartKeys.last,
onTap: () => widget.params.notifier.expandVariations(
widget.initialPath,
),
onTap: () {
widget.params.notifier.expandVariations(widget.initialPath);
},
),
],
),
Expand All @@ -935,6 +963,27 @@ class _IndentedSideLinesState extends State<_IndentedSideLines> {
}
}

class _ExpandVariationsButton extends StatelessWidget {
const _ExpandVariationsButton({
super.key,
required this.onTap,
});

final VoidCallback onTap;

@override
Widget build(BuildContext context) {
return AdaptiveInkWell(
onTap: onTap,
child: Icon(
Icons.add_box,
color: _textColor(context, 0.6),
size: _baseTextStyle.fontSize! + 5,
),
);
}
}

Color? _textColor(
BuildContext context,
double opacity, {
Expand Down
108 changes: 108 additions & 0 deletions test/view/analysis/analysis_screen_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -255,11 +255,15 @@ void main() {
expect(find.text('2… Qd7'), findsNothing);

// sidelines with nesting > 2 are collapsed -> expand them
expect(find.byIcon(Icons.indeterminate_check_box), findsNWidgets(2));
expect(find.byIcon(Icons.add_box), findsOneWidget);

await tester.tap(find.byIcon(Icons.add_box));
await tester.pumpAndSettle();

expect(find.byIcon(Icons.indeterminate_check_box), findsNWidgets(3));
expect(find.byIcon(Icons.add_box), findsNothing);

expectSameLine(tester, ['2… h5']);
expectSameLine(tester, ['2… Nc6', '3. d3']);
expectSameLine(tester, ['2… Qd7']);
Expand All @@ -275,13 +279,117 @@ void main() {

// Sidelines should be collapsed again
expect(find.byIcon(Icons.add_box), findsOneWidget);
expect(find.byIcon(Icons.indeterminate_check_box), findsNWidgets(2));

expect(find.text('2… h5'), findsNothing);
expect(find.text('2… Nc6'), findsNothing);
expect(find.text('3. d3'), findsNothing);
expect(find.text('2… Qd7'), findsNothing);
});

testWidgets('expand/collapse sidelines via icon', (tester) async {
// Will be rendered as:
// -------------------
// 1. e4 e5 [-] // <- collapse icon
// |- 1... c5 [-] // <- collapse icon
// |- 2. Nf3
// |- 2. Nc3
// |- 2. h4
// |- 1... h5
// 2. Ke2
await buildTree(
tester,
'1. e4 e5 (1... c5 2. Nf3 (2. Nc3) (2. h4)) (1... h5) 2. Ke2',
);

// Sidelines should be visible by default
expect(find.byIcon(Icons.add_box), findsNothing);
expect(find.byIcon(Icons.indeterminate_check_box), findsNWidgets(2));
expect(find.text('1… c5'), findsOneWidget);
expect(find.text('1… h5'), findsOneWidget);
expect(find.text('2. Nc3'), findsOneWidget);
expect(find.text('2. h4'), findsOneWidget);

await tester.tap(find.byIcon(Icons.indeterminate_check_box).first);

// need to wait for current move change debounce delay
await tester.pumpAndSettle();

// After collapsing the first sideline:
// 1. e4 e5
// |- [+] // <- expand icon
// 2. Ke2
expect(find.text('1… c5'), findsNothing);
expect(find.text('1… h5'), findsNothing);
expect(find.text('2. Nc3'), findsNothing);
expect(find.text('2. h4'), findsNothing);

// Expand again
expect(find.byIcon(Icons.add_box), findsOneWidget);
await tester.tap(find.byIcon(Icons.add_box));
// need to wait for current move change debounce delay
await tester.pumpAndSettle();

// Collapse the inner sidelines
await tester.tap(find.byIcon(Icons.indeterminate_check_box).last);
// need to wait for current move change debounce delay
await tester.pumpAndSettle();

// After collapsing the inner sidelines:
// 1. e4 e5 [-] // <- collapse icon
// |- 1... c5
// |- [+] // <- expand icon
// |- 1... h5
// 2. Ke2
expect(find.text('1… c5'), findsOneWidget);
expect(find.text('1… h5'), findsOneWidget);
expect(find.text('2. Nc3'), findsNothing);
expect(find.text('2. h4'), findsNothing);

expect(find.byIcon(Icons.add_box), findsOneWidget);
expect(find.byIcon(Icons.indeterminate_check_box), findsOneWidget);
});
testWidgets(
'Expanding one line does not expand the following one (regression test)',
(tester) async {
/// Will be rendered as:
/// -------------------
/// 1. e4 e5
/// |- 1... d5 2. Nf3 (2.Nc3)
/// 2. Nf3
/// |- 2. a4 d5 (2... f5)
/// -------------------
await buildTree(
tester,
'1. e4 e5 (1... d5 2. Nf3 (2. Nc3)) 2. Nf3 (2. a4 d5 (2... f5))',
);

expect(find.byIcon(Icons.indeterminate_check_box), findsNWidgets(2));
expect(find.byIcon(Icons.add_box), findsNothing);

// Collapse both lines
await tester.tap(find.byIcon(Icons.indeterminate_check_box).first);
// need to wait for current move change debounce delay
await tester.pumpAndSettle();
await tester.tap(find.byIcon(Icons.indeterminate_check_box).first);
// need to wait for current move change debounce delay
await tester.pumpAndSettle();

// In this state, there used to be a bug where expanding the first line would
// also expand the second line.
expect(find.byIcon(Icons.add_box), findsNWidgets(2));
await tester.tap(find.byIcon(Icons.add_box).first);

// need to wait for current move change debounce delay
await tester.pumpAndSettle();

expect(find.byIcon(Icons.add_box), findsOneWidget);
expect(find.byIcon(Icons.indeterminate_check_box), findsOneWidget);

// Second sideline should still be collapsed
expect(find.text('2. a4'), findsNothing);
});

testWidgets('subtrees not part of the current mainline part are cached',
(tester) async {
await buildTree(
Expand Down