diff --git a/sprites/icons/Cut.png b/sprites/icons/Cut.png new file mode 100644 index 0000000..6b14230 Binary files /dev/null and b/sprites/icons/Cut.png differ diff --git a/sprites/icons/Paste.png b/sprites/icons/Paste.png new file mode 100644 index 0000000..972b4fc Binary files /dev/null and b/sprites/icons/Paste.png differ diff --git a/src/main/resources/ocelot/desktop/colorscheme.txt b/src/main/resources/ocelot/desktop/colorscheme.txt index 4fed976..e5e6022 100644 --- a/src/main/resources/ocelot/desktop/colorscheme.txt +++ b/src/main/resources/ocelot/desktop/colorscheme.txt @@ -68,7 +68,9 @@ TextInputBorderDisabled = #666666 TextInputBorderFocused = #336666 TextInputBackground = #aaaaaa TextInputBackgroundActive = #bbbbbb +TextInputBackgroundSelected = #336666 TextInputForeground = #333333 +TextInputForegroundSelected = #aaaaaa TextInputForegroundDisabled = #888888 TextInputBorderError = #aa8888 TextInputBorderErrorDisabled = #aa8888 diff --git a/src/main/resources/ocelot/desktop/images/spritesheet/spritesheet.png b/src/main/resources/ocelot/desktop/images/spritesheet/spritesheet.png index 0a6028c..214e7a1 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 caa3018..71e1df4 100644 --- a/src/main/resources/ocelot/desktop/images/spritesheet/spritesheet.txt +++ b/src/main/resources/ocelot/desktop/images/spritesheet/spritesheet.txt @@ -47,46 +47,48 @@ 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/Cut 389 632 16 16 +icons/Delete 406 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/EEPROM 423 632 16 16 +icons/Edit 440 632 16 16 +icons/Eject 457 632 16 16 +icons/File 474 632 16 16 +icons/Floppy 491 632 16 16 +icons/Folder 508 632 16 16 +icons/FolderSlash 525 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/Guitar 542 632 16 16 +icons/HDD 559 632 16 16 +icons/Help 576 632 16 16 icons/Home 223 567 22 22 -icons/Keyboard 576 632 16 16 -icons/KeyboardOff 593 632 16 16 +icons/Keyboard 593 632 16 16 +icons/KeyboardOff 610 632 16 16 icons/LMB 453 434 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/Label 627 632 16 16 +icons/LinesHorizontal 644 632 16 16 +icons/Link 661 632 16 16 +icons/LinkSlash 678 632 16 16 +icons/Memory 695 632 16 16 +icons/Microchip 712 632 16 16 +icons/NA 729 632 16 16 icons/NotificationError 477 434 11 11 icons/NotificationInfo 489 434 11 11 icons/NotificationWarning 501 434 11 11 -icons/Ocelot 729 632 16 16 -icons/Pause 746 632 16 16 +icons/Ocelot 746 632 16 16 +icons/Paste 763 632 16 16 +icons/Pause 780 632 16 16 icons/Pin 408 434 14 14 -icons/Play 763 632 16 16 -icons/Plus 780 632 16 16 -icons/Power 797 632 16 16 +icons/Play 797 632 16 16 +icons/Plus 814 632 16 16 +icons/Power 831 632 16 16 icons/RMB 465 434 11 14 -icons/Restart 814 632 16 16 -icons/Save 831 632 16 16 -icons/SaveAs 848 632 16 16 -icons/Server 865 632 16 16 +icons/Restart 848 632 16 16 +icons/Save 865 632 16 16 +icons/SaveAs 882 632 16 16 +icons/Server 899 632 16 16 icons/SettingsKeymap 356 434 12 17 icons/SettingsSound 369 434 12 17 icons/SettingsSystem 382 434 12 17 @@ -100,90 +102,90 @@ icons/SideSouth 573 434 11 11 icons/SideUndefined 585 434 11 11 icons/SideUp 597 434 11 11 icons/SideWest 609 434 11 11 -icons/Tier0 882 632 16 16 -icons/Tier1 899 632 16 16 -icons/Tier2 916 632 16 16 -icons/Tiers 933 632 16 16 +icons/Tier0 916 632 16 16 +icons/Tier1 933 632 16 16 +icons/Tier2 950 632 16 16 +icons/Tiers 967 632 16 16 icons/Unpin 423 434 14 14 -icons/WaveLFSR 254 540 24 10 -icons/WaveNoise 279 540 24 10 -icons/WaveSawtooth 304 540 24 10 -icons/WaveSine 329 540 24 10 -icons/WaveSquare 354 540 24 10 -icons/WaveTriangle 379 540 24 10 -icons/Window 950 632 16 16 +icons/WaveLFSR 288 540 24 10 +icons/WaveNoise 313 540 24 10 +icons/WaveSawtooth 338 540 24 10 +icons/WaveSine 363 540 24 10 +icons/WaveSquare 388 540 24 10 +icons/WaveTriangle 413 540 24 10 +icons/Window 984 632 16 16 icons/WireArrowLeft 281 344 4 8 icons/WireArrowRight 286 344 4 8 items/APU0 49 655 16 96 items/APU1 66 655 16 96 items/APU2 83 655 16 96 -items/CPU0 967 632 16 16 -items/CPU1 984 632 16 16 -items/CPU2 1001 632 16 16 -items/CardBase 358 674 16 16 -items/CircuitBoard 375 674 16 16 -items/ComponentBus0 392 674 16 16 -items/ComponentBus1 409 674 16 16 -items/ComponentBus2 426 674 16 16 -items/ComponentBus3 443 674 16 16 +items/CPU0 1001 632 16 16 +items/CPU1 358 674 16 16 +items/CPU2 375 674 16 16 +items/CardBase 392 674 16 16 +items/CircuitBoard 409 674 16 16 +items/ComponentBus0 426 674 16 16 +items/ComponentBus1 443 674 16 16 +items/ComponentBus2 460 674 16 16 +items/ComponentBus3 477 674 16 16 items/DataCard0 49 526 16 128 items/DataCard1 66 526 16 128 items/DataCard2 83 526 16 128 -items/DebugCard 460 674 16 16 -items/DiskDriveMountable 477 674 16 16 -items/EEPROM 494 674 16 16 -items/FloppyDisk_dyeBlack 511 674 16 16 -items/FloppyDisk_dyeBlue 528 674 16 16 -items/FloppyDisk_dyeBrown 545 674 16 16 -items/FloppyDisk_dyeCyan 562 674 16 16 -items/FloppyDisk_dyeGray 579 674 16 16 -items/FloppyDisk_dyeGreen 596 674 16 16 -items/FloppyDisk_dyeLightBlue 613 674 16 16 -items/FloppyDisk_dyeLightGray 630 674 16 16 -items/FloppyDisk_dyeLime 647 674 16 16 -items/FloppyDisk_dyeMagenta 664 674 16 16 -items/FloppyDisk_dyeOrange 681 674 16 16 -items/FloppyDisk_dyePink 698 674 16 16 -items/FloppyDisk_dyePurple 715 674 16 16 -items/FloppyDisk_dyeRed 732 674 16 16 -items/FloppyDisk_dyeWhite 749 674 16 16 -items/FloppyDisk_dyeYellow 766 674 16 16 -items/GraphicsCard0 783 674 16 16 -items/GraphicsCard1 800 674 16 16 -items/GraphicsCard2 817 674 16 16 -items/HardDiskDrive0 834 674 16 16 -items/HardDiskDrive1 851 674 16 16 -items/HardDiskDrive2 868 674 16 16 +items/DebugCard 494 674 16 16 +items/DiskDriveMountable 511 674 16 16 +items/EEPROM 528 674 16 16 +items/FloppyDisk_dyeBlack 545 674 16 16 +items/FloppyDisk_dyeBlue 562 674 16 16 +items/FloppyDisk_dyeBrown 579 674 16 16 +items/FloppyDisk_dyeCyan 596 674 16 16 +items/FloppyDisk_dyeGray 613 674 16 16 +items/FloppyDisk_dyeGreen 630 674 16 16 +items/FloppyDisk_dyeLightBlue 647 674 16 16 +items/FloppyDisk_dyeLightGray 664 674 16 16 +items/FloppyDisk_dyeLime 681 674 16 16 +items/FloppyDisk_dyeMagenta 698 674 16 16 +items/FloppyDisk_dyeOrange 715 674 16 16 +items/FloppyDisk_dyePink 732 674 16 16 +items/FloppyDisk_dyePurple 749 674 16 16 +items/FloppyDisk_dyeRed 766 674 16 16 +items/FloppyDisk_dyeWhite 783 674 16 16 +items/FloppyDisk_dyeYellow 800 674 16 16 +items/GraphicsCard0 817 674 16 16 +items/GraphicsCard1 834 674 16 16 +items/GraphicsCard2 851 674 16 16 +items/HardDiskDrive0 868 674 16 16 +items/HardDiskDrive1 885 674 16 16 +items/HardDiskDrive2 902 674 16 16 items/InternetCard 143 567 16 32 items/LinkedCard 100 655 16 96 -items/Memory0 885 674 16 16 -items/Memory1 902 674 16 16 -items/Memory2 919 674 16 16 -items/Memory3 936 674 16 16 -items/Memory4 953 674 16 16 -items/Memory5 970 674 16 16 -items/Memory6 987 674 16 16 -items/NetworkCard 1004 674 16 16 +items/Memory0 919 674 16 16 +items/Memory1 936 674 16 16 +items/Memory2 953 674 16 16 +items/Memory3 970 674 16 16 +items/Memory4 987 674 16 16 +items/Memory5 1004 674 16 16 +items/Memory6 134 707 16 16 +items/NetworkCard 151 707 16 16 items/OcelotCard 100 526 16 128 -items/RedstoneCard0 134 707 16 16 -items/RedstoneCard1 151 707 16 16 +items/RedstoneCard0 168 707 16 16 +items/RedstoneCard1 185 707 16 16 items/SelfDestructingCard 160 567 16 32 -items/Server0 168 707 16 16 -items/Server1 185 707 16 16 -items/Server2 202 707 16 16 -items/Server3 219 707 16 16 +items/Server0 202 707 16 16 +items/Server1 219 707 16 16 +items/Server2 236 707 16 16 +items/Server3 253 707 16 16 items/SoundCard 117 526 16 128 -items/TapeCopper 236 707 16 16 -items/TapeDiamond 253 707 16 16 -items/TapeGold 270 707 16 16 -items/TapeGreg 287 707 16 16 -items/TapeIg 304 707 16 16 -items/TapeIron 321 707 16 16 -items/TapeNetherStar 338 707 16 16 -items/TapeSteel 355 707 16 16 -items/WirelessNetworkCard0 372 707 16 16 -items/WirelessNetworkCard1 389 707 16 16 -light-panel/BookmarkLeft 235 540 18 14 +items/TapeCopper 270 707 16 16 +items/TapeDiamond 287 707 16 16 +items/TapeGold 304 707 16 16 +items/TapeGreg 321 707 16 16 +items/TapeIg 338 707 16 16 +items/TapeIron 355 707 16 16 +items/TapeNetherStar 372 707 16 16 +items/TapeSteel 389 707 16 16 +items/WirelessNetworkCard0 406 707 16 16 +items/WirelessNetworkCard1 423 707 16 16 +light-panel/BookmarkLeft 269 540 18 14 light-panel/BookmarkRight 197 600 20 14 light-panel/BorderB 296 344 4 4 light-panel/BorderL 284 353 4 2 @@ -196,94 +198,94 @@ light-panel/CornerTR 326 344 4 4 light-panel/Fill 410 344 2 2 light-panel/Vent 279 305 2 38 nodes/Cable 300 316 8 8 -nodes/Camera 406 707 16 16 +nodes/Camera 440 707 16 16 nodes/Chest 438 434 14 14 -nodes/HologramProjector0 423 707 16 16 -nodes/HologramProjector1 440 707 16 16 -nodes/IronNoteBlock 457 707 16 16 -nodes/Lamp 474 707 16 16 -nodes/LampFrame 491 707 16 16 +nodes/HologramProjector0 457 707 16 16 +nodes/HologramProjector1 474 707 16 16 +nodes/IronNoteBlock 491 707 16 16 +nodes/Lamp 508 707 16 16 +nodes/LampFrame 525 707 16 16 nodes/LampGlow 49 305 128 128 -nodes/NewNode 508 707 16 16 -nodes/NoteBlock 525 707 16 16 -nodes/OpenFMRadio 542 707 16 16 -nodes/Relay 559 707 16 16 -nodes/TapeDrive 576 707 16 16 -nodes/computer/Default 593 707 16 16 -nodes/computer/DiskActivity 610 707 16 16 -nodes/computer/Error 627 707 16 16 -nodes/computer/On 644 707 16 16 -nodes/disk-drive/Default 661 707 16 16 -nodes/disk-drive/DiskActivity 678 707 16 16 -nodes/disk-drive/Floppy 695 707 16 16 +nodes/NewNode 542 707 16 16 +nodes/NoteBlock 559 707 16 16 +nodes/OpenFMRadio 576 707 16 16 +nodes/Relay 593 707 16 16 +nodes/TapeDrive 610 707 16 16 +nodes/computer/Default 627 707 16 16 +nodes/computer/DiskActivity 644 707 16 16 +nodes/computer/Error 661 707 16 16 +nodes/computer/On 678 707 16 16 +nodes/disk-drive/Default 695 707 16 16 +nodes/disk-drive/DiskActivity 712 707 16 16 +nodes/disk-drive/Floppy 729 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 712 707 16 16 -nodes/microcontroller/Error 729 707 16 16 -nodes/microcontroller/On 746 707 16 16 +nodes/microcontroller/Default 746 707 16 16 +nodes/microcontroller/Error 763 707 16 16 +nodes/microcontroller/On 780 707 16 16 nodes/ocelot-block/Default 117 655 16 80 -nodes/ocelot-block/Rx 763 707 16 16 -nodes/ocelot-block/Tx 780 707 16 16 -nodes/rack/Default 797 707 16 16 -nodes/rack/Empty 814 707 16 16 -nodes/rack/drive/0/Default 831 707 16 16 -nodes/rack/drive/0/DiskActivity 848 707 16 16 -nodes/rack/drive/0/Floppy 865 707 16 16 -nodes/rack/drive/1/Default 882 707 16 16 -nodes/rack/drive/1/DiskActivity 899 707 16 16 -nodes/rack/drive/1/Floppy 916 707 16 16 -nodes/rack/drive/2/Default 933 707 16 16 -nodes/rack/drive/2/DiskActivity 950 707 16 16 -nodes/rack/drive/2/Floppy 967 707 16 16 -nodes/rack/drive/3/Default 984 707 16 16 -nodes/rack/drive/3/DiskActivity 1001 707 16 16 -nodes/rack/drive/3/Floppy 266 655 16 16 -nodes/rack/drive/Floppy 283 655 16 16 -nodes/rack/server/0/Default 300 655 16 16 -nodes/rack/server/0/DiskActivity 317 655 16 16 -nodes/rack/server/0/Error 334 655 16 16 -nodes/rack/server/0/NetworkActivity 351 655 16 16 -nodes/rack/server/0/On 368 655 16 16 -nodes/rack/server/1/Default 385 655 16 16 -nodes/rack/server/1/DiskActivity 402 655 16 16 -nodes/rack/server/1/Error 419 655 16 16 -nodes/rack/server/1/NetworkActivity 436 655 16 16 -nodes/rack/server/1/On 453 655 16 16 -nodes/rack/server/2/Default 470 655 16 16 -nodes/rack/server/2/DiskActivity 487 655 16 16 -nodes/rack/server/2/Error 504 655 16 16 -nodes/rack/server/2/NetworkActivity 521 655 16 16 -nodes/rack/server/2/On 538 655 16 16 -nodes/rack/server/3/Default 555 655 16 16 -nodes/rack/server/3/DiskActivity 572 655 16 16 -nodes/rack/server/3/Error 589 655 16 16 -nodes/rack/server/3/NetworkActivity 606 655 16 16 -nodes/rack/server/3/On 623 655 16 16 -nodes/raid/0/DiskActivity 640 655 16 16 -nodes/raid/0/Error 657 655 16 16 -nodes/raid/1/DiskActivity 674 655 16 16 -nodes/raid/1/Error 691 655 16 16 -nodes/raid/2/DiskActivity 708 655 16 16 -nodes/raid/2/Error 725 655 16 16 -nodes/raid/Default 742 655 16 16 -nodes/screen/BottomLeft 759 655 16 16 -nodes/screen/BottomMiddle 776 655 16 16 -nodes/screen/BottomRight 793 655 16 16 -nodes/screen/ColumnBottom 810 655 16 16 -nodes/screen/ColumnMiddle 827 655 16 16 -nodes/screen/ColumnTop 844 655 16 16 -nodes/screen/Middle 861 655 16 16 -nodes/screen/MiddleLeft 878 655 16 16 -nodes/screen/MiddleRight 895 655 16 16 -nodes/screen/PowerOnOverlay 912 655 16 16 -nodes/screen/RowLeft 929 655 16 16 -nodes/screen/RowMiddle 946 655 16 16 -nodes/screen/RowRight 963 655 16 16 -nodes/screen/Standalone 980 655 16 16 -nodes/screen/TopLeft 997 655 16 16 -nodes/screen/TopMiddle 201 540 16 16 -nodes/screen/TopRight 218 540 16 16 +nodes/ocelot-block/Rx 797 707 16 16 +nodes/ocelot-block/Tx 814 707 16 16 +nodes/rack/Default 831 707 16 16 +nodes/rack/Empty 848 707 16 16 +nodes/rack/drive/0/Default 865 707 16 16 +nodes/rack/drive/0/DiskActivity 882 707 16 16 +nodes/rack/drive/0/Floppy 899 707 16 16 +nodes/rack/drive/1/Default 916 707 16 16 +nodes/rack/drive/1/DiskActivity 933 707 16 16 +nodes/rack/drive/1/Floppy 950 707 16 16 +nodes/rack/drive/2/Default 967 707 16 16 +nodes/rack/drive/2/DiskActivity 984 707 16 16 +nodes/rack/drive/2/Floppy 1001 707 16 16 +nodes/rack/drive/3/Default 266 655 16 16 +nodes/rack/drive/3/DiskActivity 283 655 16 16 +nodes/rack/drive/3/Floppy 300 655 16 16 +nodes/rack/drive/Floppy 317 655 16 16 +nodes/rack/server/0/Default 334 655 16 16 +nodes/rack/server/0/DiskActivity 351 655 16 16 +nodes/rack/server/0/Error 368 655 16 16 +nodes/rack/server/0/NetworkActivity 385 655 16 16 +nodes/rack/server/0/On 402 655 16 16 +nodes/rack/server/1/Default 419 655 16 16 +nodes/rack/server/1/DiskActivity 436 655 16 16 +nodes/rack/server/1/Error 453 655 16 16 +nodes/rack/server/1/NetworkActivity 470 655 16 16 +nodes/rack/server/1/On 487 655 16 16 +nodes/rack/server/2/Default 504 655 16 16 +nodes/rack/server/2/DiskActivity 521 655 16 16 +nodes/rack/server/2/Error 538 655 16 16 +nodes/rack/server/2/NetworkActivity 555 655 16 16 +nodes/rack/server/2/On 572 655 16 16 +nodes/rack/server/3/Default 589 655 16 16 +nodes/rack/server/3/DiskActivity 606 655 16 16 +nodes/rack/server/3/Error 623 655 16 16 +nodes/rack/server/3/NetworkActivity 640 655 16 16 +nodes/rack/server/3/On 657 655 16 16 +nodes/raid/0/DiskActivity 674 655 16 16 +nodes/raid/0/Error 691 655 16 16 +nodes/raid/1/DiskActivity 708 655 16 16 +nodes/raid/1/Error 725 655 16 16 +nodes/raid/2/DiskActivity 742 655 16 16 +nodes/raid/2/Error 759 655 16 16 +nodes/raid/Default 776 655 16 16 +nodes/screen/BottomLeft 793 655 16 16 +nodes/screen/BottomMiddle 810 655 16 16 +nodes/screen/BottomRight 827 655 16 16 +nodes/screen/ColumnBottom 844 655 16 16 +nodes/screen/ColumnMiddle 861 655 16 16 +nodes/screen/ColumnTop 878 655 16 16 +nodes/screen/Middle 895 655 16 16 +nodes/screen/MiddleLeft 912 655 16 16 +nodes/screen/MiddleRight 929 655 16 16 +nodes/screen/PowerOnOverlay 946 655 16 16 +nodes/screen/RowLeft 963 655 16 16 +nodes/screen/RowMiddle 980 655 16 16 +nodes/screen/RowRight 997 655 16 16 +nodes/screen/Standalone 201 540 16 16 +nodes/screen/TopLeft 218 540 16 16 +nodes/screen/TopMiddle 235 540 16 16 +nodes/screen/TopRight 252 540 16 16 panel/BorderB 331 344 4 4 panel/BorderL 289 353 4 2 panel/BorderR 336 344 4 4 diff --git a/src/main/scala/ocelot/desktop/util/Font.scala b/src/main/scala/ocelot/desktop/graphics/Font.scala similarity index 91% rename from src/main/scala/ocelot/desktop/util/Font.scala rename to src/main/scala/ocelot/desktop/graphics/Font.scala index fd174a8..8b8ce6f 100644 --- a/src/main/scala/ocelot/desktop/util/Font.scala +++ b/src/main/scala/ocelot/desktop/graphics/Font.scala @@ -1,7 +1,7 @@ -package ocelot.desktop.util +package ocelot.desktop.graphics import ocelot.desktop.geometry.Rect2D -import ocelot.desktop.graphics.Texture +import ocelot.desktop.util.{Logging, Resource} import org.lwjgl.opengl.GL11 import totoro.ocelot.brain.util.FontUtils @@ -15,8 +15,8 @@ import scala.io.{Codec, Source} class Font(val name: String, val fontSize: Int) extends Resource with Logging { val AtlasWidth = 4096 val AtlasHeight = 4096 - var glyphCount = 0 - var outOfRangeGlyphCount = 0 + private var glyphCount = 0 + private var outOfRangeGlyphCount = 0 private val atlas: BufferedImage = { val icmArr = Array(0.toByte, 0xff.toByte) @@ -134,3 +134,14 @@ class Font(val name: String, val fontSize: Int) extends Resource with Logging { texture.freeResource() } } + +object Font extends Resource { + val NormalFont = new Font("unscii-16", 16) + val SmallFont = new Font("unscii-8", 8) + + override def freeResource(): Unit = { + super.freeResource() + NormalFont.freeResource() + SmallFont.freeResource() + } +} diff --git a/src/main/scala/ocelot/desktop/graphics/Graphics.scala b/src/main/scala/ocelot/desktop/graphics/Graphics.scala index cab960c..51542a4 100644 --- a/src/main/scala/ocelot/desktop/graphics/Graphics.scala +++ b/src/main/scala/ocelot/desktop/graphics/Graphics.scala @@ -6,7 +6,7 @@ import ocelot.desktop.graphics.Texture.MinFilteringMode import ocelot.desktop.graphics.mesh.{Mesh2D, MeshInstance2D, MeshVertex2D} import ocelot.desktop.graphics.render.InstanceRenderer import ocelot.desktop.ui.UiHandler -import ocelot.desktop.util.{Font, Logging, Resource, Spritesheet} +import ocelot.desktop.util.{Logging, Resource, Spritesheet} import org.lwjgl.BufferUtils import org.lwjgl.opengl.{ARBFramebufferObject, GL11, GL21, GL30} @@ -25,9 +25,7 @@ class Graphics(private var width: Int, private var height: Int, private var scal private val shaderProgram = new ShaderProgram("general") private val renderer = new InstanceRenderer[MeshVertex2D, MeshInstance2D](Mesh2D.quad, MeshInstance2D, shaderProgram) - private[graphics] val normalFont = new Font("unscii-16", 16) - private val smallFont = new Font("unscii-8", 8) - private var _font: Font = normalFont + private var _font: Font = Font.NormalFont private var oldFont: Font = _font private val stack = mutable.Stack[GraphicsState](GraphicsState()) @@ -87,8 +85,6 @@ class Graphics(private var width: Int, private var height: Int, private var scal offscreenTexture.freeResource() Spritesheet.freeResource() - smallFont.freeResource() - normalFont.freeResource() renderer.freeResource() shaderProgram.freeResource() screenShaderProgram.freeResource() @@ -97,15 +93,15 @@ class Graphics(private var width: Int, private var height: Int, private var scal def font: Font = _font def setNormalFont(): Unit = { - if (_font == normalFont) return + if (_font == Font.NormalFont) return flush() - _font = normalFont + _font = Font.NormalFont } def setSmallFont(): Unit = { - if (_font == smallFont) return + if (_font == Font.SmallFont) return flush() - _font = smallFont + _font = Font.SmallFont } def save(): Unit = { diff --git a/src/main/scala/ocelot/desktop/graphics/IconSource.scala b/src/main/scala/ocelot/desktop/graphics/IconSource.scala index b14d19f..8457566 100644 --- a/src/main/scala/ocelot/desktop/graphics/IconSource.scala +++ b/src/main/scala/ocelot/desktop/graphics/IconSource.scala @@ -205,6 +205,8 @@ object IconSource { val Delete: IconSource = get("Delete") val Label: IconSource = get("Label") val Copy: IconSource = get("Copy") + val Cut: IconSource = get("Cut") + val Paste: IconSource = get("Paste") val AspectRatio: IconSource = get("AspectRatio") val Eject: IconSource = get("Eject") val Restart: IconSource = get("Restart") diff --git a/src/main/scala/ocelot/desktop/graphics/ScreenViewport.scala b/src/main/scala/ocelot/desktop/graphics/ScreenViewport.scala index 54bb181..a154aa9 100644 --- a/src/main/scala/ocelot/desktop/graphics/ScreenViewport.scala +++ b/src/main/scala/ocelot/desktop/graphics/ScreenViewport.scala @@ -4,7 +4,7 @@ import ocelot.desktop.color.{Color, RGBAColorNorm} import ocelot.desktop.geometry.Transform2D import ocelot.desktop.graphics.mesh.{Mesh2D, MeshInstance2D, MeshVertex2D} import ocelot.desktop.graphics.render.InstanceRenderer -import ocelot.desktop.util.{Font, Resource, Spritesheet} +import ocelot.desktop.util.{Resource, Spritesheet} import org.lwjgl.opengl.{ARBFramebufferObject, GL11, GL21, GL30} import java.nio.ByteBuffer @@ -19,7 +19,7 @@ class ScreenViewport(graphics: Graphics, private var _width: Int, private var _h private val shaderProgram = graphics.screenShaderProgram private val renderer = new InstanceRenderer[MeshVertex2D, MeshInstance2D](Mesh2D.quad, MeshInstance2D, shaderProgram) - private val _font = graphics.normalFont + private val _font = Font.NormalFont private val spriteRect = Spritesheet.sprites("Empty") private val emptySpriteTrans = diff --git a/src/main/scala/ocelot/desktop/ui/UiHandler.scala b/src/main/scala/ocelot/desktop/ui/UiHandler.scala index 17da776..37e4a80 100644 --- a/src/main/scala/ocelot/desktop/ui/UiHandler.scala +++ b/src/main/scala/ocelot/desktop/ui/UiHandler.scala @@ -3,7 +3,7 @@ package ocelot.desktop.ui 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.graphics.{Font, Graphics} import ocelot.desktop.ui.event.handlers.HoverHandler import ocelot.desktop.ui.event.sources.{BrainEvents, KeyEvents, MouseEvents, ScrollEvents} import ocelot.desktop.ui.event.{Capturing, CapturingEvent, Dispatchable, HoverEvent, MouseEvent} @@ -80,7 +80,7 @@ object UiHandler extends Logging { _clipboard.getData(DataFlavor.stringFlavor).toString } catch { case _: UnsupportedFlavorException => - logger.debug("Trying to paste non-Unicode content from clipboard! Replaced with empty string.") + logger.debug("Trying to paste non-Unicode content from clipboard! Replaced with an empty string.") "" } } @@ -418,6 +418,7 @@ object UiHandler extends Logging { KeyEvents.destroy() MouseEvents.destroy() graphics.freeResource() + Font.freeResource() Audio.removeAllSources() SoundBuffers.freeResource() Display.destroy() @@ -430,10 +431,9 @@ object UiHandler extends Logging { } } - 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 dispatchCapturing(dispatchOrder: DispatchOrder)(event: CapturingEvent): Unit = { + dispatchEvent(dispatchOrder.capture)(Capturing(event)) + dispatchEvent(dispatchOrder.targets.reverseIterator)(event) } private def dispatchBrainEvents(): Unit = { @@ -474,15 +474,13 @@ object UiHandler extends Logging { MouseEvents.releaseButtons() } + val broadcastDispatchOrder = DispatchOrder.broadcast + // TODO: dispatch to the focused widget instead of broadcasting to the entire hierarchy. for (event <- KeyEvents.events) { - dispatchEvent(hierarchy)(Capturing(event)) - dispatchEvent()(event) + dispatchCapturing(broadcastDispatchOrder)(event) } - MouseEvents.events - .foreach(dispatchEvent(hierarchy.reverseIterator.filter(w => w.enabled && w.receiveAllMouseEvents))) - val scrollTarget = hierarchy.reverseIterator .find(w => w.receiveScrollEvents && w.clippedBounds.contains(mousePos)) @@ -490,30 +488,33 @@ object UiHandler extends Logging { .find(w => w.enabled && w.receiveMouseEvents && w.clippedBounds.contains(mousePos)) for (scrollTarget <- scrollTarget) { - ScrollEvents.events.foreach(dispatchCapturing(scrollTarget)) + ScrollEvents.events.foreach(dispatchCapturing(DispatchOrder.resolve(scrollTarget))) } + val mouseEventDispatchOrder = DispatchOrder.resolve( + hierarchy + .iterator + .filter(w => mouseTarget.contains(w) || w.enabled && w.receiveAllMouseEvents) + .toSeq + ) + for (event <- MouseEvents.events) { if (event.state == MouseEvent.State.Pressed) { - for (mouseTarget <- mouseTarget) { - dispatchCapturing(mouseTarget)(event) - } + dispatchCapturing(mouseEventDispatchOrder)(event) } else { - dispatchEvent(hierarchy)(Capturing(event)) - dispatchEvent(hierarchy.reverseIterator)(event) + dispatchCapturing(broadcastDispatchOrder)(event) } } - hierarchy.reverseIterator.foreach { - case h: HoverHandler if !mouseTarget.contains(h) && h._mouseOver.update(false) => - dispatchCapturing(h)(HoverEvent(HoverEvent.State.Leave)) + val hoverLeaveDispatchOrder = DispatchOrder.resolve(hierarchy.iterator.collect({ + case h: HoverHandler if !mouseTarget.contains(h) && h._mouseOver.update(false) => h + }).toSeq) - case _ => - } + dispatchCapturing(hoverLeaveDispatchOrder)(HoverEvent(HoverEvent.State.Leave)) mouseTarget.foreach { case h: HoverHandler if h._mouseOver.update(true) => - dispatchCapturing(h)(HoverEvent(HoverEvent.State.Enter)) + dispatchCapturing(DispatchOrder.resolve(h))(HoverEvent(HoverEvent.State.Enter)) case _ => } @@ -548,4 +549,43 @@ object UiHandler extends Logging { graphics.flush() graphics.update() } + + /** + * Provides a dispatch order for [[CapturingEvent]]s. + * @param capture widgets to deliver events during capture phase, in hierarchy pre-order. + * @param targets widgets to deliver events during target phase, in hierarchy pre-order. + */ + private case class DispatchOrder(capture: collection.Seq[Widget], targets: collection.Seq[Widget]) + + private object DispatchOrder { + /** + * Creates a dispatch order for delivering events to multiple targets. + * @param targets event targets, in hierarchy pre-order. + */ + def resolve(targets: collection.Seq[Widget]): DispatchOrder = { + val capture = mutable.Set.empty[Widget] + + for (widget <- targets) { + capture += widget + val ancestors = widget.ancestors + while (ancestors.hasNext && capture.add(ancestors.next())) {} + } + + DispatchOrder( + capture = hierarchy.iterator.filter(capture).toSeq, + targets = targets, + ) + } + + /** + * Creates a dispatch order for delivering event to a single `target`. + */ + def resolve(target: Widget): DispatchOrder = { + val capture = (Iterator(target) ++ target.ancestors.iterator).toSeq + + DispatchOrder(capture, Seq(target)) + } + + def broadcast: DispatchOrder = DispatchOrder(hierarchy, hierarchy) + } } diff --git a/src/main/scala/ocelot/desktop/ui/event/DoubleClickEvent.scala b/src/main/scala/ocelot/desktop/ui/event/DoubleClickEvent.scala new file mode 100644 index 0000000..de2c60b --- /dev/null +++ b/src/main/scala/ocelot/desktop/ui/event/DoubleClickEvent.scala @@ -0,0 +1,5 @@ +package ocelot.desktop.ui.event + +import ocelot.desktop.geometry.Vector2D + +case class DoubleClickEvent(button: MouseEvent.Button.Value, mousePos: Vector2D) extends Event diff --git a/src/main/scala/ocelot/desktop/ui/event/MouseEvent.scala b/src/main/scala/ocelot/desktop/ui/event/MouseEvent.scala index 2ac0bda..5f27c6b 100644 --- a/src/main/scala/ocelot/desktop/ui/event/MouseEvent.scala +++ b/src/main/scala/ocelot/desktop/ui/event/MouseEvent.scala @@ -1,5 +1,8 @@ package ocelot.desktop.ui.event +case class MouseEvent(state: MouseEvent.State.Value, button: MouseEvent.Button.Value)(val stateChanged: Boolean) + extends CapturingEvent + object MouseEvent { object State extends Enumeration { val Pressed, Released = Value @@ -10,6 +13,14 @@ object MouseEvent { val Right: Button.Value = Value(1) val Middle: Button.Value = Value(2) } -} -case class MouseEvent(state: MouseEvent.State.Value, button: MouseEvent.Button.Value) extends CapturingEvent + object StateChanged { + def unapply(event: MouseEvent): Option[(State.Value, Button.Value)] = { + if (event.stateChanged) { + MouseEvent.unapply(event) + } else { + None + } + } + } +} 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 05c4465..b4ee378 100644 --- a/src/main/scala/ocelot/desktop/ui/event/handlers/MouseHandler.scala +++ b/src/main/scala/ocelot/desktop/ui/event/handlers/MouseHandler.scala @@ -2,8 +2,8 @@ package ocelot.desktop.ui.event.handlers import ocelot.desktop.geometry.Vector2D import ocelot.desktop.ui.UiHandler -import ocelot.desktop.ui.event.handlers.MouseHandler.Tolerance -import ocelot.desktop.ui.event.{ClickEvent, DragEvent, MouseEvent} +import ocelot.desktop.ui.event.handlers.MouseHandler.{DoubleClickTime, Tolerance} +import ocelot.desktop.ui.event.{ClickEvent, DoubleClickEvent, DragEvent, MouseEvent} import ocelot.desktop.ui.widget.Widget import scala.collection.mutable @@ -13,13 +13,23 @@ trait MouseHandler extends Widget { private val prevPositions = new mutable.HashMap[MouseEvent.Button.Value, Vector2D]() private val dragButtons = new mutable.HashSet[MouseEvent.Button.Value]() + private var lastClickPosition: Vector2D = Vector2D.Zero + private var lastClickTime: Long = 0 + private var lastClickButton = MouseEvent.Button.Left + override def receiveMouseEvents: Boolean = receiveClickEvents || receiveDragEvents protected def receiveClickEvents: Boolean = false protected def receiveDragEvents: Boolean = false - /** If `true`, a [[ClickEvent]] will be registered even if the mouse button is released + /** + * If `true`, drag events, once they start, will be sent on every update cycle even if the mouse does not move. + */ + protected def spamDragEvents: Boolean = true + + /** + * If `true`, a [[ClickEvent]] will be registered even if the mouse button is released * outside the tolerance threshold, as long as it stays within the widget's bounds. */ protected def allowClickReleaseOutsideThreshold: Boolean = !receiveDragEvents @@ -39,7 +49,7 @@ trait MouseHandler extends Widget { if (allowClickReleaseOutsideThreshold) { clippedBounds.contains(mousePos) } else { - (p - mousePos).lengthSquared < Tolerance * Tolerance + withinTolerance(p, mousePos) } }) ) @@ -50,11 +60,23 @@ trait MouseHandler extends Widget { if (clicked) { handleEvent(ClickEvent(button, mousePos)) + + val inTimeForDoubleClick = (System.currentTimeMillis() - lastClickTime) < DoubleClickTime * 1000 + val sameButton = lastClickButton == button + val roughlySamePosition = withinTolerance(lastClickPosition, mousePos) + if (inTimeForDoubleClick && sameButton && roughlySamePosition) { + handleEvent(DoubleClickEvent(button, mousePos)) + } + lastClickTime = System.currentTimeMillis() + lastClickPosition = mousePos + lastClickButton = button } startPositions.remove(button) } + private def withinTolerance(a: Vector2D, b: Vector2D): Boolean = (b - a).lengthSquared < Tolerance * Tolerance + override def update(): Unit = { super.update() @@ -65,14 +87,14 @@ trait MouseHandler extends Widget { val mousePos = UiHandler.mousePosition for ((button, startPos) <- startPositions) { - if (!dragButtons.contains(button) && (startPos - mousePos).lengthSquared > Tolerance * Tolerance) { + if (!dragButtons.contains(button) && !withinTolerance(startPos, mousePos)) { handleEvent(DragEvent(DragEvent.State.Start, button, mousePos, startPos, Vector2D(0, 0))) dragButtons += button prevPositions += (button -> mousePos) } } - dragButtons.foreach(button => { + for (button <- dragButtons if spamDragEvents || prevPositions(button) != mousePos) { handleEvent( DragEvent( DragEvent.State.Drag, @@ -82,12 +104,13 @@ trait MouseHandler extends Widget { mousePos - prevPositions(button), ) ) + prevPositions(button) = mousePos } - ) } } object MouseHandler { private val Tolerance = 8 + private val DoubleClickTime = 0.2 } 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 4f299aa..69f78b4 100644 --- a/src/main/scala/ocelot/desktop/ui/event/sources/MouseEvents.scala +++ b/src/main/scala/ocelot/desktop/ui/event/sources/MouseEvents.scala @@ -26,13 +26,13 @@ object MouseEvents { if (MouseEvent.Button.values.map(_.id).contains(buttonIdx)) { val button = MouseEvent.Button(buttonIdx) val state = if (Mouse.getEventButtonState) MouseEvent.State.Pressed else MouseEvent.State.Released - _events += MouseEvent(state, button) - state match { + val changed = state match { case MouseEvent.State.Pressed => - _pressedButtons += button + _pressedButtons.add(button) case MouseEvent.State.Released => - _pressedButtons -= button + _pressedButtons.remove(button) } + _events += MouseEvent(state, button)(changed) } val delta = Mouse.getEventDWheel @@ -49,7 +49,7 @@ object MouseEvents { def releaseButtons(): Unit = { for (button <- pressedButtons) { - _events += MouseEvent(MouseEvent.State.Released, button) + _events += MouseEvent(MouseEvent.State.Released, button)(stateChanged = true) } _pressedButtons.clear() diff --git a/src/main/scala/ocelot/desktop/ui/widget/Button.scala b/src/main/scala/ocelot/desktop/ui/widget/Button.scala index 98807cc..a2e5e77 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/Button.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/Button.scala @@ -16,8 +16,8 @@ class Button(tooltip: Option[Tooltip] = None) extends Widget with MouseHandler w def this(tooltip: Tooltip) = this(Some(tooltip)) protected def colorScheme: ColorScheme = ColorScheme.General - override protected val hoverAnimationColorDefault: Color = colorScheme("ButtonBackground") - override protected val hoverAnimationColorActive: Color = colorScheme("ButtonBackgroundActive") + override protected val HoverAnimationColorDefault: Color = colorScheme("ButtonBackground") + override protected val HoverAnimationColorActive: Color = colorScheme("ButtonBackgroundActive") def text: String = "" diff --git a/src/main/scala/ocelot/desktop/ui/widget/ChangeSimulationSpeedDialog.scala b/src/main/scala/ocelot/desktop/ui/widget/ChangeSimulationSpeedDialog.scala index bc4d1ad..b775a38 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/ChangeSimulationSpeedDialog.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/ChangeSimulationSpeedDialog.scala @@ -45,7 +45,8 @@ class ChangeSimulationSpeedDialog extends ModalDialog { override def onInput(text: String): Unit = { tickInterval = parseInput(text).map { interval => - inputTPS.setInput(formatTPS(interval)) + val tps = formatTPS(interval) + if (inputTPS.text != tps) inputTPS.text = tps interval } } @@ -66,7 +67,8 @@ class ChangeSimulationSpeedDialog extends ModalDialog { override def onInput(text: String): Unit = { tickInterval = parseInput(text).map { interval => - inputMSPT.setInput(formatMSPT(interval)) + val mspt = formatMSPT(interval) + if (inputMSPT.text != mspt) inputMSPT.text = mspt interval } } diff --git a/src/main/scala/ocelot/desktop/ui/widget/Checkbox.scala b/src/main/scala/ocelot/desktop/ui/widget/Checkbox.scala index c0e9dad..4885392 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/Checkbox.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/Checkbox.scala @@ -12,8 +12,8 @@ import ocelot.desktop.util.DrawUtils class Checkbox(val label: String, val initialValue: Boolean = false, val isSmall: Boolean = false) extends Widget with MouseHandler with HoverAnimation { - override protected val hoverAnimationColorDefault: Color = ColorScheme("CheckboxBackground") - override protected val hoverAnimationColorActive: Color = ColorScheme("CheckboxBackgroundActive") + override protected val HoverAnimationColorDefault: Color = ColorScheme("CheckboxBackground") + override protected val HoverAnimationColorActive: Color = ColorScheme("CheckboxBackgroundActive") private var _checked: Boolean = initialValue diff --git a/src/main/scala/ocelot/desktop/ui/widget/Slider.scala b/src/main/scala/ocelot/desktop/ui/widget/Slider.scala index ac6062d..218cfce 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/Slider.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/Slider.scala @@ -14,8 +14,8 @@ import ocelot.desktop.util.DrawUtils class Slider(var value: Float, val text: String, val snapPoints: Int = 0) extends Widget with MouseHandler with HoverAnimation { - override protected val hoverAnimationColorDefault: Color = ColorScheme("SliderBackground") - override protected val hoverAnimationColorActive: Color = ColorScheme("SliderBackgroundActive") + override protected val HoverAnimationColorDefault: Color = ColorScheme("SliderBackground") + override protected val HoverAnimationColorActive: Color = ColorScheme("SliderBackgroundActive") def onValueChanged(value: Float): Unit = {} def onValueFinal(value: Float): Unit = {} diff --git a/src/main/scala/ocelot/desktop/ui/widget/TextInput.scala b/src/main/scala/ocelot/desktop/ui/widget/TextInput.scala index 5b0bc9a..4f35a7b 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/TextInput.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/TextInput.scala @@ -3,22 +3,71 @@ package ocelot.desktop.ui.widget import ocelot.desktop.ColorScheme import ocelot.desktop.color.Color import ocelot.desktop.geometry.Size2D -import ocelot.desktop.graphics.Graphics +import ocelot.desktop.graphics.{Font, Graphics, IconSource} import ocelot.desktop.ui.UiHandler import ocelot.desktop.ui.event.handlers.MouseHandler import ocelot.desktop.ui.event.sources.KeyEvents -import ocelot.desktop.ui.event.{ClickEvent, KeyEvent, MouseEvent} +import ocelot.desktop.ui.event.{DoubleClickEvent, DragEvent, KeyEvent, MouseEvent} +import ocelot.desktop.ui.widget.TextInput.{Cursor, Selection, Text} +import ocelot.desktop.ui.widget.contextmenu.{ContextMenu, ContextMenuEntry} import ocelot.desktop.ui.widget.traits.HoverAnimation -import ocelot.desktop.util.DrawUtils +import ocelot.desktop.util.{DrawUtils, Register, Watcher} import ocelot.desktop.util.animation.ColorAnimation import org.lwjgl.input.Keyboard -import scala.collection.mutable.ArrayBuffer +import java.lang.Character.isWhitespace class TextInput(val initialText: String = "") extends Widget with MouseHandler with HoverAnimation { - override protected val hoverAnimationColorDefault: Color = ColorScheme("TextInputBackground") - override protected val hoverAnimationColorActive: Color = ColorScheme("TextInputBackgroundActive") + private val CursorBlinkTime = 2f + private val PlaceholderForegroundColor: Color = ColorScheme("TextInputForegroundDisabled") + private val BackgroundSelectedColor: Color = ColorScheme("TextInputBackgroundSelected") + private val ForegroundSelectedColor: Color = ColorScheme("TextInputForegroundSelected") + override protected val HoverAnimationColorDefault: Color = ColorScheme("TextInputBackground") + override protected val HoverAnimationColorActive: Color = ColorScheme("TextInputBackgroundActive") + // model + private val _text: Text = new Text(initialText.codePoints().toArray) + private val cursor: Cursor = new Cursor() + private val selectionWatcher = new Watcher[Option[Selection]](None) + + // updated after all events are processed so that event handlers can refer to the previous position. + private val prevCursorPosition = Register.sampling(cursor.position) + + // view + private var isFocused = false + private var scroll = 0f + private var blinkTimer = 0f + private var cursorOffset = 0f + private var selectionOffsets: Option[(Int, Int)] = None + + private val enabledRegister = Register.sampling(enabled) + + private def selection: Option[Selection] = selectionWatcher.value + private def selection_=(newValue: Option[Selection]): Unit = { + selectionWatcher.value = newValue + } + + cursor.onChange(position => { + cursorOffset = charsWidth(_text.chars, 0, position) + blinkTimer = 0 + adjustScroll() + }) + + selectionWatcher.onChange(newValue => { + selectionOffsets = newValue.map { + case Selection.Ordered(start, end) => + ( + charsWidth(_text.chars, 0, start), + charsWidth(_text.chars, 0, end), + ) + } + }) + + private val foregroundAnimation = new ColorAnimation(targetForegroundColor, 7f) + private val borderAnimation = new ColorAnimation(targetBorderColor, 7f) + + // public API + // ------------------------------------------------------------------------------------------------------------------- def onInput(text: String): Unit = {} def onConfirm(): Unit = { @@ -28,125 +77,28 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w def validator(text: String): Boolean = true final def isInputValid: Boolean = validator(text) - var isFocused = false - - def text: String = chars.mkString - def text_=(value: String): Unit = chars = value.toCharArray - - protected var placeholder: Array[Char] = "".toCharArray - def placeholder_=(value: String): Unit = placeholder = value.toCharArray - - private var cursorPos = 0 - private var cursorOffset = 0f - private var scroll = 0f - private val CursorBlinkTime = 2f - private var blinkTimer = 0f - private var chars = initialText.toCharArray - private var textWidth = 0 - private var textChanged = false - private val events = ArrayBuffer[TextEvent]() - - override protected def receiveClickEvents: Boolean = true - - // TODO: implement text selection - // override protected def receiveDragEvents: Boolean = true - - override protected def allowClickReleaseOutsideThreshold: Boolean = false - - override def receiveAllMouseEvents = true - - private var prevEnabled = enabled - - eventHandlers += { - case MouseEvent(MouseEvent.State.Released, MouseEvent.Button.Left) => - if (isFocused && !clippedBounds.contains(UiHandler.mousePosition)) { - unfocus() - } - - case ClickEvent(MouseEvent.Button.Left, pos) if enabled => - focus() - events += new CursorMoveTo(pos.x) - - case event @ KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_LEFT, _) if isFocused => - events += new CursorMoveLeft - event.consume() - - case event @ KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_RIGHT, _) if isFocused => - events += new CursorMoveRight - event.consume() - - case event @ KeyEvent(KeyEvent.State.Press, Keyboard.KEY_HOME, _) if isFocused => - events += new CursorMoveStart - event.consume() - - case event @ KeyEvent(KeyEvent.State.Press, Keyboard.KEY_END, _) if isFocused => - events += new CursorMoveEnd - event.consume() - - case event @ KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_BACK, _) if isFocused => - events += new EraseCharBack - event.consume() - - case event @ KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_DELETE, _) if isFocused => - events += new EraseCharFront - event.consume() - - case event @ KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_INSERT, _) if isFocused => - // TODO: insert the whole clipboard string at once instead of going char-by-char. - for (char <- UiHandler.clipboard.toCharArray) { - events += new WriteChar(char) - } - - event.consume() - - case event @ KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_V, _) - if isFocused - && KeyEvents.isControlDown => - - // TODO: insert the whole clipboard string at once instead of going char-by-char. - for (char <- UiHandler.clipboard.toCharArray) { - events += new WriteChar(char) - } - - event.consume() - - case event @ KeyEvent(KeyEvent.State.Press, Keyboard.KEY_RETURN, _) if isFocused => - onConfirm() - event.consume() - - case event @ KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, _, char) if isFocused && !char.isControl => - events += new WriteChar(char) - event.consume() + def text: String = new String(_text.chars, 0, _text.chars.length) + def text_=(value: String): Unit = { + _text.chars = value.codePoints().toArray + selection = None + cursor.position = cursor.position max 0 min _text.chars.length } - def setInput(text: String): Unit = { - this.chars = text.toCharArray - cursorPos = 0 - cursorOffset = 0 - textWidth = 0 - textChanged = true + private def selectedText: String = selection match { + case Some(Selection.Ordered(start, end)) => new String(_text.chars, start, end) + case None => "" } - override def minimumSize: Size2D = Size2D(200, 24) - override def maximumSize: Size2D = Size2D(Float.PositiveInfinity, 24) - - private val foregroundAnimation = new ColorAnimation(targetForegroundColor, 7f) - private val borderAnimation = new ColorAnimation(targetBorderColor, 7f) - - private def updateAnimationTargets(): Unit = { - foregroundAnimation.goto(targetForegroundColor) - borderAnimation.goto(targetBorderColor) - } + protected var placeholder: Array[Int] = Array.empty + def placeholder_=(value: String): Unit = placeholder = value.codePoints().toArray def focus(): Unit = { if (!isFocused) { if (enabled) { isFocused = true } - updateAnimationTargets() } - blinkTimer = 0 } @@ -157,8 +109,298 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w } } + // widget management + // ------------------------------------------------------------------------------------------------------------------- + override def minimumSize: Size2D = Size2D(200, 24) + override def maximumSize: Size2D = Size2D(Float.PositiveInfinity, 24) + + override def receiveAllMouseEvents = true + override protected def receiveDragEvents: Boolean = true + override protected def receiveClickEvents: Boolean = true + override protected def spamDragEvents: Boolean = false + + private def mouseInBounds: Boolean = clippedBounds.contains(UiHandler.mousePosition) + + protected def font: Font = Font.NormalFont + + private def charWidth(codePoint: Int): Int = font.charWidth(codePoint) + + /** + * Calculates given text width in pixels. + * @param from inclusive + * @param to exclusive + */ + // noinspection SameParameterValue + private def charsWidth(chars: Array[Int], from: Int, to: Int): Int = { + var width = 0 + for (index <- (from max 0) until (to min chars.length)) { + width += font.charWidth(chars(index)) + } + width + } + + eventHandlers += { + case MouseEvent.StateChanged(MouseEvent.State.Pressed, _) if enabled => + val inBounds = mouseInBounds + if (isFocused && !inBounds) unfocus() + if (!isFocused && inBounds) focus() + + if (isFocused) { + val pos = pixelToCursorPosition(UiHandler.mousePosition.x - bounds.x) + cursor.position = pos + } + } + + eventHandlers += { + case MouseEvent.StateChanged(MouseEvent.State.Pressed, MouseEvent.Button.Right) if isFocused && mouseInBounds => + val menu = new ContextMenu + + if (selection.nonEmpty) { + menu.addEntry(ContextMenuEntry("Cut", IconSource.Icons.Cut) { cutSelection() }) + menu.addEntry(ContextMenuEntry("Copy", IconSource.Icons.Copy) { copySelection() }) + } + + if (UiHandler.clipboard.nonEmpty) { + menu.addEntry(ContextMenuEntry("Paste", IconSource.Icons.Paste) { pasteSelection() }) + } + + if (_text.chars.nonEmpty) { + if (menu.children.nonEmpty) { + menu.addSeparator() + } + + menu.addEntry(ContextMenuEntry("Select all") { selectAll() }) + } + + root.get.contextMenus.open(menu) + + case MouseEvent.StateChanged(MouseEvent.State.Pressed, _) if isFocused && mouseInBounds => + selection = None + + case DoubleClickEvent(MouseEvent.Button.Left, _) if isFocused && mouseInBounds => + selectWord() + + case DragEvent(DragEvent.State.Start | DragEvent.State.Drag, MouseEvent.Button.Left, mouse) if isFocused => + val pos = if (mouse.y < bounds.y) { + 0 + } else if (mouse.y > bounds.max.y) { + _text.chars.length + } else { + pixelToCursorPosition(mouse.x - bounds.x) + } + + extendSelection(pos, prevCursorPosition.value) + cursor.position = pos + + case event @ KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_LEFT, _) if isFocused => + handleKeyMovement(selection match { + case Some(Selection.Ordered(start, _)) if !KeyEvents.isShiftDown => start + case _ => cursor.position - 1 + }) + + event.consume() + + case event @ KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_RIGHT, _) if isFocused => + handleKeyMovement(selection match { + case Some(Selection.Ordered(_, end)) if !KeyEvents.isShiftDown => end + case _ => cursor.position + 1 + }) + + event.consume() + + case event @ KeyEvent(KeyEvent.State.Press, Keyboard.KEY_HOME, _) if isFocused => + handleKeyMovement(0) + event.consume() + + case event @ KeyEvent(KeyEvent.State.Press, Keyboard.KEY_END, _) if isFocused => + handleKeyMovement(_text.chars.length) + event.consume() + + case event @ KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_BACK, _) if isFocused => + if (selection.nonEmpty) { + deleteSelection() + } else { + val (lhs, rhs) = _text.chars.splitAt(cursor.position) + if (!lhs.isEmpty) { + _text.chars = lhs.take(lhs.length - 1) ++ rhs + cursor.position -= 1 + } + } + + event.consume() + + case event @ KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_DELETE, _) if isFocused => + if (selection.nonEmpty) { + deleteSelection() + } else { + val (lhs, rhs) = _text.chars.splitAt(cursor.position) + if (!rhs.isEmpty) { + _text.chars = lhs ++ rhs.drop(1) + } + } + + event.consume() + + case event @ KeyEvent(KeyEvent.State.Press, Keyboard.KEY_A, _) + if isFocused && KeyEvents.isControlDown => + selectAll() + event.consume() + + case event @ KeyEvent(KeyEvent.State.Press, Keyboard.KEY_W, _) + if isFocused && KeyEvents.isControlDown => + selectWord() + event.consume() + + case event @ KeyEvent(KeyEvent.State.Press, Keyboard.KEY_C, _) + if isFocused && KeyEvents.isControlDown && selection.nonEmpty => + copySelection() + event.consume() + + case event @ KeyEvent(KeyEvent.State.Press, Keyboard.KEY_X, _) + if isFocused && KeyEvents.isControlDown && selection.nonEmpty => + cutSelection() + event.consume() + + case event @ KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_INSERT, _) if isFocused => + pasteSelection() + event.consume() + + case event @ KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_V, _) + if isFocused && KeyEvents.isControlDown => + pasteSelection() + event.consume() + + case event @ KeyEvent(KeyEvent.State.Press, Keyboard.KEY_RETURN, _) if isFocused => + onConfirm() + event.consume() + + case event @ KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, _, char) if isFocused && !char.isControl => + if (selection.nonEmpty) { + deleteSelection() + } + + writeChar(char) + event.consume() + } + + /** + * Will try to find out which cursor position corresponds to the point on the screen. + * @param x widget-local coordinate + */ + private def pixelToCursorPosition(x: Float): Int = { + val absoluteX = x + scroll - 4 + var width = 0 + var pos = 0 + while (width < absoluteX && pos < _text.chars.length) { + width += charWidth(_text.chars(pos)) + if (width < absoluteX) pos += 1 + } + pos + } + + private def extendSelection(position: Int, cursorPosition: Int = cursor.position): Unit = { + selection = Selection(selection.fold(cursorPosition)(_.start), position) + } + + private def handleKeyMovement(position: Int): Unit = { + if (KeyEvents.isShiftDown) { + extendSelection(position) + } else { + selection = None + } + + cursor.position = position + } + + private def selectAll(): Unit = { + selection = Selection(0, _text.chars.length) + } + + private def selectWord(): Unit = { + val from = 0 max (_text.chars.lastIndexWhere(isWhitespace, cursor.position - 1) + 1) + val to = _text.chars.indexWhere(isWhitespace, cursor.position) + val clampedTo = if (to >= 0 && to < _text.chars.length) to else _text.chars.length + selection = Selection(from, clampedTo) + } + + private def copySelection(): Unit = { + UiHandler.clipboard = selectedText + } + + private def cutSelection(): Unit = { + UiHandler.clipboard = selectedText + deleteSelection() + } + + private def pasteSelection(): Unit = { + if (selection.nonEmpty) deleteSelection() + writeString(UiHandler.clipboard) + } + + private def deleteSelection(): Unit = { + for (Selection.Ordered(start, end) <- selection) { + _text.chars = _text.chars.take(start) ++ _text.chars.drop(end) + cursor.position = start + } + } + + private def writeString(string: String): Unit = { + val (lhs, rhs) = _text.chars.splitAt(cursor.position) + val array = string.codePoints().toArray + _text.chars = lhs ++ array ++ rhs + cursor.position += array.length + } + + private def writeChar(codePoint: Int): Unit = { + val (lhs, rhs) = _text.chars.splitAt(cursor.position) + _text.chars = lhs ++ Array(codePoint) ++ rhs + cursor.position += 1 + } + + /** + * Apply a set of corrections to the scroll to make sure the cursor and text stay visible + */ + private def adjustScroll(): Unit = { + // make cursor visible + if (cursorOffset < scroll) scroll = cursorOffset + if (cursorOffset - scroll > size.width - 16) scroll = cursorOffset - size.width + 16 + // apply pressure from the left (to maximize visible text, for nicer editing experience) + val fullTextWidth = charsWidth(_text.chars, 0, _text.chars.length) + val areaWidth = size.width - 16 + if (fullTextWidth > areaWidth && fullTextWidth - scroll < areaWidth) scroll = fullTextWidth - areaWidth + } + + override def update(): Unit = { + super.update() + + // process state changes + if (_text.changed()) { + onInput(text) + updateAnimationTargets() + adjustScroll() + } + + if (enabledRegister.update()) { + updateAnimationTargets() + } + if (isFocused && !enabled) { + unfocus() + } + + // update everything + prevCursorPosition.update() + foregroundAnimation.update() + borderAnimation.update() + blinkTimer = (blinkTimer + UiHandler.dt) % CursorBlinkTime + } + + private def updateAnimationTargets(): Unit = { + foregroundAnimation.goto(targetForegroundColor) + borderAnimation.goto(targetBorderColor) + } + private def targetBorderColor: Color = ColorScheme( - if (validator(chars.mkString)) { + if (validator(text)) { if (isFocused) "TextInputBorderFocused" else if (!enabled) "TextInputBorderDisabled" else "TextInputBorder" @@ -168,39 +410,33 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w else "TextInputBorderError" } ) - private def targetForegroundColor: Color = ColorScheme( if (!enabled) "TextInputForegroundDisabled" else "TextInputForeground" ) - private val placeholderForegroundColor: Color = ColorScheme("TextInputForegroundDisabled") - override def draw(g: Graphics): Unit = { - if (textWidth == 0f && chars.nonEmpty) - textWidth = chars.map(g.font.charWidth(_)).sum - - textChanged = false - for (event <- events) event.handle(g) - events.clear() - - if (textChanged) { - val str = chars.mkString - onInput(str) - updateAnimationTargets() - } - g.rect(bounds, hoverAnimation.color) DrawUtils.ring(g, position.x, position.y, size.width, size.height, thickness = 2, borderAnimation.color) g.setScissor(position.x + 4, position.y, size.width - 8f, size.height) + for ((start, end) <- selectionOffsets) { + val width = end - start + g.rect(position.x + start + 8 - scroll, position.y + 4, width, size.height - 8, BackgroundSelectedColor) + } + g.background = Color.Transparent - g.foreground = if (chars.nonEmpty || isFocused) foregroundAnimation.color else placeholderForegroundColor + val foreground = if (_text.chars.nonEmpty || isFocused) foregroundAnimation.color else PlaceholderForegroundColor + g.foreground = foreground var charOffset = 0 - val charsToDisplay = if (chars.nonEmpty || isFocused) chars else placeholder + val charsToDisplay = if (_text.chars.nonEmpty || isFocused) _text.chars else placeholder for (char <- charsToDisplay) { + for ((start, end) <- selectionOffsets) { + g.foreground = if (charOffset >= start && charOffset < end) ForegroundSelectedColor else foreground + } + g.char(position.x + 8 + charOffset - scroll, position.y + 4, char) charOffset += g.font.charWidth(char) } @@ -209,149 +445,36 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w g.rect(position.x + 7 + cursorOffset - scroll, position.y + 4, 2, 16, borderAnimation.color) } } +} - override def update(): Unit = { - super.update() - - val nextEnabled = enabled - - if (nextEnabled != prevEnabled) { - updateAnimationTargets() - prevEnabled = nextEnabled - } - - if (isFocused && !enabled) { - unfocus() - } - - foregroundAnimation.update() - borderAnimation.update() - blinkTimer = (blinkTimer + UiHandler.dt) % CursorBlinkTime +object TextInput { + class Text(initialValue: Array[Int]) extends Watcher(initialValue) { + def chars: Array[Int] = value + def chars_=(newValue: Array[Int]): Unit = value = newValue } - private def charWidth(g: Graphics, c: Char): Int = g.font.charWidth(c) - - // noinspection SameParameterValue - private def charsWidth(g: Graphics, chars: Array[Char], first: Int, last: Int): Int = { - var width = 0 - for (index <- first to last) { - width += g.font.charWidth(chars(index)) - } - width + class Cursor(initialValue: Int = 0) extends Watcher(initialValue) { + def position: Int = value + def position_=(newValue: Int): Unit = value = newValue } - private def adjustScroll(): Unit = { - if (cursorOffset < scroll) - scroll = cursorOffset - if (cursorOffset - scroll > size.width - 16) - scroll = cursorOffset - size.width + 16 + case class Selection(start: Int, end: Int) { + require(start != end) } - private abstract class TextEvent { - def handle(g: Graphics): Unit - } - - // TODO: refactor this mess. have actions only move the cursor position explicitly. - // then calculate the cursor offset (incl. the scroll offset) based on that automatically - // rather than incrementally in the actions. - private class CursorMoveLeft extends TextEvent { - override def handle(g: Graphics): Unit = { - if (cursorPos == 0) return - - cursorOffset -= charWidth(g, chars(cursorPos - 1)) - cursorPos -= 1 - blinkTimer = 0 - - adjustScroll() - } - } - - private class CursorMoveRight extends TextEvent { - override def handle(g: Graphics): Unit = { - if (cursorPos >= chars.length) return - - cursorOffset += charWidth(g, chars(cursorPos)) - cursorPos += 1 - blinkTimer = 0 - - adjustScroll() - } - } - - private class CursorMoveStart extends TextEvent { - override def handle(g: Graphics): Unit = { - cursorPos = 0 - cursorOffset = 0 - blinkTimer = 0 - scroll = 0 - } - } - - private class CursorMoveEnd extends TextEvent { - override def handle(g: Graphics): Unit = { - cursorPos = chars.length - cursorOffset = textWidth - blinkTimer = 0 - scroll = (textWidth - size.width + 16).max(0) - } - } - - private class CursorMoveTo(mouseX: Float) extends TextEvent { - override def handle(g: Graphics): Unit = { - val absoluteX = mouseX - bounds.x + scroll - 4 - var width = 0 - var pos = 0 - while (width < absoluteX && pos < chars.length) { - width += g.font.charWidth(chars(pos)) - if (width < absoluteX) pos += 1 + object Selection { + def apply(start: Int, end: Int): Option[Selection] = { + Option.when(start != end) { + new Selection(start, end) } - - cursorPos = chars.length.min(pos).max(0) - cursorOffset = if (cursorPos > 0) charsWidth(g, chars, 0, (cursorPos - 1).max(0)) else 0 - blinkTimer = 0 - adjustScroll() } - } - private class EraseCharBack extends TextEvent { - override def handle(g: Graphics): Unit = { - val (lhs, rhs) = chars.splitAt(cursorPos) - if (lhs.isEmpty) return + object Ordered { + def unapply(selection: Selection): Some[(Int, Int)] = { + val Selection(start, end) = selection - val cw = charWidth(g, lhs.last) - chars = lhs.take(lhs.length - 1) ++ rhs - textChanged = true - textWidth -= cw - cursorOffset -= cw - cursorPos -= 1 - blinkTimer = 0 - scroll = (scroll - cw).max(0) - } - } - - private class EraseCharFront extends TextEvent { - override def handle(g: Graphics): Unit = { - val (lhs, rhs) = chars.splitAt(cursorPos) - if (rhs.isEmpty) return - - val cw = charWidth(g, rhs.head) - chars = lhs ++ rhs.drop(1) - textChanged = true - textWidth -= cw - blinkTimer = 0 - - if (rhs.drop(1).map(charWidth(g, _)).sum < size.width - 16) - scroll = (scroll - cw).max(0) - } - } - - private class WriteChar(char: Char) extends TextEvent { - override def handle(g: Graphics): Unit = { - val (lhs, rhs) = chars.splitAt(cursorPos) - chars = lhs ++ Array(char) ++ rhs - textChanged = true - textWidth += charWidth(g, char) - (new CursorMoveRight).handle(g) + Some((start min end, start max end)) + } } } } 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 a371ac3..966870b 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/contextmenu/ContextMenuEntry.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/contextmenu/ContextMenuEntry.scala @@ -69,9 +69,12 @@ class ContextMenuEntry( override protected def receiveClickEvents: Boolean = true eventHandlers += { - case MouseEvent(MouseEvent.State.Pressed, MouseEvent.Button.Left) if !contextMenu.isOpening => + case event @ MouseEvent(MouseEvent.State.Pressed, MouseEvent.Button.Left) if !contextMenu.isOpening => clickSoundSource.press.play() - case ClickEvent(MouseEvent.Button.Left, _) if !contextMenu.isOpening => clicked() + event.consume() + case event @ ClickEvent(MouseEvent.Button.Left, _) if !contextMenu.isOpening => + clicked() + event.consume() case HoverEvent(HoverEvent.State.Enter) => enter() case HoverEvent(HoverEvent.State.Leave) if !isGhost => leave() } 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 acdd3f1..2a21ace 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/settings/SystemSettingsTab.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/settings/SystemSettingsTab.scala @@ -158,7 +158,7 @@ class SystemSettingsTab extends SettingsTab with Logging { private def setConfigPath(path: String): Unit = { Settings.get.brainCustomConfigPath = Some(path) - textInput.setInput(path) + textInput.text = path restartWarning.isVisible = true } 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 3d28082..7ef20ba 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/statusbar/StatusBar.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/statusbar/StatusBar.scala @@ -157,8 +157,8 @@ class StatusBar extends Widget { override def receiveMouseEvents: Boolean = true - override protected val hoverAnimationColorActive: Color = ColorScheme("StatusBarActive") - override protected val hoverAnimationColorDefault: Color = hoverAnimationColorActive.toRGBANorm.withAlpha(0) + override protected val HoverAnimationColorActive: Color = ColorScheme("StatusBarActive") + override protected val HoverAnimationColorDefault: Color = HoverAnimationColorActive.toRGBANorm.withAlpha(0) override def draw(g: Graphics): Unit = { g.rect(bounds, hoverAnimation.color) diff --git a/src/main/scala/ocelot/desktop/ui/widget/traits/HoverAnimation.scala b/src/main/scala/ocelot/desktop/ui/widget/traits/HoverAnimation.scala index 7bcd827..0ded682 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/traits/HoverAnimation.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/traits/HoverAnimation.scala @@ -13,23 +13,23 @@ import ocelot.desktop.util.animation.ColorAnimation */ trait HoverAnimation extends Widget with EventAware with HoverHandler with Updatable { //noinspection ScalaWeakerAccess - protected val hoverAnimationSpeedEnter: Float = AnimationSpeedHoverEnter + protected val HoverAnimationSpeedEnter: Float = AnimationSpeedHoverEnter //noinspection ScalaWeakerAccess - protected val hoverAnimationSpeedLeave: Float = AnimationSpeedHoverLeave - protected val hoverAnimationColorDefault: Color = ColorScheme("ButtonBackground") - protected val hoverAnimationColorActive: Color = ColorScheme("ButtonBackgroundActive") + protected val HoverAnimationSpeedLeave: Float = AnimationSpeedHoverLeave + protected val HoverAnimationColorDefault: Color = ColorScheme("ButtonBackground") + protected val HoverAnimationColorActive: Color = ColorScheme("ButtonBackgroundActive") protected lazy val hoverAnimation: ColorAnimation = - new ColorAnimation(hoverAnimationColorDefault, hoverAnimationSpeedEnter) + new ColorAnimation(HoverAnimationColorDefault, HoverAnimationSpeedEnter) eventHandlers += { case Capturing(HoverEvent(HoverEvent.State.Enter)) => - hoverAnimation.speed = hoverAnimationSpeedEnter - hoverAnimation.goto(hoverAnimationColorActive) + hoverAnimation.speed = HoverAnimationSpeedEnter + hoverAnimation.goto(HoverAnimationColorActive) case Capturing(HoverEvent(HoverEvent.State.Leave)) => - hoverAnimation.speed = hoverAnimationSpeedLeave - hoverAnimation.goto(hoverAnimationColorDefault) + hoverAnimation.speed = HoverAnimationSpeedLeave + hoverAnimation.goto(HoverAnimationColorDefault) } override def update(): Unit = { diff --git a/src/main/scala/ocelot/desktop/util/Register.scala b/src/main/scala/ocelot/desktop/util/Register.scala index 6bb2757..8b37097 100644 --- a/src/main/scala/ocelot/desktop/util/Register.scala +++ b/src/main/scala/ocelot/desktop/util/Register.scala @@ -1,7 +1,5 @@ package ocelot.desktop.util -import ocelot.desktop.ui.widget.Updatable - /** * Stores a value updated by calls to [[update]]. */ diff --git a/src/main/scala/ocelot/desktop/util/Watcher.scala b/src/main/scala/ocelot/desktop/util/Watcher.scala new file mode 100644 index 0000000..0e8831a --- /dev/null +++ b/src/main/scala/ocelot/desktop/util/Watcher.scala @@ -0,0 +1,33 @@ +package ocelot.desktop.util + +/** + * Keeps a reference to an object + * and tells whether there were any changes to the value since the last check. + */ +class Watcher[T](initialValue: T) { + private var dirty = false + private var _callback: Option[T => Unit] = None + private var _value: T = initialValue + + def value: T = _value + def value_=(newValue: T): Unit = { + dirty = _value != newValue + if (dirty) { + _callback.foreach(_(newValue)) + } + _value = newValue + } + + def onChange(callback: T => Unit): Unit = _callback = Some(callback) + + def changed(): Boolean = { + if (dirty) { + dirty = false + true + } else false + } +} + +object Watcher { + def apply[T](value: T) = new Watcher(value) +} diff --git a/src/main/scala/ocelot/desktop/windows/OcelotInterfaceWindow.scala b/src/main/scala/ocelot/desktop/windows/OcelotInterfaceWindow.scala index 39aa839..985481d 100644 --- a/src/main/scala/ocelot/desktop/windows/OcelotInterfaceWindow.scala +++ b/src/main/scala/ocelot/desktop/windows/OcelotInterfaceWindow.scala @@ -24,7 +24,7 @@ class OcelotInterfaceWindow(storage: OcelotInterfaceLogStorage) extends PanelWin children :+= new TextInput() { override def onConfirm(): Unit = { pushLine(text) - setInput("") + text = "" } }