From de2782b9f8fbcdd19d26ff6052eca628a6e6e321 Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Mon, 22 Jun 2026 03:44:12 +0300 Subject: [PATCH 1/2] feat(menu): implement File -> New, Open..., Save actions via Intents/Actions --- lib/core/actions/sql_editor_actions.dart | 13 ++ .../main_screen/querya_window_title_bar.dart | 27 ++- lib/features/mysql/mysql_sql_workspace.dart | 186 ++++++++++------ .../postgresql/postgres_sql_workspace.dart | 198 ++++++++++++------ lib/features/sqlite/sqlite_sql_workspace.dart | 182 ++++++++++------ 5 files changed, 409 insertions(+), 197 deletions(-) create mode 100644 lib/core/actions/sql_editor_actions.dart diff --git a/lib/core/actions/sql_editor_actions.dart b/lib/core/actions/sql_editor_actions.dart new file mode 100644 index 00000000..f7ef4cfe --- /dev/null +++ b/lib/core/actions/sql_editor_actions.dart @@ -0,0 +1,13 @@ +import 'package:flutter/widgets.dart'; + +class NewSqlIntent extends Intent { + const NewSqlIntent(); +} + +class OpenSqlIntent extends Intent { + const OpenSqlIntent(); +} + +class SaveSqlIntent extends Intent { + const SaveSqlIntent(); +} diff --git a/lib/features/main_screen/querya_window_title_bar.dart b/lib/features/main_screen/querya_window_title_bar.dart index bf6e2937..77adde20 100644 --- a/lib/features/main_screen/querya_window_title_bar.dart +++ b/lib/features/main_screen/querya_window_title_bar.dart @@ -1,9 +1,11 @@ import 'package:bitsdojo_window/bitsdojo_window.dart'; import 'package:flutter/material.dart' as material show BuildContext, Container, Icon, Icons, MainAxisSize, Widget; +import 'package:flutter/widgets.dart' show Actions, FocusManager; import 'package:querya_desktop/core/theme/querya_theme_scope.dart'; import 'package:querya_desktop/features/connections/driver_manager_dialog.dart'; import 'package:querya_desktop/features/settings/preferences_dialog.dart'; +import 'package:querya_desktop/core/actions/sql_editor_actions.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart'; /// Custom bitsdojo title bar styled from [QueryaThemeScope] workbench tokens. @@ -76,12 +78,29 @@ class QueryaWindowTitleBar extends StatelessWidget { MenuButton( subMenu: [ MenuButton( - onPressed: (_) {}, child: const Text('New')), - MenuButton( - onPressed: (_) {}, + onPressed: (ctx) { + Actions.maybeInvoke( + FocusManager.instance.primaryFocus?.context ?? ctx, + const NewSqlIntent(), + ); + }, + child: const Text('New')), + MenuButton( + onPressed: (ctx) { + Actions.maybeInvoke( + FocusManager.instance.primaryFocus?.context ?? ctx, + const OpenSqlIntent(), + ); + }, child: const Text('Open...')), MenuButton( - onPressed: (_) {}, child: const Text('Save')), + onPressed: (ctx) { + Actions.maybeInvoke( + FocusManager.instance.primaryFocus?.context ?? ctx, + const SaveSqlIntent(), + ); + }, + child: const Text('Save')), const MenuDivider(), MenuButton( onPressed: (_) => appWindow.close(), diff --git a/lib/features/mysql/mysql_sql_workspace.dart b/lib/features/mysql/mysql_sql_workspace.dart index e2778fa2..339128c3 100644 --- a/lib/features/mysql/mysql_sql_workspace.dart +++ b/lib/features/mysql/mysql_sql_workspace.dart @@ -1,8 +1,11 @@ import 'dart:async'; +import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart' as material; import 'package:flutter/services.dart' show LogicalKeyboardKey; +import 'package:file_selector/file_selector.dart'; +import 'package:querya_desktop/core/actions/sql_editor_actions.dart'; import 'package:querya_desktop/core/database/mysql_service.dart'; import 'package:querya_desktop/core/layout/vertical_split_pane.dart'; import 'package:querya_desktop/core/storage/app_settings.dart'; @@ -237,78 +240,135 @@ class _MysqlSqlWorkspaceState extends material.State { return v.toInt(); } + Future _openSqlFile() async { + try { + final file = await openFile( + acceptedTypeGroups: const [ + XTypeGroup( + label: 'SQL query', + extensions: ['sql'], + ), + ], + ); + if (file == null) return; + final text = await file.readAsString(); + if (!mounted) return; + _sqlController.value = material.TextEditingValue( + text: text, + selection: material.TextSelection.collapsed(offset: text.length), + ); + } catch (_) {} + } + + Future _saveSqlFile() async { + try { + final name = 'query_${DateTime.now().toIso8601String().replaceAll(':', '-')}.sql'; + final location = await getSaveLocation( + acceptedTypeGroups: const [ + XTypeGroup(label: 'SQL', extensions: ['sql']), + ], + suggestedName: name, + ); + final path = location?.path; + if (path == null || path.isEmpty) return; + await File(path).writeAsString(_sqlController.text); + } catch (_) {} + } + @override material.Widget build(material.BuildContext context) { final theme = Theme.of(context); - return material.CallbackShortcuts( - bindings: { - const material.SingleActivator(LogicalKeyboardKey.f5): () { - if (!_running) { - unawaited(_execute()); - } - }, + return Actions( + actions: >{ + NewSqlIntent: CallbackAction( + onInvoke: (intent) { + _sqlController.clear(); + return null; + }, + ), + OpenSqlIntent: CallbackAction( + onInvoke: (intent) { + unawaited(_openSqlFile()); + return null; + }, + ), + SaveSqlIntent: CallbackAction( + onInvoke: (intent) { + unawaited(_saveSqlFile()); + return null; + }, + ), }, - child: material.Focus( - autofocus: true, - child: VerticalSplitPane( - fraction: _topFraction, - maxFraction: 0.85, - top: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - _MysqlSqlToolbar( - onExecute: _running ? null : _execute, - running: _running, - queryTimeoutSeconds: _queryTimeoutSeconds, - onQueryTimeoutChanged: _onStmtTimeoutChanged, - onOpenPreferences: () => showPreferencesDialog(context), - onOpenHistory: widget.connectionRow.id != null && !_running - ? () { - showSqlQueryHistoryDialog( - context: context, - connectionId: widget.connectionRow.id!, - databaseName: widget.connectionRow.databaseName, - sqlController: _sqlController, - ); - } - : null, - ), - const Divider(height: 1), - Expanded( - child: QueryEditorTab( - controller: _sqlController, - fontSize: _editorFontSize, + child: material.CallbackShortcuts( + bindings: { + const material.SingleActivator(LogicalKeyboardKey.f5): () { + if (!_running) { + unawaited(_execute()); + } + }, + }, + child: material.Focus( + autofocus: true, + child: VerticalSplitPane( + fraction: _topFraction, + maxFraction: 0.85, + top: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _MysqlSqlToolbar( + onExecute: _running ? null : _execute, + running: _running, + queryTimeoutSeconds: _queryTimeoutSeconds, + onQueryTimeoutChanged: _onStmtTimeoutChanged, + onOpenPreferences: () => showPreferencesDialog(context), + onOpenHistory: widget.connectionRow.id != null && !_running + ? () { + showSqlQueryHistoryDialog( + context: context, + connectionId: widget.connectionRow.id!, + databaseName: widget.connectionRow.databaseName, + sqlController: _sqlController, + ); + } + : null, ), - ), - ], - ), - bottom: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - material.Container( - constraints: const material.BoxConstraints(minHeight: 44), - padding: const material.EdgeInsets.symmetric( - horizontal: 12, + const Divider(height: 1), + Expanded( + child: QueryEditorTab( + controller: _sqlController, + fontSize: _editorFontSize, + ), ), - decoration: material.BoxDecoration( - color: theme.colorScheme.muted.withValues(alpha: 0.6), + ], + ), + bottom: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + material.Container( + constraints: const material.BoxConstraints(minHeight: 44), + padding: const material.EdgeInsets.symmetric( + horizontal: 12, + ), + decoration: material.BoxDecoration( + color: theme.colorScheme.muted.withValues(alpha: 0.6), + ), + alignment: material.Alignment.centerLeft, + child: const Text('Data Output').semiBold().small(), ), - alignment: material.Alignment.centerLeft, - child: const Text('Data Output').semiBold().small(), - ), - const Divider(height: 1), - Expanded( - child: ResultsTab( - columns: _columns, - rows: _rows, - errorMessage: _error, - isLoading: _running, - affectedRows: _affectedRows, - statusLine: _statusLine, + const Divider(height: 1), + Expanded( + child: ResultsTab( + columns: _columns, + rows: _rows, + errorMessage: _error, + isLoading: _running, + affectedRows: _affectedRows, + statusLine: _statusLine, + ), ), - ), - ], + ], + ), ), ), ), diff --git a/lib/features/postgresql/postgres_sql_workspace.dart b/lib/features/postgresql/postgres_sql_workspace.dart index d72b79a9..f6ecb83f 100644 --- a/lib/features/postgresql/postgres_sql_workspace.dart +++ b/lib/features/postgresql/postgres_sql_workspace.dart @@ -1,7 +1,10 @@ import 'dart:async'; +import 'dart:io'; import 'package:flutter/material.dart' as material; import 'package:flutter/services.dart' show LogicalKeyboardKey; +import 'package:file_selector/file_selector.dart'; +import 'package:querya_desktop/core/actions/sql_editor_actions.dart'; import 'package:postgres/postgres.dart' as pg; import 'package:querya_desktop/core/database/postgres_service.dart'; import 'package:querya_desktop/core/database/postgres_sql.dart'; @@ -377,84 +380,141 @@ class _PostgresSqlWorkspaceState extends material.State { return v.toString(); } + Future _openSqlFile() async { + try { + final file = await openFile( + acceptedTypeGroups: const [ + XTypeGroup( + label: 'SQL query', + extensions: ['sql'], + ), + ], + ); + if (file == null) return; + final text = await file.readAsString(); + if (!mounted) return; + _sqlController.value = material.TextEditingValue( + text: text, + selection: material.TextSelection.collapsed(offset: text.length), + ); + } catch (_) {} + } + + Future _saveSqlFile() async { + try { + final name = 'query_${DateTime.now().toIso8601String().replaceAll(':', '-')}.sql'; + final location = await getSaveLocation( + acceptedTypeGroups: const [ + XTypeGroup(label: 'SQL', extensions: ['sql']), + ], + suggestedName: name, + ); + final path = location?.path; + if (path == null || path.isEmpty) return; + await File(path).writeAsString(_sqlController.text); + } catch (_) {} + } + @override material.Widget build(material.BuildContext context) { final theme = Theme.of(context); - return material.CallbackShortcuts( - bindings: { - const material.SingleActivator(LogicalKeyboardKey.f5): () { - if (!_running) _execute(); - }, + return Actions( + actions: >{ + NewSqlIntent: CallbackAction( + onInvoke: (intent) { + _sqlController.clear(); + return null; + }, + ), + OpenSqlIntent: CallbackAction( + onInvoke: (intent) { + unawaited(_openSqlFile()); + return null; + }, + ), + SaveSqlIntent: CallbackAction( + onInvoke: (intent) { + unawaited(_saveSqlFile()); + return null; + }, + ), }, - child: material.Focus( - autofocus: true, - child: VerticalSplitPane( - fraction: _topFraction, - maxFraction: 0.85, - top: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - _SqlToolbar( - sessionDatabase: _effectiveSessionDatabase(), - onExecute: _running ? null : _execute, - running: _running, - autocommit: _autocommit, - onAutocommitChanged: (v) => setState(() => _autocommit = v), - queryTimeoutSeconds: _queryTimeoutSeconds, - onQueryTimeoutChanged: _onStmtTimeoutChanged, - onOpenPreferences: () => showPreferencesDialog(context), - onOpenHistory: widget.connectionRow.id != null && !_running - ? () { - showSqlQueryHistoryDialog( - context: context, - connectionId: widget.connectionRow.id!, - databaseName: _effectiveSessionDatabase(), - sqlController: _sqlController, - ); - } - : null, - txOpen: _txOpen, - onBegin: _running ? null : () => _runTxCommand('BEGIN'), - onCommit: _running ? null : () => _runTxCommand('COMMIT'), - onRollback: - _running ? null : () => _runTxCommand('ROLLBACK'), - ), - const Divider(height: 1), - Expanded( - child: QueryEditorTab( - controller: _sqlController, - fontSize: _editorFontSize, + child: material.CallbackShortcuts( + bindings: { + const material.SingleActivator(LogicalKeyboardKey.f5): () { + if (!_running) _execute(); + }, + }, + child: material.Focus( + autofocus: true, + child: VerticalSplitPane( + fraction: _topFraction, + maxFraction: 0.85, + top: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _SqlToolbar( + sessionDatabase: _effectiveSessionDatabase(), + onExecute: _running ? null : _execute, + running: _running, + autocommit: _autocommit, + onAutocommitChanged: (v) => setState(() => _autocommit = v), + queryTimeoutSeconds: _queryTimeoutSeconds, + onQueryTimeoutChanged: _onStmtTimeoutChanged, + onOpenPreferences: () => showPreferencesDialog(context), + onOpenHistory: widget.connectionRow.id != null && !_running + ? () { + showSqlQueryHistoryDialog( + context: context, + connectionId: widget.connectionRow.id!, + databaseName: _effectiveSessionDatabase(), + sqlController: _sqlController, + ); + } + : null, + txOpen: _txOpen, + onBegin: _running ? null : () => _runTxCommand('BEGIN'), + onCommit: _running ? null : () => _runTxCommand('COMMIT'), + onRollback: + _running ? null : () => _runTxCommand('ROLLBACK'), ), - ), - ], - ), - bottom: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - material.Container( - constraints: const material.BoxConstraints(minHeight: 44), - padding: const material.EdgeInsets.symmetric( - horizontal: 12, + const Divider(height: 1), + Expanded( + child: QueryEditorTab( + controller: _sqlController, + fontSize: _editorFontSize, + ), ), - decoration: material.BoxDecoration( - color: theme.colorScheme.muted.withValues(alpha: 0.6), + ], + ), + bottom: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + material.Container( + constraints: const material.BoxConstraints(minHeight: 44), + padding: const material.EdgeInsets.symmetric( + horizontal: 12, + ), + decoration: material.BoxDecoration( + color: theme.colorScheme.muted.withValues(alpha: 0.6), + ), + alignment: material.Alignment.centerLeft, + child: const Text('Data Output').semiBold().small(), ), - alignment: material.Alignment.centerLeft, - child: const Text('Data Output').semiBold().small(), - ), - const Divider(height: 1), - Expanded( - child: ResultsTab( - columns: _columns, - rows: _rows, - errorMessage: _error, - isLoading: _running, - affectedRows: _affectedRows, - statusLine: _statusLine, + const Divider(height: 1), + Expanded( + child: ResultsTab( + columns: _columns, + rows: _rows, + errorMessage: _error, + isLoading: _running, + affectedRows: _affectedRows, + statusLine: _statusLine, + ), ), - ), - ], + ], + ), ), ), ), diff --git a/lib/features/sqlite/sqlite_sql_workspace.dart b/lib/features/sqlite/sqlite_sql_workspace.dart index b2449b70..45d38d52 100644 --- a/lib/features/sqlite/sqlite_sql_workspace.dart +++ b/lib/features/sqlite/sqlite_sql_workspace.dart @@ -1,6 +1,9 @@ import 'dart:async'; +import 'dart:io'; import 'package:flutter/material.dart' as material; import 'package:flutter/services.dart' show LogicalKeyboardKey; +import 'package:file_selector/file_selector.dart'; +import 'package:querya_desktop/core/actions/sql_editor_actions.dart'; import 'package:querya_desktop/core/database/sqlite_service.dart'; import 'package:querya_desktop/core/layout/vertical_split_pane.dart'; import 'package:querya_desktop/core/storage/app_settings.dart'; @@ -182,76 +185,133 @@ class _SqliteSqlWorkspaceState extends material.State { } } + Future _openSqlFile() async { + try { + final file = await openFile( + acceptedTypeGroups: const [ + XTypeGroup( + label: 'SQL query', + extensions: ['sql'], + ), + ], + ); + if (file == null) return; + final text = await file.readAsString(); + if (!mounted) return; + _sqlController.value = material.TextEditingValue( + text: text, + selection: material.TextSelection.collapsed(offset: text.length), + ); + } catch (_) {} + } + + Future _saveSqlFile() async { + try { + final name = 'query_${DateTime.now().toIso8601String().replaceAll(':', '-')}.sql'; + final location = await getSaveLocation( + acceptedTypeGroups: const [ + XTypeGroup(label: 'SQL', extensions: ['sql']), + ], + suggestedName: name, + ); + final path = location?.path; + if (path == null || path.isEmpty) return; + await File(path).writeAsString(_sqlController.text); + } catch (_) {} + } + @override material.Widget build(material.BuildContext context) { final theme = Theme.of(context); - return material.CallbackShortcuts( - bindings: { - const material.SingleActivator(LogicalKeyboardKey.f5): () { - if (!_running) { - unawaited(_execute()); - } - }, + return Actions( + actions: >{ + NewSqlIntent: CallbackAction( + onInvoke: (intent) { + _sqlController.clear(); + return null; + }, + ), + OpenSqlIntent: CallbackAction( + onInvoke: (intent) { + unawaited(_openSqlFile()); + return null; + }, + ), + SaveSqlIntent: CallbackAction( + onInvoke: (intent) { + unawaited(_saveSqlFile()); + return null; + }, + ), }, - child: material.Focus( - autofocus: true, - child: VerticalSplitPane( - fraction: _topFraction, - maxFraction: 0.85, - top: material.Column( - crossAxisAlignment: material.CrossAxisAlignment.stretch, - children: [ - _SqliteSqlToolbar( - onExecute: _running ? null : _execute, - running: _running, - onOpenPreferences: () => showPreferencesDialog(context), - onOpenHistory: widget.connectionRow.id != null && !_running - ? () { - showSqlQueryHistoryDialog( - context: context, - connectionId: widget.connectionRow.id!, - databaseName: widget.connectionRow.databaseName, - sqlController: _sqlController, - ); - } - : null, - ), - const Divider(height: 1), - material.Expanded( - child: QueryEditorTab( - controller: _sqlController, - fontSize: _editorFontSize, + child: material.CallbackShortcuts( + bindings: { + const material.SingleActivator(LogicalKeyboardKey.f5): () { + if (!_running) { + unawaited(_execute()); + } + }, + }, + child: material.Focus( + autofocus: true, + child: VerticalSplitPane( + fraction: _topFraction, + maxFraction: 0.85, + top: material.Column( + crossAxisAlignment: material.CrossAxisAlignment.stretch, + children: [ + _SqliteSqlToolbar( + onExecute: _running ? null : _execute, + running: _running, + onOpenPreferences: () => showPreferencesDialog(context), + onOpenHistory: widget.connectionRow.id != null && !_running + ? () { + showSqlQueryHistoryDialog( + context: context, + connectionId: widget.connectionRow.id!, + databaseName: widget.connectionRow.databaseName, + sqlController: _sqlController, + ); + } + : null, ), - ), - ], - ), - bottom: material.Column( - crossAxisAlignment: material.CrossAxisAlignment.stretch, - children: [ - material.Container( - constraints: const material.BoxConstraints(minHeight: 44), - padding: const material.EdgeInsets.symmetric( - horizontal: 12, + const Divider(height: 1), + material.Expanded( + child: QueryEditorTab( + controller: _sqlController, + fontSize: _editorFontSize, + ), ), - decoration: material.BoxDecoration( - color: theme.colorScheme.muted.withValues(alpha: 0.6), + ], + ), + bottom: material.Column( + crossAxisAlignment: material.CrossAxisAlignment.stretch, + children: [ + material.Container( + constraints: const material.BoxConstraints(minHeight: 44), + padding: const material.EdgeInsets.symmetric( + horizontal: 12, + ), + decoration: material.BoxDecoration( + color: theme.colorScheme.muted.withValues(alpha: 0.6), + ), + alignment: material.Alignment.centerLeft, + child: const Text('Data Output').semiBold().small(), ), - alignment: material.Alignment.centerLeft, - child: const Text('Data Output').semiBold().small(), - ), - const Divider(height: 1), - material.Expanded( - child: ResultsTab( - columns: _columns, - rows: _rows, - errorMessage: _error, - isLoading: _running, - affectedRows: _affectedRows, - statusLine: _statusLine, + const Divider(height: 1), + material.Expanded( + child: ResultsTab( + columns: _columns, + rows: _rows, + errorMessage: _error, + isLoading: _running, + affectedRows: _affectedRows, + statusLine: _statusLine, + ), ), - ), - ], + ], + ), ), ), ), From 7ad8224023436ad142e7381c5e39030402168799 Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Mon, 22 Jun 2026 03:48:34 +0300 Subject: [PATCH 2/2] refactor(menu): remove unnecessary widgets import in title bar --- lib/features/main_screen/querya_window_title_bar.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/features/main_screen/querya_window_title_bar.dart b/lib/features/main_screen/querya_window_title_bar.dart index 77adde20..2bfe0e9d 100644 --- a/lib/features/main_screen/querya_window_title_bar.dart +++ b/lib/features/main_screen/querya_window_title_bar.dart @@ -1,7 +1,6 @@ import 'package:bitsdojo_window/bitsdojo_window.dart'; import 'package:flutter/material.dart' as material show BuildContext, Container, Icon, Icons, MainAxisSize, Widget; -import 'package:flutter/widgets.dart' show Actions, FocusManager; import 'package:querya_desktop/core/theme/querya_theme_scope.dart'; import 'package:querya_desktop/features/connections/driver_manager_dialog.dart'; import 'package:querya_desktop/features/settings/preferences_dialog.dart';