Add context menus

This commit is contained in:
LeshaInc 2020-06-06 16:32:22 +03:00
parent 17fae213ba
commit 47a1ef160e
No known key found for this signature in database
GPG Key ID: B4855290FC36DE72
20 changed files with 419 additions and 14 deletions

View File

@ -14,4 +14,6 @@ case class IntColor(color: Int) extends Color {
override def toRGBANorm: RGBAColorNorm = toRGBA.toRGBANorm
override def toHSVA: HSVAColor = toRGBANorm.toHSVA
def withAlpha(a: Float): RGBAColorNorm = toRGBANorm.withAlpha(a)
}

View File

@ -5,6 +5,7 @@ import ocelot.desktop.geometry.{Rect2D, Size2D, Vector2D}
import ocelot.desktop.graphics.Graphics
import ocelot.desktop.ui.event.handlers.{ClickHandler, DragHandler, HoverHandler}
import ocelot.desktop.ui.event.{ClickEvent, DragEvent, HoverEvent, MouseEvent}
import ocelot.desktop.ui.widget.contextmenu.{ContextMenu, ContextMenuEntry}
import ocelot.desktop.ui.widget.window.Window
import ocelot.desktop.ui.widget.{Widget, WorkspaceView}
import ocelot.desktop.util.DrawUtils
@ -37,6 +38,11 @@ trait Node extends Widget with DragHandler with ClickHandler with HoverHandler {
case ClickEvent(MouseEvent.Button.Left, _) =>
window.foreach(window => workspaceView.windowPool.openWindow(window))
case ClickEvent(MouseEvent.Button.Right, _) =>
val menu = new ContextMenu
setupContextMenu(menu)
workspaceView.rootWidget.contextMenus.open(menu)
case DragEvent(DragEvent.State.Start, MouseEvent.Button.Left, pos) =>
startMoving(pos)
@ -65,6 +71,22 @@ trait Node extends Widget with DragHandler with ClickHandler with HoverHandler {
highlight.goto(NoHighlight)
}
def setupContextMenu(menu: ContextMenu): Unit = {
menu.addEntry(new ContextMenuEntry("Cut"))
menu.addEntry(new ContextMenuEntry("Copy"))
menu.addEntry(new ContextMenuEntry("Paste"))
menu.addSeparator()
menu.addEntry(new ContextMenuEntry("Disconnect", () => {
disconnectFromAll()
}))
menu.addEntry(new ContextMenuEntry("Delete", () => {
disconnectFromAll()
workspaceView.nodes -= (this)
}))
}
def environment: Environment
def icon: String = "icons/NA"
@ -91,6 +113,12 @@ trait Node extends Widget with DragHandler with ClickHandler with HoverHandler {
node.onConnectionRemoved(portB, this, portA)
}
def disconnectFromAll(): Unit = {
for ((a, node, b) <- connections) {
disconnect(a, node, b)
}
}
def isConnected(portA: NodePort, node: Node, portB: NodePort): Boolean = {
_connections.contains((portA, node, portB))
}
@ -139,6 +167,8 @@ trait Node extends Widget with DragHandler with ClickHandler with HoverHandler {
override def minimumSize: Size2D = Size2D(68, 68)
override def maximumSize: Size2D = minimumSize
private def startMoving(pos: Vector2D): Unit = {
highlight.goto(MovingHighlight)
isMoving = true
@ -195,5 +225,5 @@ trait Node extends Widget with DragHandler with ClickHandler with HoverHandler {
}
}
lazy val window: Option[Window] = None
def window: Option[Window] = None
}

View File

@ -9,6 +9,7 @@ import ocelot.desktop.util.TierColor
class NodeTypeWidget(val nodeType: NodeType) extends Widget with ClickHandler {
override def minimumSize: Size2D = Size2D(68, 68)
override def maximumSize: Size2D = minimumSize
size = maximumSize

View File

@ -4,6 +4,7 @@ import ocelot.desktop.OcelotDesktop
import ocelot.desktop.color.Color
import ocelot.desktop.graphics.Graphics
import ocelot.desktop.node.Node
import ocelot.desktop.ui.widget.contextmenu.{ContextMenu, ContextMenuEntry, ContextMenuSubmenu}
import ocelot.desktop.util.TierColor
import totoro.ocelot.brain.entity.traits.Computer
import totoro.ocelot.brain.entity.{CPU, Case, EEPROM, GraphicsCard, HDDManaged, InternetCard, Memory, NetworkCard}
@ -28,7 +29,29 @@ class ComputerNode(val computer: Case) extends Node {
override def environment: Computer = computer
override val icon: String = "nodes/Computer"
override val iconColor: Color = TierColor.get(computer.tier)
override def iconColor: Color = TierColor.get(computer.tier)
override def setupContextMenu(menu: ContextMenu): Unit = {
if (computer.machine.isRunning)
menu.addEntry(new ContextMenuEntry("Turn off", () => computer.turnOff()))
else
menu.addEntry(new ContextMenuEntry("Turn on", () => computer.turnOn()))
menu.addEntry(new ContextMenuSubmenu("Set tier") {
addEntry(new ContextMenuEntry("Tier 1", () => changeTier(Tier.One)))
addEntry(new ContextMenuEntry("Tier 2", () => changeTier(Tier.Two)))
addEntry(new ContextMenuEntry("Tier 3", () => changeTier(Tier.Three)))
addEntry(new ContextMenuEntry("Tier 4 (Creative)", () => changeTier(Tier.Four)))
})
menu.addSeparator()
super.setupContextMenu(menu)
}
private def changeTier(n: Int): Unit = {
computer.tier = n
currentWindow = new ComputerWindow(computer)
}
override def draw(g: Graphics): Unit = {
super.draw(g)
@ -46,5 +69,12 @@ class ComputerNode(val computer: Case) extends Node {
g.sprite("nodes/ComputerActivityOverlay", position.x + 2, position.y + 2, size.width - 4, size.height - 4)
}
override lazy val window: Option[ComputerWindow] = Some(new ComputerWindow(computer))
private var currentWindow: ComputerWindow = _
override def window: Option[ComputerWindow] = {
if (currentWindow == null) {
currentWindow = new ComputerWindow(computer)
Some(currentWindow)
} else Some(currentWindow)
}
}

View File

@ -4,9 +4,11 @@ import ocelot.desktop.OcelotDesktop
import ocelot.desktop.color.Color
import ocelot.desktop.graphics.Graphics
import ocelot.desktop.node.Node
import ocelot.desktop.ui.widget.contextmenu.{ContextMenu, ContextMenuEntry, ContextMenuSubmenu}
import ocelot.desktop.util.TierColor
import totoro.ocelot.brain.entity.traits.Environment
import totoro.ocelot.brain.entity.{Keyboard, Screen}
import totoro.ocelot.brain.util.Tier
class ScreenNode(val screen: Screen) extends Node {
OcelotDesktop.workspace.add(screen)
@ -22,6 +24,27 @@ class ScreenNode(val screen: Screen) extends Node {
override def iconColor: Color = TierColor.get(screen.tier)
override def setupContextMenu(menu: ContextMenu): Unit = {
if (screen.getPowerState)
menu.addEntry(new ContextMenuEntry("Turn off", () => screen.setPowerState(false)))
else
menu.addEntry(new ContextMenuEntry("Turn on", () => screen.setPowerState(true)))
menu.addEntry(new ContextMenuSubmenu("Set tier") {
addEntry(new ContextMenuEntry("Tier 1", () => changeTier(Tier.One)))
addEntry(new ContextMenuEntry("Tier 2", () => changeTier(Tier.Two)))
addEntry(new ContextMenuEntry("Tier 3", () => changeTier(Tier.Three)))
})
menu.addSeparator()
super.setupContextMenu(menu)
}
private def changeTier(n: Int): Unit = {
screen.tier = n
currentWindow = new ScreenWindow(screen)
}
override def draw(g: Graphics): Unit = {
super.draw(g)
@ -29,5 +52,12 @@ class ScreenNode(val screen: Screen) extends Node {
g.sprite("nodes/ScreenOnOverlay", position.x + 2, position.y + 2, size.width - 4, size.height - 4)
}
override lazy val window: Option[ScreenWindow] = Some(new ScreenWindow(screen))
private var currentWindow: ScreenWindow = _
override def window: Option[ScreenWindow] = {
if (currentWindow == null) {
currentWindow = new ScreenWindow(screen)
Some(currentWindow)
} else Some(currentWindow)
}
}

View File

@ -203,6 +203,9 @@ object UiHandler extends Logging {
for (event <- KeyEvents.events)
hierarchy.reverseIterator.foreach(_.handleEvent(event))
for (event <- MouseEvents.events)
hierarchy.reverseIterator.filter(_.receiveAllMouseEvents).foreach(_.handleEvent(event))
val scrollTarget = hierarchy.reverseIterator
.find(w => w.receiveScrollEvents && w.clippedBounds.contains(mousePos))

View File

@ -48,6 +48,7 @@ class LinearLayout(widget: Widget,
var usedSpace = 0f
for (child <- widget.children) {
child.recalculateBounds()
val minSize = child.minimumSize
if (stretch) {

View File

@ -54,6 +54,7 @@ class IconButton(releasedIcon: String, pressedIcon: String,
private def pressedIconSize: Size2D = Spritesheet.spriteSize(pressedIcon) * sizeMultiplier
override def minimumSize: Size2D = releasedIconSize.max(pressedIconSize)
override def maximumSize: Size2D = minimumSize
override def draw(g: Graphics): Unit = {
if (alphaAnimation.value < 1f)

View File

@ -14,7 +14,8 @@ class Label extends Widget {
private var length = text.length * 8
private def wideLength(g: Graphics): Int = text.map(g.font.charWidth(_)).sum
override def minimumSize: Size2D = Size2D(length, 8)
override def minimumSize: Size2D = Size2D(length, if (isSmall) 8 else 16)
override def maximumSize: Size2D = minimumSize.copy(width = Float.PositiveInfinity)
override def draw(g: Graphics): Unit = {
if (isSmall) g.setSmallFont()

View File

@ -1,9 +1,14 @@
package ocelot.desktop.ui.widget
import ocelot.desktop.ui.layout.LinearLayout
import ocelot.desktop.ui.layout.CopyLayout
import ocelot.desktop.ui.widget.contextmenu.ContextMenus
class RootWidget extends Widget {
override protected val layout = new LinearLayout(this)
override protected val layout = new CopyLayout(this)
children :+= new WorkspaceView
val workspaceView: WorkspaceView = new WorkspaceView
val contextMenus: ContextMenus = new ContextMenus
children :+= workspaceView
children :+= contextMenus
}

View File

@ -8,6 +8,7 @@ class SlotWidget extends Widget {
def tierIcon: String = null
override def minimumSize: Size2D = Size2D(36, 36)
override def maximumSize: Size2D = minimumSize
override def draw(g: Graphics): Unit = {
g.sprite("EmptySlot", bounds)

View File

@ -21,7 +21,10 @@ class Widget {
final def relayout(): Unit = layout.relayout()
final def recalculateBounds(): Unit = layout.recalculateBounds()
final def recalculateBounds(): Unit = {
layout.recalculateBounds()
size = _size
}
final def relayoutParent(): Unit = {
shouldRelayoutParent = true
@ -62,8 +65,8 @@ class Widget {
val clamped = value.clamped(minimumSize, maximumSize)
if (clamped != _size) {
recalculateBounds()
_size = clamped.copy()
layout.recalculateBounds()
_size = value.clamped(minimumSize, maximumSize)
relayout()
shouldRelayoutParent = true
}
@ -128,4 +131,6 @@ class Widget {
def receiveScrollEvents: Boolean = false
def receiveMouseEvents: Boolean = false
def receiveAllMouseEvents: Boolean = false
}

View File

@ -39,6 +39,8 @@ class WorkspaceView extends Widget with DragHandler with ClickHandler with Hover
createDefaultWorkspace()
def rootWidget: RootWidget = parent.get.asInstanceOf[RootWidget]
def addNode(node: Node, pos: Vector2D = newNodePos): Unit = {
node.position = pos + cameraOffset
resolveCollision(node)
@ -87,6 +89,7 @@ class WorkspaceView extends Widget with DragHandler with ClickHandler with Hover
else
p - Vector2D(32, 32)
nodeSelector.recalculateBounds()
windowPool.openWindow(nodeSelector)
newNodePos = pos - cameraOffset

View File

@ -0,0 +1,93 @@
package ocelot.desktop.ui.widget.contextmenu
import ocelot.desktop.color.IntColor
import ocelot.desktop.geometry.Padding2D
import ocelot.desktop.graphics.Graphics
import ocelot.desktop.ui.layout.LinearLayout
import ocelot.desktop.ui.widget.{PaddingBox, Widget}
import ocelot.desktop.util.animation.ValueAnimation
import ocelot.desktop.util.animation.easing.EaseInOutQuad
import ocelot.desktop.util.{DrawUtils, Orientation}
class ContextMenu extends Widget {
private[contextmenu] var isClosing = false
private[contextmenu] var isOpening = false
protected var _contextMenus: ContextMenus = _
protected val alpha = new ValueAnimation(0f, 8f)
protected var inner: Widget = new Widget {
override protected val layout = new LinearLayout(this, orientation = Orientation.Vertical)
}
children :+= new PaddingBox(inner, Padding2D(top = 4, bottom = 4))
private[contextmenu] def contextMenus: ContextMenus = _contextMenus
private[contextmenu] def contextMenus_=(value: ContextMenus): Unit = {
entries.foreach(_.contextMenus = value)
_contextMenus = value
}
def addEntry(entry: ContextMenuEntry): Unit = {
entry.contextMenus = contextMenus
entry.contextMenu = this
inner.children :+= entry
}
def addSeparator(): Unit = {
inner.children :+= new Separator
}
private def entries: Array[ContextMenuEntry] = inner.children
.filter(_.isInstanceOf[ContextMenuEntry])
.map(_.asInstanceOf[ContextMenuEntry])
def isClosed: Boolean = {
alpha.value == 0f
}
def open(): Unit = {
isClosing = false
isOpening = true
alpha.jump(0.001f)
alpha.goto(1f)
var offset = 0.5f
val step = 0.5f / entries.length.toFloat
for (entry <- entries) {
entry.isGhost = false
entry.textAlpha.jump(offset)
entry.textAlpha.goto(1f)
entry.trans.easing = EaseInOutQuad
entry.trans.jump(-6f + offset * 6f)
entry.trans.goto(0f)
offset -= step
}
}
def close(): Unit = {
isClosing = true
isOpening = false
alpha.goto(0f)
for (entry <- entries) {
entry.isGhost = true
entry.textAlpha.goto(1f)
entry.trans.easing = EaseInOutQuad
entry.trans.goto(-8f)
}
}
override def draw(g: Graphics): Unit = {
alpha.update()
if (alpha.value < 1f) g.beginGroupAlpha() else isOpening = false
DrawUtils.shadow(g, bounds.x - 8, bounds.y - 8, bounds.w + 16, bounds.h + 20, 0.5f)
g.rect(bounds, IntColor(0x222222).withAlpha(0.8f))
DrawUtils.ring(g, bounds.x, bounds.y, bounds.w, bounds.h, 1, IntColor(0x444444))
drawChildren(g)
if (alpha.value < 1f) g.endGroupAlpha(alpha.value)
}
}

View File

@ -0,0 +1,75 @@
package ocelot.desktop.ui.widget.contextmenu
import ocelot.desktop.color.{Color, IntColor}
import ocelot.desktop.geometry.{Padding2D, Size2D}
import ocelot.desktop.graphics.Graphics
import ocelot.desktop.ui.event.handlers.{ClickHandler, HoverHandler}
import ocelot.desktop.ui.event.{ClickEvent, HoverEvent, MouseEvent}
import ocelot.desktop.ui.widget.{Label, PaddingBox, Widget}
import ocelot.desktop.util.animation.ValueAnimation
import ocelot.desktop.util.animation.easing.{EaseInQuad, EaseOutQuad}
class ContextMenuEntry(label: String, onClick: () => Unit = () => {}) extends Widget with ClickHandler with HoverHandler {
private[contextmenu] val alpha = new ValueAnimation(0f, 10f)
private[contextmenu] val textAlpha = new ValueAnimation(0f, 5f)
private[contextmenu] val trans = new ValueAnimation(0f, 20f)
private[contextmenu] var contextMenus: ContextMenus = _
private[contextmenu] var contextMenu: ContextMenu = _
private[contextmenu] var isGhost: Boolean = false
children :+= new PaddingBox(new Label {
override def text: String = label
override def color: Color = IntColor(0xB0B0B0)
}, Padding2D(left = 12f, right = 16f, top = 5f, bottom = 5f))
override def receiveMouseEvents: Boolean = !isGhost
eventHandlers += {
case ClickEvent(MouseEvent.Button.Left, _) if !contextMenu.isOpening => clicked()
case HoverEvent(HoverEvent.State.Enter) => enter()
case HoverEvent(HoverEvent.State.Leave) if !isGhost => leave()
}
override def minimumSize: Size2D = layout.minimumSize.max(Size2D(150, 1))
protected def clicked(): Unit = {
onClick()
contextMenus.closeAll()
contextMenus.setGhost(this)
isGhost = true
alpha.goto(0f)
textAlpha.goto(0f)
alpha.speed = 2.5f
textAlpha.speed = 2.5f
trans.speed = 0f
}
protected def enter(): Unit = {
alpha.speed = 10f
alpha.goto(1f)
trans.easing = EaseInQuad
trans.goto(2f)
}
protected def leave(): Unit = {
alpha.speed = 1f
alpha.goto(0f)
trans.easing = EaseOutQuad
trans.goto(0f)
}
override def draw(g: Graphics): Unit = {
alpha.update()
textAlpha.update()
trans.update()
g.rect(bounds.mapW(_ - 8).mapX(_ + 4), IntColor(0x333333).withAlpha(alpha.value))
g.save()
g.translate(trans.value, 0f)
g.alphaMultiplier = textAlpha.value
drawChildren(g)
g.restore()
}
}

View File

@ -0,0 +1,49 @@
package ocelot.desktop.ui.widget.contextmenu
import ocelot.desktop.color.{Color, IntColor}
import ocelot.desktop.geometry.Vector2D
import ocelot.desktop.graphics.Graphics
import ocelot.desktop.ui.UiHandler
import ocelot.desktop.ui.event.HoverEvent
class ContextMenuSubmenu(label: String) extends ContextMenuEntry(label) {
private val parentEntry = this
private val submenu = new ContextMenu {
override def update(): Unit = {
super.update()
if (!isClosing && !bounds.inflate(4).contains(UiHandler.mousePosition)
&& !parentEntry.isHovered) close()
}
}
def addEntry(entry: ContextMenuEntry): Unit = {
submenu.addEntry(entry)
}
def addSeparator(): Unit = {
submenu.addSeparator()
}
override def clicked(): Unit = {}
override def leave(): Unit = {
if (submenu.isClosed) super.leave()
}
override def update(): Unit = {
if (!isHovered && submenu.isClosed) super.leave()
super.update()
}
override def draw(g: Graphics): Unit = {
super.draw(g)
g.background = Color.Transparent
g.foreground = IntColor(0xB0B0B0).withAlpha(textAlpha.value)
g.text(position.x + width - 24, position.y + 5, ">")
}
eventHandlers += {
case HoverEvent(HoverEvent.State.Enter) if !isGhost =>
contextMenus.open(submenu, position + Vector2D(width, 0), isSubmenu = true)
}
}

View File

@ -0,0 +1,59 @@
package ocelot.desktop.ui.widget.contextmenu
import ocelot.desktop.geometry.Vector2D
import ocelot.desktop.graphics.Graphics
import ocelot.desktop.ui.UiHandler
import ocelot.desktop.ui.event.{KeyEvent, MouseEvent}
import ocelot.desktop.ui.layout.Layout
import ocelot.desktop.ui.widget.Widget
import org.lwjgl.input.Keyboard
class ContextMenus extends Widget {
override protected val layout: Layout = new Layout(this)
private var ghost: Option[ContextMenuEntry] = None
private def menus: Array[ContextMenu] = children.map(_.asInstanceOf[ContextMenu])
private[contextmenu] def setGhost(g: ContextMenuEntry): Unit = {
ghost = Some(g)
}
override def receiveAllMouseEvents: Boolean = true
eventHandlers += {
case KeyEvent(KeyEvent.State.Press, Keyboard.KEY_ESCAPE, _) =>
closeAll()
case MouseEvent(MouseEvent.State.Press, _) =>
if (!menus.map(_.bounds).exists(_.contains(UiHandler.mousePosition))) closeAll()
}
def open(menu: ContextMenu, pos: Vector2D = UiHandler.mousePosition + Vector2D(2, 2), isSubmenu: Boolean = false): Unit = {
if (!isSubmenu) for (child <- menus) close(child)
menu.position = pos
menu.size = menu.minimumSize
menu.contextMenus = this
menu.open()
children :+= menu
}
def close(menu: ContextMenu): Unit = {
menu.close()
}
def closeAll(): Unit = {
for (child <- menus) close(child)
}
override def draw(g: Graphics): Unit = {
super.draw(g)
if (ghost.isDefined && ghost.get.alpha == 0f) ghost = None
ghost.foreach(_.draw(g))
}
override def update(): Unit = {
super.update()
children = children.filterNot(_.asInstanceOf[ContextMenu].isClosed)
}
}

View File

@ -0,0 +1,16 @@
package ocelot.desktop.ui.widget.contextmenu
import ocelot.desktop.color.IntColor
import ocelot.desktop.geometry.{Rect2D, Size2D, Vector2D}
import ocelot.desktop.graphics.Graphics
import ocelot.desktop.ui.widget.Widget
class Separator extends Widget {
override def minimumSize: Size2D = Size2D(50, 9)
override def maximumSize: Size2D = Size2D(Float.PositiveInfinity, 9)
override def draw(g: Graphics): Unit = {
g.rect(Rect2D(position + Vector2D(0, 4), size.copy(height = 1)), IntColor(0x444444))
}
}

View File

@ -69,7 +69,7 @@ object DrawUtils {
def windowWithShadow(g: Graphics, x: Float, y: Float, w: Float, h: Float,
backgroundAlpha: Float, shadowAlpha: Float): Unit = {
DrawUtils.shadow(g, x - 8, y - 8, w + 12, h + 20, shadowAlpha)
DrawUtils.shadow(g, x - 8, y - 8, w + 16, h + 20, shadowAlpha)
DrawUtils.windowBorder(g, x, y, w, h, RGBAColorNorm(1, 1, 1, backgroundAlpha))
}
@ -123,7 +123,7 @@ object DrawUtils {
g.sprite("ShadowBorder", 0, 0, h - 48, 24, col)
g.restore()
g.rect(x + 24, y + 24, w - 48, h - 49, RGBAColorNorm(0, 0, 0, a))
g.rect(x + 24, y + 24, w - 48, h - 48, RGBAColorNorm(0, 0, 0, a))
}
private def rotSprite(g: Graphics, sprite: String, x: Float, y: Float, w: Float, h: Float, angle: Float,

View File

@ -3,7 +3,7 @@ package ocelot.desktop.util.animation
import ocelot.desktop.ui.UiHandler
import ocelot.desktop.util.animation.easing.{Easing, EasingFunction}
class ValueAnimation(init: Float = 0f, speed: Float = 10f) {
class ValueAnimation(init: Float = 0f, var speed: Float = 10f) {
private var _value = init
private var start = init
private var end = init