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
14 changes: 9 additions & 5 deletions lib/features/extensions/extension_workspace_home.dart
Original file line number Diff line number Diff line change
Expand Up @@ -119,11 +119,15 @@ class _ExtensionWorkspaceHomeState
child: Text('Return to ${widget.lastSelectedExtensionObject!.name}'),
),
],
const Spacer(),
QueryaTabStrip(
labels: const ['Overview', 'SQL'],
selectedIndex: _tab,
onSelected: (index) => unawaited(_selectTab(index)),
material.Expanded(
child: material.Align(
alignment: material.Alignment.centerRight,
child: QueryaTabStrip(
labels: const ['Overview', 'SQL'],
selectedIndex: _tab,
onSelected: (index) => unawaited(_selectTab(index)),
),
),
),
],
),
Expand Down
14 changes: 9 additions & 5 deletions lib/features/mysql/mysql_workspace_home.dart
Original file line number Diff line number Diff line change
Expand Up @@ -120,11 +120,15 @@ class _MysqlWorkspaceHomeState extends material.State<MysqlWorkspaceHome> {
Text('Return to ${widget.lastSelectedMysqlObject!.name}'),
),
],
const Spacer(),
QueryaTabStrip(
labels: const ['Overview', 'SQL'],
selectedIndex: _tab,
onSelected: (index) => unawaited(_selectTab(index)),
material.Expanded(
child: material.Align(
alignment: material.Alignment.centerRight,
child: QueryaTabStrip(
labels: const ['Overview', 'SQL'],
selectedIndex: _tab,
onSelected: (index) => unawaited(_selectTab(index)),
),
),
),
],
),
Expand Down
14 changes: 9 additions & 5 deletions lib/features/postgresql/postgres_workspace_home.dart
Original file line number Diff line number Diff line change
Expand Up @@ -134,11 +134,15 @@ class _PostgresWorkspaceHomeState
child: Text('Return to ${widget.lastSelectedPostgresObject!.name}'),
),
],
const Spacer(),
QueryaTabStrip(
labels: const ['Overview', 'SQL'],
selectedIndex: _tab,
onSelected: (index) => unawaited(_selectTab(index)),
material.Expanded(
child: material.Align(
alignment: material.Alignment.centerRight,
child: QueryaTabStrip(
labels: const ['Overview', 'SQL'],
selectedIndex: _tab,
onSelected: (index) => unawaited(_selectTab(index)),
),
),
),
],
),
Expand Down
14 changes: 9 additions & 5 deletions lib/features/sqlite/sqlite_workspace_home.dart
Original file line number Diff line number Diff line change
Expand Up @@ -112,11 +112,15 @@ class _SqliteWorkspaceHomeState extends material.State<SqliteWorkspaceHome> {
'Return to ${widget.lastSelectedSqliteObject!.name}'),
),
],
const Spacer(),
QueryaTabStrip(
labels: const ['Overview', 'SQL'],
selectedIndex: _tab,
onSelected: _selectTab,
material.Expanded(
child: material.Align(
alignment: material.Alignment.centerRight,
child: QueryaTabStrip(
labels: const ['Overview', 'SQL'],
selectedIndex: _tab,
onSelected: _selectTab,
),
),
),
],
),
Expand Down
17 changes: 7 additions & 10 deletions lib/features/workspace/sql_query_tab_bar.dart
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,13 @@ class SqlQueryTabBar extends material.StatelessWidget {
child: material.Row(
children: [
material.Expanded(
child: material.SingleChildScrollView(
scrollDirection: material.Axis.horizontal,
child: QueryaTabStrip(
labels: sessions.map((s) => s.title).toList(),
selectedIndex: selectedIndex,
onSelected: onSelect,
onClose: onClose,
onAdd: onAdd,
canClose: sessions.length > 1 ? (_) => true : (_) => false,
),
child: QueryaTabStrip(
labels: sessions.map((s) => s.title).toList(),
selectedIndex: selectedIndex,
onSelected: onSelect,
onClose: onClose,
onAdd: onAdd,
canClose: sessions.length > 1 ? (_) => true : (_) => false,
),
),
],
Expand Down
103 changes: 93 additions & 10 deletions lib/shared/widgets/querya_tab_strip.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart' as material;
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
Expand All @@ -23,6 +24,7 @@ class QueryaTabStrip extends material.StatefulWidget {
this.onClose,
this.onAdd,
this.canClose,
this.scrollController,
}) : assert(labels.length > 0);

final List<String> labels;
Expand All @@ -31,12 +33,13 @@ class QueryaTabStrip extends material.StatefulWidget {
final material.ValueChanged<int>? onClose;
final material.VoidCallback? onAdd;
final bool Function(int index)? canClose;
final material.ScrollController? scrollController;

@override
material.State<QueryaTabStrip> createState() => _QueryaTabStripState();
material.State<QueryaTabStrip> createState() => QueryaTabStripState();
}

class _QueryaTabStripState extends material.State<QueryaTabStrip>
class QueryaTabStripState extends material.State<QueryaTabStrip>
with material.TickerProviderStateMixin {
late List<material.FocusNode> _focusNodes;
late List<bool> _focused;
Expand All @@ -45,14 +48,32 @@ class _QueryaTabStripState extends material.State<QueryaTabStrip>

late final QueryaSpringController _indicatorLeft;
late final QueryaSpringController _indicatorWidth;
material.ScrollController? _internalScrollController;
material.ScrollController get _effectiveScrollController =>
widget.scrollController ??
(_internalScrollController ??= material.ScrollController());

var _indicatorReady = false;
var _layoutScheduled = false;

@material.visibleForTesting
List<material.GlobalKey> get tabKeysForTesting => List.unmodifiable(_tabKeys);

@material.visibleForTesting
bool get indicatorReadyForTesting => _indicatorReady;

@material.visibleForTesting
material.ScrollController get scrollControllerForTesting =>
_effectiveScrollController;

@override
void initState() {
super.initState();
_indicatorLeft = QueryaSpringController(vsync: this);
_indicatorWidth = QueryaSpringController(vsync: this);
if (widget.scrollController == null) {
_internalScrollController = material.ScrollController();
}
_createFocusState();
}

Expand All @@ -79,10 +100,16 @@ class _QueryaTabStripState extends material.State<QueryaTabStrip>
@override
void didUpdateWidget(covariant QueryaTabStrip oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.scrollController != widget.scrollController) {
if (oldWidget.scrollController == null && widget.scrollController != null) {
_internalScrollController?.dispose();
_internalScrollController = null;
} else if (oldWidget.scrollController != null && widget.scrollController == null) {
_internalScrollController = material.ScrollController();
}
}
if (oldWidget.labels.length != widget.labels.length) {
_disposeFocusNodes();
_createFocusState();
_indicatorReady = false;
_syncTabState(oldWidget.labels.length, widget.labels.length);
}
if (oldWidget.selectedIndex != widget.selectedIndex ||
oldWidget.labels.length != widget.labels.length ||
Expand All @@ -97,10 +124,29 @@ class _QueryaTabStripState extends material.State<QueryaTabStrip>
(index) =>
material.FocusNode(debugLabel: 'Querya tab ${widget.labels[index]}'),
);
_focused = List.filled(widget.labels.length, false);
_focused = List.filled(widget.labels.length, false, growable: true);
_tabKeys = List.generate(widget.labels.length, (_) => material.GlobalKey());
}

void _syncTabState(int oldLength, int newLength) {
if (newLength > oldLength) {
for (var i = oldLength; i < newLength; i++) {
_focusNodes.add(
material.FocusNode(debugLabel: 'Querya tab ${widget.labels[i]}'),
);
_focused.add(false);
_tabKeys.add(material.GlobalKey());
}
} else if (newLength < oldLength) {
for (var i = newLength; i < oldLength; i++) {
_focusNodes[i].dispose();
}
_focusNodes.removeRange(newLength, oldLength);
_focused.removeRange(newLength, oldLength);
_tabKeys.removeRange(newLength, oldLength);
}
}

void _disposeFocusNodes() {
for (final node in _focusNodes) {
node.dispose();
Expand All @@ -112,6 +158,7 @@ class _QueryaTabStripState extends material.State<QueryaTabStrip>
_indicatorLeft.dispose();
_indicatorWidth.dispose();
_disposeFocusNodes();
_internalScrollController?.dispose();
super.dispose();
}

Expand Down Expand Up @@ -167,11 +214,17 @@ class _QueryaTabStripState extends material.State<QueryaTabStrip>
_indicatorLeft.jumpTo(offset.dx);
_indicatorWidth.jumpTo(width);
setState(() => _indicatorReady = true);
return;
} else {
_indicatorLeft.animateTo(offset.dx);
_indicatorWidth.animateTo(width);
}

_indicatorLeft.animateTo(offset.dx);
_indicatorWidth.animateTo(width);
material.Scrollable.ensureVisible(
tabContext,
alignmentPolicy: material.ScrollPositionAlignmentPolicy.keepVisibleAtEnd,
duration: context.motionDuration(QueryaMotion.fast),
curve: context.motionCurve(QueryaMotion.enter),
);
}

@override
Expand Down Expand Up @@ -293,7 +346,7 @@ class _QueryaTabStripState extends material.State<QueryaTabStrip>
],
);

return material.KeyedSubtree(
final stripContent = material.KeyedSubtree(
key: _stripKey,
child: material.Stack(
alignment: material.Alignment.centerLeft,
Expand All @@ -308,6 +361,36 @@ class _QueryaTabStripState extends material.State<QueryaTabStrip>
],
),
);

return material.IntrinsicWidth(
child: material.Listener(
onPointerSignal: (signal) {
if (signal is PointerScrollEvent &&
_effectiveScrollController.hasClients) {
if (signal.scrollDelta.dy != 0 && signal.scrollDelta.dx == 0) {
final target = (_effectiveScrollController.offset +
signal.scrollDelta.dy)
.clamp(
0.0,
_effectiveScrollController.position.maxScrollExtent,
);
if (target != _effectiveScrollController.offset) {
_effectiveScrollController.jumpTo(target);
}
}
}
},
child: material.ScrollConfiguration(
behavior:
const material.ScrollBehavior().copyWith(scrollbars: false),
child: material.SingleChildScrollView(
controller: _effectiveScrollController,
scrollDirection: material.Axis.horizontal,
child: stripContent,
),
),
),
);
}
}

Expand Down
Loading
Loading