mirror of
https://gitlab.com/cc-ru/ocelot/ocelot-desktop.git
synced 2025-12-20 02:59:19 +01:00
Basic implementation for settings dialog with sound volume controls
This commit is contained in:
parent
954ddbce1f
commit
0fb1ac2ead
3
.gitignore
vendored
3
.gitignore
vendored
@ -46,3 +46,6 @@ cacerts
|
||||
/save
|
||||
|
||||
*~
|
||||
|
||||
# Ocelot configuration file
|
||||
/ocelot.conf
|
||||
|
||||
@ -69,4 +69,13 @@ HistogramBarTop = #ccfdcc
|
||||
HistogramBarFill = #64cc65
|
||||
HistogramGrid = #336633
|
||||
HistogramFill = #73ff7360
|
||||
HistogramEdge = #ccfdcc
|
||||
HistogramEdge = #ccfdcc
|
||||
|
||||
VerticalMenuBackground = #222222cc
|
||||
VerticalMenuEntryActive = #333333
|
||||
VerticalMenuEntryForeground = #bbbbbb
|
||||
|
||||
SliderBackground = #aaaaaa
|
||||
SliderBorder = #888888
|
||||
SliderHandler = #bbbbbb
|
||||
SliderForeground = #333333
|
||||
|
||||
18
src/main/resources/ocelot/desktop/ocelot.conf
Normal file
18
src/main/resources/ocelot/desktop/ocelot.conf
Normal 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
|
||||
}
|
||||
}
|
||||
@ -3,7 +3,7 @@ package ocelot.desktop
|
||||
import li.flor.nativejfilechooser.NativeJFileChooser
|
||||
import ocelot.desktop.audio.{Audio, SoundSource}
|
||||
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 org.apache.commons.io.FileUtils
|
||||
import org.apache.logging.log4j.LogManager
|
||||
@ -36,6 +36,9 @@ object OcelotDesktop extends Logging {
|
||||
logger.info("Starting up Ocelot Desktop")
|
||||
|
||||
Ocelot.initialize(LogManager.getLogger(Ocelot))
|
||||
|
||||
val settingsFile = new File("ocelot.conf")
|
||||
Settings.load(settingsFile)
|
||||
ColorScheme.load(Source.fromURL(getClass.getResource("/ocelot/desktop/colorscheme.txt")))
|
||||
|
||||
createWorkspace()
|
||||
@ -72,6 +75,8 @@ object OcelotDesktop extends Logging {
|
||||
|
||||
Ocelot.shutdown()
|
||||
|
||||
Settings.save(settingsFile)
|
||||
|
||||
logger.info("Thanks for using Ocelot Desktop")
|
||||
System.exit(0)
|
||||
}
|
||||
@ -189,6 +194,10 @@ object OcelotDesktop extends Logging {
|
||||
}).start()
|
||||
}
|
||||
|
||||
def settings(): Unit = {
|
||||
UiHandler.root.modalDialogPool.pushDialog(new SettingsDialog())
|
||||
}
|
||||
|
||||
def cleanup(): Unit = {
|
||||
FileUtils.deleteDirectory(tmpPath.toFile)
|
||||
}
|
||||
|
||||
61
src/main/scala/ocelot/desktop/Settings.scala
Normal file
61
src/main/scala/ocelot/desktop/Settings.scala
Normal 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
package ocelot.desktop.audio
|
||||
|
||||
import ocelot.desktop.Settings
|
||||
import ocelot.desktop.util.Logging
|
||||
import org.lwjgl.openal.AL10
|
||||
|
||||
@ -45,7 +46,7 @@ object Audio extends Logging {
|
||||
private def _beep(pattern: String, frequency: Short, duration: Short): Unit = {
|
||||
val source = AL10.alGenSources()
|
||||
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.alSourcei(source, AL10.AL_LOOPING, AL10.AL_FALSE)
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
package ocelot.desktop.audio
|
||||
|
||||
import ocelot.desktop.Settings
|
||||
import ocelot.desktop.util.{Logging, Resource, ResourceManager}
|
||||
import org.lwjgl.openal.AL10
|
||||
|
||||
@ -13,7 +14,7 @@ class SoundSource(soundBuffer: SoundBuffer, looping: Boolean = false) extends Re
|
||||
sourceId = AL10.alGenSources()
|
||||
AL10.alSourcei(sourceId, AL10.AL_BUFFER, soundBuffer.getBufferId)
|
||||
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.alSourcei(sourceId, AL10.AL_LOOPING, if (looping) AL10.AL_TRUE else AL10.AL_FALSE)
|
||||
} else {
|
||||
@ -32,6 +33,12 @@ class SoundSource(soundBuffer: SoundBuffer, looping: Boolean = false) extends Re
|
||||
def pause(): Unit = if (isPlaying) AL10.alSourcePause(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 = {
|
||||
if (sourceId != -1) {
|
||||
stop()
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
package ocelot.desktop.ui.widget
|
||||
|
||||
import ocelot.desktop.OcelotDesktop
|
||||
import ocelot.desktop.geometry.Padding2D
|
||||
import ocelot.desktop.ui.layout.LinearLayout
|
||||
import ocelot.desktop.ui.widget.modal.ModalDialog
|
||||
|
||||
@ -23,6 +23,8 @@ class MenuBar extends Widget {
|
||||
menu.addEntry(new ContextMenuEntry("Exit", () => OcelotDesktop.exit()))
|
||||
}))
|
||||
|
||||
addEntry(new MenuBarButton("Settings", () => OcelotDesktop.settings()))
|
||||
|
||||
addEntry(new Widget {
|
||||
override def maximumSize: Size2D = Size2D(Float.PositiveInfinity, 1)
|
||||
}) // fill remaining space
|
||||
|
||||
79
src/main/scala/ocelot/desktop/ui/widget/SettingsDialog.scala
Normal file
79
src/main/scala/ocelot/desktop/ui/widget/SettingsDialog.scala
Normal 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))
|
||||
}
|
||||
45
src/main/scala/ocelot/desktop/ui/widget/Slider.scala
Normal file
45
src/main/scala/ocelot/desktop/ui/widget/Slider.scala
Normal 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)
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
7
src/main/scala/ocelot/desktop/util/MathUtils.scala
Normal file
7
src/main/scala/ocelot/desktop/util/MathUtils.scala
Normal 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)
|
||||
}
|
||||
}
|
||||
@ -19,6 +19,10 @@ object ResourceManager {
|
||||
}
|
||||
}
|
||||
|
||||
def forEach(predicate: Resource => Unit): Unit = {
|
||||
resources.foreach(predicate)
|
||||
}
|
||||
|
||||
def freeResource(resource: Resource): Unit = {
|
||||
resource.freeResource()
|
||||
resources -= resource
|
||||
|
||||
13
src/main/scala/ocelot/desktop/util/SettingsData.scala
Normal file
13
src/main/scala/ocelot/desktop/util/SettingsData.scala
Normal 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
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user