mirror of
https://gitlab.com/cc-ru/ocelot/ocelot-desktop.git
synced 2025-12-19 18:49:19 +01:00
Merge branch 'feature/better-text-edit' into 'develop'
Better text edit See merge request cc-ru/ocelot/ocelot-desktop!120
This commit is contained in:
commit
6db5ff3f37
BIN
sprites/icons/Cut.png
Normal file
BIN
sprites/icons/Cut.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 239 B |
BIN
sprites/icons/Paste.png
Normal file
BIN
sprites/icons/Paste.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 206 B |
@ -68,7 +68,9 @@ TextInputBorderDisabled = #666666
|
||||
TextInputBorderFocused = #336666
|
||||
TextInputBackground = #aaaaaa
|
||||
TextInputBackgroundActive = #bbbbbb
|
||||
TextInputBackgroundSelected = #336666
|
||||
TextInputForeground = #333333
|
||||
TextInputForegroundSelected = #aaaaaa
|
||||
TextInputForegroundDisabled = #888888
|
||||
TextInputBorderError = #aa8888
|
||||
TextInputBorderErrorDisabled = #aa8888
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 149 KiB After Width: | Height: | Size: 150 KiB |
@ -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
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
@ -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 = {
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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 =
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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 = ""
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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 = {}
|
||||
|
||||
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
package ocelot.desktop.util
|
||||
|
||||
import ocelot.desktop.ui.widget.Updatable
|
||||
|
||||
/**
|
||||
* Stores a value updated by calls to [[update]].
|
||||
*/
|
||||
|
||||
33
src/main/scala/ocelot/desktop/util/Watcher.scala
Normal file
33
src/main/scala/ocelot/desktop/util/Watcher.scala
Normal file
@ -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)
|
||||
}
|
||||
@ -24,7 +24,7 @@ class OcelotInterfaceWindow(storage: OcelotInterfaceLogStorage) extends PanelWin
|
||||
children :+= new TextInput() {
|
||||
override def onConfirm(): Unit = {
|
||||
pushLine(text)
|
||||
setInput("")
|
||||
text = ""
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user