Basic implementation for settings dialog with sound volume controls

This commit is contained in:
UnicornFreedom 2022-05-10 16:21:48 +02:00
parent 954ddbce1f
commit 0fb1ac2ead
16 changed files with 331 additions and 5 deletions

3
.gitignore vendored
View File

@ -46,3 +46,6 @@ cacerts
/save /save
*~ *~
# Ocelot configuration file
/ocelot.conf

View File

@ -70,3 +70,12 @@ HistogramBarFill = #64cc65
HistogramGrid = #336633 HistogramGrid = #336633
HistogramFill = #73ff7360 HistogramFill = #73ff7360
HistogramEdge = #ccfdcc HistogramEdge = #ccfdcc
VerticalMenuBackground = #222222cc
VerticalMenuEntryActive = #333333
VerticalMenuEntryForeground = #bbbbbb
SliderBackground = #aaaaaa
SliderBorder = #888888
SliderHandler = #bbbbbb
SliderForeground = #333333

View File

@ -0,0 +1,18 @@
# Ocelot configuration. This file uses typesafe config's HOCON syntax.
# Try setting your syntax highlighting to Ruby, to help readability. At least
# in Sublime Text that works really well.
ocelot {
sound {
# Volume level for all sounds in Ocelot. Ranges from 0.0 to 1.0
# Set to 0.0 o disable sound completely.
volumeMaster: 1.0
# Volume level for computer case beeps. Ranges from 0.0 to 1.0
# Set to 0.0 o disable sound completely.
volumeBeep: 0.4
# Volume level for environmental sounds (like computer fans or HDD activity). Ranges from 0.0 to 1.0
# Set to 0.0 o disable sound completely.
volumeEnvironment: 1.0
}
}

View File

@ -3,7 +3,7 @@ package ocelot.desktop
import li.flor.nativejfilechooser.NativeJFileChooser import li.flor.nativejfilechooser.NativeJFileChooser
import ocelot.desktop.audio.{Audio, SoundSource} import ocelot.desktop.audio.{Audio, SoundSource}
import ocelot.desktop.ui.UiHandler import ocelot.desktop.ui.UiHandler
import ocelot.desktop.ui.widget.{ExitConfirmationDialog, RootWidget} import ocelot.desktop.ui.widget.{ExitConfirmationDialog, RootWidget, SettingsDialog}
import ocelot.desktop.util._ import ocelot.desktop.util._
import org.apache.commons.io.FileUtils import org.apache.commons.io.FileUtils
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
@ -36,6 +36,9 @@ object OcelotDesktop extends Logging {
logger.info("Starting up Ocelot Desktop") logger.info("Starting up Ocelot Desktop")
Ocelot.initialize(LogManager.getLogger(Ocelot)) Ocelot.initialize(LogManager.getLogger(Ocelot))
val settingsFile = new File("ocelot.conf")
Settings.load(settingsFile)
ColorScheme.load(Source.fromURL(getClass.getResource("/ocelot/desktop/colorscheme.txt"))) ColorScheme.load(Source.fromURL(getClass.getResource("/ocelot/desktop/colorscheme.txt")))
createWorkspace() createWorkspace()
@ -72,6 +75,8 @@ object OcelotDesktop extends Logging {
Ocelot.shutdown() Ocelot.shutdown()
Settings.save(settingsFile)
logger.info("Thanks for using Ocelot Desktop") logger.info("Thanks for using Ocelot Desktop")
System.exit(0) System.exit(0)
} }
@ -189,6 +194,10 @@ object OcelotDesktop extends Logging {
}).start() }).start()
} }
def settings(): Unit = {
UiHandler.root.modalDialogPool.pushDialog(new SettingsDialog())
}
def cleanup(): Unit = { def cleanup(): Unit = {
FileUtils.deleteDirectory(tmpPath.toFile) FileUtils.deleteDirectory(tmpPath.toFile)
} }

View File

