[go: nahoru, domu]

blob: 615764da330345da54f73903ea73fe39d140fb6f [file] [log] [blame]
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.compose.desktop
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionContext
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.ui.platform.Keyboard
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.window.MenuBar
import java.awt.Container
import java.awt.Frame
import java.awt.event.ComponentAdapter
import java.awt.event.ComponentEvent
import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent
import java.awt.image.BufferedImage
import javax.swing.JFrame
import javax.swing.JMenuBar
import javax.swing.SwingUtilities
import javax.swing.WindowConstants
/**
* Local composition of [AppWindow]. [AppWindow] is a high level window implementation. This local
* composition is used to get the current [AppWindow].
*/
val LocalAppWindow = compositionLocalOf<AppWindow>()
internal val LocalLayerContainer = compositionLocalOf<Container>()
/**
* Opens a window with the given content.
*
* @param title The title of the window.
* The title is displayed in the windows's native border.
* @param size The initial size of the window.
* @param location The initial position of the window in screen space. This parameter is
* ignored if [center] is set to true.
* @param centered Determines if the window is centered on startup. The default value for the
* window is true.
* @param icon The icon for the window displayed on the system taskbar.
* @param menuBar Window menu bar. The menu bar can be displayed inside a window (Windows,
* Linux) or at the top of the screen (Mac OS).
* @param undecorated Removes the native window border if set to true. The default value is false.
* @param resizable Makes the window resizable if is set to true and unresizable if is set to
* false. The default value is true.
* @param events Allows to describe events of the window.
* Supported events: onOpen, onClose, onMinimize, onMaximize, onRestore, onFocusGet, onFocusLost,
* onResize, onRelocate.
* @param onDismissRequest Executes when the user tries to close the Window.
*/
fun Window(
title: String = "JetpackDesktopWindow",
size: IntSize = IntSize(800, 600),
location: IntOffset = IntOffset.Zero,
centered: Boolean = true,
icon: BufferedImage? = null,
menuBar: MenuBar? = null,
undecorated: Boolean = false,
resizable: Boolean = true,
events: WindowEvents = WindowEvents(),
onDismissRequest: (() -> Unit)? = null,
content: @Composable () -> Unit = { }
) = SwingUtilities.invokeLater {
AppWindow(
title = title,
size = size,
location = location,
centered = centered,
icon = icon,
menuBar = menuBar,
undecorated = undecorated,
resizable = resizable,
events = events,
onDismissRequest = onDismissRequest
).show {
content()
}
}
/**
* AppWindow is a class that represents a window.
*/
class AppWindow : AppFrame {
/**
* Gets ComposeWindow object.
*/
override val window: ComposeWindow
init {
require(SwingUtilities.isEventDispatchThread()) {
"AppWindow should be created inside AWT Event Thread (use SwingUtilities.invokeLater " +
"or just dsl for creating window: Window { })"
}
window = ComposeWindow(parent = this)
window.apply {
defaultCloseOperation = WindowConstants.DISPOSE_ON_CLOSE
addWindowListener(object : WindowAdapter() {
override fun windowClosing(event: WindowEvent) {
if (defaultCloseOperation != WindowConstants.DO_NOTHING_ON_CLOSE) {
onDispose?.invoke()
onDismiss?.invoke()
events.invokeOnClose()
AppManager.removeWindow(parent)
isClosed = true
}
}
override fun windowIconified(event: WindowEvent) {
events.invokeOnMinimize()
}
override fun windowDeiconified(event: WindowEvent) {
events.invokeOnRestore()
}
})
addWindowFocusListener(object : WindowAdapter() {
override fun windowGainedFocus(event: WindowEvent) {
// Dialogs should not receive a common application menu bar
if (invoker == null) {
window.setJMenuBar(parent.menuBar?.menuBar)
}
events.invokeOnFocusGet()
}
override fun windowLostFocus(event: WindowEvent) {
events.invokeOnFocusLost()
}
})
addWindowStateListener(object : WindowAdapter() {
override fun windowStateChanged(event: WindowEvent) {
val state = getState()
if (state != Frame.NORMAL && state != Frame.ICONIFIED) {
events.invokeOnMaximize()
}
}
})
addComponentListener(object : ComponentAdapter() {
override fun componentResized(e: ComponentEvent) {
val size = IntSize(width, height)
events.invokeOnResize(size)
}
override fun componentMoved(e: ComponentEvent) {
val location = IntOffset(x, y)
events.invokeOnRelocate(location)
}
})
}
}
internal constructor(
attached: AppFrame? = null,
title: String = "JetpackDesktopWindow",
size: IntSize = IntSize(800, 600),
location: IntOffset = IntOffset.Zero,
centered: Boolean = true,
icon: BufferedImage? = null,
menuBar: MenuBar? = null,
undecorated: Boolean = false,
resizable: Boolean = true,
events: WindowEvents = WindowEvents(),
onDismissRequest: (() -> Unit)? = null
) : this(
title = title,
size = size,
location = location,
centered = centered,
icon = icon,
menuBar = menuBar,
undecorated = undecorated,
resizable = resizable,
events = events,
onDismissRequest = onDismissRequest
) {
this.invoker = attached
}
/**
* Creates an instance of AppWindow. AppWindow is a class that represents a window.
*
* @param title The title of the window.
* The title is displayed in the windows's native border.
* @param size The initial size of the window.
* @param location The initial position of the window in screen space. This parameter is
* ignored if [center] is set to true.
* @param centered Determines if the window is centered on startup. The default value for the
* window is true.
* @param icon The icon for the window displayed on the system taskbar.
* @param menuBar Window menu bar. The menu bar can be displayed inside a window (Windows,
* Linux) or at the top of the screen (Mac OS).
* @param undecorated Removes the native window border if set to true. The default value is false.
* @param resizable Makes the window resizable if is set to true and unresizable if is set to
* false. The default value is true.
* @param events Allows to describe events of the window.
* Supported events: onOpen, onClose, onMinimize, onMaximize, onRestore, onFocusGet, onFocusLost,
* onResize, onRelocate.
* @param onDismissRequest Executes when the user tries to close the AppWindow.
*/
constructor(
title: String = "JetpackDesktopWindow",
size: IntSize = IntSize(800, 600),
location: IntOffset = IntOffset.Zero,
centered: Boolean = true,
icon: BufferedImage? = null,
menuBar: MenuBar? = null,
undecorated: Boolean = false,
resizable: Boolean = true,
events: WindowEvents = WindowEvents(),
onDismissRequest: (() -> Unit)? = null
) {
AppManager.addWindow(this)
setTitle(title)
setIcon(icon)
setSize(size.width, size.height)
this.resizable = resizable
if (centered) {
setWindowCentered()
} else {
setLocation(location.x, location.y)
}
this.menuBar = menuBar
if (this.menuBar == null && AppManager.sharedMenuBar != null) {
this.menuBar = AppManager.sharedMenuBar!!
}
this.events = events
window.setUndecorated(undecorated)
onDismiss = onDismissRequest
}
internal var pair: AppFrame? = null
internal override fun connectPair(window: AppFrame) {
pair = window
}
internal override fun disconnectPair() {
pair = null
}
/**
* Sets the title of the window.
*
* @param title Window title text.
*/
override fun setTitle(title: String) {
window.setTitle(title)
}
/**
* Sets the image icon of the window.
*
* @param image Image of the icon.
*/
override fun setIcon(image: BufferedImage?) {
this.icon = image
if (icon != null) {
try {
val taskbar = java.awt.Taskbar.getTaskbar()
taskbar.setIconImage(icon)
} catch (e: UnsupportedOperationException) {
println("The os does not support: 'Taskbar.setIconImage'")
}
window.setIconImage(icon)
}
}
/**
* Sets the menu bar of the window. The menu bar can be displayed inside a window (Windows,
* Linux) or at the top of the screen (Mac OS).
*
* @param manuBar Window menu bar.
*/
override fun setMenuBar(menuBar: MenuBar) {
this.menuBar = menuBar
window.setJMenuBar(menuBar.menuBar)
}
/**
* Removes the menu bar of the window.
*/
override fun removeMenuBar() {
this.menuBar = null
window.setJMenuBar(JMenuBar())
}
/**
* Returns true if the window is in fullscreen mode, false otherwise.
*/
override val isFullscreen: Boolean
get() = window.layer.wrapped.fullscreen
/**
* Switches the window to fullscreen mode if the window is resizable. If the window is in
* fullscreen mode [minimize] and [maximize] methods are ignored.
*/
override fun makeFullscreen() {
if (!isFullscreen && resizable) {
window.layer.wrapped.fullscreen = true
}
}
/**
* Minimizes the window to the taskbar. If the window is in fullscreen mode this method
* is ignored.
*/
override fun minimize() {
if (!isFullscreen) {
window.setExtendedState(JFrame.ICONIFIED)
}
}
/**
* Maximizes the window to fill all available screen space. If the window is in fullscreen mode
* this method is ignored.
*/
override fun maximize() {
if (!isFullscreen) {
window.setExtendedState(JFrame.MAXIMIZED_BOTH)
}
}
/**
* Restores the previous state and size of the window after
* maximizing/minimizing/fullscreen mode.
*/
override fun restore() {
if (isFullscreen) {
window.layer.wrapped.fullscreen = false
}
window.setExtendedState(JFrame.NORMAL)
}
private var _resizable: Boolean = true
/**
* Sets the ability to resize the window. True - the window can be resized,
* false - the window cannot be resized. If the window is in fullscreen mode
* setter of this property is ignored. If this property is true the [makeFullscreen()]
* method is ignored.
*/
override var resizable: Boolean
get() {
return window.isResizable()
}
set(value) {
if (!isFullscreen) {
_resizable = value
window.setResizable(value)
}
}
/**
* Sets the new size of the window.
*
* @param width the new width of the window.
* @param height the new height of the window.
*/
override fun setSize(width: Int, height: Int) {
// better check min/max values of current window size
var w = width
if (w <= 0) {
w = this.width
}
var h = height
if (h <= 0) {
h = this.height
}
window.setSize(w, h)
}
/**
* Sets the new position of the window on the screen.
*
* @param x the new x-coordinate of the window.
* @param y the new y-coordinate of the window.
*/
override fun setLocation(x: Int, y: Int) {
window.setLocation(x, y)
}
/**
* Sets the window to the center of the current screen.
*/
override fun setWindowCentered() {
val screenBounds = window.graphicsConfiguration.getBounds()
val x = (screenBounds.width - width) / 2 + screenBounds.x
val y = (screenBounds.height - height) / 2 + screenBounds.y
window.setLocation(x, y)
}
private fun onCreate(
parentComposition: CompositionContext? = null,
content: @Composable () -> Unit
) {
window.setContent(parentComposition) {
CompositionLocalProvider(
LocalAppWindow provides this,
LocalLayerContainer provides window,
content = content
)
}
}
/**
* Shows a window with the given Compose content.
*
* @param parentComposition The parent composition reference to coordinate
* scheduling of composition updates.
* If null then default root composition will be used.
* @param content Composable content of the window.
*/
fun show(
parentComposition: CompositionContext? = null,
content: @Composable () -> Unit
) {
if (invoker != null) {
invoker!!.lockWindow()
window.setAlwaysOnTop(true)
}
onCreate(parentComposition) {
window.layer.owners.keyboard = keyboard
content()
}
window.setVisible(true)
events.invokeOnOpen()
}
/**
* Closes the window.
*/
override fun close() {
window.dispatchEvent(WindowEvent(window, WindowEvent.WINDOW_CLOSING))
}
internal override fun dispose() {
invoker?.unlockWindow()
}
internal override fun lockWindow() {
window.apply {
defaultCloseOperation = WindowConstants.DO_NOTHING_ON_CLOSE
setFocusableWindowState(false)
setResizable(false)
setEnabled(false)
}
invoker?.connectPair(this)
}
internal override fun unlockWindow() {
window.apply {
defaultCloseOperation = WindowConstants.DISPOSE_ON_CLOSE
setFocusableWindowState(true)
setEnabled(true)
toFront()
requestFocus()
}
resizable = _resizable
disconnectPair()
}
/**
* Gets the Keyboard object of the window.
*/
val keyboard: Keyboard = Keyboard()
}