Skip to content

Commit

Permalink
15 Toolbar - Headings dropdown after text size
Browse files Browse the repository at this point in the history
  • Loading branch information
Sebastian Soimaru authored and adrian-moisa committed Aug 26, 2023
1 parent 7d4e80c commit 4ca6bbf
Show file tree
Hide file tree
Showing 8 changed files with 129 additions and 237 deletions.
1 change: 1 addition & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ If you want to learn more about the specs, all tickets are available by followin
- Created `EditableTextPaintService`. `EditableTextLineBoxRenderer` has many overrides concerned with computing the layout dimensions. Therefore the painting logic for selection/highlight boxes is better separated here. Separating the layout dimensions logic and painting logic helps improves readability and maintainability.
- Move code that is closely related to other modules to the respective modules
- Renamed `EditorRendererInner` to `EditorTextAreaRenderer` following the conventions for editable text line.
- Replaced header style buttons with a dropdown [#15](https:/visual-space/visual-editor/issues/15)


## Custom Embeds [#157](https:/visual-space/visual-editor/issues/157)
Expand Down
6 changes: 2 additions & 4 deletions lib/selection/controllers/selection-handles.controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -158,8 +158,7 @@ class SelectionHandlesController {
_context,
rootOverlay: true,
debugRequiredFor: debugRequiredFor,
)
.insert(toolbar!);
).insert(toolbar!);
_toolbarController.forward(from: 0);

// Make sure handles are visible as well
Expand Down Expand Up @@ -232,8 +231,7 @@ class SelectionHandlesController {
_context,
rootOverlay: true,
debugRequiredFor: debugRequiredFor,
)
.insertAll(_handles!);
).insertAll(_handles!);
}

