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

View File

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

View File

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

View File

@ -8,7 +8,9 @@ import 'outline.dart';
void main(List<String> args) {
var docfile = LineFile();
docfile.openFile("./example.md", 0);
//if (args.length > 1) {
docfile.openFile("./example.md", 0);
//}
var doc = ChangeNotifierProvider.value(value: docfile);
runApp(MultiProvider(
@ -26,7 +28,11 @@ class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(
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(),
);
}

View File

@ -62,7 +62,7 @@ class _Outline extends State<Outline> {
style: TextStyle(color: foreground, fontSize: 14.0 - sections[index].level, fontFamily: "Iosevka"),
),
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(
index: index,

View File

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

View File

@ -116,6 +116,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
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:
dependency: transitive
description: flutter
@ -172,4 +179,4 @@ packages:
version: "2.1.2"
sdks:
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.
cupertino_icons: ^1.0.2
provider: ^6.0.2
scrollable_positioned_list: ^0.3.2
dev_dependencies:
flutter_test: