diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c1c281a..a3954dd 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -16,8 +16,8 @@ variables: PACKAGE_NAME: "ocelot-desktop-${CI_COMMIT_TAG}.jar" stages: - - test - build + - test - upload - deploy - release @@ -29,12 +29,14 @@ test: script: - sbt test -scalafmt: - stage: test - before_script: - - sbt -v sbtVersion - script: - - sbt scalafmtCheckAll +# Rest in piece. +#scalafmt: +# stage: test +# allow_failure: true +# before_script: +# - sbt -v sbtVersion +# script: +# - sbt scalafmtCheckAll build: stage: build diff --git a/.scalafmt.conf b/.scalafmt.conf index 6c56d0c..04111a4 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,24 +1,15 @@ -version = 3.8.6 +version = 3.9.9 runner.dialect = scala213 preset = default maxColumn = 120 -indent.defnSite = 2 align = { preset = none - openParenDefnSite = true + openParenDefnSite = false } newlines = { source = keep - topLevelStatementBlankLines = [ - { blanks { before = 1, after = 1, beforeAll = -1, afterAll = -1 } } - ] -} - -binPack = { - preset = Oneline - literalsExclude = [] } rewrite = { @@ -30,7 +21,15 @@ rewrite = { } docstrings = { - oneline = fold + style = SpaceAsterisk + blankFirstLine = unfold + oneline = unfold wrap = keep forceBlankLineBefore = false -} \ No newline at end of file +} + +indent { + defnSite = 2 + extendSite = 2 + withSiteRelativeToExtends = 2 +} diff --git a/build.sbt b/build.sbt index 2fe1f42..50dd457 100644 --- a/build.sbt +++ b/build.sbt @@ -1,5 +1,5 @@ name := "ocelot-desktop" -version := "1.13.1" +version := "1.14.0" scalaVersion := "2.13.10" lazy val root = project.in(file(".")) @@ -21,9 +21,9 @@ libraryDependencies += "org.scala-lang" % "scala-reflect" % scalaVersion.value libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.19" % "test" libraryDependencies += "org.scalatest" %% "scalatest-funsuite" % "3.2.19" % "test" -libraryDependencies += "org.apache.logging.log4j" % "log4j-core" % "2.20.0" -libraryDependencies += "org.apache.logging.log4j" % "log4j-api" % "2.20.0" -libraryDependencies += "org.apache.logging.log4j" % "log4j-slf4j-impl" % "2.20.0" +libraryDependencies += "org.apache.logging.log4j" % "log4j-core" % "2.25.1" +libraryDependencies += "org.apache.logging.log4j" % "log4j-api" % "2.25.1" +libraryDependencies += "org.apache.logging.log4j" % "log4j-slf4j-impl" % "2.25.1" val lwjglVersion = "2.9.3" @@ -32,7 +32,9 @@ libraryDependencies += "org.lwjgl.lwjgl" % "lwjgl-platform" % lwjglVersion class libraryDependencies += "org.lwjgl.lwjgl" % "lwjgl-platform" % lwjglVersion classifier "natives-windows" libraryDependencies += "org.lwjgl.lwjgl" % "lwjgl-platform" % lwjglVersion classifier "natives-osx" -libraryDependencies += "com.github.stephengold" % "j-ogg-all" % "1.0.3" +Compile / unmanagedResourceDirectories += baseDirectory.value / "lib" / "native" + +libraryDependencies += "com.github.stephengold" % "j-ogg-all" % "1.0.6" libraryDependencies += "com.github.wendykierp" % "JTransforms" % "3.1" libraryDependencies += "com.github.sarxos" % "webcam-capture" % "0.3.12" diff --git a/doc/lwjgl-apple-silicon-compilation.md b/doc/lwjgl-apple-silicon-compilation.md new file mode 100644 index 0000000..037c523 --- /dev/null +++ b/doc/lwjgl-apple-silicon-compilation.md @@ -0,0 +1,25 @@ +# How to compile LWJGL2 for Apple Silicon +LWJGL2 does not provide official native ARM support, therefore Ocelot uses a specially modified version by **shadowfacts** + +Article: https://shadowfacts.net/2022/lwjgl-arm64/ + +Repository: https://github.com/shadowfacts/lwjgl2-arm64 + +This procedure is completely optional, as the precompiled library is already checked into the repository at `lib/native/liblwjgl-arm64.dylib` + +## Compilation +This assumes that you are running macOS on **Apple Silicon** *(cross-compiling LWJGL seems to be impossible)* + +1. Acquire a JDK8 built for ARM - for example, [Zulu 8 JDK](https://www.azul.com/downloads/?version=java-8-lts&os=macos&architecture=arm-64-bit&package=jdk#zulu), and add it to your `JAVA_HOME` +2. Get [`maven`](https://maven.apache.org/) and [`ant`](https://ant.apache.org/) - you may install them via `brew`, but be careful, as they will try to install a JDK as a dependency +3. Clone the LWJGL repository: +```bash +% git clone https://github.com/shadowfacts/lwjgl-arm64.git +``` +4. Run the following commands in the repo: +```bash +% ant generate-all +% ant jars +% ant compile-native +``` +5. Copy the compiled library from `libs/macosx/liblwjgl.dylib` into the `ocelot-desktop` project as `lib/native/liblwjgl-arm64.dylib` \ No newline at end of file diff --git a/lib/native/liblwjgl-arm64.dylib b/lib/native/liblwjgl-arm64.dylib new file mode 100644 index 0000000..709fa77 Binary files /dev/null and b/lib/native/liblwjgl-arm64.dylib differ diff --git a/lib/ocelot-brain b/lib/ocelot-brain index 4b2d2fc..da5dd88 160000 --- a/lib/ocelot-brain +++ b/lib/ocelot-brain @@ -1 +1 @@ -Subproject commit 4b2d2fcec19f8e238dd1c4bdb09d42b11e9bb6e6 +Subproject commit da5dd8877035f4994b77047658561d316319b54b diff --git a/spritepack/spritepack.sh b/spritepack/spritepack.sh old mode 100644 new mode 100755 diff --git a/sprites/icons/SettingsKeymap.png b/sprites/icons/SettingsKeymap.png new file mode 100644 index 0000000..d4414ae Binary files /dev/null and b/sprites/icons/SettingsKeymap.png differ diff --git a/sprites/icons/SideAny.png b/sprites/icons/SideAny.png new file mode 100644 index 0000000..72e1ba5 Binary files /dev/null and b/sprites/icons/SideAny.png differ diff --git a/sprites/icons/SideDown.png b/sprites/icons/SideDown.png new file mode 100644 index 0000000..727cdb6 Binary files /dev/null and b/sprites/icons/SideDown.png differ diff --git a/sprites/icons/SideEast.png b/sprites/icons/SideEast.png new file mode 100644 index 0000000..499e658 Binary files /dev/null and b/sprites/icons/SideEast.png differ diff --git a/sprites/icons/SideNone.png b/sprites/icons/SideNone.png new file mode 100644 index 0000000..6343e02 Binary files /dev/null and b/sprites/icons/SideNone.png differ diff --git a/sprites/icons/SideNorth.png b/sprites/icons/SideNorth.png new file mode 100644 index 0000000..9a3368d Binary files /dev/null and b/sprites/icons/SideNorth.png differ diff --git a/sprites/icons/SideSouth.png b/sprites/icons/SideSouth.png new file mode 100644 index 0000000..b27381c Binary files /dev/null and b/sprites/icons/SideSouth.png differ diff --git a/sprites/icons/SideUndefined.png b/sprites/icons/SideUndefined.png new file mode 100644 index 0000000..e52176e Binary files /dev/null and b/sprites/icons/SideUndefined.png differ diff --git a/sprites/icons/SideUp.png b/sprites/icons/SideUp.png new file mode 100644 index 0000000..9cfa148 Binary files /dev/null and b/sprites/icons/SideUp.png differ diff --git a/sprites/icons/SideWest.png b/sprites/icons/SideWest.png new file mode 100644 index 0000000..707359b Binary files /dev/null and b/sprites/icons/SideWest.png differ diff --git a/sprites/particles/Smoke.png b/sprites/particles/Smoke.png new file mode 100644 index 0000000..97b7cba Binary files /dev/null and b/sprites/particles/Smoke.png differ diff --git a/sprites/screen/BorderB.png b/sprites/screen/BorderB.png deleted file mode 100644 index 7b68a2a..0000000 Binary files a/sprites/screen/BorderB.png and /dev/null differ diff --git a/sprites/screen/BorderT.png b/sprites/screen/BorderT.png deleted file mode 100644 index 409be1f..0000000 Binary files a/sprites/screen/BorderT.png and /dev/null differ diff --git a/sprites/screen/CornerBL.png b/sprites/screen/CornerBL.png deleted file mode 100644 index d05919f..0000000 Binary files a/sprites/screen/CornerBL.png and /dev/null differ diff --git a/sprites/screen/CornerBR.png b/sprites/screen/CornerBR.png deleted file mode 100644 index 328000d..0000000 Binary files a/sprites/screen/CornerBR.png and /dev/null differ diff --git a/sprites/screen/CornerTL.png b/sprites/screen/CornerTL.png deleted file mode 100644 index 3e96c2c..0000000 Binary files a/sprites/screen/CornerTL.png and /dev/null differ diff --git a/sprites/screen/CornerTR.png b/sprites/screen/CornerTR.png deleted file mode 100644 index 3939c86..0000000 Binary files a/sprites/screen/CornerTR.png and /dev/null differ diff --git a/sprites/screen/InnerBorderB.png b/sprites/screen/InnerBorderB.png new file mode 100644 index 0000000..d71b670 Binary files /dev/null and b/sprites/screen/InnerBorderB.png differ diff --git a/sprites/screen/InnerBorderT.png b/sprites/screen/InnerBorderT.png new file mode 100644 index 0000000..fa74da2 Binary files /dev/null and b/sprites/screen/InnerBorderT.png differ diff --git a/sprites/screen/InnerCornerBL.png b/sprites/screen/InnerCornerBL.png new file mode 100644 index 0000000..de52a05 Binary files /dev/null and b/sprites/screen/InnerCornerBL.png differ diff --git a/sprites/screen/InnerCornerBR.png b/sprites/screen/InnerCornerBR.png new file mode 100644 index 0000000..fb161f8 Binary files /dev/null and b/sprites/screen/InnerCornerBR.png differ diff --git a/sprites/screen/InnerCornerTL.png b/sprites/screen/InnerCornerTL.png new file mode 100644 index 0000000..f941c2f Binary files /dev/null and b/sprites/screen/InnerCornerTL.png differ diff --git a/sprites/screen/InnerCornerTR.png b/sprites/screen/InnerCornerTR.png new file mode 100644 index 0000000..1d10a9f Binary files /dev/null and b/sprites/screen/InnerCornerTR.png differ diff --git a/sprites/screen/OuterBorderT.png b/sprites/screen/OuterBorderT.png new file mode 100644 index 0000000..a8d9694 Binary files /dev/null and b/sprites/screen/OuterBorderT.png differ diff --git a/sprites/screen/OuterCornerBL.png b/sprites/screen/OuterCornerBL.png new file mode 100644 index 0000000..d048106 Binary files /dev/null and b/sprites/screen/OuterCornerBL.png differ diff --git a/sprites/screen/OuterCornerBR.png b/sprites/screen/OuterCornerBR.png new file mode 100644 index 0000000..52e15a3 Binary files /dev/null and b/sprites/screen/OuterCornerBR.png differ diff --git a/sprites/screen/OuterCornerTL.png b/sprites/screen/OuterCornerTL.png new file mode 100644 index 0000000..7589052 Binary files /dev/null and b/sprites/screen/OuterCornerTL.png differ diff --git a/sprites/screen/OuterCornerTR.png b/sprites/screen/OuterCornerTR.png new file mode 100644 index 0000000..06ecbb1 Binary files /dev/null and b/sprites/screen/OuterCornerTR.png differ diff --git a/src/main/resources/ocelot/desktop/colorscheme.txt b/src/main/resources/ocelot/desktop/colorscheme.txt index 1878c16..4fed976 100644 --- a/src/main/resources/ocelot/desktop/colorscheme.txt +++ b/src/main/resources/ocelot/desktop/colorscheme.txt @@ -11,7 +11,7 @@ AboutButtonBackgroundActive = #888888 PortDown = #8382d8 PortUp = #75bdc1 PortNorth = #c8ca5f -PortSouth = #990da3 +PortSouth = #ed8ef4 PortEast = #7ec95f PortWest = #db7d75 PortAny = #9b9b9b @@ -23,6 +23,7 @@ Tier3 = #c354cd Label = #333333 LabelError = #aa0000 +LabelDisabled = #888888 Scrollbar = #e5e5e526 ScrollbarThumb = #cc3f72 @@ -46,6 +47,8 @@ ComputerAddress = #333333 ScreenOff = #000000 +WindowBackground = #c6c6c6 + StatusBar = #181818 StatusBarActive = #ffffff04 StatusBarBorder = #222222 @@ -190,3 +193,6 @@ Flash = #ffffff RelayTextLow = #009900 RelayTextMid = #999900 RelayTextHigh = #990000 + +BoomCardGlowStart = #ff4010 +BoomCardGlowEnd = #ff1010 diff --git a/src/main/resources/ocelot/desktop/images/spritesheet/spritesheet.png b/src/main/resources/ocelot/desktop/images/spritesheet/spritesheet.png index bf5252f..4fb8b1f 100644 Binary files a/src/main/resources/ocelot/desktop/images/spritesheet/spritesheet.png and b/src/main/resources/ocelot/desktop/images/spritesheet/spritesheet.png differ diff --git a/src/main/resources/ocelot/desktop/images/spritesheet/spritesheet.txt b/src/main/resources/ocelot/desktop/images/spritesheet/spritesheet.txt index 213b10d..9cbf02c 100644 --- a/src/main/resources/ocelot/desktop/images/spritesheet/spritesheet.txt +++ b/src/main/resources/ocelot/desktop/images/spritesheet/spritesheet.txt @@ -1,22 +1,22 @@ BackgroundPattern 0 0 304 304 BarSegment 385 434 16 4 -Empty 134 618 16 16 -EmptySlot 237 567 18 18 +Empty 134 632 16 16 +EmptySlot 246 567 18 18 Knob 203 434 50 50 KnobCenter 254 434 50 50 KnobLimits 305 434 50 50 Loading 0 305 48 448 Logo 305 0 168 200 -ShadowBorder 201 540 1 24 +ShadowBorder 279 305 1 24 ShadowCorner 233 674 24 24 -TabArrow 225 600 8 14 -blocks/Generic 151 618 16 16 -blocks/HologramEffect 209 549 4 4 -blocks/HologramProjector1Top 168 618 16 16 -blocks/HologramProjector2Top 185 618 16 16 -blocks/HologramProjectorSide 202 618 16 16 -buttons/BottomDrawerClose 256 567 18 18 -buttons/BottomDrawerOpen 275 567 18 18 +TabArrow 569 567 8 14 +blocks/Generic 151 632 16 16 +blocks/HologramEffect 291 305 4 4 +blocks/HologramProjector1Top 168 632 16 16 +blocks/HologramProjector2Top 185 632 16 16 +blocks/HologramProjectorSide 202 632 16 16 +buttons/BottomDrawerClose 265 567 18 18 +buttons/BottomDrawerOpen 284 567 18 18 buttons/OpenFMRadioCloseOff 404 445 7 8 buttons/OpenFMRadioCloseOn 412 445 7 8 buttons/OpenFMRadioRedstoneOff 359 445 8 8 @@ -25,303 +25,319 @@ buttons/OpenFMRadioStartOff 258 674 24 24 buttons/OpenFMRadioStartOn 283 674 24 24 buttons/OpenFMRadioStopOff 308 674 24 24 buttons/OpenFMRadioStopOn 333 674 24 24 -buttons/OpenFMRadioVolumeDownOff 234 600 10 10 -buttons/OpenFMRadioVolumeDownOn 245 600 10 10 -buttons/OpenFMRadioVolumeUpOff 256 600 10 10 -buttons/OpenFMRadioVolumeUpOn 267 600 10 10 -buttons/PowerOff 294 567 18 18 -buttons/PowerOn 313 567 18 18 +buttons/OpenFMRadioVolumeDownOff 578 567 10 10 +buttons/OpenFMRadioVolumeDownOn 589 567 10 10 +buttons/OpenFMRadioVolumeUpOff 600 567 10 10 +buttons/OpenFMRadioVolumeUpOn 611 567 10 10 +buttons/PowerOff 303 567 18 18 +buttons/PowerOn 322 567 18 18 buttons/RackRelayOff 134 655 65 18 buttons/RackRelayOn 200 655 65 18 -icons/Antenna 219 618 16 16 -icons/ArrowRight 236 618 16 16 -icons/AspectRatio 253 618 16 16 -icons/Book 270 618 16 16 -icons/ButtonCheck 134 600 17 17 -icons/ButtonClipboard 152 600 17 17 -icons/ButtonRandomize 170 600 17 17 -icons/CPU 287 618 16 16 -icons/Card 304 618 16 16 -icons/Close 209 600 15 14 -icons/Code 321 618 16 16 -icons/ComponentBus 338 618 16 16 -icons/Copy 355 618 16 16 -icons/Cross 372 618 16 16 -icons/Delete 389 618 16 16 -icons/DragLMB 500 567 21 14 -icons/DragRMB 522 567 21 14 -icons/EEPROM 406 618 16 16 -icons/Edit 423 618 16 16 -icons/Eject 440 618 16 16 -icons/File 457 618 16 16 -icons/Floppy 474 618 16 16 -icons/Folder 491 618 16 16 -icons/FolderSlash 508 618 16 16 -icons/Grid 168 567 22 22 -icons/GridOff 191 567 22 22 -icons/Guitar 525 618 16 16 -icons/HDD 542 618 16 16 -icons/Help 559 618 16 16 -icons/Home 214 567 22 22 -icons/Keyboard 576 618 16 16 -icons/KeyboardOff 593 618 16 16 -icons/LMB 350 655 11 14 -icons/Label 610 618 16 16 -icons/LinesHorizontal 627 618 16 16 -icons/Link 644 618 16 16 -icons/LinkSlash 661 618 16 16 -icons/Memory 678 618 16 16 -icons/Microchip 695 618 16 16 -icons/NA 712 618 16 16 -icons/NotificationError 374 655 11 11 -icons/NotificationInfo 386 655 11 11 -icons/NotificationWarning 398 655 11 11 -icons/Ocelot 729 618 16 16 -icons/Pin 305 655 14 14 -icons/Plus 746 618 16 16 -icons/Power 763 618 16 16 -icons/RMB 362 655 11 14 -icons/Restart 780 618 16 16 -icons/Save 797 618 16 16 -icons/SaveAs 814 618 16 16 -icons/Server 831 618 16 16 -icons/SettingsSound 266 655 12 17 -icons/SettingsSystem 279 655 12 17 -icons/SettingsUI 292 655 12 17 -icons/Tier0 848 618 16 16 -icons/Tier1 865 618 16 16 -icons/Tier2 882 618 16 16 -icons/Tiers 899 618 16 16 -icons/Unpin 320 655 14 14 -icons/WaveLFSR 901 707 24 10 -icons/WaveNoise 926 707 24 10 -icons/WaveSawtooth 951 707 24 10 -icons/WaveSine 976 707 24 10 -icons/WaveSquare 134 724 24 10 -icons/WaveTriangle 159 724 24 10 -icons/Window 916 618 16 16 -icons/WireArrowLeft 203 540 4 8 -icons/WireArrowRight 208 540 4 8 +icons/Antenna 219 632 16 16 +icons/ArrowRight 236 632 16 16 +icons/AspectRatio 253 632 16 16 +icons/Book 270 632 16 16 +icons/ButtonCheck 143 600 17 17 +icons/ButtonClipboard 161 600 17 17 +icons/ButtonRandomize 179 600 17 17 +icons/CPU 287 632 16 16 +icons/Card 304 632 16 16 +icons/Close 553 567 15 14 +icons/Code 321 632 16 16 +icons/ComponentBus 338 632 16 16 +icons/Copy 355 632 16 16 +icons/Cross 372 632 16 16 +icons/Delete 389 632 16 16 +icons/DragLMB 509 567 21 14 +icons/DragRMB 531 567 21 14 +icons/EEPROM 406 632 16 16 +icons/Edit 423 632 16 16 +icons/Eject 440 632 16 16 +icons/File 457 632 16 16 +icons/Floppy 474 632 16 16 +icons/Folder 491 632 16 16 +icons/FolderSlash 508 632 16 16 +icons/Grid 177 567 22 22 +icons/GridOff 200 567 22 22 +icons/Guitar 525 632 16 16 +icons/HDD 542 632 16 16 +icons/Help 559 632 16 16 +icons/Home 223 567 22 22 +icons/Keyboard 576 632 16 16 +icons/KeyboardOff 593 632 16 16 +icons/LMB 298 540 11 14 +icons/Label 610 632 16 16 +icons/LinesHorizontal 627 632 16 16 +icons/Link 644 632 16 16 +icons/LinkSlash 661 632 16 16 +icons/Memory 678 632 16 16 +icons/Microchip 695 632 16 16 +icons/NA 712 632 16 16 +icons/NotificationError 322 540 11 11 +icons/NotificationInfo 334 540 11 11 +icons/NotificationWarning 346 540 11 11 +icons/Ocelot 729 632 16 16 +icons/Pin 253 540 14 14 +icons/Plus 746 632 16 16 +icons/Power 763 632 16 16 +icons/RMB 310 540 11 14 +icons/Restart 780 632 16 16 +icons/Save 797 632 16 16 +icons/SaveAs 814 632 16 16 +icons/Server 831 632 16 16 +icons/SettingsKeymap 201 540 12 17 +icons/SettingsSound 214 540 12 17 +icons/SettingsSystem 227 540 12 17 +icons/SettingsUI 240 540 12 17 +icons/SideAny 358 540 11 11 +icons/SideDown 370 540 11 11 +icons/SideEast 382 540 11 11 +icons/SideNone 394 540 11 11 +icons/SideNorth 406 540 11 11 +icons/SideSouth 418 540 11 11 +icons/SideUndefined 430 540 11 11 +icons/SideUp 442 540 11 11 +icons/SideWest 454 540 11 11 +icons/Tier0 848 632 16 16 +icons/Tier1 865 632 16 16 +icons/Tier2 882 632 16 16 +icons/Tiers 899 632 16 16 +icons/Unpin 268 540 14 14 +icons/WaveLFSR 237 600 24 10 +icons/WaveNoise 262 600 24 10 +icons/WaveSawtooth 287 600 24 10 +icons/WaveSine 312 600 24 10 +icons/WaveSquare 337 600 24 10 +icons/WaveTriangle 362 600 24 10 +icons/Window 916 632 16 16 +icons/WireArrowLeft 281 305 4 8 +icons/WireArrowRight 286 305 4 8 items/APU0 49 655 16 96 items/APU1 66 655 16 96 items/APU2 83 655 16 96 -items/CPU0 933 618 16 16 -items/CPU1 950 618 16 16 -items/CPU2 967 618 16 16 -items/CardBase 984 618 16 16 -items/CircuitBoard 1001 618 16 16 -items/ComponentBus0 134 635 16 16 -items/ComponentBus1 151 635 16 16 -items/ComponentBus2 168 635 16 16 -items/ComponentBus3 185 635 16 16 +items/CPU0 933 632 16 16 +items/CPU1 950 632 16 16 +items/CPU2 967 632 16 16 +items/CardBase 984 632 16 16 +items/CircuitBoard 1001 632 16 16 +items/ComponentBus0 358 674 16 16 +items/ComponentBus1 375 674 16 16 +items/ComponentBus2 392 674 16 16 +items/ComponentBus3 409 674 16 16 items/DataCard0 49 526 16 128 items/DataCard1 66 526 16 128 items/DataCard2 83 526 16 128 -items/DebugCard 202 635 16 16 -items/DiskDriveMountable 219 635 16 16 -items/EEPROM 236 635 16 16 -items/FloppyDisk_dyeBlack 253 635 16 16 -items/FloppyDisk_dyeBlue 270 635 16 16 -items/FloppyDisk_dyeBrown 287 635 16 16 -items/FloppyDisk_dyeCyan 304 635 16 16 -items/FloppyDisk_dyeGray 321 635 16 16 -items/FloppyDisk_dyeGreen 338 635 16 16 -items/FloppyDisk_dyeLightBlue 355 635 16 16 -items/FloppyDisk_dyeLightGray 372 635 16 16 -items/FloppyDisk_dyeLime 389 635 16 16 -items/FloppyDisk_dyeMagenta 406 635 16 16 -items/FloppyDisk_dyeOrange 423 635 16 16 -items/FloppyDisk_dyePink 440 635 16 16 -items/FloppyDisk_dyePurple 457 635 16 16 -items/FloppyDisk_dyeRed 474 635 16 16 -items/FloppyDisk_dyeWhite 491 635 16 16 -items/FloppyDisk_dyeYellow 508 635 16 16 -items/GraphicsCard0 525 635 16 16 -items/GraphicsCard1 542 635 16 16 -items/GraphicsCard2 559 635 16 16 -items/HardDiskDrive0 576 635 16 16 -items/HardDiskDrive1 593 635 16 16 -items/HardDiskDrive2 610 635 16 16 -items/InternetCard 134 567 16 32 +items/DebugCard 426 674 16 16 +items/DiskDriveMountable 443 674 16 16 +items/EEPROM 460 674 16 16 +items/FloppyDisk_dyeBlack 477 674 16 16 +items/FloppyDisk_dyeBlue 494 674 16 16 +items/FloppyDisk_dyeBrown 511 674 16 16 +items/FloppyDisk_dyeCyan 528 674 16 16 +items/FloppyDisk_dyeGray 545 674 16 16 +items/FloppyDisk_dyeGreen 562 674 16 16 +items/FloppyDisk_dyeLightBlue 579 674 16 16 +items/FloppyDisk_dyeLightGray 596 674 16 16 +items/FloppyDisk_dyeLime 613 674 16 16 +items/FloppyDisk_dyeMagenta 630 674 16 16 +items/FloppyDisk_dyeOrange 647 674 16 16 +items/FloppyDisk_dyePink 664 674 16 16 +items/FloppyDisk_dyePurple 681 674 16 16 +items/FloppyDisk_dyeRed 698 674 16 16 +items/FloppyDisk_dyeWhite 715 674 16 16 +items/FloppyDisk_dyeYellow 732 674 16 16 +items/GraphicsCard0 749 674 16 16 +items/GraphicsCard1 766 674 16 16 +items/GraphicsCard2 783 674 16 16 +items/HardDiskDrive0 800 674 16 16 +items/HardDiskDrive1 817 674 16 16 +items/HardDiskDrive2 834 674 16 16 +items/InternetCard 143 567 16 32 items/LinkedCard 100 655 16 96 -items/Memory0 627 635 16 16 -items/Memory1 644 635 16 16 -items/Memory2 661 635 16 16 -items/Memory3 678 635 16 16 -items/Memory4 695 635 16 16 -items/Memory5 712 635 16 16 -items/Memory6 729 635 16 16 -items/NetworkCard 746 635 16 16 +items/Memory0 851 674 16 16 +items/Memory1 868 674 16 16 +items/Memory2 885 674 16 16 +items/Memory3 902 674 16 16 +items/Memory4 919 674 16 16 +items/Memory5 936 674 16 16 +items/Memory6 953 674 16 16 +items/NetworkCard 970 674 16 16 items/OcelotCard 100 526 16 128 -items/RedstoneCard0 763 635 16 16 -items/RedstoneCard1 780 635 16 16 -items/SelfDestructingCard 151 567 16 32 -items/Server0 797 635 16 16 -items/Server1 814 635 16 16 -items/Server2 831 635 16 16 -items/Server3 848 635 16 16 +items/RedstoneCard0 987 674 16 16 +items/RedstoneCard1 1004 674 16 16 +items/SelfDestructingCard 160 567 16 32 +items/Server0 134 707 16 16 +items/Server1 151 707 16 16 +items/Server2 168 707 16 16 +items/Server3 185 707 16 16 items/SoundCard 117 526 16 128 -items/TapeCopper 865 635 16 16 -items/TapeDiamond 882 635 16 16 -items/TapeGold 899 635 16 16 -items/TapeGreg 916 635 16 16 -items/TapeIg 933 635 16 16 -items/TapeIron 950 635 16 16 -items/TapeNetherStar 967 635 16 16 -items/TapeSteel 984 635 16 16 -items/WirelessNetworkCard0 1001 635 16 16 -items/WirelessNetworkCard1 358 674 16 16 -light-panel/BookmarkLeft 882 707 18 14 -light-panel/BookmarkRight 188 600 20 14 -light-panel/BorderB 214 549 4 4 -light-panel/BorderL 304 549 4 2 -light-panel/BorderR 219 549 4 4 -light-panel/BorderT 224 549 4 4 -light-panel/CornerBL 229 549 4 4 -light-panel/CornerBR 234 549 4 4 -light-panel/CornerTL 239 549 4 4 -light-panel/CornerTR 244 549 4 4 -light-panel/Fill 207 560 2 2 +items/TapeCopper 202 707 16 16 +items/TapeDiamond 219 707 16 16 +items/TapeGold 236 707 16 16 +items/TapeGreg 253 707 16 16 +items/TapeIg 270 707 16 16 +items/TapeIron 287 707 16 16 +items/TapeNetherStar 304 707 16 16 +items/TapeSteel 321 707 16 16 +items/WirelessNetworkCard0 338 707 16 16 +items/WirelessNetworkCard1 355 707 16 16 +light-panel/BookmarkLeft 218 600 18 14 +light-panel/BookmarkRight 197 600 20 14 +light-panel/BorderB 296 305 4 4 +light-panel/BorderL 284 314 4 2 +light-panel/BorderR 301 305 4 4 +light-panel/BorderT 306 305 4 4 +light-panel/CornerBL 311 305 4 4 +light-panel/CornerBR 316 305 4 4 +light-panel/CornerTL 321 305 4 4 +light-panel/CornerTR 326 305 4 4 +light-panel/Fill 410 305 2 2 light-panel/Vent 356 434 2 38 nodes/Cable 377 445 8 8 -nodes/Camera 375 674 16 16 -nodes/Chest 335 655 14 14 -nodes/HologramProjector0 392 674 16 16 -nodes/HologramProjector1 409 674 16 16 -nodes/IronNoteBlock 426 674 16 16 -nodes/Lamp 443 674 16 16 -nodes/LampFrame 460 674 16 16 +nodes/Camera 372 707 16 16 +nodes/Chest 283 540 14 14 +nodes/HologramProjector0 389 707 16 16 +nodes/HologramProjector1 406 707 16 16 +nodes/IronNoteBlock 423 707 16 16 +nodes/Lamp 440 707 16 16 +nodes/LampFrame 457 707 16 16 nodes/LampGlow 49 305 128 128 -nodes/NewNode 477 674 16 16 -nodes/NoteBlock 494 674 16 16 -nodes/OpenFMRadio 511 674 16 16 -nodes/Relay 528 674 16 16 -nodes/TapeDrive 545 674 16 16 -nodes/computer/Default 562 674 16 16 -nodes/computer/DiskActivity 579 674 16 16 -nodes/computer/Error 596 674 16 16 -nodes/computer/On 613 674 16 16 -nodes/disk-drive/Default 630 674 16 16 -nodes/disk-drive/DiskActivity 647 674 16 16 -nodes/disk-drive/Floppy 664 674 16 16 +nodes/NewNode 474 707 16 16 +nodes/NoteBlock 491 707 16 16 +nodes/OpenFMRadio 508 707 16 16 +nodes/Relay 525 707 16 16 +nodes/TapeDrive 542 707 16 16 +nodes/computer/Default 559 707 16 16 +nodes/computer/DiskActivity 576 707 16 16 +nodes/computer/Error 593 707 16 16 +nodes/computer/On 610 707 16 16 +nodes/disk-drive/Default 627 707 16 16 +nodes/disk-drive/DiskActivity 644 707 16 16 +nodes/disk-drive/Floppy 661 707 16 16 nodes/holidays/Christmas 134 674 32 32 nodes/holidays/Halloween 167 674 32 32 nodes/holidays/Valentines 200 674 32 32 -nodes/microcontroller/Default 681 674 16 16 -nodes/microcontroller/Error 698 674 16 16 -nodes/microcontroller/On 715 674 16 16 +nodes/microcontroller/Default 678 707 16 16 +nodes/microcontroller/Error 695 707 16 16 +nodes/microcontroller/On 712 707 16 16 nodes/ocelot-block/Default 117 655 16 80 -nodes/ocelot-block/Rx 732 674 16 16 -nodes/ocelot-block/Tx 749 674 16 16 -nodes/rack/Default 766 674 16 16 -nodes/rack/Empty 783 674 16 16 -nodes/rack/drive/0/Default 800 674 16 16 -nodes/rack/drive/0/DiskActivity 817 674 16 16 -nodes/rack/drive/0/Floppy 834 674 16 16 -nodes/rack/drive/1/Default 851 674 16 16 -nodes/rack/drive/1/DiskActivity 868 674 16 16 -nodes/rack/drive/1/Floppy 885 674 16 16 -nodes/rack/drive/2/Default 902 674 16 16 -nodes/rack/drive/2/DiskActivity 919 674 16 16 -nodes/rack/drive/2/Floppy 936 674 16 16 -nodes/rack/drive/3/Default 953 674 16 16 -nodes/rack/drive/3/DiskActivity 970 674 16 16 -nodes/rack/drive/3/Floppy 987 674 16 16 -nodes/rack/drive/Floppy 1004 674 16 16 -nodes/rack/server/0/Default 134 707 16 16 -nodes/rack/server/0/DiskActivity 151 707 16 16 -nodes/rack/server/0/Error 168 707 16 16 -nodes/rack/server/0/NetworkActivity 185 707 16 16 -nodes/rack/server/0/On 202 707 16 16 -nodes/rack/server/1/Default 219 707 16 16 -nodes/rack/server/1/DiskActivity 236 707 16 16 -nodes/rack/server/1/Error 253 707 16 16 -nodes/rack/server/1/NetworkActivity 270 707 16 16 -nodes/rack/server/1/On 287 707 16 16 -nodes/rack/server/2/Default 304 707 16 16 -nodes/rack/server/2/DiskActivity 321 707 16 16 -nodes/rack/server/2/Error 338 707 16 16 -nodes/rack/server/2/NetworkActivity 355 707 16 16 -nodes/rack/server/2/On 372 707 16 16 -nodes/rack/server/3/Default 389 707 16 16 -nodes/rack/server/3/DiskActivity 406 707 16 16 -nodes/rack/server/3/Error 423 707 16 16 -nodes/rack/server/3/NetworkActivity 440 707 16 16 -nodes/rack/server/3/On 457 707 16 16 -nodes/raid/0/DiskActivity 474 707 16 16 -nodes/raid/0/Error 491 707 16 16 -nodes/raid/1/DiskActivity 508 707 16 16 -nodes/raid/1/Error 525 707 16 16 -nodes/raid/2/DiskActivity 542 707 16 16 -nodes/raid/2/Error 559 707 16 16 -nodes/raid/Default 576 707 16 16 -nodes/screen/BottomLeft 593 707 16 16 -nodes/screen/BottomMiddle 610 707 16 16 -nodes/screen/BottomRight 627 707 16 16 -nodes/screen/ColumnBottom 644 707 16 16 -nodes/screen/ColumnMiddle 661 707 16 16 -nodes/screen/ColumnTop 678 707 16 16 -nodes/screen/Middle 695 707 16 16 -nodes/screen/MiddleLeft 712 707 16 16 -nodes/screen/MiddleRight 729 707 16 16 -nodes/screen/PowerOnOverlay 746 707 16 16 -nodes/screen/RowLeft 763 707 16 16 -nodes/screen/RowMiddle 780 707 16 16 -nodes/screen/RowRight 797 707 16 16 -nodes/screen/Standalone 814 707 16 16 -nodes/screen/TopLeft 831 707 16 16 -nodes/screen/TopMiddle 848 707 16 16 -nodes/screen/TopRight 865 707 16 16 -panel/BorderB 249 549 4 4 -panel/BorderL 309 549 4 2 -panel/BorderR 254 549 4 4 -panel/BorderT 259 549 4 4 -panel/CornerBL 264 549 4 4 -panel/CornerBR 269 549 4 4 -panel/CornerTL 274 549 4 4 -panel/CornerTR 279 549 4 4 -panel/Fill 210 560 2 2 +nodes/ocelot-block/Rx 729 707 16 16 +nodes/ocelot-block/Tx 746 707 16 16 +nodes/rack/Default 763 707 16 16 +nodes/rack/Empty 780 707 16 16 +nodes/rack/drive/0/Default 797 707 16 16 +nodes/rack/drive/0/DiskActivity 814 707 16 16 +nodes/rack/drive/0/Floppy 831 707 16 16 +nodes/rack/drive/1/Default 848 707 16 16 +nodes/rack/drive/1/DiskActivity 865 707 16 16 +nodes/rack/drive/1/Floppy 882 707 16 16 +nodes/rack/drive/2/Default 899 707 16 16 +nodes/rack/drive/2/DiskActivity 916 707 16 16 +nodes/rack/drive/2/Floppy 933 707 16 16 +nodes/rack/drive/3/Default 950 707 16 16 +nodes/rack/drive/3/DiskActivity 967 707 16 16 +nodes/rack/drive/3/Floppy 984 707 16 16 +nodes/rack/drive/Floppy 1001 707 16 16 +nodes/rack/server/0/Default 266 655 16 16 +nodes/rack/server/0/DiskActivity 283 655 16 16 +nodes/rack/server/0/Error 300 655 16 16 +nodes/rack/server/0/NetworkActivity 317 655 16 16 +nodes/rack/server/0/On 334 655 16 16 +nodes/rack/server/1/Default 351 655 16 16 +nodes/rack/server/1/DiskActivity 368 655 16 16 +nodes/rack/server/1/Error 385 655 16 16 +nodes/rack/server/1/NetworkActivity 402 655 16 16 +nodes/rack/server/1/On 419 655 16 16 +nodes/rack/server/2/Default 436 655 16 16 +nodes/rack/server/2/DiskActivity 453 655 16 16 +nodes/rack/server/2/Error 470 655 16 16 +nodes/rack/server/2/NetworkActivity 487 655 16 16 +nodes/rack/server/2/On 504 655 16 16 +nodes/rack/server/3/Default 521 655 16 16 +nodes/rack/server/3/DiskActivity 538 655 16 16 +nodes/rack/server/3/Error 555 655 16 16 +nodes/rack/server/3/NetworkActivity 572 655 16 16 +nodes/rack/server/3/On 589 655 16 16 +nodes/raid/0/DiskActivity 606 655 16 16 +nodes/raid/0/Error 623 655 16 16 +nodes/raid/1/DiskActivity 640 655 16 16 +nodes/raid/1/Error 657 655 16 16 +nodes/raid/2/DiskActivity 674 655 16 16 +nodes/raid/2/Error 691 655 16 16 +nodes/raid/Default 708 655 16 16 +nodes/screen/BottomLeft 725 655 16 16 +nodes/screen/BottomMiddle 742 655 16 16 +nodes/screen/BottomRight 759 655 16 16 +nodes/screen/ColumnBottom 776 655 16 16 +nodes/screen/ColumnMiddle 793 655 16 16 +nodes/screen/ColumnTop 810 655 16 16 +nodes/screen/Middle 827 655 16 16 +nodes/screen/MiddleLeft 844 655 16 16 +nodes/screen/MiddleRight 861 655 16 16 +nodes/screen/PowerOnOverlay 878 655 16 16 +nodes/screen/RowLeft 895 655 16 16 +nodes/screen/RowMiddle 912 655 16 16 +nodes/screen/RowRight 929 655 16 16 +nodes/screen/Standalone 946 655 16 16 +nodes/screen/TopLeft 963 655 16 16 +nodes/screen/TopMiddle 980 655 16 16 +nodes/screen/TopRight 997 655 16 16 +panel/BorderB 331 305 4 4 +panel/BorderL 289 314 4 2 +panel/BorderR 336 305 4 4 +panel/BorderT 341 305 4 4 +panel/CornerBL 346 305 4 4 +panel/CornerBR 351 305 4 4 +panel/CornerTL 356 305 4 4 +panel/CornerTR 361 305 4 4 +panel/Fill 413 305 2 2 particles/Note 377 434 7 10 -screen/BorderB 206 549 2 8 -screen/BorderT 203 549 2 10 -screen/CornerBL 386 445 8 8 -screen/CornerBR 395 445 8 8 -screen/CornerTL 359 434 8 10 -screen/CornerTR 368 434 8 10 -window/BorderDark 203 560 1 4 -window/BorderLight 205 560 1 4 -window/CornerBL 284 549 4 4 -window/CornerBR 289 549 4 4 -window/CornerTL 294 549 4 4 -window/CornerTR 299 549 4 4 +particles/Smoke 134 567 8 64 +screen/InnerBorderB 281 321 2 4 +screen/InnerBorderT 284 321 2 4 +screen/InnerCornerBL 366 305 4 4 +screen/InnerCornerBR 371 305 4 4 +screen/InnerCornerTL 376 305 4 4 +screen/InnerCornerTR 381 305 4 4 +screen/OuterBorderT 281 314 2 6 +screen/OuterCornerBL 386 445 8 8 +screen/OuterCornerBR 395 445 8 8 +screen/OuterCornerTL 359 434 8 10 +screen/OuterCornerTR 368 434 8 10 +window/BorderDark 406 305 1 4 +window/BorderLight 408 305 1 4 +window/CornerBL 386 305 4 4 +window/CornerBR 391 305 4 4 +window/CornerTL 396 305 4 4 +window/CornerTR 401 305 4 4 window/OpenFMRadio 474 0 232 105 window/case/Motherboard 123 434 79 70 window/rack/Lines 49 434 73 91 window/rack/Motherboard 178 305 100 78 -window/rack/NetworkBack 221 554 1 2 -window/rack/NetworkBottom 223 554 1 2 -window/rack/NetworkConnector 225 554 1 2 -window/rack/NetworkLeft 227 554 1 2 -window/rack/NetworkRight 229 554 1 2 -window/rack/NetworkTop 231 554 1 2 -window/rack/NodeBack 314 549 5 1 -window/rack/NodeBottom 320 549 5 1 -window/rack/NodeLeft 326 549 5 1 -window/rack/NodeRight 332 549 5 1 -window/rack/NodeTop 338 549 5 1 -window/rack/SideBack 209 554 1 3 -window/rack/SideBottom 211 554 1 3 -window/rack/SideConnector 213 554 1 3 -window/rack/SideLeft 215 554 1 3 -window/rack/SideRight 217 554 1 3 -window/rack/SideTop 219 554 1 3 +window/rack/NetworkBack 293 326 1 2 +window/rack/NetworkBottom 295 326 1 2 +window/rack/NetworkConnector 297 326 1 2 +window/rack/NetworkLeft 299 326 1 2 +window/rack/NetworkRight 301 326 1 2 +window/rack/NetworkTop 303 326 1 2 +window/rack/NodeBack 287 321 5 1 +window/rack/NodeBottom 293 321 5 1 +window/rack/NodeLeft 299 321 5 1 +window/rack/NodeRight 305 321 5 1 +window/rack/NodeTop 311 321 5 1 +window/rack/SideBack 281 326 1 3 +window/rack/SideBottom 283 326 1 3 +window/rack/SideConnector 285 326 1 3 +window/rack/SideLeft 287 326 1 3 +window/rack/SideRight 289 326 1 3 +window/rack/SideTop 291 326 1 3 window/raid/Slots 134 540 66 26 -window/tape/Back 332 567 20 15 -window/tape/BackPressed 353 567 20 15 -window/tape/Forward 374 567 20 15 -window/tape/ForwardPressed 395 567 20 15 -window/tape/Play 416 567 20 15 -window/tape/PlayPressed 437 567 20 15 +window/tape/Back 341 567 20 15 +window/tape/BackPressed 362 567 20 15 +window/tape/Forward 383 567 20 15 +window/tape/ForwardPressed 404 567 20 15 +window/tape/Play 425 567 20 15 +window/tape/PlayPressed 446 567 20 15 window/tape/Screen 134 526 146 13 -window/tape/Stop 458 567 20 15 -window/tape/StopPressed 479 567 20 15 +window/tape/Stop 467 567 20 15 +window/tape/StopPressed 488 567 20 15 diff --git a/src/main/resources/ocelot/desktop/shader/general_3d.vert b/src/main/resources/ocelot/desktop/shader/general_3d.vert index 815ef98..897e2df 100644 --- a/src/main/resources/ocelot/desktop/shader/general_3d.vert +++ b/src/main/resources/ocelot/desktop/shader/general_3d.vert @@ -1,4 +1,4 @@ -#version 140 +#version 150 in vec3 inPos; in vec3 inNormal; diff --git a/src/main/resources/ocelot/desktop/sounds/minecraft/countdown_beep.ogg b/src/main/resources/ocelot/desktop/sounds/minecraft/countdown_beep.ogg new file mode 100644 index 0000000..85ca9c9 Binary files /dev/null and b/src/main/resources/ocelot/desktop/sounds/minecraft/countdown_beep.ogg differ diff --git a/src/main/resources/ocelot/desktop/sounds/minecraft/explosion.ogg b/src/main/resources/ocelot/desktop/sounds/minecraft/explosion.ogg index dc55636..0062f0e 100644 Binary files a/src/main/resources/ocelot/desktop/sounds/minecraft/explosion.ogg and b/src/main/resources/ocelot/desktop/sounds/minecraft/explosion.ogg differ diff --git a/src/main/scala/ocelot/desktop/OcelotDesktop.scala b/src/main/scala/ocelot/desktop/OcelotDesktop.scala index dd6c482..38cc86c 100644 --- a/src/main/scala/ocelot/desktop/OcelotDesktop.scala +++ b/src/main/scala/ocelot/desktop/OcelotDesktop.scala @@ -383,6 +383,7 @@ object OcelotDesktop extends LoggingConfiguration with Logging { loadWorld(nbt) resetAutosave() + logger.info(s"Workspace successfully loaded from: $path") } } } else Failure(new FileNotFoundException("Specified directory does not contain 'workspace.nbt'")) diff --git a/src/main/scala/ocelot/desktop/Settings.scala b/src/main/scala/ocelot/desktop/Settings.scala index a2e6b95..931a5aa 100644 --- a/src/main/scala/ocelot/desktop/Settings.scala +++ b/src/main/scala/ocelot/desktop/Settings.scala @@ -5,11 +5,9 @@ import ocelot.desktop.Settings.ExtendedConfig import ocelot.desktop.util.{Logging, SettingsData} import org.apache.commons.lang3.SystemUtils -import java.io.InputStream import java.nio.charset.StandardCharsets import java.nio.file.{Files, Path} import java.util -import scala.io.{Codec, Source} class Settings(val config: Config) extends SettingsData { // TODO: refactor this mess (having to declare every field 3 times is extremely error-prone) @@ -44,9 +42,12 @@ class Settings(val config: Config) extends SettingsData { windowSize.y -= 16 } + keymap.load(config.getConfig("ocelot.keymap")) + recentWorkspace = config.getOptionalString("ocelot.workspace.recent") pinNewWindows = config.getBooleanOrElse("ocelot.workspace.pinNewWindows", default = true) - unfocusedWindowTransparency = config.getDoubleOrElse("ocelot.workspace.unfocusedWindowTransparency", 0.5) + unfocusedWindowTransparency = config.getDoubleOrElse("ocelot.workspace.unfocusedWindowTransparency", 0.5).toFloat + unfocusedWindowHide = config.getBooleanOrElse("ocelot.workspace.unfocusedWindowHide", default = false) saveOnExit = config.getBooleanOrElse("ocelot.workspace.saveOnExit", default = true) autosave = config.getBooleanOrElse("ocelot.workspace.autosave", default = true) autosavePeriod = config.getIntOrElse("ocelot.workspace.autosavePeriod", default = 300) @@ -105,6 +106,9 @@ object Settings extends Logging { def withValue(path: String, value: Option[Any]): Config = config.withValue(path, ConfigValueFactory.fromAnyRef(value.orNull)) + + def withValue(path: String, value: Int): Config = + config.withValue(path, ConfigValueFactory.fromAnyRef(value)) } class Int2D(var x: Int, var y: Int) { @@ -132,39 +136,18 @@ object Settings extends Logging { def get: Settings = settings def load(path: Path): Unit = { - import java.lang.System.{lineSeparator => EOL} - if (Files.exists(path)) { - var stream: InputStream = null - try { - stream = Files.newInputStream(path) - val source = Source.fromInputStream(stream)(Codec.UTF8) - val plain = source.getLines().mkString("", EOL, EOL) - val config = ConfigFactory.parseString(plain) - settings = new Settings(config) - source.close() - + settings = new Settings(ConfigFactory.parseFile(path.toFile)) logger.info(s"Loaded Ocelot Desktop configuration from: $path") - return } catch { - case _: Throwable => - logger.info(s"Failed to parse $path, using default Ocelot Desktop configuration.") - } finally { - if (stream != null) - stream.close() + case t: Throwable => logger.error(s"Failed to parse $path!", t) } } - val defaults = { - val in = getClass.getResourceAsStream("/ocelot/desktop/ocelot.conf") - val config = Source.fromInputStream(in)(Codec.UTF8).getLines().mkString("", EOL, EOL) - in.close() - ConfigFactory.parseString(config) - } - - settings = new Settings(defaults) + logger.info(s"Using default Ocelot Desktop configuration...") + settings = new Settings(ConfigFactory.parseResources("/ocelot/desktop/ocelot.conf")) } def save(path: Path): Unit = { @@ -186,9 +169,11 @@ object Settings extends Logging { .withValuePreserveOrigin("ocelot.window.fullscreen", settings.windowFullscreen) .withValuePreserveOrigin("ocelot.window.disableVsync", settings.disableVsync) .withValuePreserveOrigin("ocelot.window.debugLwjgl", settings.debugLwjgl) + .withValue("ocelot.keymap", settings.keymap.save()) .withValue("ocelot.workspace.recent", settings.recentWorkspace) .withValuePreserveOrigin("ocelot.workspace.pinNewWindows", settings.pinNewWindows) .withValuePreserveOrigin("ocelot.workspace.unfocusedWindowTransparency", settings.unfocusedWindowTransparency) + .withValuePreserveOrigin("ocelot.workspace.unfocusedWindowHide", settings.unfocusedWindowHide) .withValuePreserveOrigin("ocelot.workspace.saveOnExit", settings.saveOnExit) .withValuePreserveOrigin("ocelot.workspace.autosave", settings.autosave) .withValuePreserveOrigin("ocelot.workspace.autosavePeriod", settings.autosavePeriod) diff --git a/src/main/scala/ocelot/desktop/audio/AL10W.scala b/src/main/scala/ocelot/desktop/audio/AL10W.scala index 3b0c7ee..52e7e6e 100644 --- a/src/main/scala/ocelot/desktop/audio/AL10W.scala +++ b/src/main/scala/ocelot/desktop/audio/AL10W.scala @@ -21,7 +21,7 @@ object AL10W extends Logging { val exc = OpenAlException(func, errName, err) if (Settings.get.logAudioErrorStacktrace) { - logger.error(exc) + logger.error(exc.getMessage, exc) } else { logger.error(exc.getMessage) } diff --git a/src/main/scala/ocelot/desktop/audio/Audio.scala b/src/main/scala/ocelot/desktop/audio/Audio.scala index 680f9d3..7bfce80 100644 --- a/src/main/scala/ocelot/desktop/audio/Audio.scala +++ b/src/main/scala/ocelot/desktop/audio/Audio.scala @@ -246,4 +246,12 @@ object Audio extends Logging { AL10W.alDeleteBuffers(buf.get(i)) } } + + def removeAllSources(): Unit = synchronized { + for (sourceId <- sources.values) { + deleteSource(sourceId) + } + + sources.clear() + } } diff --git a/src/main/scala/ocelot/desktop/audio/SoundBuffers.scala b/src/main/scala/ocelot/desktop/audio/SoundBuffers.scala index cfeee21..0a62f33 100644 --- a/src/main/scala/ocelot/desktop/audio/SoundBuffers.scala +++ b/src/main/scala/ocelot/desktop/audio/SoundBuffers.scala @@ -44,6 +44,8 @@ object SoundBuffers extends Resource { lazy val MinecraftClickRelease: SoundBuffer = load("/ocelot/desktop/sounds/minecraft/click_release.ogg") lazy val MinecraftExplosion: SoundBuffer = load("/ocelot/desktop/sounds/minecraft/explosion.ogg") + lazy val SelfDestructingCardCountdownBeep: SoundBuffer = load("/ocelot/desktop/sounds/minecraft/countdown_beep.ogg") + lazy val NoteBlock: Map[String, SoundBuffer] = List( "banjo", "basedrum", "bass", "bell", "bit", "chime", "cow_bell", "didgeridoo", "flute", "guitar", "harp", "hat", "iron_xylophone", "pling", "snare", "xylophone", diff --git a/src/main/scala/ocelot/desktop/audio/SoundSource.scala b/src/main/scala/ocelot/desktop/audio/SoundSource.scala index 9568f44..44b45b4 100644 --- a/src/main/scala/ocelot/desktop/audio/SoundSource.scala +++ b/src/main/scala/ocelot/desktop/audio/SoundSource.scala @@ -34,15 +34,15 @@ class SoundSource( Audio.getSourceStatus(this) } - def isPlaying: Boolean = { + def playing: Boolean = { status == SoundSource.Status.Playing } - def isPaused: Boolean = { + def paused: Boolean = { status == SoundSource.Status.Paused } - def isStopped: Boolean = { + def stopped: Boolean = { status == SoundSource.Status.Stopped } @@ -124,9 +124,6 @@ object SoundSource { SoundSource.fromBuffer(SoundBuffers.MinecraftClickRelease, SoundCategory.Interface) } - lazy val MinecraftExplosion: SoundSource = - SoundSource.fromBuffer(SoundBuffers.MinecraftExplosion, SoundCategory.Environment) - lazy val MachineFloppyInsert: SoundSource = SoundSource.fromBuffer(SoundBuffers.MachineFloppyInsert, SoundCategory.Environment) diff --git a/src/main/scala/ocelot/desktop/color/RGBAColorNorm.scala b/src/main/scala/ocelot/desktop/color/RGBAColorNorm.scala index 51c563f..fc16860 100644 --- a/src/main/scala/ocelot/desktop/color/RGBAColorNorm.scala +++ b/src/main/scala/ocelot/desktop/color/RGBAColorNorm.scala @@ -1,5 +1,6 @@ package ocelot.desktop.color +import ocelot.desktop.geometry.FloatUtils.ExtendedFloat import ocelot.desktop.geometry.Vector3D import java.nio.ByteBuffer @@ -60,6 +61,15 @@ case class RGBAColorNorm(r: Float, g: Float, b: Float, a: Float = 1f) extends Co HSVAColor(hue, saturation, value, a) } + def lerp(dst: RGBAColorNorm, t: Float): RGBAColorNorm = { + RGBAColorNorm( + r.lerp(dst.r, t), + g.lerp(dst.g, t), + b.lerp(dst.b, t), + a.lerp(dst.a, t), + ) + } + def withAlpha(alpha: Float): RGBAColorNorm = RGBAColorNorm(r, g, b, alpha) // ʕ•ᴥ•ʔ diff --git a/src/main/scala/ocelot/desktop/entity/OpenFMRadio.scala b/src/main/scala/ocelot/desktop/entity/OpenFMRadio.scala index cd93ba6..0153c38 100644 --- a/src/main/scala/ocelot/desktop/entity/OpenFMRadio.scala +++ b/src/main/scala/ocelot/desktop/entity/OpenFMRadio.scala @@ -152,7 +152,7 @@ class OpenFMRadio extends Entity with Environment with DeviceInfo with Logging { } def isPlaying: Boolean = - playbackSoundSource.isDefined && playbackSoundSource.get.isPlaying || playbackThread.isDefined + playbackSoundSource.isDefined && playbackSoundSource.get.playing || playbackThread.isDefined @Callback() def start(context: Context, args: Arguments): Array[AnyRef] = diff --git a/src/main/scala/ocelot/desktop/geometry/FloatUtils.scala b/src/main/scala/ocelot/desktop/geometry/FloatUtils.scala index a064b1f..724503b 100644 --- a/src/main/scala/ocelot/desktop/geometry/FloatUtils.scala +++ b/src/main/scala/ocelot/desktop/geometry/FloatUtils.scala @@ -4,6 +4,6 @@ object FloatUtils { implicit class ExtendedFloat(val v: Float) extends AnyVal { def lerp(that: Float, alpha: Float): Float = v * (1 - alpha) + that * alpha - def clamp(min: Float = 0f, max: Float = 1f): Float = v.min(max).max(min) + def clamped(min: Float = 0f, max: Float = 1f): Float = v.min(max).max(min) } } diff --git a/src/main/scala/ocelot/desktop/geometry/Rect2D.scala b/src/main/scala/ocelot/desktop/geometry/Rect2D.scala index 8f4a8f6..68e344b 100644 --- a/src/main/scala/ocelot/desktop/geometry/Rect2D.scala +++ b/src/main/scala/ocelot/desktop/geometry/Rect2D.scala @@ -76,7 +76,9 @@ case class Rect2D(x: Float, y: Float, w: Float, h: Float) { this } - def inflate(addition: Float): Rect2D = Rect2D(x - addition, y - addition, w + addition * 2, h + addition * 2) + def inflated(dx: Float, dy: Float): Rect2D = Rect2D(x - dx, y - dy, w + dx * 2, h + dy * 2) + + def inflated(delta: Float): Rect2D = inflated(delta, delta) def center: Vector2D = { min + (size * 0.5f).toVector diff --git a/src/main/scala/ocelot/desktop/geometry/Transform2D.scala b/src/main/scala/ocelot/desktop/geometry/Transform2D.scala index 2bfb98d..e5723da 100644 --- a/src/main/scala/ocelot/desktop/geometry/Transform2D.scala +++ b/src/main/scala/ocelot/desktop/geometry/Transform2D.scala @@ -24,7 +24,7 @@ object Transform2D { Transform2D.translate(-1f, 1f) >> Transform2D.scale(2f / width, -2f / height) def rotate(angle: Float): Transform2D = { - val (s, c) = (math.sin(angle).asInstanceOf[Float], math.cos(angle).asInstanceOf[Float]) + val (s, c) = (math.sin(angle).toFloat, math.cos(angle).toFloat) // format: off Transform2D( diff --git a/src/main/scala/ocelot/desktop/geometry/Vector2D.scala b/src/main/scala/ocelot/desktop/geometry/Vector2D.scala index cb167f0..501dd25 100644 --- a/src/main/scala/ocelot/desktop/geometry/Vector2D.scala +++ b/src/main/scala/ocelot/desktop/geometry/Vector2D.scala @@ -36,9 +36,6 @@ case class Vector2D(x: Float, y: Float) extends Persistable { def *(scalar: Float): Vector2D = Vector2D(x * scalar, y * scalar) - // TODO: remove - def *(scalar: Double): Vector2D = Vector2D(x * scalar, y * scalar) - def /(scalar: Float): Vector2D = Vector2D(x / scalar, y / scalar) def snap(v: Float): Vector2D = Vector2D((x / v).floor * v, (y / v).floor * v) diff --git a/src/main/scala/ocelot/desktop/geometry/Vector3D.scala b/src/main/scala/ocelot/desktop/geometry/Vector3D.scala index 22cd0cf..d0cb9de 100644 --- a/src/main/scala/ocelot/desktop/geometry/Vector3D.scala +++ b/src/main/scala/ocelot/desktop/geometry/Vector3D.scala @@ -45,7 +45,7 @@ case class Vector3D(x: Float, y: Float, z: Float) { ) def angle(that: Vector3D): Float = { - math.acos((dot(that) / length / that.length).clamp(-1, 1)).toFloat + math.acos((dot(that) / length / that.length).clamped(-1, 1)).toFloat } def lerp(that: Vector3D, alpha: Float): Vector3D = { diff --git a/src/main/scala/ocelot/desktop/graphics/Graphics.scala b/src/main/scala/ocelot/desktop/graphics/Graphics.scala index 8e76669..cab960c 100644 --- a/src/main/scala/ocelot/desktop/graphics/Graphics.scala +++ b/src/main/scala/ocelot/desktop/graphics/Graphics.scala @@ -2,7 +2,6 @@ package ocelot.desktop.graphics import ocelot.desktop.color.{Color, RGBAColorNorm} import ocelot.desktop.geometry.{Rect2D, Size2D, Transform2D, Vector2D} -import ocelot.desktop.graphics.IconSource.Animation import ocelot.desktop.graphics.Texture.MinFilteringMode import ocelot.desktop.graphics.mesh.{Mesh2D, MeshInstance2D, MeshVertex2D} import ocelot.desktop.graphics.render.InstanceRenderer @@ -280,68 +279,87 @@ class Graphics(private var width: Int, private var height: Int, private var scal // I hate scala. Overloaded methods with default arguments are not allowed def sprite(icon: IconSource, bounds: Rect2D): Unit = { - sprite(icon.path, bounds.x, bounds.y, bounds.w, bounds.h, Color.White, icon.animation) + sprite(icon, bounds.x, bounds.y, bounds.w, bounds.h, Color.White) + } + + def sprite(icon: IconSource, bounds: Rect2D, color: Color): Unit = { + sprite(icon, bounds.x, bounds.y, bounds.w, bounds.h, color) } def sprite(icon: IconSource, pos: Vector2D, size: Size2D): Unit = { - sprite(icon.path, pos.x, pos.y, size.width, size.height, Color.White, icon.animation) + sprite(icon, pos.x, pos.y, size.width, size.height, Color.White) + } + + def sprite(icon: IconSource, pos: Vector2D, color: Color): Unit = { + sprite(icon, pos.x, pos.y, color) } def sprite(icon: IconSource, pos: Vector2D, size: Size2D, color: Color): Unit = { - sprite(icon.path, pos.x, pos.y, size.width, size.height, color, icon.animation) + sprite(icon, pos.x, pos.y, size.width, size.height, color) } def sprite(icon: IconSource, x: Float, y: Float): Unit = { - sprite(icon.path, x, y, icon.animation) + sprite(icon, x, y, Color.White) } def sprite(icon: IconSource, x: Float, y: Float, width: Float, height: Float): Unit = { - sprite(icon.path, x, y, width, height, animation = icon.animation) + sprite(icon, x, y, width, height, Color.White) } - def sprite(icon: IconSource, x: Float, y: Float, width: Float, height: Float, color: Color): Unit = { - sprite(icon.path, x, y, width, height, color, icon.animation) + def sprite(icon: IconSource, x: Float, y: Float, color: Color): Unit = { + val size = Spritesheet.spriteSize(icon.path) + sprite(icon, x, y, size.width, size.height, color) } - def sprite(name: String, bounds: Rect2D): Unit = { - sprite(name, bounds.origin, bounds.size, Color.White) + def sprite( + icon: IconSource, + x: Float, + y: Float, + width: Float, + height: Float, + color: Color, + ): Unit = { + sprite = icon.path + foreground = color + + val spriteRect = icon.animation.map { animation => + val duration = animation.frames.map(_._2).sum + var timeOffset = 0f + var curFrame = 0 + + breakable { + for ((idx, dur) <- animation.frames) { + timeOffset += dur + curFrame = idx + if (timeOffset >= time % duration) break + } + } + + val size = animation.frameSize match { + case Some(size) => Size2D(this.spriteRect.w, this.spriteRect.w * size.height / size.width) + case None => Size2D(this.spriteRect.w, this.spriteRect.w) + } + + this.spriteRect.copy(y = this.spriteRect.y + curFrame * size.height, h = size.height) + } + + _rect(x, y, width, height, fixUV = true, spriteRect) } - def sprite(name: String, x: Float, y: Float, color: Color): Unit = { - sprite(name, Vector2D(x, y), Spritesheet.spriteSize(name), color) - } - - def sprite(name: String, pos: Vector2D, color: Color): Unit = { - sprite(name, pos, Spritesheet.spriteSize(name), color) - } - - def sprite(name: String, pos: Vector2D, size: Size2D, color: Color): Unit = { - sprite(name, pos.x, pos.y, size.width, size.height, color) - } - - def sprite(name: String, pos: Vector2D, size: Size2D): Unit = { - sprite(name, pos.x, pos.y, size.width, size.height) - } - - def sprite(name: String, x: Float, y: Float): Unit = { - sprite(name, x, y, Color.White, None) - } - - def sprite(name: String, x: Float, y: Float, animation: Option[Animation]): Unit = { - sprite(name, x, y, Color.White, animation) - } - - def sprite(name: String, x: Float, y: Float, color: Color, animation: Option[Animation]): Unit = { - val size = Spritesheet.spriteSize(name) - sprite(name, x, y, size.width, size.height, color, animation) - } - - def sprite(name: String, x: Float, y: Float, width: Float, height: Float, - color: Color = Color.White, - animation: Option[Animation] = None): Unit = { + def sprite( + name: String, + x: Float, + y: Float, + width: Float, + height: Float, + color: Color, + spriteRect: Option[Rect2D], + fixUV: Boolean = true, + ): Unit = { sprite = name foreground = color - _rect(x, y, width, height, fixUV = true, animation) + + _rect(x, y, width, height, fixUV, spriteRect) } def rect(r: Rect2D, color: Color): Unit = { @@ -349,7 +367,7 @@ class Graphics(private var width: Int, private var height: Int, private var scal } def rect(x: Float, y: Float, width: Float, height: Float, color: Color = RGBAColorNorm(1f, 1f, 1f)): Unit = { - sprite("Empty", x, y, width, height, color) + sprite(IconSource.Empty, x, y, width, height, color) } private def checkFont(): Unit = { @@ -364,28 +382,8 @@ class Graphics(private var width: Int, private var height: Int, private var scal private def _rect(x: Float, y: Float, width: Float, height: Float, fixUV: Boolean = true, - animation: Option[Animation] = None): Unit = { - val spriteRect = animation match { - case None => this.spriteRect - case Some(animation) => - val duration = animation.frames.map(_._2).sum - var timeOffset = 0f - var curFrame = 0 - - breakable { - for ((idx, dur) <- animation.frames) { - timeOffset += dur - curFrame = idx - if (timeOffset >= time % duration) break - } - } - - val size = animation.frameSize match { - case Some(size) => Size2D(this.spriteRect.w, this.spriteRect.w * size.height / size.width) - case None => Size2D(this.spriteRect.w, this.spriteRect.w) - } - this.spriteRect.copy(y = this.spriteRect.y + curFrame * size.height, h = size.height) - } + spriteRectOptional: Option[Rect2D] = None): Unit = { + val spriteRect = spriteRectOptional.getOrElse(this.spriteRect) val uvTransform = Transform2D.translate(spriteRect.x, spriteRect.y) >> (if (fixUV) diff --git a/src/main/scala/ocelot/desktop/graphics/IconSource.scala b/src/main/scala/ocelot/desktop/graphics/IconSource.scala index ecb83c1..10d3d56 100644 --- a/src/main/scala/ocelot/desktop/graphics/IconSource.scala +++ b/src/main/scala/ocelot/desktop/graphics/IconSource.scala @@ -3,96 +3,148 @@ package ocelot.desktop.graphics import ocelot.desktop.geometry.Size2D import ocelot.desktop.ui.widget.modal.notification.NotificationType.NotificationType import totoro.ocelot.brain.entity.tape.Tape.{Kind => TapeKind} +import totoro.ocelot.brain.util.Direction.{Direction, Down, East, North, South, Up, West} import totoro.ocelot.brain.util.DyeColor import totoro.ocelot.brain.util.ExtendedTier.ExtendedTier import totoro.ocelot.brain.util.Tier.Tier -case class IconSource( - path: String, - animation: Option[IconSource.Animation] = None, -) +case class IconSource(path: String, animation: Option[IconSource.Animation] = None) object IconSource { - val CardIcon: IconSource = IconSource("icons/Card") - val CpuIcon: IconSource = IconSource("icons/CPU") - val HddIcon: IconSource = IconSource("icons/HDD") - val EepromIcon: IconSource = IconSource("icons/EEPROM") - val FloppyIcon: IconSource = IconSource("icons/Floppy") - val MemoryIcon: IconSource = IconSource("icons/Memory") - val ServerIcon: IconSource = IconSource("icons/Server") - val ComponentBusIcon: IconSource = IconSource("icons/ComponentBus") + case class Animation(frames: Array[(Int, Float)], frameSize: Option[Size2D]) - val TierIcon: Tier => IconSource = { tier => - IconSource(s"icons/Tier${tier.id}") + object Animation { + def apply(frames: (Int, Float)*) = new Animation(frames.toArray, None) + def apply(size: Size2D)(frames: (Int, Float)*) = new Animation(frames.toArray, Some(size)) + def apply(size: Option[Size2D] = None)(frames: Array[(Int, Float)]) = new Animation(frames, size) } - object Items { + class IconScope(directory: String)(implicit parent: Option[IconScope]) { + // this makes nested `IconScope` declarations use `this` as their parent scope. + implicit def scope: Option[IconScope] = Some(this) + + private def prefix: String = parent match { + case Some(parent) => s"${parent.prefix}/$directory" + case None => directory + } + + protected def get(name: String): IconSource = { + get(name, None) + } + + protected def get(name: String, animation: Animation): IconSource = { + get(name, Some(animation)) + } + + protected def get(name: String, animation: Option[Animation]): IconSource = { + IconSource(s"$prefix/$name", animation) + } + } + + private implicit val scope: Option[IconScope] = None + + val Empty: IconSource = IconSource("Empty") + val EmptySlot: IconSource = IconSource("EmptySlot") + val ShadowCorner: IconSource = IconSource("ShadowCorner") + val ShadowBorder: IconSource = IconSource("ShadowBorder") + val TabArrow: IconSource = IconSource("TabArrow") + val BarSegment: IconSource = IconSource("BarSegment") + val BackgroundPattern: IconSource = IconSource("BackgroundPattern") + val Logo: IconSource = IconSource("Logo") + + val Knob: IconSource = IconSource("Knob") + val KnobLimits: IconSource = IconSource("KnobLimits") + val KnobCenter: IconSource = IconSource("KnobCenter") + + val Loading: IconSource = IconSource( + "Loading", + Some( + Animation(Size2D(48, 32))( + (0, 0.7f), + (1, 0.7f), + (2, 0.7f), + (3, 0.7f), + (4, 0.7f), + (5, 0.7f), + (6, 0.7f), + (7, 0.7f), + (8, 0.7f), + (9, 0.7f), + (10, 0.7f), + (11, 0.7f), + (12, 0.7f), + (13, 0.7f), + ) + ), + ) + + object Items extends IconScope("items") { val Cpu: Tier => IconSource = { tier => - IconSource(s"items/CPU${tier.id}") + get(s"CPU${tier.id}") } val Apu: Tier => IconSource = { tier => - IconSource(s"items/APU${tier.id}", animation = Some(Animations.Apu)) + get(s"APU${tier.id}", Animations.Apu) } val GraphicsCard: Tier => IconSource = { tier => - IconSource(s"items/GraphicsCard${tier.id}") + get(s"GraphicsCard${tier.id}") } - val NetworkCard: IconSource = IconSource("items/NetworkCard") + val NetworkCard: IconSource = get("NetworkCard") val WirelessNetworkCard: Tier => IconSource = { tier => - IconSource(s"items/WirelessNetworkCard${tier.id}") + get(s"WirelessNetworkCard${tier.id}") } - val LinkedCard: IconSource = IconSource("items/LinkedCard", animation = Some(Animations.LinkedCard)) + val LinkedCard: IconSource = get("LinkedCard", Animations.LinkedCard) - val InternetCard: IconSource = IconSource("items/InternetCard", animation = Some(Animations.InternetCard)) + val InternetCard: IconSource = get("InternetCard", Animations.InternetCard) val RedstoneCard: Tier => IconSource = { tier => - IconSource(s"items/RedstoneCard${tier.id}") + get(s"RedstoneCard${tier.id}") } val DataCard: Tier => IconSource = { tier => - IconSource(s"items/DataCard${tier.id}", animation = Some(Animations.DataCard)) + get(s"DataCard${tier.id}", Animations.DataCard) } - val SoundCard: IconSource = IconSource("items/SoundCard", animation = Some(Animations.DataCard)) + val SoundCard: IconSource = get("SoundCard", Animations.DataCard) val SelfDestructingCard: IconSource = - IconSource("items/SelfDestructingCard", animation = Some(Animations.SelfDestructingCard)) + get("SelfDestructingCard", Animations.SelfDestructingCard) - val OcelotCard: IconSource = IconSource("items/OcelotCard", animation = Some(Animations.OcelotCard)) + val OcelotCard: IconSource = get("OcelotCard", Animations.OcelotCard) val HardDiskDrive: Tier => IconSource = { tier => - IconSource(s"items/HardDiskDrive${tier.id}") + get(s"HardDiskDrive${tier.id}") } - val Eeprom: IconSource = IconSource("items/EEPROM") + val Eeprom: IconSource = get("EEPROM") val FloppyDisk: DyeColor => IconSource = { color => - IconSource(s"items/FloppyDisk_${color.name}") + get(s"FloppyDisk_${color.name}") } val Memory: ExtendedTier => IconSource = { tier => - IconSource(s"items/Memory${tier.id}") + get(s"Memory${tier.id}") } val Server: Tier => IconSource = { tier => - IconSource(s"items/Server${tier.id}") + get(s"Server${tier.id}") } val ComponentBus: Tier => IconSource = { tier => - IconSource(s"items/ComponentBus${tier.id}") + get(s"ComponentBus${tier.id}") } val Tape: TapeKind => IconSource = { case TapeKind.Golder => Tape(TapeKind.Gold) case TapeKind.NetherStarrer => Tape(TapeKind.NetherStar) - case kind => IconSource(s"items/Tape$kind") + case kind => get(s"Tape$kind") } - val DiskDriveMountable: IconSource = IconSource("items/DiskDriveMountable") + val DiskDriveMountable: IconSource = get("DiskDriveMountable") // noinspection ScalaWeakerAccess object Animations { @@ -114,223 +166,368 @@ object IconSource { } } - case class Animation(frames: Array[(Int, Float)], frameSize: Option[Size2D]) + object Icons extends IconScope("icons") { + val Card: IconSource = get("Card") + val Cpu: IconSource = get("CPU") + val Hdd: IconSource = get("HDD") + val Eeprom: IconSource = get("EEPROM") + val Floppy: IconSource = get("Floppy") + val Memory: IconSource = get("Memory") + val Server: IconSource = get("Server") + val ComponentBus: IconSource = get("ComponentBus") - object Animation { - def apply(frames: (Int, Float)*) = new Animation(frames.toArray, None) - def apply(size: Size2D, frames: (Int, Float)*) = new Animation(frames.toArray, Some(size)) - def apply(size: Option[Size2D] = None, frames: Array[(Int, Float)]) = new Animation(frames, size) + val Tier: Tier => IconSource = { tier => + get(s"Tier${tier.id}") + } + + val SideNone: IconSource = get("SideNone") + val SideAny: IconSource = get("SideAny") + val SideUndefined: IconSource = get("SideUndefined") + val Side: Direction => IconSource = { + case Down => get("SideDown") + case Up => get("SideUp") + case North => get("SideNorth") + case South => get("SideSouth") + case West => get("SideWest") + case East => get("SideEast") + case _ => SideUndefined + } + + val Notification: NotificationType => IconSource = { notificationType => + get(s"Notification$notificationType") + } + + val NA: IconSource = get("NA") + val SettingsKeymap: IconSource = get("SettingsKeymap") + val SettingsSystem: IconSource = get("SettingsSystem") + val SettingsSound: IconSource = get("SettingsSound") + val SettingsUI: IconSource = get("SettingsUI") + val Delete: IconSource = get("Delete") + val Label: IconSource = get("Label") + val Copy: IconSource = get("Copy") + val AspectRatio: IconSource = get("AspectRatio") + val Eject: IconSource = get("Eject") + val Restart: IconSource = get("Restart") + val Edit: IconSource = get("Edit") + val Folder: IconSource = get("Folder") + val FolderSlash: IconSource = get("FolderSlash") + val Code: IconSource = get("Code") + val File: IconSource = get("File") + val Link: IconSource = get("Link") + val LinkSlash: IconSource = get("LinkSlash") + val Power: IconSource = get("Power") + val Save: IconSource = get("Save") + val SaveAs: IconSource = get("SaveAs") + val Plus: IconSource = get("Plus") + val Cross: IconSource = get("Cross") + val Microchip: IconSource = get("Microchip") + val Antenna: IconSource = get("Antenna") + val Window: IconSource = get("Window") + val Tiers: IconSource = get("Tiers") + val LinesHorizontal: IconSource = get("LinesHorizontal") + val ArrowRight: IconSource = get("ArrowRight") + val Book: IconSource = get("Book") + val Help: IconSource = get("Help") + val Ocelot: IconSource = get("Ocelot") + val Guitar: IconSource = get("Guitar") + val Keyboard: IconSource = get("Keyboard") + val KeyboardOff: IconSource = get("KeyboardOff") + val ButtonRandomize: IconSource = get("ButtonRandomize") + val ButtonClipboard: IconSource = get("ButtonClipboard") + val ButtonCheck: IconSource = get("ButtonCheck") + val Home: IconSource = get("Home") + val Pin: IconSource = get("Pin") + val Unpin: IconSource = get("Unpin") + val Close: IconSource = get("Close") + val Grid: IconSource = get("Grid") + val GridOff: IconSource = get("GridOff") + val LMB: IconSource = get("LMB") + val RMB: IconSource = get("RMB") + val DragLMB: IconSource = get("DragLMB") + val DragRMB: IconSource = get("DragRMB") + + val WireArrowLeft: IconSource = get("WireArrowLeft") + val WireArrowRight: IconSource = get("WireArrowRight") + + val WaveSine: IconSource = get("WaveSine") + val WaveTriangle: IconSource = get("WaveTriangle") + val WaveSawtooth: IconSource = get("WaveSawtooth") + val WaveSquare: IconSource = get("WaveSquare") + val WaveNoise: IconSource = get("WaveNoise") + val WaveLFSR: IconSource = get("WaveLFSR") } - // ----------------------- Ocelot interface icons ----------------------- + object Nodes extends IconScope("nodes") { + val NewNode: IconSource = get("NewNode") - val Notification: NotificationType => IconSource = { notificationType => - IconSource(s"icons/Notification$notificationType") - } - - val Loading: IconSource = IconSource( - "Loading", - animation = Some(Animation( - Size2D(48, 32), - (0, 0.7f), - (1, 0.7f), - (2, 0.7f), - (3, 0.7f), - (4, 0.7f), - (5, 0.7f), - (6, 0.7f), - (7, 0.7f), - (8, 0.7f), - (9, 0.7f), - (10, 0.7f), - (11, 0.7f), - (12, 0.7f), - (13, 0.7f), - )), - ) - - val SettingsSystem: IconSource = IconSource("icons/SettingsSystem") - val SettingsSound: IconSource = IconSource("icons/SettingsSound") - val SettingsUI: IconSource = IconSource("icons/SettingsUI") - val Delete: IconSource = IconSource("icons/Delete") - val Label: IconSource = IconSource("icons/Label") - val Copy: IconSource = IconSource("icons/Copy") - val AspectRatio: IconSource = IconSource("icons/AspectRatio") - val Eject: IconSource = IconSource("icons/Eject") - val Restart: IconSource = IconSource("icons/Restart") - val Edit: IconSource = IconSource("icons/Edit") - val Folder: IconSource = IconSource("icons/Folder") - val FolderSlash: IconSource = IconSource("icons/FolderSlash") - val Code: IconSource = IconSource("icons/Code") - val File: IconSource = IconSource("icons/File") - val Link: IconSource = IconSource("icons/Link") - val LinkSlash: IconSource = IconSource("icons/LinkSlash") - val Power: IconSource = IconSource("icons/Power") - val Save: IconSource = IconSource("icons/Save") - val SaveAs: IconSource = IconSource("icons/SaveAs") - val Plus: IconSource = IconSource("icons/Plus") - val Cross: IconSource = IconSource("icons/Cross") - val Microchip: IconSource = IconSource("icons/Microchip") - val Antenna: IconSource = IconSource("icons/Antenna") - val Window: IconSource = IconSource("icons/Window") - val Tiers: IconSource = IconSource("icons/Tiers") - val LinesHorizontal: IconSource = IconSource("icons/LinesHorizontal") - val ArrowRight: IconSource = IconSource("icons/ArrowRight") - val Book: IconSource = IconSource("icons/Book") - val Help: IconSource = IconSource("icons/Help") - val Ocelot: IconSource = IconSource("icons/Ocelot") - val Guitar: IconSource = IconSource("icons/Guitar") - val Keyboard: IconSource = IconSource("icons/Keyboard") - val KeyboardOff: IconSource = IconSource("icons/KeyboardOff") - - // ----------------------- Node icons ----------------------- - - val NA: IconSource = IconSource("icons/NA") - - object Nodes { - val NewNode: IconSource = IconSource("nodes/NewNode") - - val Cable: IconSource = IconSource("nodes/Cable") - val Camera: IconSource = IconSource("nodes/Camera") - val Chest: IconSource = IconSource("nodes/Chest") + val Cable: IconSource = get("Cable") + val Camera: IconSource = get("Camera") + val Chest: IconSource = get("Chest") val HologramProjector: Tier => IconSource = { tier => - IconSource(s"nodes/HologramProjector${tier.id}") + get(s"HologramProjector${tier.id}") } - val IronNoteBlock: IconSource = IconSource("nodes/IronNoteBlock") - val NoteBlock: IconSource = IconSource("nodes/NoteBlock") - val OpenFMRadio: IconSource = IconSource("nodes/OpenFMRadio") - val Relay: IconSource = IconSource("nodes/Relay") - val TapeDrive: IconSource = IconSource("nodes/TapeDrive") + val IronNoteBlock: IconSource = get("IronNoteBlock") + val NoteBlock: IconSource = get("NoteBlock") + val OpenFMRadio: IconSource = get("OpenFMRadio") + val Relay: IconSource = get("Relay") + val TapeDrive: IconSource = get("TapeDrive") - object Computer extends PowerIconSource with DiskActivityIconSource { - override protected def prefix: String = "nodes/computer" + val Lamp: IconSource = get("Lamp") + val LampFrame: IconSource = get("LampFrame") + val LampGlow: IconSource = get("LampGlow") - val Default: IconSource = IconSource(s"$prefix/Default") + object Computer extends IconScope("computer") with PowerIconSource with DiskActivityIconSource { + val Default: IconSource = get("Default") } - object DiskDrive extends DiskActivityIconSource with FloppyDriveIconSource { - override protected def prefix: String = "nodes/disk-drive" - - val Default: IconSource = IconSource(s"$prefix/Default") + object DiskDrive extends IconScope("disk-drive") with DiskActivityIconSource with FloppyDriveIconSource { + val Default: IconSource = get("Default") } - object Lamp extends IconSource("nodes/Lamp") { - val Frame: IconSource = IconSource("nodes/LampFrame") - val Glow: IconSource = IconSource("nodes/LampGlow") + object Microcontroller extends IconScope("microcontroller") with PowerIconSource { + val Default: IconSource = get("Default") } - object Microcontroller extends PowerIconSource { - override protected def prefix: String = "nodes/microcontroller" - - val Default: IconSource = IconSource(s"$prefix/Default") - } - - object OcelotBlock { - val Default: IconSource = IconSource( - "nodes/ocelot-block/Default", - animation = Some(Animation(Size2D(16, 16), (0, 30f), (1, 5f), (2, 2f), (0, 20f), (3, 3f), (4, 2f))), + object OcelotBlock extends IconScope("ocelot-block") { + val Default: IconSource = get( + "Default", + Some(Animation(Size2D(16, 16))((0, 30f), (1, 5f), (2, 2f), (0, 20f), (3, 3f), (4, 2f))), ) - val Rx: IconSource = IconSource("nodes/ocelot-block/Rx") - val Tx: IconSource = IconSource("nodes/ocelot-block/Tx") + val Rx: IconSource = get("Rx") + val Tx: IconSource = get("Tx") } - object Rack { - protected val prefix: String = "nodes/rack" - - val Empty: IconSource = IconSource(s"$prefix/Empty") - val Default: IconSource = IconSource(s"$prefix/Default") + object Rack extends IconScope("rack") { + val Empty: IconSource = get("Empty") + val Default: IconSource = get("Default") val Server: Array[Server] = Array.tabulate(4)(new Server(_)) val Drive: Array[Drive] = Array.tabulate(4)(new Drive(_)) - class Server(val slot: Int) extends PowerIconSource with DiskActivityIconSource with NetworkActivityIconSource { - override protected def prefix: String = s"${Rack.prefix}/server/$slot" + class Server(val slot: Int) + extends IconScope(s"server/$slot") + with PowerIconSource + with DiskActivityIconSource + with NetworkActivityIconSource { - val Default: IconSource = IconSource(s"$prefix/Default") + val Default: IconSource = get("Default") } - class Drive(val slot: Int) extends DiskActivityIconSource with FloppyDriveIconSource { - override protected def prefix: String = s"${Rack.prefix}/drive/$slot" + class Drive(val slot: Int) + extends IconScope(s"drive/$slot") + with DiskActivityIconSource + with FloppyDriveIconSource { - val Default: IconSource = IconSource(s"$prefix/Default") + val Default: IconSource = get("Default") } } - object Raid { - protected val prefix: String = "nodes/raid" - - val Default: IconSource = IconSource(s"$prefix/Default") + object Raid extends IconScope("raid") { + val Default: IconSource = get("Default") val Drive: Array[Drive] = Array.tabulate(3)(new Drive(_)) - class Drive(val slot: Int) extends DiskActivityIconSource { - override protected def prefix: String = s"${Raid.prefix}/$slot" - - val Error: IconSource = IconSource(s"$prefix/Error") + class Drive(val slot: Int) extends IconScope(slot.toString) with DiskActivityIconSource { + val Error: IconSource = get("Error") } } - object Screen { - protected val prefix: String = "nodes/screen" + object Screen extends IconScope("screen") { + val Standalone: IconSource = get("Standalone") + val PowerOnOverlay: IconSource = get("PowerOnOverlay") - val Standalone: IconSource = IconSource(s"$prefix/Standalone") - val PowerOnOverlay: IconSource = IconSource(s"$prefix/PowerOnOverlay") + val ColumnTop: IconSource = get("ColumnTop") + val ColumnMiddle: IconSource = get("ColumnMiddle") + val ColumnBottom: IconSource = get("ColumnBottom") - val ColumnTop: IconSource = IconSource(s"$prefix/ColumnTop") - val ColumnMiddle: IconSource = IconSource(s"$prefix/ColumnMiddle") - val ColumnBottom: IconSource = IconSource(s"$prefix/ColumnBottom") + val RowLeft: IconSource = get("RowLeft") + val RowMiddle: IconSource = get("RowMiddle") + val RowRight: IconSource = get("RowRight") - val RowLeft: IconSource = IconSource(s"$prefix/RowLeft") - val RowMiddle: IconSource = IconSource(s"$prefix/RowMiddle") - val RowRight: IconSource = IconSource(s"$prefix/RowRight") + val TopLeft: IconSource = get("TopLeft") + val TopMiddle: IconSource = get("TopMiddle") + val TopRight: IconSource = get("TopRight") - val TopLeft: IconSource = IconSource(s"$prefix/TopLeft") - val TopMiddle: IconSource = IconSource(s"$prefix/TopMiddle") - val TopRight: IconSource = IconSource(s"$prefix/TopRight") + val MiddleLeft: IconSource = get("MiddleLeft") + val Middle: IconSource = get("Middle") + val MiddleRight: IconSource = get("MiddleRight") - val MiddleLeft: IconSource = IconSource(s"$prefix/MiddleLeft") - val Middle: IconSource = IconSource(s"$prefix/Middle") - val MiddleRight: IconSource = IconSource(s"$prefix/MiddleRight") - - val BottomLeft: IconSource = IconSource(s"$prefix/BottomLeft") - val BottomMiddle: IconSource = IconSource(s"$prefix/BottomMiddle") - val BottomRight: IconSource = IconSource(s"$prefix/BottomRight") + val BottomLeft: IconSource = get("BottomLeft") + val BottomMiddle: IconSource = get("BottomMiddle") + val BottomRight: IconSource = get("BottomRight") } - object Holidays { - protected val prefix: String = "nodes/holidays" - - val Christmas: IconSource = IconSource(s"$prefix/Christmas") - val Valentines: IconSource = IconSource(s"$prefix/Valentines") - val Halloween: IconSource = IconSource(s"$prefix/Halloween") + object Holidays extends IconScope("holidays") { + val Christmas: IconSource = get("Christmas") + val Valentines: IconSource = get("Valentines") + val Halloween: IconSource = get("Halloween") } } - trait PowerIconSource { - protected def prefix: String - - val On: IconSource = IconSource(s"$prefix/On") - val Error: IconSource = IconSource(s"$prefix/Error") + trait PowerIconSource extends IconScope { + val On: IconSource = get("On") + val Error: IconSource = get("Error") } - trait DiskActivityIconSource { - protected def prefix: String - - val DiskActivity: IconSource = IconSource(s"$prefix/DiskActivity") + trait DiskActivityIconSource extends IconScope { + val DiskActivity: IconSource = get("DiskActivity") } - trait NetworkActivityIconSource { - protected def prefix: String - - val NetworkActivity: IconSource = IconSource(s"$prefix/NetworkActivity") + trait NetworkActivityIconSource extends IconScope { + val NetworkActivity: IconSource = get("NetworkActivity") } - trait FloppyDriveIconSource { - protected def prefix: String + trait FloppyDriveIconSource extends IconScope { + val Floppy: IconSource = get("Floppy") + } - val Floppy: IconSource = IconSource(s"$prefix/Floppy") + object Screen extends IconScope("screen") { + val InnerCornerTL: IconSource = get("InnerCornerTL") + val InnerCornerTR: IconSource = get("InnerCornerTR") + val InnerCornerBL: IconSource = get("InnerCornerBL") + val InnerCornerBR: IconSource = get("InnerCornerBR") + + val OuterCornerTL: IconSource = get("OuterCornerTL") + val OuterCornerTR: IconSource = get("OuterCornerTR") + val OuterCornerBL: IconSource = get("OuterCornerBL") + val OuterCornerBR: IconSource = get("OuterCornerBR") + + val InnerBorderT: IconSource = get("InnerBorderT") + val OuterBorderT: IconSource = get("OuterBorderT") + + val InnerBorderB: IconSource = get("InnerBorderB") + } + + object Particles extends IconScope("particles") { + val Note: IconSource = get("Note") + val Smoke: IconSource = get("Smoke") + } + + object Buttons extends IconScope("buttons") { + val BottomDrawerOpen: IconSource = get("BottomDrawerOpen") + val BottomDrawerClose: IconSource = get("BottomDrawerClose") + + val PowerOff: IconSource = get("PowerOff") + val PowerOn: IconSource = get("PowerOn") + + val OpenFMRadioVolumeOff: Boolean => IconSource = { isUp => + get(s"OpenFMRadioVolume${if (isUp) "Up" else "Down"}Off") + } + + val OpenFMRadioVolumeOn: Boolean => IconSource = { isUp => + get(s"OpenFMRadioVolume${if (isUp) "Up" else "Down"}On") + } + + val OpenFMRadioRedstoneOff: IconSource = get("OpenFMRadioRedstoneOff") + val OpenFMRadioRedstoneOn: IconSource = get("OpenFMRadioRedstoneOn") + + val OpenFMRadioCloseOff: IconSource = get("OpenFMRadioCloseOff") + val OpenFMRadioCloseOn: IconSource = get("OpenFMRadioCloseOn") + + val OpenFMRadioStartOff: IconSource = get("OpenFMRadioStartOff") + val OpenFMRadioStopOn: IconSource = get("OpenFMRadioStopOn") + + val RackRelayOff: IconSource = get("RackRelayOff") + val RackRelayOn: IconSource = get("RackRelayOn") + } + + object Window extends IconScope("window") { + val CornerTL: IconSource = get("CornerTL") + val CornerTR: IconSource = get("CornerTR") + val CornerBL: IconSource = get("CornerBL") + val CornerBR: IconSource = get("CornerBR") + + val BorderLight: IconSource = get("BorderLight") + val BorderDark: IconSource = get("BorderDark") + + object Tape extends IconScope("tape") { + abstract class TapeButtonIconSource private[Tape] (sprite: String) { + val Released: IconSource = get(sprite) + val Pressed: IconSource = get(s"${sprite}Pressed") + } + + object Back extends TapeButtonIconSource("Back") + object Play extends TapeButtonIconSource("Play") + object Stop extends TapeButtonIconSource("Stop") + object Forward extends TapeButtonIconSource("Forward") + + val Screen: IconSource = get("Screen") + } + + object Case extends IconScope("case") { + val Motherboard: IconSource = get("Motherboard") + } + + object Rack extends IconScope("rack") { + val Motherboard: IconSource = get("Motherboard") + val Lines: IconSource = get("Lines") + + trait DirectionIconSource { + protected def iconPrefix: String + + val DirectionIcon: Direction => IconSource = { direction => + get(s"$iconPrefix${direction.side.capitalize}") + } + } + + trait ConnectorIconSource { + protected def iconPrefix: String + + val Connector: IconSource = get(s"${iconPrefix}Connector") + } + + object Side extends DirectionIconSource with ConnectorIconSource { + override protected def iconPrefix: String = "Side" + } + + object Network extends DirectionIconSource with ConnectorIconSource { + override protected def iconPrefix: String = "Network" + } + + object Node extends DirectionIconSource { + override protected def iconPrefix: String = "Node" + } + } + + object Raid extends IconScope("raid") { + val Slots: IconSource = get("Slots") + } + + val OpenFMRadio: IconSource = get("OpenFMRadio") + } + + object Panel extends IconScope("panel") { + val CornerTL: IconSource = get("CornerTL") + val CornerTR: IconSource = get("CornerTR") + val CornerBL: IconSource = get("CornerBL") + val CornerBR: IconSource = get("CornerBR") + + val BorderT: IconSource = get("BorderT") + val BorderB: IconSource = get("BorderB") + val BorderL: IconSource = get("BorderL") + val BorderR: IconSource = get("BorderR") + + val Fill: IconSource = get("Fill") + } + + object LightPanel extends IconScope("light-panel") { + val CornerTL: IconSource = get("CornerTL") + val CornerTR: IconSource = get("CornerTR") + val CornerBL: IconSource = get("CornerBL") + val CornerBR: IconSource = get("CornerBR") + + val BorderT: IconSource = get("BorderT") + val BorderB: IconSource = get("BorderB") + val BorderL: IconSource = get("BorderL") + val BorderR: IconSource = get("BorderR") + + val Fill: IconSource = get("Fill") + val Vent: IconSource = get("Vent") + + val BookmarkLeft: IconSource = get("BookmarkLeft") + val BookmarkRight: IconSource = get("BookmarkRight") } } diff --git a/src/main/scala/ocelot/desktop/inventory/Inventory.scala b/src/main/scala/ocelot/desktop/inventory/Inventory.scala index bd9e1dc..48ed459 100644 --- a/src/main/scala/ocelot/desktop/inventory/Inventory.scala +++ b/src/main/scala/ocelot/desktop/inventory/Inventory.scala @@ -1,17 +1,22 @@ package ocelot.desktop.inventory import ocelot.desktop.inventory.Inventory.SlotObserver -import ocelot.desktop.ui.event.{Event, EventAware} +import ocelot.desktop.ui.event.{BrainEvent, Dispatchable, EventAware} +import ocelot.desktop.util.Disposable import totoro.ocelot.brain.event.NodeEvent import scala.collection.mutable -/** Provides an inventory — a collection of [[Item]]s indexed by slots. */ -trait Inventory extends EventAware { +/** + * Provides an inventory — a collection of [[Item]]s indexed by slots. + */ +trait Inventory extends EventAware with Disposable { // parallels totoro.ocelot.brain.entity.traits.Inventory // this is intentional - /** The type of items stored in this inventory. */ + /** + * The type of items stored in this inventory. + */ type I <: Item private type WeakHashSet[A] = mutable.WeakHashMap[A, Unit] @@ -20,13 +25,20 @@ trait Inventory extends EventAware { private val itemSlots = mutable.HashMap.empty[I, Int] private val observers = mutable.HashMap.empty[Int, WeakHashSet[SlotObserver]] - /** Called after a new item is added to the inventory. + override def dispose(): Unit = { + super.dispose() + inventoryIterator.foreach(_.removeAndDispose()) + } + + /** + * Called after a new item is added to the inventory. * * @param slot the slot the item was added to */ def onItemAdded(slot: Slot): Unit - /** Called after an item is removed from the inventory. + /** + * Called after an item is removed from the inventory. * * When the item is replaced by another one, the event are sequenced in the following order: * @@ -41,7 +53,9 @@ trait Inventory extends EventAware { */ def onItemRemoved(slot: Slot, removedItem: I, replacedBy: Option[I]): Unit - /** An iterator over all slots occupied in this inventory. */ + /** + * An iterator over all slots occupied in this inventory. + */ def inventoryIterator: Iterator[Slot] = slotItems.keysIterator.map(Slot(_)) def clearInventory(): Unit = { @@ -67,18 +81,20 @@ trait Inventory extends EventAware { super.shouldReceiveEventsFor(address) || inventoryIterator.flatMap(_.get).exists(_.shouldReceiveEventsFor(address)) - override def handleEvent(event: Event): Unit = { + override def handleEvent(event: Dispatchable): Unit = { super.handleEvent(event) for (slot <- inventoryIterator; item <- slot.get) { event match { - case n: NodeEvent if !item.shouldReceiveEventsFor(n.address) => // ignore + case BrainEvent(e: NodeEvent) if !item.shouldReceiveEventsFor(e.address) => // ignore case _ => item.handleEvent(event) } } } - /** A proxy to access a slot of the inventory. */ + /** + * A proxy to access a slot of the inventory. + */ final class Slot private[Inventory] (val index: Int) { require(index >= 0) @@ -86,27 +102,46 @@ trait Inventory extends EventAware { def nonEmpty: Boolean = !isEmpty - /** Inserts the `item` into this slot (replacing the previous item if any). */ + /** + * Inserts the `item` into this slot (replacing the previous item if any). + */ def put(item: inventory.I): Unit = { setSlot(index, Some(item)) } - /** Allows inserting/removing the item in this slot. */ + /** + * Allows inserting/removing the item in this slot. + */ def set(item: Option[inventory.I]): Unit = { setSlot(index, item) } - /** Removes the item contained in this slot if there is one. */ + /** + * Removes the item contained in this slot if there is one. + */ def remove(): Unit = { setSlot(index, None) } - /** The [[Item]] contained in this slot. */ + /** + * Removes the item contained in this slot if there is one, calling the item's [[dispose]] method. + */ + def removeAndDispose(): Unit = { + for (item <- get) { + remove() + item.dispose() + } + } + + /** + * The [[Item]] contained in this slot. + */ def get: Option[inventory.I] = slotItems.get(index) val inventory: Inventory.this.type = Inventory.this - /** Registers an observer to receive item added/removed events. + /** + * Registers an observer to receive item added/removed events. * * @note The inventory keeps a '''weak''' reference to the `observer`. */ @@ -148,8 +183,9 @@ trait Inventory extends EventAware { } final object Slot { - - /** Creates a proxy to an inventory slot. */ + /** + * Creates a proxy to an inventory slot. + */ def apply(index: Int) = new Slot(index) } @@ -206,13 +242,15 @@ trait Inventory extends EventAware { object Inventory { trait SlotObserver { - /** Called after an item was inserted into this slot. + /** + * Called after an item was inserted into this slot. * * @note [[Inventory.onItemAdded]] is called before this method. */ def onItemAdded(): Unit - /** Called after an item was removed from this slot. + /** + * Called after an item was removed from this slot. * * In particular, the slot no longer contains the removed item. * @@ -220,7 +258,9 @@ object Inventory { */ def onItemRemoved(removedItem: Item, replacedBy: Option[Item]): Unit - /** Called when an item contained in this slot sends a notification via [[Item.notifySlot]]. */ + /** + * Called when an item contained in this slot sends a notification via [[Item.notifySlot]]. + */ def onItemNotification(notification: Item.Notification): Unit } } diff --git a/src/main/scala/ocelot/desktop/inventory/Items.scala b/src/main/scala/ocelot/desktop/inventory/Items.scala index ed09fbd..3034fab 100644 --- a/src/main/scala/ocelot/desktop/inventory/Items.scala +++ b/src/main/scala/ocelot/desktop/inventory/Items.scala @@ -20,80 +20,7 @@ object Items extends Logging { // this is just to force load the class during initialization def init(): Unit = {} - /** Registers a recoverer for [[ItemRecoverer.sourceClass]]. */ - def registerRecoverer(recoverer: ItemRecoverer[_, _]): Unit = { - if (!_recoverers.contains(recoverer.sourceClass)) { - _recoverers(recoverer.sourceClass) = recoverer - logger.info(s"Registered a recoverer for ${recoverer.sourceClass.getName}") - } - } - - private def registerItemFactoryRecoverers(factory: ItemFactory): Unit = { - for (recoverer <- factory.recoverers) { - registerRecoverer(recoverer) - } - } - - def registerSingleton(factory: ItemFactory): Unit = { - _groups += SingletonItemGroup(factory.name, factory) - registerItemFactoryRecoverers(factory) - } - - def registerTiered(name: String, tiers: IterableOnce[Tier])(factory: Tier => ItemFactory): Unit = { - val group = TieredItemGroup(name, tiers.iterator.map(tier => (tier, factory(tier))).toSeq) - _groups += group - - for ((_, factory) <- group.factories) { - registerItemFactoryRecoverers(factory) - } - } - - def registerExtendedTiered(name: String, tiers: IterableOnce[ExtendedTier])( - factory: ExtendedTier => ItemFactory - ): Unit = { - val group = ExtendedTieredItemGroup(name, tiers.iterator.map(tier => (tier, factory(tier))).toSeq) - _groups += group - - for ((_, factory) <- group.factories) { - registerItemFactoryRecoverers(factory) - } - } - - def registerArbitrary(name: String, icon: IconSource, factories: IterableOnce[(String, ItemFactory)]): Unit = { - val group = ArbitraryItemGroup(name, icon, factories.iterator.toSeq) - _groups += group - - for ((_, factory) <- group.factories) { - registerItemFactoryRecoverers(factory) - } - } - - def groups: Iterable[ItemGroup] = _groups - - /** Attempts to recover an [[Item]] from `source`. - * - * Checks superclasses and traits while looking for a recoverer. - */ - def recover[A](source: A): Option[Item] = { - linearizationOrder(source.getClass.asInstanceOf[Class[_]]) - .flatMap(_recoverers.get) - .map(_.asInstanceOf[ItemRecoverer[_ >: A, _ <: Item]].recover(source)) - .nextOption() - } - - sealed trait ItemGroup { - def name: String - } - - case class SingletonItemGroup(name: String, factory: ItemFactory) extends ItemGroup - - case class TieredItemGroup(name: String, factories: Seq[(Tier, ItemFactory)]) extends ItemGroup - - case class ExtendedTieredItemGroup(name: String, factories: Seq[(ExtendedTier, ItemFactory)]) extends ItemGroup - - case class ArbitraryItemGroup(name: String, icon: IconSource, factories: Seq[(String, ItemFactory)]) extends ItemGroup - - ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + logger.debug("Initialize item serialization...") registerTiered("CPU", Tier.One to Tier.Three)(new CpuItem.Factory(_)) registerTiered("APU", Tier.Two to Tier.Creative)(tier => new ApuItem.Factory(tier.saturatingSub(1))) @@ -166,4 +93,81 @@ object Items extends Logging { .map(new TapeItem.Factory(_)) .map(factory => (f"${factory.name}%s (${Tape.lengthMinutes(factory.kind)}%.0f min)", factory)), ) + + logger.debug("Item serialization initialization finished.") + + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** Registers a recoverer for [[ItemRecoverer.sourceClass]]. */ + private def registerRecoverer(recoverer: ItemRecoverer[_, _]): Unit = { + if (!_recoverers.contains(recoverer.sourceClass)) { + _recoverers(recoverer.sourceClass) = recoverer + logger.debug(s"Registered a recoverer for ${recoverer.sourceClass.getName}") + } + } + + private def registerItemFactoryRecoverers(factory: ItemFactory): Unit = { + for (recoverer <- factory.recoverers) { + registerRecoverer(recoverer) + } + } + + def registerSingleton(factory: ItemFactory): Unit = { + _groups += SingletonItemGroup(factory.name, factory) + registerItemFactoryRecoverers(factory) + } + + def registerTiered(name: String, tiers: IterableOnce[Tier])(factory: Tier => ItemFactory): Unit = { + val group = TieredItemGroup(name, tiers.iterator.map(tier => (tier, factory(tier))).toSeq) + _groups += group + + for ((_, factory) <- group.factories) { + registerItemFactoryRecoverers(factory) + } + } + + def registerExtendedTiered(name: String, tiers: IterableOnce[ExtendedTier])( + factory: ExtendedTier => ItemFactory + ): Unit = { + val group = ExtendedTieredItemGroup(name, tiers.iterator.map(tier => (tier, factory(tier))).toSeq) + _groups += group + + for ((_, factory) <- group.factories) { + registerItemFactoryRecoverers(factory) + } + } + + def registerArbitrary(name: String, icon: IconSource, factories: IterableOnce[(String, ItemFactory)]): Unit = { + val group = ArbitraryItemGroup(name, icon, factories.iterator.toSeq) + _groups += group + + for ((_, factory) <- group.factories) { + registerItemFactoryRecoverers(factory) + } + } + + def groups: Iterable[ItemGroup] = _groups + + /** Attempts to recover an [[Item]] from `source`. + * + * Checks superclasses and traits while looking for a recoverer. + */ + def recover[A](source: A): Option[Item] = { + linearizationOrder(source.getClass.asInstanceOf[Class[_]]) + .flatMap(_recoverers.get) + .map(_.asInstanceOf[ItemRecoverer[_ >: A, _ <: Item]].recover(source)) + .nextOption() + } + + sealed trait ItemGroup { + def name: String + } + + case class SingletonItemGroup(name: String, factory: ItemFactory) extends ItemGroup + + case class TieredItemGroup(name: String, factories: Seq[(Tier, ItemFactory)]) extends ItemGroup + + case class ExtendedTieredItemGroup(name: String, factories: Seq[(ExtendedTier, ItemFactory)]) extends ItemGroup + + case class ArbitraryItemGroup(name: String, icon: IconSource, factories: Seq[(String, ItemFactory)]) extends ItemGroup } diff --git a/src/main/scala/ocelot/desktop/inventory/PersistedInventory.scala b/src/main/scala/ocelot/desktop/inventory/PersistedInventory.scala index cea5551..68687bc 100644 --- a/src/main/scala/ocelot/desktop/inventory/PersistedInventory.scala +++ b/src/main/scala/ocelot/desktop/inventory/PersistedInventory.scala @@ -104,7 +104,7 @@ trait PersistedInventory extends Inventory with Logging with Persistable { } protected def onSlotLoadFailed(slotIndex: Int): Unit = { - Slot(slotIndex).remove() + Slot(slotIndex).removeAndDispose() } protected def saveEntityItem(slotNbt: NBTTagCompound, item: EntityItem): Unit = { diff --git a/src/main/scala/ocelot/desktop/inventory/SyncedInventory.scala b/src/main/scala/ocelot/desktop/inventory/SyncedInventory.scala index d593725..c6dc3e4 100644 --- a/src/main/scala/ocelot/desktop/inventory/SyncedInventory.scala +++ b/src/main/scala/ocelot/desktop/inventory/SyncedInventory.scala @@ -178,7 +178,7 @@ trait SyncedInventory extends PersistedInventory with EventAware with Logging { ) logger.error("Breaking the loop forcefully by removing the items.") - Slot(slotIndex).remove() + Slot(slotIndex).removeAndDispose() brainInventory.inventory(slotIndex).remove() } else { direction match { diff --git a/src/main/scala/ocelot/desktop/inventory/item/EepromItem.scala b/src/main/scala/ocelot/desktop/inventory/item/EepromItem.scala index 727a06d..f4c85b7 100644 --- a/src/main/scala/ocelot/desktop/inventory/item/EepromItem.scala +++ b/src/main/scala/ocelot/desktop/inventory/item/EepromItem.scala @@ -45,8 +45,8 @@ class EepromItem(val eeprom: EEPROM) extends Item with ComponentItem with Persis } override def fillRmbMenu(menu: ContextMenu): Unit = { - menu.addEntry(new ContextMenuSubmenu("External data source", Some(ContextMenuIcon(IconSource.Code))) { - addEntry(ContextMenuEntry("Local file", IconSource.File) { + menu.addEntry(new ContextMenuSubmenu("External data source", Some(ContextMenuIcon(IconSource.Icons.Code))) { + addEntry(ContextMenuEntry("Local file", IconSource.Icons.File) { OcelotDesktop.showFileChooserDialog(JFileChooser.OPEN_DIALOG, JFileChooser.FILES_ONLY) { file => Try { for (file <- file) { @@ -56,7 +56,7 @@ class EepromItem(val eeprom: EEPROM) extends Item with ComponentItem with Persis } }) - addEntry(ContextMenuEntry("File via URL", IconSource.Link) { + addEntry(ContextMenuEntry("File via URL", IconSource.Icons.Link) { new InputDialog( title = "File via URL", onConfirmed = { text => @@ -74,7 +74,7 @@ class EepromItem(val eeprom: EEPROM) extends Item with ComponentItem with Persis }) if (eeprom.codePath.nonEmpty || eeprom.codeURL.nonEmpty) { - addEntry(ContextMenuEntry("Detach", IconSource.LinkSlash) { + addEntry(ContextMenuEntry("Detach", IconSource.Icons.LinkSlash) { eeprom.codeBytes = Some(Array.empty) }) } diff --git a/src/main/scala/ocelot/desktop/inventory/item/FloppyItem.scala b/src/main/scala/ocelot/desktop/inventory/item/FloppyItem.scala index 097ec56..d8e3f65 100644 --- a/src/main/scala/ocelot/desktop/inventory/item/FloppyItem.scala +++ b/src/main/scala/ocelot/desktop/inventory/item/FloppyItem.scala @@ -3,37 +3,44 @@ package ocelot.desktop.inventory.item import ocelot.desktop.graphics.IconSource import ocelot.desktop.inventory.traits.{ComponentItem, DiskItem, PersistableItem} import ocelot.desktop.inventory.{Item, ItemFactory, ItemRecoverer} +import ocelot.desktop.ui.widget.DiskEditWindow import ocelot.desktop.ui.widget.tooltip.ItemTooltip import totoro.ocelot.brain.entity.fs.ReadWriteLabel import totoro.ocelot.brain.entity.traits.{Disk, Entity, Floppy} import totoro.ocelot.brain.entity.{FloppyManaged, FloppyUnmanaged} -import totoro.ocelot.brain.loot.Loot.{FloppyFactory => LootFloppyFactory, LootFloppy} +import totoro.ocelot.brain.loot.Loot.{LootFloppy, FloppyFactory => LootFloppyFactory} import totoro.ocelot.brain.util.DyeColor import totoro.ocelot.brain.util.Tier.Tier -class FloppyItem(var floppy: Floppy) extends Item with ComponentItem with PersistableItem with DiskItem { +class FloppyItem(var floppy: Floppy) + extends Item + with ComponentItem + with PersistableItem + with DiskItem + with DiskEditWindow.HasColor { + override def entity: Entity with Disk = floppy override def diskKind: String = "Floppy" override def name: String = floppy.name.getOrElse(super.name) - override def label: Option[String] = floppy.label.labelOption + override def diskLabel: Option[String] = floppy.label.labelOption - override def isLabelWriteable: Boolean = floppy.label.isInstanceOf[ReadWriteLabel] - - override def setLabel(label: Option[String]): Unit = { + override def diskLabel_=(label: Option[String]): Unit = { floppy.label.setLabel(label.orNull) } - override def color: Some[DyeColor] = Some(floppy.color) + override def isLabelWriteable: Boolean = floppy.label.isInstanceOf[ReadWriteLabel] - override def setColor(color: DyeColor): Unit = { + override def color: DyeColor = floppy.color + + override def color_=(color: DyeColor): Unit = { floppy.color = color } override def setManaged(managed: Boolean): Unit = { - val label = this.label + val label = this.diskLabel reinserting { floppy = @@ -41,13 +48,13 @@ class FloppyItem(var floppy: Floppy) extends Item with ComponentItem with Persis else new FloppyUnmanaged(floppy.name, floppy.color) } - setLabel(label) + diskLabel = label } override def fillTooltip(tooltip: ItemTooltip): Unit = { super.fillTooltip(tooltip) - for (label <- label) { + for (label <- diskLabel) { if (label != name) { // this is true for loot floppies addDiskLabelTooltip(tooltip, label) diff --git a/src/main/scala/ocelot/desktop/inventory/item/HddItem.scala b/src/main/scala/ocelot/desktop/inventory/item/HddItem.scala index 87f1a61..60a20b2 100644 --- a/src/main/scala/ocelot/desktop/inventory/item/HddItem.scala +++ b/src/main/scala/ocelot/desktop/inventory/item/HddItem.scala @@ -6,7 +6,7 @@ import ocelot.desktop.inventory.traits.{ComponentItem, DiskItem, PersistableItem import ocelot.desktop.inventory.{Item, ItemFactory, ItemRecoverer} import ocelot.desktop.ui.widget.tooltip.ItemTooltip import totoro.ocelot.brain.entity.fs.{Label, ReadWriteLabel} -import totoro.ocelot.brain.entity.traits.{Disk, Entity} +import totoro.ocelot.brain.entity.traits.{Disk, Entity, Environment} import totoro.ocelot.brain.entity.{HDDManaged, HDDUnmanaged} import totoro.ocelot.brain.util.Tier.Tier @@ -29,14 +29,14 @@ class HddItem(var hdd: Hdd) extends Item with ComponentItem with PersistableItem case Hdd.Unmanaged(hdd) => hdd.label } - def label: Option[String] = fsLabel.labelOption + def diskLabel: Option[String] = fsLabel.labelOption + + override def diskLabel_=(label: Option[String]): Unit = fsLabel.setLabel(label.orNull) override def isLabelWriteable: Boolean = fsLabel.isInstanceOf[ReadWriteLabel] - override def setLabel(label: Option[String]): Unit = fsLabel.setLabel(label.orNull) - override def setManaged(managed: Boolean): Unit = { - val label = this.label + val label = this.diskLabel reinserting { hdd = @@ -44,13 +44,13 @@ class HddItem(var hdd: Hdd) extends Item with ComponentItem with PersistableItem else Hdd(new HDDUnmanaged(tier.get)) } - setLabel(label) + diskLabel = label } override def fillTooltip(tooltip: ItemTooltip): Unit = { super.fillTooltip(tooltip) - label.foreach(addDiskLabelTooltip(tooltip, _)) + diskLabel.foreach(addDiskLabelTooltip(tooltip, _)) hdd match { case Hdd.Managed(hdd) => addSourcePathTooltip(tooltip, hdd) diff --git a/src/main/scala/ocelot/desktop/inventory/item/LinkedCardItem.scala b/src/main/scala/ocelot/desktop/inventory/item/LinkedCardItem.scala index 6ccdda9..f5ae6cd 100644 --- a/src/main/scala/ocelot/desktop/inventory/item/LinkedCardItem.scala +++ b/src/main/scala/ocelot/desktop/inventory/item/LinkedCardItem.scala @@ -22,7 +22,7 @@ class LinkedCardItem(val linkedCard: LinkedCard) extends Item with ComponentItem override def fillRmbMenu(menu: ContextMenu): Unit = { menu.addEntry( - ContextMenuEntry("Set channel", IconSource.Antenna) { + ContextMenuEntry("Set channel", IconSource.Icons.Antenna) { new TunnelDialog( tunnel => linkedCard.tunnel = tunnel, linkedCard.tunnel, diff --git a/src/main/scala/ocelot/desktop/inventory/item/OcelotCardItem.scala b/src/main/scala/ocelot/desktop/inventory/item/OcelotCardItem.scala index 69a6629..a285054 100644 --- a/src/main/scala/ocelot/desktop/inventory/item/OcelotCardItem.scala +++ b/src/main/scala/ocelot/desktop/inventory/item/OcelotCardItem.scala @@ -23,7 +23,7 @@ class OcelotCardItem(val ocelotCard: OcelotCard) override def tooltipNameColor: Color = ColorScheme("OcelotCardTooltip") override def fillRmbMenu(menu: ContextMenu): Unit = { - menu.addEntry(ContextMenuEntry("Open console", IconSource.Window) { + menu.addEntry(ContextMenuEntry("Open console", IconSource.Icons.Window) { window.open() }) diff --git a/src/main/scala/ocelot/desktop/inventory/item/RedstoneCardItem.scala b/src/main/scala/ocelot/desktop/inventory/item/RedstoneCardItem.scala index 29635dc..60a86ac 100644 --- a/src/main/scala/ocelot/desktop/inventory/item/RedstoneCardItem.scala +++ b/src/main/scala/ocelot/desktop/inventory/item/RedstoneCardItem.scala @@ -13,7 +13,7 @@ import totoro.ocelot.brain.nbt.NBTTagCompound import totoro.ocelot.brain.util.Tier import totoro.ocelot.brain.util.Tier.Tier -abstract class RedstoneCardItem extends Item with ComponentItem with PersistableItem with CardItem {} +abstract class RedstoneCardItem extends Item with ComponentItem with PersistableItem with CardItem object RedstoneCardItem { abstract class Factory extends ItemFactory { @@ -43,7 +43,7 @@ object RedstoneCardItem { } override def fillRmbMenu(menu: ContextMenu): Unit = { - menu.addEntry(ContextMenuEntry("Redstone I/O", IconSource.ArrowRight) { + menu.addEntry(ContextMenuEntry("Redstone I/O", IconSource.Icons.ArrowRight) { windowed.window.open() }) @@ -63,6 +63,12 @@ object RedstoneCardItem { windowed.save(nbt) } + + override def dispose(): Unit = { + windowed.closeAndDisposeWindow() + + super.dispose() + } } object Tier1 { @@ -93,7 +99,7 @@ object RedstoneCardItem { } override def fillRmbMenu(menu: ContextMenu): Unit = { - menu.addEntry(ContextMenuEntry("Bundled I/O", IconSource.LinesHorizontal) { + menu.addEntry(ContextMenuEntry("Bundled I/O", IconSource.Icons.LinesHorizontal) { windowed.window.open() }) @@ -111,6 +117,12 @@ object RedstoneCardItem { windowed.save(nbt) } + + override def dispose(): Unit = { + windowed.closeAndDisposeWindow() + + super.dispose() + } } object Tier2 { diff --git a/src/main/scala/ocelot/desktop/inventory/item/SelfDestructingCardItem.scala b/src/main/scala/ocelot/desktop/inventory/item/SelfDestructingCardItem.scala index 380c87c..83bd7ff 100644 --- a/src/main/scala/ocelot/desktop/inventory/item/SelfDestructingCardItem.scala +++ b/src/main/scala/ocelot/desktop/inventory/item/SelfDestructingCardItem.scala @@ -22,7 +22,7 @@ class SelfDestructingCardItem(val card: SelfDestructingCard) tooltip.addLine( if (card.remainingTime < 0) "Fuse has not been set" else if (card.remainingTime == 0) "BOOM!" - else card.remainingTime.toString + else f"Time to explosion: ${card.remainingTime / 20f}%.2fs" ) } } diff --git a/src/main/scala/ocelot/desktop/inventory/item/SoundCardItem.scala b/src/main/scala/ocelot/desktop/inventory/item/SoundCardItem.scala index a473d2a..2913a4a 100644 --- a/src/main/scala/ocelot/desktop/inventory/item/SoundCardItem.scala +++ b/src/main/scala/ocelot/desktop/inventory/item/SoundCardItem.scala @@ -1,25 +1,52 @@ package ocelot.desktop.inventory.item +import ocelot.desktop.audio.{Audio, SoundCategory, SoundSamples, SoundSource, SoundStream} import ocelot.desktop.graphics.IconSource import ocelot.desktop.inventory.traits.{CardItem, ComponentItem, PersistableItem} import ocelot.desktop.inventory.{Item, ItemFactory, ItemRecoverer} +import ocelot.desktop.ui.event.BrainEvent import ocelot.desktop.ui.widget.card.SoundCardWindow import ocelot.desktop.ui.widget.contextmenu.{ContextMenu, ContextMenuEntry} import ocelot.desktop.ui.widget.window.Windowed +import ocelot.desktop.util.Lazy +import totoro.ocelot.brain.Settings import totoro.ocelot.brain.entity.sound_card.SoundCard -import totoro.ocelot.brain.entity.traits.{Entity, Environment} +import totoro.ocelot.brain.event.SoundCardAudioEvent import totoro.ocelot.brain.util.Tier import totoro.ocelot.brain.util.Tier.Tier class SoundCardItem(val soundCard: SoundCard) - extends Item with ComponentItem with PersistableItem with CardItem with Windowed[SoundCardWindow] { + extends Item + with ComponentItem + with PersistableItem + with CardItem + with Windowed[SoundCardWindow] { override def createWindow(): SoundCardWindow = new SoundCardWindow(soundCard) - override def entity: Entity with Environment = soundCard + override def entity: SoundCard = soundCard + + private val streamPair = Lazy(Audio.newStream(SoundCategory.Records)) + private def stream: SoundStream = streamPair.getSync._1 + private def source: SoundSource = streamPair.getSync._2 + + eventHandlers += { + case BrainEvent(event: SoundCardAudioEvent) if !Audio.isDisabled => + val samples = SoundSamples(event.data, Settings.get.soundCardSampleRate, SoundSamples.Format.Mono8) + stream.enqueue(samples) + source.volume = event.volume + } + + override def dispose(): Unit = { + super.dispose() + + for (stream <- streamPair.getOption) { + stream._2.stop() + } + } override def fillRmbMenu(menu: ContextMenu): Unit = { - menu.addEntry(ContextMenuEntry("Open card interface", IconSource.Window) { + menu.addEntry(ContextMenuEntry("Open card interface", IconSource.Icons.Window) { window.open() }) diff --git a/src/main/scala/ocelot/desktop/inventory/traits/ComponentItem.scala b/src/main/scala/ocelot/desktop/inventory/traits/ComponentItem.scala index 53dab89..4e79a24 100644 --- a/src/main/scala/ocelot/desktop/inventory/traits/ComponentItem.scala +++ b/src/main/scala/ocelot/desktop/inventory/traits/ComponentItem.scala @@ -22,7 +22,7 @@ trait ComponentItem extends EntityItem { } } - private val copyAddressEntry = ContextMenuEntry("Copy address", IconSource.Copy) { + private val copyAddressEntry = ContextMenuEntry("Copy address", IconSource.Icons.Copy) { UiHandler.clipboard = entity.node.address } diff --git a/src/main/scala/ocelot/desktop/inventory/traits/CpuLikeItem.scala b/src/main/scala/ocelot/desktop/inventory/traits/CpuLikeItem.scala index ee98e5e..023957e 100644 --- a/src/main/scala/ocelot/desktop/inventory/traits/CpuLikeItem.scala +++ b/src/main/scala/ocelot/desktop/inventory/traits/CpuLikeItem.scala @@ -13,7 +13,7 @@ trait CpuLikeItem extends ComponentItem { override def entity: Entity with GenericCPU override def fillRmbMenu(menu: ContextMenu): Unit = { - menu.addEntry(new ContextMenuSubmenu("Set architecture", Some(ContextMenuIcon(IconSource.Microchip))) { + menu.addEntry(new ContextMenuSubmenu("Set architecture", Some(ContextMenuIcon(IconSource.Icons.Microchip))) { for (arch <- entity.allArchitectures) { val name = MachineAPI.getArchitectureName(arch) + (if (arch == entity.architecture) " (current)" else "") diff --git a/src/main/scala/ocelot/desktop/inventory/traits/DiskItem.scala b/src/main/scala/ocelot/desktop/inventory/traits/DiskItem.scala index d006806..150d62f 100644 --- a/src/main/scala/ocelot/desktop/inventory/traits/DiskItem.scala +++ b/src/main/scala/ocelot/desktop/inventory/traits/DiskItem.scala @@ -4,40 +4,38 @@ import ocelot.desktop.OcelotDesktop import ocelot.desktop.graphics.IconSource import ocelot.desktop.node.nodes.RaidNode import ocelot.desktop.ui.widget.DiskEditWindow +import ocelot.desktop.ui.widget.DiskEditWindow.{CanChangeManaged, EditableDisk, Lockable} import ocelot.desktop.ui.widget.contextmenu.{ContextMenu, ContextMenuEntry} import ocelot.desktop.ui.widget.tooltip.ItemTooltip import ocelot.desktop.ui.widget.window.{Window, Windowed} import totoro.ocelot.brain.entity.traits.{Disk, DiskManaged, DiskRealPathAware, DiskUnmanaged, Entity} -import totoro.ocelot.brain.util.DyeColor import javax.swing.JFileChooser import scala.util.Try -/** A utility mixin for HDDs and floppies. */ -trait DiskItem extends ComponentItem with Windowed[DiskEditWindow] { +/** + * A utility mixin for HDDs and floppies. + */ +trait DiskItem + extends ComponentItem + with EditableDisk + with Lockable + with CanChangeManaged + with Windowed[DiskEditWindow] { + override def entity: Entity with Disk - def diskKind: String + override def disk: Option[Entity with Disk] = Option(entity) - def color: Option[DyeColor] = None + override def capacity: Long = disk.fold(0L)(_.capacity) - def setColor(color: DyeColor): Unit = throw new UnsupportedOperationException() - - def label: Option[String] - - def isLabelWriteable: Boolean - - def setLabel(label: Option[String]): Unit - - def setManaged(managed: Boolean): Unit - - def lock(): Unit = reinserting { + override def lock(): Unit = reinserting { entity.setLocked(OcelotDesktop.player.nickname) } - override def createWindow(): DiskEditWindow = new DiskEditWindow(DiskItem.this) + override def createWindow(): DiskEditWindow = new DiskEditWindow(this) - def isEditingAllowed: Boolean = { + def editingAllowed: Boolean = { slot.fold(false)(_.inventory match { case _: RaidNode => false case _ => true @@ -45,16 +43,19 @@ trait DiskItem extends ComponentItem with Windowed[DiskEditWindow] { } override def fillRmbMenu(menu: ContextMenu): Unit = { - if (isEditingAllowed) { + if (editingAllowed) { // Real path entity match { case diskManaged: DiskManaged => - DiskItem.addRealPathContextMenuEntries(menu, diskManaged, + DiskItem.addRealPathContextMenuEntries( + menu, + diskManaged, realPathSetter => { reinserting { realPathSetter() } - }) + }, + ) case _ => } @@ -86,18 +87,22 @@ trait DiskItem extends ComponentItem with Windowed[DiskEditWindow] { override def fillTooltip(tooltip: ItemTooltip): Unit = { super.fillTooltip(tooltip) + if (entity != null) { - tooltip.addLine(s"Capacity: ${entity.capacity / 1024} kB") + tooltip.addLine(s"Capacity: ${capacity / 1024} kB") } } } object DiskItem { - def addRealPathContextMenuEntries(menu: ContextMenu, diskRealPathAware: DiskRealPathAware, - realPathSetter: (() => Unit) => Unit): Unit = { + def addRealPathContextMenuEntries( + menu: ContextMenu, + diskRealPathAware: DiskRealPathAware, + realPathSetter: (() => Unit) => Unit, + ): Unit = { menu.addEntry(ContextMenuEntry( if (diskRealPathAware.customRealPath.isDefined) "Change directory" else "Set directory", - IconSource.Folder, + IconSource.Icons.Folder, ) { OcelotDesktop.showFileChooserDialog(JFileChooser.OPEN_DIALOG, JFileChooser.DIRECTORIES_ONLY) { dir => Try { @@ -112,7 +117,7 @@ object DiskItem { }) if (diskRealPathAware.customRealPath.isDefined) { - menu.addEntry(ContextMenuEntry("Reset directory", IconSource.FolderSlash) { + menu.addEntry(ContextMenuEntry("Reset directory", IconSource.Icons.FolderSlash) { realPathSetter(() => { // trigger component_removed / component_added signals diskRealPathAware.customRealPath = None @@ -122,7 +127,7 @@ object DiskItem { } def addEditDiskContextMenuEntries[T <: Window](menu: ContextMenu, windowed: Windowed[T]): Unit = { - menu.addEntry(ContextMenuEntry("Edit disk", IconSource.Edit) { + menu.addEntry(ContextMenuEntry("Edit disk", IconSource.Icons.Edit) { windowed.window.open() }) } diff --git a/src/main/scala/ocelot/desktop/node/BoomCardFxHandler.scala b/src/main/scala/ocelot/desktop/node/BoomCardFxHandler.scala new file mode 100644 index 0000000..4e110ee --- /dev/null +++ b/src/main/scala/ocelot/desktop/node/BoomCardFxHandler.scala @@ -0,0 +1,102 @@ +package ocelot.desktop.node + +import ocelot.desktop.audio.{SoundBuffers, SoundCategory, SoundSource} +import ocelot.desktop.color.Color +import ocelot.desktop.geometry.FloatUtils.ExtendedFloat +import ocelot.desktop.graphics.{Graphics, IconSource} +import ocelot.desktop.inventory.item.SelfDestructingCardItem +import ocelot.desktop.node.BoomCardFxHandler.{ExpandIntensity, ExpandPeriod, FlickerAlpha, FlickerDuty, GlowAlpha, MaxSize, MinSize} +import ocelot.desktop.ui.UiHandler +import ocelot.desktop.ui.event.BrainEvent +import ocelot.desktop.{ColorScheme, OcelotDesktop} +import totoro.ocelot.brain.event.SelfDestructingCardBoomEvent + +trait BoomCardFxHandler extends Node with PositionalSoundSourcesNode with SmokeParticleNode { + private var boomPhase: Float = -1 + + private lazy val explosionSound = { + SoundSource.fromBuffer(SoundBuffers.MinecraftExplosion, SoundCategory.Environment) + } + + private lazy val countdownBeepSound = { + SoundSource.fromBuffer(SoundBuffers.SelfDestructingCardCountdownBeep, SoundCategory.Environment) + } + + override def soundSources: Seq[SoundSource] = super.soundSources ++ Seq( + explosionSound, + countdownBeepSound, + ) + + eventHandlers += { + case BrainEvent(_: SelfDestructingCardBoomEvent) => + OcelotDesktop.updateThreadTasks.add(() => { + explosionSound.play() + emitSmoke() + destroy() + }) + } + + protected def selfDestructingCards: IterableOnce[SelfDestructingCardItem] + + private var phase = 0f + private var flickerPhase = 0f + + private def updateBoomCardState(): Unit = { + phase = (phase + UiHandler.dt / ExpandPeriod) % 1f + flickerPhase = 0f.max(flickerPhase - UiHandler.dt) + boomPhase = -1 + + for (item <- selfDestructingCards) { + if (item.card.time > 0) { + // If multiple SDCs are ticking, let the most soon exploding one to define the glow + boomPhase = boomPhase.max(1 - item.card.time.toFloat / item.card.initialTime) + + if (item.card.lastBeepTime < 0 || item.card.lastBeepTime - item.card.time >= 20) { + countdownBeepSound.play() + item.card.lastBeepTime = item.card.time + flickerPhase = FlickerDuty + } + } + } + } + + override def update(): Unit = { + super.update() + updateBoomCardState() + } + + private def expandFactor(phase: Float): Float = { + math.sin(2 * math.Pi * phase).toFloat + } + + override def drawLight(g: Graphics): Unit = { + super.drawLight(g) + + if (boomPhase > 0) { + val expand = expandFactor(phase) + val glowSize = MinSize.lerp(MaxSize, boomPhase + ExpandIntensity * expand) + val alpha = boomPhase * GlowAlpha + + if (flickerPhase >= 0.01) { + g.rect(bounds, Color.White.withAlpha(FlickerAlpha * (1 - boomPhase))) + } + + g.sprite( + IconSource.Nodes.LampGlow, + position - size * glowSize, + size * (1 + 2 * glowSize), + ColorScheme("BoomCardGlowStart").lerp(ColorScheme("BoomCardGlowEnd"), boomPhase).withAlpha(alpha), + ) + } + } +} + +object BoomCardFxHandler { + private val ExpandPeriod = 1f + private val ExpandIntensity = 0.05f + private val FlickerDuty = 0.33f + private val FlickerAlpha = 0.05f + private val MinSize = 0.1f + private val MaxSize = 0.5f + private val GlowAlpha = 0.4f +} diff --git a/src/main/scala/ocelot/desktop/node/ComputerAwareNode.scala b/src/main/scala/ocelot/desktop/node/ComputerAwareNode.scala index 671a10c..c12da38 100644 --- a/src/main/scala/ocelot/desktop/node/ComputerAwareNode.scala +++ b/src/main/scala/ocelot/desktop/node/ComputerAwareNode.scala @@ -1,9 +1,6 @@ package ocelot.desktop.node -import ocelot.desktop.OcelotDesktop -import ocelot.desktop.{Settings => DesktopSettings} import ocelot.desktop.audio._ -import ocelot.desktop.geometry.Vector2D import ocelot.desktop.graphics.{Graphics, IconSource} import ocelot.desktop.inventory.SyncedInventory import ocelot.desktop.node.ComputerAwareNode._ @@ -11,85 +8,39 @@ import ocelot.desktop.node.Node.Size import ocelot.desktop.ui.UiHandler import ocelot.desktop.ui.event.BrainEvent import ocelot.desktop.ui.event.handlers.DiskActivityHandler -import ocelot.desktop.ui.widget.ComputerErrorMessageLabel +import ocelot.desktop.ui.particle.Particle import ocelot.desktop.util.Messages -import totoro.ocelot.brain.Settings +import ocelot.desktop.{ColorScheme, Settings => DesktopSettings} import totoro.ocelot.brain.entity.traits.{Entity, Environment, WorkspaceAware} import totoro.ocelot.brain.event._ import java.util.Calendar -import scala.collection.mutable.ArrayBuffer abstract class ComputerAwareNode(entity: Entity with Environment with WorkspaceAware) - extends EntityNode(entity) + extends EntityNode(entity) with SyncedInventory with DiskActivityHandler with OcelotLogParticleNode + with BoomCardFxHandler with ShiftClickNode { - // access should be synchronized because messages are added in the update thread - private val messages = ArrayBuffer.empty[(Float, ComputerErrorMessageLabel)] - - private def addErrorMessage(message: ComputerErrorMessageLabel): Unit = messages.synchronized { - messages += ((0f, message)) - } - - private lazy val soundCardSounds: (SoundStream, SoundSource) = Audio.newStream(SoundCategory.Records) - private def soundCardStream: SoundStream = soundCardSounds._1 - private def soundCardSource: SoundSource = soundCardSounds._2 - eventHandlers += { case BrainEvent(event: MachineCrashEvent) => val message = Messages.lift(event.message) match { case Some(message) => logger.info(s"[EVENT] Machine crash (address = ${event.address})! Message code ${event.message}: $message") message - case None => logger.info(s"[EVENT] Machine crash (address = ${event.address})! Message: ${event.message}") event.message } - - addErrorMessage(new ComputerErrorMessageLabel(this, message)) + UiHandler.root.workspaceView.particleSystem.add(new ErrorMessageParticle(message)) case BrainEvent(event: BeepEvent) if !Audio.isDisabled => BeepGenerator.newBeep(".", event.frequency, event.duration).play() case BrainEvent(event: BeepPatternEvent) if !Audio.isDisabled => BeepGenerator.newBeep(event.pattern, 1000, 200).play() - - case BrainEvent(event: SoundCardAudioEvent) if !Audio.isDisabled => - val samples = SoundSamples(event.data, Settings.get.soundCardSampleRate, SoundSamples.Format.Mono8) - soundCardStream.enqueue(samples) - soundCardSource.volume = event.volume - - case BrainEvent(_: SelfDestructingCardBoomEvent) => - OcelotDesktop.updateThreadTasks.add(() => { - SoundSource.MinecraftExplosion.play() - destroy() - }) - } - - override def update(): Unit = { - messages.synchronized { - messages.mapInPlace { case (t, message) => (t + ErrorMessageMoveSpeed * UiHandler.dt, message) } - messages.filterInPlace(_._1 <= 1f) - } - - super.update() - } - - private def drawMessageParticles(g: Graphics): Unit = messages.synchronized { - for ((time, message) <- messages.reverseIterator) { - message.position = message.initialPosition + Vector2D(0, -MaxErrorMessageDistance * time) - message.alpha = 1 - time - message.draw(g) - } - } - - override def drawParticles(g: Graphics): Unit = { - super.drawParticles(g) - drawMessageParticles(g) } protected def drawOverlay(g: Graphics): Unit = HolidayIcon match { @@ -110,9 +61,19 @@ abstract class ComputerAwareNode(entity: Entity with Environment with WorkspaceA override def draw(g: Graphics): Unit = { super.draw(g) - drawOverlay(g) } + + private class ErrorMessageParticle(message: String) extends Particle(speed = ErrorMessageMoveSpeed) { + private val offsetX = size.width / 2 - message.length * 4 + private val offsetY = -8 + override def draw(g: Graphics): Unit = { + g.setSmallFont() + g.foreground = ColorScheme("ErrorMessage").withAlpha(1 - (2 * time - 1).min(1).max(0)) + g.text(position.x + offsetX, position.y + offsetY - MaxErrorMessageDistance * time, message) + g.setNormalFont() + } + } } object ComputerAwareNode { diff --git a/src/main/scala/ocelot/desktop/node/EntityNode.scala b/src/main/scala/ocelot/desktop/node/EntityNode.scala index d7a7768..e1f29fb 100644 --- a/src/main/scala/ocelot/desktop/node/EntityNode.scala +++ b/src/main/scala/ocelot/desktop/node/EntityNode.scala @@ -35,7 +35,7 @@ abstract class EntityNode(val entity: Entity with Environment) extends Node { override def setupContextMenu(menu: ContextMenu, event: ClickEvent): Unit = { if (exposeAddress && entity.node != null && entity.node.address != null) { menu.addEntry( - ContextMenuEntry("Copy address", IconSource.Copy) { + ContextMenuEntry("Copy address", IconSource.Icons.Copy) { UiHandler.clipboard = entity.node.address } ) diff --git a/src/main/scala/ocelot/desktop/node/LabeledNode.scala b/src/main/scala/ocelot/desktop/node/LabeledNode.scala index a3b7644..3599c79 100644 --- a/src/main/scala/ocelot/desktop/node/LabeledNode.scala +++ b/src/main/scala/ocelot/desktop/node/LabeledNode.scala @@ -26,7 +26,7 @@ trait LabeledNode extends Node { } override def setupContextMenu(menu: ContextMenu, event: ClickEvent): Unit = { - menu.addEntry(ContextMenuEntry("Set label", IconSource.Label) { + menu.addEntry(ContextMenuEntry("Set label", IconSource.Icons.Label) { new InputDialog( "Set label", text => { @@ -40,6 +40,8 @@ trait LabeledNode extends Node { } override def drawLabel(g: Graphics): Unit = { + super.drawLabel(g) + for (label <- label) { g.setSmallFont() g.background = RGBAColor(0, 0, 0, 0) diff --git a/src/main/scala/ocelot/desktop/node/Node.scala b/src/main/scala/ocelot/desktop/node/Node.scala index 7a83cf7..1734cce 100644 --- a/src/main/scala/ocelot/desktop/node/Node.scala +++ b/src/main/scala/ocelot/desktop/node/Node.scala @@ -14,6 +14,7 @@ import ocelot.desktop.util.Persistable import ocelot.desktop.util.animation.ColorAnimation import totoro.ocelot.brain.nbt.NBTTagCompound import totoro.ocelot.brain.network +import totoro.ocelot.brain.util.Direction.Direction import scala.collection.mutable.ArrayBuffer @@ -89,12 +90,12 @@ abstract class Node extends Widget with MouseHandler with HoverHandler with Pers def setupContextMenu(menu: ContextMenu, event: ClickEvent): Unit = { if (ports.nonEmpty) { - menu.addEntry(ContextMenuEntry("Disconnect", IconSource.LinkSlash, SoundSource.InterfaceClickLow) { + menu.addEntry(ContextMenuEntry("Disconnect", IconSource.Icons.LinkSlash, SoundSource.InterfaceClickLow) { disconnectFromAll() }) } - menu.addEntry(ContextMenuEntry("Remove", IconSource.Delete, SoundSource.InterfaceClickLow) { + menu.addEntry(ContextMenuEntry("Remove", IconSource.Icons.Delete, SoundSource.InterfaceClickLow) { destroy() }) } @@ -103,11 +104,11 @@ abstract class Node extends Widget with MouseHandler with HoverHandler with Pers super.update() if (isHovered || isMoving) { - root.get.statusBar.addMouseEntry("icons/RMB", "Menu") - root.get.statusBar.addMouseEntry("icons/DragLMB", "Move node") + root.get.statusBar.addMouseEntry(IconSource.Icons.RMB, "Menu") + root.get.statusBar.addMouseEntry(IconSource.Icons.DragLMB, "Move node") if (ports.nonEmpty) { - root.get.statusBar.addMouseEntry("icons/DragRMB", "Connect/Disconnect") + root.get.statusBar.addMouseEntry(IconSource.Icons.DragRMB, "Connect/Disconnect") } } } @@ -118,7 +119,7 @@ abstract class Node extends Widget with MouseHandler with HoverHandler with Pers super.dispose() } - def iconSource: IconSource = IconSource.NA + def icon: IconSource = IconSource.Icons.NA def iconColor: Color = RGBAColor(255, 255, 255) @@ -126,6 +127,8 @@ abstract class Node extends Widget with MouseHandler with HoverHandler with Pers def getNodeByPort(port: NodePort): network.Node = throw new IllegalArgumentException("this node has no ports") + def rotatable: Boolean + def connections: Iterator[(NodePort, Node, NodePort)] = _connections.iterator def connect(portA: NodePort, node: Node, portB: NodePort): Unit = { @@ -260,29 +263,51 @@ abstract class Node extends Widget with MouseHandler with HoverHandler with Pers drawHighlight(g) g.sprite( - iconSource.path, + icon, position.x + HighlightThickness, position.y + HighlightThickness, size.width - HighlightThickness * 2, size.height - HighlightThickness * 2, iconColor, - iconSource.animation, ) } + private def drawPortLegend(g: Graphics): Unit = { + if (highlight.color.a < 0.001) return + + g.setSmallFont() + g.background = PortLegendBg.mapA(_ * highlight.color.a) + + val sidedPorts = ports.iterator.filter(_.direction.isDefined).toSeq.sorted + val legendHeight = sidedPorts.size * 8 + val startY = position.y + (height - legendHeight) / 2 + + for ((port, line) <- sidedPorts.iterator.zipWithIndex) { + g.foreground = port.getColor.toRGBANorm.mapA(_ * highlight.color.a) + g.text(bounds.max.x + HoverInfoMargin, startY + line * 8, directionLabel(port.direction.get), shrink = 1) + } + + g.setNormalFont() + } + def drawLight(g: Graphics): Unit = {} - def drawLabel(g: Graphics): Unit = {} - - def drawParticles(g: Graphics): Unit = {} + def drawLabel(g: Graphics): Unit = { + drawPortLegend(g) + } def drawPorts(g: Graphics): Unit = { for ((port, rects) <- portsBounds) { val color = port.getColor - for (rect <- rects) + for (rect <- rects) { g.rect(rect, color) + } } } + + def directionLabel(direction: Direction): String = { + (if (rotatable) direction.side else direction.cardinal).capitalize + } } object Node { @@ -290,9 +315,12 @@ object Node { protected val HoveredHighlight: RGBAColor = RGBAColor(160, 160, 160) protected val NoHighlight: RGBAColor = RGBAColor(160, 160, 160, 0) + private val PortLegendBg = Color.Black.withAlpha(0.8f) + val TexelCount = 16f val Scale = 4f val HighlightThickness = 2f + private val HoverInfoMargin = 9f val NoHighlightSize: Float = TexelCount * Scale val Size: Float = NoHighlightSize + HighlightThickness * 2f diff --git a/src/main/scala/ocelot/desktop/node/NodePort.scala b/src/main/scala/ocelot/desktop/node/NodePort.scala index c65e8cc..3b7b67a 100644 --- a/src/main/scala/ocelot/desktop/node/NodePort.scala +++ b/src/main/scala/ocelot/desktop/node/NodePort.scala @@ -2,6 +2,7 @@ package ocelot.desktop.node import ocelot.desktop.ColorScheme import ocelot.desktop.color.Color +import ocelot.desktop.graphics.IconSource import totoro.ocelot.brain.util.Direction case class NodePort(direction: Option[Direction.Value] = None) extends Ordered[NodePort] { @@ -15,6 +16,11 @@ case class NodePort(direction: Option[Direction.Value] = None) extends Ordered[N case _ => ColorScheme("PortAny") } + def getIcon: IconSource = direction match { + case Some(direction) => IconSource.Icons.Side(direction) + case None => IconSource.Icons.SideAny + } + override def compare(that: NodePort): Int = this.direction.compare(that.direction) def toByte: Byte = direction match { diff --git a/src/main/scala/ocelot/desktop/node/NodeTypeWidget.scala b/src/main/scala/ocelot/desktop/node/NodeTypeWidget.scala index 641a675..0f5e9e3 100644 --- a/src/main/scala/ocelot/desktop/node/NodeTypeWidget.scala +++ b/src/main/scala/ocelot/desktop/node/NodeTypeWidget.scala @@ -2,7 +2,7 @@ package ocelot.desktop.node import ocelot.desktop.color.Color import ocelot.desktop.geometry.Size2D -import ocelot.desktop.graphics.Graphics +import ocelot.desktop.graphics.{Graphics, IconSource} import ocelot.desktop.node.Node.Size import ocelot.desktop.ui.event.handlers.{HoverHandler, MouseHandler} import ocelot.desktop.ui.event.{ClickEvent, HoverEvent, MouseEvent} @@ -39,20 +39,19 @@ class NodeTypeWidget(val nodeType: NodeType) extends Widget with MouseHandler wi val size = Spritesheet.spriteSize(nodeType.icon) * 4 g.sprite( - nodeType.icon.path, + nodeType.icon, position.x + Size / 2 - size.width / 2, position.y + Size / 2 - size.height / 2, size.width, size.height, nodeType.tier.map(TierColor.get).getOrElse(Color.White), - nodeType.icon.animation, ) } override def update(): Unit = { super.update() if (isHovered) { - root.get.statusBar.addMouseEntry("icons/LMB", "Add node") + root.get.statusBar.addMouseEntry(IconSource.Icons.LMB, "Add node") } } } diff --git a/src/main/scala/ocelot/desktop/node/OcelotLogParticleNode.scala b/src/main/scala/ocelot/desktop/node/OcelotLogParticleNode.scala index e490219..dda5257 100644 --- a/src/main/scala/ocelot/desktop/node/OcelotLogParticleNode.scala +++ b/src/main/scala/ocelot/desktop/node/OcelotLogParticleNode.scala @@ -8,62 +8,53 @@ import ocelot.desktop.graphics.Graphics import ocelot.desktop.node.OcelotLogParticleNode._ import ocelot.desktop.ui.UiHandler import ocelot.desktop.ui.event.BrainEvent +import ocelot.desktop.ui.particle.Particle -import scala.collection.mutable import scala.util.Random trait OcelotLogParticleNode extends Node { - private case class LogParticle( - var time: Float = -LogParticleGrow, - angle: Float = Random.between(0f, 2 * math.Pi.toFloat * LogParticleMaxAngle), - ) - - // access should be synchronized because log particles are added in the update thread - private val logParticles = mutable.ArrayDeque.empty[LogParticle] - - private def addLogParticle(): Unit = logParticles.synchronized { - if (logParticles.length < MaxLogParticles) { - logParticles += LogParticle() - } - } + private var queuedParticles: Int = 0 eventHandlers += { case BrainEvent(OcelotInterface.LogEvent.CardToUser(_, _)) => - addLogParticle() + queuedParticles += 1 } override def update(): Unit = { super.update() - - logParticles.synchronized { - logParticles.foreach(particle => particle.time += LogParticleMoveSpeed * UiHandler.dt) - logParticles.filterInPlace(_.time <= 1f) - } + spawnParticles() } - private def drawLogParticles(g: Graphics): Unit = logParticles.synchronized { - for (particle <- logParticles) { - val size = (1 + particle.time / LogParticleGrow).clamp() * LogParticleSize - val offset = particle.time.clamp() * LogParticleMoveDistance - val alpha = 1 - particle.time.clamp() + private def spawnParticles(): Unit = { + val system = UiHandler.root.workspaceView.particleSystem + val toSpawn = queuedParticles min (MaxLogParticles - system.count[LogParticle](Some(this))) max 0 - val r1 = (bounds.w max bounds.h) / math.sqrt(2) + offset + LogParticlePadding + for (_ <- 0 until toSpawn) { + system.add(new LogParticle) + } + + queuedParticles = 0 + } + + private class LogParticle extends Particle(time = -LogParticleGrow, speed = LogParticleMoveSpeed, origin = Some(this)) { + private val angle: Float = Random.between(0f, 2 * math.Pi.toFloat * LogParticleMaxAngle) + override def draw(g: Graphics): Unit = { + val size = (1 + time / LogParticleGrow).clamped() * LogParticleSize + val offset = time.clamped() * LogParticleMoveDistance + val alpha = 1 - time.clamped() + + val r1 = (bounds.w max bounds.h) / math.sqrt(2).toFloat + offset + LogParticlePadding val r2 = r1 + size for (i <- 0 until LogParticleCount) { - val angle = particle.angle + (2 * math.Pi).toFloat * i / LogParticleCount - val v = Vector2D.unit(angle) + val a = angle + (2 * math.Pi).toFloat * i / LogParticleCount + val v = Vector2D.unit(a) val p1 = v * r1 + bounds.center val p2 = v * r2 + bounds.center g.line(p1, p2, 1f, ColorScheme("LogParticle").mapA(_ => alpha)) } } } - - override def drawParticles(g: Graphics): Unit = { - super.drawParticles(g) - drawLogParticles(g) - } } object OcelotLogParticleNode { diff --git a/src/main/scala/ocelot/desktop/node/PositionalSoundSourcesNode.scala b/src/main/scala/ocelot/desktop/node/PositionalSoundSourcesNode.scala index 054c2eb..dfce7a5 100644 --- a/src/main/scala/ocelot/desktop/node/PositionalSoundSourcesNode.scala +++ b/src/main/scala/ocelot/desktop/node/PositionalSoundSourcesNode.scala @@ -4,18 +4,22 @@ import ocelot.desktop.audio.SoundSource import ocelot.desktop.geometry.Vector3D import ocelot.desktop.{OcelotDesktop, Settings} +/** + * Updates sound sources' position depending on where the camera is. + * + * @note OpenAL only applies positioning to mono sources! + * If your source is stereo, this trait will have no audible effect. + */ trait PositionalSoundSourcesNode extends Node { // Every node can have multiple sound sources playing at the same time - def soundSources: Seq[SoundSource] + def soundSources: Seq[SoundSource] = Seq() override def update(): Unit = { super.update() - var soundPosition: Vector3D = null - // Calculating position of sound source relative to center of workspace // but only if corresponding setting is enabled - if (Settings.get.soundPositional) { + val soundPosition = if (Settings.get.soundPositional) { val rootWidthHalf = OcelotDesktop.root.width / 2 val rootHeightHalf = OcelotDesktop.root.height / 2 @@ -26,13 +30,13 @@ trait PositionalSoundSourcesNode extends Node { // large monitors the sound may become too "non-audiophile" val limit = 0.05f - soundPosition = Vector3D( + Vector3D( (nodeCenterX - rootWidthHalf) / rootWidthHalf * limit, (nodeCenterY - rootHeightHalf) / rootHeightHalf * limit, 0, ) } else { - soundPosition = Vector3D.Zero + Vector3D.Zero } for (soundSource <- soundSources) diff --git a/src/main/scala/ocelot/desktop/node/ShiftClickNode.scala b/src/main/scala/ocelot/desktop/node/ShiftClickNode.scala index 3293c64..d39e02d 100644 --- a/src/main/scala/ocelot/desktop/node/ShiftClickNode.scala +++ b/src/main/scala/ocelot/desktop/node/ShiftClickNode.scala @@ -1,5 +1,6 @@ package ocelot.desktop.node +import ocelot.desktop.graphics.IconSource import ocelot.desktop.ui.event.sources.KeyEvents import ocelot.desktop.ui.event.{ClickEvent, MouseEvent} @@ -22,6 +23,6 @@ trait ShiftClickNode extends Node { super.update() if (isHovered || isMoving) - root.get.statusBar.addKeyMouseEntry("icons/LMB", "SHIFT", hoveredShiftStatusBarText) + root.get.statusBar.addKeyMouseEntry(IconSource.Icons.LMB, "SHIFT", hoveredShiftStatusBarText) } } diff --git a/src/main/scala/ocelot/desktop/node/SmokeParticleNode.scala b/src/main/scala/ocelot/desktop/node/SmokeParticleNode.scala new file mode 100644 index 0000000..3b292b4 --- /dev/null +++ b/src/main/scala/ocelot/desktop/node/SmokeParticleNode.scala @@ -0,0 +1,79 @@ +package ocelot.desktop.node + +import ocelot.desktop.color.RGBAColorNorm +import ocelot.desktop.geometry.{Size2D, Vector2D} +import ocelot.desktop.graphics.{Graphics, IconSource} +import ocelot.desktop.node.SmokeParticleNode._ +import ocelot.desktop.ui.UiHandler +import ocelot.desktop.ui.particle.Particle +import ocelot.desktop.util.Spritesheet + +import scala.util.Random + +trait SmokeParticleNode extends Node { + protected def emitSmoke(): Unit = synchronized { + for (_ <- 1 to randomCount) { + UiHandler.root.workspaceView.particleSystem.add(new SmokeParticle) + } + } + + private class SmokeParticle extends Particle(ttl = randomDuration) { + private val color: RGBAColorNorm = randomColor + private var velocity: Vector2D = Vector2D(randomVelocityComponent, randomVelocityComponent) + private var offset: Vector2D = Vector2D(0, 0) + + override def update(dt: Float): Unit = { + time += dt + offset += (velocity + SmokeParticleVolatilizationSpeed) * dt * speed + velocity *= math.pow(SmokeParticleVelocityDamping, dt).toFloat + } + + override def draw(g: Graphics): Unit = { + val spriteRect = Spritesheet.sprites(IconSource.Particles.Smoke.path) + + val animationFrameCount = spriteRect.h / spriteRect.w + val animationFrame = (time / ttl * animationFrameCount).toInt + + val particlePosition = bounds.center + offset - SmokeParticleSize.toVector * .5f + + g.sprite( + IconSource.Particles.Smoke.path, + particlePosition.x, + particlePosition.y, + SmokeParticleSize.width, + SmokeParticleSize.height, + color, + Some(spriteRect.copy( + y = spriteRect.y + animationFrame * spriteRect.w, + h = spriteRect.w, + )), + ) + } + } +} + +private object SmokeParticleNode { + private final val SmokeParticleSize: Size2D = Size2D(32, 32) + private final val SmokeParticleVelocityRange: Float = 300 + private final val SmokeParticleVolatilizationSpeed: Vector2D = Vector2D(0, -50) + private final val SmokeParticleVelocityDamping: Float = .1f + private final val SmokeParticleCount: (Int, Int) = (10, 20) + private final val SmokeParticleAnimationDuration: (Float, Float) = (1f, 4f) + + private def randomVelocityComponent: Float = Random.between( + -SmokeParticleVelocityRange, + SmokeParticleVelocityRange, + ) + private def randomColor: RGBAColorNorm = { + val channel = Random.between(.5f, .9f) + RGBAColorNorm(channel, channel, channel) + } + private def randomDuration: Float = Random.between( + SmokeParticleAnimationDuration._1, + SmokeParticleAnimationDuration._2, + ) + private def randomCount: Int = Random.between( + SmokeParticleCount._1, + SmokeParticleCount._2, + ) +} diff --git a/src/main/scala/ocelot/desktop/node/WindowedNode.scala b/src/main/scala/ocelot/desktop/node/WindowedNode.scala index a09e05c..caa80a3 100644 --- a/src/main/scala/ocelot/desktop/node/WindowedNode.scala +++ b/src/main/scala/ocelot/desktop/node/WindowedNode.scala @@ -1,9 +1,9 @@ package ocelot.desktop.node +import ocelot.desktop.graphics.IconSource import ocelot.desktop.ui.event.sources.KeyEvents import ocelot.desktop.ui.event.{ClickEvent, MouseEvent} import ocelot.desktop.ui.widget.window.{Window, Windowed} -import org.lwjgl.input.Keyboard trait WindowedNode[T <: Window] extends Node with Windowed[T] { override def dispose(): Unit = { @@ -14,7 +14,7 @@ trait WindowedNode[T <: Window] extends Node with Windowed[T] { override def update(): Unit = { if (isHovered || isMoving) - root.get.statusBar.addMouseEntry("icons/LMB", if (windowCreated && window.isOpen) "Close" else "Open") + root.get.statusBar.addMouseEntry(IconSource.Icons.LMB, if (windowCreated && window.isOpen) "Close" else "Open") super.update() } diff --git a/src/main/scala/ocelot/desktop/node/nodes/CableNode.scala b/src/main/scala/ocelot/desktop/node/nodes/CableNode.scala index 11e3e69..f3c8e34 100644 --- a/src/main/scala/ocelot/desktop/node/nodes/CableNode.scala +++ b/src/main/scala/ocelot/desktop/node/nodes/CableNode.scala @@ -6,7 +6,9 @@ import ocelot.desktop.node.EntityNode import totoro.ocelot.brain.entity.Cable class CableNode(val cable: Cable) extends EntityNode(cable) { - override def iconSource: IconSource = IconSource.Nodes.Cable + override def icon: IconSource = IconSource.Nodes.Cable + + override def rotatable: Boolean = false override def minimumSize: Size2D = Size2D(36, 36) diff --git a/src/main/scala/ocelot/desktop/node/nodes/CameraNode.scala b/src/main/scala/ocelot/desktop/node/nodes/CameraNode.scala index 60b93e2..8ef7562 100644 --- a/src/main/scala/ocelot/desktop/node/nodes/CameraNode.scala +++ b/src/main/scala/ocelot/desktop/node/nodes/CameraNode.scala @@ -6,7 +6,9 @@ import ocelot.desktop.node.{EntityNode, LabeledEntityNode, WindowedNode} import ocelot.desktop.windows.CameraWindow class CameraNode(val camera: Camera) extends EntityNode(camera) with LabeledEntityNode with WindowedNode[CameraWindow] { - override def iconSource: IconSource = IconSource.Nodes.Camera + override def icon: IconSource = IconSource.Nodes.Camera + + override def rotatable: Boolean = true override def createWindow(): CameraWindow = new CameraWindow(this) } diff --git a/src/main/scala/ocelot/desktop/node/nodes/ChestNode.scala b/src/main/scala/ocelot/desktop/node/nodes/ChestNode.scala index fd205fa..6ccaba3 100644 --- a/src/main/scala/ocelot/desktop/node/nodes/ChestNode.scala +++ b/src/main/scala/ocelot/desktop/node/nodes/ChestNode.scala @@ -10,7 +10,9 @@ import ocelot.desktop.windows.ChestWindow class ChestNode extends LabeledNode with WindowedNode[ChestWindow] with PersistedInventory { override type I = Item with PersistableItem - override def iconSource: IconSource = IconSource.Nodes.Chest + override def icon: IconSource = IconSource.Nodes.Chest + + override def rotatable: Boolean = false override def minimumSize: Size2D = Size2D(28, 30) * 2 + 4 diff --git a/src/main/scala/ocelot/desktop/node/nodes/ColorfulLampNode.scala b/src/main/scala/ocelot/desktop/node/nodes/ColorfulLampNode.scala index 272dfd3..a4182bb 100644 --- a/src/main/scala/ocelot/desktop/node/nodes/ColorfulLampNode.scala +++ b/src/main/scala/ocelot/desktop/node/nodes/ColorfulLampNode.scala @@ -10,6 +10,8 @@ class ColorfulLampNode(val lamp: ColorfulLamp) extends EntityNode(lamp) with Lab private var lastColor: RGBAColor = RGBAColor(0, 0, 0) private var mouseHover: Boolean = false + override def rotatable: Boolean = false + override def label: Option[String] = super.label.filter(_ => mouseHover) override def draw(g: Graphics): Unit = { @@ -22,12 +24,12 @@ class ColorfulLampNode(val lamp: ColorfulLamp) extends EntityNode(lamp) with Lab ) g.rect(position.x + 2, position.y + 2, size.width - 4, size.height - 4, lastColor) - g.sprite(IconSource.Nodes.Lamp.Frame, position.x + 2, position.y + 2, size.width - 4, size.height - 4) + g.sprite(IconSource.Nodes.LampFrame, position.x + 2, position.y + 2, size.width - 4, size.height - 4) } override def drawLight(g: Graphics): Unit = { super.drawLight(g) - g.sprite(IconSource.Nodes.Lamp.Glow, position - size / 2, size * 2, lastColor) + g.sprite(IconSource.Nodes.LampGlow, position - size / 2, size * 2, lastColor) } eventHandlers += { diff --git a/src/main/scala/ocelot/desktop/node/nodes/ComputerNode.scala b/src/main/scala/ocelot/desktop/node/nodes/ComputerNode.scala index a536982..b6b64ed 100644 --- a/src/main/scala/ocelot/desktop/node/nodes/ComputerNode.scala +++ b/src/main/scala/ocelot/desktop/node/nodes/ComputerNode.scala @@ -2,6 +2,7 @@ package ocelot.desktop.node.nodes import ocelot.desktop.color.Color import ocelot.desktop.graphics.{Graphics, IconSource} +import ocelot.desktop.inventory.item.SelfDestructingCardItem import ocelot.desktop.node.Node.HighlightThickness import ocelot.desktop.node.{ComputerAwareNode, WindowedNode} import ocelot.desktop.ui.event.ClickEvent @@ -15,10 +16,15 @@ import totoro.ocelot.brain.entity.traits.Inventory import totoro.ocelot.brain.util.Tier class ComputerNode(val computerCase: Case) - extends ComputerAwareNode(computerCase) with AudibleComputerAware with WindowedNode[ComputerWindow] { - override val iconSource: IconSource = IconSource.Nodes.Computer.Default + extends ComputerAwareNode(computerCase) + with AudibleComputerAware + with WindowedNode[ComputerWindow] { + + override val icon: IconSource = IconSource.Nodes.Computer.Default override def iconColor: Color = TierColor.get(computerCase.tier) + override def rotatable: Boolean = true + override def setupContextMenu(menu: ContextMenu, event: ClickEvent): Unit = { addPowerContextMenuEntries(menu) addTierContextMenuEntries(menu) @@ -28,6 +34,12 @@ class ComputerNode(val computerCase: Case) super.setupContextMenu(menu, event) } + override protected def selfDestructingCards: IterableOnce[SelfDestructingCardItem] = { + inventoryIterator.flatMap(_.get).collect { + case item: SelfDestructingCardItem => item + } + } + override def update(): Unit = { super.update() diff --git a/src/main/scala/ocelot/desktop/node/nodes/DiskDriveNode.scala b/src/main/scala/ocelot/desktop/node/nodes/DiskDriveNode.scala index e848510..2f629cf 100644 --- a/src/main/scala/ocelot/desktop/node/nodes/DiskDriveNode.scala +++ b/src/main/scala/ocelot/desktop/node/nodes/DiskDriveNode.scala @@ -21,7 +21,9 @@ class DiskDriveNode(entity: FloppyDiskDrive) with ShiftClickNode with WindowedNode[DiskDriveWindow] { - override def iconSource: IconSource = IconSource.Nodes.DiskDrive.Default + override def icon: IconSource = IconSource.Nodes.DiskDrive.Default + + override def rotatable: Boolean = true override def setupContextMenu(menu: ContextMenu, event: ClickEvent): Unit = { if (isFloppyItemPresent) { diff --git a/src/main/scala/ocelot/desktop/node/nodes/HologramProjectorNode.scala b/src/main/scala/ocelot/desktop/node/nodes/HologramProjectorNode.scala index 6b78e1d..9a61ff7 100644 --- a/src/main/scala/ocelot/desktop/node/nodes/HologramProjectorNode.scala +++ b/src/main/scala/ocelot/desktop/node/nodes/HologramProjectorNode.scala @@ -8,7 +8,9 @@ import totoro.ocelot.brain.entity.HologramProjector class HologramProjectorNode(val hologramProjector: HologramProjector) extends EntityNode(hologramProjector) with LabeledEntityNode with WindowedNode[HologramProjectorWindow] { - override def iconSource: IconSource = IconSource.Nodes.HologramProjector(hologramProjector.tier) + override def icon: IconSource = IconSource.Nodes.HologramProjector(hologramProjector.tier) + + override def rotatable: Boolean = false override def createWindow(): HologramProjectorWindow = new HologramProjectorWindow(this) } diff --git a/src/main/scala/ocelot/desktop/node/nodes/IronNoteBlockNode.scala b/src/main/scala/ocelot/desktop/node/nodes/IronNoteBlockNode.scala index f1d6a43..2d4999e 100644 --- a/src/main/scala/ocelot/desktop/node/nodes/IronNoteBlockNode.scala +++ b/src/main/scala/ocelot/desktop/node/nodes/IronNoteBlockNode.scala @@ -9,7 +9,9 @@ import totoro.ocelot.brain.event.{EventBus, NoteBlockTriggerEvent} import scala.util.Random class IronNoteBlockNode(val ironNoteBlock: IronNoteBlock) extends NoteBlockNodeBase(ironNoteBlock) { - override def iconSource: IconSource = IconSource.Nodes.IronNoteBlock + override def icon: IconSource = IconSource.Nodes.IronNoteBlock + + override def rotatable: Boolean = false override def onClick(event: ClickEvent): Unit = { event match { diff --git a/src/main/scala/ocelot/desktop/node/nodes/MicrocontrollerNode.scala b/src/main/scala/ocelot/desktop/node/nodes/MicrocontrollerNode.scala index ff80185..b12368a 100644 --- a/src/main/scala/ocelot/desktop/node/nodes/MicrocontrollerNode.scala +++ b/src/main/scala/ocelot/desktop/node/nodes/MicrocontrollerNode.scala @@ -20,7 +20,8 @@ class MicrocontrollerNode(val microcontroller: Microcontroller) with ComputerAware with DefaultSlotItemsFillable with WindowedNode[ComputerWindow] { - override val iconSource: IconSource = IconSource.Nodes.Microcontroller.Default + + override val icon: IconSource = IconSource.Nodes.Microcontroller.Default override def setupContextMenu(menu: ContextMenu, event: ClickEvent): Unit = { addPowerContextMenuEntries(menu) @@ -54,6 +55,8 @@ class MicrocontrollerNode(val microcontroller: Microcontroller) override def getNodeByPort(port: NodePort): network.Node = microcontroller.sidedNode(port.direction.get) + override def rotatable: Boolean = true + override def shouldReceiveEventsFor(address: String): Boolean = { address == microcontroller.machine.node.address || super.shouldReceiveEventsFor(address) @@ -65,6 +68,12 @@ class MicrocontrollerNode(val microcontroller: Microcontroller) override def computerType: ComputerType = ComputerType.Microcontroller override def brainInventory: Inventory = microcontroller.inventory.owner + override protected def selfDestructingCards: IterableOnce[SelfDestructingCardItem] = { + inventoryIterator.flatMap(_.get).collect { + case item: SelfDestructingCardItem => item + } + } + override def addSlotsBasedOnTier(): Unit = { microcontroller.tier match { case Tier.One => @@ -120,23 +129,26 @@ class MicrocontrollerNode(val microcontroller: Microcontroller) override def fillSlotsWithDefaultItems(): Unit = { cardSlots(0).item = { - if (microcontroller.tier == Tier.Two) + if (microcontroller.tier >= Tier.Two) { WirelessNetworkCardItem.Tier2.Factory.build() - else + } else { WirelessNetworkCardItem.Tier1.Factory.build() + } } cardSlots(1).item = { - if (microcontroller.tier == Tier.Two) + if (microcontroller.tier == Tier.Creative) { RedstoneCardItem.Tier2.Factory.build() - else + } else { RedstoneCardItem.Tier1.Factory.build() + } } cpuSlot.item = new CpuItem.Factory(Tier.One).build() - for (memorySlot <- memorySlots) + for (memorySlot <- memorySlots) { memorySlot.item = new MemoryItem.Factory(Tier.One.toExtended(false)).build() + } eepromSlot.item = EepromItem.Factory.Empty.build() } diff --git a/src/main/scala/ocelot/desktop/node/nodes/NoteBlockNode.scala b/src/main/scala/ocelot/desktop/node/nodes/NoteBlockNode.scala index 62291fd..aef4aa6 100644 --- a/src/main/scala/ocelot/desktop/node/nodes/NoteBlockNode.scala +++ b/src/main/scala/ocelot/desktop/node/nodes/NoteBlockNode.scala @@ -7,10 +7,12 @@ import totoro.ocelot.brain.entity.NoteBlock import totoro.ocelot.brain.event.{EventBus, NoteBlockTriggerEvent} class NoteBlockNode(val noteBlock: NoteBlock) extends NoteBlockNodeBase(noteBlock) { - override def iconSource: IconSource = IconSource.Nodes.NoteBlock + override def icon: IconSource = IconSource.Nodes.NoteBlock + + override def rotatable: Boolean = false override def setupContextMenu(menu: ContextMenu, event: ClickEvent): Unit = { - menu.addEntry(new ContextMenuSubmenu("Instrument", Some(ContextMenuIcon(IconSource.Guitar))) { + menu.addEntry(new ContextMenuSubmenu("Instrument", Some(ContextMenuIcon(IconSource.Icons.Guitar))) { { val maxLen = NoteBlockNode.Instruments.map(_._2.length).max diff --git a/src/main/scala/ocelot/desktop/node/nodes/NoteBlockNodeBase.scala b/src/main/scala/ocelot/desktop/node/nodes/NoteBlockNodeBase.scala index ea4f2ff..e59c748 100644 --- a/src/main/scala/ocelot/desktop/node/nodes/NoteBlockNodeBase.scala +++ b/src/main/scala/ocelot/desktop/node/nodes/NoteBlockNodeBase.scala @@ -3,15 +3,14 @@ package ocelot.desktop.node.nodes import ocelot.desktop.ColorScheme import ocelot.desktop.audio.{SoundBuffers, SoundCategory, SoundSource} import ocelot.desktop.geometry.{Size2D, Vector2D} -import ocelot.desktop.graphics.Graphics +import ocelot.desktop.graphics.{Graphics, IconSource} import ocelot.desktop.node.{EntityNode, LabeledEntityNode} import ocelot.desktop.ui.UiHandler import ocelot.desktop.ui.event.BrainEvent +import ocelot.desktop.ui.particle.Particle import totoro.ocelot.brain.entity.traits.{Entity, Environment} import totoro.ocelot.brain.event.NoteBlockTriggerEvent -import scala.collection.mutable - abstract class NoteBlockNodeBase(entity: Entity with Environment) extends EntityNode(entity) with LabeledEntityNode { eventHandlers += { case BrainEvent(event: NoteBlockTriggerEvent) => @@ -22,31 +21,25 @@ abstract class NoteBlockNodeBase(entity: Entity with Environment) extends Entity volume = event.volume.toFloat.min(1f).max(0f), ).play() - addParticle(event.pitch) - } - - private val particles = mutable.ArrayBuffer[(Float, Int)]() - - private def addParticle(pitch: Int): Unit = { - synchronized { - particles += ((0f, pitch)) - } - } - - override def drawParticles(g: Graphics): Unit = synchronized { - for ((time, pitch) <- particles.reverseIterator) { - val col = ColorScheme("Note" + pitch.min(24).max(0)).withAlpha(1 - (2 * time - 1).min(1).max(0)) - g.sprite("particles/Note", position + Vector2D(pitch / 24f * 40f + 5, height / 2 - 10 - 100 * time), - Size2D(14, 20), col) - } - particles.mapInPlace { case (t, p) => (t + 1.2f * UiHandler.dt, p) } - particles.filterInPlace(_._1 <= 1f) + UiHandler.root.workspaceView.particleSystem.add(new NoteParticle(event.pitch)) } override def update(): Unit = { super.update() + if (isHovered || isMoving) { + root.get.statusBar.addMouseEntry(IconSource.Icons.LMB, "Play sample") + } + } - if (isHovered || isMoving) - root.get.statusBar.addMouseEntry("icons/LMB", "Play sample") + private class NoteParticle(pitch: Int) extends Particle(speed = 1.2f) { + override def draw(g: Graphics): Unit = { + val col = ColorScheme("Note" + pitch.min(24).max(0)).withAlpha(1 - (2 * time - 1).min(1).max(0)) + g.sprite( + IconSource.Particles.Note, + position + Vector2D(pitch / 24f * 40f + 5, height / 2 - 10 - 100 * time), + Size2D(14, 20), + col, + ) + } } } diff --git a/src/main/scala/ocelot/desktop/node/nodes/OcelotBlockNode.scala b/src/main/scala/ocelot/desktop/node/nodes/OcelotBlockNode.scala index d0a2c5a..3d07fb0 100644 --- a/src/main/scala/ocelot/desktop/node/nodes/OcelotBlockNode.scala +++ b/src/main/scala/ocelot/desktop/node/nodes/OcelotBlockNode.scala @@ -22,7 +22,9 @@ class OcelotBlockNode(val ocelot: OcelotBlock) override def name: String = "Ocelot Block" - override def iconSource: IconSource = IconSource.Nodes.OcelotBlock.Default + override def icon: IconSource = IconSource.Nodes.OcelotBlock.Default + + override def rotatable: Boolean = false override def ocelotInterface: OcelotInterface = ocelot @@ -62,17 +64,16 @@ class OcelotBlockNode(val ocelot: OcelotBlock) } private def drawActivity(g: Graphics, icon: IconSource, lastActivity: Long, currentTime: Long): Unit = { - val alpha = (1 - (currentTime - lastActivity) / ActivityFadeOutMs).clamp() + val alpha = (1 - (currentTime - lastActivity) / ActivityFadeOutMs).clamped() if (alpha > 0) { g.sprite( - icon.path, + icon, position.x + HighlightThickness, position.y + HighlightThickness, size.width - HighlightThickness * 2, size.height - HighlightThickness * 2, RGBAColorNorm(1f, 1f, 1f, alpha), - icon.animation, ) } } diff --git a/src/main/scala/ocelot/desktop/node/nodes/OpenFMRadioNode.scala b/src/main/scala/ocelot/desktop/node/nodes/OpenFMRadioNode.scala index 7cacfcc..53b031b 100644 --- a/src/main/scala/ocelot/desktop/node/nodes/OpenFMRadioNode.scala +++ b/src/main/scala/ocelot/desktop/node/nodes/OpenFMRadioNode.scala @@ -9,7 +9,9 @@ import ocelot.desktop.windows.OpenFMRadioWindow class OpenFMRadioNode(val openFMRadio: OpenFMRadio) extends EntityNode(openFMRadio) with LabeledEntityNode with WindowedNode[OpenFMRadioWindow] { - override def iconSource: IconSource = IconSource.Nodes.OpenFMRadio + override def icon: IconSource = IconSource.Nodes.OpenFMRadio + + override def rotatable: Boolean = true private var lastTick = OcelotDesktop.ticker.tick private var animationIndex = 0 diff --git a/src/main/scala/ocelot/desktop/node/nodes/RackNode.scala b/src/main/scala/ocelot/desktop/node/nodes/RackNode.scala index 2081665..b332f3e 100644 --- a/src/main/scala/ocelot/desktop/node/nodes/RackNode.scala +++ b/src/main/scala/ocelot/desktop/node/nodes/RackNode.scala @@ -3,7 +3,7 @@ package ocelot.desktop.node.nodes import ocelot.desktop.geometry.{Rect2D, Size2D, Vector2D} import ocelot.desktop.graphics.{Graphics, IconSource} import ocelot.desktop.inventory.Item -import ocelot.desktop.inventory.item.{DiskDriveMountableItem, ServerItem} +import ocelot.desktop.inventory.item.{DiskDriveMountableItem, SelfDestructingCardItem, ServerItem} import ocelot.desktop.inventory.traits.RackMountableItem import ocelot.desktop.node.Node.{HighlightThickness, NoHighlightSize, Size, TexelCount} import ocelot.desktop.node.{ComputerAwareNode, NodePort, WindowedNode} @@ -18,7 +18,7 @@ import totoro.ocelot.brain.network import totoro.ocelot.brain.util.Direction class RackNode(val rack: Rack) extends ComputerAwareNode(rack) with WindowedNode[RackWindow] { - override val iconSource: IconSource = IconSource.Nodes.Rack.Empty + override val icon: IconSource = IconSource.Nodes.Rack.Empty override def exposeAddress = false override def ports: Array[NodePort] = Array( @@ -31,6 +31,8 @@ class RackNode(val rack: Rack) extends ComputerAwareNode(rack) with WindowedNode override def getNodeByPort(port: NodePort): network.Node = rack.sidedNode(port.direction.get) + override def rotatable: Boolean = true + override def shouldReceiveEventsFor(address: String): Boolean = { super.shouldReceiveEventsFor(address) || rack.inventory.entities.exists { @@ -43,6 +45,15 @@ class RackNode(val rack: Rack) extends ComputerAwareNode(rack) with WindowedNode } } + override protected def selfDestructingCards: IterableOnce[SelfDestructingCardItem] = { + inventoryIterator.flatMap(_.get).collect({ + case item: ServerItem => + item.inventoryIterator.flatMap(_.get).collect { + case card: SelfDestructingCardItem => card + } + }).flatten + } + override def setupContextMenu(menu: ContextMenu, event: ClickEvent): Unit = { RackNode.addContextMenuEntriesOfMountable(menu, getMountableByClick(event)) @@ -183,7 +194,7 @@ object RackNode { def addContextMenuEntriesOfMountable(menu: ContextMenu, item: Option[RackMountableItem]): Unit = { item match { case Some(serverItem: ServerItem) => - menu.addEntry(ContextMenuEntry("Set up", IconSource.Window) { + menu.addEntry(ContextMenuEntry("Set up", IconSource.Icons.Window) { serverItem.window.open() }) @@ -206,7 +217,7 @@ object RackNode { "Change floppy" else "Set floppy", - IconSource.Save, + IconSource.Icons.Save, ) { diskDriveMountableItem.window.open() }) diff --git a/src/main/scala/ocelot/desktop/node/nodes/RaidNode.scala b/src/main/scala/ocelot/desktop/node/nodes/RaidNode.scala index 7261f81..f440955 100644 --- a/src/main/scala/ocelot/desktop/node/nodes/RaidNode.scala +++ b/src/main/scala/ocelot/desktop/node/nodes/RaidNode.scala @@ -8,26 +8,34 @@ import ocelot.desktop.node.Node.{HighlightThickness, NoHighlightSize} import ocelot.desktop.node.{EntityNode, LabeledEntityNode, WindowedNode} import ocelot.desktop.ui.event.ClickEvent import ocelot.desktop.ui.event.handlers.DiskActivityHandler -import ocelot.desktop.ui.widget.contextmenu.ContextMenu +import ocelot.desktop.ui.widget.DiskEditWindow +import ocelot.desktop.ui.widget.DiskEditWindow.EditableDisk +import ocelot.desktop.ui.widget.contextmenu.{ContextMenu, ContextMenuEntry} import ocelot.desktop.ui.widget.slot.HddSlotWidget +import ocelot.desktop.ui.widget.window.Windowed import ocelot.desktop.util.{DefaultSlotItemsFillable, DrawUtils} import ocelot.desktop.windows.RaidWindow import totoro.ocelot.brain.entity.Raid -import totoro.ocelot.brain.entity.traits.Inventory +import totoro.ocelot.brain.entity.traits.{Environment, Inventory} +import totoro.ocelot.brain.nbt.NBTTagCompound import totoro.ocelot.brain.util.Tier import scala.util.Random class RaidNode(val raid: Raid) - extends EntityNode(raid) + extends EntityNode(raid) with SyncedInventory with LabeledEntityNode with DiskActivityHandler with DefaultSlotItemsFillable - with WindowedNode[RaidWindow] { + with WindowedNode[RaidWindow] + with EditableDisk { + var diskSlots: Array[HddSlotWidget] = Array.tabulate(3)(index => new HddSlotWidget(Slot(index), Tier.Three)) - override val iconSource: IconSource = IconSource.Nodes.Raid.Default + override val icon: IconSource = IconSource.Nodes.Raid.Default + + override def rotatable: Boolean = true override def draw(g: Graphics): Unit = { super.draw(g) @@ -55,31 +63,52 @@ class RaidNode(val raid: Raid) // Required for disk activity events processing override def shouldReceiveEventsFor(address: String): Boolean = super.shouldReceiveEventsFor(address) || raid.filesystem.exists(_.node.address == address) + + private val diskEditWindow = new Windowed[DiskEditWindow] { + override protected def createWindow(): DiskEditWindow = new DiskEditWindow(RaidNode.this) + + override protected def windowNBTKey: String = "diskEditWindow" + } override def setupContextMenu(menu: ContextMenu, event: ClickEvent): Unit = { - DiskItem.addRealPathContextMenuEntries(menu, raid, + DiskItem.addRealPathContextMenuEntries( + menu, + raid, realPathSetter => { realPathSetter() - }) + }, + ) - // TODO: Implement DiskDriveWindow later, because at this moment every - // TODO: instance of 'Windowed' trait can have only 1 persistable window - - // TODO: Perhaps we should rework this system with List[Window] and - // TODO: registerWindow(...) or something similar - -// menu.addEntry(ContextMenuEntry("Edit disk", IconSource.Edit) { -// window.open() -// }) + menu.addEntry(ContextMenuEntry("Edit disk", IconSource.Icons.Edit) { + diskEditWindow.window.open() + }) menu.addSeparator() super.setupContextMenu(menu, event) } + override def load(nbt: NBTTagCompound): Unit = { + super.load(nbt) + + diskEditWindow.load(nbt) + } + + override def save(nbt: NBTTagCompound): Unit = { + super.save(nbt) + + diskEditWindow.save(nbt) + } + + override def dispose(): Unit = { + diskEditWindow.closeAndDisposeWindow() + + super.dispose() + } + // -------------------------------- LabeledEntityNode -------------------------------- - override def fallbackLabelAddress: Option[String] = raid.filesystem.map(_.node.address) + override def fallbackLabelAddress: Option[String] = diskAddress // -------------------------------- Inventory -------------------------------- @@ -97,4 +126,22 @@ class RaidNode(val raid: Raid) // -------------------------------- Window -------------------------------- override def createWindow(): RaidWindow = new RaidWindow(this) + + // -------------------------------- EditableDisk -------------------------------- + + override def disk: Option[Environment] = raid.filesystem + + override def diskKind: String = "RAID" + + override def diskAddress: Option[String] = raid.filesystem.map(_.node.address) + + override def capacity: Long = raid.filesystem.fold(0L)(_.fileSystem.spaceTotal) + + override def diskLabel: Option[String] = Option(raid.label.getLabel) + + override def diskLabel_=(label: Option[String]): Unit = raid.label.setLabel(label.orNull) + + override def isLabelWriteable: Boolean = true + + override def editingAllowed: Boolean = raid.filesystem.isDefined } diff --git a/src/main/scala/ocelot/desktop/node/nodes/RelayNode.scala b/src/main/scala/ocelot/desktop/node/nodes/RelayNode.scala index 12685db..36ecf84 100644 --- a/src/main/scala/ocelot/desktop/node/nodes/RelayNode.scala +++ b/src/main/scala/ocelot/desktop/node/nodes/RelayNode.scala @@ -11,7 +11,7 @@ import totoro.ocelot.brain.util.Direction class RelayNode(val relay: Relay) extends EntityNode(relay) with SyncedInventory with LabeledNode with WindowedNode[RelayWindow] { - override val iconSource: IconSource = IconSource.Nodes.Relay + override val icon: IconSource = IconSource.Nodes.Relay override def ports: Array[NodePort] = Array( NodePort(Some(Direction.North)), @@ -24,6 +24,8 @@ class RelayNode(val relay: Relay) override def getNodeByPort(port: NodePort): network.Node = relay.sidedNode(port.direction.get) + override def rotatable: Boolean = false + override def exposeAddress = false override def shouldReceiveEventsFor(address: String): Boolean = { diff --git a/src/main/scala/ocelot/desktop/node/nodes/ScreenNode.scala b/src/main/scala/ocelot/desktop/node/nodes/ScreenNode.scala index 5214337..9bfb6c1 100644 --- a/src/main/scala/ocelot/desktop/node/nodes/ScreenNode.scala +++ b/src/main/scala/ocelot/desktop/node/nodes/ScreenNode.scala @@ -91,32 +91,34 @@ class ScreenNode(val screen: Screen) } } - override def iconSource: IconSource = IconSource.Nodes.Screen.Standalone + override def icon: IconSource = IconSource.Nodes.Screen.Standalone override def iconColor: Color = TierColor.get(screen.tier) + override def rotatable: Boolean = true + override def setupContextMenu(menu: ContextMenu, event: ClickEvent): Unit = { // no synchronization here: the methods to turn the screen on/off are indirect. if (screen.getPowerState) { - menu.addEntry(ContextMenuEntry("Turn off", IconSource.Power) { + menu.addEntry(ContextMenuEntry("Turn off", IconSource.Icons.Power) { screen.setPowerState(false) }) } else { - menu.addEntry(ContextMenuEntry("Turn on", IconSource.Power) { + menu.addEntry(ContextMenuEntry("Turn on", IconSource.Icons.Power) { screen.setPowerState(true) }) } - menu.addEntry(ContextMenuEntry("Set aspect ratio", IconSource.AspectRatio) { + menu.addEntry(ContextMenuEntry("Set aspect ratio", IconSource.Icons.AspectRatio) { new ScreenAspectRatioDialog(this).show() }) if (keyboard.isDefined) { - menu.addEntry(ContextMenuEntry("Remove keyboard", IconSource.KeyboardOff) { + menu.addEntry(ContextMenuEntry("Remove keyboard", IconSource.Icons.KeyboardOff) { detachKeyboard() }) } else { - menu.addEntry(ContextMenuEntry("Add keyboard", IconSource.Keyboard) { + menu.addEntry(ContextMenuEntry("Add keyboard", IconSource.Icons.Keyboard) { attachKeyboard() }) } diff --git a/src/main/scala/ocelot/desktop/node/nodes/TapeDriveNode.scala b/src/main/scala/ocelot/desktop/node/nodes/TapeDriveNode.scala index 114abe8..3cdce38 100644 --- a/src/main/scala/ocelot/desktop/node/nodes/TapeDriveNode.scala +++ b/src/main/scala/ocelot/desktop/node/nodes/TapeDriveNode.scala @@ -18,7 +18,9 @@ class TapeDriveNode(val tapeDrive: TapeDrive) with PositionalSoundSourcesNode with WindowedNode[TapeDriveWindow] { - override def iconSource: IconSource = IconSource.Nodes.TapeDrive + override def icon: IconSource = IconSource.Nodes.TapeDrive + + override def rotatable: Boolean = true private lazy val soundTapeRewind: SoundSource = SoundSource.fromBuffer( SoundBuffers.MachineTapeRewind, @@ -55,9 +57,9 @@ class TapeDriveNode(val tapeDrive: TapeDrive) val isRewinding = tapeDrive.state.state == TapeDriveState.State.Rewinding || tapeDrive.state.state == TapeDriveState.State.Forwarding - if (!isRewinding && soundTapeRewind.isPlaying) { + if (!isRewinding && soundTapeRewind.playing) { soundTapeRewind.stop() - } else if (isRewinding && !soundTapeRewind.isPlaying && !Audio.isDisabled) { + } else if (isRewinding && !soundTapeRewind.playing && !Audio.isDisabled) { soundTapeRewind.play() } } @@ -70,7 +72,7 @@ class TapeDriveNode(val tapeDrive: TapeDrive) // -------------------------------- PositionalSoundSourcesNode -------------------------------- - override def soundSources: Seq[SoundSource] = Seq(source) + override def soundSources: Seq[SoundSource] = super.soundSources ++ Seq(source) // -------------------------------- Inventory -------------------------------- diff --git a/src/main/scala/ocelot/desktop/ui/UiHandler.scala b/src/main/scala/ocelot/desktop/ui/UiHandler.scala index fea5aed..8f2ef55 100644 --- a/src/main/scala/ocelot/desktop/ui/UiHandler.scala +++ b/src/main/scala/ocelot/desktop/ui/UiHandler.scala @@ -4,16 +4,15 @@ import buildinfo.BuildInfo import ocelot.desktop.audio.{Audio, SoundBuffers, SoundSource} import ocelot.desktop.geometry.{Rect2D, Size2D, Vector2D} import ocelot.desktop.graphics.Graphics -import ocelot.desktop.ui.event.{Event, MouseEvent} import ocelot.desktop.ui.event.handlers.HoverHandler -import ocelot.desktop.ui.event.sources.{KeyEvents, MouseEvents, ScrollEvents} -import ocelot.desktop.ui.widget.window.Window +import ocelot.desktop.ui.event.sources.{BrainEvents, KeyEvents, MouseEvents, ScrollEvents} +import ocelot.desktop.ui.event.{Capturing, CapturingEvent, Dispatchable, MouseEvent} import ocelot.desktop.ui.widget.{RootWidget, Widget} import ocelot.desktop.util._ import ocelot.desktop.{OcelotDesktop, Settings} import org.apache.commons.lang3.SystemUtils import org.lwjgl.BufferUtils -import org.lwjgl.input.{Keyboard, Mouse} +import org.lwjgl.input.Mouse import org.lwjgl.opengl._ import java.awt.Toolkit @@ -24,11 +23,12 @@ import java.nio.channels.Channels import java.nio.file.Paths import javax.imageio.ImageIO import javax.swing.JFileChooser -import scala.annotation.tailrec import scala.collection.mutable +import scala.collection.mutable.ArrayBuffer import scala.concurrent.duration.DurationInt import scala.util.Try + object UiHandler extends Logging { var root: RootWidget = _ var graphics: Graphics = _ @@ -210,8 +210,15 @@ object UiHandler extends Logging { ContextAttribs.CONTEXT_CORE_PROFILE_BIT_ARB, ContextAttribs.CONTEXT_FORWARD_COMPATIBLE_BIT_ARB, ) + + val pf = new PixelFormat() + .withDepthBits(24) + .withStencilBits(8) + .withAlphaBits(8) + .withSRGB(true) + logger.info(s"Creating an OpenGL context: $attrs") - Display.create((new PixelFormat).withSRGB(true), attrs) + Display.create(pf, attrs) if (Settings.get.windowValidatePosition) fixInsaneInitialWindowGeometry() @@ -245,6 +252,7 @@ object UiHandler extends Logging { val arch = System.getProperty("os.arch") val is64bit = arch.startsWith("amd64") + val isArm64 = arch.startsWith("aarch64") || arch.startsWith("arm64") val libs = { if (SystemUtils.IS_OS_WINDOWS) { @@ -252,9 +260,12 @@ object UiHandler extends Logging { Array("jinput-dx8_64.dll", "jinput-raw_64.dll", "jinput-wintab.dll", "lwjgl64.dll", "OpenAL64.dll") else Array("jinput-dx8.dll", "jinput-raw.dll", "jinput-wintab.dll", "lwjgl.dll", "OpenAL32.dll") - } else if (SystemUtils.IS_OS_MAC_OSX) - Array("liblwjgl.dylib") - else if (SystemUtils.IS_OS_LINUX) { + } else if (SystemUtils.IS_OS_MAC_OSX) { + if (isArm64) + Array("liblwjgl-arm64.dylib") + else + Array("liblwjgl.dylib") + } else if (SystemUtils.IS_OS_LINUX) { if (is64bit) Array("libjinput-linux64.so", "liblwjgl64.so", "libopenal64.so") else @@ -266,7 +277,8 @@ object UiHandler extends Logging { logger.debug(s"Unpacking native libraries to: $librariesPath") for (lib <- libs) { - val dest = new File(Paths.get(librariesPath, lib).toString) + val destinationFilename = if (SystemUtils.IS_OS_MAC_OSX) "liblwjgl.dylib" else lib + val dest = new File(Paths.get(librariesPath, destinationFilename).toString) if (!dest.exists()) { val source = getClass.getResourceAsStream("/" + lib) @@ -374,7 +386,7 @@ object UiHandler extends Logging { // we are processing screenshots here to make sure that // the current frame was fully drawn on the screen, // but the update for the next frame yet not happened (if the window, for example, is to be resized) - if (KeyEvents.isReleased(Keyboard.KEY_F12)) { + if (KeyEvents.isReleased(Settings.get.keymap(Keybind.Screenshot))) { SoundSource.InterfaceShutter.play() val image = graphics.screenshot() root.flash.bang() @@ -403,29 +415,60 @@ object UiHandler extends Logging { _terminating = true root.workspaceView.dispose() + BrainEvents.destroy() KeyEvents.destroy() MouseEvents.destroy() graphics.freeResource() + Audio.removeAllSources() SoundBuffers.freeResource() Display.destroy() Audio.destroy() } - private def dispatchEvent(iter: => IterableOnce[Widget] = hierarchy.reverseIterator)(event: Event): Unit = { - for (widget <- iter) { - if (event.consumed) { - return - } - + private def dispatchEvent(iter: => IterableOnce[Widget] = hierarchy.reverseIterator)(event: Dispatchable): Unit = { + for (widget <- iter if !event.consumed) { widget.handleEvent(event) } } + private def dispatchCapturing(target: Widget)(event: CapturingEvent): Unit = { + val ancestors = target.ancestors.toSeq + dispatchEvent(ancestors.reverseIterator ++ Some(target))(Capturing(event)) + dispatchEvent(Some(target))(event) + } + + private def dispatchBrainEvents(): Unit = { + import totoro.ocelot.brain + + val cutoff = BrainEvents.events.peekLast() + + if (cutoff == null) { + return + } + + // we're taking a snapshot of all events we have received at the point of the call. + // new events may still be added to the queue, but we'll defer everything after the cutoff until the next update. + var cutoffReached = false + val events = ArrayBuffer.empty[brain.event.Event] + + while (!cutoffReached) { + val event = BrainEvents.events.poll() + cutoffReached = event eq cutoff + events += event + } + + for (event <- events) { + root.workspaceView.dispatchBrainEvent(event) + } + } + private def update(): Unit = { if (shouldUpdateHierarchy) { _updateHierarchy() } + dispatchBrainEvents() + val mousePos = mousePosition if (mousePos.x < 0 || mousePos.y < 0 || mousePos.x > root.width || mousePos.y > root.height) { KeyEvents.releaseKeys() @@ -433,7 +476,10 @@ object UiHandler extends Logging { } // TODO: dispatch to the focused widget instead of broadcasting to the entire hierarchy. - KeyEvents.events.foreach(dispatchEvent()) + for (event <- KeyEvents.events) { + dispatchEvent(hierarchy)(Capturing(event)) + dispatchEvent()(event) + } MouseEvents.events .foreach(dispatchEvent(hierarchy.reverseIterator.filter(w => w.enabled && w.receiveAllMouseEvents))) @@ -444,17 +490,17 @@ object UiHandler extends Logging { val mouseTarget = hierarchy.reverseIterator .find(w => w.enabled && w.receiveMouseEvents && w.clippedBounds.contains(mousePos)) - ScrollEvents.events.foreach(dispatchEvent(scrollTarget)) + for (scrollTarget <- scrollTarget) { + ScrollEvents.events.foreach(dispatchCapturing(scrollTarget)) + } for (event <- MouseEvents.events) { - if (event.state == MouseEvent.State.Press) { - dispatchEvent(mouseTarget)(event) - - // TODO: this should be done in the event capturing phase in [[Window]] itself. - for (mouseTarget <- mouseTarget if !mouseTarget.isInstanceOf[Window]) { - focusParentWindow(mouseTarget.parent) + if (event.state == MouseEvent.State.Pressed) { + for (mouseTarget <- mouseTarget) { + dispatchCapturing(mouseTarget)(event) } } else { + dispatchEvent(hierarchy)(Capturing(event)) dispatchEvent(hierarchy.reverseIterator)(event) } } @@ -472,15 +518,6 @@ object UiHandler extends Logging { root.update() } - @tailrec - private def focusParentWindow(parent: Option[Widget]): Unit = { - parent match { - case Some(window: Window) => window.focus() - case Some(widget) => focusParentWindow(widget.parent) - case None => - } - } - private def updateWindowSizeAndPosition(): Unit = { val width = Display.getWidth val height = Display.getHeight diff --git a/src/main/scala/ocelot/desktop/ui/event/CapturingEvent.scala b/src/main/scala/ocelot/desktop/ui/event/CapturingEvent.scala new file mode 100644 index 0000000..448d6bd --- /dev/null +++ b/src/main/scala/ocelot/desktop/ui/event/CapturingEvent.scala @@ -0,0 +1,3 @@ +package ocelot.desktop.ui.event + +trait CapturingEvent extends Event diff --git a/src/main/scala/ocelot/desktop/ui/event/Dispatchable.scala b/src/main/scala/ocelot/desktop/ui/event/Dispatchable.scala new file mode 100644 index 0000000..397083c --- /dev/null +++ b/src/main/scala/ocelot/desktop/ui/event/Dispatchable.scala @@ -0,0 +1,28 @@ +package ocelot.desktop.ui.event + +sealed trait Dispatchable { + def consume(): Unit + + def consumed: Boolean +} + +trait Event extends Dispatchable { + private var _consumed: Boolean = false + + def consume(): Unit = { + _consumed = true + } + + def consumed: Boolean = _consumed +} + +/** + * A wrapper for an event that is dispatched during the capture phase. + */ +case class Capturing[A <: CapturingEvent](event: A) extends Dispatchable { + override def consume(): Unit = { + event.consume() + } + + override def consumed: Boolean = event.consumed +} diff --git a/src/main/scala/ocelot/desktop/ui/event/Event.scala b/src/main/scala/ocelot/desktop/ui/event/Event.scala deleted file mode 100644 index a547eec..0000000 --- a/src/main/scala/ocelot/desktop/ui/event/Event.scala +++ /dev/null @@ -1,11 +0,0 @@ -package ocelot.desktop.ui.event - -trait Event extends AnyRef { - private var _consumed: Boolean = false - - def consume(): Unit = { - _consumed = true - } - - def consumed: Boolean = _consumed -} diff --git a/src/main/scala/ocelot/desktop/ui/event/EventAware.scala b/src/main/scala/ocelot/desktop/ui/event/EventAware.scala index 3459153..97f86e3 100644 --- a/src/main/scala/ocelot/desktop/ui/event/EventAware.scala +++ b/src/main/scala/ocelot/desktop/ui/event/EventAware.scala @@ -7,7 +7,7 @@ trait EventAware { def shouldReceiveEventsFor(address: String): Boolean = false - def handleEvent(event: Event): Unit = { + def handleEvent(event: Dispatchable): Unit = { eventHandlers(event) } } diff --git a/src/main/scala/ocelot/desktop/ui/event/KeyEvent.scala b/src/main/scala/ocelot/desktop/ui/event/KeyEvent.scala index c35d148..ed6865e 100644 --- a/src/main/scala/ocelot/desktop/ui/event/KeyEvent.scala +++ b/src/main/scala/ocelot/desktop/ui/event/KeyEvent.scala @@ -6,4 +6,4 @@ object KeyEvent { } } -case class KeyEvent(state: KeyEvent.State.Value, code: Int, char: Char) extends Event +case class KeyEvent(state: KeyEvent.State.Value, code: Int, char: Char) extends CapturingEvent diff --git a/src/main/scala/ocelot/desktop/ui/event/MouseEvent.scala b/src/main/scala/ocelot/desktop/ui/event/MouseEvent.scala index ec6486b..2ac0bda 100644 --- a/src/main/scala/ocelot/desktop/ui/event/MouseEvent.scala +++ b/src/main/scala/ocelot/desktop/ui/event/MouseEvent.scala @@ -2,7 +2,7 @@ package ocelot.desktop.ui.event object MouseEvent { object State extends Enumeration { - val Press, Release = Value + val Pressed, Released = Value } object Button extends Enumeration { @@ -12,4 +12,4 @@ object MouseEvent { } } -case class MouseEvent(state: MouseEvent.State.Value, button: MouseEvent.Button.Value) extends Event +case class MouseEvent(state: MouseEvent.State.Value, button: MouseEvent.Button.Value) extends CapturingEvent diff --git a/src/main/scala/ocelot/desktop/ui/event/ScrollEvent.scala b/src/main/scala/ocelot/desktop/ui/event/ScrollEvent.scala index 5cfa4b2..291546e 100644 --- a/src/main/scala/ocelot/desktop/ui/event/ScrollEvent.scala +++ b/src/main/scala/ocelot/desktop/ui/event/ScrollEvent.scala @@ -1,3 +1,3 @@ package ocelot.desktop.ui.event -case class ScrollEvent(offset: Int) extends Event +case class ScrollEvent(offset: Int) extends CapturingEvent diff --git a/src/main/scala/ocelot/desktop/ui/event/handlers/MouseHandler.scala b/src/main/scala/ocelot/desktop/ui/event/handlers/MouseHandler.scala index d904971..05c4465 100644 --- a/src/main/scala/ocelot/desktop/ui/event/handlers/MouseHandler.scala +++ b/src/main/scala/ocelot/desktop/ui/event/handlers/MouseHandler.scala @@ -25,10 +25,10 @@ trait MouseHandler extends Widget { protected def allowClickReleaseOutsideThreshold: Boolean = !receiveDragEvents eventHandlers += { - case MouseEvent(MouseEvent.State.Press, button) => + case MouseEvent(MouseEvent.State.Pressed, button) => startPositions += (button -> UiHandler.mousePosition) - case MouseEvent(MouseEvent.State.Release, button) => + case MouseEvent(MouseEvent.State.Released, button) => val mousePos = UiHandler.mousePosition val dragStopped = receiveDragEvents && dragButtons.remove(button) diff --git a/src/main/scala/ocelot/desktop/ui/event/sources/BrainEvents.scala b/src/main/scala/ocelot/desktop/ui/event/sources/BrainEvents.scala new file mode 100644 index 0000000..353b521 --- /dev/null +++ b/src/main/scala/ocelot/desktop/ui/event/sources/BrainEvents.scala @@ -0,0 +1,19 @@ +package ocelot.desktop.ui.event.sources + +import totoro.ocelot.brain.event.{Event, EventBus} + +import java.util.concurrent.ConcurrentLinkedDeque + +object BrainEvents { + val events = new ConcurrentLinkedDeque[Event]() + + private val eventSubscription = EventBus.subscribe { event => + events.add(event) + + () + } + + def destroy(): Unit = { + eventSubscription.cancel() + } +} diff --git a/src/main/scala/ocelot/desktop/ui/event/sources/MouseEvents.scala b/src/main/scala/ocelot/desktop/ui/event/sources/MouseEvents.scala index 8b048d3..4f299aa 100644 --- a/src/main/scala/ocelot/desktop/ui/event/sources/MouseEvents.scala +++ b/src/main/scala/ocelot/desktop/ui/event/sources/MouseEvents.scala @@ -25,12 +25,12 @@ object MouseEvents { if (MouseEvent.Button.values.map(_.id).contains(buttonIdx)) { val button = MouseEvent.Button(buttonIdx) - val state = if (Mouse.getEventButtonState) MouseEvent.State.Press else MouseEvent.State.Release + val state = if (Mouse.getEventButtonState) MouseEvent.State.Pressed else MouseEvent.State.Released _events += MouseEvent(state, button) state match { - case MouseEvent.State.Press => + case MouseEvent.State.Pressed => _pressedButtons += button - case MouseEvent.State.Release => + case MouseEvent.State.Released => _pressedButtons -= button } } @@ -49,7 +49,7 @@ object MouseEvents { def releaseButtons(): Unit = { for (button <- pressedButtons) { - _events += MouseEvent(MouseEvent.State.Release, button) + _events += MouseEvent(MouseEvent.State.Released, button) } _pressedButtons.clear() diff --git a/src/main/scala/ocelot/desktop/ui/layout/LinearLayout.scala b/src/main/scala/ocelot/desktop/ui/layout/LinearLayout.scala index 2d8197a..9d0a4ae 100644 --- a/src/main/scala/ocelot/desktop/ui/layout/LinearLayout.scala +++ b/src/main/scala/ocelot/desktop/ui/layout/LinearLayout.scala @@ -4,12 +4,16 @@ import ocelot.desktop.geometry.Size2D import ocelot.desktop.ui.widget.Widget import ocelot.desktop.util.Orientation -class LinearLayout(widget: Widget, - orientation: Orientation.Value = Orientation.Horizontal, - justifyContent: JustifyContent.Value = JustifyContent.Start, - alignItems: AlignItems.Value = AlignItems.Stretch, - gap: Float = 0f) - extends Layout(widget) { +import scala.collection.immutable.ArraySeq + +class LinearLayout( + widget: Widget, + orientation: Orientation.Value = Orientation.Horizontal, + justifyContent: JustifyContent.Value = JustifyContent.Start, + alignItems: AlignItems.Value = AlignItems.Stretch, + gap: Float = 0f, +) extends Layout(widget) { + override def recalculateBounds(): Unit = { super.recalculateBounds() @@ -138,7 +142,12 @@ class LinearLayout(widget: Widget, pos += child.size.width + gap } - (orientation, alignItems) match { + val align = (child match { + case w: LinearLayout.WithOptions => w.align + case _ => None + }).getOrElse(alignItems) + + (orientation, align) match { case (Orientation.Vertical, AlignItems.Start) | (Orientation.Vertical, AlignItems.Stretch) => child.rawSetPosition(child.position.setX(widget.position.x)) @@ -160,3 +169,17 @@ class LinearLayout(widget: Widget, } } } + +object LinearLayout { + class WithOptions(widget: Widget, val align: Option[AlignItems.Value]) extends Widget { + override protected val layout: Layout = new CopyLayout(this) + + children = ArraySeq(widget) + } + + object WithOptions { + def apply( + align: Option[AlignItems.Value] = None, + )(widget: Widget) = new WithOptions(widget, align) + } +} diff --git a/src/main/scala/ocelot/desktop/ui/particle/Particle.scala b/src/main/scala/ocelot/desktop/ui/particle/Particle.scala new file mode 100644 index 0000000..60ffda5 --- /dev/null +++ b/src/main/scala/ocelot/desktop/ui/particle/Particle.scala @@ -0,0 +1,27 @@ +package ocelot.desktop.ui.particle + +import ocelot.desktop.graphics.Graphics +import ocelot.desktop.ui.widget.Widget + +/** + * Everything a single particle needs to live and thrive. + * @param time current lifetime of the particle + * @param speed speed modifier + * @param ttl when the `time` will reach this value - the particle is going to be discarded + * @param origin optional, marks the widget that "spawned" this particle + */ + +case class Particle( + var time: Float = 0.0f, + var speed: Float = 1.0f, + var ttl: Float = 1.0f, + origin: Option[Widget] = None, +) { + def expired: Boolean = time >= ttl + + def update(dt: Float): Unit = { + time += speed * dt + } + + def draw(g: Graphics): Unit = {} +} diff --git a/src/main/scala/ocelot/desktop/ui/particle/ParticleSystem.scala b/src/main/scala/ocelot/desktop/ui/particle/ParticleSystem.scala new file mode 100644 index 0000000..42e93a2 --- /dev/null +++ b/src/main/scala/ocelot/desktop/ui/particle/ParticleSystem.scala @@ -0,0 +1,54 @@ +package ocelot.desktop.ui.particle + +import ocelot.desktop.graphics.Graphics +import ocelot.desktop.ui.widget.Widget + +import scala.collection.mutable +import scala.collection.mutable.ListBuffer +import scala.reflect.{ClassTag, classTag} + +class ParticleSystem { + private val particles: ListBuffer[Particle] = ListBuffer.empty + private val counts = mutable.HashMap.empty[(Class[_], Option[Widget]), Int] + + def add(particle: Particle): Unit = { + particles += particle + updateCount(particle, 1) + } + + def count[A : ClassTag](origin: Option[Widget]): Int = counts.getOrElse((classTag[A].runtimeClass, origin), 0) + + /** + * Calls `update` method on all particles, advances time. + * Removes expired particles. + * @param dt delta time + */ + def update(dt: Float): Unit = { + particles.filterInPlace { particle => + particle.update(dt) + val expired = particle.expired + + if (expired) { + updateCount(particle, -1) + } + + !expired + } + } + + def draw(g: Graphics): Unit = { + particles.foreach(_.draw(g)) + } + + def clear(): Unit = { + particles.clear() + } + + private def updateCount(particle: Particle, delta: Int): Unit = { + counts.updateWith((particle.getClass, particle.origin)) { v => + val newValue = v.getOrElse(0) + delta + + Option.when(newValue > 0)(newValue) + } + } +} diff --git a/src/main/scala/ocelot/desktop/ui/widget/Button.scala b/src/main/scala/ocelot/desktop/ui/widget/Button.scala index 7e983ac..98807cc 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/Button.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/Button.scala @@ -26,7 +26,7 @@ class Button(tooltip: Option[Tooltip] = None) extends Widget with MouseHandler w override protected def receiveClickEvents: Boolean = true eventHandlers += { - case MouseEvent(MouseEvent.State.Press, MouseEvent.Button.Left) if enabled => + case MouseEvent(MouseEvent.State.Pressed, MouseEvent.Button.Left) if enabled => clickSoundSource.press.play() case ClickEvent(MouseEvent.Button.Left, _) if enabled => diff --git a/src/main/scala/ocelot/desktop/ui/widget/ComputerErrorMessageLabel.scala b/src/main/scala/ocelot/desktop/ui/widget/ComputerErrorMessageLabel.scala deleted file mode 100644 index afa21ca..0000000 --- a/src/main/scala/ocelot/desktop/ui/widget/ComputerErrorMessageLabel.scala +++ /dev/null @@ -1,23 +0,0 @@ -package ocelot.desktop.ui.widget - -import ocelot.desktop.ColorScheme -import ocelot.desktop.color.Color -import ocelot.desktop.geometry.Vector2D -import ocelot.desktop.node.Node - -class ComputerErrorMessageLabel(node: Node, override val text: String) extends Label { - override def isSmall: Boolean = true - - var alpha: Float = 1f - - override def color: Color = ColorScheme("ErrorMessage").toRGBANorm.mapA(_ * alpha) - - val initialPosition: Vector2D = { - val position = node.position - val size = node.size - - position + Vector2D(size.width / 2 - minimumSize.width / 2, -minimumSize.height) - } - - position = initialPosition -} diff --git a/src/main/scala/ocelot/desktop/ui/widget/DiskEditWindow.scala b/src/main/scala/ocelot/desktop/ui/widget/DiskEditWindow.scala index 488d4d7..3cc583f 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/DiskEditWindow.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/DiskEditWindow.scala @@ -4,35 +4,33 @@ import ocelot.desktop.ColorScheme import ocelot.desktop.color.Color import ocelot.desktop.geometry.{Padding2D, Size2D} import ocelot.desktop.graphics.IconSource -import ocelot.desktop.inventory.traits.DiskItem import ocelot.desktop.ui.layout.{AlignItems, LinearLayout} +import ocelot.desktop.ui.widget.DiskEditWindow.{CanChangeManaged, EditableDisk, HasColor, Lockable} import ocelot.desktop.ui.widget.window.PanelWindow import ocelot.desktop.util.SizeFormatting.formatSize import ocelot.desktop.util.animation.ColorAnimation import ocelot.desktop.util.{DiskDriveAware, Orientation} -import totoro.ocelot.brain.entity.traits.{DiskManaged, DiskUnmanaged} +import totoro.ocelot.brain.entity.traits.{DiskManaged, DiskUnmanaged, Environment} import totoro.ocelot.brain.util.DyeColor -class DiskEditWindow(item: DiskItem) extends PanelWindow { - private def diskSize: Option[Long] = Some(item.entity).collect { case disk: DiskManaged => +class DiskEditWindow(disk: EditableDisk) extends PanelWindow { + private def diskSize: Option[Long] = disk.disk.collect { case disk: DiskManaged => disk.fileSystem.fileSystem.spaceUsed } - private def diskCapacity: Long = item.entity.capacity - - private def isWriteable: Boolean = item.entity match { - case disk: DiskManaged => !disk.fileSystem.fileSystem.isReadOnly - case disk: DiskUnmanaged => !disk.isLocked + private def isWriteable: Boolean = disk.disk match { + case Some(disk: DiskManaged) => !disk.fileSystem.fileSystem.isReadOnly + case Some(disk: DiskUnmanaged) => !disk.isLocked + case None => false } - private def isManaged: Boolean = item.entity match { - case _: DiskManaged => true - case _: DiskUnmanaged => false + private def isManaged: Boolean = disk.disk match { + case Some(_: DiskManaged) => true + case Some(_: DiskUnmanaged) => false + case None => false } - private def address: Option[String] = Option(item.entity.node).flatMap(node => Option(node.address)) - - override protected def title: String = s"${item.diskKind} " + address.getOrElse("") + override protected def title: String = s"${disk.diskKind} " + disk.diskAddress.getOrElse("") override protected def titleMaxLength: Int = 32 @@ -42,8 +40,8 @@ class DiskEditWindow(item: DiskItem) extends PanelWindow { children :+= new PaddingBox( new Label { override def text: String = diskSize match { - case Some(used) => s"Used: ${formatSize(used)} / ${formatSize(diskCapacity)}" - case None => s"Capacity: ${formatSize(diskCapacity)}" + case Some(used) => s"Used: ${formatSize(used)} / ${formatSize(disk.capacity)}" + case None => s"Capacity: ${formatSize(disk.capacity)}" } }, Padding2D(bottom = 12), @@ -51,96 +49,170 @@ class DiskEditWindow(item: DiskItem) extends PanelWindow { children :+= new PaddingBox( new Widget { - override val layout = new LinearLayout(this, - orientation = Orientation.Horizontal, alignItems = AlignItems.Center) + override val layout = new LinearLayout( + this, + orientation = Orientation.Horizontal, + alignItems = AlignItems.Center, + ) children :+= new Label("Label") - children :+= new TextInput(item.label.getOrElse("")) { + children :+= new TextInput(disk.diskLabel.getOrElse("")) { override def onConfirm(): Unit = { super.onConfirm() - item.setLabel(Some(text).filter(_.nonEmpty)) + disk.diskLabel = Some(text).filter(_.nonEmpty) } - override def enabled: Boolean = item.isLabelWriteable + override def enabled: Boolean = disk.editingAllowed && disk.isLabelWriteable } }, Padding2D(bottom = 12), ) - if (item.color.nonEmpty) { - val colorSelector: Widget = new Widget { - override val layout = new LinearLayout(this, orientation = Orientation.Vertical, gap = 2) + disk match { + case disk: EditableDisk with HasColor => + val colorSelector: Widget = new Widget { + override val layout = new LinearLayout(this, orientation = Orientation.Vertical, gap = 2) - for (row <- DyeColor.All.iterator.grouped(8)) { - children :+= new Widget { - override val layout = new LinearLayout(this, orientation = Orientation.Horizontal, gap = 2) + for (row <- DyeColor.All.iterator.grouped(8)) { + children :+= new Widget { + override val layout = new LinearLayout(this, orientation = Orientation.Horizontal, gap = 2) - for (dyeColor <- row) { - def isColorSelected: Boolean = dyeColor == item.color.get + for (dyeColor <- row) { + def isColorSelected: Boolean = dyeColor == disk.color - val floppyIcon = IconSource.Items.FloppyDisk(dyeColor).path - children :+= new IconButton( - floppyIcon, - floppyIcon, - mode = IconButton.Mode.Radio, - drawBackground = true, - tooltip = Some(DiskDriveAware.ColorNames(dyeColor)), - padding = 2, - sizeMultiplier = 2f, - model = IconButton.ReadOnlyModel(isColorSelected), - ) { - private val borderAnimation = new ColorAnimation(targetBorderColor, IconButton.AnimationSpeed) + val floppyIcon = IconSource.Items.FloppyDisk(dyeColor) + children :+= new IconButton( + floppyIcon, + floppyIcon, + mode = IconButton.Mode.Radio, + drawBackground = true, + tooltip = Some(DiskDriveAware.ColorNames(dyeColor)), + padding = 2, + sizeMultiplier = 2f, + model = IconButton.ReadOnlyModel(isColorSelected), + ) { + private val borderAnimation = new ColorAnimation(targetBorderColor, IconButton.AnimationSpeed) - private def targetBorderColor: Color = - if (isColorSelected) ColorScheme("FloppyIconButtonBorderSelected") - else ColorScheme("ButtonBorder") + private def targetBorderColor: Color = + if (isColorSelected) ColorScheme("FloppyIconButtonBorderSelected") + else ColorScheme("ButtonBorder") - override protected def updateAnimationTargets(): Unit = { - super.updateAnimationTargets() - borderAnimation.goto(targetBorderColor) - } + override protected def updateAnimationTargets(): Unit = { + super.updateAnimationTargets() + borderAnimation.goto(targetBorderColor) + } - override def borderColor: Color = borderAnimation.color + override def borderColor: Color = borderAnimation.color - override def onPressed(): Unit = { - if (!isColorSelected) { - item.setColor(dyeColor) + override def onPressed(): Unit = { + if (disk.editingAllowed && !isColorSelected) { + disk.color = dyeColor + } + } + + override def update(): Unit = { + super.update() + borderAnimation.update() } } + } - override def update(): Unit = { - super.update() - borderAnimation.update() - } + } + } + } + + children :+= new PaddingBox(colorSelector, Padding2D(bottom = 12)) + + case _ => + } + + private object BottomRow extends Widget { + override val layout = new LinearLayout(this, orientation = Orientation.Horizontal, gap = 8) + + disk match { + case disk: EditableDisk with Lockable => + children :+= new Button { + override def text: String = "Make read-only" + + override def onClick(): Unit = { + if (disk.editingAllowed) { + disk.lock() } } + override def enabled: Boolean = isWriteable } - } + + case _ => } - children :+= new PaddingBox(colorSelector, Padding2D(bottom = 12)) + disk match { + case disk: EditableDisk with CanChangeManaged => + children :+= new Button { + override def text: String = s"Make ${if (isManaged) "unmanaged" else "managed"}" + + override def onClick(): Unit = { + if (disk.editingAllowed) { + disk.setManaged(!isManaged) + } + } + + override def maximumSize: Size2D = super.maximumSize.copy(width = Float.PositiveInfinity) + } + + case _ => + } } - children :+= new Widget { - override val layout = new LinearLayout(this, orientation = Orientation.Horizontal, gap = 8) - - children :+= new Button { - override def text: String = "Make read-only" - - override def onClick(): Unit = item.lock() - - override def enabled: Boolean = isWriteable - } - - children :+= new Button { - override def text: String = s"Make ${if (isManaged) "unmanaged" else "managed"}" - - override def onClick(): Unit = item.setManaged(!isManaged) - - override def maximumSize: Size2D = super.maximumSize.copy(width = Float.PositiveInfinity) - } + if (BottomRow.children.nonEmpty) { + children :+= BottomRow } }) + + override def update(): Unit = { + super.update() + + if (!disk.editingAllowed) { + close() + } + } +} + +object DiskEditWindow { + trait EditableDisk { + def disk: Option[Environment] + + def diskAddress: Option[String] = for ( + disk <- disk; + node <- Option(disk.node); + address <- Option(node.address) + ) yield address + + def capacity: Long + + def diskKind: String + + def diskLabel: Option[String] + + def diskLabel_=(label: Option[String]): Unit + + def isLabelWriteable: Boolean + + def editingAllowed: Boolean + } + + trait HasColor { + def color: DyeColor + + def color_=(color: DyeColor): Unit + } + + trait Lockable { + def lock(): Unit + } + + trait CanChangeManaged { + def setManaged(managed: Boolean): Unit + } } diff --git a/src/main/scala/ocelot/desktop/ui/widget/EventHandlers.scala b/src/main/scala/ocelot/desktop/ui/widget/EventHandlers.scala index 70a15f7..7b0901b 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/EventHandlers.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/EventHandlers.scala @@ -1,29 +1,27 @@ package ocelot.desktop.ui.widget -import ocelot.desktop.ui.event.Event +import ocelot.desktop.ui.event.{Dispatchable, Event} -import scala.collection.mutable.ListBuffer +import scala.collection.mutable.ArrayBuffer -class EventHandlers extends PartialFunction[Event, Unit] { - private val handlers = new ListBuffer[EventHandler] +class EventHandlers extends PartialFunction[Dispatchable, Unit] { + private val handlers = ArrayBuffer.empty[EventHandler] def +=(handler: EventHandler): Unit = handlers += handler def -=(handler: EventHandler): Unit = handlers -= handler - override def isDefinedAt(event: Event): Boolean = handlers.exists(_.isDefinedAt(event)) + override def isDefinedAt(event: Dispatchable): Boolean = handlers.exists(_.isDefinedAt(event)) - override def apply(event: Event): Unit = { + override def apply(event: Dispatchable): Unit = { for (handler <- handlers) { if (event.consumed) { return } - if (handler.isDefinedAt(event)) { - handler(event) - } + handler.applyOrElse(event, (_: Dispatchable) => ()) } } - type EventHandler = PartialFunction[Event, Unit] + type EventHandler = PartialFunction[Dispatchable, Unit] } diff --git a/src/main/scala/ocelot/desktop/ui/widget/Histogram.scala b/src/main/scala/ocelot/desktop/ui/widget/Histogram.scala index be259a4..ce21bb0 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/Histogram.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/Histogram.scala @@ -2,7 +2,7 @@ package ocelot.desktop.ui.widget import ocelot.desktop.ColorScheme import ocelot.desktop.color.Color -import ocelot.desktop.graphics.Graphics +import ocelot.desktop.graphics.{Graphics, IconSource} import scala.collection.mutable.ArrayBuffer @@ -13,8 +13,8 @@ class Histogram extends Widget { private def drawBars(g: Graphics): Unit = { def drawBarSegment(i: Int, color: Color): Unit = { - g.sprite("BarSegment", position.x, position.y + i * 6, 16, 4, color) - g.sprite("BarSegment", position.x + 18, position.y + i * 6, 16, 4, color) + g.sprite(IconSource.BarSegment, position.x, position.y + i * 6, 16, 4, color) + g.sprite(IconSource.BarSegment, position.x + 18, position.y + i * 6, 16, 4, color) } val ratio = history.last diff --git a/src/main/scala/ocelot/desktop/ui/widget/Icon.scala b/src/main/scala/ocelot/desktop/ui/widget/Icon.scala index f796545..71532b8 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/Icon.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/Icon.scala @@ -17,16 +17,13 @@ class Icon(icon: IconSource, size: Size2D = null, private val color: Color = Col def iconColor: Color = color def iconSize: Size2D = { - if (size != null) size - else if (icon.animation.isDefined && icon.animation.get.frameSize.isDefined) - icon.animation.get.frameSize.get - else Spritesheet.spriteSize(icon.path) + Option(size).getOrElse(Spritesheet.spriteSize(icon)) } override def minimumSize: Size2D = iconSize override def maximumSize: Size2D = minimumSize override def draw(g: Graphics): Unit = { - g.sprite(icon.path, bounds.x, bounds.y, bounds.w, bounds.h, iconColor, icon.animation) + g.sprite(icon, bounds, iconColor) } } diff --git a/src/main/scala/ocelot/desktop/ui/widget/IconButton.scala b/src/main/scala/ocelot/desktop/ui/widget/IconButton.scala index 77150d3..0bfb149 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/IconButton.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/IconButton.scala @@ -4,7 +4,7 @@ import ocelot.desktop.ColorScheme import ocelot.desktop.audio.{ClickSoundSource, SoundSource} import ocelot.desktop.color.Color import ocelot.desktop.geometry.Size2D -import ocelot.desktop.graphics.Graphics +import ocelot.desktop.graphics.{Graphics, IconSource} import ocelot.desktop.ui.event.handlers.{HoverHandler, MouseHandler} import ocelot.desktop.ui.event.{ClickEvent, HoverEvent, MouseEvent} import ocelot.desktop.ui.widget.IconButton.{AnimationSpeed, Mode} @@ -14,8 +14,8 @@ import ocelot.desktop.util.animation.{ColorAnimation, ValueAnimation} import ocelot.desktop.util.{DrawUtils, Spritesheet} class IconButton( - releasedIcon: String, - pressedIcon: String, + releasedIconSource: IconSource, + pressedIconSource: IconSource, releasedColor: Color = Color.White, pressedColor: Color = Color.White, sizeMultiplier: Float = 1, @@ -35,7 +35,7 @@ class IconButton( case HoverEvent(HoverEvent.State.Enter) => onHoverEnter() case HoverEvent(HoverEvent.State.Leave) => onHoverLeave() - case MouseEvent(MouseEvent.State.Press, MouseEvent.Button.Left) => + case MouseEvent(MouseEvent.State.Pressed, MouseEvent.Button.Left) => mode match { case Mode.Regular => handlePress() case _ => // the other modes are triggered on click. @@ -43,7 +43,7 @@ class IconButton( clickSoundSource.press.play() - case MouseEvent(MouseEvent.State.Release, MouseEvent.Button.Left) => mode match { + case MouseEvent(MouseEvent.State.Released, MouseEvent.Button.Left) => mode match { case Mode.Regular if model.pressed => handleRelease() clickSoundSource.release.play() @@ -51,7 +51,8 @@ class IconButton( case _ => // the other modes are triggered on click. } - case ClickEvent(MouseEvent.Button.Left, _) => mode match { + case ClickEvent(MouseEvent.Button.Left, _) => + mode match { case Mode.Regular => // regular buttons are handled above. case Mode.Switch if model.pressed => handleRelease() @@ -62,12 +63,38 @@ class IconButton( handlePress() clickSoundSource.release.play() } + + onClicked() } + /** + * Called when the state is changed to pressed. + * + * UX note: override this for [[Mode.Switch]] and [[Mode.Radio]] buttons. + * + * @see [[onClicked]] for [[Mode.Regular]] buttons. + */ def onPressed(): Unit = {} + /** + * Called when the state is changed to released. + * + * UX note: override this for [[Mode.Switch]] and [[Mode.Radio]] buttons. + * + * @see [[onClicked]] for [[Mode.Regular]] buttons. + */ def onReleased(): Unit = {} + /** + * Called when the button is clicked (the mouse button has been released while it's within the button's bounds), + * regardless of the state. + * + * UX note: override this for [[Mode.Regular]] buttons. + * + * @see [[onPressed]] and [[onReleased]] for [[Mode.Switch]] and [[Mode.Radio]] buttons. + */ + def onClicked(): Unit = {} + private def onHoverEnter(): Unit = { updateColorAnimationTargets() @@ -108,9 +135,9 @@ class IconButton( size = minimumSize - private def releasedIconSize: Size2D = Spritesheet.spriteSize(releasedIcon) * sizeMultiplier + private def releasedIconSize: Size2D = Spritesheet.spriteSize(releasedIconSource) * sizeMultiplier - private def pressedIconSize: Size2D = Spritesheet.spriteSize(pressedIcon) * sizeMultiplier + private def pressedIconSize: Size2D = Spritesheet.spriteSize(pressedIconSource) * sizeMultiplier override def minimumSize: Size2D = releasedIconSize.max(pressedIconSize) + (padding * 2.0f) override def maximumSize: Size2D = minimumSize @@ -127,7 +154,7 @@ class IconButton( if (iconMixAnimation.value < 1f) { g.sprite( - releasedIcon, + releasedIconSource, position + (size - releasedIconSize) / 2f, releasedIconSize, releasedColorAnimation.color.toRGBANorm.mapA(_ * (1f - iconMixAnimation.value)), @@ -136,7 +163,7 @@ class IconButton( if (iconMixAnimation.value > 0f) { g.sprite( - pressedIcon, + pressedIconSource, position + (size - pressedIconSize) / 2f, pressedIconSize, pressedColorAnimation.color.toRGBANorm.mapA(_ * iconMixAnimation.value), @@ -178,22 +205,24 @@ object IconButton { sealed trait Mode object Mode { - - /** Your regular, run-of-the-mill button you love. + /** + * Your regular, run-of-the-mill button you love. * * When the LMB is depressed, [[IconButton.onPressed]] is called. * After it's released, [[IconButton.onReleased]] is called. */ case object Regular extends Mode - /** A toggleable button, or a switch. + /** + * A toggleable button, or a switch. * * Clicking the LMB while the button is pressed releases it, and [[IconButton.onReleased]] is called. * Clicking the LMB while the button is released depresses it, and [[IconButton.onPressed]] is called. */ case object Switch extends Mode - /** A radio button is like a switch except clicking on it while it's depressed doesn't do anything. + /** + * A radio button is like a switch except clicking on it while it's depressed doesn't do anything. * * [[IconButton.onReleased]] is never called (unless you do it yourself). * @@ -202,18 +231,21 @@ object IconButton { case object Radio extends Mode } - /** This is the visible button state. + /** + * This is the visible button state. * * When [[IconButton.update]] notices a state change, it runs its animations. * * Note that callbacks are run after running the setter regardless of whether it does anything. */ trait Model { - - /** Whether to display the pressed button state. */ + /** + * Whether to display the pressed button state. + */ def pressed: Boolean - /** This setter is used by [[IconButton]] to respond to user interaction. + /** + * This setter is used by [[IconButton]] to respond to user interaction. * * You may want to override this method to do nothing * if you're proxying another model and updating it in callbacks. @@ -221,10 +253,14 @@ object IconButton { def pressed_=(newValue: Boolean): Unit } - /** This is the default model that simply reads from a variable and writes to it. */ + /** + * This is the default model that simply reads from a variable and writes to it. + */ case class DefaultModel(override var pressed: Boolean) extends Model - /** This is a model that ignores user input (presumably you handle it in callbacks). */ + /** + * This is a model that ignores user input (presumably you handle it in callbacks). + */ class ReadOnlyModel(f: () => Boolean) extends Model { override def pressed: Boolean = f() @@ -232,8 +268,8 @@ object IconButton { } object ReadOnlyModel { - - /** A utility method to create a new instance of [[ReadOnlyModel]]. + /** + * A utility method to create a new instance of [[ReadOnlyModel]]. * * The value returned by [[ReadOnlyModel.pressed]] is obtained * by evaluating the provided expression '''every time'''. diff --git a/src/main/scala/ocelot/desktop/ui/widget/Knob.scala b/src/main/scala/ocelot/desktop/ui/widget/Knob.scala index 1dbefc0..e76c117 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/Knob.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/Knob.scala @@ -2,7 +2,7 @@ package ocelot.desktop.ui.widget import ocelot.desktop.color.{Color, IntColor} import ocelot.desktop.geometry.{Size2D, Vector2D} -import ocelot.desktop.graphics.Graphics +import ocelot.desktop.graphics.{Graphics, IconSource} import ocelot.desktop.ui.UiHandler import ocelot.desktop.ui.event.MouseEvent import totoro.ocelot.brain.util.DyeColor @@ -28,14 +28,14 @@ abstract class Knob(dyeColor: DyeColor = DyeColor.Red) extends Widget { private var startValue: Int = 0 eventHandlers += { - case MouseEvent(MouseEvent.State.Press, MouseEvent.Button.Left) => + case MouseEvent(MouseEvent.State.Pressed, MouseEvent.Button.Left) => val mousePos = UiHandler.mousePosition if (bounds.contains(mousePos)) { movePos = Some(mousePos) startValue = input } - case MouseEvent(MouseEvent.State.Release, MouseEvent.Button.Left) => + case MouseEvent(MouseEvent.State.Released, MouseEvent.Button.Left) => movePos = None } @@ -47,14 +47,14 @@ abstract class Knob(dyeColor: DyeColor = DyeColor.Red) extends Widget { } override def draw(g: Graphics): Unit = { - g.sprite("KnobLimits", position.x, position.y, 25, 25) + g.sprite(IconSource.KnobLimits, position.x, position.y, 25, 25) g.save() g.translate(position.x + 12.5f, position.y + 12.5f) g.rotate(input.toFloat / 15f * 4.71239f - 0.785398f) - g.sprite("Knob", -12.5f, -12.5f, 25, 25, color) + g.sprite(IconSource.Knob, -12.5f, -12.5f, 25, 25, color) g.restore() - g.sprite("KnobCenter", position.x, position.y, 25, 25) + g.sprite(IconSource.KnobCenter, position.x, position.y, 25, 25) val centerColor = color.toRGBANorm.copy(a = output / 15f) - g.sprite("KnobCenter", position.x - 1, position.y - 1, 27, 27, centerColor) + g.sprite(IconSource.KnobCenter, position.x - 1, position.y - 1, 27, 27, centerColor) } } diff --git a/src/main/scala/ocelot/desktop/ui/widget/LogWidget.scala b/src/main/scala/ocelot/desktop/ui/widget/LogWidget.scala index 49c579e..b9953b6 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/LogWidget.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/LogWidget.scala @@ -9,7 +9,7 @@ import ocelot.desktop.ui.widget.LogWidget.{BorderThickness, EntryMargin, EntryPa import ocelot.desktop.util.{DrawUtils, Orientation} import scala.annotation.tailrec -import scala.collection.mutable.ArrayBuffer +import scala.collection.mutable abstract class LogWidget extends Widget { override protected val layout: Layout = new LinearLayout(this, orientation = Orientation.Vertical) @@ -17,18 +17,30 @@ abstract class LogWidget extends Widget { private object MessageListWidget extends Widget { messageList => override protected val layout: Layout = new Layout(this) - // NOTE: access to entries must be synchronized! - private val entries = ArrayBuffer.empty[Entry] + private val entries = mutable.ArrayDeque.empty[Entry] val dummyEntry: Entry = new RxEntry("", nextMessageY) - override def minimumSize: Size2D = Size2D(dummyEntry.minimumSize.width + 2 * EntryMargin, nextMessageY) + private def removedOffset: Float = entries.headOption.map(_.y - EntryMargin).getOrElse(0) - private def nextMessageY: Float = entries.synchronized { + private def applyRemovedOffset(): Unit = { + val offset = removedOffset + + for (entry <- entries) { + entry.y -= offset + } + } + + override def minimumSize: Size2D = Size2D( + dummyEntry.minimumSize.width + 2 * EntryMargin, + nextMessageY - removedOffset, + ) + + private def nextMessageY: Float = { entries.lastOption.map(_.maxY).getOrElse(0f) + EntryMargin } - def addEntry(entry: LogEntry): Unit = entries.synchronized { + def addEntry(entry: LogEntry): Unit = { entries += (entry match { case LogEntry.Rx(message) => new RxEntry(message, nextMessageY) case LogEntry.Tx(message) => new TxEntry(message, nextMessageY) @@ -37,19 +49,11 @@ abstract class LogWidget extends Widget { parent.get.recalculateBoundsAndRelayout() } - def removeFirst(count: Int): Unit = entries.synchronized { + def removeFirst(count: Int): Unit = { if (count >= entries.length) { entries.clear() } else { entries.dropInPlace(count) - - val offset = entries(0).y - EntryMargin - - if (offset > 0) { - for (entry <- entries) { - entry.y -= offset - } - } } parent.get.recalculateBoundsAndRelayout() @@ -68,7 +72,9 @@ abstract class LogWidget extends Widget { } } - override def draw(g: Graphics): Unit = entries.synchronized { + override def draw(g: Graphics): Unit = { + applyRemovedOffset() + val firstVisibleIdx = firstVisibleIdxSearch(parent.get.asInstanceOf[ScrollView].offset.y) for (entry <- entries.iterator.drop(firstVisibleIdx).takeWhile(_.absoluteBounds.y <= clippedBounds.max.y)) { @@ -118,7 +124,7 @@ abstract class LogWidget extends Widget { def border: RGBAColorNorm override def draw(g: Graphics): Unit = { - val innerBounds = absoluteBounds.inflate(-BorderThickness) + val innerBounds = absoluteBounds.inflated(-BorderThickness) g.rect(innerBounds, background) DrawUtils.ring( g, diff --git a/src/main/scala/ocelot/desktop/ui/widget/MenuBar.scala b/src/main/scala/ocelot/desktop/ui/widget/MenuBar.scala index 9aabde2..5e81b32 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/MenuBar.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/MenuBar.scala @@ -3,12 +3,13 @@ package ocelot.desktop.ui.widget import ocelot.desktop.audio.SoundSource import ocelot.desktop.geometry.{Padding2D, Size2D} import ocelot.desktop.graphics.{Graphics, IconSource} -import ocelot.desktop.ui.event.KeyEvent import ocelot.desktop.ui.widget.contextmenu.{ContextMenuEntry, ContextMenuSubmenu} import ocelot.desktop.ui.widget.help.{AboutDialog, UpdateCheckerDialog} import ocelot.desktop.ui.widget.settings.SettingsDialog +import ocelot.desktop.util.Keybind.{QuickLoad, QuickSave} +import ocelot.desktop.util.Keymap.Press import ocelot.desktop.{ColorScheme, OcelotDesktop} -import org.lwjgl.input.Keyboard + class MenuBar extends Widget { override def receiveMouseEvents: Boolean = true @@ -22,28 +23,28 @@ class MenuBar extends Widget { addEntry(new MenuBarSubmenu( "File", menu => { - menu.addEntry(ContextMenuEntry("New", IconSource.Plus) { OcelotDesktop.newWorkspace() }) - menu.addEntry(ContextMenuEntry("Open", IconSource.Folder) { OcelotDesktop.showOpenDialog() }) - menu.addEntry(ContextMenuEntry("Save", IconSource.Save) { OcelotDesktop.save() }) - menu.addEntry(ContextMenuEntry("Save as", IconSource.SaveAs) { OcelotDesktop.saveAs() }) + menu.addEntry(ContextMenuEntry("New", IconSource.Icons.Plus) { OcelotDesktop.newWorkspace() }) + menu.addEntry(ContextMenuEntry("Open", IconSource.Icons.Folder) { OcelotDesktop.showOpenDialog() }) + menu.addEntry(ContextMenuEntry("Save", IconSource.Icons.Save) { OcelotDesktop.save() }) + menu.addEntry(ContextMenuEntry("Save as", IconSource.Icons.SaveAs) { OcelotDesktop.saveAs() }) menu.addSeparator() - menu.addEntry(ContextMenuEntry("Exit", IconSource.Cross, SoundSource.InterfaceClickLow) { OcelotDesktop.exit() }) + menu.addEntry(ContextMenuEntry("Exit", IconSource.Icons.Cross, SoundSource.InterfaceClickLow) { OcelotDesktop.exit() }) }, )) addEntry(new MenuBarSubmenu( "Player", menu => { - menu.addEntry(ContextMenuEntry("Create new", IconSource.Plus) { OcelotDesktop.showAddPlayerDialog() }) + menu.addEntry(ContextMenuEntry("Create new", IconSource.Icons.Plus) { OcelotDesktop.showAddPlayerDialog() }) menu.addSeparator() OcelotDesktop.players.foreach(player => { menu.addEntry(new ContextMenuSubmenu( s"${if (player == OcelotDesktop.players.head) "● " else " "}${player.nickname}", onClick = () => OcelotDesktop.selectPlayer(player.nickname), ) { - addEntry(ContextMenuEntry("Remove", IconSource.Delete, SoundSource.InterfaceClickLow) { + addEntry(ContextMenuEntry("Remove", IconSource.Icons.Delete, SoundSource.InterfaceClickLow) { OcelotDesktop.removePlayer(player.nickname) }) }) @@ -56,12 +57,12 @@ class MenuBar extends Widget { addEntry(new MenuBarSubmenu( "Help", menu => { - menu.addEntry(ContextMenuEntry("Check for Updates", IconSource.Help) { + menu.addEntry(ContextMenuEntry("Check for Updates", IconSource.Icons.Help) { new UpdateCheckerDialog().show() }) menu.addSeparator() - menu.addEntry(ContextMenuEntry("Manual", IconSource.Book) {}.setEnabled(false)) - menu.addEntry(ContextMenuEntry("About", IconSource.Ocelot) { new AboutDialog().show() }) + menu.addEntry(ContextMenuEntry("Manual", IconSource.Icons.Book) {}.setEnabled(false)) + menu.addEntry(ContextMenuEntry("About", IconSource.Icons.Ocelot) { new AboutDialog().show() }) }, )) @@ -70,8 +71,8 @@ class MenuBar extends Widget { }) // fill remaining space eventHandlers += { - case KeyEvent(KeyEvent.State.Press, Keyboard.KEY_F5, _) => OcelotDesktop.save() - case KeyEvent(KeyEvent.State.Press, Keyboard.KEY_F9, _) => OcelotDesktop.showOpenDialog() + case Press(QuickSave) => OcelotDesktop.save() + case Press(QuickLoad) => OcelotDesktop.showOpenDialog() } override def draw(g: Graphics): Unit = { diff --git a/src/main/scala/ocelot/desktop/ui/widget/MenuBarButton.scala b/src/main/scala/ocelot/desktop/ui/widget/MenuBarButton.scala index 25235d4..364b300 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/MenuBarButton.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/MenuBarButton.scala @@ -31,7 +31,7 @@ class MenuBarButton(label: String, handler: () => Unit = () => {}) extends Widge def onMouseLeave(): Unit = colorAnimation.goto(ColorScheme("TitleBarBackground")) eventHandlers += { - case MouseEvent(MouseEvent.State.Press, MouseEvent.Button.Left) => + case MouseEvent(MouseEvent.State.Pressed, MouseEvent.Button.Left) => clickSoundSource.press.play() case ClickEvent(MouseEvent.Button.Left, _) => diff --git a/src/main/scala/ocelot/desktop/ui/widget/RootWidget.scala b/src/main/scala/ocelot/desktop/ui/widget/RootWidget.scala index bdc79a4..521bdc9 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/RootWidget.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/RootWidget.scala @@ -4,17 +4,18 @@ import ocelot.desktop.OcelotDesktop import ocelot.desktop.color.RGBAColor import ocelot.desktop.graphics.Graphics import ocelot.desktop.ui.UiHandler -import ocelot.desktop.ui.event.KeyEvent import ocelot.desktop.ui.layout.{CopyLayout, LinearLayout} import ocelot.desktop.ui.widget.contextmenu.ContextMenus import ocelot.desktop.ui.widget.itemdrag.DraggedItemPool import ocelot.desktop.ui.widget.modal.ModalDialogPool import ocelot.desktop.ui.widget.statusbar.StatusBar import ocelot.desktop.ui.widget.tooltip.TooltipPool +import ocelot.desktop.util.Keybind.{Fullscreen, ReloadWorkspace, UIDebug} +import ocelot.desktop.util.Keymap.Release import ocelot.desktop.util.{DrawUtils, Orientation} -import org.lwjgl.input.Keyboard import totoro.ocelot.brain.nbt.NBTTagCompound + class RootWidget(setupDefaultWorkspace: Boolean = true) extends Widget { override protected val layout = new CopyLayout(this) @@ -50,10 +51,10 @@ class RootWidget(setupDefaultWorkspace: Boolean = true) extends Widget { private var isDebugViewVisible = false eventHandlers += { - case KeyEvent(KeyEvent.State.Release, Keyboard.KEY_F1, _) => + case Release(UIDebug) => isDebugViewVisible = !isDebugViewVisible - case KeyEvent(KeyEvent.State.Release, Keyboard.KEY_F3, _) => + case Release(ReloadWorkspace) => OcelotDesktop.withTickLockAcquired { val backendNBT = new NBTTagCompound val frontendNBT = new NBTTagCompound @@ -63,7 +64,7 @@ class RootWidget(setupDefaultWorkspace: Boolean = true) extends Widget { UiHandler.root.workspaceView.load(frontendNBT) } - case KeyEvent(KeyEvent.State.Release, Keyboard.KEY_F11, _) => + case Release(Fullscreen) => UiHandler.fullScreen = !UiHandler.fullScreen } diff --git a/src/main/scala/ocelot/desktop/ui/widget/ScreenView.scala b/src/main/scala/ocelot/desktop/ui/widget/ScreenView.scala new file mode 100644 index 0000000..75fd90f --- /dev/null +++ b/src/main/scala/ocelot/desktop/ui/widget/ScreenView.scala @@ -0,0 +1,204 @@ +package ocelot.desktop.ui.widget + +import ocelot.desktop.audio.{SoundBuffers, SoundCategory, SoundSource} +import ocelot.desktop.geometry.{Rect2D, Size2D, Vector2D} +import ocelot.desktop.graphics.Graphics +import ocelot.desktop.graphics.Texture.MinFilteringMode +import ocelot.desktop.node.nodes.ScreenNode +import ocelot.desktop.node.nodes.ScreenNode.{FontHeight, FontWidth} +import ocelot.desktop.ui.UiHandler +import ocelot.desktop.ui.event.{KeyEvent, MouseEvent, ScrollEvent} +import ocelot.desktop.ui.layout.Layout +import ocelot.desktop.ui.widget.ScreenView.ScaleTag +import ocelot.desktop.util.{Keybind, Persistable, Register} +import ocelot.desktop.{ColorScheme, OcelotDesktop, Settings} +import org.lwjgl.input.Keyboard +import totoro.ocelot.brain.nbt.NBTTagCompound +import totoro.ocelot.brain.util.{ClipboardSplitter, Tier} + +// TODO: use an interface instead of ScreenNode. +abstract class ScreenView(screenNode: ScreenNode) extends Widget with Persistable { + override protected val layout: Layout = new Layout(this) + + private var lastMousePos = Vector2D.Zero + + val scale: Register.Writeable[Float] = Register(1f) + def scaleX: Float = (FontWidth * scale.value).floor / FontWidth + def scaleY: Float = (FontHeight * scale.value).floor / FontHeight + + private def screenWidth: Int = screenNode.screenWidth + private def screenHeight: Int = screenNode.screenHeight + + override def minimumSize: Size2D = Size2D( + screenWidth * FontWidth * scaleX, + screenHeight * FontHeight * scaleY, + ) + + override def maximumSize: Size2D = minimumSize + + private def toBufferCoords(p: Vector2D): Vector2D = { + // no synchronization here (see the note in ScreenNode): the method to change this property is indirect. + if (screenNode.screen.getPrecisionMode) { + Vector2D( + (p.x - position.x) / FontWidth / scaleX, + (p.y - position.y) / FontHeight / scaleY, + ) + } else { + Vector2D( + math.floor((p.x - position.x) / FontWidth / scaleX), + math.floor((p.y - position.y) / FontHeight / scaleY), + ) + } + } + + private def bufferCoordsInBounds(p: Vector2D): Boolean = + new Rect2D(0, 0, screenWidth, screenHeight).contains(p) + + private def mouseCoordsInBounds: Boolean = + bufferCoordsInBounds(toBufferCoords(UiHandler.mousePosition)) + + override def receiveMouseEvents: Boolean = true + + override def receiveScrollEvents: Boolean = true + + protected def isFocused: Boolean + + private def shouldHandleInput: Boolean = isFocused && !root.get.modalDialogPool.isVisible + + // OC doesn't trigger several touch events in a row; the same holds for drop events. + // For the following inputs: + // LMB down, RMB down, RMB up, LMB up + // ...OC only registers LMB down and RMB up, dropping the other two events. + private var pressedButton: Option[MouseEvent.Button.Value] = None + + // NOTE: events are handled before update(). + // if the brain initiates a viewport size change, mouse events could be sent for coordinates outside the new bounds. + // TODO: look into how OpenComputers deals with that, if it does at all. + eventHandlers += { + case event: KeyEvent if shouldHandleInput && event.code != Keyboard.KEY_ESCAPE => + event.state match { + case KeyEvent.State.Press | KeyEvent.State.Repeat => + screenNode.screen.keyDown(event.char, event.code, OcelotDesktop.player) + + // note: in OpenComputers, key_down signal is fired __before__ clipboard signal + if (event.code == Settings.get.keymap(Keybind.Insert)) { + sendClipboard() + } + + case KeyEvent.State.Release => + screenNode.screen.keyUp(event.char, event.code, OcelotDesktop.player) + } + + event.consume() + + case event: MouseEvent if shouldHandleInput => + val pos = toBufferCoords(UiHandler.mousePosition) + val inBounds = bufferCoordsInBounds(pos) + + if (inBounds) { + lastMousePos = pos + } + + event.state match { + case MouseEvent.State.Pressed if inBounds && screenNode.screen.tier > Tier.One => + if (pressedButton.isEmpty) { + screenNode.screen.mouseDown(pos.x, pos.y, event.button.id, OcelotDesktop.player) + } + + pressedButton = Some(event.button) + event.consume() + + case MouseEvent.State.Released => + if (inBounds && event.button == MouseEvent.Button.Middle) { + sendClipboard() + event.consume() + } + + if (pressedButton.nonEmpty) { + screenNode.screen.mouseUp(lastMousePos.x, lastMousePos.y, event.button.id, OcelotDesktop.player) + pressedButton = None + + if (inBounds) { + event.consume() + } + } + + case _ => + } + + case event: ScrollEvent if mouseCoordsInBounds && shouldHandleInput && screenNode.screen.tier > Tier.One => + screenNode.screen.mouseScroll(lastMousePos.x, lastMousePos.y, event.offset, OcelotDesktop.player) + event.consume() + } + + private val clipboardSplitter = new ClipboardSplitter + + private def sendClipboard(): Unit = { + clipboardSplitter.split(UiHandler.clipboard) match { + case Some(value) => + for (part <- value) { + screenNode.screen.clipboard(part, OcelotDesktop.player) + } + + case None => + SoundSource.fromBuffer(SoundBuffers.NoteBlock("harp"), SoundCategory.Records).play() + } + } + + override def save(nbt: NBTTagCompound): Unit = { + nbt.setFloat(ScaleTag, scale.value) + super.save(nbt) + } + + override def load(nbt: NBTTagCompound): Unit = { + super.load(nbt) + scale.nextValue = nbt.getFloat(ScaleTag) + } + + private val screenSize = Register.sampling(Size2D(screenWidth, screenHeight)) + + override def update(): Unit = { + super.update() + + // NOTE: the single bar is intentional! both operands have to be evaluated. + if (scale.update() | screenSize.update()) { + recalculateBoundsAndRelayout() + } + + val mousePos = toBufferCoords(UiHandler.mousePosition) + + if (bufferCoordsInBounds(mousePos) && mousePos != lastMousePos) { + lastMousePos = mousePos + + if (isFocused && screenNode.screen.tier > Tier.One) { + for (button <- pressedButton) { + screenNode.screen.mouseDrag(lastMousePos.x, lastMousePos.y, button.id, OcelotDesktop.player) + } + } + } + } + + override def draw(g: Graphics): Unit = { + // no synchronization here (see the note in ScreenNode): the methods to turn the screen on/off are indirect. + if (screenNode.screen.getPowerState) { + screenNode.drawScreenData( + g, + position.x, + position.y, + scaleX, + scaleY, + if (Settings.get.screenWindowMipmap) { + MinFilteringMode.LinearMipmapLinear + } else { + MinFilteringMode.Nearest + }, + ) + } else { + g.rect(bounds, ColorScheme("ScreenOff")) + } + } +} + +object ScreenView { + private val ScaleTag: String = "scale" +} diff --git a/src/main/scala/ocelot/desktop/ui/widget/ScrollView.scala b/src/main/scala/ocelot/desktop/ui/widget/ScrollView.scala index 9b03fd9..3fe45e1 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/ScrollView.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/ScrollView.scala @@ -1,16 +1,25 @@ package ocelot.desktop.ui.widget import ocelot.desktop.ColorScheme +import ocelot.desktop.geometry.FloatUtils.ExtendedFloat import ocelot.desktop.geometry.{Rect2D, Size2D, Vector2D} import ocelot.desktop.graphics.Graphics import ocelot.desktop.ui.UiHandler import ocelot.desktop.ui.event.handlers.HoverHandler import ocelot.desktop.ui.event.sources.KeyEvents -import ocelot.desktop.ui.event.{MouseEvent, ScrollEvent} +import ocelot.desktop.ui.event.{Capturing, MouseEvent, ScrollEvent} +import ocelot.desktop.ui.layout.Layout +import ocelot.desktop.ui.widget.ScrollView.{DecayFactor, DragState, MaxScrollVelocity, MinThumbSize, ScrollVelocity} import ocelot.desktop.util.Logging -import org.lwjgl.input.Keyboard class ScrollView(val inner: Widget) extends Widget with Logging with HoverHandler { + override protected val layout: Layout = new Layout(this) { + override def relayout(): Unit = { + inner.rawSetPosition(position - Vector2D(xOffset, yOffset)) + inner.relayout() + } + } + inner.size = Size2D.Zero children +:= inner @@ -18,15 +27,12 @@ class ScrollView(val inner: Widget) extends Widget with Logging with HoverHandle private var xOffset = 0f private var yOffset = 0f - private var xAcceleration = 0f - private var yAcceleration = 0f - private var dragging = 0 - private var mouseOldPos = Vector2D(0, 0) + private var xVelocity = 0f + private var yVelocity = 0f + private var dragState: DragState = DragState.None private var vAnim = 0f - private var vAnimDir = -1 private var hAnim = 0f - private var hAnimDir = -1 def offset: Vector2D = Vector2D(xOffset, yOffset) @@ -35,100 +41,125 @@ class ScrollView(val inner: Widget) extends Widget with Logging with HoverHandle override def receiveMouseEvents = true eventHandlers += { - case ScrollEvent(offset) => - if (bounds.contains(UiHandler.mousePosition)) { - if (KeyEvents.isShiftDown) { - xAcceleration -= offset * 385f - } else { - yAcceleration -= offset * 385f - scrollToEnd = false - } - } - - case event: MouseEvent if event.state == MouseEvent.State.Press => - val pos = UiHandler.mousePosition - mouseOldPos = pos - - dragging = { - if (vThumbBounds.contains(pos)) 1 - else if (hThumbBounds.contains(pos)) 2 - else 0 - } - - if (dragging == 1) { + case ScrollEvent(offset) if bounds.contains(UiHandler.mousePosition) => + if (KeyEvents.isShiftDown) { + xVelocity -= offset * ScrollVelocity + } else { + yVelocity -= offset * ScrollVelocity scrollToEnd = false } - case event: MouseEvent if event.state == MouseEvent.State.Release => - dragging = 0 + case Capturing(event @ MouseEvent(MouseEvent.State.Pressed, MouseEvent.Button.Left)) => + val pos = UiHandler.mousePosition + + if (vThumbBounds.contains(pos) || hThumbBounds.contains(pos)) { + event.consume() + } + + dragState = if (vThumbBounds.contains(pos)) { + scrollToEnd = false + DragState.Vertical(yOffset, pos) + } else if (hThumbBounds.contains(pos)) { + DragState.Horizontal(xOffset, pos) + } else { + DragState.None + } + + case MouseEvent(MouseEvent.State.Released, MouseEvent.Button.Left) => + dragState = DragState.None } override def minimumSize: Size2D = Size2D.Zero override def size_=(value: Size2D): Unit = { super.size_=(value) - clampOffsets() + clampOffsets(resetVelocities = true) } override def shouldClip = true override def draw(g: Graphics): Unit = { - if (xOffset.abs > 0 || yOffset.abs > 0) { - inner.position = position - Vector2D(xOffset, yOffset) - } - inner.draw(g) - if (vThumbVisible) + if (vThumbVisible) { drawVThumb(g) + } - if (hThumbVisible) + if (hThumbVisible) { drawHThumb(g) + } + } + + private def nextOffsetVelocity(x: Float, v: Float, dt: Float): (Float, Float) = { + var x_ = x + var v_ = v + v_ = v_.clamped(-MaxScrollVelocity, MaxScrollVelocity) + x_ += v_ * dt + // exponential decay. + v_ *= math.exp(-DecayFactor * UiHandler.dt).toFloat + + (x_, v_) } override def update(): Unit = { super.update() recalculateBounds() - xAcceleration *= 1f - 5f * UiHandler.dt - xAcceleration = xAcceleration.min(1540f).max(-1540f) - xOffset += xAcceleration * UiHandler.dt + val prevXOffset = xOffset + val prevYOffset = yOffset + + // in plain text, the logic is: + // - if we're not holding down a thumb, apply the accumulated scroll velocity + // - otherwise reset the velocity to zero and set the offset depending on where the cursor is + + if (dragState.isHorizontal) { + xVelocity = 0 + } else { + val next = nextOffsetVelocity(xOffset, xVelocity, UiHandler.dt) + xOffset = next._1 + xVelocity = next._2 + } if (scrollToEnd) { yOffset = maxYOffset - yAcceleration = 0 + yVelocity = 0 - if (dragging == 1) { - dragging = 0 + if (dragState.isVertical) { + dragState = DragState.None } + } else if (dragState.isVertical) { + yVelocity = 0 } else { - yAcceleration *= 1f - 5f * UiHandler.dt - yAcceleration = yAcceleration.min(1540f).max(-1540f) - yOffset += yAcceleration * UiHandler.dt + val next = nextOffsetVelocity(yOffset, yVelocity, UiHandler.dt) + yOffset = next._1 + yVelocity = next._2 } - clampOffsets() + clampOffsets(resetVelocities = true) val mousePos = UiHandler.mousePosition - vAnimDir = if (isHovered && vThumbHoverArea.contains(mousePos) || dragging == 1) 1 else -1 - hAnimDir = if (isHovered && hThumbHoverArea.contains(mousePos) || dragging == 2) 1 else -1 + val vAnimDir = if (isHovered && vThumbHoverArea.contains(mousePos) || dragState.isVertical) 1 else -1 + val hAnimDir = if (isHovered && hThumbHoverArea.contains(mousePos) || dragState.isHorizontal) 1 else -1 - vAnim = math.max(0f, math.min(1f, vAnim + UiHandler.dt / 0.2f * vAnimDir)) - hAnim = math.max(0f, math.min(1f, hAnim + UiHandler.dt / 0.2f * hAnimDir)) + vAnim = (vAnim + UiHandler.dt / 0.2f * vAnimDir).clamped() + hAnim = (hAnim + UiHandler.dt / 0.2f * hAnimDir).clamped() - if (dragging == 0) return + dragState match { + case DragState.Vertical(startOffset, startPos) => + yOffset = startOffset + (mousePos - startPos).y / vThumbCoeff + clampOffsets(resetVelocities = false) - if (dragging == 1) { - val dy = (mousePos - mouseOldPos).y - yOffset += dy / vThumbCoeff - } else if (dragging == 2) { - val dx = (mousePos - mouseOldPos).x - xOffset += dx / hThumbCoeff + case DragState.Horizontal(startOffset, startPos) => + xOffset = startOffset + (mousePos - startPos).x / hThumbCoeff + clampOffsets(resetVelocities = false) + + case DragState.None => } - clampOffsets() - mouseOldPos = mousePos + if (prevXOffset != xOffset || prevYOffset != yOffset) { + relayout() + } } private def drawVThumb(g: Graphics): Unit = { @@ -147,20 +178,37 @@ class ScrollView(val inner: Widget) extends Widget with Logging with HoverHandle private def maxYOffset: Float = inner.size.height - size.height - private def clampOffsets(): Unit = { - xOffset = xOffset.min(maxXOffset).max(0f) - yOffset = yOffset.min(maxYOffset).max(0f) + private def clampOffsets(resetVelocities: Boolean): Unit = { + val newXOffset = xOffset.clamped(max = maxXOffset) + val newYOffset = yOffset.clamped(max = maxYOffset) + + if (resetVelocities) { + if (xOffset != newXOffset) { + xVelocity = 0 + } + + if (yOffset != newYOffset) { + yVelocity = 0 + } + } + + xOffset = newXOffset + yOffset = newYOffset } private def vThumbHoverArea: Rect2D = { Rect2D(position.x + size.width - 12, position.y, 12, size.height) } + private def computeThumbSize(preferredSize: Float, scrollBarSize: Float): Float = { + preferredSize.max(MinThumbSize).min(scrollBarSize) + } + private def vThumbBounds: Rect2D = { val x = position.x + size.width - 12 val y = position.y + yOffset * vThumbCoeff - Rect2D(x, y + 2, 12, vThumbCoeff * size.height) + Rect2D(x, y + 2, 12, computeThumbSize(vThumbCoeff * size.height, vScrollBarSize)) } private def hThumbHoverArea: Rect2D = { @@ -171,14 +219,45 @@ class ScrollView(val inner: Widget) extends Widget with Logging with HoverHandle val x = position.x + xOffset * hThumbCoeff val y = position.y + size.height - 12 - Rect2D(x + 2, y, hThumbCoeff * size.width, 12) + Rect2D(x + 2, y, computeThumbSize(hThumbCoeff * size.width, hScrollBarSize), 12) } protected def vThumbVisible: Boolean = inner.size.height > size.height protected def hThumbVisible: Boolean = inner.size.width > size.width - private def vThumbCoeff: Float = (size.height - 4) / inner.size.height + private def vScrollBarSize: Float = size.height - 4 - private def hThumbCoeff: Float = (size.width - 14) / inner.size.width + private def hScrollBarSize: Float = size.width - 14 + + private def vThumbCoeff: Float = vScrollBarSize / inner.size.height + + private def hThumbCoeff: Float = hScrollBarSize / inner.size.width +} + +object ScrollView { + private val ScrollVelocity = 385f + private val MaxScrollVelocity = ScrollVelocity * 4 + private val DecayFactor = 5f + private val MinThumbSize = 15f + + /** + * Which of the thumbs are being dragged. + */ + sealed trait DragState { + def isVertical: Boolean = false + def isHorizontal: Boolean = false + } + + object DragState { + case class Vertical(startOffset: Float, startPos: Vector2D) extends DragState { + override def isVertical: Boolean = true + } + + case class Horizontal(startOffset: Float, startPos: Vector2D) extends DragState { + override def isHorizontal: Boolean = true + } + + case object None extends DragState + } } diff --git a/src/main/scala/ocelot/desktop/ui/widget/Slider.scala b/src/main/scala/ocelot/desktop/ui/widget/Slider.scala index 8e30473..ac6062d 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/Slider.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/Slider.scala @@ -29,12 +29,12 @@ class Slider(var value: Float, val text: String, val snapPoints: Int = 0) private val soundInterval = 3.0f private def calculateValue(x: Float): Unit = { - value = ((x - bounds.x - handleWidth / 2f) / (bounds.w - handleWidth)).clamp(0f, 1f) + value = ((x - bounds.x - handleWidth / 2f) / (bounds.w - handleWidth)).clamped(0f, 1f) onValueChanged(value) } eventHandlers += { - case MouseEvent(MouseEvent.State.Press, MouseEvent.Button.Left) => + case MouseEvent(MouseEvent.State.Pressed, MouseEvent.Button.Left) => clickSoundSource.press.play() case ClickEvent(MouseEvent.Button.Left, pos) => diff --git a/src/main/scala/ocelot/desktop/ui/widget/TextInput.scala b/src/main/scala/ocelot/desktop/ui/widget/TextInput.scala index 7153bd0..5b0bc9a 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/TextInput.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/TextInput.scala @@ -58,7 +58,7 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w private var prevEnabled = enabled eventHandlers += { - case MouseEvent(MouseEvent.State.Release, MouseEvent.Button.Left) => + case MouseEvent(MouseEvent.State.Released, MouseEvent.Button.Left) => if (isFocused && !clippedBounds.contains(UiHandler.mousePosition)) { unfocus() } diff --git a/src/main/scala/ocelot/desktop/ui/widget/TunnelDialog.scala b/src/main/scala/ocelot/desktop/ui/widget/TunnelDialog.scala index e044fc4..6fe6299 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/TunnelDialog.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/TunnelDialog.scala @@ -3,6 +3,7 @@ package ocelot.desktop.ui.widget import ocelot.desktop.ColorScheme import ocelot.desktop.audio.{ClickSoundSource, SoundSource} import ocelot.desktop.geometry.{Padding2D, Size2D} +import ocelot.desktop.graphics.IconSource import ocelot.desktop.ui.UiHandler import ocelot.desktop.ui.layout.LinearLayout import ocelot.desktop.ui.widget.modal.ModalDialog @@ -37,27 +38,27 @@ class TunnelDialog( children :+= input children :+= new PaddingBox( new IconButton( - "icons/ButtonRandomize", - "icons/ButtonRandomize", + IconSource.Icons.ButtonRandomize, + IconSource.Icons.ButtonRandomize, drawBackground = true, releasedColor = ColorScheme("ButtonForeground"), padding = 3.5f, tooltip = Some("Generate random channel name"), ) { - override def onPressed(): Unit = input.text = UUID.randomUUID().toString + override def onClicked(): Unit = input.text = UUID.randomUUID().toString }, new Padding2D(left = 8, right = 8), ) children :+= new IconButton( - "icons/ButtonClipboard", - "icons/ButtonCheck", + IconSource.Icons.ButtonClipboard, + IconSource.Icons.ButtonCheck, drawBackground = true, releasedColor = ColorScheme("ButtonForeground"), pressedColor = ColorScheme("ButtonConfirm"), padding = 3.5f, tooltip = Some("Copy channel name to the clipboard"), ) { - override def onPressed(): Unit = UiHandler.clipboard = input.text + override def onClicked(): Unit = UiHandler.clipboard = input.text } } diff --git a/src/main/scala/ocelot/desktop/ui/widget/Viewport3DWidget.scala b/src/main/scala/ocelot/desktop/ui/widget/Viewport3DWidget.scala index 58030b1..9c7c586 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/Viewport3DWidget.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/Viewport3DWidget.scala @@ -4,7 +4,7 @@ import ocelot.desktop.color.Color import ocelot.desktop.geometry.FloatUtils.ExtendedFloat import ocelot.desktop.geometry._ import ocelot.desktop.graphics.scene.{Camera3D, Scene3D} -import ocelot.desktop.graphics.{Graphics, Viewport3D} +import ocelot.desktop.graphics.{Graphics, IconSource, Viewport3D} import ocelot.desktop.ui.UiHandler import ocelot.desktop.ui.event.handlers.{HoverHandler, MouseHandler} import ocelot.desktop.ui.event.sources.KeyEvents @@ -92,13 +92,13 @@ abstract class Viewport3DWidget extends Widget with MouseHandler with HoverHandl protected def setupToolbar(toolbar: Widget): Unit = { toolbar.children :+= new IconButton( - "icons/Home", - "icons/Home", + IconSource.Icons.Home, + IconSource.Icons.Home, pressedColor = Color.White.withAlpha(0.5f), releasedColor = Color.White.withAlpha(0.2f), darkenActiveColorFactor = 0.5f, ) { - override def onPressed(): Unit = { + override def onClicked(): Unit = { cameraFinalTarget = HomeTarget cameraFinalRotation = HomeRotation cameraFinalDistance = HomeDistance @@ -113,7 +113,7 @@ abstract class Viewport3DWidget extends Widget with MouseHandler with HoverHandl eventHandlers += { case ScrollEvent(offset) => cameraFinalDistance = (if (offset > 0) cameraFinalDistance / ScrollSpeed else cameraFinalDistance * ScrollSpeed) - .clamp(MinDistance, MaxDistance) + .clamped(MinDistance, MaxDistance) case event @ DragEvent(DragEvent.State.Drag, MouseEvent.Button.Left, _) if KeyEvents.isShiftDown => val basis = cameraFinalTransform.basis @@ -150,8 +150,8 @@ abstract class Viewport3DWidget extends Widget with MouseHandler with HoverHandl scene = createScene() if (isHovered) { - root.get.statusBar.addMouseEntry("icons/LMB", "Rotate view") - root.get.statusBar.addKeyMouseEntry("icons/LMB", "Shift", "Pan view") + root.get.statusBar.addMouseEntry(IconSource.Icons.LMB, "Rotate view") + root.get.statusBar.addKeyMouseEntry(IconSource.Icons.LMB, "SHIFT", "Pan view") } } diff --git a/src/main/scala/ocelot/desktop/ui/widget/Widget.scala b/src/main/scala/ocelot/desktop/ui/widget/Widget.scala index 5adc139..d26ca2e 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/Widget.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/Widget.scala @@ -59,6 +59,10 @@ class Widget extends EventAware with Updatable with Disposable { final def root: Option[RootWidget] = _root + final def ancestors: Iterator[Widget] = { + Iterator.unfold(this)(_.parent.map(w => (w, w))) + } + def enabled: Boolean = true def minimumSize: Size2D = layout.minimumSize diff --git a/src/main/scala/ocelot/desktop/ui/widget/WorkspaceView.scala b/src/main/scala/ocelot/desktop/ui/widget/WorkspaceView.scala index e040ae7..965ca88 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/WorkspaceView.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/WorkspaceView.scala @@ -6,20 +6,24 @@ import ocelot.desktop.graphics.{Graphics, IconSource} import ocelot.desktop.node.Node.Size import ocelot.desktop.node.nodes.{ComputerNode, ScreenNode} import ocelot.desktop.node.{EntityNode, Node, NodePort} +import ocelot.desktop.ui.UiHandler import ocelot.desktop.ui.event._ import ocelot.desktop.ui.event.handlers.{HoverHandler, MouseHandler} -import ocelot.desktop.ui.event.sources.KeyEvents +import ocelot.desktop.ui.event.sources.{BrainEvents, KeyEvents} import ocelot.desktop.ui.layout.{CopyLayout, Layout} +import ocelot.desktop.ui.particle.ParticleSystem import ocelot.desktop.ui.widget.WorkspaceView.NodeLoadException import ocelot.desktop.ui.widget.window.{NodeSelector, ProfilerWindow, WindowPool} +import ocelot.desktop.util.Keybind.{Center, Profiler} +import ocelot.desktop.util.Keymap.Press import ocelot.desktop.util.ReflectionUtils.findUnaryConstructor import ocelot.desktop.util.animation.ValueAnimation import ocelot.desktop.util.{DrawUtils, Logging, Persistable} -import ocelot.desktop.{ColorScheme, OcelotDesktop} -import org.lwjgl.input.Keyboard +import ocelot.desktop.{ColorScheme, OcelotDesktop, Settings} +import totoro.ocelot.brain import totoro.ocelot.brain.entity.traits.{Environment, SidedEnvironment} import totoro.ocelot.brain.entity.{Case, Screen} -import totoro.ocelot.brain.event.{EventBus, InventoryEvent, NodeEvent} +import totoro.ocelot.brain.event.{InventoryEvent, NodeEvent} import totoro.ocelot.brain.nbt.ExtendedNBT._ import totoro.ocelot.brain.nbt.{NBT, NBTBase, NBTTagCompound} import totoro.ocelot.brain.util.{Direction, Tier} @@ -28,10 +32,12 @@ import scala.collection.immutable.ArraySeq import scala.collection.{immutable, mutable} import scala.jdk.CollectionConverters._ + class WorkspaceView extends Widget with Persistable with MouseHandler with HoverHandler with Logging { @volatile var nodes: immutable.Seq[Node] = immutable.ArraySeq[Node]() + var particleSystem = new ParticleSystem var windowPool = new WindowPool var nodeSelector = new NodeSelector var profilerWindow = new ProfilerWindow @@ -50,7 +56,7 @@ class WorkspaceView extends Widget with Persistable with MouseHandler with Hover override def hierarchy: ArraySeq[Widget] = ArraySeq.from(nodes) } - private val eventSubscription = EventBus.subscribe { + def dispatchBrainEvent(event: brain.event.Event): Unit = event match { case event: NodeEvent => nodes .filter(_.shouldReceiveEventsFor(event.address)) @@ -63,6 +69,7 @@ class WorkspaceView extends Widget with Persistable with MouseHandler with Hover def reset(): Unit = { nodes.foreach(_.dispose()) nodes = nodes.empty + particleSystem.clear() windowPool.deleteAllWindows() nodeSelector = new NodeSelector profilerWindow = new ProfilerWindow @@ -73,7 +80,6 @@ class WorkspaceView extends Widget with Persistable with MouseHandler with Hover super.dispose() nodes.foreach(_.dispose()) - eventSubscription.cancel() } @throws[NodeLoadException] @@ -262,10 +268,10 @@ class WorkspaceView extends Widget with Persistable with MouseHandler with Hover nodeSelector.close() } - case KeyEvent(KeyEvent.State.Press, Keyboard.KEY_F4, _) => + case Press(Profiler) => profilerWindow.open() - case KeyEvent(KeyEvent.State.Press, Keyboard.KEY_HOME, _) => + case Press(Center) => moveCameraOffset(-cameraOffset) } @@ -456,12 +462,11 @@ class WorkspaceView extends Widget with Persistable with MouseHandler with Hover } } - private def newConnectionTarget: Option[(Node, NodePort)] = { - val (node, _, endpoint) = newConnection.get + private def newConnectionTarget: Option[(Node, NodePort)] = newConnection flatMap { case (node, _, endpoint) => val validTargets = { nodes.iterator .filter(_ != node) - .filter(_.bounds.inflate(20).contains(endpoint)) + .filter(_.bounds.inflated(20).contains(endpoint)) .filter(_.ports.nonEmpty) } @@ -487,9 +492,41 @@ class WorkspaceView extends Widget with Persistable with MouseHandler with Hover } } - private def drawSelectorConnection(g: Graphics, aRect: Rect2D, bRect: Rect2D, - thickness: Float = 4, - color: Color = RGBAColor(150, 150, 150)): Unit = { + private def drawNewConnectionHints(g: Graphics): Unit = { + val (node, port, _) = newConnection.get + + g.setSmallFont() + + for ((node, port) <- Some((node, port)) ++ newConnectionTarget) { + val iconBoundsInflate = -(node.bounds.w - 22) / 2 + g.rect(node.bounds.inflated(-10), RGBAColor(0, 0, 0, 150)) + g.sprite(port.getIcon, node.bounds.inflated(iconBoundsInflate), port.getColor) + drawCenteredConnectionHintLabel( + g, + node.bounds, + port.direction.map(node.directionLabel(_)).getOrElse("Any"), + port.getColor + ) + } + + g.setNormalFont() + } + + private def drawCenteredConnectionHintLabel(g: Graphics, bounds: Rect2D, text: String, color: Color): Unit = { + // the area should be reasonably big to fit icon + text + if (bounds.w >= 48) { + val x = bounds.x + bounds.w / 2.0f - (text.length * 7.0f) / 2.0f + val y = bounds.y + bounds.h - 18 + g.foreground = color + g.text(x, y, text, shrink = 1) + } + } + + private def drawSelectorConnection( + g: Graphics, aRect: Rect2D, bRect: Rect2D, + thickness: Float = 4, + color: Color = RGBAColor(150, 150, 150) + ): Unit = { if (aRect.collides(bRect)) return val (a, b) = if (aRect.x > bRect.x) (aRect, bRect) else (bRect, aRect) @@ -537,7 +574,7 @@ class WorkspaceView extends Widget with Persistable with MouseHandler with Hover for (x <- 0 to numRepeatsX) { for (y <- 0 to numRepeatsY) { - g.sprite("BackgroundPattern", x.toFloat * 304, y.toFloat * 304, 304, 304) + g.sprite(IconSource.BackgroundPattern, x.toFloat * 304, y.toFloat * 304, 304, 304) } } @@ -574,7 +611,6 @@ class WorkspaceView extends Widget with Persistable with MouseHandler with Hover nodes.foreach(_.draw(g)) nodes.foreach(_.drawLight(g)) nodes.foreach(_.drawLabel(g)) - nodes.foreach(_.drawParticles(g)) portsAlpha.update() portsAlpha.goto(if (newConnection.isDefined) 1 else 0) @@ -596,19 +632,27 @@ class WorkspaceView extends Widget with Persistable with MouseHandler with Hover ) } + if (newConnection.isDefined) { + drawNewConnectionHints(g) + } + if (!nodeSelector.isClosed) drawSelectorConnection(g, Rect2D(newNodePos.x + cameraOffset.x, newNodePos.y + cameraOffset.y, 64, 64), nodeSelector.bounds, color = color) + particleSystem.draw(g) + drawChildren(g) } override def update(): Unit = { super.update() + nodes.foreach(_.update()) + particleSystem.update(UiHandler.dt) if (isHovered) { - root.get.statusBar.addKeyEntry("HOME", "Reset camera") + root.get.statusBar.addKeyEntry(Settings.get.keymap.name(Center), "Reset camera") } if (isHovered || nodes.exists(_.isHovered)) { @@ -616,11 +660,11 @@ class WorkspaceView extends Widget with Persistable with MouseHandler with Hover } if (isHovered && newConnection.isEmpty) { - if (nodeSelector.isClosed) - root.get.statusBar.addMouseEntry("icons/LMB", "Add node") - else - root.get.statusBar.addMouseEntry("icons/LMB", "Close selector") - root.get.statusBar.addMouseEntry("icons/DragLMB", "Move camera") + root.get.statusBar.addMouseEntry( + IconSource.Icons.LMB, + if (nodeSelector.isClosed) "Add node" else "Close selector", + ) + root.get.statusBar.addMouseEntry(IconSource.Icons.DragLMB, "Move camera") } } } diff --git a/src/main/scala/ocelot/desktop/ui/widget/card/SoundCardWindow.scala b/src/main/scala/ocelot/desktop/ui/widget/card/SoundCardWindow.scala index 956b2cf..6b14431 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/card/SoundCardWindow.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/card/SoundCardWindow.scala @@ -3,7 +3,7 @@ package ocelot.desktop.ui.widget.card import ocelot.desktop.ColorScheme import ocelot.desktop.color.Color import ocelot.desktop.geometry.{Padding2D, Rect2D, Size2D, Vector2D} -import ocelot.desktop.graphics.Graphics +import ocelot.desktop.graphics.{Graphics, IconSource} import ocelot.desktop.ui.layout.{Layout, LinearLayout} import ocelot.desktop.ui.widget.card.SoundCardWindow._ import ocelot.desktop.ui.widget.window.{PanelWindow, Window} @@ -167,9 +167,9 @@ class SoundCardWindow(card: SoundCard) extends PanelWindow { g.line(b, Vector2D(mx, b.y), 2, col) if (b.x > px) { - g.sprite("icons/WireArrowRight", b.x - 4, b.y - 4, col) + g.sprite(IconSource.Icons.WireArrowRight, b.x - 4, b.y - 4, col) } else { - g.sprite("icons/WireArrowLeft", b.x, b.y - 4, col) + g.sprite(IconSource.Icons.WireArrowLeft, b.x, b.y - 4, col) } existingConnectors += Connector(column, colorIdx, from, to) @@ -275,12 +275,12 @@ object SoundCardWindow { } private val waves = Array( - ("icons/WaveSine", SignalGenerator.Sine.getClass), - ("icons/WaveTriangle", SignalGenerator.Triangle.getClass), - ("icons/WaveSawtooth", SignalGenerator.Sawtooth.getClass), - ("icons/WaveSquare", SignalGenerator.Square.getClass), - ("icons/WaveNoise", classOf[SignalGenerator.Noise]), - ("icons/WaveLFSR", classOf[SignalGenerator.LFSR]), + (IconSource.Icons.WaveSine, SignalGenerator.Sine.getClass), + (IconSource.Icons.WaveTriangle, SignalGenerator.Triangle.getClass), + (IconSource.Icons.WaveSawtooth, SignalGenerator.Sawtooth.getClass), + (IconSource.Icons.WaveSquare, SignalGenerator.Square.getClass), + (IconSource.Icons.WaveNoise, classOf[SignalGenerator.Noise]), + (IconSource.Icons.WaveLFSR, classOf[SignalGenerator.LFSR]), ) private def drawEnvelope(g: Graphics, env: ADSREnvelope, bounds: Rect2D, elapsedMs: Float): Unit = { @@ -445,21 +445,21 @@ object SoundCardWindow { val sh = (if (isFirst) 10 else 0) + (if (isLast) 50 else 0) h -= sh - g.sprite("light-panel/BorderL", x, y, 4, h) - g.sprite("light-panel/BorderR", x + w - 4, y, 4, h) - g.sprite("light-panel/Fill", x + 4, y, w - 8, h) + g.sprite(IconSource.LightPanel.BorderL, x, y, 4, h) + g.sprite(IconSource.LightPanel.BorderR, x + w - 4, y, 4, h) + g.sprite(IconSource.LightPanel.Fill, x + 4, y, w - 8, h) if (isFirst) { - g.sprite("light-panel/CornerTL", x, y - 4, 4, 4) - g.sprite("light-panel/CornerTR", x + w - 4, y - 4, 4, 4) - g.sprite("light-panel/BorderT", x + 4, y - 4, w - 8, 4) + g.sprite(IconSource.LightPanel.CornerTL, x, y - 4, 4, 4) + g.sprite(IconSource.LightPanel.CornerTR, x + w - 4, y - 4, 4, 4) + g.sprite(IconSource.LightPanel.BorderT, x + 4, y - 4, w - 8, 4) } if (isLast) { - g.sprite("light-panel/CornerBL", x, y + h, 4, 4) - g.sprite("light-panel/CornerBR", x + w - 4, y + h, 4, 4) - g.sprite("light-panel/BorderB", x + 4, y + h, w - 8, 4) - g.sprite("light-panel/Vent", x, y + h + 8, w, 38) + g.sprite(IconSource.LightPanel.CornerBL, x, y + h, 4, 4) + g.sprite(IconSource.LightPanel.CornerBR, x + w - 4, y + h, 4, 4) + g.sprite(IconSource.LightPanel.BorderB, x + 4, y + h, w - 8, 4) + g.sprite(IconSource.LightPanel.Vent, x, y + h + 8, w, 38) } for (i <- 0 until 6) { @@ -469,13 +469,13 @@ object SoundCardWindow { y -= sy h += sh - g.sprite("light-panel/BookmarkLeft", x, y + 16, 18, 14) - g.sprite("light-panel/BookmarkLeft", x, y + 32, 18, 14) - g.sprite("light-panel/BookmarkLeft", x, y + 48, 18, 14) + g.sprite(IconSource.LightPanel.BookmarkLeft, x, y + 16, 18, 14) + g.sprite(IconSource.LightPanel.BookmarkLeft, x, y + 32, 18, 14) + g.sprite(IconSource.LightPanel.BookmarkLeft, x, y + 48, 18, 14) - g.sprite("light-panel/BookmarkRight", x + w - 20, y + 16, 20, 14) - g.sprite("light-panel/BookmarkRight", x + w - 20, y + 32, 20, 14) - g.sprite("light-panel/BookmarkRight", x + w - 20, y + 48, 20, 14) + g.sprite(IconSource.LightPanel.BookmarkRight, x + w - 20, y + 16, 20, 14) + g.sprite(IconSource.LightPanel.BookmarkRight, x + w - 20, y + 32, 20, 14) + g.sprite(IconSource.LightPanel.BookmarkRight, x + w - 20, y + 48, 20, 14) g.setSmallFont() g.background = Color.Transparent diff --git a/src/main/scala/ocelot/desktop/ui/widget/contextmenu/ContextMenu.scala b/src/main/scala/ocelot/desktop/ui/widget/contextmenu/ContextMenu.scala index 0157248..884b73e 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/contextmenu/ContextMenu.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/contextmenu/ContextMenu.scala @@ -1,10 +1,10 @@ package ocelot.desktop.ui.widget.contextmenu import ocelot.desktop.ColorScheme -import ocelot.desktop.geometry.Padding2D +import ocelot.desktop.geometry.{Padding2D, Size2D} import ocelot.desktop.graphics.Graphics -import ocelot.desktop.ui.layout.LinearLayout -import ocelot.desktop.ui.widget.{PaddingBox, Widget} +import ocelot.desktop.ui.layout.{CopyLayout, Layout, LinearLayout} +import ocelot.desktop.ui.widget.{PaddingBox, ScrollView, Widget} import ocelot.desktop.util.animation.ValueAnimation import ocelot.desktop.util.animation.easing.EaseInOutQuad import ocelot.desktop.util.{DrawUtils, Orientation} @@ -12,6 +12,8 @@ import ocelot.desktop.util.{DrawUtils, Orientation} import scala.collection.immutable.ArraySeq class ContextMenu extends Widget { + override protected val layout: Layout = new CopyLayout(this) + private[contextmenu] var isClosing = false private[contextmenu] var isOpening = false @@ -22,7 +24,11 @@ class ContextMenu extends Widget { override protected val layout = new LinearLayout(this, orientation = Orientation.Vertical) } - children :+= new PaddingBox(inner, Padding2D(top = 4, bottom = 4)) + private val scrollViewContents = new PaddingBox(inner, Padding2D(top = 4, bottom = 4)) + + children :+= new ScrollView(scrollViewContents) + + def preferredSize: Size2D = scrollViewContents.minimumSize private[contextmenu] def contextMenus: ContextMenus = _contextMenus diff --git a/src/main/scala/ocelot/desktop/ui/widget/contextmenu/ContextMenuEntry.scala b/src/main/scala/ocelot/desktop/ui/widget/contextmenu/ContextMenuEntry.scala index 459c324..a371ac3 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/contextmenu/ContextMenuEntry.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/contextmenu/ContextMenuEntry.scala @@ -69,7 +69,7 @@ class ContextMenuEntry( override protected def receiveClickEvents: Boolean = true eventHandlers += { - case MouseEvent(MouseEvent.State.Press, MouseEvent.Button.Left) if !contextMenu.isOpening => + case MouseEvent(MouseEvent.State.Pressed, MouseEvent.Button.Left) if !contextMenu.isOpening => clickSoundSource.press.play() case ClickEvent(MouseEvent.Button.Left, _) if !contextMenu.isOpening => clicked() case HoverEvent(HoverEvent.State.Enter) => enter() diff --git a/src/main/scala/ocelot/desktop/ui/widget/contextmenu/ContextMenuSubmenu.scala b/src/main/scala/ocelot/desktop/ui/widget/contextmenu/ContextMenuSubmenu.scala index 9f1e842..709b89a 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/contextmenu/ContextMenuSubmenu.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/contextmenu/ContextMenuSubmenu.scala @@ -22,7 +22,7 @@ class ContextMenuSubmenu( override def update(): Unit = { super.update() if ( - !isClosing && !bounds.inflate(4).contains(UiHandler.mousePosition) + !isClosing && !bounds.inflated(4).contains(UiHandler.mousePosition) && !parentEntry.isHovered ) close() } diff --git a/src/main/scala/ocelot/desktop/ui/widget/contextmenu/ContextMenus.scala b/src/main/scala/ocelot/desktop/ui/widget/contextmenu/ContextMenus.scala index 88ead9d..2f20db2 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/contextmenu/ContextMenus.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/contextmenu/ContextMenus.scala @@ -1,5 +1,6 @@ package ocelot.desktop.ui.widget.contextmenu +import ocelot.desktop.geometry.FloatUtils.ExtendedFloat import ocelot.desktop.geometry.Vector2D import ocelot.desktop.graphics.Graphics import ocelot.desktop.ui.UiHandler @@ -27,7 +28,7 @@ class ContextMenus extends Widget { case KeyEvent(KeyEvent.State.Press, Keyboard.KEY_ESCAPE, _) => closeAll() - case MouseEvent(MouseEvent.State.Press, _) => + case MouseEvent(MouseEvent.State.Pressed, _) => if (!menus.map(_.bounds).exists(_.contains(UiHandler.mousePosition))) closeAll() } @@ -47,17 +48,28 @@ class ContextMenus extends Widget { if (!isSubmenu) for (child <- menus) close(child) menu.recalculateBounds() - menu.size = menu.minimumSize - val size = menu.size - var pos = openPos + def fit(openPos: Float, flipPos: Float, menuSize: Float, size: Float): Float = { + if (openPos + menuSize <= size) { + // fits in the positive direction. + openPos + } else if (flipPos >= menuSize) { + // fits in the negative direction. + flipPos - menuSize + } else { + // lay in the positive direction, moving back to fit, but not outside the edge. + (size - menuSize).max(0) + } + } - if (pos.x + size.width > width) - pos = pos.setX(xFlipPos - size.width) - if (pos.y + size.height > height) - pos = pos.setY(yFlipPos - size.height) + val size = menu.preferredSize + val pos = Vector2D( + fit(openPos.x, xFlipPos, size.width, width), + fit(openPos.y, yFlipPos, size.height, height), + ) menu.position = pos + menu.size = size.copy(height = size.height.clamped(max = height)) menu.contextMenus = this menu.open() diff --git a/src/main/scala/ocelot/desktop/ui/widget/help/AboutDialog.scala b/src/main/scala/ocelot/desktop/ui/widget/help/AboutDialog.scala index 3b4d364..53fae51 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/help/AboutDialog.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/help/AboutDialog.scala @@ -4,7 +4,7 @@ import buildinfo.BuildInfo import ocelot.desktop.ColorScheme import ocelot.desktop.color.Color import ocelot.desktop.geometry.{Padding2D, Size2D} -import ocelot.desktop.graphics.Graphics +import ocelot.desktop.graphics.{Graphics, IconSource} import ocelot.desktop.ui.layout.LinearLayout import ocelot.desktop.ui.widget.{Button, Filler, Label, PaddingBox, Widget} import ocelot.desktop.ui.widget.modal.ModalDialog @@ -25,7 +25,7 @@ class AboutDialog extends ModalDialog { override def minimumSize: Size2D = Spritesheet.spriteSize("Logo") + 40 override def draw(g: Graphics): Unit = { - g.sprite("Logo", bounds.x + 20, bounds.y + 80, ColorScheme("AboutLogo")) + g.sprite(IconSource.Logo, bounds.x + 20, bounds.y + 80, ColorScheme("AboutLogo")) drawChildren(g) } } diff --git a/src/main/scala/ocelot/desktop/ui/widget/modal/ModalDialogPool.scala b/src/main/scala/ocelot/desktop/ui/widget/modal/ModalDialogPool.scala index 74a07d3..bdad624 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/modal/ModalDialogPool.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/modal/ModalDialogPool.scala @@ -3,6 +3,7 @@ package ocelot.desktop.ui.widget.modal import ocelot.desktop.color.RGBAColorNorm import ocelot.desktop.geometry.Size2D import ocelot.desktop.graphics.Graphics +import ocelot.desktop.ui.UiHandler import ocelot.desktop.ui.event.ClickEvent import ocelot.desktop.ui.event.handlers.MouseHandler import ocelot.desktop.ui.layout.Layout @@ -30,6 +31,7 @@ class ModalDialogPool extends Widget with MouseHandler { def pushDialog(dialog: ModalDialog): Unit = this.synchronized { children :+= dialog + root.get.contextMenus.closeAll() dialog.open() } diff --git a/src/main/scala/ocelot/desktop/ui/widget/modal/notification/NotificationDialog.scala b/src/main/scala/ocelot/desktop/ui/widget/modal/notification/NotificationDialog.scala index 5736d83..705db90 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/modal/notification/NotificationDialog.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/modal/notification/NotificationDialog.scala @@ -31,7 +31,7 @@ class NotificationDialog(message: String, notificationType: NotificationType = N // Icon children :+= new PaddingBox( - new Icon(IconSource.Notification(notificationType), Size2D(22, 22)), + new Icon(IconSource.Icons.Notification(notificationType), Size2D(22, 22)), Padding2D.equal(10), ) diff --git a/src/main/scala/ocelot/desktop/ui/widget/settings/KeymapSettingsTab.scala b/src/main/scala/ocelot/desktop/ui/widget/settings/KeymapSettingsTab.scala new file mode 100644 index 0000000..cf91f72 --- /dev/null +++ b/src/main/scala/ocelot/desktop/ui/widget/settings/KeymapSettingsTab.scala @@ -0,0 +1,76 @@ +package ocelot.desktop.ui.widget.settings + +import ocelot.desktop.color.Color +import ocelot.desktop.{ColorScheme, Settings} +import ocelot.desktop.geometry.{Padding2D, Size2D} +import ocelot.desktop.graphics.IconSource +import ocelot.desktop.ui.event.KeyEvent +import ocelot.desktop.ui.layout.LinearLayout +import ocelot.desktop.ui.widget.{Button, Label, PaddingBox, Widget} +import ocelot.desktop.util.Keybind.Keybind +import ocelot.desktop.util.{Keybind, Orientation} +import org.lwjgl.input.Keyboard + + +class KeymapSettingsTab extends SettingsTab { + override val layout = new LinearLayout(this, orientation = Orientation.Vertical, gap = 8) + + override val icon: IconSource = IconSource.Icons.SettingsKeymap + override val label: String = "Keymap" + + private def section(title: String, keybinds: Seq[Keybind]): Widget = new PaddingBox(new Widget { + override val layout = new LinearLayout(this, orientation = Orientation.Vertical) + + override def maximumSize: Size2D = minimumSize.copy(width = 257) + + children :+= new PaddingBox(new Label(title, small = true) { + override def maximumSize: Size2D = minimumSize + override def color: Color = ColorScheme("LabelDisabled") + }, Padding2D(top = 4, bottom = 8)) + + keybinds.foreach(keybind => { + children :+= new PaddingBox(new Widget { + children :+= new PaddingBox(new Label(keybind.description), Padding2D(top = 4, right = 8)) + children :+= new Button { + private var listening: Boolean = false + + eventHandlers += { + case KeyEvent(KeyEvent.State.Release, code, _) if listening => + Settings.get.keymap.set(keybind, code) + listening = false + } + + override def text: String = if (listening) "..." else Keyboard.getKeyName(Settings.get.keymap(keybind)) + override def onClick(): Unit = { + listening = !listening + } + } + }, Padding2D(bottom = 4)) + }) + }, Padding2D(bottom = 8)) + + children :+= new PaddingBox(new Widget { + children :+= new Widget { + override val layout = new LinearLayout(this, orientation = Orientation.Vertical) + children :+= section("OpenComputers", Seq( + Keybind.Insert, + )) + children :+= section("Ocelot", Seq( + Keybind.UIDebug, + Keybind.FPSCounter, + Keybind.ReloadWorkspace, + Keybind.Profiler, + Keybind.Fullscreen, + )) + } + children :+= new Widget { + override def maximumSize: Size2D = Size2D(16, 8) + } + children :+= section("Workspace", Seq( + Keybind.Center, + Keybind.QuickSave, + Keybind.QuickLoad, + Keybind.Screenshot, + )) + }, Padding2D(bottom = 8)) +} diff --git a/src/main/scala/ocelot/desktop/ui/widget/settings/SettingsDialog.scala b/src/main/scala/ocelot/desktop/ui/widget/settings/SettingsDialog.scala index 7d71513..2e83b87 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/settings/SettingsDialog.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/settings/SettingsDialog.scala @@ -16,6 +16,7 @@ class SettingsDialog extends ModalDialog { private val tabs = Seq( new UISettingsTab, new SoundSettingsTab, + new KeymapSettingsTab, new SystemSettingsTab, ) diff --git a/src/main/scala/ocelot/desktop/ui/widget/settings/SoundSettingsTab.scala b/src/main/scala/ocelot/desktop/ui/widget/settings/SoundSettingsTab.scala index 798fc4e..e0f8f16 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/settings/SoundSettingsTab.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/settings/SoundSettingsTab.scala @@ -10,7 +10,7 @@ import ocelot.desktop.util.Orientation class SoundSettingsTab extends SettingsTab { override val layout = new LinearLayout(this, orientation = Orientation.Vertical, gap = 8) - override val icon: IconSource = IconSource.SettingsSound + override val icon: IconSource = IconSource.Icons.SettingsSound override val label: String = "Sound" private def addSlider(name: String, value: Float, valueSetter: Float => Unit): Unit = { diff --git a/src/main/scala/ocelot/desktop/ui/widget/settings/SystemSettingsTab.scala b/src/main/scala/ocelot/desktop/ui/widget/settings/SystemSettingsTab.scala index ae7941a..acdd3f1 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/settings/SystemSettingsTab.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/settings/SystemSettingsTab.scala @@ -17,7 +17,7 @@ import scala.util.{Failure, Success} class SystemSettingsTab extends SettingsTab with Logging { private val OpenComputersConfigResource = "/application.conf" - override val icon: IconSource = IconSource.SettingsSystem + override val icon: IconSource = IconSource.Icons.SettingsSystem override val label: String = "System" override def applySettings(): Unit = { @@ -43,15 +43,15 @@ class SystemSettingsTab extends SettingsTab with Logging { children :+= new PaddingBox(textInput, Padding2D(right = 8)) children :+= new IconButton( - "icons/Folder", - "icons/Folder", + IconSource.Icons.Folder, + IconSource.Icons.Folder, releasedColor = ColorScheme("ButtonForeground"), pressedColor = ColorScheme("ButtonForegroundPressed"), drawBackground = true, padding = 3.5f, tooltip = Some("Search for OpenComputers configuration file"), ) { - override def onPressed(): Unit = { + override def onClicked(): Unit = { OcelotDesktop.showFileChooserDialog(JFileChooser.OPEN_DIALOG, JFileChooser.FILES_ONLY) { case Some(dir) => setConfigPath(dir.getCanonicalPath) diff --git a/src/main/scala/ocelot/desktop/ui/widget/settings/UISettingsTab.scala b/src/main/scala/ocelot/desktop/ui/widget/settings/UISettingsTab.scala index e406c4f..4057e7a 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/settings/UISettingsTab.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/settings/UISettingsTab.scala @@ -9,7 +9,7 @@ import ocelot.desktop.ui.widget.{Checkbox, Label, PaddingBox, Slider, Widget} import ocelot.desktop.util.MathUtils.roundAt class UISettingsTab extends SettingsTab { - override val icon: IconSource = IconSource.SettingsUI + override val icon: IconSource = IconSource.Icons.SettingsUI override val label: String = "UI" override def applySettings(): Unit = { @@ -20,6 +20,8 @@ class UISettingsTab extends SettingsTab { if (UiHandler.scalingFactor != Settings.get.scaleFactor) { UiHandler.scalingFactor = Settings.get.scaleFactor } + + UiHandler.root.workspaceView.windowPool.refreshWindows() } children :+= new PaddingBox( @@ -124,6 +126,21 @@ class UISettingsTab extends SettingsTab { Padding2D(bottom = 8), ) + children :+= new PaddingBox( + new Slider(Settings.get.unfocusedWindowTransparency, "Inactive window") { + override def minimumSize: Size2D = Size2D(512, 24) + + override def formatText: String = if (value < 0.001f) s"$text: hide" else f"$text: ${value * 100}%.0f%% opacity" + + override def onValueFinal(value: Float): Unit = { + Settings.get.unfocusedWindowTransparency = value + Settings.get.unfocusedWindowHide = value < 0.001f + applySettings() + } + }, + Padding2D(bottom = 8), + ) + children :+= new PaddingBox(new Label("Tooltip delays:"), Padding2D(bottom = 8)) children :+= new PaddingBox( diff --git a/src/main/scala/ocelot/desktop/ui/widget/slot/CardSlotWidget.scala b/src/main/scala/ocelot/desktop/ui/widget/slot/CardSlotWidget.scala index 9a53b0b..9a36839 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/slot/CardSlotWidget.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/slot/CardSlotWidget.scala @@ -6,6 +6,6 @@ import ocelot.desktop.inventory.traits.CardItem import totoro.ocelot.brain.util.Tier.Tier class CardSlotWidget(slot: Inventory#Slot, _tier: Tier) extends SlotWidget[CardItem](slot) { - override def ghostIcon: Option[IconSource] = Some(IconSource.CardIcon) + override def ghostIcon: Option[IconSource] = Some(IconSource.Icons.Card) override def slotTier: Option[Tier] = Some(_tier) } diff --git a/src/main/scala/ocelot/desktop/ui/widget/slot/ComponentBusSlotWidget.scala b/src/main/scala/ocelot/desktop/ui/widget/slot/ComponentBusSlotWidget.scala index b65a127..decda41 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/slot/ComponentBusSlotWidget.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/slot/ComponentBusSlotWidget.scala @@ -6,6 +6,6 @@ import ocelot.desktop.inventory.item.ComponentBusItem import totoro.ocelot.brain.util.Tier.Tier class ComponentBusSlotWidget(slot: Inventory#Slot, _tier: Tier) extends SlotWidget[ComponentBusItem](slot) { - override def ghostIcon: Option[IconSource] = Some(IconSource.ComponentBusIcon) + override def ghostIcon: Option[IconSource] = Some(IconSource.Icons.ComponentBus) override def slotTier: Option[Tier] = Some(_tier) } diff --git a/src/main/scala/ocelot/desktop/ui/widget/slot/CpuSlotWidget.scala b/src/main/scala/ocelot/desktop/ui/widget/slot/CpuSlotWidget.scala index 9947f63..c79bd1a 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/slot/CpuSlotWidget.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/slot/CpuSlotWidget.scala @@ -6,6 +6,6 @@ import ocelot.desktop.inventory.traits.CpuLikeItem import totoro.ocelot.brain.util.Tier.Tier class CpuSlotWidget(slot: Inventory#Slot, _tier: Tier) extends SlotWidget[CpuLikeItem](slot) { - override def ghostIcon: Option[IconSource] = Some(IconSource.CpuIcon) + override def ghostIcon: Option[IconSource] = Some(IconSource.Icons.Cpu) override def slotTier: Option[Tier] = Some(_tier) } diff --git a/src/main/scala/ocelot/desktop/ui/widget/slot/EepromSlotWidget.scala b/src/main/scala/ocelot/desktop/ui/widget/slot/EepromSlotWidget.scala index 58f91f2..b9cd63f 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/slot/EepromSlotWidget.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/slot/EepromSlotWidget.scala @@ -5,5 +5,5 @@ import ocelot.desktop.inventory.Inventory import ocelot.desktop.inventory.item.EepromItem class EepromSlotWidget(slot: Inventory#Slot) extends SlotWidget[EepromItem](slot) { - override def ghostIcon: Option[IconSource] = Some(IconSource.EepromIcon) + override def ghostIcon: Option[IconSource] = Some(IconSource.Icons.Eeprom) } diff --git a/src/main/scala/ocelot/desktop/ui/widget/slot/FloppySlotWidget.scala b/src/main/scala/ocelot/desktop/ui/widget/slot/FloppySlotWidget.scala index 12176c1..53cb45e 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/slot/FloppySlotWidget.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/slot/FloppySlotWidget.scala @@ -6,7 +6,7 @@ import ocelot.desktop.inventory.Inventory import ocelot.desktop.inventory.item.FloppyItem class FloppySlotWidget(slot: Inventory#Slot) extends SlotWidget[FloppyItem](slot) { - override def ghostIcon: Option[IconSource] = Some(IconSource.FloppyIcon) + override def ghostIcon: Option[IconSource] = Some(IconSource.Icons.Floppy) override def onItemAdded(): Unit = { super.onItemAdded() diff --git a/src/main/scala/ocelot/desktop/ui/widget/slot/HddSlotWidget.scala b/src/main/scala/ocelot/desktop/ui/widget/slot/HddSlotWidget.scala index b7bf786..e994810 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/slot/HddSlotWidget.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/slot/HddSlotWidget.scala @@ -6,6 +6,6 @@ import ocelot.desktop.inventory.item.HddItem import totoro.ocelot.brain.util.Tier.Tier class HddSlotWidget(slot: Inventory#Slot, _tier: Tier) extends SlotWidget[HddItem](slot) { - override def ghostIcon: Option[IconSource] = Some(IconSource.HddIcon) + override def ghostIcon: Option[IconSource] = Some(IconSource.Icons.Hdd) override def slotTier: Option[Tier] = Some(_tier) } diff --git a/src/main/scala/ocelot/desktop/ui/widget/slot/MemorySlotWidget.scala b/src/main/scala/ocelot/desktop/ui/widget/slot/MemorySlotWidget.scala index e945b81..8a7d696 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/slot/MemorySlotWidget.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/slot/MemorySlotWidget.scala @@ -6,6 +6,6 @@ import ocelot.desktop.inventory.item.MemoryItem import totoro.ocelot.brain.util.Tier.Tier class MemorySlotWidget(slot: Inventory#Slot, _tier: Tier) extends SlotWidget[MemoryItem](slot) { - override def ghostIcon: Option[IconSource] = Some(IconSource.MemoryIcon) + override def ghostIcon: Option[IconSource] = Some(IconSource.Icons.Memory) override def slotTier: Option[Tier] = Some(_tier) } diff --git a/src/main/scala/ocelot/desktop/ui/widget/slot/RackMountableSlotWidget.scala b/src/main/scala/ocelot/desktop/ui/widget/slot/RackMountableSlotWidget.scala index 9cb4b8d..c1c0f1a 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/slot/RackMountableSlotWidget.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/slot/RackMountableSlotWidget.scala @@ -6,7 +6,7 @@ import ocelot.desktop.inventory.item.ServerItem import ocelot.desktop.inventory.traits.RackMountableItem class RackMountableSlotWidget(slot: Inventory#Slot) extends SlotWidget[RackMountableItem](slot) { - override def ghostIcon: Option[IconSource] = Some(IconSource.ServerIcon) + override def ghostIcon: Option[IconSource] = Some(IconSource.Icons.Server) override def onItemRemoved(removedItem: RackMountableItem, replacedBy: Option[RackMountableItem]): Unit = { super.onItemRemoved(removedItem, replacedBy) diff --git a/src/main/scala/ocelot/desktop/ui/widget/slot/SlotWidget.scala b/src/main/scala/ocelot/desktop/ui/widget/slot/SlotWidget.scala index 482fb16..355f553 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/slot/SlotWidget.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/slot/SlotWidget.scala @@ -57,7 +57,7 @@ class SlotWidget[I <: Item](private val slot: Inventory#Slot)(implicit slotItemT def slotTier: Option[Tier] = None - def tierIcon: Option[IconSource] = slotTier.map(IconSource.TierIcon) + def tierIcon: Option[IconSource] = slotTier.map(IconSource.Icons.Tier) def itemIcon: Option[IconSource] = item.map(_.icon) @@ -109,8 +109,8 @@ class SlotWidget[I <: Item](private val slot: Inventory#Slot)(implicit slotItemT for (item <- item) { item.fillRmbMenu(menu) menu.addEntry( - ContextMenuEntry("Remove", IconSource.Delete, SoundSource.InterfaceClickLow) { - slot.remove() + ContextMenuEntry("Remove", IconSource.Icons.Delete, SoundSource.InterfaceClickLow) { + slot.removeAndDispose() } ) } @@ -218,7 +218,7 @@ class SlotWidget[I <: Item](private val slot: Inventory#Slot)(implicit slotItemT case evt @ DragEvent(DragEvent.State.Start, MouseEvent.Button.Left, _) => val pos = evt.start - val iconBounds = bounds.inflate(-2) + val iconBounds = bounds.inflated(-2) if (iconBounds.contains(pos)) { for (item <- item) { @@ -230,9 +230,9 @@ class SlotWidget[I <: Item](private val slot: Inventory#Slot)(implicit slotItemT } override final def draw(g: Graphics): Unit = { - g.sprite("EmptySlot", bounds) + g.sprite(IconSource.EmptySlot, bounds) - val iconBounds = bounds.inflate(-2) + val iconBounds = bounds.inflated(-2) itemIcon match { case Some(itemIcon) => g.sprite(itemIcon, iconBounds) diff --git a/src/main/scala/ocelot/desktop/ui/widget/statusbar/KeyMouseEntry.scala b/src/main/scala/ocelot/desktop/ui/widget/statusbar/KeyMouseEntry.scala index 02fe05c..43440b9 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/statusbar/KeyMouseEntry.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/statusbar/KeyMouseEntry.scala @@ -3,11 +3,11 @@ package ocelot.desktop.ui.widget.statusbar import ocelot.desktop.ColorScheme import ocelot.desktop.color.Color import ocelot.desktop.geometry.Size2D -import ocelot.desktop.graphics.Graphics +import ocelot.desktop.graphics.{Graphics, IconSource} import ocelot.desktop.ui.widget.Widget import ocelot.desktop.util.{DrawUtils, Spritesheet} -class KeyMouseEntry(val icon: String, val key: String, val text: String) extends Widget { +class KeyMouseEntry(val icon: IconSource, val key: String, val text: String) extends Widget { private val iconSize = Spritesheet.spriteSize(icon) override def minimumSize: Size2D = Size2D(iconSize.width + key.length * 8 + 40 + text.length * 8, 16) diff --git a/src/main/scala/ocelot/desktop/ui/widget/statusbar/MouseEntry.scala b/src/main/scala/ocelot/desktop/ui/widget/statusbar/MouseEntry.scala index af9fd3c..c26a884 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/statusbar/MouseEntry.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/statusbar/MouseEntry.scala @@ -3,11 +3,11 @@ package ocelot.desktop.ui.widget.statusbar import ocelot.desktop.ColorScheme import ocelot.desktop.color.Color import ocelot.desktop.geometry.Size2D -import ocelot.desktop.graphics.Graphics +import ocelot.desktop.graphics.{Graphics, IconSource} import ocelot.desktop.ui.widget.Widget import ocelot.desktop.util.Spritesheet -class MouseEntry(val icon: String, val text: String) extends Widget { +class MouseEntry(val icon: IconSource, val text: String) extends Widget { private val iconSize = Spritesheet.spriteSize(icon) override def minimumSize: Size2D = Size2D(iconSize.width + 24 + text.length * 8, 16) diff --git a/src/main/scala/ocelot/desktop/ui/widget/statusbar/StatusBar.scala b/src/main/scala/ocelot/desktop/ui/widget/statusbar/StatusBar.scala index e1f13ca..a61ffb7 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/statusbar/StatusBar.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/statusbar/StatusBar.scala @@ -5,17 +5,19 @@ import ocelot.desktop.geometry.{Padding2D, Size2D} import ocelot.desktop.graphics.{Graphics, IconSource} import ocelot.desktop.ui.UiHandler import ocelot.desktop.ui.event.handlers.{HoverHandler, MouseHandler} -import ocelot.desktop.ui.event.{ClickEvent, HoverEvent, KeyEvent, MouseEvent} +import ocelot.desktop.ui.event.{ClickEvent, HoverEvent, MouseEvent} import ocelot.desktop.ui.layout.{AlignItems, LinearLayout} import ocelot.desktop.ui.widget._ import ocelot.desktop.ui.widget.contextmenu.{ContextMenu, ContextMenuEntry} import ocelot.desktop.ui.widget.traits.HoverAnimation +import ocelot.desktop.util.Keybind.FPSCounter +import ocelot.desktop.util.Keymap.Press import ocelot.desktop.{ColorScheme, OcelotDesktop} -import org.lwjgl.input.Keyboard import scala.collection.immutable.ArraySeq import scala.concurrent.duration.DurationInt + class StatusBar extends Widget { override protected val layout: LinearLayout = new LinearLayout(this, alignItems = AlignItems.Center) @@ -28,7 +30,7 @@ class StatusBar extends Widget { private var showFPS = false eventHandlers += { - case KeyEvent(KeyEvent.State.Press, Keyboard.KEY_F2, _) => + case Press(FPSCounter) => showFPS = !showFPS if (showFPS) { children :+= new PaddingBox( @@ -65,13 +67,13 @@ class StatusBar extends Widget { case ClickEvent(MouseEvent.Button.Right, pos) => val menu = new ContextMenu menu.addEntry( - ContextMenuEntry("Change simulation speed", IconSource.Edit) { + ContextMenuEntry("Change simulation speed", IconSource.Icons.Edit) { new ChangeSimulationSpeedDialog().show() } ) menu.addEntry( - ContextMenuEntry("Reset simulation speed", IconSource.Restart) { + ContextMenuEntry("Reset simulation speed", IconSource.Icons.Restart) { OcelotDesktop.ticker.tickInterval = 50.millis } ) @@ -100,7 +102,7 @@ class StatusBar extends Widget { super.update() if (isHovered) { - root.get.statusBar.addMouseEntry("icons/RMB", "Change simulation speed") + root.get.statusBar.addMouseEntry(IconSource.Icons.RMB, "Change simulation speed") } } @@ -133,22 +135,16 @@ class StatusBar extends Widget { Padding2D(left = 8), ) - def addMouseEntry(icon: String, text: String): Unit = { - if ( - !keyMouseEntries.children.filter(_.isInstanceOf[MouseEntry]).map(_.asInstanceOf[MouseEntry]).exists( - _.icon == icon - ) - ) + def addMouseEntry(icon: IconSource, text: String): Unit = { + if (!keyMouseEntries.children.collect({ case e: MouseEntry => e.icon }).contains(icon)) { keyMouseEntries.children :+= new MouseEntry(icon, text) + } } - def addKeyMouseEntry(icon: String, key: String, text: String): Unit = { - if ( - !keyMouseEntries.children.filter(_.isInstanceOf[KeyMouseEntry]).map(_.asInstanceOf[KeyMouseEntry]).exists(v => - v.icon == icon && v.key == key - ) - ) + def addKeyMouseEntry(icon: IconSource, key: String, text: String): Unit = { + if (!keyMouseEntries.children.collect({ case e: KeyMouseEntry => e }).exists(e => e.icon == icon && e.key == key)) { keyMouseEntries.children :+= new KeyMouseEntry(icon, key, text) + } } def addKeyEntry(key: String, text: String): Unit = { diff --git a/src/main/scala/ocelot/desktop/ui/widget/tooltip/TooltipPool.scala b/src/main/scala/ocelot/desktop/ui/widget/tooltip/TooltipPool.scala index f4e4392..f6924ff 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/tooltip/TooltipPool.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/tooltip/TooltipPool.scala @@ -82,7 +82,7 @@ class TooltipPool extends Widget { } tooltip.tooltip.position = tooltip.tooltip.position + (UiHandler.mousePosition + offset - tooltip.tooltip.position) * - (if (tooltip.tooltip.isClosing) Math.pow(Easing.easeInQuad(tooltip.tooltip.getAlpha), 6) else 1.0) + (if (tooltip.tooltip.isClosing) math.pow(Easing.easeInQuad(tooltip.tooltip.getAlpha), 6).toFloat else 1f) }) if (dueToClean) { diff --git a/src/main/scala/ocelot/desktop/ui/widget/traits/HoverHighlight.scala b/src/main/scala/ocelot/desktop/ui/widget/traits/HoverHighlight.scala index 6230bf8..13147f4 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/traits/HoverHighlight.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/traits/HoverHighlight.scala @@ -1,6 +1,7 @@ package ocelot.desktop.ui.widget.traits import ocelot.desktop.color.Color +import ocelot.desktop.geometry.Rect2D import ocelot.desktop.graphics.Graphics import ocelot.desktop.ui.Const.{SlotHighlightAlpha, SlotHighlightAnimationSpeedEnter, SlotHighlightAnimationSpeedLeave} import ocelot.desktop.ui.event.HoverEvent @@ -21,11 +22,16 @@ trait HoverHighlight extends Widget with HoverHandler with Updatable { highlightAlpha.goto(0f) } + protected def highlightBounds: Rect2D = bounds.inflated(-2) + override def draw(g: Graphics): Unit = { super.draw(g) + drawHighlight(g) + } + protected def drawHighlight(g: Graphics): Unit = { if (highlightAlpha.value > 0.005f) { - g.rect(bounds.inflate(-2), Color.White.mapA(_ => highlightAlpha.value)) + g.rect(highlightBounds, Color.White.mapA(_ => highlightAlpha.value)) } } diff --git a/src/main/scala/ocelot/desktop/ui/widget/verticalmenu/VerticalMenuButton.scala b/src/main/scala/ocelot/desktop/ui/widget/verticalmenu/VerticalMenuButton.scala index c332135..58ed193 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/verticalmenu/VerticalMenuButton.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/verticalmenu/VerticalMenuButton.scala @@ -46,7 +46,7 @@ class VerticalMenuButton(icon: IconSource, label: String, handler: VerticalMenuB def onMouseLeave(): Unit = colorAnimation.goto(ColorScheme("VerticalMenuBackground")) eventHandlers += { - case MouseEvent(MouseEvent.State.Press, MouseEvent.Button.Left) => + case MouseEvent(MouseEvent.State.Pressed, MouseEvent.Button.Left) => clickSoundSource.press.play() case ClickEvent(MouseEvent.Button.Left, _) => @@ -64,7 +64,7 @@ class VerticalMenuButton(icon: IconSource, label: String, handler: VerticalMenuB colorAnimation.update() g.rect(bounds, colorAnimation.color) g.rect(bounds.x + bounds.w - 2f, bounds.y, 2f, bounds.h, ColorScheme("VerticalMenuBorder")) - if (selected) g.sprite("TabArrow", bounds.x + bounds.w - 8f, bounds.y + bounds.h / 2f - 7f, 8, 14) + if (selected) g.sprite(IconSource.TabArrow, bounds.x + bounds.w - 8f, bounds.y + bounds.h / 2f - 7f, 8, 14) drawChildren(g) } diff --git a/src/main/scala/ocelot/desktop/ui/widget/window/BasicWindow.scala b/src/main/scala/ocelot/desktop/ui/widget/window/BasicWindow.scala index 6ac77bf..f4de9b9 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/window/BasicWindow.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/window/BasicWindow.scala @@ -28,7 +28,11 @@ trait BasicWindow extends Window { override protected def onUnfocused(): Unit = { super.onUnfocused() - alpha.goto(Settings.get.unfocusedWindowTransparency.toFloat) + if (Settings.get.unfocusedWindowHide) { + close() + } else { + alpha.goto(Settings.get.unfocusedWindowTransparency) + } } override def save(nbt: NBTTagCompound): Unit = { @@ -46,9 +50,9 @@ trait BasicWindow extends Window { alpha.update() - if (alpha.value < 0.001) { + if (state == Window.State.Closing && alpha.value < 0.001) { state = Window.State.Closed - } else if (alpha.value > 0.999) { + } else if (state == Window.State.Opening && alpha.value > 0.999) { state = Window.State.Open } } @@ -69,4 +73,15 @@ trait BasicWindow extends Window { if (alpha.value < 1f) g.endGroupAlpha(alpha.value) } + + override def refresh(): Unit = { + // currently is only used to update unfocused transparency values (could be extended in the future) + if (!isFocused) { + if (Settings.get.unfocusedWindowHide && (isOpen || isOpening)) { + this.close() + } else { + alpha.goto(Settings.get.unfocusedWindowTransparency) + } + } + } } diff --git a/src/main/scala/ocelot/desktop/ui/widget/window/PanelWindow.scala b/src/main/scala/ocelot/desktop/ui/widget/window/PanelWindow.scala index 1148190..edece5e 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/window/PanelWindow.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/window/PanelWindow.scala @@ -14,7 +14,12 @@ trait PanelWindow extends BasicWindow { protected def titleMaxLength: Int = 32 - def setInner(inner: Widget, padding: Padding2D = Padding2D(bottom = 13, left = 12, right = 12)): Unit = { + def setInner( + inner: Widget, + padding: Padding2D = Padding2D(bottom = 13, left = 12, right = 12), + titlePadding: Padding2D = Padding2D(top = 8, left = 12, right = 12, bottom = 2), + contentFactory: (Widget, Padding2D) => Widget = new PaddingBox(_, _), + ): Unit = { children = ArraySeq.empty children :+= new PaddingBox( @@ -22,9 +27,9 @@ trait PanelWindow extends BasicWindow { override def title: String = PanelWindow.this.title override def titleMaxLength: Int = PanelWindow.this.titleMaxLength }, - Padding2D(top = 8, left = 12, right = 12, bottom = 2), + titlePadding, ) - children :+= new PaddingBox(inner, padding) + children :+= contentFactory(inner, padding) } } diff --git a/src/main/scala/ocelot/desktop/ui/widget/window/TitleBar.scala b/src/main/scala/ocelot/desktop/ui/widget/window/TitleBar.scala index b1710c1..6061029 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/window/TitleBar.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/window/TitleBar.scala @@ -1,5 +1,6 @@ package ocelot.desktop.ui.widget.window +import ocelot.desktop.graphics.IconSource import ocelot.desktop.ui.layout.{AlignItems, Layout, LinearLayout} import ocelot.desktop.ui.widget.{IconButton, Label, Widget} import ocelot.desktop.util.Orientation @@ -27,8 +28,8 @@ class TitleBar(val window: Window) extends Widget { } children :+= new IconButton( - "icons/Pin", - "icons/Unpin", + IconSource.Icons.Pin, + IconSource.Icons.Unpin, mode = IconButton.Mode.Switch, darkenActiveColorFactor = 0.5f, model = IconButton.ReadOnlyModel(isPinned), @@ -39,10 +40,10 @@ class TitleBar(val window: Window) extends Widget { } children :+= new IconButton( - "icons/Close", - "icons/Close", + IconSource.Icons.Close, + IconSource.Icons.Close, darkenActiveColorFactor = 0.5f, ) { - override def onPressed(): Unit = onCloseRequested() + override def onClicked(): Unit = onCloseRequested() } } diff --git a/src/main/scala/ocelot/desktop/ui/widget/window/Window.scala b/src/main/scala/ocelot/desktop/ui/widget/window/Window.scala index f14f5f1..d94b0c7 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/window/Window.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/window/Window.scala @@ -4,7 +4,7 @@ import ocelot.desktop.Settings import ocelot.desktop.geometry.{Rect2D, Size2D, Vector2D} import ocelot.desktop.ui.UiHandler import ocelot.desktop.ui.event.handlers.MouseHandler -import ocelot.desktop.ui.event.{DragEvent, KeyEvent, MouseEvent} +import ocelot.desktop.ui.event.{Capturing, DragEvent, KeyEvent, MouseEvent} import ocelot.desktop.ui.widget.Widget import ocelot.desktop.util.Persistable import org.lwjgl.input.Keyboard @@ -30,7 +30,7 @@ trait Window extends Widget with Persistable with MouseHandler { override protected def receiveDragEvents: Boolean = true eventHandlers += { - case MouseEvent(MouseEvent.State.Press, _) => + case Capturing(MouseEvent(MouseEvent.State.Pressed, _)) => focus() case ev @ DragEvent(DragEvent.State.Start, MouseEvent.Button.Left, mousePos) => @@ -61,7 +61,7 @@ trait Window extends Widget with Persistable with MouseHandler { while (_state != prevState) { prevState = _state - _state = newState match { + _state = _state match { case Window.State.Opening => onOpening() case Window.State.Open => onOpen() case Window.State.Closing => onClosing() @@ -174,6 +174,10 @@ trait Window extends Widget with Persistable with MouseHandler { } } + /** Updates the window state / render, by reading and applying fresh setting values. + */ + def refresh(): Unit = {} + /** Disposes of the window immediately. This will make it dramatically pop out of existence, * without playing the closing animation. * diff --git a/src/main/scala/ocelot/desktop/ui/widget/window/WindowPool.scala b/src/main/scala/ocelot/desktop/ui/widget/window/WindowPool.scala index 662d13d..706ab36 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/window/WindowPool.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/window/WindowPool.scala @@ -1,7 +1,6 @@ package ocelot.desktop.ui.widget.window import ocelot.desktop.geometry.Vector2D -import ocelot.desktop.ui.event.handlers.MouseHandler import ocelot.desktop.ui.layout.Layout import ocelot.desktop.ui.widget.Widget @@ -82,6 +81,10 @@ class WindowPool extends Widget { windows.zipWithIndex.foreach { case (window, index) => window.poolIndex = index } } + def refreshWindows(): Unit = { + windows.foreach(_.refresh()) + } + def movePinnedWindows(delta: Vector2D): Unit = { windows.filter(_.isPinned).foreach(_.position += delta) } diff --git a/src/main/scala/ocelot/desktop/ui/widget/window/Windowed.scala b/src/main/scala/ocelot/desktop/ui/widget/window/Windowed.scala index c0c4828..3983705 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/window/Windowed.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/window/Windowed.scala @@ -1,9 +1,9 @@ package ocelot.desktop.ui.widget.window -import ocelot.desktop.util.Persistable +import ocelot.desktop.util.{Disposable, Persistable} import totoro.ocelot.brain.nbt.NBTTagCompound -trait Windowed[T <: Window] extends Persistable { +trait Windowed[T <: Window] extends Persistable with Disposable { protected def createWindow(): T private var _window: Option[T] = None @@ -33,6 +33,10 @@ trait Windowed[T <: Window] extends Persistable { window } + override def dispose(): Unit = { + super.dispose() + closeAndDisposeWindow() + } // ------------------------------- NBT ------------------------------- protected def windowNBTKey: String = "window" diff --git a/src/main/scala/ocelot/desktop/util/AudibleComputerAware.scala b/src/main/scala/ocelot/desktop/util/AudibleComputerAware.scala index fe601a9..d12257f 100644 --- a/src/main/scala/ocelot/desktop/util/AudibleComputerAware.scala +++ b/src/main/scala/ocelot/desktop/util/AudibleComputerAware.scala @@ -10,9 +10,9 @@ trait AudibleComputerAware extends ComputerAware { ) def updateRunningSound(): Unit = { - if (!computer.machine.isRunning && soundComputerRunning.isPlaying) { + if (!computer.machine.isRunning && soundComputerRunning.playing) { soundComputerRunning.stop() - } else if (computer.machine.isRunning && !soundComputerRunning.isPlaying && !Audio.isDisabled) { + } else if (computer.machine.isRunning && !soundComputerRunning.playing && !Audio.isDisabled) { soundComputerRunning.play() } } diff --git a/src/main/scala/ocelot/desktop/util/ComputerAware.scala b/src/main/scala/ocelot/desktop/util/ComputerAware.scala index 95a496a..4f07450 100644 --- a/src/main/scala/ocelot/desktop/util/ComputerAware.scala +++ b/src/main/scala/ocelot/desktop/util/ComputerAware.scala @@ -2,7 +2,7 @@ package ocelot.desktop.util import ocelot.desktop.graphics.IconSource import ocelot.desktop.inventory.item._ -import ocelot.desktop.inventory.traits.ComponentItem +import ocelot.desktop.inventory.traits.EntityItem import ocelot.desktop.inventory.{Item, SyncedInventory} import ocelot.desktop.ui.widget.contextmenu.{ContextMenu, ContextMenuEntry, ContextMenuIcon, ContextMenuSubmenu} import ocelot.desktop.ui.widget.slot._ @@ -17,8 +17,14 @@ import totoro.ocelot.brain.util.Tier.Tier import scala.math.Ordering.Implicits.infixOrderingOps import scala.reflect.ClassTag -trait ComputerAware extends Logging with SyncedInventory with DefaultSlotItemsFillable with Windowed[ComputerWindow] { - override type I = Item with ComponentItem +trait ComputerAware + extends Logging + with SyncedInventory + with DefaultSlotItemsFillable + with Windowed[ComputerWindow] + with Disposable { + + override type I = Item with EntityItem def computer: Computer with TieredPersistable @@ -42,16 +48,16 @@ trait ComputerAware extends Logging with SyncedInventory with DefaultSlotItemsFi def addPowerContextMenuEntries(menu: ContextMenu): Unit = { if (computer.machine.isRunning) { - menu.addEntry(ContextMenuEntry("Turn off", IconSource.Power) { + menu.addEntry(ContextMenuEntry("Turn off", IconSource.Icons.Power) { turnOff() }) - menu.addEntry(ContextMenuEntry("Reboot", IconSource.Restart) { + menu.addEntry(ContextMenuEntry("Reboot", IconSource.Icons.Restart) { turnOff() turnOn() }) } else { - menu.addEntry(ContextMenuEntry("Turn on", IconSource.Power) { + menu.addEntry(ContextMenuEntry("Turn on", IconSource.Icons.Power) { turnOn() }) } @@ -59,11 +65,13 @@ trait ComputerAware extends Logging with SyncedInventory with DefaultSlotItemsFi def addTierContextMenuEntries(menu: ContextMenu, maxNonCreativeTier: Tier = Tier.Three): Unit = { menu.addEntry( - new ContextMenuSubmenu("Change tier", Some(ContextMenuIcon(IconSource.Tiers))) { + new ContextMenuSubmenu("Change tier", Some(ContextMenuIcon(IconSource.Icons.Tiers))) { def addTierEntry(tier: Tier): Unit = { - addEntry(ContextMenuEntry(tier.label) { + val entry = ContextMenuEntry(s"${tier.label}${if (tier == computer.tier) " (current)" else ""}") { changeTier(tier) - }) + } + entry.setEnabled(tier != computer.tier) + addEntry(entry) } for (tier <- Tier.One to maxNonCreativeTier) @@ -109,8 +117,11 @@ trait ComputerAware extends Logging with SyncedInventory with DefaultSlotItemsFi .minByOption(_.slotTier) } - for (item <- items; newSlot <- findBestSlot(item, slots)) { - newSlot.item = item + for (item <- items) { + findBestSlot(item, slots) match{ + case Some(newSlot) => newSlot.item = item + case None => item.dispose() + } } } @@ -132,7 +143,7 @@ trait ComputerAware extends Logging with SyncedInventory with DefaultSlotItemsFi protected def addSlotsBasedOnTier(): Unit - final def createSlots(): Unit = { + private final def createSlots(): Unit = { for (slot <- slots) slot.dispose() diff --git a/src/main/scala/ocelot/desktop/util/DiskDriveAware.scala b/src/main/scala/ocelot/desktop/util/DiskDriveAware.scala index cea3914..95d1d5d 100644 --- a/src/main/scala/ocelot/desktop/util/DiskDriveAware.scala +++ b/src/main/scala/ocelot/desktop/util/DiskDriveAware.scala @@ -46,7 +46,7 @@ trait DiskDriveAware extends Logging with SyncedInventory with DefaultSlotItemsF bounds.y, bounds.w, bounds.h, - IntColor(ColorValues(item.color.value)), + IntColor(ColorValues(item.color)), ) } } @@ -61,7 +61,7 @@ trait DiskDriveAware extends Logging with SyncedInventory with DefaultSlotItemsF } def addEjectContextMenuEntry(menu: ContextMenu): Unit = { - menu.addEntry(ContextMenuEntry("Eject", IconSource.Eject) { + menu.addEntry(ContextMenuEntry("Eject", IconSource.Icons.Eject) { eject() }) } diff --git a/src/main/scala/ocelot/desktop/util/DrawUtils.scala b/src/main/scala/ocelot/desktop/util/DrawUtils.scala index 50fb29a..adfbdbb 100644 --- a/src/main/scala/ocelot/desktop/util/DrawUtils.scala +++ b/src/main/scala/ocelot/desktop/util/DrawUtils.scala @@ -1,7 +1,8 @@ package ocelot.desktop.util +import ocelot.desktop.ColorScheme import ocelot.desktop.color.{Color, RGBAColor, RGBAColorNorm} -import ocelot.desktop.geometry.Vector2D +import ocelot.desktop.geometry.{Rect2D, Vector2D} import ocelot.desktop.graphics.{Graphics, IconSource} import ocelot.desktop.graphics.IconSource.{DiskActivityIconSource, NetworkActivityIconSource, PowerIconSource} import ocelot.desktop.node.Node.NoHighlightSize @@ -11,45 +12,97 @@ import totoro.ocelot.brain.entity.traits.{DiskActivityAware, NetworkActivityAwar object DrawUtils { def screenBorder( g: Graphics, - x: Float, - y: Float, - w: Float, - h: Float, + outer: Rect2D, + inner: Rect2D, color: Color = RGBAColor(255, 255, 255), ): Unit = { - g.sprite("screen/CornerTL", x - 16, y - 20, 16, 20, color) - g.sprite("screen/CornerTR", x + w, y - 20, 16, 20, color) - g.sprite("screen/CornerBL", x - 16, y + h, 16, 16, color) - g.sprite("screen/CornerBR", x + w, y + h, 16, 16, color) + val thicknessOuterTop = 12 + val thickness = 8 + val top = thickness + thicknessOuterTop + val side = 2 * thickness + val horizontal = 2 * side + val vertical = top + side - g.sprite("screen/BorderT", x, y - 20, w, 20, color) - g.sprite("screen/BorderB", x, y + h, w, 16, color) + val rightBorderX = outer.max.x - side + val bottomBorderY = outer.max.y - side + // filler (only visible if the screen view is too small). + g.rect( + outer.x + thickness, + outer.y + thicknessOuterTop, + outer.w - 2 * thickness, + outer.h - thickness - thicknessOuterTop, + ColorScheme("WindowBackground").withAlpha(color.toRGBANorm.a), + ) + + // inner border: corners. + g.sprite(IconSource.Screen.InnerCornerTL, inner.x - thickness, inner.y - thickness, thickness, thickness, color) + g.sprite(IconSource.Screen.InnerCornerTR, inner.max.x, inner.y - thickness, thickness, thickness, color) + g.sprite(IconSource.Screen.InnerCornerBL, inner.x - thickness, inner.max.y, thickness, thickness, color) + g.sprite(IconSource.Screen.InnerCornerBR, inner.max.x, inner.max.y, thickness, thickness, color) + + // inner border: top and bottom. + g.sprite(IconSource.Screen.InnerBorderT, inner.x, inner.y - thickness, inner.w, thickness, color) + g.sprite(IconSource.Screen.InnerBorderB, inner.x, inner.max.y, inner.w, thickness, color) + + // inner border: left. g.save() - g.translate(x - 16, y) + g.translate(inner.x - thickness, inner.y) g.rotate(270.toRadians) - g.sprite("screen/BorderB", -h, 0, h, 16, color) + g.sprite(IconSource.Screen.InnerBorderT, -inner.h, 0, inner.h, thickness, color) g.restore() + // inner border: right. g.save() - g.translate(x + w, y) + g.translate(inner.max.x, inner.y) g.rotate(270.toRadians) - g.sprite("screen/BorderB", -h, 0, h, 16, color) + g.sprite(IconSource.Screen.InnerBorderB, -inner.h, 0, inner.h, thickness, color) + g.restore() + + // outer border: corners. + g.sprite(IconSource.Screen.OuterCornerTL, outer.x, outer.y, side, top, color) + g.sprite(IconSource.Screen.OuterCornerTR, rightBorderX, outer.y, side, top, color) + g.sprite(IconSource.Screen.OuterCornerBL, outer.x, bottomBorderY, side, side, color) + g.sprite(IconSource.Screen.OuterCornerBR, rightBorderX, bottomBorderY, side, side, color) + + // outer border: top and bottom. + g.sprite(IconSource.Screen.OuterBorderT, outer.x + side, outer.y, outer.w - horizontal, thicknessOuterTop, color) + g.sprite( + IconSource.Screen.InnerBorderT, + outer.x + side, + outer.max.y - thickness, + outer.w - horizontal, + thickness, + color, + ) + + // outer border: left. + g.save() + g.translate(outer.x, outer.y + top) + g.rotate(270.toRadians) + g.sprite(IconSource.Screen.InnerBorderB, -outer.h + vertical, 0, outer.h - vertical, thickness, color) + g.restore() + + // outer border: right. + g.save() + g.translate(outer.max.x - thickness, outer.y + top) + g.rotate(270.toRadians) + g.sprite(IconSource.Screen.InnerBorderT, -outer.h + vertical, 0, outer.h - vertical, thickness, color) g.restore() } def panel(g: Graphics, x: Float, y: Float, w: Float, h: Float): Unit = { - g.sprite("panel/CornerTL", x, y, 4, 4) - g.sprite("panel/CornerTR", x + w - 4, y, 4, 4) - g.sprite("panel/CornerBL", x, y + h - 4, 4, 4) - g.sprite("panel/CornerBR", x + w - 4, y + h - 4, 4, 4) + g.sprite(IconSource.Panel.CornerTL, x, y, 4, 4) + g.sprite(IconSource.Panel.CornerTR, x + w - 4, y, 4, 4) + g.sprite(IconSource.Panel.CornerBL, x, y + h - 4, 4, 4) + g.sprite(IconSource.Panel.CornerBR, x + w - 4, y + h - 4, 4, 4) - g.sprite("panel/BorderT", x + 4, y, w - 8, 4) - g.sprite("panel/BorderB", x + 4, y + h - 4, w - 8, 4) - g.sprite("panel/BorderL", x, y + 4, 4, h - 8) - g.sprite("panel/BorderR", x + w - 4, y + 4, 4, h - 8) + g.sprite(IconSource.Panel.BorderT, x + 4, y, w - 8, 4) + g.sprite(IconSource.Panel.BorderB, x + 4, y + h - 4, w - 8, 4) + g.sprite(IconSource.Panel.BorderL, x, y + 4, 4, h - 8) + g.sprite(IconSource.Panel.BorderR, x + w - 4, y + 4, 4, h - 8) - g.sprite("panel/Fill", x + 4, y + 4, w - 8, h - 8) + g.sprite(IconSource.Panel.Fill, x + 4, y + 4, w - 8, h - 8) } def isValidPolyline(points: Array[Vector2D]): Boolean = { @@ -95,6 +148,8 @@ object DrawUtils { if (alpha < 1f) g.endGroupAlpha(alpha) } + type BorderRenderer = (Graphics, Float, Float, Float, Float, Color) => Unit + def windowWithShadow( g: Graphics, x: Float, @@ -131,19 +186,19 @@ object DrawUtils { h: Float, color: Color = RGBAColor(255, 255, 255), ): Unit = { - g.sprite("window/CornerTL", x, y, 8, 8, color) - g.sprite("window/CornerTR", x + w - 8, y, 8, 8, color) - g.sprite("window/CornerBL", x, y + h - 8, 8, 8, color) - g.sprite("window/CornerBR", x + w - 8, y + h - 8, 8, 8, color) + g.sprite(IconSource.Window.CornerTL, x, y, 8, 8, color) + g.sprite(IconSource.Window.CornerTR, x + w - 8, y, 8, 8, color) + g.sprite(IconSource.Window.CornerBL, x, y + h - 8, 8, 8, color) + g.sprite(IconSource.Window.CornerBR, x + w - 8, y + h - 8, 8, 8, color) - g.sprite("window/BorderLight", x + 8, y, w - 16, 8, color) - g.sprite("window/BorderDark", x + 8, y + h - 8, w - 16, 8, color) + g.sprite(IconSource.Window.BorderLight, x + 8, y, w - 16, 8, color) + g.sprite(IconSource.Window.BorderDark, x + 8, y + h - 8, w - 16, 8, color) g.save() g.translate(x, y + 8) g.rotate(270.toRadians) - g.sprite("window/BorderLight", 0, 0, -h + 16, 8, color) - g.sprite("window/BorderDark", 0, w - 8, -h + 16, 8, color) + g.sprite(IconSource.Window.BorderLight, 0, 0, -h + 16, 8, color) + g.sprite(IconSource.Window.BorderDark, 0, w - 8, -h + 16, 8, color) g.restore() g.rect(x + 8, y + 8, w - 16, h - 16, RGBAColor(198, 198, 198, color.toRGBA.a)) @@ -152,24 +207,24 @@ object DrawUtils { def shadow(g: Graphics, x: Float, y: Float, w: Float, h: Float, a: Float = 0.8f): Unit = { val col = RGBAColorNorm(1, 1, 1, a) - rotSprite(g, "ShadowCorner", x, y, 24, 24, 180.toRadians, col) - rotSprite(g, "ShadowCorner", x + w - 24, y, 24, 24, 270.toRadians, col) - rotSprite(g, "ShadowCorner", x, y + h - 24, 24, 24, 90.toRadians, col) - g.sprite("ShadowCorner", x + w - 24, y + h - 24, 24, 24, col) + rotSprite(g, IconSource.ShadowCorner, x, y, 24, 24, 180.toRadians, col) + rotSprite(g, IconSource.ShadowCorner, x + w - 24, y, 24, 24, 270.toRadians, col) + rotSprite(g, IconSource.ShadowCorner, x, y + h - 24, 24, 24, 90.toRadians, col) + g.sprite(IconSource.ShadowCorner, x + w - 24, y + h - 24, 24, 24, col) - g.sprite("ShadowBorder", x + 24, y + 24, w - 48, -24, col) - g.sprite("ShadowBorder", x + 24, y + h - 24, w - 48, 24, col) + g.sprite(IconSource.ShadowBorder, x + 24, y + 24, w - 48, -24, col) + g.sprite(IconSource.ShadowBorder, x + 24, y + h - 24, w - 48, 24, col) g.save() g.translate(x + 24, y + 24) g.rotate(90.toRadians) - g.sprite("ShadowBorder", 0, 0, h - 48, 24, col) + g.sprite(IconSource.ShadowBorder, 0, 0, h - 48, 24, col) g.restore() g.save() g.translate(x + w - 24, y + h - 24) g.rotate(270.toRadians) - g.sprite("ShadowBorder", 0, 0, h - 48, 24, col) + g.sprite(IconSource.ShadowBorder, 0, 0, h - 48, 24, col) g.restore() g.rect(x + 24, y + 24, w - 48, h - 48, RGBAColorNorm(0, 0, 0, a)) @@ -177,7 +232,7 @@ object DrawUtils { private def rotSprite( g: Graphics, - sprite: String, + icon: IconSource, x: Float, y: Float, w: Float, @@ -188,7 +243,7 @@ object DrawUtils { g.save() g.translate(x + w / 2f, y + h / 2f) g.rotate(angle) - g.sprite(sprite, -w / 2f, -h / 2f, w, h, col) + g.sprite(icon, -w / 2f, -h / 2f, w, h, col) g.restore() } diff --git a/src/main/scala/ocelot/desktop/util/Font.scala b/src/main/scala/ocelot/desktop/util/Font.scala index 596f01f..fd174a8 100644 --- a/src/main/scala/ocelot/desktop/util/Font.scala +++ b/src/main/scala/ocelot/desktop/util/Font.scala @@ -8,6 +8,7 @@ import totoro.ocelot.brain.util.FontUtils import java.awt.image.{BufferedImage, DataBufferByte, IndexColorModel} import java.io.InputStream import java.nio.ByteBuffer +import scala.collection.immutable.IntMap import scala.collection.mutable import scala.io.{Codec, Source} @@ -23,7 +24,7 @@ class Font(val name: String, val fontSize: Int) extends Resource with Logging { new BufferedImage(AtlasWidth, AtlasHeight, BufferedImage.TYPE_BYTE_BINARY, icm) } - val map = new mutable.HashMap[Int, Rect2D] + var map: IntMap[Rect2D] = _ var texture: Texture = _ init() @@ -33,6 +34,8 @@ class Font(val name: String, val fontSize: Int) extends Resource with Logging { private def init(): Unit = { logger.info(f"Loading font $name") + val rawMap = new mutable.HashMap[Int, Rect2D] + var ox = 0 var oy = 0 @@ -55,7 +58,7 @@ class Font(val name: String, val fontSize: Int) extends Resource with Logging { oy += fontSize } - map(charCode) = Rect2D( + rawMap(charCode) = Rect2D( ox.asInstanceOf[Float] / AtlasWidth.asInstanceOf[Float], oy.asInstanceOf[Float] / AtlasHeight.asInstanceOf[Float], width.asInstanceOf[Float] / AtlasWidth.asInstanceOf[Float], @@ -97,6 +100,7 @@ class Font(val name: String, val fontSize: Int) extends Resource with Logging { logger.info(s"Packed $glyphCount glyphs into ${AtlasWidth}x$AtlasHeight 1-bit texture ($size bytes)") logger.info(s"Skipped $outOfRangeGlyphCount non-BMP glyphs") + map = IntMap.from(rawMap) texture = makeTexture() } diff --git a/src/main/scala/ocelot/desktop/util/Keybind.scala b/src/main/scala/ocelot/desktop/util/Keybind.scala new file mode 100644 index 0000000..443bf2c --- /dev/null +++ b/src/main/scala/ocelot/desktop/util/Keybind.scala @@ -0,0 +1,31 @@ +package ocelot.desktop.util + +/** + * Collection of keybind handles. + * Each represents some action you may want to execute in Ocelot by pressing a key (or key combination). + */ + +object Keybind extends Enumeration { + type Keybind = KeybindVal + + // OpenComputers + val Insert: Keybind = KeybindVal("Insert text") + + // Workspace + val Center: Keybind = KeybindVal("Center camera") + val QuickSave: Keybind = KeybindVal("Quick save") + val QuickLoad: Keybind = KeybindVal("Quick load") + val Screenshot: Keybind = KeybindVal("Save screenshot") + + // Ocelot + val UIDebug: Keybind = KeybindVal("UI Debug Mode") + val FPSCounter: Keybind = KeybindVal("FPS counter") + val ReloadWorkspace: Keybind = KeybindVal("Reload workspace") + val Profiler: Keybind = KeybindVal("Ocelot profiler") + val Fullscreen: Keybind = KeybindVal("Toggle fullscreen mode") + + protected class KeybindVal(val description: String) extends super.Val + private object KeybindVal { + def apply(description: String): KeybindVal = new KeybindVal(description) + } +} diff --git a/src/main/scala/ocelot/desktop/util/Keymap.scala b/src/main/scala/ocelot/desktop/util/Keymap.scala new file mode 100644 index 0000000..51d91cd --- /dev/null +++ b/src/main/scala/ocelot/desktop/util/Keymap.scala @@ -0,0 +1,116 @@ +package ocelot.desktop.util + +import com.typesafe.config.{Config, ConfigValue, ConfigValueFactory} +import ocelot.desktop.Settings +import ocelot.desktop.ui.event.KeyEvent +import ocelot.desktop.ui.event.KeyEvent.State +import ocelot.desktop.util.Keybind.Keybind +import org.lwjgl.input.Keyboard + +import scala.collection.mutable +import scala.jdk.CollectionConverters._ + + +class Keymap { + // default mappings + val map: mutable.Map[Keybind.Value, Int] = mutable.Map( + // OpenComputers + Keybind.Insert -> Keyboard.KEY_INSERT, + + // Workspace + Keybind.Center -> Keyboard.KEY_HOME, + Keybind.QuickSave -> Keyboard.KEY_F5, + Keybind.QuickLoad -> Keyboard.KEY_F9, + Keybind.Screenshot -> Keyboard.KEY_F12, + + // Ocelot + Keybind.UIDebug -> Keyboard.KEY_F1, + Keybind.FPSCounter -> Keyboard.KEY_F2, + Keybind.ReloadWorkspace -> Keyboard.KEY_F3, + Keybind.Profiler -> Keyboard.KEY_F4, + Keybind.Fullscreen -> Keyboard.KEY_F11, + ) + + + /** Retrieves the LWJGL keycode which is associated with the given keybind. + * Will return `Keyboard.KEY_NONE` if the binding is not found. + * + * @param keybind the keybind + * @return the actual LWJGL key associated with the given keybind, + * or `Keyboard.KEY_NONE` if none is. + */ + def apply(keybind: Keybind): Int = map.getOrElse(keybind, Keyboard.KEY_NONE) + + /** Adds a new keybind mapping and optionally returns previous value. + * If the map already contains a + * mapping for the keybind, it will be overridden by the new value. + * + * @param keybind the keybind to update + * @param key the new LWJGL key code + * @return an option value containing the LWJGL key code associated with the keybind + * before the `set` operation was executed, or `None` if this keybind + * was not defined in the keymap before. + */ + def set(keybind: Keybind.Value, key: Int): Option[Int] = map.put(keybind, key) + + /** + * Retrieves the name for a keyboard key associated with the given keybind. + * @param keybind the keybind + * @return the name of the button on the keyboard, for example "HOME", or "F1" + */ + def name(keybind: Keybind): String = Keyboard.getKeyName(apply(keybind)) + + + override def clone(): Keymap = { + val clone = new Keymap + clone.map.addAll(this.map) + clone + } + + + /** + * Tries to find a keybind that corresponds to the specific LWJGL key code + * @param key the LWJGL key code + * @return Option containing the keybind (if found) + */ + private def findByCode(key: Int): Option[Keybind] = map.iterator.collectFirst { case (keybind: Keybind, `key`) => keybind } + + + /** + * Attempts to read known keybindings from a config. + * Will ignore missing or unfamiliar values, retaining default values. + */ + def load(config: Config): Unit = { + Keybind.values.foreach(keybind => { + val path = keybind.toString.toLowerCase + if (config.hasPath(path)) { + set(keybind, config.getInt(path)) + } + }) + } + + /** + * Generates a ConfigValue with this keybindings map, + * (which can be embedded into any Config). + */ + def save(): ConfigValue = ConfigValueFactory.fromMap((map.view map { + case (key: Keybind, value: Int) => (key.toString.toLowerCase(), value) + }).toMap.asJava) +} + +object Keymap { + object Press { + def unapply(event: KeyEvent): Option[Keybind] = event match { + case KeyEvent(State.Press, code, _) => + Settings.get.keymap.findByCode(code) + case _ => None + } + } + object Release { + def unapply(event: KeyEvent): Option[Keybind] = event match { + case KeyEvent(State.Release, code, _) => + Settings.get.keymap.findByCode(code) + case _ => None + } + } +} diff --git a/src/main/scala/ocelot/desktop/util/Lazy.scala b/src/main/scala/ocelot/desktop/util/Lazy.scala new file mode 100644 index 0000000..9911e3b --- /dev/null +++ b/src/main/scala/ocelot/desktop/util/Lazy.scala @@ -0,0 +1,23 @@ +package ocelot.desktop.util + +class Lazy[A](init: () => A) { + private var _value = Option.empty[A] + + def get: A = _value match { + case Some(value) => value + + case None => + val v = init() + _value = Some(v) + + v + } + + def getSync: A = synchronized(get) + + def getOption: Option[A] = _value +} + +object Lazy { + def apply[A](init: => A): Lazy[A] = new Lazy(() => init) +} diff --git a/src/main/scala/ocelot/desktop/util/OcelotInterfaceLogStorage.scala b/src/main/scala/ocelot/desktop/util/OcelotInterfaceLogStorage.scala index 8d8405b..820e81b 100644 --- a/src/main/scala/ocelot/desktop/util/OcelotInterfaceLogStorage.scala +++ b/src/main/scala/ocelot/desktop/util/OcelotInterfaceLogStorage.scala @@ -22,8 +22,6 @@ trait OcelotInterfaceLogStorage extends EventAware with Persistable with Windowe private var _messageLimit: Int = 1000 - // NOTE: access must be synchronized! - // ocelot.log() is a direct method, so it may push events even if the tick lock is not acquired private val _entries = mutable.ArrayDeque.empty[LogEntry] eventHandlers += { @@ -98,7 +96,7 @@ trait OcelotInterfaceLogStorage extends EventAware with Persistable with Windowe addEntries(entries.toSeq) } - override def save(nbt: NBTTagCompound): Unit = _entries.synchronized { + override def save(nbt: NBTTagCompound): Unit = { super.save(nbt) nbt.setTagList(EntriesTag, _entries.map(saveEntry(_).asInstanceOf[NBTBase]).asJava) @@ -107,36 +105,36 @@ trait OcelotInterfaceLogStorage extends EventAware with Persistable with Windowe def messageLimit: Int = _messageLimit - def messageLimit_=(limit: Int): Unit = _entries.synchronized { + def messageLimit_=(limit: Int): Unit = { require(limit > 0) ensureFreeSpace(_entries.length - limit) _messageLimit = limit } - def entryCount: Int = _entries.synchronized { + def entryCount: Int = { _entries.length } - def clear(): Unit = _entries.synchronized { + def clear(): Unit = { val count = _entries.length _entries.clear() window.onMessagesRemoved(count) } - private def addEntry(entry: LogEntry): Unit = _entries.synchronized { + private def addEntry(entry: LogEntry): Unit = { ensureFreeSpace(1) _entries += entry onMessagesAdded(Some(entry)) } - private def addEntries(entries: Seq[LogEntry]): Unit = _entries.synchronized { + private def addEntries(entries: Seq[LogEntry]): Unit = { ensureFreeSpace(entries.length) val prevCount = _entries.length _entries ++= entries.view.takeRight(messageLimit) onMessagesAdded(_entries.view.takeRight(_entries.length - prevCount)) } - private def ensureFreeSpace(n: Int): Unit = _entries.synchronized { + private def ensureFreeSpace(n: Int): Unit = { val prevCount = _entries.length _entries.takeRightInPlace(messageLimit - n) val removedCount = prevCount - _entries.length diff --git a/src/main/scala/ocelot/desktop/util/Register.scala b/src/main/scala/ocelot/desktop/util/Register.scala new file mode 100644 index 0000000..c3840c2 --- /dev/null +++ b/src/main/scala/ocelot/desktop/util/Register.scala @@ -0,0 +1,64 @@ +package ocelot.desktop.util + +import ocelot.desktop.ui.widget.Updatable + +/** + * Stores a value updated by calls to [[update]]. + */ +trait Register[T] { + /** + * The currently stored value. + */ + def value: T + + /** + * Updates the stored value. + * + * @return `true` if the value has changed. + */ + def update(): Boolean +} + +object Register { + class Writeable[T](initialValue: T) extends Register[T] { + private var _value: T = initialValue + override def value: T = _value + + /** + * The value this register will be set to on next update. + */ + var nextValue: T = _value + + override def update(): Boolean = { + val changed = nextValue != _value + _value = nextValue + + changed + } + } + + class Sampling[T](next: () => T) extends Register[T] { + private var _value = next() + override def value: T = _value + + override def update(): Boolean = { + val nextValue = next() + val changed = nextValue != _value + _value = nextValue + + changed + } + } + + /** + * Creates a [[Register.Writeable writeable register]] with the given initial value. + */ + def apply[T](initialValue: T): Writeable[T] = new Writeable(initialValue) + + /** + * Creates a [[Register.Sampling register]] that reevaluates the provided expression on every update. + * + * The expression is evaluated to compute the initial value. + */ + def sampling[T](nextValue: => T): Sampling[T] = new Sampling(() => nextValue) +} diff --git a/src/main/scala/ocelot/desktop/util/SettingsData.scala b/src/main/scala/ocelot/desktop/util/SettingsData.scala index a277660..dae90ca 100644 --- a/src/main/scala/ocelot/desktop/util/SettingsData.scala +++ b/src/main/scala/ocelot/desktop/util/SettingsData.scala @@ -12,6 +12,7 @@ class SettingsData { def this(data: SettingsData) { this() updateWith(data) + keymap = data.keymap.clone() } var brainCustomConfigPath: Option[String] = None @@ -33,10 +34,13 @@ class SettingsData { var disableVsync: Boolean = false var debugLwjgl: Boolean = false + var keymap: Keymap = new Keymap + var recentWorkspace: Option[String] = None var pinNewWindows: Boolean = true - var unfocusedWindowTransparency: Double = 0.5 + var unfocusedWindowTransparency: Float = 0.5f + var unfocusedWindowHide: Boolean = false var saveOnExit: Boolean = true var autosave: Boolean = true var autosavePeriod: Int = 300 diff --git a/src/main/scala/ocelot/desktop/util/Spritesheet.scala b/src/main/scala/ocelot/desktop/util/Spritesheet.scala index 3e4cf81..b1cd27b 100644 --- a/src/main/scala/ocelot/desktop/util/Spritesheet.scala +++ b/src/main/scala/ocelot/desktop/util/Spritesheet.scala @@ -15,22 +15,22 @@ object Spritesheet extends Resource with Logging { def spriteSize(sprite: String): Size2D = sprites(sprite).size * resolution - def spriteSize(iconSource: IconSource): Size2D = iconSource.animation match { + def spriteSize(icon: IconSource): Size2D = icon.animation match { case Some(animation) => animation.frameSize match { case Some(size) => size case None => - val size = spriteSize(iconSource.path) + val size = spriteSize(icon.path) Size2D(size.width, size.width) } - case None => spriteSize(iconSource.path) + case None => spriteSize(icon.path) } def load(): Unit = { - logger.info("Loading sprites") + logger.info("Loading sprites...") val imageURL = getClass.getResource("/ocelot/desktop/images/spritesheet/spritesheet.png") val image = ImageIO.read(imageURL) diff --git a/src/main/scala/ocelot/desktop/util/animation/ColorAnimation.scala b/src/main/scala/ocelot/desktop/util/animation/ColorAnimation.scala index 08ec2cf..1b90ba1 100644 --- a/src/main/scala/ocelot/desktop/util/animation/ColorAnimation.scala +++ b/src/main/scala/ocelot/desktop/util/animation/ColorAnimation.scala @@ -10,7 +10,7 @@ class ColorAnimation(init: Color, _speed: Float = 6f) { private val b = new ValueAnimation(initRGBA.b, _speed) private val a = new ValueAnimation(initRGBA.a, _speed) - def color: Color = RGBAColorNorm(r.value, g.value, b.value, a.value) + def color: RGBAColorNorm = RGBAColorNorm(r.value, g.value, b.value, a.value) def easing: EasingFunction = a.easing diff --git a/src/main/scala/ocelot/desktop/windows/ComputerWindow.scala b/src/main/scala/ocelot/desktop/windows/ComputerWindow.scala index baacfb6..0968127 100644 --- a/src/main/scala/ocelot/desktop/windows/ComputerWindow.scala +++ b/src/main/scala/ocelot/desktop/windows/ComputerWindow.scala @@ -2,7 +2,7 @@ package ocelot.desktop.windows import ocelot.desktop.audio.{ClickSoundSource, SoundSource} import ocelot.desktop.geometry.{Padding2D, Size2D} -import ocelot.desktop.graphics.Graphics +import ocelot.desktop.graphics.{Graphics, IconSource} import ocelot.desktop.inventory.item.ServerItem import ocelot.desktop.ui.layout.{Layout, LinearLayout} import ocelot.desktop.ui.widget._ @@ -24,8 +24,8 @@ class ComputerWindow(computerAware: ComputerAware) extends BasicWindow { bottomDrawerAnimation.goDown() private val drawerButton = new IconButton( - "buttons/BottomDrawerOpen", - "buttons/BottomDrawerClose", + IconSource.Buttons.BottomDrawerOpen, + IconSource.Buttons.BottomDrawerClose, mode = IconButton.Mode.Switch, darkenActiveColorFactor = 0.2f, tooltip = Some("Toggle computer usage histogram"), @@ -200,8 +200,8 @@ class ComputerWindow(computerAware: ComputerAware) extends BasicWindow { // Power button children :+= new IconButton( - "buttons/PowerOff", - "buttons/PowerOn", + IconSource.Buttons.PowerOff, + IconSource.Buttons.PowerOn, mode = IconButton.Mode.Switch, sizeMultiplier = 2, model = IconButton.ReadOnlyModel(computerAware.computer.machine.isRunning), @@ -277,7 +277,11 @@ class ComputerWindow(computerAware: ComputerAware) extends BasicWindow { override def draw(g: Graphics): Unit = { // Background image g.sprite( - s"window/${if (isServerMachineType) "rack" else "case"}/Motherboard", + if (isServerMachineType) { + IconSource.Window.Rack.Motherboard + } else { + IconSource.Window.Case.Motherboard + }, position.x, position.y, width, diff --git a/src/main/scala/ocelot/desktop/windows/HologramProjectorWindow.scala b/src/main/scala/ocelot/desktop/windows/HologramProjectorWindow.scala index 9699996..b41a234 100644 --- a/src/main/scala/ocelot/desktop/windows/HologramProjectorWindow.scala +++ b/src/main/scala/ocelot/desktop/windows/HologramProjectorWindow.scala @@ -4,6 +4,7 @@ import ocelot.desktop.OcelotDesktop import ocelot.desktop.color.{Color, IntColor} import ocelot.desktop.geometry.FloatUtils.ExtendedFloat import ocelot.desktop.geometry._ +import ocelot.desktop.graphics.IconSource import ocelot.desktop.graphics.mesh.{Mesh3D, MeshBuilder3D} import ocelot.desktop.graphics.scene.{Scene3D, SceneMesh3D} import ocelot.desktop.node.nodes.HologramProjectorNode @@ -138,8 +139,8 @@ class HologramProjectorWindow(val hologramProjectorNode: HologramProjectorNode) super.setupToolbar(toolbar) toolbar.children :+= new IconButton( - "icons/GridOff", - "icons/Grid", + IconSource.Icons.GridOff, + IconSource.Icons.Grid, pressedColor = Color.White.withAlpha(0.5f), releasedColor = Color.White.withAlpha(0.2f), mode = IconButton.Mode.Switch, @@ -174,7 +175,7 @@ class HologramProjectorWindow(val hologramProjectorNode: HologramProjectorNode) val subTick = (System.nanoTime() - workspace.getLastTickNanoTime).toFloat / 1e9f / OcelotDesktop.tpsCounter.dt.max(1e-9f) - val time = workspace.getIngameTime + subTick.clamp(0, 1) + val time = workspace.getIngameTime + subTick.clamped(0, 1) var transform = Transform3D.translate(0.5f, 0.5f, 0.5f) * Transform3D.rotate( diff --git a/src/main/scala/ocelot/desktop/windows/OpenFMRadioWindow.scala b/src/main/scala/ocelot/desktop/windows/OpenFMRadioWindow.scala index 0567389..5976133 100644 --- a/src/main/scala/ocelot/desktop/windows/OpenFMRadioWindow.scala +++ b/src/main/scala/ocelot/desktop/windows/OpenFMRadioWindow.scala @@ -3,7 +3,7 @@ package ocelot.desktop.windows import ocelot.desktop.color.{Color, IntColor} import ocelot.desktop.entity.OpenFMRadio import ocelot.desktop.geometry.{Padding2D, Size2D} -import ocelot.desktop.graphics.Graphics +import ocelot.desktop.graphics.{Graphics, IconSource} import ocelot.desktop.node.nodes.OpenFMRadioNode import ocelot.desktop.ui.layout.{AlignItems, Layout, LinearLayout} import ocelot.desktop.ui.widget._ @@ -28,11 +28,11 @@ class OpenFMRadioWindow(radioNode: OpenFMRadioNode) extends BasicWindow { private def addVolumeUpOrDownButton(isUp: Boolean, x: Float): Unit = { children :+= new PaddingBox( new IconButton( - s"buttons/OpenFMRadioVolume${if (isUp) "Up" else "Down"}Off", - s"buttons/OpenFMRadioVolume${if (isUp) "Up" else "Down"}On", + IconSource.Buttons.OpenFMRadioVolumeOff(isUp), + IconSource.Buttons.OpenFMRadioVolumeOn(isUp), sizeMultiplier = scale, ) { - override def onPressed(): Unit = { + override def onClicked(): Unit = { if (isUp) radio.volUp() else @@ -60,8 +60,8 @@ class OpenFMRadioWindow(radioNode: OpenFMRadioNode) extends BasicWindow { // Redstone button children :+= new PaddingBox( new IconButton( - "buttons/OpenFMRadioRedstoneOff", - "buttons/OpenFMRadioRedstoneOn", + IconSource.Buttons.OpenFMRadioRedstoneOff, + IconSource.Buttons.OpenFMRadioRedstoneOn, mode = IconButton.Mode.Switch, sizeMultiplier = scale, model = new IconButton.Model { override var pressed: Boolean = radio.isListenRedstone }, @@ -72,11 +72,11 @@ class OpenFMRadioWindow(radioNode: OpenFMRadioNode) extends BasicWindow { // Close button children :+= new PaddingBox( new IconButton( - "buttons/OpenFMRadioCloseOff", - "buttons/OpenFMRadioCloseOn", + IconSource.Buttons.OpenFMRadioCloseOff, + IconSource.Buttons.OpenFMRadioCloseOn, sizeMultiplier = scale, ) { - override def onPressed(): Unit = close() + override def onClicked(): Unit = close() }, Padding2D(0, 0, 0, 10), ) @@ -101,8 +101,8 @@ class OpenFMRadioWindow(radioNode: OpenFMRadioNode) extends BasicWindow { // Start/stop button children :+= new PaddingBox( new IconButton( - "buttons/OpenFMRadioStartOff", - "buttons/OpenFMRadioStopOn", + IconSource.Buttons.OpenFMRadioStartOff, + IconSource.Buttons.OpenFMRadioStopOn, mode = IconButton.Mode.Switch, sizeMultiplier = scale, model = IconButton.ReadOnlyModel(radio.isPlaying), @@ -186,7 +186,7 @@ class OpenFMRadioWindow(radioNode: OpenFMRadioNode) extends BasicWindow { override def draw(g: Graphics): Unit = { beginDraw(g) // I hate this - g.sprite("window/OpenFMRadio", position.x, position.y, size.width, size.height) + g.sprite(IconSource.Window.OpenFMRadio, position.x, position.y, size.width, size.height) drawChildren(g) endDraw(g) } diff --git a/src/main/scala/ocelot/desktop/windows/RackWindow.scala b/src/main/scala/ocelot/desktop/windows/RackWindow.scala index 80f7caa..ef7e0a3 100644 --- a/src/main/scala/ocelot/desktop/windows/RackWindow.scala +++ b/src/main/scala/ocelot/desktop/windows/RackWindow.scala @@ -3,8 +3,8 @@ package ocelot.desktop.windows import ocelot.desktop.ColorScheme import ocelot.desktop.audio.{ClickSoundSource, SoundSource} import ocelot.desktop.color.Color -import ocelot.desktop.geometry.{Padding2D, Size2D} -import ocelot.desktop.graphics.Graphics +import ocelot.desktop.geometry.{Padding2D, Rect2D, Size2D} +import ocelot.desktop.graphics.{Graphics, IconSource} import ocelot.desktop.inventory.item.{NetworkCardItem, ServerItem} import ocelot.desktop.node.nodes.RackNode import ocelot.desktop.ui.layout.{Layout, LinearLayout} @@ -25,14 +25,6 @@ class RackWindow(rackNode: RackNode) extends PanelWindow { private val nodeButtonsGap = 12 private val nodeButtonsWidth = 10 - private def directionToSpriteName(prefix: String, direction: Direction.Value): String = prefix + (direction match { - case Direction.Bottom => "Bottom" - case Direction.Top => "Top" - case Direction.Back => "Back" - case Direction.Right => "Right" - case _ => "Left" - }) - private def shouldConnectionBeVisible(slotWidget: RackMountableSlotWidget, connectableIndex: Int) = { connectableIndex == 0 || ( @@ -81,23 +73,11 @@ class RackWindow(rackNode: RackNode) extends PanelWindow { override def draw(g: Graphics): Unit = { // Relay mode line if (rackNode.rack.isRelayEnabled) { - g.sprite( - s"window/rack/NetworkConnector", - position.x + 12, - position.y + 172, - 102, - 4, - ) + g.sprite(IconSource.Window.Rack.Network.Connector, position.x + 12, position.y + 172, 102, 4) } // Lines background - g.sprite( - "window/rack/Lines", - position.x + linesMarginLeft, - position.y, - size.width, - size.height, - ) + g.sprite(IconSource.Window.Rack.Lines, position.x + linesMarginLeft, position.y, size.width, size.height) super.draw(g) } @@ -124,31 +104,28 @@ class RackWindow(rackNode: RackNode) extends PanelWindow { var y = position.y var connectionHeight: Float = 0 - var prefix: String = null for (connectableIndex <- 0 until 4) { connectionHeight = if (connectableIndex == 0) lineSideHeight else lineNetworkHeight if (shouldConnectionBeVisible(mountableSlotWidget, connectableIndex)) { val connection = rackNode.rack.nodeMapping(mountableIndex)(connectableIndex) - prefix = s"window/rack/${if (connectableIndex == 0) "Side" else "Network"}" + val source = if (connectableIndex == 0) { + IconSource.Window.Rack.Side + } else { + IconSource.Window.Rack.Network + } // Connector - g.sprite( - s"${prefix}Connector", - position.x, - y, - 2, - connectionHeight, - ) + g.sprite(source.Connector, position.x, y, 2, connectionHeight) // Line - if (connection.isDefined) { + for (connection <- connection) { g.sprite( - directionToSpriteName(prefix, connection.get), + source.DirectionIcon(connection), position.x + 2, y, - connection.get match { + connection match { case Direction.Bottom => nodeButtonsGap case Direction.Top => nodeButtonsGap * 2 + nodeButtonsWidth case Direction.Back => nodeButtonsGap * 3 + nodeButtonsWidth * 2 @@ -180,10 +157,12 @@ class RackWindow(rackNode: RackNode) extends PanelWindow { val connectableIndex = i val isNetwork = connectableIndex > 0 - children :+= new Button { + children :+= new Button with HoverHighlight { override def enabled: Boolean = shouldConnectionBeVisible(mountableSlotWidget, connectableIndex) + override protected def highlightBounds: Rect2D = bounds.inflated(-2, 0) + override def minimumSize: Size2D = Size2D( nodeButtonsWidth, if (isNetwork) lineNetworkHeight else lineSideHeight, @@ -197,21 +176,14 @@ class RackWindow(rackNode: RackNode) extends PanelWindow { rackNode.rack.connect( mountableIndex, connectableIndex - 1, - // Connection already exists, removing it - if (oldConnection.isDefined && oldConnection.get == direction) - None - // Connecting normally - else - Some(direction), + Option.unless(oldConnection.contains(direction))(direction), ) } override def draw(g: Graphics): Unit = { if (enabled) { - g.sprite( - directionToSpriteName("window/rack/Node", direction), - bounds, - ) + g.sprite(IconSource.Window.Rack.Node.DirectionIcon(direction), bounds) + drawHighlight(g) } } } @@ -257,8 +229,8 @@ class RackWindow(rackNode: RackNode) extends PanelWindow { // Relay enable button children :+= new PaddingBox( new IconButton( - "buttons/RackRelayOff", - "buttons/RackRelayOn", + IconSource.Buttons.RackRelayOff, + IconSource.Buttons.RackRelayOn, mode = IconButton.Mode.Switch, sizeMultiplier = 2, model = IconButton.ReadOnlyModel(rackNode.rack.isRelayEnabled), diff --git a/src/main/scala/ocelot/desktop/windows/RaidWindow.scala b/src/main/scala/ocelot/desktop/windows/RaidWindow.scala index 02544a5..f55787d 100644 --- a/src/main/scala/ocelot/desktop/windows/RaidWindow.scala +++ b/src/main/scala/ocelot/desktop/windows/RaidWindow.scala @@ -3,7 +3,7 @@ package ocelot.desktop.windows import ocelot.desktop.ColorScheme import ocelot.desktop.color.Color import ocelot.desktop.geometry.{Padding2D, Size2D} -import ocelot.desktop.graphics.Graphics +import ocelot.desktop.graphics.{Graphics, IconSource} import ocelot.desktop.node.nodes.RaidNode import ocelot.desktop.ui.layout.{AlignItems, Layout, LinearLayout} import ocelot.desktop.ui.widget._ @@ -37,13 +37,7 @@ class RaidWindow(raidNode: RaidNode) extends PanelWindow { override def draw(g: Graphics): Unit = { // Background border - g.sprite( - "window/raid/Slots", - position.x, - position.y, - size.width, - size.height, - ) + g.sprite(IconSource.Window.Raid.Slots, position, size) super.draw(g) } diff --git a/src/main/scala/ocelot/desktop/windows/ScreenWindow.scala b/src/main/scala/ocelot/desktop/windows/ScreenWindow.scala index d61a25c..46b9d24 100644 --- a/src/main/scala/ocelot/desktop/windows/ScreenWindow.scala +++ b/src/main/scala/ocelot/desktop/windows/ScreenWindow.scala @@ -1,123 +1,63 @@ package ocelot.desktop.windows -import ocelot.desktop.audio.SoundSource -import ocelot.desktop.color.RGBAColor -import ocelot.desktop.geometry.{Rect2D, Size2D, Vector2D} -import ocelot.desktop.graphics.Graphics -import ocelot.desktop.graphics.Texture.MinFilteringMode +import ocelot.desktop.color.RGBAColorNorm +import ocelot.desktop.geometry.{Padding2D, Rect2D, Size2D, Vector2D} +import ocelot.desktop.graphics.{Graphics, IconSource} import ocelot.desktop.node.nodes.ScreenNode import ocelot.desktop.node.nodes.ScreenNode.{FontHeight, FontWidth} import ocelot.desktop.ui.UiHandler -import ocelot.desktop.ui.event.sources.{KeyEvents, MouseEvents} -import ocelot.desktop.ui.event.{DragEvent, KeyEvent, MouseEvent, ScrollEvent} -import ocelot.desktop.ui.widget.window.BasicWindow +import ocelot.desktop.ui.event.sources.KeyEvents +import ocelot.desktop.ui.event.{DragEvent, MouseEvent} +import ocelot.desktop.ui.layout.{AlignItems, LinearLayout} +import ocelot.desktop.ui.widget.window.PanelWindow +import ocelot.desktop.ui.widget.{PaddingBox, ScreenView} import ocelot.desktop.util.{DrawUtils, Logging} import ocelot.desktop.windows.ScreenWindow._ -import ocelot.desktop.{ColorScheme, OcelotDesktop, Settings} -import org.apache.commons.lang3.StringUtils -import org.lwjgl.input.Keyboard -import totoro.ocelot.brain.entity.Screen import totoro.ocelot.brain.nbt.NBTTagCompound -import totoro.ocelot.brain.util.Tier -class ScreenWindow(screenNode: ScreenNode) extends BasicWindow with Logging { - private var lastMousePos = Vector2D(0, 0) - private var sentTouchEvent = false +class ScreenWindow(screenNode: ScreenNode) extends PanelWindow with Logging { private var startingWidth = 0f private var scaleDragPoint: Option[Vector2D] = None - private var _scale = 1f - private var scaleX: Float = 1f - private var scaleY: Float = 1f + override protected def title: String = screenNode.label.get - private def scale: Float = _scale - - private def scale_=(value: Float): Any = { - _scale = value - - scaleX = (FontWidth * scale).floor / FontWidth - scaleY = (FontHeight * scale).floor / FontHeight - } - - private def screen: Screen = screenNode.screen + override protected def titleMaxLength: Int = + ((screenWidth * FontWidth * View.scaleX - 15) / FontWidth).toInt private def screenWidth: Int = screenNode.screenWidth private def screenHeight: Int = screenNode.screenHeight - override def minimumSize: Size2D = Size2D( - screenWidth * FontWidth * scaleX + BorderHorizontal, - screenHeight * scaleY * FontHeight + BorderVertical, - ) - override def receiveScrollEvents: Boolean = true + override def maximumSize: Size2D = minimumSize + + private object View extends ScreenView(screenNode) { + override protected def isFocused: Boolean = ScreenWindow.this.isFocused + } + + setInner( + View, + padding = Padding2D( + right = BorderRight, + bottom = BorderBottom, + left = BorderLeft, + ), + titlePadding = Padding2D( + top = 2, + right = BorderRight - 4, + bottom = 2, + left = BorderLeft - 4, + ), + contentFactory = + (inner, padding) => LinearLayout.WithOptions(align = Some(AlignItems.Center))(new PaddingBox(inner, padding)), + ) + eventHandlers += { - case event: KeyEvent if shouldHandleKeys => - event.state match { - case KeyEvent.State.Press | KeyEvent.State.Repeat => - screen.keyDown(event.char, event.code, OcelotDesktop.player) - - // note: in OpenComputers, key_down signal is fired __before__ clipboard signal - if (event.code == Keyboard.KEY_INSERT) - screen.clipboard(UiHandler.clipboard, OcelotDesktop.player) - - case KeyEvent.State.Release => - screen.keyUp(event.char, event.code, OcelotDesktop.player) - } - - event.consume() - - case event: MouseEvent if shouldHandleKeys => - val pos = convertMousePos(UiHandler.mousePosition) - val inside = checkBounds(pos) - - if (inside) - lastMousePos = pos - - event.state match { - case MouseEvent.State.Press => - if (inside && screen.tier > Tier.One) { - screen.mouseDown(pos.x, pos.y, event.button.id, OcelotDesktop.player) - sentTouchEvent = true - event.consume() - } - - if ( - pinButtonBounds.contains(UiHandler.mousePosition) || closeButtonBounds.contains(UiHandler.mousePosition) - ) { - SoundSource.InterfaceClick.press.play() - } - - case MouseEvent.State.Release => - if (event.button == MouseEvent.Button.Middle && inside) { - screen.clipboard(UiHandler.clipboard, OcelotDesktop.player) - event.consume() - } - - if (sentTouchEvent) { - screen.mouseUp(lastMousePos.x, lastMousePos.y, event.button.id, OcelotDesktop.player) - event.consume() - sentTouchEvent = false - } else if (pinButtonBounds.contains(UiHandler.mousePosition)) { - if (isPinned) unpin() else pin() - SoundSource.InterfaceClick.release.play() - } else if (closeButtonBounds.contains(UiHandler.mousePosition)) { - close() - SoundSource.InterfaceClick.release.play() - } - - case _ => - } - - case event: ScrollEvent if shouldHandleKeys && screen.tier > Tier.One => - screen.mouseScroll(lastMousePos.x, lastMousePos.y, event.offset, OcelotDesktop.player) - event.consume() - case event @ DragEvent(DragEvent.State.Start, MouseEvent.Button.Left, _) => if (scaleDragRegion.contains(event.start)) { scaleDragPoint = Some(event.start) - startingWidth = screenWidth * FontWidth * scaleX + startingWidth = screenWidth * FontWidth * View.scaleX } case DragEvent(DragEvent.State.Drag, MouseEvent.Button.Left, mousePos) => @@ -126,7 +66,7 @@ class ScreenWindow(screenNode: ScreenNode) extends BasicWindow with Logging { val sy = point.y - mousePos.y val uiScale = UiHandler.scalingFactor - var newScale = scale + var newScale = View.scale.nextValue // TODO: refactor this mess, make it consider both sizes and not have two nearby slightly off "snap points" @@ -135,7 +75,7 @@ class ScreenWindow(screenNode: ScreenNode) extends BasicWindow with Logging { val maxWidth = screenWidth * FontWidth var midScale = (newWidth / maxWidth).max(0f) - if (!KeyEvents.isShiftDown && scale <= 1.001) + if (!KeyEvents.isShiftDown && View.scale.nextValue <= 1.001) midScale = midScale.min(1f) val lowScale = (FontWidth * midScale * uiScale).floor / FontWidth / uiScale @@ -147,7 +87,7 @@ class ScreenWindow(screenNode: ScreenNode) extends BasicWindow with Logging { val maxHeight = screenHeight * FontHeight var midScale = (newHeight / maxHeight).max(0f) - if (!KeyEvents.isShiftDown && scale <= 1.001) + if (!KeyEvents.isShiftDown && View.scale.nextValue <= 1.001) midScale = midScale.min(1f) val lowScale = (FontHeight * midScale * uiScale).floor / FontHeight / uiScale @@ -156,28 +96,30 @@ class ScreenWindow(screenNode: ScreenNode) extends BasicWindow with Logging { newScale = if (midScale - lowScale > highScale - midScale) highScale else lowScale } - if (newScale != scale) - scale = newScale - // enforce minimal screen size - if (scale <= 0.249f) { - scale = 0.25f + if (newScale <= 0.249f) { + newScale = 0.25f } + + View.scale.nextValue = newScale } case DragEvent(DragEvent.State.Stop, MouseEvent.Button.Left, _) => scaleDragPoint = None } - private def shouldHandleKeys: Boolean = isFocused && !root.get.modalDialogPool.isVisible - override def save(nbt: NBTTagCompound): Unit = { - nbt.setFloat("scale", scale) + View.save(nbt) super.save(nbt) } + override def load(nbt: NBTTagCompound): Unit = { + super.load(nbt) + View.load(nbt) + } + override def fitToCenter(): Unit = { - scale = math.min( + View.scale.nextValue = math.min( ((UiHandler.root.width * 0.9f) / (screenWidth * FontWidth + BorderHorizontal)).min(1f).max(0f), ((UiHandler.root.height * 0.9f) / (screenHeight * FontHeight + BorderVertical)).min(1f).max(0f), ) @@ -185,36 +127,13 @@ class ScreenWindow(screenNode: ScreenNode) extends BasicWindow with Logging { super.fitToCenter() } - override def load(nbt: NBTTagCompound): Unit = { - scale = nbt.getFloat("scale") - - super.load(nbt) - } - - private def checkBounds(p: Vector2D): Boolean = p.x >= 0 && p.y >= 0 && p.x < screenWidth && p.y < screenHeight - - private def convertMousePos(p: Vector2D): Vector2D = { - // no synchronization here (see the note in ScreenNode): the method to change this property is indirect. - if (screen.getPrecisionMode) { - Vector2D( - (p.x - BorderLeft - position.x) / FontWidth / scaleX, - (p.y - BorderTop - position.y) / FontHeight / scaleY, - ) - } else { - Vector2D( - math.floor((p.x - BorderLeft - position.x) / FontWidth / scaleX), - math.floor((p.y - BorderTop - position.y) / FontHeight / scaleY), - ) - } - } - override protected def dragRegions: Iterator[Rect2D] = Iterator( - Rect2D(position.x, position.y, size.width, BorderTop.toFloat), - Rect2D(position.x, position.y, BorderLeft.toFloat, size.height), + Rect2D(position.x, position.y, size.width, View.position.y - position.y), + Rect2D(position.x, position.y, View.position.x - position.x, size.height), // these two must not include `scaleDragRegion` - Rect2D(position.x, position.y + size.height - BorderBottom, size.width - BorderRight, BorderBottom.toFloat), - Rect2D(position.x + size.width - BorderRight, position.y, BorderRight.toFloat, size.height - BorderBottom), + Rect2D(position.x, View.bounds.max.y, size.width - BorderRight, bounds.max.y - View.bounds.max.y), + Rect2D(View.bounds.max.x, position.y, bounds.max.y - View.bounds.max.y, size.height - BorderBottom), ) private def scaleDragRegion: Rect2D = Rect2D( @@ -228,93 +147,26 @@ class ScreenWindow(screenNode: ScreenNode) extends BasicWindow with Logging { super.update() if (scaleDragPoint.isDefined || scaleDragRegion.contains(UiHandler.mousePosition)) { - root.get.statusBar.addMouseEntry("icons/DragLMB", "Scale screen") - root.get.statusBar.addKeyMouseEntry("icons/DragLMB", "SHIFT", "Scale screen (magnify)") - } - - val currentMousePos = convertMousePos(UiHandler.mousePosition) - if (!checkBounds(currentMousePos) || currentMousePos == lastMousePos) return - - lastMousePos = currentMousePos - - if (isFocused && screen.tier > Tier.One) { - for (button <- MouseEvents.pressedButtons) { - screen.mouseDrag(lastMousePos.x, lastMousePos.y, button.id, OcelotDesktop.player) - } + root.get.statusBar.addMouseEntry(IconSource.Icons.DragLMB, "Scale screen") + root.get.statusBar.addKeyMouseEntry(IconSource.Icons.DragLMB, "SHIFT", "Scale screen (magnify)") } } - private def pinButtonBounds: Rect2D = Rect2D( - position.x + screenWidth * FontWidth * scaleX - 13, - position.y + 3, - 14, - 14, - ) - - private def closeButtonBounds: Rect2D = Rect2D( - position.x + screenWidth * FontWidth * scaleX + 2, - position.y + 3, - 15, - 14, - ) - override def draw(g: Graphics): Unit = { beginDraw(g) - - val startX = position.x + BorderLeft - val startY = position.y + BorderTop - val windowWidth = screenWidth * FontWidth * scaleX - val windowHeight = screenHeight * FontHeight * scaleY - - DrawUtils.shadow(g, startX - 22, startY - 22, windowWidth + 44, windowHeight + 52, 0.5f) - DrawUtils.screenBorder(g, startX, startY, windowWidth, windowHeight) - - // no synchronization here (see the note in ScreenNode): the methods to turn the screen on/off are indirect. - if (screen.getPowerState) { - screenNode.drawScreenData( - g, - startX, - startY, - scaleX, - scaleY, - if (Settings.get.screenWindowMipmap) { - MinFilteringMode.LinearMipmapLinear - } else { - MinFilteringMode.Nearest - }, - ) - - } else { - g.rect(startX, startY, windowWidth, windowHeight, ColorScheme("ScreenOff")) - } - - g.setSmallFont() - g.background = RGBAColor(0, 0, 0, 0) - g.foreground = RGBAColor(110, 110, 110) - - val freeSpace = ((windowWidth - 15) / 8).toInt - val label = screenNode.label.get - val text = if (label.length <= freeSpace) - label - else - StringUtils.substring(label, 0, (freeSpace - 1).max(0).min(label.length)) + "…" - - g.text(startX - 4, startY - 14, text) - g.setNormalFont() - - g.sprite(if (isPinned) "icons/Unpin" else "icons/Pin", pinButtonBounds) - g.sprite("icons/Close", closeButtonBounds) - + DrawUtils.shadow(g, position.x - 8, position.y - 8, size.width + 16, size.height + 20, 0.5f) + DrawUtils.screenBorder(g, bounds, View.bounds, RGBAColorNorm(1, 1, 1)) + drawChildren(g) endDraw(g) } } object ScreenWindow { - private val BorderTop = 20 - private val BorderLeft = 16 - private val BorderRight = 16 - private val BorderBottom = 16 + val BorderTop = 20 + val BorderLeft = 16 + val BorderRight = 16 + val BorderBottom = 16 - private val BorderVertical = BorderTop + BorderBottom - private val BorderHorizontal = BorderLeft + BorderRight + val BorderVertical: Int = BorderTop + BorderBottom + val BorderHorizontal: Int = BorderLeft + BorderRight } diff --git a/src/main/scala/ocelot/desktop/windows/TapeDriveWindow.scala b/src/main/scala/ocelot/desktop/windows/TapeDriveWindow.scala index 9be7ff1..08d097b 100644 --- a/src/main/scala/ocelot/desktop/windows/TapeDriveWindow.scala +++ b/src/main/scala/ocelot/desktop/windows/TapeDriveWindow.scala @@ -3,7 +3,8 @@ package ocelot.desktop.windows import ocelot.desktop.audio.{Audio, ClickSoundSource, SoundBuffers, SoundCategory, SoundSource} import ocelot.desktop.color.{Color, RGBAColorNorm} import ocelot.desktop.geometry.{Padding2D, Size2D} -import ocelot.desktop.graphics.Graphics +import ocelot.desktop.graphics.{Graphics, IconSource} +import ocelot.desktop.graphics.IconSource.Window.Tape.TapeButtonIconSource import ocelot.desktop.inventory.item.TapeItem import ocelot.desktop.node.nodes.TapeDriveNode import ocelot.desktop.ui.layout.{AlignItems, Layout, LinearLayout} @@ -38,12 +39,12 @@ class TapeDriveWindow(val tapeDriveNode: TapeDriveNode) extends PanelWindow { override def draw(g: Graphics): Unit = { // Screen background g.sprite( - "window/tape/Screen", + IconSource.Window.Tape.Screen, bounds, ) // A barely noticeable overlay showing the playback progress - // Btw Computronix doesn't have this feature, so I won't ruin the canon and make it too annoying + // Btw Computronics doesn't have this feature, so I won't ruin the canon and make it too annoying val playedPart = tapeDriveNode.tapeDrive.position.toFloat / tapeDriveNode.tapeDrive.size.toFloat if (playedPart > 0) { @@ -110,13 +111,13 @@ class TapeDriveWindow(val tapeDriveNode: TapeDriveNode) extends PanelWindow { override protected val layout: Layout = new LinearLayout(this, gap = 0) def addButton( - sprite: String, + source: TapeButtonIconSource, pressedState: TapeDriveState.State, isToggle: Boolean = true, ): Unit = { children :+= new IconButton( - s"window/tape/$sprite", - s"window/tape/${sprite}Pressed", + source.Released, + source.Pressed, sizeMultiplier = 2, mode = if (isToggle) @@ -130,16 +131,26 @@ class TapeDriveWindow(val tapeDriveNode: TapeDriveNode) extends PanelWindow { DefaultModel(false) }, ) with HoverHighlight { - override def onPressed(): Unit = tapeDriveNode.tapeDrive.state.switchState(pressedState) + override def onPressed(): Unit = { + if (isToggle) { + tapeDriveNode.tapeDrive.state.switchState(pressedState) + } + } + + override def onClicked(): Unit = { + if (!isToggle) { + tapeDriveNode.tapeDrive.state.switchState(pressedState) + } + } override protected def clickSoundSource: ClickSoundSource = TapeButtonSound } } - addButton("Back", TapeDriveState.State.Rewinding) - addButton("Play", TapeDriveState.State.Playing) - addButton("Stop", TapeDriveState.State.Stopped, isToggle = false) - addButton("Forward", TapeDriveState.State.Forwarding) + addButton(IconSource.Window.Tape.Back, TapeDriveState.State.Rewinding) + addButton(IconSource.Window.Tape.Play, TapeDriveState.State.Playing) + addButton(IconSource.Window.Tape.Stop, TapeDriveState.State.Stopped, isToggle = false) + addButton(IconSource.Window.Tape.Forward, TapeDriveState.State.Forwarding) } }, Padding2D(top = 10, left = 18),