// Causes the overlay to update its rendering.
Expand Down
3 changes: 1 addition & 2 deletions lib/shared/widgets/editor-dropdown.dart
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ class _EditorDropdownState<T> extends State<EditorDropdown<T>> {
height: widget.iconSize * 1.75,
),
child: Container(
padding: EdgeInsets.fromLTRB(8, 0, 8, 0),
padding: EdgeInsets.fromLTRB(8, 0, 4, 0),
child: Row(
mainAxisSize: MainAxisSize.min,
children: children,
Expand Down Expand Up @@ -268,7 +268,6 @@ class _EditorDropdownState<T> extends State<EditorDropdown<T>> {
List<DropDownOptionM<T>> _getSelectedOptionsFromTextSelection() {
final attribute = _attributes?[widget.attribute.key];
var selectedOptions = <DropDownOptionM<T>>[];

// Get the attribute value
if (attribute != null) {
// Custom Value
Expand Down
10 changes: 10 additions & 0 deletions lib/toolbar/models/header-styles.const.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Default header styles used in the editor
// Every style is linked to the suitable attribute
const Map<String, int> HEADER_STYLES = {
'N': 0,
'H1': 1,
'H2': 2,
'H3': 3,
};

const INITIAL_HEADER_STYLE = 0;
191 changes: 0 additions & 191 deletions lib/toolbar/widgets/buttons/select-header-style-buttons.dart
Original file line number Diff line number Diff line change
@@ -1,191 +0,0 @@
import 'dart:async';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

import '../../../controller/controllers/editor-controller.dart';
import '../../../document/models/attributes/attribute.model.dart';
import '../../../document/models/attributes/attributes-aliases.model.dart';
import '../../../document/models/attributes/attributes.model.dart';
import '../../../editor/services/run-build.service.dart';
import '../../../shared/models/editor-icon-theme.model.dart';
import '../../../shared/state/editor-state-receiver.dart';
import '../../../shared/state/editor.state.dart';
import '../../../styles/services/styles.service.dart';
import '../../services/toolbar.service.dart';
import '../toolbar.dart';

// Lists the 3 (currently hardcoded) heading types.
// To be replaced with a dropdown in the future.
// TODO Split in methods
// ignore: must_be_immutable
class SelectHeaderStyleButtons extends StatefulWidget with EditorStateReceiver {
final EditorController controller;
final double iconSize;
final double buttonsSpacing;
final EditorIconThemeM? iconTheme;
late EditorState _state;

SelectHeaderStyleButtons({
required this.controller,
required this.buttonsSpacing,
this.iconSize = defaultIconSize,
this.iconTheme,
Key? key,
}) : super(key: key) {
controller.setStateInEditorStateReceiver(this);
}

@override
_SelectHeaderStyleButtonsState createState() => _SelectHeaderStyleButtonsState();

@override
void cacheStateStore(EditorState state) {
_state = state;
}
}

class _SelectHeaderStyleButtonsState extends State<SelectHeaderStyleButtons> {
late final RunBuildService _runBuildService;
late final ToolbarService _toolbarService;
late final StylesService _stylesService;

AttributeM? _attr;
StreamSubscription? _runBuild$L;

@override
void initState() {
_runBuildService = RunBuildService(widget._state);
_toolbarService = ToolbarService(widget._state);
_stylesService = StylesService(widget._state);

_attr = _getHeaderAttr();

_subscribeToRunBuild();
super.initState();
}

@override
void didUpdateWidget(covariant SelectHeaderStyleButtons oldWidget) {
super.didUpdateWidget(oldWidget);

// If a new controller was generated by setState() in the parent
// we need to subscribe to the new state store.
if (oldWidget.controller != widget.controller) {
_runBuild$L?.cancel();
widget.controller.setStateInEditorStateReceiver(widget);
_subscribeToRunBuild();
_attr = _getHeaderAttr();
}
}

@override
void dispose() {
_runBuild$L?.cancel();
super.dispose();
}

@override
Widget build(BuildContext context) {
final _valueToText = <AttributeM, String>{
AttributesM.header: 'N',
AttributesAliasesM.h1: 'H1',
AttributesAliasesM.h2: 'H2',
AttributesAliasesM.h3: 'H3',
};

final _valueAttribute = <AttributeM>[AttributesM.header, AttributesAliasesM.h1, AttributesAliasesM.h2, AttributesAliasesM.h3];
final _valueString = <String>['N', 'H1', 'H2', 'H3'];

final theme = Theme.of(context);
final style = TextStyle(
fontWeight: FontWeight.w600,
fontSize: widget.iconSize * 0.7,
);

return Row(
mainAxisSize: MainAxisSize.min,
children: List.generate(4, (index) {
return Container(
// ignore: prefer_const_constructors
margin: EdgeInsets.symmetric(
horizontal: !kIsWeb ? 1.0 : widget.buttonsSpacing,
),
child: ConstrainedBox(
constraints: BoxConstraints.tightFor(
width: widget.iconSize * iconButtonFactor,
height: widget.iconSize * iconButtonFactor,
),
child: RawMaterialButton(
hoverElevation: 0,
highlightElevation: 0,
elevation: 0,
visualDensity: VisualDensity.compact,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
widget.iconTheme?.borderRadius ?? 2,
),
),
fillColor: _valueToText[_attr] == _valueString[index]
? (widget.iconTheme?.iconSelectedFillColor ?? theme.toggleableActiveColor)
: (widget.iconTheme?.iconUnselectedFillColor ?? theme.canvasColor),

// Export a nice and clean version of this method in the styles service. Similar to other buttons.
onPressed: isEnabled
? () => _stylesService.formatSelection(
_valueAttribute[index],
)
: null,
child: Text(
_valueString[index],
style: style.copyWith(
color: isEnabled
? _valueToText[_attr] == _valueString[index]
? (widget.iconTheme?.iconSelectedColor ?? theme.primaryIconTheme.color)
: (widget.iconTheme?.iconUnselectedColor ?? theme.iconTheme.color)
: theme.disabledColor,
),
),
),
),
);
}),
);
}

// === PRIVATE ===

bool get isEnabled => _toolbarService.isStylingEnabled;

void _subscribeToRunBuild() {
_runBuild$L = _runBuildService.runBuild$.listen(
(_) => setState(() {
_attr = _getHeaderAttr();
}),
);
}

AttributeM? _getHeaderAttr() {
if (!_documentControllerInitialised) {
return null;
}

final toggler = _toolbarService.getToolbarButtonToggler();
final attribute = toggler[AttributesM.header.key];

if (attribute != null) {
// Checkbox tapping causes text selection to go to offset 0
toggler.remove(AttributesM.header.key);

return attribute;
}

final selectionStyle = _stylesService.getSelectionStyle();

return selectionStyle.attributes[AttributesM.header.key] ?? AttributesM.header;
}

bool get _documentControllerInitialised {
return widget._state.refs.documentControllerInitialised == true;
}
}
94 changes: 94 additions & 0 deletions lib/toolbar/widgets/dropdowns/header-styles-dropdown.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import 'package:flutter/material.dart';

import '../../../controller/controllers/editor-controller.dart';
import '../../../document/models/attributes/attribute.model.dart';
import '../../../document/models/attributes/attributes-aliases.model.dart';
import '../../../document/models/attributes/attributes.model.dart';
import '../../../shared/models/editor-icon-theme.model.dart';
import '../../../shared/state/editor-state-receiver.dart';
import '../../../shared/state/editor.state.dart';
import '../../../shared/widgets/editor-dropdown.dart';
import '../../../styles/services/styles.service.dart';
import '../../models/dropdown-option.model.dart';

// Controls the header style of the currently selected text.
// ignore: must_be_immutable
class HeaderStylesDropdown extends StatelessWidget
with EditorStateReceiver {
late final StylesService _stylesService;

final Map<String, int> headerStyles;
final double iconSize;
final EditorIconThemeM? iconTheme;
final EditorController controller;
int initialHeaderStyleValue;
late List<DropDownOptionM<int>> options;
late EditorState _state;

HeaderStylesDropdown({
required this.headerStyles,
required this.controller,
required this.initialHeaderStyleValue,
this.iconSize = 40,
this.iconTheme,
Key? key,
}) : super(key: key) {
controller.setStateInEditorStateReceiver(this);
_stylesService = StylesService(_state);

options = _mapStylesToDropdownOptions(headerStyles);
}

@override
void cacheStateStore(EditorState state) {
_state = state;
}

@override
Widget build(BuildContext context) => EditorDropdown<int>(
iconTheme: iconTheme,
iconSize: iconSize,
attribute: AttributesM.header,
controller: controller,
options: options,
initialValue: _getInitialHeaderStyle(),
onSelected: (style) => _stylesService.formatSelection(
_getHeaderAttributeByStyle(style.value),
),
);

List<DropDownOptionM<int>> _getInitialHeaderStyle() {
return [
options.firstWhere(
(option) => option.value == initialHeaderStyleValue,
),
];
}

List<DropDownOptionM<int>> _mapStylesToDropdownOptions(
Map<String, int> headerStyles,
) =>
headerStyles.entries
.map(
(style) => DropDownOptionM(
name: style.key,
value: style.value,
),
)
.toList();

AttributeM _getHeaderAttributeByStyle(int size) {
if (size == 1) {
return AttributesAliasesM.h1;
}
if (size == 2) {
return AttributesAliasesM.h2;
}
if (size == 3) {
return AttributesAliasesM.h3;
}

// Normal text
return AttributesM.header;
}
}
5 changes: 0 additions & 5 deletions lib/toolbar/widgets/dropdowns/markers-dropdown.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,6 @@ import '../../../shared/state/editor.state.dart';
import '../../../shared/widgets/editor-dropdown.dart';
import '../../models/dropdown-option.model.dart';

// When the dropdown renders the list we highlight the options selected in the current text selection.
// Most attributes use primitive values such as the sizes dropdown.
// However the markers use a custom data format to store multiple layers in one attribute.
// Therefore we need a custom method to read the attributes values and
// to convert them into selected dropdown options.
// When the dropdown renders the list we highlight the options selected in the current text selection.
// Most attributes use primitive values such as the sizes dropdown.
// However the markers use a custom data format to store multiple layers in one attribute.
Expand Down
Loading

0 comments on commit 4ca6bbf

Please sign in to comment.