@ -0,0 +1,61 @@
package ocelot.desktop
import com.typesafe.config.{Config, ConfigFactory, ConfigRenderOptions, ConfigValueFactory}
import ocelot.desktop.util.{Logging, SettingsData}
import java.io.File
import java.nio.charset.StandardCharsets
import java.nio.file.Files
import scala.io.{Codec, Source}
class Settings(val config: Config) extends SettingsData {
volumeMaster = (config.getDouble("ocelot.sound.volumeMaster") max 0 min 1).toFloat
volumeBeep = (config.getDouble("ocelot.sound.volumeBeep") max 0 min 1).toFloat
volumeEnvironment = (config.getDouble("ocelot.sound.volumeEnvironment") max 0 min 1).toFloat
}
object Settings extends Logging {
private val renderOptions = ConfigRenderOptions.defaults().setJson(false).setOriginComments(false)
private var settings: Settings = _
def get: Settings = settings
def load(file: File): Unit = {
import java.lang.System.{lineSeparator => EOL}
val defaults = {
val in = getClass.getResourceAsStream("/ocelot/desktop/ocelot.conf")
val config = Source.fromInputStream(in)(Codec.UTF8).getLines().mkString("", EOL, EOL)
in.close()
ConfigFactory.parseString(config)
}
try {
val source = Source.fromFile(file)(Codec.UTF8)
val plain = source.getLines().mkString("", EOL, EOL)
val config = ConfigFactory.parseString(plain)
settings = new Settings(config)
source.close()
}
catch {
case e: Throwable =>
if (file.exists()) {
logger.warn("Failed loading config, using defaults.", e)
}
settings = new Settings(defaults)
}
}
implicit class ExtendedConfig(val config: Config) {
def withValuePreserveOrigin(path: String, value: Any): Config = {
config.withValue(path, ConfigValueFactory.fromAnyRef(value).withOrigin(config.getValue(path).origin()))
}
}
def save(file: File): Unit = {
if (settings != null) {
val updatedConfig = settings.config
.withValuePreserveOrigin("ocelot.sound.volumeMaster", settings.volumeMaster)
.withValuePreserveOrigin("ocelot.sound.volumeBeep", settings.volumeBeep)
.withValuePreserveOrigin("ocelot.sound.volumeEnvironment", settings.volumeEnvironment)
Files.write(file.toPath, updatedConfig.root().render(renderOptions).getBytes(StandardCharsets.UTF_8))
}
}
}

View File

