mirror of
https://gitlab.com/cc-ru/ocelot/ocelot-desktop.git
synced 2025-12-20 02:59:19 +01:00
355 lines
11 KiB
Scala
355 lines
11 KiB
Scala
package ocelot.desktop.ui
|
|
|
|
import buildinfo.BuildInfo
|
|
import ocelot.desktop.audio.Audio
|
|
import ocelot.desktop.geometry.{Size2D, Vector2D}
|
|
import ocelot.desktop.graphics.Graphics
|
|
import ocelot.desktop.ui.event.MouseEvent
|
|
import ocelot.desktop.ui.event.handlers.HoverHandler
|
|
import ocelot.desktop.ui.event.sources.{KeyEvents, MouseEvents, ScrollEvents}
|
|
import ocelot.desktop.ui.widget.{RootWidget, Widget}
|
|
import ocelot.desktop.util._
|
|
import ocelot.desktop.{OcelotDesktop, Settings}
|
|
import org.apache.commons.io.FileUtils
|
|
import org.apache.commons.lang3.SystemUtils
|
|
import org.lwjgl.BufferUtils
|
|
import org.lwjgl.input.Mouse
|
|
import org.lwjgl.opengl._
|
|
|
|
import java.awt.Toolkit
|
|
import java.awt.datatransfer.{DataFlavor, StringSelection, UnsupportedFlavorException}
|
|
import java.io.{File, FileOutputStream}
|
|
import java.nio.ByteBuffer
|
|
import java.nio.channels.Channels
|
|
import java.nio.file.{Files, Paths}
|
|
import javax.imageio.ImageIO
|
|
import scala.collection.mutable
|
|
|
|
object UiHandler extends Logging {
|
|
var root: RootWidget = _
|
|
var graphics: Graphics = _
|
|
|
|
private val hierarchy = mutable.ArrayBuffer(root: Widget)
|
|
private var shouldUpdateHierarchy = true
|
|
private val fpsCalculator = new FPSCalculator
|
|
private val ticker = new Ticker
|
|
|
|
ticker.tickInterval = 1000f / 60f
|
|
|
|
def getHierarchy: Array[Widget] = hierarchy.toArray
|
|
|
|
def updateHierarchy(): Unit = {
|
|
shouldUpdateHierarchy = true
|
|
}
|
|
|
|
private def _updateHierarchy(): Unit = {
|
|
val stack = mutable.Stack(root: Widget)
|
|
hierarchy.clear()
|
|
while (stack.nonEmpty) {
|
|
val widget = stack.pop()
|
|
hierarchy += widget
|
|
for (child <- widget.hierarchy.reverseIterator) {
|
|
stack.push(child)
|
|
}
|
|
}
|
|
|
|
shouldUpdateHierarchy = false
|
|
}
|
|
|
|
def fps: Float = fpsCalculator.fps
|
|
|
|
def dt: Float = fpsCalculator.dt
|
|
|
|
def mousePosition: Vector2D = {
|
|
Vector2D(Mouse.getX, Display.getHeight - Mouse.getY)
|
|
}
|
|
|
|
private val _clipboard = Toolkit.getDefaultToolkit.getSystemClipboard
|
|
|
|
def clipboard: String = try {
|
|
_clipboard.getData(DataFlavor.stringFlavor).toString
|
|
} catch {
|
|
case _: UnsupportedFlavorException =>
|
|
logger.debug("Trying to paste non-Unicode content from clipboard! Replaced with empty string.")
|
|
""
|
|
}
|
|
|
|
def clipboard_=(value: String): Unit = {
|
|
val data = new StringSelection(value)
|
|
_clipboard.setContents(data, data)
|
|
}
|
|
|
|
def isFullScreen: Boolean = Display.isFullscreen
|
|
|
|
def isFullScreen_=(value: Boolean): Any = {
|
|
if (value) {
|
|
Display.setDisplayModeAndFullscreen(Display.getDesktopDisplayMode)
|
|
}
|
|
else {
|
|
// Updating size from settings
|
|
val settingsSize = Settings.get.windowSize
|
|
root.size = if (settingsSize.isSet) Size2D(settingsSize.x, settingsSize.y) else Size2D(800, 600)
|
|
|
|
// Setting normal display mode (non-fullscreen by default)
|
|
Display.setDisplayMode(new DisplayMode(root.size.width.toInt, root.size.height.toInt))
|
|
|
|
// Updating position from settings
|
|
if (Settings.get.windowPosition.isSet) {
|
|
if (Settings.get.windowValidatePosition) {
|
|
// the configuration can have incorrect values for coordinates
|
|
// and that can cause the window to "disappear" from the screen
|
|
// TODO: after upgrading to LWJGL 3 consider the multi-monitor setups
|
|
val desktop = Display.getDesktopDisplayMode
|
|
if (Settings.get.windowPosition.x < -Settings.get.windowSize.x)
|
|
Settings.get.windowPosition.x = 0
|
|
if (Settings.get.windowPosition.x > desktop.getWidth)
|
|
Settings.get.windowPosition.x = desktop.getWidth - Settings.get.windowSize.x
|
|
if (Settings.get.windowPosition.y < -Settings.get.windowSize.y)
|
|
Settings.get.windowPosition.y = 0
|
|
if (Settings.get.windowPosition.y > desktop.getHeight)
|
|
Settings.get.windowPosition.y = desktop.getHeight - Settings.get.windowSize.y
|
|
}
|
|
Display.setLocation(Settings.get.windowPosition.x, Settings.get.windowPosition.y)
|
|
}
|
|
|
|
// Window should be resizable after exit from fullscreen mode
|
|
// But it seems like there's lwjgl bug with internal resizable value caching
|
|
// How to reproduce:
|
|
// 1) Enter fullscreen mode via setDisplayModeAndFullscreen(...) or setFullScreen(true)
|
|
// 2) Set window resizable via setResizable(true)
|
|
// 3) Exit from fullscreen mode
|
|
// 4) Set window resizable again
|
|
// Result:
|
|
// Window is not resizable and can't be resized with mouse
|
|
// Expected behaviour:
|
|
// Window should be resizable
|
|
Display.setResizable(false)
|
|
Display.setResizable(true)
|
|
}
|
|
|
|
Settings.get.windowFullscreen = value
|
|
}
|
|
|
|
def init(root: RootWidget): Unit = {
|
|
this.root = root
|
|
root.relayout()
|
|
|
|
loadLibraries()
|
|
|
|
isFullScreen = Settings.get.windowFullscreen
|
|
windowTitle = "Ocelot Desktop v" + BuildInfo.version
|
|
|
|
loadIcons()
|
|
Display.setVSyncEnabled(true)
|
|
Display.create()
|
|
KeyEvents.init()
|
|
MouseEvents.init()
|
|
|
|
logger.info(s"Created window with ${root.size}")
|
|
logger.info(s"OpenGL vendor: ${GL11.glGetString(GL11.GL_VENDOR)}")
|
|
logger.info(s"OpenGL renderer: ${GL11.glGetString(GL11.GL_RENDERER)}")
|
|
|
|
Spritesheet.load()
|
|
graphics = new Graphics
|
|
|
|
Audio.init()
|
|
}
|
|
|
|
private var nativeLibrariesDir: String = _
|
|
|
|
private def loadLibraries(): Unit = {
|
|
// we cannot remove DLL files on Windows after they were loaded by Ocelot
|
|
// therefore we will create them in local directory and keep for future
|
|
nativeLibrariesDir = if (SystemUtils.IS_OS_WINDOWS)
|
|
Paths.get(this.getClass.getProtectionDomain.getCodeSource.getLocation.toURI.resolve( "natives")).toString
|
|
else Files.createTempDirectory("ocelot-desktop").toString
|
|
|
|
val arch = System.getProperty("os.arch")
|
|
val is64bit = arch.startsWith("amd64")
|
|
|
|
val libs = if (SystemUtils.IS_OS_WINDOWS)
|
|
if (is64bit)
|
|
Array("jinput-dx8_64.dll", "jinput-raw_64.dll", "jinput-wintab.dll", "lwjgl64.dll", "OpenAL64.dll")
|
|
else
|
|
Array("jinput-dx8.dll", "jinput-raw.dll", "jinput-wintab.dll", "lwjgl.dll", "OpenAL32.dll")
|
|
else if (SystemUtils.IS_OS_MAC_OSX)
|
|
Array("liblwjgl.dylib")
|
|
else if (SystemUtils.IS_OS_LINUX)
|
|
if (is64bit)
|
|
Array("libjinput-linux64.so", "liblwjgl64.so", "libopenal64.so")
|
|
else
|
|
Array("libjinput-linux.so", "liblwjgl.so", "libopenal.so")
|
|
else
|
|
throw new Exception("Unsupported OS")
|
|
|
|
logger.debug("Unpacking native libraries to: " + nativeLibrariesDir)
|
|
for (lib <- libs) {
|
|
val dest = new File(Paths.get(nativeLibrariesDir, lib).toString)
|
|
|
|
if (!dest.exists()) {
|
|
val source = getClass.getResourceAsStream("/" + lib)
|
|
|
|
if (!dest.getParentFile.exists())
|
|
dest.getParentFile.mkdirs()
|
|
|
|
val output = new FileOutputStream(dest)
|
|
output.getChannel.transferFrom(Channels.newChannel(source), 0, Long.MaxValue)
|
|
output.flush()
|
|
output.close()
|
|
source.close()
|
|
}
|
|
}
|
|
|
|
System.setProperty("org.lwjgl.librarypath", nativeLibrariesDir)
|
|
}
|
|
|
|
private def loadIcons(): Unit = {
|
|
val sizes = Array(256, 128, 64, 32, 16)
|
|
val list = new Array[ByteBuffer](sizes.length)
|
|
|
|
for ((size, i) <- sizes.zipWithIndex) {
|
|
val imageURL = getClass.getResource(s"/ocelot/desktop/images/icon$size.png")
|
|
val image = ImageIO.read(imageURL)
|
|
val pixels = new Array[Int](image.getWidth * image.getHeight)
|
|
image.getRGB(0, 0, image.getWidth, image.getHeight, pixels, 0, image.getWidth)
|
|
val buf = BufferUtils.createByteBuffer(image.getWidth * image.getHeight * 4)
|
|
|
|
for (y <- 0 until image.getHeight) {
|
|
for (x <- 0 until image.getWidth) {
|
|
val pixel = pixels(y * image.getWidth + x)
|
|
buf.put(((pixel >> 16) & 0xFF).toByte)
|
|
buf.put(((pixel >> 8) & 0xFF).toByte)
|
|
buf.put((pixel & 0xFF).toByte)
|
|
buf.put(((pixel >> 24) & 0xFF).toByte)
|
|
}
|
|
}
|
|
|
|
buf.flip()
|
|
list(i) = buf
|
|
}
|
|
|
|
Display.setIcon(list)
|
|
logger.info(s"Loaded window icons of sizes ${sizes.mkString(", ")}")
|
|
}
|
|
|
|
//noinspection ScalaWeakerAccess
|
|
def windowTitle: String = Display.getTitle
|
|
|
|
//noinspection ScalaWeakerAccess
|
|
def windowTitle_=(title: String): Unit = Display.setTitle(title)
|
|
|
|
private var exitRequested = false
|
|
|
|
def exit(): Unit = {
|
|
exitRequested = true
|
|
}
|
|
|
|
def start(): Unit = {
|
|
while (!exitRequested) {
|
|
if (Display.isCloseRequested)
|
|
OcelotDesktop.exit()
|
|
|
|
OcelotDesktop.withTickLockAcquired {
|
|
updateWindowSizeAndPosition()
|
|
|
|
Audio.update()
|
|
KeyEvents.update()
|
|
MouseEvents.update()
|
|
|
|
Profiler.startTimeMeasurement("000_update")
|
|
update()
|
|
Profiler.endTimeMeasurement("000_update")
|
|
|
|
Profiler.startTimeMeasurement("001_draw")
|
|
draw()
|
|
Profiler.endTimeMeasurement("001_draw")
|
|
}
|
|
|
|
Profiler.startTimeMeasurement("002_sleep")
|
|
ticker.waitNext()
|
|
Display.update()
|
|
fpsCalculator.tick()
|
|
Profiler.endTimeMeasurement("002_sleep")
|
|
}
|
|
}
|
|
|
|
def terminate(): Unit = {
|
|
root.workspaceView.dispose()
|
|
KeyEvents.destroy()
|
|
MouseEvents.destroy()
|
|
Display.destroy()
|
|
Audio.destroy()
|
|
|
|
if (!SystemUtils.IS_OS_WINDOWS)
|
|
FileUtils.deleteDirectory(new File(nativeLibrariesDir))
|
|
}
|
|
|
|
private def update(): Unit = {
|
|
if (shouldUpdateHierarchy) _updateHierarchy()
|
|
|
|
val mousePos = mousePosition
|
|
if (mousePos.x < 0 || mousePos.y < 0 || mousePos.x > root.width || mousePos.y > root.height) {
|
|
KeyEvents.releaseKeys()
|
|
MouseEvents.releaseButtons()
|
|
}
|
|
|
|
for (event <- KeyEvents.events)
|
|
hierarchy.reverseIterator.foreach(_.handleEvent(event))
|
|
|
|
for (event <- MouseEvents.events)
|
|
hierarchy.reverseIterator.filter(_.receiveAllMouseEvents).foreach(_.handleEvent(event))
|
|
|
|
val scrollTarget = hierarchy.reverseIterator
|
|
.find(w => w.receiveScrollEvents && w.clippedBounds.contains(mousePos))
|
|
|
|
val mouseTarget = hierarchy.reverseIterator
|
|
.find(w => w.receiveMouseEvents && w.clippedBounds.contains(mousePos))
|
|
|
|
for (event <- ScrollEvents.events)
|
|
scrollTarget.foreach(_.handleEvent(event))
|
|
|
|
for (event <- MouseEvents.events)
|
|
if (event.state == MouseEvent.State.Press) {
|
|
mouseTarget.foreach(_.handleEvent(event))
|
|
} else
|
|
hierarchy.reverseIterator.foreach(_.handleEvent(event))
|
|
|
|
hierarchy.reverseIterator.foreach {
|
|
case handler: HoverHandler if !mouseTarget.contains(handler) => handler.setHovered(false)
|
|
case _ =>
|
|
}
|
|
|
|
mouseTarget.foreach {
|
|
case handler: HoverHandler => handler.setHovered(true)
|
|
case _ =>
|
|
}
|
|
|
|
root.update()
|
|
}
|
|
|
|
private def updateWindowSizeAndPosition(): Unit = {
|
|
val width = Display.getWidth
|
|
val height = Display.getHeight
|
|
|
|
graphics.resize(width, height)
|
|
root.size = Size2D(width, height)
|
|
|
|
// Settings fields should be updated only in non-fullscreen mode
|
|
if (isFullScreen)
|
|
return
|
|
|
|
Settings.get.windowSize.set(width, height)
|
|
Settings.get.windowPosition.set(Display.getX, Display.getY)
|
|
}
|
|
|
|
private def draw(): Unit = {
|
|
graphics.setViewport(root.width.asInstanceOf[Int], root.height.asInstanceOf[Int])
|
|
graphics.clear()
|
|
|
|
root.draw(graphics)
|
|
|
|
graphics.flush()
|
|
graphics.update()
|
|
}
|
|
}
|