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() } }