@ -1,5 +1,6 @@
package ocelot.desktop.audio package ocelot.desktop.audio
import ocelot.desktop.Settings
import ocelot.desktop.util.Logging import ocelot.desktop.util.Logging
import org.lwjgl.openal.AL10 import org.lwjgl.openal.AL10
@ -45,7 +46,7 @@ object Audio extends Logging {
private def _beep(pattern: String, frequency: Short, duration: Short): Unit = { private def _beep(pattern: String, frequency: Short, duration: Short): Unit = {
val source = AL10.alGenSources() val source = AL10.alGenSources()
AL10.alSourcef(source, AL10.AL_PITCH, 1) AL10.alSourcef(source, AL10.AL_PITCH, 1)
AL10.alSourcef(source, AL10.AL_GAIN, 0.3f) AL10.alSourcef(source, AL10.AL_GAIN, Settings.get.volumeBeep * Settings.get.volumeMaster)
AL10.alSource3f(source, AL10.AL_POSITION, 0, 0, 0) AL10.alSource3f(source, AL10.AL_POSITION, 0, 0, 0)
AL10.alSourcei(source, AL10.AL_LOOPING, AL10.AL_FALSE) AL10.alSourcei(source, AL10.AL_LOOPING, AL10.AL_FALSE)

View File

@ -1,5 +1,6 @@
package ocelot.desktop.audio package ocelot.desktop.audio
import ocelot.desktop.Settings
import ocelot.desktop.util.{Logging, Resource, ResourceManager} import ocelot.desktop.util.{Logging, Resource, ResourceManager}
import org.lwjgl.openal.AL10 import org.lwjgl.openal.AL10
@ -13,7 +14,7 @@ class SoundSource(soundBuffer: SoundBuffer, looping: Boolean = false) extends Re
sourceId = AL10.alGenSources() sourceId = AL10.alGenSources()
AL10.alSourcei(sourceId, AL10.AL_BUFFER, soundBuffer.getBufferId) AL10.alSourcei(sourceId, AL10.AL_BUFFER, soundBuffer.getBufferId)
AL10.alSourcef(sourceId, AL10.AL_PITCH, 1f) AL10.alSourcef(sourceId, AL10.AL_PITCH, 1f)
AL10.alSourcef(sourceId, AL10.AL_GAIN, 1f) AL10.alSourcef(sourceId, AL10.AL_GAIN, Settings.get.volumeEnvironment * Settings.get.volumeMaster)
AL10.alSource3f(sourceId, AL10.AL_POSITION, 0f, 0f, 0f) AL10.alSource3f(sourceId, AL10.AL_POSITION, 0f, 0f, 0f)
AL10.alSourcei(sourceId, AL10.AL_LOOPING, if (looping) AL10.AL_TRUE else AL10.AL_FALSE) AL10.alSourcei(sourceId, AL10.AL_LOOPING, if (looping) AL10.AL_TRUE else AL10.AL_FALSE)
} else { } else {
@ -32,6 +33,12 @@ class SoundSource(soundBuffer: SoundBuffer, looping: Boolean = false) extends Re
def pause(): Unit = if (isPlaying) AL10.alSourcePause(sourceId) def pause(): Unit = if (isPlaying) AL10.alSourcePause(sourceId)
def stop(): Unit = if (isPlaying || isPaused) AL10.alSourceStop(sourceId) def stop(): Unit = if (isPlaying || isPaused) AL10.alSourceStop(sourceId)
def setVolume(value: Float): Unit = {
if (sourceId != -1) {
AL10.alSourcef(sourceId, AL10.AL_GAIN, value)
}
}
override def freeResource(): Unit = { override def freeResource(): Unit = {
if (sourceId != -1) { if (sourceId != -1) {
stop() stop()

View File

@ -1,6 +1,5 @@
package ocelot.desktop.ui.widget package ocelot.desktop.ui.widget
import ocelot.desktop.OcelotDesktop
import ocelot.desktop.geometry.Padding2D import ocelot.desktop.geometry.Padding2D
import ocelot.desktop.ui.layout.LinearLayout import ocelot.desktop.ui.layout.LinearLayout
import ocelot.desktop.ui.widget.modal.ModalDialog import ocelot.desktop.ui.widget.modal.ModalDialog

View File

@ -23,6 +23,8 @@ class MenuBar extends Widget {
menu.addEntry(new ContextMenuEntry("Exit", () => OcelotDesktop.exit())) menu.addEntry(new ContextMenuEntry("Exit", () => OcelotDesktop.exit()))
})) }))
addEntry(new MenuBarButton("Settings", () => OcelotDesktop.settings()))
addEntry(new Widget { addEntry(new Widget {
override def maximumSize: Size2D = Size2D(Float.PositiveInfinity, 1) override def maximumSize: Size2D = Size2D(Float.PositiveInfinity, 1)
}) // fill remaining space }) // fill remaining space

View File

@ -0,0 +1,79 @@
package ocelot.desktop.ui.widget
import ocelot.desktop.Settings
import ocelot.desktop.audio.SoundSource
import ocelot.desktop.geometry.{Padding2D, Size2D}
import ocelot.desktop.ui.layout.LinearLayout
import ocelot.desktop.ui.widget.modal.ModalDialog
import ocelot.desktop.ui.widget.verticalmenu.{VerticalMenu, VerticalMenuButton}
import ocelot.desktop.util.{Orientation, ResourceManager, SettingsData}
class SettingsDialog extends ModalDialog {
private val menu = new VerticalMenu
menu.addEntry(new VerticalMenuButton("Sound", () => {}))
private val settingsData = new SettingsData()
settingsData.updateWith(Settings.get)
private def applySettings(): Unit = {
Settings.get.updateWith(settingsData)
ResourceManager.forEach {
case soundSource: SoundSource => soundSource.setVolume(Settings.get.volumeEnvironment * Settings.get.volumeMaster)
case _ =>
}
}
children :+= new PaddingBox(new Widget {
override val layout = new LinearLayout(this, orientation = Orientation.Horizontal)
children :+= new Widget {
override val layout = new LinearLayout(this, orientation = Orientation.Vertical)
children :+= menu
}
children :+= new PaddingBox(new Widget {
override val layout = new LinearLayout(this, orientation = Orientation.Vertical)
children :+= new PaddingBox(new Slider(settingsData.volumeMaster, "Master Volume") {
override def minimumSize: Size2D = Size2D(512, 24)
override def onValueChanged(value: Float): Unit = settingsData.volumeMaster = value
}, Padding2D(bottom = 8))
children :+= new PaddingBox(new Slider(settingsData.volumeBeep, "Beep Volume") {
override def minimumSize: Size2D = Size2D(512, 24)
override def onValueChanged(value: Float): Unit = settingsData.volumeBeep = value
}, Padding2D(bottom = 8))
children :+= new PaddingBox(new Slider(settingsData.volumeEnvironment, "Environment Volume") {
override def minimumSize: Size2D = Size2D(512, 24)
override def onValueChanged(value: Float): Unit = settingsData.volumeEnvironment = value
}, Padding2D(bottom = 8))
children :+= new Widget {
override val layout = new LinearLayout(this, orientation = Orientation.Horizontal)
children :+= new Widget {
override def maximumSize: Size2D = Size2D(Float.PositiveInfinity, 1)
}
children :+= new Button {
override def text: String = "Ok"
override def onClick(): Unit = {
applySettings()
close()
}
}
children :+= new PaddingBox(new Button {
override def text: String = "Cancel"
override def onClick(): Unit = close()
}, Padding2D(left = 8))
children :+= new PaddingBox(new Button {
override def text: String = "Apply"
override def onClick(): Unit = applySettings()
}, Padding2D(left = 8))
}
}, Padding2D(left = 8))
}, Padding2D.equal(16))
}

View File

@ -0,0 +1,45 @@
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.ui.event.handlers.{ClickHandler, DragHandler}
import ocelot.desktop.ui.event.{ClickEvent, DragEvent, MouseEvent}
import ocelot.desktop.util.DrawUtils
import ocelot.desktop.util.MathUtils.ExtendedFloat
class Slider(var value: Float, text: String) extends Widget with ClickHandler with DragHandler {
def onValueChanged(value: Float): Unit = {}
override def receiveMouseEvents: Boolean = true
private val handleWidth = 8.0f
private def calculateValue(x: Float): Unit = {
value = ((x - bounds.x - handleWidth / 2f) / (bounds.w - handleWidth)).clamp(0f, 1f)
onValueChanged(value)
}
eventHandlers += {
case ClickEvent(MouseEvent.Button.Left, pos) => calculateValue(pos.x)
case DragEvent(_, MouseEvent.Button.Left, pos) => calculateValue(pos.x)
}
override def minimumSize: Size2D = Size2D(24 + text.length * 8, 24)
override def maximumSize: Size2D = minimumSize.copy(width = Float.PositiveInfinity)
override def draw(g: Graphics): Unit = {
g.rect(bounds, ColorScheme("SliderBackground"))
DrawUtils.ring(g, position.x, position.y, width, height, 2, ColorScheme("SliderBorder"))
g.rect(position.x + value * (bounds.w - handleWidth), position.y, handleWidth, height, ColorScheme("SliderHandler"))
DrawUtils.ring(g, position.x + value * (bounds.w - handleWidth), position.y, handleWidth, height, 2, ColorScheme("SliderBorder"))
g.background = Color.Transparent
g.foreground = ColorScheme("SliderForeground")
val fullText = f"$text: ${value * 100}%.0f%%"
val textWidth = fullText.iterator.map(g.font.charWidth(_)).sum
g.text(position.x + ((width - textWidth) / 2).round, position.y + 4, fullText)
}
}

View File

@ -0,0 +1,25 @@
package ocelot.desktop.ui.widget.verticalmenu
import ocelot.desktop.geometry.Padding2D
import ocelot.desktop.graphics.Graphics
import ocelot.desktop.ui.layout.LinearLayout
import ocelot.desktop.ui.widget.{PaddingBox, Widget}
import ocelot.desktop.util.Orientation
import ocelot.desktop.ColorScheme
class VerticalMenu extends Widget {
override def receiveMouseEvents: Boolean = true
private val entries: Widget = new Widget {
override val layout = new LinearLayout(this, orientation = Orientation.Vertical)
}
children :+= new PaddingBox(entries, Padding2D.equal(1f))
def addEntry(w: Widget): Unit = entries.children :+= w
override def draw(g: Graphics): Unit = {
g.rect(bounds, ColorScheme("VerticalMenuBackground"))
drawChildren(g)
}
}

View File

@ -0,0 +1,44 @@
package ocelot.desktop.ui.widget.verticalmenu
import ocelot.desktop.ColorScheme
import ocelot.desktop.color.Color
import ocelot.desktop.geometry.{Padding2D, Size2D}
import ocelot.desktop.graphics.Graphics
import ocelot.desktop.ui.event.handlers.{ClickHandler, HoverHandler}
import ocelot.desktop.ui.event.{ClickEvent, HoverEvent, MouseEvent}
import ocelot.desktop.ui.widget.{Label, PaddingBox, Widget}
import ocelot.desktop.util.animation.ColorAnimation
class VerticalMenuButton(label: String, handler: () => Unit = () => {})
extends Widget with ClickHandler with HoverHandler {
val colorAnimation: ColorAnimation = new ColorAnimation(ColorScheme("VerticalMenuBackground"), 0.6f)
children :+= new PaddingBox(new Label {
override def text: String = label
override def color: Color = ColorScheme("VerticalMenuEntryForeground")
override def maximumSize: Size2D = Size2D(Float.PositiveInfinity, 16)
}, Padding2D(left = 8, right = 8, top = 1, bottom = 1))
override def receiveMouseEvents: Boolean = true
def onClick(): Unit = handler()
def onMouseEnter(): Unit = colorAnimation.goto(ColorScheme("VerticalMenuEntryActive"))
def onMouseLeave(): Unit = colorAnimation.goto(ColorScheme("VerticalMenuBackground"))
eventHandlers += {
case ClickEvent(MouseEvent.Button.Left, _) =>
onClick()
case HoverEvent(HoverEvent.State.Enter) =>
onMouseEnter()
case HoverEvent(HoverEvent.State.Leave) =>
onMouseLeave()
}
override def draw(g: Graphics): Unit = {
colorAnimation.update()
g.rect(bounds, colorAnimation.color)
drawChildren(g)
}
}

View File

@ -0,0 +1,7 @@
package ocelot.desktop.util
object MathUtils {
implicit class ExtendedFloat(val x: Float) {
def clamp(a: Float, b: Float): Float = x.max(a).min(b)
}
}

View File

@ -19,6 +19,10 @@ object ResourceManager {
} }
} }
def forEach(predicate: Resource => Unit): Unit = {
resources.foreach(predicate)
}
def freeResource(resource: Resource): Unit = { def freeResource(resource: Resource): Unit = {
resource.freeResource() resource.freeResource()
resources -= resource resources -= resource

View File

@ -0,0 +1,13 @@
package ocelot.desktop.util
class SettingsData {
var volumeMaster: Float = 1f
var volumeBeep: Float = 1f
var volumeEnvironment: Float = 1f
def updateWith(data: SettingsData): Unit = {
this.volumeMaster = data.volumeMaster
this.volumeBeep = data.volumeBeep
this.volumeEnvironment = data.volumeEnvironment
}
}