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
94 changes: 40 additions & 54 deletions lib/features/workspace/generic_sql_workspace.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1247,6 +1247,9 @@ class GenericSqlWorkspaceState extends material.State<GenericSqlWorkspace> {
onClose: _sessions.length > 1
? (index) => unawaited(closeTab(index))
: null,
actions: _activeSession.isDiagram
? null
: _buildToolbar(context, _activeSession),
trailing: widget.showDiagram
? QueryaButton.secondary(
key: const material.ValueKey('open_diagram_tab'),
Expand Down Expand Up @@ -1276,6 +1279,40 @@ class GenericSqlWorkspaceState extends material.State<GenericSqlWorkspace> {
);
}

/// The run controls of [session], shown in the tab strip.
material.Widget _buildToolbar(
material.BuildContext context,
SqlQueryTabSession session,
) {
return SqlWorkspaceToolbar(
session: session,
delegate: widget.delegate,
effectiveDatabase: effectiveDatabase,
autocommit: _autocommit,
supportsAutocommit: widget.supportsAutocommit,
supportsStmtTimeout: widget.supportsStmtTimeout,
txOpen: _txOpen,
queryTimeoutSeconds: _queryTimeoutSeconds,
headerBadge: widget.headerBadge,
extraTrailing: widget.extraToolbarTrailing?.call(context, session),
historyEnabled: widget.connectionRow.id != null && !session.running,
onExecute: (statementAtCursor) =>
unawaited(execute(session, statementAtCursor)),
onExplain: () => unawaited(explain(session)),
onCancel: () => unawaited(cancelRunning(session)),
onOpenHistory: () => _openHistory(session),
onOpenFile: () => unawaited(openSqlFile()),
onSaveFile: () => unawaited(saveSqlFile()),
onRunTxCommand: (cmd) => unawaited(runTxCommand(cmd)),
onToggleAutocommit: () {
invalidateAllPanes();
setState(() => _autocommit = !_autocommit);
widget.onAutocommitChanged?.call(_autocommit);
},
onTimeoutChanged: _onStmtTimeoutChanged,
);
}

material.Widget _buildSessionPane(
material.BuildContext context,
SqlQueryTabSession session,
Expand Down Expand Up @@ -1306,45 +1343,9 @@ class GenericSqlWorkspaceState extends material.State<GenericSqlWorkspace> {
key: material.ValueKey(session.id),
fraction: session.topFraction,
maxFraction: 0.85,
top: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_SqlWorkspaceToolbarContainer(
child: SqlWorkspaceToolbar(
session: session,
delegate: widget.delegate,
effectiveDatabase: effectiveDatabase,
autocommit: _autocommit,
supportsAutocommit: widget.supportsAutocommit,
supportsStmtTimeout: widget.supportsStmtTimeout,
txOpen: _txOpen,
queryTimeoutSeconds: _queryTimeoutSeconds,
headerBadge: widget.headerBadge,
extraTrailing: widget.extraToolbarTrailing?.call(context, session),
historyEnabled: widget.connectionRow.id != null && !session.running,
onExecute: (statementAtCursor) => unawaited(execute(session, statementAtCursor)),
onExplain: () => unawaited(explain(session)),
onCancel: () => unawaited(cancelRunning(session)),
onOpenHistory: () => _openHistory(session),
onOpenFile: () => unawaited(openSqlFile()),
onSaveFile: () => unawaited(saveSqlFile()),
onRunTxCommand: (cmd) => unawaited(runTxCommand(cmd)),
onToggleAutocommit: () {
invalidateAllPanes();
setState(() => _autocommit = !_autocommit);
widget.onAutocommitChanged?.call(_autocommit);
},
onTimeoutChanged: _onStmtTimeoutChanged,
),
),
const QueryaDivider(height: 1),
Expanded(
child: QueryEditorTab(
controller: session.controller,
fontSize: _editorFontSize,
),
),
],
top: QueryEditorTab(
controller: session.controller,
fontSize: _editorFontSize,
),
bottom: material.Padding(
// The results sit in a card like the editor above them.
Expand Down Expand Up @@ -1402,18 +1403,3 @@ class GenericSqlWorkspaceState extends material.State<GenericSqlWorkspace> {
);
}
}

class _SqlWorkspaceToolbarContainer extends StatelessWidget {
const _SqlWorkspaceToolbarContainer({required this.child});

final material.Widget child;

@override
material.Widget build(material.BuildContext context) {
return material.Container(
padding: const material.EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: SqlEditorChrome.sqlToolbarDecoration(context),
child: child,
);
}
}
80 changes: 63 additions & 17 deletions lib/features/workspace/sql_query_tab_bar.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'dart:math' as math;

import 'package:flutter/widgets.dart';
import 'package:flutter/material.dart' as material;
import 'package:querya_desktop/features/workspace/sql_editor_chrome.dart';
Expand All @@ -14,6 +16,7 @@ class SqlQueryTabBar extends material.StatelessWidget {
required this.onSelect,
required this.onAdd,
this.onClose,
this.actions,
this.trailing,
});

