Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
17 changes: 17 additions & 0 deletions lib/const/shortcut_info.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ enum IcarusShortcutAction {
switchSide,
switchSideThisPage,
openDeleteMenu,
selectAll,
rotateClockwise,
rotateCounterclockwise,
saveStrategy,
Expand Down Expand Up @@ -304,6 +305,18 @@ class ShortcutInfo {
intent: ContextualDeleteIntent(),
searchAliases: ['delete'],
),
// Taken by CanvasSelectionLayer while the canvas is on screen, beside the
// fixed Delete, Backspace and Escape.
IcarusShortcutDefinition(
action: IcarusShortcutAction.selectAll,
title: 'Select All',
defaultBinding: IcarusKeyBinding(
trigger: LogicalKeyboardKey.keyA,
primary: true,
),
intent: SelectAllIntent(),
searchAliases: ['select'],
),
IcarusShortcutDefinition(
action: IcarusShortcutAction.rotateClockwise,
title: 'Rotate Clockwise',
Expand Down Expand Up @@ -525,6 +538,10 @@ class ContextualDeleteIntent extends Intent {
const ContextualDeleteIntent();
}

class SelectAllIntent extends Intent {
const SelectAllIntent();
}

class RotateHoveredIntent extends Intent {
const RotateHoveredIntent({required this.clockwise});

Expand Down
7 changes: 5 additions & 2 deletions lib/hive/hive_adapters.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion lib/hive/hive_adapters.g.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,7 @@ types:
index: 4
AppPreferences:
typeId: 28
nextIndex: 23
nextIndex: 24
fields:
defaultThemeProfileIdForNewStrategies:
index: 0
Expand Down Expand Up @@ -405,6 +405,8 @@ types:
index: 21
transparentVideoBackground:
index: 22
canvasSelectionHintSeen:
index: 23
PlacedViewConeAgent:
typeId: 29
nextIndex: 11
Expand Down
417 changes: 212 additions & 205 deletions lib/interactive_map.dart

Large diffs are not rendered by default.

164 changes: 164 additions & 0 deletions lib/providers/canvas_selection_provider.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:icarus/const/line_provider.dart';
import 'package:icarus/providers/ability_provider.dart';
import 'package:icarus/providers/action_provider.dart';
import 'package:icarus/providers/agent_provider.dart';
import 'package:icarus/providers/collab/strategy_capabilities_provider.dart';
import 'package:icarus/providers/drawing_provider.dart';
import 'package:icarus/providers/image_provider.dart';
import 'package:icarus/providers/interaction_state_provider.dart';
import 'package:icarus/providers/strategy_page_session_provider.dart';
import 'package:icarus/providers/strategy_provider.dart';
import 'package:icarus/providers/text_provider.dart';
import 'package:icarus/providers/utility_provider.dart';

/// The kinds of thing the user can select on the canvas. A lineup is
/// selected as its link: the throw spot and landing come with it.
enum SelectableKind { agent, ability, text, image, utility, drawing, lineup }

/// One selected item on the page on screen.
typedef SelectedItem = ({SelectableKind kind, String id});

/// What the user has selected on the page on screen. Selection belongs to
/// this window only: it is never saved or synced, and it empties when the
/// page, the strategy, or the tool changes. Items that leave the page (an
/// undo, a teammate's delete) leave the selection with them.
final canvasSelectionProvider =
NotifierProvider<CanvasSelectionNotifier, Set<SelectedItem>>(
CanvasSelectionNotifier.new,
);

/// The ids of every item drawn as selected: the selected items, plus the
/// throw spots and landings of selected lineups, which are drawn as their
/// own items.
final selectedCanvasIdsProvider = Provider<Set<String>>((ref) {
final selection = ref.watch(canvasSelectionProvider);
if (selection.isEmpty) return const {};
final lineupIds = {
for (final item in selection)
if (item.kind == SelectableKind.lineup) item.id,
};
final links = lineupIds.isEmpty
? const <LineUpLink>[]
: ref.watch(lineUpProvider.select((state) => state.links));
return {
for (final item in selection) item.id,
for (final link in links)
if (lineupIds.contains(link.id)) ...[link.originId, link.landingId],
};
});

class CanvasSelectionNotifier extends Notifier<Set<SelectedItem>> {
@override
Set<SelectedItem> build() {
void clearOnChange<T>(ProviderListenable<T> provider) {
ref.listen<T>(provider, (previous, next) {
if (previous != next) clear();
});
}

clearOnChange(strategyProvider.select((state) => state.strategyId));
clearOnChange(
strategyPageSessionProvider.select((state) => state.activePageId),
);
clearOnChange(interactionStateProvider);
clearOnChange(
currentStrategyCapabilitiesProvider.select(
(capabilities) => capabilities.canEditPages,
),
);

void pruneOnChange<T>(ProviderListenable<T> provider) {
ref.listen<T>(provider, (_, __) => _prune());
}

pruneOnChange(agentProvider);
pruneOnChange(abilityProvider);
pruneOnChange(textProvider);
pruneOnChange(placedImageProvider.select((state) => state.images));
pruneOnChange(utilityProvider);
pruneOnChange(drawingProvider.select((state) => state.elements));
pruneOnChange(lineUpProvider.select((state) => state.links));
return const {};
}

/// Every item on the page on screen.
Set<SelectedItem> _everything() => {
for (final agent in ref.read(agentProvider))
(kind: SelectableKind.agent, id: agent.id),
for (final ability in ref.read(abilityProvider))
(kind: SelectableKind.ability, id: ability.id),
for (final text in ref.read(textProvider))
(kind: SelectableKind.text, id: text.id),
for (final image in ref.read(placedImageProvider).images)
(kind: SelectableKind.image, id: image.id),
for (final utility in ref.read(utilityProvider))
(kind: SelectableKind.utility, id: utility.id),
for (final drawing in ref.read(drawingProvider).elements)
(kind: SelectableKind.drawing, id: drawing.id),
for (final link in ref.read(lineUpProvider).links)
(kind: SelectableKind.lineup, id: link.id),
};

void _prune() {
if (state.isEmpty) return;
final present = _everything();
final kept = state.where(present.contains).toSet();
if (kept.length != state.length) state = kept;
}

/// Replaces the selection with [items].
void select(Iterable<SelectedItem> items) => state = {...items};

/// Adds [items] to the selection.
void add(Iterable<SelectedItem> items) => state = {...state, ...items};

/// Selects [item] if it isn't, and unselects it if it is.
void toggle(SelectedItem item) => state =
state.contains(item) ? ({...state}..remove(item)) : {...state, item};

void selectAll() => state = _everything();

void clear() {
if (state.isNotEmpty) state = const {};
}

/// Deletes everything selected as one undo step.
void deleteSelected() {
final selection = state;
if (selection.isEmpty ||
!ref.read(currentStrategyCapabilitiesProvider).canEditPages) {
return;
}
ref.read(actionProvider.notifier).performTransaction(
groups: const [],
mutation: () {
final drawingIds = <String>{};
for (final item in selection) {
switch (item.kind) {
case SelectableKind.agent:
ref.read(agentProvider.notifier).removeAgentAsAction(item.id);
case SelectableKind.ability:
ref.read(abilityProvider.notifier).removeAbilityAsAction(item.id);
case SelectableKind.text:
ref.read(textProvider.notifier).removeTextAsAction(item.id);
case SelectableKind.image:
ref
.read(placedImageProvider.notifier)
.removeImageAsAction(item.id);
case SelectableKind.utility:
ref.read(utilityProvider.notifier).removeUtilityAsAction(item.id);
case SelectableKind.drawing:
drawingIds.add(item.id);
case SelectableKind.lineup:
ref.read(lineUpProvider.notifier).deleteLink(item.id);
}
}
if (drawingIds.isNotEmpty) {
ref.read(drawingProvider.notifier).deleteDrawingsById(drawingIds);
}
},
);
clear();
}
}
9 changes: 9 additions & 0 deletions lib/providers/drawing_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,15 @@ class DrawingProvider extends Notifier<DrawingState> {
_triggerRepaint();
}

/// Deletes the drawings in [ids], each recorded as its own deletion (wrap
/// the call in performTransaction for one undo step).
void deleteDrawingsById(Set<String> ids) {
// Last first, so the indexes still to delete stay where they were.
for (final (index, drawing) in [...state.elements.indexed].reversed) {
if (ids.contains(drawing.id)) deleteDrawing(index);
}
}

void onErase(Offset mousePos) {
if (!ref.canEditPages) return;
final Set<int> indicesToDelete = {
Expand Down
15 changes: 15 additions & 0 deletions lib/providers/user_preferences_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,11 @@ class AppPreferences extends HiveObject {
/// is a lossless .mov for editing, not an MP4 to share.
final bool transparentVideoBackground;

/// Whether this install has said that a drag on the map selects and Space
/// moves the map. Stores written before selection existed read false, so
/// updating users are told once.
final bool canvasSelectionHintSeen;

AppPreferences({
required this.defaultThemeProfileIdForNewStrategies,
this.autosaveEnabled = true,
Expand All @@ -169,6 +174,7 @@ class AppPreferences extends HiveObject {
this.welcomeTourSeen = false,
this.transparentScreenshotBackground = false,
this.transparentVideoBackground = false,
this.canvasSelectionHintSeen = false,
}) : customColorValues = List.unmodifiable(customColorValues ?? const []),
customShortcutBindings =
Map.unmodifiable(customShortcutBindings ?? const {});
Expand Down Expand Up @@ -196,6 +202,7 @@ class AppPreferences extends HiveObject {
bool? welcomeTourSeen,
bool? transparentScreenshotBackground,
bool? transparentVideoBackground,
bool? canvasSelectionHintSeen,
}) {
return AppPreferences(
defaultThemeProfileIdForNewStrategies:
Expand Down Expand Up @@ -232,6 +239,8 @@ class AppPreferences extends HiveObject {
this.transparentScreenshotBackground,
transparentVideoBackground:
transparentVideoBackground ?? this.transparentVideoBackground,
canvasSelectionHintSeen:
canvasSelectionHintSeen ?? this.canvasSelectionHintSeen,
);
}
}
Expand Down Expand Up @@ -736,6 +745,12 @@ class AppPreferencesNotifier extends Notifier<AppPreferences> {
);
}

Future<void> markCanvasSelectionHintSeen() {
return _updatePreferences(
(current) => current.copyWith(canvasSelectionHintSeen: true),
);
}

Future<void> setDrawingDefaults({
int? colorValue,
double? thickness,
Expand Down
Loading
Loading