Skip to content

Commit

Permalink
feat(map,selection): add ability to select multiple systems and delet…
Browse files Browse the repository at this point in the history
…e them in bulk
  • Loading branch information
updraft0 committed Jun 19, 2024
1 parent 0cb2ce7 commit 4c02859
Show file tree
Hide file tree
Showing 12 changed files with 331 additions and 29 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -300,10 +300,14 @@ enum MapRequest derives CanEqual:
name: Option[NewSystemName] = None // TODO: weirdly cannot nest Option[Option[_]]
)

/** Remove a system from the map (this only deletes display data and clears the pinned status)
/** Remove a system from the map
*/
case RemoveSystem(systemId: SystemId)

/** Remove multiple systems from the map
*/
case RemoveSystems(systemIds: Array[SystemId])

/** Remove a connection from the map
*/
case RemoveSystemConnection(connectionId: ConnectionId)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,7 @@ enum MapRequest derives CanEqual:
case UpdateSystemSignatures(systemId: SystemId, replaceAll: Boolean, scanned: List[NewMapSystemSignature])
case RenameSystem(systemId: SystemId, name: Option[String])
case RemoveSystem(systemId: SystemId)
case RemoveSystems(systemIds: Chunk[SystemId])
case RemoveSystemSignatures(systemId: SystemId, signatures: Option[NonEmptyChunk[SigId]])
case RemoveSystemConnection(connectionId: ConnectionId)
// internals
Expand Down Expand Up @@ -387,7 +388,17 @@ object MapEntity extends ReactiveEntity[MapEnv, MapId, MapState, Identified[MapR
)
case Identified(Some(sid), rs: MapRequest.RemoveSystem) =>
whenSystemExists(rs.systemId, state)(
identified(sid, "removeFromDisplay", removeSystemAndConnection(mapId, state, sid, rs))
identified(sid, "removeFromDisplay", removeSystemAndConnection(mapId, state, sid, rs.systemId))
)
case Identified(Some(sid), rss: MapRequest.RemoveSystems) =>
// TODO this could be optimized further
foldLeft(
state,
rss.systemIds,
(systemId, state) =>
whenSystemExists(systemId, state)(
identified(sid, "removeFromDisplay", removeSystemAndConnection(mapId, state, sid, systemId))
)
)
case Identified(Some(sid), rsc: MapRequest.RemoveSystemConnection) =>
identified(sid, "removeSystemConnection", removeSystemConnection(mapId, state, sid, rsc))
Expand Down Expand Up @@ -623,26 +634,26 @@ object MapEntity extends ReactiveEntity[MapEnv, MapId, MapState, Identified[MapR
mapId: MapId,
state: MapState,
sessionId: MapSessionId,
rs: MapRequest.RemoveSystem
systemId: SystemId
) =
val connections = state.connectionsForSystem(rs.systemId)
val connections = state.connectionsForSystem(systemId)
val connectionIds = Chunk.from(connections.valuesIterator.map(_.connection.id))
val otherSystemIds = Chunk.from(connections.valuesIterator.map(_.connection.toSystemId).filter(_ != rs.systemId)) ++
Chunk.from(connections.valuesIterator.map(_.connection.fromSystemId).filter(_ != rs.systemId))
val otherSystemIds = Chunk.from(connections.valuesIterator.map(_.connection.toSystemId).filter(_ != systemId)) ++
Chunk.from(connections.valuesIterator.map(_.connection.fromSystemId).filter(_ != systemId))
for
// mark connections as removed
_ <- query.map.deleteMapWormholeConnections(mapId, connectionIds, sessionId.characterId)
// remove signatures that have those connections
_ <- query.map.deleteSignaturesWithConnectionIds(mapId, connectionIds, sessionId.characterId)
// remove the display of the system
_ <- query.map.deleteMapSystemDisplay(mapId, rs.systemId)
_ <- query.map.deleteMapSystemDisplay(mapId, systemId)
// recompute all the connection ranks
connectionRanks <- MapQueries.getWormholeConnectionRanksAll(mapId)
// load all the affected connections with sigs
connectionsWithSigs <- MapQueries.getWormholeConnectionsWithSigsBySystemIds(mapId, otherSystemIds)
yield withState(
state.removeSystem(
rs.systemId,
systemId,
connectionIds,
otherSystemIds,
connectionRanks,
Expand All @@ -651,7 +662,7 @@ object MapEntity extends ReactiveEntity[MapEnv, MapId, MapState, Identified[MapR
)(nextState =>
nextState -> broadcast(
MapResponse.SystemRemoved(
state.systems(rs.systemId),
state.systems(systemId),
connectionIds,
connectionsWithSigs.map(whcs => whcs.connection.id -> whcs).toMap,
connectionRanks = nextState.connectionRanks
Expand Down Expand Up @@ -1066,6 +1077,14 @@ private inline def removeSignatureById(arr: Chunk[MapSystemSignature], idOpt: Op
)
.getOrElse(arr)

private def foldLeft[A](
initial: MapState,
xs: Chunk[A],
f: (A, MapState) => ZIO[MapEnv, Throwable, (MapState, Chunk[Identified[MapResponse]])]
) =
ZIO.foldLeft(xs)((initial, Chunk.empty[Identified[MapResponse]])):
case ((prev, responses), a) => f(a, prev).map((s, r) => (s, responses ++ r))

private def combineMany(
initial: MapState,
fs: Chunk[MapState => ZIO[MapEnv, Throwable, (MapState, Chunk[Identified[MapResponse]])]]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,8 @@ object MapSession:
)
case protocol.MapRequest.RemoveSystem(systemId) =>
ctx.mapQ.offer(Identified(Some(ctx.sessionId), MapRequest.RemoveSystem(systemId)))
case protocol.MapRequest.RemoveSystems(systemIds) =>
ctx.mapQ.offer(Identified(Some(ctx.sessionId), MapRequest.RemoveSystems(Chunk.fromArray(systemIds))))
// signatures
case addSig: protocol.MapRequest.AddSystemSignature =>
ctx.mapQ.offer(
Expand Down
5 changes: 5 additions & 0 deletions ui/src/main/css/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
@import 'views/nav-top-view.css';
@import 'views/solar-system-info-view.css';
@import 'views/map-system-signature-view.css';
@import 'views/map-selection-view.css';
@import 'views/system-view.css';

/* ok? */
Expand Down Expand Up @@ -228,6 +229,10 @@ $box-height: 40px;
/*box-shadow: 0px 1px 4px rgba(0, 0, 0, 0.16), 0px 0px 0px 3px rgba(51, 51, 51,1);*/
}

.system-selected-bulk {
background-color: $red-darkest;
}


.system-class-h {
color: $green;
Expand Down
6 changes: 3 additions & 3 deletions ui/src/main/css/components/tooltip.css
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
.tooltip {
position: absolute;
background-color: $gray-dark;
background-color: $gray;

visibility: hidden;
opacity: 0;
Expand All @@ -23,7 +23,7 @@
color: $gray-lightest;
font-size: 1em;
margin: 0.2em;
border-bottom: 1px solid $gray-darker;
border-bottom: 1px solid $gray-dark;
}
}

Expand All @@ -41,6 +41,6 @@

&:hover + .tooltip {
visibility: visible;
opacity: 90%;
opacity: 0.95;
}
}
7 changes: 7 additions & 0 deletions ui/src/main/css/views/map-selection-view.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
div.selection-rectangle {
position: absolute;
z-index: 20;
background-color: $orange-dark;
opacity: 0.4;
border: 1px dashed $gray-lighter;
}
8 changes: 8 additions & 0 deletions ui/src/main/scala/controltower/page/map/MapAction.scala
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ enum MapAction:
*/
case Remove(systemId: SystemId) extends MapAction with SingleSystemAction

/** Remove a system from the map
*/
case RemoveMultiple(systemIds: Array[SystemId]) extends MapAction

/** Remove a single connection from the map
*/
case RemoveConnection(connectionId: ConnectionId) extends MapAction
Expand All @@ -43,6 +47,10 @@ enum MapAction:
*/
case Select(systemId: Option[SystemId])

/** Toggle bulk selection for a system
*/
case ToggleBulkSelection(systemId: SystemId)

/** Toggle whether a system is pinned on the map
*/
case TogglePinned(systemId: SystemId) extends MapAction with SingleSystemAction
Expand Down
14 changes: 12 additions & 2 deletions ui/src/main/scala/controltower/page/map/view/MapController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,9 @@ class MapController(rds: ReferenceDataStore, val clock: Signal[Instant])(using O
val allConnections = HVar[Map[ConnectionId, MapWormholeConnectionWithSigs]](Map.empty)
val allLocations = HVar[Map[SystemId, Array[CharacterLocation]]](Map.empty)

val selectedSystemId = Var[Option[Long]](None)
val selectedConnectionId = Var[Option[ConnectionId]](None)
val selectedSystemId = Var[Option[Long]](None)
val bulkSelectedSystemIds = Var[Array[SystemId]](Array.empty[SystemId])
val selectedConnectionId = Var[Option[ConnectionId]](None)

val lastError = Var[Option[String]](None)

Expand Down Expand Up @@ -136,6 +137,8 @@ class MapController(rds: ReferenceDataStore, val clock: Signal[Instant])(using O
)
case (MapAction.Remove(systemId), _) =>
Some(MapRequest.RemoveSystem(systemId))
case (MapAction.RemoveMultiple(systemIds), _) =>
Some(MapRequest.RemoveSystems(systemIds))
case (MapAction.RemoveConnection(connectionId), _) =>
Some(MapRequest.RemoveSystemConnection(connectionId))
case (MapAction.RemoveSignatures(systemId, sigIds), _) =>
Expand All @@ -153,6 +156,13 @@ class MapController(rds: ReferenceDataStore, val clock: Signal[Instant])(using O
(selectedConnectionId, None)
)
None
case (MapAction.ToggleBulkSelection(systemId), _) =>
bulkSelectedSystemIds.update(arr =>
arr.indexOf(SystemId(systemId)) match
case -1 => arr.appended(SystemId(systemId))
case idx => arr.filterNot(_ == SystemId(systemId))
)
None
case (MapAction.TogglePinned(systemId), allSystems) =>
allSystems.get(systemId).map(sys => MapRequest.UpdateSystem(systemId, isPinned = Some(!sys.system.isPinned)))

Expand Down
66 changes: 57 additions & 9 deletions ui/src/main/scala/controltower/page/map/view/MapView.scala
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import controltower.ui.*
import io.laminext.websocket.*
import org.updraft0.controltower.constant
import org.updraft0.controltower.protocol.*
import org.scalajs.dom.KeyboardEvent

import java.time.Instant
import scala.concurrent.ExecutionContext.Implicits.global
Expand Down Expand Up @@ -129,6 +130,7 @@ private final class MapView(
mssV.signal,
controller.pos,
controller.selectedSystemId.signal,
controller.bulkSelectedSystemIds.signal,
controller.allLocations.signal.map(_.getOrElse(constant.SystemId(systemId), Array.empty[CharacterLocation])),
connectingSystem.current,
ws.isConnected,
Expand Down Expand Up @@ -165,7 +167,7 @@ private final class MapView(
modalKeyBinding(
"KeyA",
controller.mapRole.map(RoleController.canAddSystem).combineWith(ws.isConnected).map(_ && _),
identity,
_.map(ev => (ev, ())),
(_, onClose) =>
Modal.show(
(closeMe, owner) => systemAddView(controller.actionsBus, closeMe, rds, controller.pos)(using owner),
Expand All @@ -178,14 +180,26 @@ private final class MapView(
modalKeyBinding(
"Delete",
controller.mapRole.map(RoleController.canRemoveSystem).combineWith(ws.isConnected).map(_ && _),
_.withCurrentValueOf(controller.selectedSystem).filterNot(_.isEmpty).map(_.get),
_.filterWith(controller.bulkSelectedSystemIds, _.isEmpty)
.filterWith(controller.selectedSystem, _.isDefined)
.withCurrentValueOf(controller.selectedSystem)
.map((ev, opt) => (ev, opt.get)),
(system, onClose) => removeSystemConfirm(system, controller.actionsBus, onClose)(using rds)
),
// delete -> remove multiple systems
modalKeyBinding(
"Delete",
controller.mapRole.map(RoleController.canRemoveSystem).combineWith(ws.isConnected).map(_ && _),
_.filterWith(controller.bulkSelectedSystemIds, _.nonEmpty).withCurrentValueOf(controller.bulkSelectedSystemIds),
(systemIds, onClose) => removeMultipleSystems(systemIds, controller.actionsBus, onClose)(using rds)
),
// P -> paste system signatures
modalKeyBinding(
"KeyP",
controller.mapRole.map(RoleController.canEditSignatures).combineWith(ws.isConnected).map(_ && _),
_.withCurrentValueOf(controller.selectedSystem).filterNot(_.isEmpty).map(_.get),
_.filterWith(controller.selectedSystem, _.isDefined)
.withCurrentValueOf(controller.selectedSystem)
.map((ev, opt) => (ev, opt.get)),
{ (system, onClose) =>
val solarSystem = static.solarSystemMap(system.system.systemId)
Modal.show(
Expand All @@ -205,21 +219,49 @@ private final class MapView(
)
}
),
// R -> rename system
modalKeyBinding(
"KeyR",
controller.mapRole.map(RoleController.canRenameSystem).combineWith(ws.isConnected).map(_ && _),
_.filterWith(controller.selectedSystem, _.isDefined)
.withCurrentValueOf(controller.selectedSystem)
.map((ev, opt) => (ev, opt.get)),
(system, onClose) =>
Modal.show(
(closeMe, owner) =>
systemRenameView(
system.system.systemId,
system.system.name.getOrElse(""),
controller.actionsBus,
closeMe
),
onClose,
true,
cls := "system-rename-dialog"
)
),
div(
idAttr := "map-parent",
cls := "grid",
cls := "g-20px",
toolbarView.view,
inContext(self =>
onClick.stopPropagation.compose(_.withCurrentValueOf(mapCtx.userPreferences)) --> ((ev, prefs) =>
onPointerUp.compose(_.withCurrentValueOf(mapCtx.userPreferences)) --> ((ev, prefs) =>
// TODO: unsure about the checks against self.ref - should always be true?

// note: this click handler cleans up any selection
if (prefs.clickResetsSelection && ev.currentTarget == self.ref)
Var.set(controller.selectedSystemId -> None, controller.selectedConnectionId -> None)

if (ev.currentTarget == self.ref)
// always clean up the multiple system selection
controller.bulkSelectedSystemIds.set(Array.empty)
)
),
toolbarView.view,
div(
idAttr := "map-inner",
children.command <-- systemNodes,
SelectionView(controller.selectedSystemId.signal, controller.bulkSelectedSystemIds.writer, systemNodes).view,
svg.svg(
svg.cls := "connection-container",
svg.style := "position: absolute; left: 0px; top: 0px;",
Expand All @@ -228,6 +270,9 @@ private final class MapView(
svg.overflow := "visible",
children.command <-- connectionNodes,
connectionInProgress.view
),
controller.bulkSelectedSystemIds.signal --> (sss =>
org.scalajs.dom.console.log(s"ids selected: ${sss.mkString(",")}")
)
)
),
Expand All @@ -241,12 +286,15 @@ private final class MapView(

private def modalKeyBinding[B](
code: String,
isConnected: Signal[Boolean],
compose: EventStream[Unit] => EventStream[B],
shouldEnable: Signal[Boolean],
compose: EventStream[KeyboardEvent] => EventStream[(KeyboardEvent, B)],
action: (B, Observer[Unit]) => Unit
) =
documentEvents(_.onKeyDown.filter(ev => !ev.repeat && ev.code == code).preventDefault).mapToUnit
.compose(es => compose(es).filterWith(isConnected).filterWith(notInKeyHandler.signal)) --> { b =>
documentEvents(
_.onKeyDown
.filter(ev => !ev.repeat && ev.code == code && !ev.ctrlKey && !ev.shiftKey && !ev.metaKey)
).compose(es => compose(es).filterWith(shouldEnable).filterWith(notInKeyHandler.signal)) --> { (ev, b) =>
ev.preventDefault()
inKeyHandler.set(true)
action(b, Observer(_ => inKeyHandler.set(false)))
}
Expand Down
Loading

0 comments on commit 4c02859

Please sign in to comment.