Line Jump / Focus / Cursor Fixes / Home/End Selection bug / Cursor Blink

This commit is contained in:
Sarah Jamie Lewis 2022-10-08 18:24:24 -07:00
parent a5d326b8c3
commit cdbe4d8c9b
8 changed files with 83 additions and 76 deletions

View File

@ -3,6 +3,7 @@ import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
import 'package:subwrite/annotation.dart'; import 'package:subwrite/annotation.dart';
import 'package:subwrite/section.dart'; import 'package:subwrite/section.dart';
@ -19,7 +20,7 @@ class LineFile extends ChangeNotifier {
late String projectPath; late String projectPath;
late String path; late String path;
ScrollController scrollController = ScrollController(); ItemScrollController scrollController = ItemScrollController();
void openFile(String path, int startLine) { void openFile(String path, int startLine) {
this.path = path; this.path = path;
@ -37,7 +38,6 @@ class LineFile extends ChangeNotifier {
parse(); parse();
cursor.moveCursorToDocStart(); cursor.moveCursorToDocStart();
focus.requestFocus(); focus.requestFocus();
scrollController = ScrollController(initialScrollOffset: (startLine * 17.0 - (17.0 * 10.0)).clamp(0.0, double.infinity));
notifyListeners(); notifyListeners();
} }
@ -57,12 +57,12 @@ class LineFile extends ChangeNotifier {
cursor.moveCursor(0, 1, backingLines, keepAnchor: event.isShiftPressed); cursor.moveCursor(0, 1, backingLines, keepAnchor: event.isShiftPressed);
cursor.publishCursor(backingLines); cursor.publishCursor(backingLines);
} else if (event.logicalKey == LogicalKeyboardKey.home) { } else if (event.logicalKey == LogicalKeyboardKey.home) {
cursor.clearSelection();
cursor.moveCursorToLineStart(); cursor.moveCursorToLineStart();
cursor.clearSelection();
cursor.publishCursor(backingLines); cursor.publishCursor(backingLines);
} else if (event.logicalKey == LogicalKeyboardKey.end) { } else if (event.logicalKey == LogicalKeyboardKey.end) {
cursor.clearSelection();
cursor.moveCursorToLineEnd(backingLines[cursor.line - 1].text.length); cursor.moveCursorToLineEnd(backingLines[cursor.line - 1].text.length);
cursor.clearSelection();
cursor.publishCursor(backingLines); cursor.publishCursor(backingLines);
} else if (event.logicalKey == LogicalKeyboardKey.enter) { } else if (event.logicalKey == LogicalKeyboardKey.enter) {
insertLine(); insertLine();
@ -116,7 +116,7 @@ class LineFile extends ChangeNotifier {
void saveFile() { void saveFile() {
String content = ''; String content = '';
for (var l in backingLines) { for (var l in backingLines) {
content += l.text + '\n'; content += '${l.text}\n';
} }
File(path).writeAsStringSync(content); File(path).writeAsStringSync(content);
} }
@ -161,7 +161,6 @@ class LineFile extends ChangeNotifier {
// Delete 2nd to n-1 Lines // Delete 2nd to n-1 Lines
for (var line = (ncursor.anchorLine - 1); line >= ncursor.line; line -= 1) { for (var line = (ncursor.anchorLine - 1); line >= ncursor.line; line -= 1) {
print("deleting $line ${backingLines[line].text}");
backingLines.removeAt(line); backingLines.removeAt(line);
} }
renumberFrom(ncursor.line); renumberFrom(ncursor.line);
@ -306,19 +305,11 @@ class LineFile extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
String base64() {
String content = '';
backingLines.forEach((l) {
content += l.text + '\n';
});
return base64Encode(utf8.encode(content));
}
void copy() { void copy() {
var ncursor = cursor.normalized(); var ncursor = cursor.normalized();
var firstLine = backingLines[ncursor.line - 1].text.substring(ncursor.column) + "\n"; var firstLine = "${backingLines[ncursor.line - 1].text.substring(ncursor.column)}\n";
for (var line = ncursor.line + 1; line < ncursor.anchorLine; line++) { for (var line = ncursor.line + 1; line < ncursor.anchorLine; line++) {
firstLine += backingLines[line - 1].text + "\n"; firstLine += "${backingLines[line - 1].text}\n";
} }
var textToCopy = firstLine; var textToCopy = firstLine;
if (ncursor.anchorLine != ncursor.line) { if (ncursor.anchorLine != ncursor.line) {
@ -345,6 +336,7 @@ class LineFile extends ChangeNotifier {
} }
void mouseDownUpdate(int line, int col) { void mouseDownUpdate(int line, int col) {
focus.requestFocus();
cursor.mouseDownUpdate(line, col, backingLines); cursor.mouseDownUpdate(line, col, backingLines);
} }

View File

@ -13,18 +13,14 @@ class InputListener extends StatefulWidget {
} }
class _InputListener extends State<InputListener> { class _InputListener extends State<InputListener> {
late FocusNode focusNode;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
focusNode = FocusNode();
} }
@override @override
void dispose() { void dispose() {
super.dispose(); super.dispose();
focusNode.dispose();
} }
void newTextDialog(context, String title, String hint, Function(String) callback) { void newTextDialog(context, String title, String hint, Function(String) callback) {
@ -98,16 +94,11 @@ class _InputListener extends State<InputListener> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (!focusNode.hasFocus) {
focusNode.requestFocus();
}
LineFile doc = Provider.of<LineFile>(context); LineFile doc = Provider.of<LineFile>(context);
return GestureDetector( return GestureDetector(
child: Focus( child: Focus(
child: widget.child, focusNode: doc.focus,
focusNode: focusNode,
autofocus: true, autofocus: true,
onKey: (FocusNode node, RawKeyEvent event) { onKey: (FocusNode node, RawKeyEvent event) {
doc.handleKey(event); doc.handleKey(event);
@ -115,6 +106,7 @@ class _InputListener extends State<InputListener> {
newTextDialog(context, "New Note", "Note", (note) => {doc.addNote(note)}); newTextDialog(context, "New Note", "Note", (note) => {doc.addNote(note)});
} }
return KeyEventResult.handled; return KeyEventResult.handled;
})); },
child: widget.child));
} }
} }

View File

@ -1,3 +1,5 @@
import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:subwrite/theme.dart'; import 'package:subwrite/theme.dart';
@ -9,6 +11,7 @@ import 'highlighter.dart';
double gutterWidth = 64; double gutterWidth = 64;
double rightMargin = 32; double rightMargin = 32;
double leftMargin = 5.0;
class LineInfo extends ChangeNotifier { class LineInfo extends ChangeNotifier {
String path; String path;
@ -17,11 +20,12 @@ class LineInfo extends ChangeNotifier {
int? cursorStart; int? cursorStart;
int? cursorEnd; int? cursorEnd;
bool lineSelected = false; bool lineSelected = false;
List<LineAnnotation> annotations = List.empty(growable: true); List<LineAnnotation> annotations = List.empty(growable: true);
LineInfo(this.path, this.lineNumber, this.text); LineInfo(this.path, this.lineNumber, this.text);
Widget build(BuildContext context) { Widget build(BuildContext context, AnimationController animationController) {
bool highlightRisk = false; bool highlightRisk = false;
var backgroundStyle = TextStyle(backgroundColor: cursorStart != null ? sidebarHighlight : background); var backgroundStyle = TextStyle(backgroundColor: cursorStart != null ? sidebarHighlight : background);
@ -41,11 +45,11 @@ class LineInfo extends ChangeNotifier {
); );
List<Widget> stackElements = List.empty(growable: true); List<Widget> stackElements = List.empty(growable: true);
stackElements.add(highlightedLine);
var charWidth = (fontSize / 2.0); var charWidth = (fontSize / 2.0);
var lineHeight = (fontSize * 1.2); var lineHeight = (fontSize * 1.2);
var usableSize = (constraints.maxWidth - gutterWidth - rightMargin); var usableSize = (constraints.maxWidth - gutterWidth - rightMargin - leftMargin);
var sidebarContent = List<Widget>.empty(growable: true); var sidebarContent = List<Widget>.empty(growable: true);
sidebarContent.add(Text('$lineNumber'.padLeft(5), style: gutterStyle)); sidebarContent.add(Text('$lineNumber'.padLeft(5), style: gutterStyle));
@ -55,35 +59,47 @@ class LineInfo extends ChangeNotifier {
return Padding(padding: EdgeInsets.symmetric(horizontal: 5.0, vertical: 0.0), child: Tooltip(message: e.description!, child: e.getIcon(15.0))); return Padding(padding: EdgeInsets.symmetric(horizontal: 5.0, vertical: 0.0), child: Tooltip(message: e.description!, child: e.getIcon(15.0)));
}).toList(); }).toList();
if (icons != null) { sidebarContent.addAll(icons);
sidebarContent.addAll(icons);
}
icons = annotations.where((element) => element.annotationType == AnnotationType.todo).map((e) { icons = annotations.where((element) => element.annotationType == AnnotationType.todo).map((e) {
highlightRisk = true; highlightRisk = true;
return Padding(padding: EdgeInsets.symmetric(horizontal: 5.0, vertical: 0.0), child: e.getIcon(15.0)); return Padding(padding: EdgeInsets.symmetric(horizontal: 5.0, vertical: 0.0), child: e.getIcon(15.0));
}).toList(); }).toList();
if (icons != null) { sidebarContent.addAll(icons);
sidebarContent.addAll(icons);
} var idealHeight = (lineHeight * (1.0 + ((text.length * charWidth) / usableSize).floor())).ceilToDouble();
var highlightedLineContainer = Container(
padding: EdgeInsets.only(right: 0, top: 0, bottom: 0, left: 0),
margin: EdgeInsets.only(right: rightMargin, top: 0, bottom: 0, left: leftMargin),
decoration: BoxDecoration(
color: backgroundStyle.backgroundColor,
),
width: constraints.maxWidth - rightMargin - gutterWidth - leftMargin,
height: idealHeight,
child: highlightedLine,
);
stackElements.add(highlightedLineContainer);
if (cursorStart != null) { if (cursorStart != null) {
var maxCharsBeforeWrapping = (usableSize / charWidth).floorToDouble(); var maxCharsBeforeWrapping = (usableSize / charWidth).floorToDouble();
var cursorPosTop = (cursorStart! / maxCharsBeforeWrapping).floorToDouble() * lineHeight; var cursorPosTop = (cursorStart! / maxCharsBeforeWrapping).floorToDouble() * lineHeight;
var cursorPosLeft = ((cursorStart! % maxCharsBeforeWrapping) * charWidth) - charWidth / 2.0; var cursorPosLeft = ((cursorStart! % maxCharsBeforeWrapping) * charWidth) - charWidth / 2.0;
TextStyle cusrsorStyle = TextStyle(fontFamily: 'Iosevka', fontSize: fontSize, color: foreground, backgroundColor: Colors.transparent, letterSpacing: -2.0); TextStyle cursorStyle = TextStyle(fontFamily: 'Iosevka', fontSize: fontSize, color: foreground, backgroundColor: Colors.transparent, letterSpacing: -2.0);
var cursorPos = Positioned(top: cursorPosTop.toDouble(), left: cursorPosLeft.toDouble(), child: Text("|", style: cusrsorStyle)); var cursorPos = Positioned(top: cursorPosTop.toDouble(), left: leftMargin+cursorPosLeft.toDouble(), child: FadeTransition(
stackElements.add(cursorPos); opacity: animationController, child: Text("|", style: cursorStyle)));
stackElements.add( cursorPos);
} }
var idealHeight = (lineHeight * (1.0 + ((text.length * charWidth) / usableSize).floor())).ceilToDouble();
var cursorOverlay = Stack(children: stackElements); var cursorOverlay = Stack(children: stackElements);
return Listener( return Listener(
onPointerDown: (event) { onPointerDown: (event) {
//var c = (event.localPosition.dx - gutterWidth) / (fontSize / 2.0);
var inMargin = (event.localPosition.dx - gutterWidth) > usableSize; var inMargin = (event.localPosition.dx - gutterWidth) > usableSize;
var c = cursorFromMouse(usableSize, event.localPosition.dx - gutterWidth, event.localPosition.dy); var c = cursorFromMouse(usableSize, event.localPosition.dx - gutterWidth, event.localPosition.dy);
var file = Provider.of<LineFile>(context, listen: false); var file = Provider.of<LineFile>(context, listen: false);
@ -92,7 +108,6 @@ class LineInfo extends ChangeNotifier {
} }
}, },
onPointerMove: (event) { onPointerMove: (event) {
//var c = (event.localPosition.dx - gutterWidth) / (fontSize / 2.0)
var inMargin = (event.localPosition.dx - gutterWidth) > usableSize; var inMargin = (event.localPosition.dx - gutterWidth) > usableSize;
var c = cursorFromMouse(usableSize, event.localPosition.dx - gutterWidth, event.localPosition.dy); var c = cursorFromMouse(usableSize, event.localPosition.dx - gutterWidth, event.localPosition.dy);
var file = Provider.of<LineFile>(context, listen: false); var file = Provider.of<LineFile>(context, listen: false);
@ -104,7 +119,6 @@ class LineInfo extends ChangeNotifier {
}, },
child: MouseRegion( child: MouseRegion(
onEnter: (event) { onEnter: (event) {
//var c = (event.localPosition.dx - gutterWidth) / (fontSize / 2.0);
var inMargin = (event.localPosition.dx - gutterWidth) > usableSize; var inMargin = (event.localPosition.dx - gutterWidth) > usableSize;
var c = cursorFromMouse(usableSize, event.localPosition.dx - gutterWidth, event.localPosition.dy); var c = cursorFromMouse(usableSize, event.localPosition.dx - gutterWidth, event.localPosition.dy);
var file = Provider.of<LineFile>(context, listen: false); var file = Provider.of<LineFile>(context, listen: false);
@ -115,7 +129,6 @@ class LineInfo extends ChangeNotifier {
} }
}, },
onHover: (event) { onHover: (event) {
//var c = (event.localPosition.dx - gutterWidth) / (fontSize / 2.0);
var c = cursorFromMouse(usableSize, event.localPosition.dx - gutterWidth, event.localPosition.dy); var c = cursorFromMouse(usableSize, event.localPosition.dx - gutterWidth, event.localPosition.dy);
var file = Provider.of<LineFile>(context, listen: false); var file = Provider.of<LineFile>(context, listen: false);
if (event.down) { if (event.down) {
@ -145,16 +158,7 @@ class LineInfo extends ChangeNotifier {
margin: EdgeInsets.only(right: 1, top: 0, bottom: 0, left: 0), margin: EdgeInsets.only(right: 1, top: 0, bottom: 0, left: 0),
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: Flex(direction: Axis.horizontal, mainAxisAlignment: MainAxisAlignment.start, children: sidebarContent)), child: Flex(direction: Axis.horizontal, mainAxisAlignment: MainAxisAlignment.start, children: sidebarContent)),
Container( cursorOverlay,
padding: EdgeInsets.zero,
margin: EdgeInsets.only(right: rightMargin, top: 0, bottom: 0, left: 0),
decoration: BoxDecoration(
color: backgroundStyle.backgroundColor,
),
width: constraints.maxWidth - rightMargin - gutterWidth,
height: idealHeight,
child: cursorOverlay,
),
])))); ]))));
}, },
); );

