Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a simulated DevTools environment for developing extensions #6251

Merged
merged 8 commits into from
Aug 25, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ class _ExtensionIFrameController extends DisposableController
}

@override
void vmServiceConnectionChanged({String? uri}) {
void vmServiceConnectionChanged({required String? uri}) {
_postMessage(
DevToolsExtensionEvent(
DevToolsExtensionEventType.vmServiceConnection,
Expand Down
31 changes: 0 additions & 31 deletions packages/devtools_app/lib/src/shared/common_widgets.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1021,37 +1021,6 @@ class CenteredCircularProgressIndicator extends StatelessWidget {
}
}

/// An extension on [ScrollController] to facilitate having the scrolling widget
/// auto scroll to the bottom on new content.
extension ScrollControllerAutoScroll on ScrollController {
// TODO(devoncarew): We lose dock-to-bottom when we receive content when we're
// off screen.

/// Return whether the view is currently scrolled to the bottom.
bool get atScrollBottom {
final pos = position;
return pos.pixels == pos.maxScrollExtent;
}

/// Scroll the content to the bottom using the app's default animation
/// duration and curve..
Future<void> autoScrollToBottom() async {
await animateTo(
position.maxScrollExtent,
duration: rapidDuration,
curve: defaultCurve,
);

// Scroll again if we've received new content in the interim.
if (hasClients) {
final pos = position;
if (pos.pixels != pos.maxScrollExtent) {
jumpTo(pos.maxScrollExtent);
}
}
}
}

/// An extension on [LinkedScrollControllerGroup] to facilitate having the
/// scrolling widgets auto scroll to the bottom on new content.
///
Expand Down
31 changes: 31 additions & 0 deletions packages/devtools_app_shared/lib/src/ui/common.dart
Original file line number Diff line number Diff line change
Expand Up @@ -713,3 +713,34 @@ final class FormattedJson extends StatelessWidget {
);
}
}

/// An extension on [ScrollController] to facilitate having the scrolling widget
/// auto scroll to the bottom on new content.
extension ScrollControllerAutoScroll on ScrollController {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cool!

// TODO(devoncarew): We lose dock-to-bottom when we receive content when we're
// off screen.

/// Return whether the view is currently scrolled to the bottom.
bool get atScrollBottom {
final pos = position;
return pos.pixels == pos.maxScrollExtent;
}

/// Scroll the content to the bottom using the app's default animation
/// duration and curve..
Future<void> autoScrollToBottom() async {
await animateTo(
position.maxScrollExtent,
duration: rapidDuration,
curve: defaultCurve,
);

// Scroll again if we've received new content in the interim.
if (hasClients) {
final pos = position;
if (pos.pixels != pos.maxScrollExtent) {
jumpTo(pos.maxScrollExtent);
}
}
}
}
4 changes: 4 additions & 0 deletions packages/devtools_app_shared/lib/src/utils/globals.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,7 @@ final Map<Type, Object> globals = <Type, Object>{};
void setGlobal(Type clazz, Object instance) {
globals[clazz] = instance;
}

void removeGlobal(Type clazz) {
globals.remove(clazz);
}
2 changes: 1 addition & 1 deletion packages/devtools_extensions/lib/src/api/api.dart
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ abstract interface class DevToolsExtensionHostInterface {
/// This method should send a [DevToolsExtensionEventType.vmServiceConnection]
/// event to the extension to notify it of the vm service uri it should
/// establish a connection to.
void vmServiceConnectionChanged({String? uri});
void vmServiceConnectionChanged({required String? uri});

/// Handles events sent by the extension.
///
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,20 @@
part of '_simulated_devtools_environment.dart';

class _SimulatedDevToolsController extends DisposableController
with AutoDisposeControllerMixin
implements DevToolsExtensionHostInterface {
/// Logs of the post message communication that goes back and forth between
/// the extension and the simulated DevTools environment.
final messageLogs = ListValueNotifier<_PostMessageLogEntry>([]);

void init() {
html.window.addEventListener('message', _handleMessage);
addAutoDisposeListener(serviceManager.connectedState, () {
if (!serviceManager.connectedState.value.connected) {
vmServiceConnectionChanged(uri: null);
messageLogs.clear();
}
});
}

void _handleMessage(html.Event e) {
Expand Down Expand Up @@ -41,13 +48,13 @@ class _SimulatedDevToolsController extends DisposableController
}

@override
void vmServiceConnectionChanged({String? uri}) {
uri = 'http://127.0.0.1:60851/fH-kAEXc7MQ=/';
void vmServiceConnectionChanged({required String? uri}) {
// TODO(kenz): add some validation and error handling if [uri] is bad input.
final normalizedUri = normalizeVmServiceUri(uri!);
final normalizedUri =
uri != null ? normalizeVmServiceUri(uri).toString() : null;
final event = DevToolsExtensionEvent(
DevToolsExtensionEventType.vmServiceConnection,
data: {'uri': normalizedUri.toString()},
data: {'uri': normalizedUri},
);
_postMessageToExtension(event);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
// found in the LICENSE file.

// ignore: avoid_web_libraries_in_flutter, as designed
import 'dart:async';
import 'dart:html' as html;

import 'package:devtools_app_shared/service.dart';
import 'package:devtools_app_shared/ui.dart';
import 'package:devtools_app_shared/utils.dart';
import 'package:devtools_shared/devtools_shared.dart';
Expand All @@ -29,25 +31,40 @@ class SimulatedDevToolsWrapper extends StatefulWidget {
const SimulatedDevToolsWrapper({
super.key,
required this.child,
required this.connected,
});

final Widget child;

final bool connected;

@override
State<SimulatedDevToolsWrapper> createState() =>
_SimulatedDevToolsWrapperState();
}

class _SimulatedDevToolsWrapperState extends State<SimulatedDevToolsWrapper> {
class _SimulatedDevToolsWrapperState extends State<SimulatedDevToolsWrapper>
with AutoDisposeMixin {
late final _SimulatedDevToolsController simController;

late ConnectedState connectionState;
kenzieschmoll marked this conversation as resolved.
Show resolved Hide resolved

bool get connected => connectionState.connected;

@override
void initState() {
super.initState();
simController = _SimulatedDevToolsController()..init();

connectionState = serviceManager.connectedState.value;
addAutoDisposeListener(serviceManager.connectedState, () {
setState(() {
connectionState = serviceManager.connectedState.value;
});
});
}

@override
void dispose() {
simController.dispose();
super.dispose();
}

@override
Expand Down Expand Up @@ -79,10 +96,10 @@ class _SimulatedDevToolsWrapperState extends State<SimulatedDevToolsWrapper> {
),
const PaddedDivider(),
_VmServiceConnection(
connected: widget.connected,
connected: connected,
simController: simController,
),
if (widget.connected)
if (connected)
Padding(
padding: const EdgeInsets.symmetric(vertical: denseSpacing),
child: _SimulatedApi(simController: simController),
Expand All @@ -92,9 +109,20 @@ class _SimulatedDevToolsWrapperState extends State<SimulatedDevToolsWrapper> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Logs:',
style: theme.textTheme.titleMedium,
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Logs:',
style: theme.textTheme.titleMedium,
),
DevToolsButton.iconOnly(
icon: Icons.clear,
outlined: false,
tooltip: 'Clear logs',
onPressed: () => simController.messageLogs.clear(),
),
],
),
const PaddedDivider.thin(),
Expanded(
Expand Down Expand Up @@ -133,41 +161,62 @@ class _SimulatedApi extends StatelessWidget {
}
}

class _LogMessages extends StatelessWidget {
class _LogMessages extends StatefulWidget {
const _LogMessages({required this.simController});

final _SimulatedDevToolsController simController;

@override
State<_LogMessages> createState() => _LogMessagesState();
}

class _LogMessagesState extends State<_LogMessages> {
final _scrollController = ScrollController();

@override
void dispose() {
_scrollController.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return ValueListenableBuilder(
valueListenable: simController.messageLogs,
valueListenable: widget.simController.messageLogs,
builder: (context, logs, _) {
return ListView.builder(
itemCount: logs.length,
itemBuilder: (context, index) {
final log = logs[index];
Widget logEntry = Padding(
padding: const EdgeInsets.symmetric(vertical: densePadding),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'[${log.timestamp.toString()}] from ${log.source.display}',
style: theme.fixedFontStyle,
),
FormattedJson(
json: log.data,
),
],
),
);
if (index != 0) {
logEntry = OutlineDecoration.onlyTop(child: logEntry);
}
return logEntry;
},
if (_scrollController.hasClients && _scrollController.atScrollBottom) {
unawaited(_scrollController.autoScrollToBottom());
}
return Scrollbar(
controller: _scrollController,
thumbVisibility: true,
child: ListView.builder(
controller: _scrollController,
itemCount: logs.length,
itemBuilder: (context, index) {
final log = logs[index];
Widget logEntry = Padding(
padding: const EdgeInsets.symmetric(vertical: densePadding),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'[${log.timestamp.toString()}] from ${log.source.display}',
style: theme.fixedFontStyle,
),
FormattedJson(
json: log.data,
),
],
),
);
if (index != 0) {
logEntry = OutlineDecoration.onlyTop(child: logEntry);
}
return logEntry;
},
),
);
},
);
Expand Down
Loading
Loading