Expand All @@ -23,33 +26,76 @@ class SqlQueryTabBar extends material.StatelessWidget {
final material.VoidCallback onAdd;
final material.ValueChanged<int>? onClose;

/// The run controls of the active query tab, right of the tabs. They get
/// what the tabs leave ([minTabsWidth] stays with the tabs), so the strip is
/// the only control row above the editor (#1558).
final material.Widget? actions;

/// Shown at the right end of the strip (the schema diagram button).
final material.Widget? trailing;

/// Width kept for the tabs when the actions are wide: at least this much,
/// and enough for every tab and the add button as tabs are added, up to
/// [maxTabsShare] of the strip, so a new tab and the add button are not pushed
/// out of view by the controls.
static const double minTabsWidth = 180;
static const double maxTabsShare = 0.6;

@override
material.Widget build(material.BuildContext context) {
// The same strip as the toolbar under it: workbench surface, hairline.
// The same surface as the rest of the SQL pane: workbench surface, hairline.
return material.Container(
height: 40,
padding: const material.EdgeInsets.symmetric(horizontal: 8),
decoration: SqlEditorChrome.sqlToolbarDecoration(context),
child: material.Row(
children: [
material.Expanded(
child: QueryaTabStrip(
labels: sessions.map((s) => s.title).toList(),
selectedIndex: selectedIndex,
onSelected: onSelect,
onClose: onClose,
onAdd: onAdd,
canClose: sessions.length > 1 ? (_) => true : (_) => false,
child: material.LayoutBuilder(
builder: (context, constraints) {
final tabsWidth = math.min(
math.max(
minTabsWidth,
QueryaTabStrip.naturalWidth(
[for (final s in sessions) s.title],
closable: sessions.length > 1,
),
),
),
if (trailing != null) ...[
const material.SizedBox(width: 8),
trailing!,
],
],
constraints.maxWidth * maxTabsShare,
);
final tail = material.Row(
mainAxisSize: material.MainAxisSize.min,
children: [
// Flexible: the actions are measured against what is left after
// the trailing button, and size their labels to it.
if (actions != null) material.Flexible(child: actions!),
if (actions != null && trailing != null)
const material.SizedBox(width: 8),
if (trailing != null) trailing!,
],
);
return material.Row(
children: [
material.Expanded(
child: QueryaTabStrip(
labels: sessions.map((s) => s.title).toList(),
selectedIndex: selectedIndex,
onSelected: onSelect,
onClose: onClose,
onAdd: onAdd,
canClose: sessions.length > 1 ? (_) => true : (_) => false,
),
),
if (actions != null || trailing != null) ...[
const material.SizedBox(width: 8),
material.ConstrainedBox(
constraints: material.BoxConstraints(
maxWidth: (constraints.maxWidth - tabsWidth - 8)
.clamp(0.0, double.infinity),
),
child: tail,
),
],
],
);
},
),
);
}
Expand Down
10 changes: 6 additions & 4 deletions lib/features/workspace/sql_workspace_toolbar.dart
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ class OpenPreferencesChoice extends SqlWorkspaceSessionChoice {
const OpenPreferencesChoice();
}

/// Standalone single-row toolbar for SQL workspaces.
/// The run controls of a SQL workspace. They sit right of the query tabs in
/// the tab strip, so they size themselves to the width the tabs leave (#1558).
///
/// Encapsulates statement and script execution controls, explain/cancel actions,
/// history, format, file operations, active database/transaction indicators,
Expand Down Expand Up @@ -84,9 +85,10 @@ class SqlWorkspaceToolbar extends StatelessWidget {
final void Function(int? seconds) onTimeoutChanged;

/// Below this width only the run button keeps its label; the rest are
/// icons. Below [iconOnlyWidth] the run button is an icon too.
static const double labelsWidth = 900;
static const double iconOnlyWidth = 620;
/// icons. Below [iconOnlyWidth] the run button is an icon too. The widths are
/// of the room the tab strip leaves, not of the window.
static const double labelsWidth = 640;
static const double iconOnlyWidth = 420;
static String get _modLabel => Platform.isMacOS ? '⌘' : 'Ctrl+';

List<QueryaActionMenuItem<SqlWorkspaceSessionChoice>> _sessionMenuItems() => [
Expand Down
33 changes: 33 additions & 0 deletions lib/shared/widgets/querya_tab_strip.dart
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,39 @@ class QueryaTabStrip extends material.StatefulWidget {
final bool Function(int index)? canClose;
final material.ScrollController? scrollController;

/// Width the strip needs to show every tab and the add button without
/// scrolling: each tab at its widest (selected, with its close target) and
/// the add button. Mirrors the metrics of the tabs below; a test keeps the
/// two in step.
static double naturalWidth(
List<String> labels, {
required bool closable,
bool addButton = true,
}) {
var width = 0.0;
for (var i = 0; i < labels.length; i++) {
final text = material.TextPainter(
text: material.TextSpan(
text: labels[i],
style: const material.TextStyle(
fontSize: 14,
height: 20 / 14,
fontWeight: material.FontWeight.w600,
),
),
textDirection: material.TextDirection.ltr,
maxLines: 1,
)..layout();
// Border 2 + 2, side padding, the 2 px gap and 24 px close target, and 4 px
// of slack: the measured strip is ~2 px per tab wider than the sum.
width += text.width + 4 + 4 + (closable ? 10 + 2 + 24 + 5 : 24);
if (i > 0) width += 6;
}
// 4 px gap and the add button: 16 px icon with 6 px padding.
if (addButton) width += 4 + 28;
return width;
}

/// Tighter strip for toolbars: the same as [QueryaControlSize.sm], as tall
/// as a dense icon button instead of a full tab row.
final bool dense;
Expand Down
Loading
Loading