View File

@ -8,7 +8,9 @@ import 'outline.dart';
void main(List<String> args) { void main(List<String> args) {
var docfile = LineFile(); var docfile = LineFile();
docfile.openFile("./example.md", 0); //if (args.length > 1) {
docfile.openFile("./example.md", 0);
//}
var doc = ChangeNotifierProvider.value(value: docfile); var doc = ChangeNotifierProvider.value(value: docfile);
runApp(MultiProvider( runApp(MultiProvider(
@ -26,7 +28,11 @@ class MyApp extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MaterialApp( return MaterialApp(
title: 'subwrite', title: 'subwrite',
theme: ThemeData(scrollbarTheme: ScrollbarThemeData(thumbColor: MaterialStateProperty.all(header))), theme: ThemeData(
scrollbarTheme: ScrollbarThemeData(
thumbColor: MaterialStateProperty.all(header),
thumbVisibility: MaterialStateProperty.all(true),
)),
home: const SarahDownApp(), home: const SarahDownApp(),
); );
} }

View File

@ -62,7 +62,7 @@ class _Outline extends State<Outline> {
style: TextStyle(color: foreground, fontSize: 14.0 - sections[index].level, fontFamily: "Iosevka"), style: TextStyle(color: foreground, fontSize: 14.0 - sections[index].level, fontFamily: "Iosevka"),
), ),
onTap: () { onTap: () {
Provider.of<LineFile>(context, listen: false).scrollController.jumpTo(sections[index].lineNumber * 17.0); Provider.of<LineFile>(context, listen: false).scrollController.jumpTo(index: sections[index].lineNumber - 1);
}, },
trailing: ReorderableDragStartListener( trailing: ReorderableDragStartListener(
index: index, index: index,

View File

@ -1,23 +1,32 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
import 'file.dart'; import 'file.dart';
import 'line_info.dart'; import 'line_info.dart';
class View extends StatefulWidget { class View extends StatefulWidget {
View({Key? key}) : super(key: key); View({Key? key}) : super(key: key);
@override @override
_View createState() => _View(); _View createState() => _View();
} }
class _View extends State<View> { class _View extends State<View> with SingleTickerProviderStateMixin {
late AnimationController _animationController;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_animationController = AnimationController(vsync: this, duration: const Duration(milliseconds: 300));
_animationController.repeat(reverse: true);
} }
@override @override
void dispose() { void dispose() {
_animationController.dispose();
super.dispose(); super.dispose();
} }
@ -25,22 +34,18 @@ class _View extends State<View> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
LineFile doc = Provider.of<LineFile>(context); LineFile doc = Provider.of<LineFile>(context);
return Scrollbar( return ScrollablePositionedList.builder(
controller: doc.scrollController, physics: const AlwaysScrollableScrollPhysics(),
trackVisibility: true, itemScrollController: doc.scrollController,
thumbVisibility: true, itemCount: doc.lines(),
child: ListView.builder( padding: EdgeInsets.zero,
physics: const AlwaysScrollableScrollPhysics(), shrinkWrap: true,
controller: doc.scrollController, itemBuilder: (BuildContext bcontext, int index) {
itemCount: doc.lines(), return ChangeNotifierProvider.value(
padding: EdgeInsets.zero, value: doc.backingLines[index],
shrinkWrap: true, builder: (lcontext, lineinfo) {
itemBuilder: (BuildContext bcontext, int index) { return Provider.of<LineInfo>(lcontext).build(context,_animationController);
return ChangeNotifierProvider.value( });
value: doc.backingLines[index], });
builder: (lcontext, lineinfo) {
return Provider.of<LineInfo>(lcontext).build(lcontext);
});
}));
} }
} }

View File

@ -116,6 +116,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "6.0.3" version: "6.0.3"
scrollable_positioned_list:
dependency: "direct main"
description:
name: scrollable_positioned_list
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.5"
sky_engine: sky_engine:
dependency: transitive dependency: transitive
description: flutter description: flutter
@ -172,4 +179,4 @@ packages:
version: "2.1.2" version: "2.1.2"
sdks: sdks:
dart: ">=2.18.0 <3.0.0" dart: ">=2.18.0 <3.0.0"
flutter: ">=1.16.0" flutter: ">=2.12.0"

View File

@ -37,6 +37,7 @@ dependencies:
# Use with the CupertinoIcons class for iOS style icons. # Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.2 cupertino_icons: ^1.0.2
provider: ^6.0.2 provider: ^6.0.2
scrollable_positioned_list: ^0.3.2
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: