From 93a6a1ce7e93e97d16d8a897a1d198fe76c21651 Mon Sep 17 00:00:00 2001 From: Tim Allen Date: Wed, 8 Aug 2018 18:46:58 +1000 Subject: [PATCH] Update to v106r57 release. byuu says: I've added tool tips to hiro for Windows, GTK, and Qt. I'm unsure how to add them for Cocoa. I wasted am embarrassing ~14 hours implementing tool tips from scratch on Windows, because the `TOOLTIPS_CLASS` widget just absolutely refused to show up, no matter what I tried. As such, they're not quite 100% native, but I would really appreciate any patch submissions to help improve my implementation. I added tool tips to all of the confusing settings in bsnes. And of course, for those of you who don't like them, there's a configuration file setting to turn them off globally. I also improved Mega Drive handling of the Game Genie a bit, and restructured the way the Settings class works in bsnes. Starting now, I'm feature-freezing bsnes and higan. From this point forward: - polishing up and fixing bugs caused by the ruby/hiro changes - adding DRC to XAudio2, and maybe exclusive mode to WGL - correcting FEoEZ (English) to load and work again out of the box Once that's done, a final beta of bsnes will go out, I'll fix any reported bugs that I'm able to, and then v107 should be ready. This time with higan being functional, but marked as v107 beta. v108 will restore higan to production status again, alongside bsnes. --- higan/emulator/emulator.hpp | 2 +- higan/md/cartridge/cartridge.cpp | 17 +- higan/sfc/interface/configuration.cpp | 110 +++---- higan/sfc/interface/configuration.hpp | 3 + higan/target-bsnes/bsnes.cpp | 4 +- higan/target-bsnes/input/hotkeys.cpp | 4 +- higan/target-bsnes/input/input.cpp | 4 +- higan/target-bsnes/presentation/about.cpp | 2 +- .../presentation/presentation.cpp | 140 ++++----- .../presentation/presentation.hpp | 2 +- higan/target-bsnes/program/audio.cpp | 48 +-- higan/target-bsnes/program/input.cpp | 6 +- higan/target-bsnes/program/paths.cpp | 24 +- higan/target-bsnes/program/platform.cpp | 22 +- higan/target-bsnes/program/program.cpp | 14 +- higan/target-bsnes/program/utility.cpp | 2 +- higan/target-bsnes/program/video.cpp | 39 ++- .../resource/locales/japanese.bml | 2 +- higan/target-bsnes/settings/audio.cpp | 40 ++- higan/target-bsnes/settings/drivers.cpp | 85 ++++-- higan/target-bsnes/settings/emulator.cpp | 62 ++-- higan/target-bsnes/settings/hotkeys.cpp | 3 + higan/target-bsnes/settings/input.cpp | 11 +- higan/target-bsnes/settings/paths.cpp | 48 +-- higan/target-bsnes/settings/settings.cpp | 161 +++++----- higan/target-bsnes/settings/settings.hpp | 107 ++++++- higan/target-bsnes/settings/video.cpp | 12 +- higan/target-bsnes/tools/cheat-editor.cpp | 26 +- higan/target-bsnes/tools/state-manager.cpp | 3 + higan/target-bsnes/tools/tools.cpp | 6 - .../presentation/presentation.cpp | 285 ++++++++++++------ .../presentation/presentation.hpp | 28 +- higan/target-higan/program/game.cpp | 12 +- higan/target-higan/program/platform.cpp | 22 +- higan/target-higan/program/program.hpp | 6 +- higan/target-higan/program/utility.cpp | 29 +- higan/target-higan/settings/settings.cpp | 31 +- higan/target-higan/settings/settings.hpp | 17 +- higan/target-higan/settings/video.cpp | 43 ++- hiro/cocoa/sizable.cpp | 1 + hiro/cocoa/widget/widget.cpp | 7 +- hiro/cocoa/widget/widget.hpp | 1 + hiro/core/application.cpp | 18 +- hiro/core/application.hpp | 4 + hiro/core/core.hpp | 83 +---- hiro/core/object.hpp | 4 +- hiro/core/shared.hpp | 10 +- hiro/core/sizable.cpp | 9 + hiro/core/sizable.hpp | 5 +- hiro/core/widget/tab-frame-item.hpp | 37 +++ hiro/core/widget/tab-frame.hpp | 38 +++ hiro/core/widget/widget.cpp | 21 +- hiro/core/widget/widget.hpp | 8 +- hiro/extension/vertical-layout.cpp | 2 +- hiro/gtk/application.cpp | 52 +++- hiro/gtk/application.hpp | 15 +- hiro/gtk/keyboard.cpp | 8 +- hiro/gtk/mouse.cpp | 4 +- hiro/gtk/sizable.cpp | 1 + hiro/gtk/widget/widget.cpp | 8 +- hiro/gtk/widget/widget.hpp | 1 + hiro/gtk/window.cpp | 23 +- hiro/gtk/window.hpp | 1 - hiro/qt/application.cpp | 51 +++- hiro/qt/application.hpp | 12 +- hiro/qt/keyboard.cpp | 30 +- hiro/qt/sizable.cpp | 1 + hiro/qt/widget/widget.cpp | 9 +- hiro/qt/widget/widget.hpp | 1 + hiro/qt/window.cpp | 6 +- hiro/windows/application.cpp | 33 +- hiro/windows/application.hpp | 5 + hiro/windows/header.hpp | 8 + hiro/windows/platform.cpp | 2 + hiro/windows/platform.hpp | 2 + hiro/windows/sizable.cpp | 1 + hiro/windows/tool-tip.cpp | 161 ++++++++++ hiro/windows/tool-tip.hpp | 22 ++ hiro/windows/utility.cpp | 101 +++++-- hiro/windows/widget/button.cpp | 64 ++-- hiro/windows/widget/button.hpp | 3 +- hiro/windows/widget/canvas.cpp | 153 +++++----- hiro/windows/widget/canvas.hpp | 4 + hiro/windows/widget/check-button.cpp | 41 +-- hiro/windows/widget/check-button.hpp | 3 +- hiro/windows/widget/check-label.cpp | 3 +- hiro/windows/widget/combo-button.cpp | 3 +- hiro/windows/widget/frame.cpp | 3 +- hiro/windows/widget/hex-edit.cpp | 111 +++---- hiro/windows/widget/hex-edit.hpp | 2 +- hiro/windows/widget/horizontal-scroll-bar.cpp | 3 +- hiro/windows/widget/horizontal-slider.cpp | 3 +- hiro/windows/widget/label.cpp | 56 ++-- hiro/windows/widget/label.hpp | 2 + hiro/windows/widget/line-edit.cpp | 3 +- hiro/windows/widget/progress-bar.cpp | 3 +- hiro/windows/widget/radio-button.cpp | 41 +-- hiro/windows/widget/radio-button.hpp | 3 +- hiro/windows/widget/radio-label.cpp | 2 +- hiro/windows/widget/tab-frame.cpp | 15 +- hiro/windows/widget/tab-frame.hpp | 1 - hiro/windows/widget/table-view.cpp | 40 ++- hiro/windows/widget/table-view.hpp | 2 +- hiro/windows/widget/text-edit.cpp | 3 +- hiro/windows/widget/vertical-scroll-bar.cpp | 3 +- hiro/windows/widget/vertical-slider.cpp | 3 +- hiro/windows/widget/viewport.cpp | 100 +++--- hiro/windows/widget/viewport.hpp | 4 + hiro/windows/widget/widget.cpp | 81 ++++- hiro/windows/widget/widget.hpp | 12 +- icarus/heuristics/heuristics.cpp | 7 + icarus/heuristics/heuristics.hpp | 7 + icarus/heuristics/mega-drive.cpp | 6 +- nall/image.hpp | 2 +- nall/image/core.hpp | 2 +- nall/queue.hpp | 41 ++- nall/windows/utf8.hpp | 69 +++-- ruby/audio/audio.cpp | 8 +- ruby/audio/oss.cpp | 7 +- ruby/audio/pulseaudio.cpp | 4 +- ruby/audio/xaudio2.cpp | 113 +++---- 121 files changed, 2014 insertions(+), 1310 deletions(-) create mode 100644 hiro/core/widget/tab-frame-item.hpp create mode 100644 hiro/core/widget/tab-frame.hpp create mode 100644 hiro/windows/tool-tip.cpp create mode 100644 hiro/windows/tool-tip.hpp diff --git a/higan/emulator/emulator.hpp b/higan/emulator/emulator.hpp index 9266cc37..f857d9f8 100644 --- a/higan/emulator/emulator.hpp +++ b/higan/emulator/emulator.hpp @@ -13,7 +13,7 @@ using namespace nall; namespace Emulator { static const string Name = "higan"; - static const string Version = "106.56"; + static const string Version = "106.57"; static const string Author = "byuu"; static const string License = "GPLv3"; static const string Website = "https://byuu.org/"; diff --git a/higan/md/cartridge/cartridge.cpp b/higan/md/cartridge/cartridge.cpp index 70bcf96f..d5276ff1 100644 --- a/higan/md/cartridge/cartridge.cpp +++ b/higan/md/cartridge/cartridge.cpp @@ -70,18 +70,17 @@ auto Cartridge::load() -> bool { write = {&Cartridge::writeBanked, this}; } - if(patch) { + if(information.document["game/board/slot(type=MegaDrive)"]) { slot = new Cartridge{depth + 1}; if(!slot->load()) slot.reset(); - read = {&Cartridge::readLockOn, this}; - write = {&Cartridge::writeLockOn, this}; - } - if(rom.data[0x120>>1]==0x4761 || rom.data[0x120>>1]==0x6147) { - slot = new Cartridge{depth + 1}; - if(!slot->load()) slot.reset(); - read = {&Cartridge::readGameGenie, this}; - write = {&Cartridge::writeGameGenie, this}; + if(patch) { + read = {&Cartridge::readLockOn, this}; + write = {&Cartridge::writeLockOn, this}; + } else { + read = {&Cartridge::readGameGenie, this}; + write = {&Cartridge::writeGameGenie, this}; + } } //easter egg: power draw increases with each successively stacked cartridge diff --git a/higan/sfc/interface/configuration.cpp b/higan/sfc/interface/configuration.cpp index e6627249..03d2f807 100644 --- a/higan/sfc/interface/configuration.cpp +++ b/higan/sfc/interface/configuration.cpp @@ -1,92 +1,58 @@ Configuration configuration; +auto Configuration::process(Markup::Node document, bool load) -> void { + #define bind(type, path, name) \ + if(load) { \ + if(auto node = document[path]) name = node.type(); \ + } else { \ + document(path).setValue(name); \ + } \ + + bind(natural, "System/CPU/Version", system.cpu.version); + bind(natural, "System/PPU1/Version", system.ppu1.version); + bind(natural, "System/PPU1/VRAM/Size", system.ppu1.vram.size); + bind(natural, "System/PPU2/Version", system.ppu2.version); + + bind(boolean, "Video/BlurEmulation", video.blurEmulation); + bind(boolean, "Video/ColorEmulation", video.colorEmulation); + + bind(boolean, "Hacks/FastPPU/Enable", hacks.ppuFast.enable); + bind(boolean, "Hacks/FastPPU/NoSpriteLimit", hacks.ppuFast.noSpriteLimit); + bind(boolean, "Hacks/FastPPU/HiresMode7", hacks.ppuFast.hiresMode7); + bind(boolean, "Hacks/FastDSP/Enable", hacks.dspFast.enable); + + #undef bind +} + auto Configuration::read() -> string { - return { - "system\n" - " cpu version=", system.cpu.version, "\n" - " ppu1 version=", system.ppu1.version, "\n" - " vram size=0x", hex(system.ppu1.vram.size), "\n" - " ppu2 version=", system.ppu2.version, "\n" - "\n" - "video\n" - " blurEmulation: ", video.blurEmulation, "\n" - " colorEmulation: ", video.colorEmulation, "\n" - "\n" - "hacks\n" - " ppuFast\n" - " enable: ", hacks.ppuFast.enable, "\n" - " noSpriteLimit: ", hacks.ppuFast.noSpriteLimit, "\n" - " hiresMode7: ", hacks.ppuFast.hiresMode7, "\n" - " dspFast\n" - " enable: ", hacks.dspFast.enable, "\n" - }; + Markup::Node document; + process(document, false); + return BML::serialize(document, " "); } auto Configuration::read(string name) -> string { - #define bind(id) { \ - string key = {string{#id}.transform(".", "/")}; \ - if(name == key) return name; \ - } - bind(system.cpu.version); - bind(system.ppu1.version); - bind(system.ppu1.vram.size); - bind(system.ppu2.version); - - bind(video.blurEmulation); - bind(video.colorEmulation); - - bind(hacks.ppuFast.enable); - bind(hacks.ppuFast.noSpriteLimit); - bind(hacks.ppuFast.hiresMode7); - bind(hacks.dspFast.enable); - #undef bind - return {}; + auto document = BML::unserialize(read()); + return document[name].text(); } auto Configuration::write(string configuration) -> bool { *this = {}; - auto document = BML::unserialize(configuration); - if(!document) return false; - - #define bind(type, id) { \ - string key = {string{#id}.transform(".", "/")}; \ - if(auto node = document[key]) id = node.type(); \ + if(auto document = BML::unserialize(configuration)) { + return process(document, true), true; } - bind(natural, system.cpu.version); - bind(natural, system.ppu1.version); - bind(natural, system.ppu1.vram.size); - bind(natural, system.ppu2.version); - bind(boolean, video.blurEmulation); - bind(boolean, video.colorEmulation); - - bind(boolean, hacks.ppuFast.enable); - bind(boolean, hacks.ppuFast.noSpriteLimit); - bind(boolean, hacks.ppuFast.hiresMode7); - bind(boolean, hacks.dspFast.enable); - #undef bind - return true; + return false; } auto Configuration::write(string name, string value) -> bool { - #define bind(type, id) { \ - string key = {string{#id}.transform(".", "/")}; \ - if(name == key) return id = Markup::Node().setValue(value).type(), true; \ + if(SuperFamicom::system.loaded() && name.beginsWith("System/")) return false; + + auto document = BML::unserialize(read()); + if(auto node = document[name]) { + node.setValue(value); + return process(document, true), true; } - bind(boolean, video.blurEmulation); - bind(boolean, video.colorEmulation); - bind(boolean, hacks.ppuFast.enable); - bind(boolean, hacks.ppuFast.noSpriteLimit); - bind(boolean, hacks.ppuFast.hiresMode7); - bind(boolean, hacks.dspFast.enable); - if(SuperFamicom::system.loaded()) return false; - - bind(natural, system.cpu.version); - bind(natural, system.ppu1.version); - bind(natural, system.ppu1.vram.size); - bind(natural, system.ppu2.version); - #undef bind return false; } diff --git a/higan/sfc/interface/configuration.hpp b/higan/sfc/interface/configuration.hpp index dad1cdad..53aafce6 100644 --- a/higan/sfc/interface/configuration.hpp +++ b/higan/sfc/interface/configuration.hpp @@ -34,6 +34,9 @@ struct Configuration { bool enable = false; } dspFast; } hacks; + +private: + auto process(Markup::Node document, bool load) -> void; }; extern Configuration configuration; diff --git a/higan/target-bsnes/bsnes.cpp b/higan/target-bsnes/bsnes.cpp index 2ef8f701..bbaed88d 100644 --- a/higan/target-bsnes/bsnes.cpp +++ b/higan/target-bsnes/bsnes.cpp @@ -24,13 +24,15 @@ auto hiro::initialize() -> void { #include auto nall::main(vector arguments) -> void { + Application::setScreenSaver(settings.general.screenSaver); + Application::setToolTips(settings.general.toolTips); + string locale; // = "日本語"; for(auto argument : arguments) { if(argument.beginsWith("--locale=")) { locale = argument.trimLeft("--locale=", 1L); } } - Application::setScreenSaver(!settings["UserInterface/SuppressScreenSaver"].boolean()); Application::locale().scan(locate("locales/")); Application::locale().select(locale); emulator = new SuperFamicom::Interface; diff --git a/higan/target-bsnes/input/hotkeys.cpp b/higan/target-bsnes/input/hotkeys.cpp index 624ce281..b104b4cb 100644 --- a/higan/target-bsnes/input/hotkeys.cpp +++ b/higan/target-bsnes/input/hotkeys.cpp @@ -47,8 +47,8 @@ auto InputManager::bindHotkeys() -> void { video.setBlocking(false); audio.setBlocking(false); }).onRelease([] { - video.setBlocking(settings["Video/Blocking"].boolean()); - audio.setBlocking(settings["Audio/Blocking"].boolean()); + video.setBlocking(settings.video.blocking); + audio.setBlocking(settings.audio.blocking); })); hotkeys.append(InputHotkey("Pause Emulation").onPress([] { diff --git a/higan/target-bsnes/input/input.cpp b/higan/target-bsnes/input/input.cpp index d168a4da..151f5c1c 100644 --- a/higan/target-bsnes/input/input.cpp +++ b/higan/target-bsnes/input/input.cpp @@ -188,10 +188,10 @@ auto InputManager::initialize() -> void { input.onChange({&InputManager::onChange, this}); lastPoll = chrono::millisecond(); - frequency = max(1u, settings["Input/Frequency"].natural()); + frequency = max(1u, settings.input.frequency); turboCounter = 0; - turboFrequency = max(1, settings["Input/Turbo/Frequency"].natural()); + turboFrequency = max(1, settings.input.turbo.frequency); auto information = emulator->information(); auto ports = emulator->ports(); diff --git a/higan/target-bsnes/presentation/about.cpp b/higan/target-bsnes/presentation/about.cpp index f15cf7ab..ce64f5d2 100644 --- a/higan/target-bsnes/presentation/about.cpp +++ b/higan/target-bsnes/presentation/about.cpp @@ -14,7 +14,7 @@ auto AboutWindow::create() -> void { versionLabel.setText({tr("Version"), ":"}).setAlignment(1.0); versionValue.setText(Emulator::Version); authorLabel.setText({tr("Author"), ":"}).setAlignment(1.0); - authorValue.setText(Emulator::Author); + authorValue.setText(Emulator::Author).setToolTip("ビュウ"); licenseLabel.setText({tr("License"), ":"}).setAlignment(1.0); licenseValue.setText(Emulator::License); websiteLabel.setText({tr("Website"), ":"}).setAlignment(1.0); diff --git a/higan/target-bsnes/presentation/presentation.cpp b/higan/target-bsnes/presentation/presentation.cpp index 6f60ecc7..08905258 100644 --- a/higan/target-bsnes/presentation/presentation.cpp +++ b/higan/target-bsnes/presentation/presentation.cpp @@ -27,39 +27,39 @@ auto Presentation::create() -> void { updateSizeMenu(); outputMenu.setIcon(Icon::Emblem::Image).setText("Output"); centerViewport.setText("Center").onActivate([&] { - settings["View/Output"].setValue("Center"); + settings.video.output = "Center"; resizeViewport(); }); scaleViewport.setText("Scale").onActivate([&] { - settings["View/Output"].setValue("Scale"); + settings.video.output = "Scale"; resizeViewport(); }); stretchViewport.setText("Stretch").onActivate([&] { - settings["View/Output"].setValue("Stretch"); + settings.video.output = "Stretch"; resizeViewport(); }); - if(settings["View/Output"].text() == "Center") centerViewport.setChecked(); - if(settings["View/Output"].text() == "Scale") scaleViewport.setChecked(); - if(settings["View/Output"].text() == "Stretch") stretchViewport.setChecked(); - aspectCorrection.setText("Aspect Correction").setChecked(settings["View/AspectCorrection"].boolean()).onToggle([&] { - settings["View/AspectCorrection"].setValue(aspectCorrection.checked()); + if(settings.video.output == "Center") centerViewport.setChecked(); + if(settings.video.output == "Scale") scaleViewport.setChecked(); + if(settings.video.output == "Stretch") stretchViewport.setChecked(); + aspectCorrection.setText("Aspect Correction").setChecked(settings.video.aspectCorrection).onToggle([&] { + settings.video.aspectCorrection = aspectCorrection.checked(); resizeWindow(); }); - overscanCropping.setText("Overscan Cropping").setChecked(settings["View/OverscanCropping"].boolean()).onToggle([&] { - settings["View/OverscanCropping"].setValue(overscanCropping.checked()); + showOverscanArea.setText("Show Overscan Area").setChecked(settings.video.overscan).onToggle([&] { + settings.video.overscan = showOverscanArea.checked(); resizeWindow(); }); - blurEmulation.setText("Blur Emulation").setChecked(settings["View/BlurEmulation"].boolean()).onToggle([&] { - settings["View/BlurEmulation"].setValue(blurEmulation.checked()); - emulator->configure("video/blurEmulation", blurEmulation.checked()); + blurEmulation.setText("Blur Emulation").setChecked(settings.video.blur).onToggle([&] { + settings.video.blur = blurEmulation.checked(); + emulator->configure("Video/BlurEmulation", blurEmulation.checked()); }).doToggle(); shaderMenu.setIcon(Icon::Emblem::Image).setText("Shader"); - muteAudio.setText("Mute Audio").setChecked(settings["Audio/Mute"].boolean()).onToggle([&] { - settings["Audio/Mute"].setValue(muteAudio.checked()); + muteAudio.setText("Mute Audio").setChecked(settings.audio.mute).onToggle([&] { + settings.audio.mute = muteAudio.checked(); program.updateAudioEffects(); }); - showStatusBar.setText("Show Status Bar").setChecked(settings["UserInterface/ShowStatusBar"].boolean()).onToggle([&] { - settings["UserInterface/ShowStatusBar"].setValue(showStatusBar.checked()); + showStatusBar.setText("Show Status Bar").setChecked(settings.general.statusBar).onToggle([&] { + settings.general.statusBar = showStatusBar.checked(); if(!showStatusBar.checked()) { layout.remove(statusLayout); } else { @@ -112,9 +112,7 @@ auto Presentation::create() -> void { program.removeState("Quick/Redo"); } })); - speedMenu.setIcon(Icon::Device::Clock).setText("Speed").setEnabled( - !settings["Video/Blocking"].boolean() && settings["Audio/Blocking"].boolean() - ); + speedMenu.setIcon(Icon::Device::Clock).setText("Speed").setEnabled(!settings.video.blocking && settings.audio.blocking); speedSlowest.setText("50% (Slowest)").setProperty("multiplier", "2.0").onActivate([&] { program.updateAudioFrequency(); }); speedSlow.setText("75% (Slow)").setProperty("multiplier", "1.333").onActivate([&] { program.updateAudioFrequency(); }); speedNormal.setText("100% (Normal)").setProperty("multiplier", "1.0").onActivate([&] { program.updateAudioFrequency(); }); @@ -153,9 +151,7 @@ auto Presentation::create() -> void { icon.alphaBlend(0x000000); iconCanvas.setIcon(icon); - if(!settings["UserInterface/ShowStatusBar"].boolean()) { - layout.remove(statusLayout); - } + if(!settings.general.statusBar) layout.remove(statusLayout); auto font = Font().setBold(); auto back = Color{ 32, 32, 32}; @@ -240,21 +236,21 @@ auto Presentation::clearViewport() -> void { } auto Presentation::resizeViewport() -> void { - uint windowWidth = viewportLayout.geometry().width(); - uint windowHeight = viewportLayout.geometry().height(); + uint layoutWidth = viewportLayout.geometry().width(); + uint layoutHeight = viewportLayout.geometry().height(); - uint width = 256 * (settings["View/AspectCorrection"].boolean() ? 8.0 / 7.0 : 1.0); - uint height = (settings["View/OverscanCropping"].boolean() ? 224.0 : 240.0); + uint width = 256 * (settings.video.aspectCorrection ? 8.0 / 7.0 : 1.0); + uint height = (settings.video.overscan ? 240.0 : 224.0); uint viewportWidth, viewportHeight; if(visible() && !fullScreen()) { - uint widthMultiplier = windowWidth / width; - uint heightMultiplier = windowHeight / height; + uint widthMultiplier = layoutWidth / width; + uint heightMultiplier = layoutHeight / height; uint multiplier = max(1, min(widthMultiplier, heightMultiplier)); - settings["View/Multiplier"].setValue(multiplier); + settings.video.multiplier = multiplier; for(auto item : sizeGroup.objects()) { - if(auto property = item->property("multiplier")) { - if(property.natural() == multiplier) item->setChecked(); + if(auto property = item.property("multiplier")) { + if(property.natural() == multiplier) item.setChecked(); } } } @@ -262,26 +258,26 @@ auto Presentation::resizeViewport() -> void { if(!emulator->loaded()) return clearViewport(); if(!video) return; - if(settings["View/Output"].text() == "Center") { - uint widthMultiplier = windowWidth / width; - uint heightMultiplier = windowHeight / height; + if(settings.video.output == "Center") { + uint widthMultiplier = layoutWidth / width; + uint heightMultiplier = layoutHeight / height; uint multiplier = min(widthMultiplier, heightMultiplier); viewportWidth = width * multiplier; viewportHeight = height * multiplier; - } else if(settings["View/Output"].text() == "Scale") { - double widthMultiplier = (double)windowWidth / width; - double heightMultiplier = (double)windowHeight / height; + } else if(settings.video.output == "Scale") { + double widthMultiplier = (double)layoutWidth / width; + double heightMultiplier = (double)layoutHeight / height; double multiplier = min(widthMultiplier, heightMultiplier); viewportWidth = width * multiplier; viewportHeight = height * multiplier; - } else if(settings["View/Output"].text() == "Stretch" || 1) { - viewportWidth = windowWidth; - viewportHeight = windowHeight; + } else if(settings.video.output == "Stretch" || 1) { + viewportWidth = layoutWidth; + viewportHeight = layoutHeight; } //center viewport within viewportLayout by use of viewportLayout padding - uint paddingWidth = windowWidth - viewportWidth; - uint paddingHeight = windowHeight - viewportHeight; + uint paddingWidth = layoutWidth - viewportWidth; + uint paddingHeight = layoutHeight - viewportHeight; viewportLayout.setPadding({ paddingWidth / 2, paddingHeight / 2, paddingWidth - paddingWidth / 2, paddingHeight - paddingHeight / 2 @@ -294,26 +290,22 @@ auto Presentation::resizeWindow() -> void { if(fullScreen()) return; if(maximized()) setMaximized(false); - uint width = 256 * (settings["View/AspectCorrection"].boolean() ? 8.0 / 7.0 : 1.0); - uint height = (settings["View/OverscanCropping"].boolean() ? 224.0 : 240.0); - uint statusHeight = settings["UserInterface/ShowStatusBar"].boolean() ? StatusHeight : 0; + uint width = 256 * (settings.video.aspectCorrection ? 8.0 / 7.0 : 1.0); + uint height = (settings.video.overscan ? 240.0 : 224.0); + uint multiplier = max(1, settings.video.multiplier); + uint statusHeight = settings.general.statusBar ? StatusHeight : 0; - uint multiplier = settings["View/Multiplier"].natural(); - if(!multiplier) multiplier = 2; - - setMinimumSize({width, height + StatusHeight}); + setMinimumSize({width, height + statusHeight}); setSize({width * multiplier, height * multiplier + statusHeight}); resizeViewport(); } auto Presentation::toggleFullscreenMode() -> void { if(!fullScreen()) { - if(settings["UserInterface/ShowStatusBar"].boolean()) { - layout.remove(statusLayout); - } + if(settings.general.statusBar) layout.remove(statusLayout); menuBar.setVisible(false); setFullScreen(true); - video.setExclusive(settings["Video/Exclusive"].boolean()); + video.setExclusive(settings.video.exclusive); if(video.exclusive()) setVisible(false); if(!input.acquired()) input.acquire(); resizeViewport(); @@ -323,9 +315,7 @@ auto Presentation::toggleFullscreenMode() -> void { video.setExclusive(false); setFullScreen(false); menuBar.setVisible(true); - if(settings["UserInterface/ShowStatusBar"].boolean()) { - layout.append(statusLayout, Size{~0, StatusHeight}); - } + if(settings.general.statusBar) layout.append(statusLayout, Size{~0, StatusHeight}); resizeWindow(); setCentered(); } @@ -406,13 +396,13 @@ auto Presentation::updateSizeMenu() -> void { item.setProperty("multiplier", multiplier); item.setText({multiplier, "x (", 240 * multiplier, "p)"}); item.onActivate([=] { - settings["View/Multiplier"].setValue(multiplier); + settings.video.multiplier = multiplier; resizeWindow(); }); sizeGroup.append(item); } for(auto item : sizeGroup.objects()) { - if(settings["View/Multiplier"].natural() == item.property("multiplier").natural()) { + if(settings.video.multiplier == item.property("multiplier").natural()) { item.setChecked(); } } @@ -433,9 +423,9 @@ auto Presentation::updateStateMenus() -> void { if(auto item = action.cast()) { if(auto name = item.property("name")) { if(auto offset = states.find([&](auto& state) { return state.name == name; })) { - item.setText({item.property("title"), " [", chrono::local::datetime(states[*offset].date), "]"}); + item.setText({item.property("title"), " (", chrono::local::datetime(states[*offset].date), ")"}); } else { - item.setText({item.property("title"), " [Empty]"}); + item.setText({item.property("title"), " (empty)"}); } } } @@ -446,10 +436,10 @@ auto Presentation::updateStateMenus() -> void { if(auto name = item.property("name")) { if(auto offset = states.find([&](auto& state) { return state.name == name; })) { item.setEnabled(true); - item.setText({item.property("title"), " [", chrono::local::datetime(states[*offset].date), "]"}); + item.setText({item.property("title"), " (", chrono::local::datetime(states[*offset].date), ")"}); } else { item.setEnabled(false); - item.setText({item.property("title"), " [Empty]"}); + item.setText({item.property("title"), " (empty)"}); } } } @@ -461,7 +451,7 @@ auto Presentation::updateRecentGames() -> void { //remove missing games from list for(uint index = 0; index < RecentGames;) { - auto games = settings[string{"Game/Recent/", 1 + index}].text(); + auto games = settings[{"Game/Recent/", 1 + index}].text(); bool missing = false; if(games) { for(auto& game : games.split("|")) { @@ -472,8 +462,8 @@ auto Presentation::updateRecentGames() -> void { //will read one past the end of Games/Recent[RecentGames] by design: //this will always return an empty string to clear the last item in the list for(uint offset = index; offset < RecentGames; offset++) { - settings[string{"Game/Recent/", 1 + offset}].setValue( - settings[string{"Game/Recent/", 2 + offset}].text() + settings[{"Game/Recent/", 1 + offset}].setValue( + settings[{"Game/Recent/", 2 + offset}].text() ); } } else { @@ -484,7 +474,7 @@ auto Presentation::updateRecentGames() -> void { //update list for(auto index : range(RecentGames)) { MenuItem item; - if(auto game = settings[string{"Game/Recent/", 1 + index}].text()) { + if(auto game = settings[{"Game/Recent/", 1 + index}].text()) { string displayName; auto games = game.split("|"); for(auto& part : games) { @@ -498,7 +488,7 @@ auto Presentation::updateRecentGames() -> void { program.load(); }); } else { - item.setText({"[", tr("Empty"), "]"}); + item.setText({"(", tr("empty"), ")"}); item.setEnabled(false); } loadRecentGame.append(item); @@ -535,36 +525,36 @@ auto Presentation::updateShaders() -> void { MenuRadioItem none{&shaderMenu}; none.setText("None").onActivate([&] { - settings["Video/Shader"].setValue("None"); + settings.video.shader = "None"; program.updateVideoShader(); }); shaders.append(none); MenuRadioItem blur{&shaderMenu}; blur.setText("Blur").onActivate([&] { - settings["Video/Shader"].setValue("Blur"); + settings.video.shader = "Blur"; program.updateVideoShader(); }); shaders.append(blur); auto location = locate("shaders/"); - if(settings["Video/Driver"].text() == "OpenGL") { + if(settings.video.driver == "OpenGL") { for(auto shader : directory::folders(location, "*.shader")) { if(shaders.objectCount() == 2) shaderMenu.append(MenuSeparator()); MenuRadioItem item{&shaderMenu}; item.setText(string{shader}.trimRight(".shader/", 1L)).onActivate([=] { - settings["Video/Shader"].setValue({location, shader}); + settings.video.shader = {location, shader}; program.updateVideoShader(); }); shaders.append(item); } } - if(settings["Video/Shader"].text() == "None") none.setChecked(); - if(settings["Video/Shader"].text() == "Blur") blur.setChecked(); + if(settings.video.shader == "None") none.setChecked(); + if(settings.video.shader == "Blur") blur.setChecked(); for(auto item : shaders.objects()) { - if(settings["Video/Shader"].text() == string{location, item.text(), ".shader/"}) { + if(settings.video.shader == string{location, item.text(), ".shader/"}) { item.setChecked(); } } diff --git a/higan/target-bsnes/presentation/presentation.hpp b/higan/target-bsnes/presentation/presentation.hpp index b36b7f72..fd5143b4 100644 --- a/higan/target-bsnes/presentation/presentation.hpp +++ b/higan/target-bsnes/presentation/presentation.hpp @@ -62,7 +62,7 @@ struct Presentation : Window { Group outputGroup{¢erViewport, &scaleViewport, &stretchViewport}; MenuSeparator outputSeparator{&outputMenu}; MenuCheckItem aspectCorrection{&outputMenu}; - MenuCheckItem overscanCropping{&outputMenu}; + MenuCheckItem showOverscanArea{&outputMenu}; MenuCheckItem blurEmulation{&outputMenu}; Menu shaderMenu{&settingsMenu}; MenuSeparator settingsSeparatorA{&settingsMenu}; diff --git a/higan/target-bsnes/program/audio.cpp b/higan/target-bsnes/program/audio.cpp index 298ca567..9284acb2 100644 --- a/higan/target-bsnes/program/audio.cpp +++ b/higan/target-bsnes/program/audio.cpp @@ -1,12 +1,12 @@ auto Program::updateAudioDriver(Window parent) -> void { auto changed = (bool)audio; - audio.create(settings["Audio/Driver"].text()); + audio.create(settings.audio.driver); audio.setContext(presentation.viewport.handle()); audio.setChannels(2); if(changed) { - settings["Audio/Device"].setValue(audio.device()); - settings["Audio/Frequency"].setValue(audio.frequency()); - settings["Audio/Latency"].setValue(audio.latency()); + settings.audio.device = audio.device(); + settings.audio.frequency = audio.frequency(); + settings.audio.latency = audio.latency(); } updateAudioExclusive(); updateAudioDevice(); @@ -15,63 +15,65 @@ auto Program::updateAudioDriver(Window parent) -> void { if(!audio.ready()) { MessageDialog({ - "Error: failed to initialize [", settings["Audio/Driver"].text(), "] audio driver." + "Error: failed to initialize [", settings.audio.driver, "] audio driver." }).setParent(parent).error(); - settings["Audio/Driver"].setValue("None"); + settings.audio.driver = "None"; return updateAudioDriver(parent); } } auto Program::updateAudioExclusive() -> void { - audio.setExclusive(settings["Audio/Exclusive"].boolean()); + audio.setExclusive(settings.audio.exclusive); updateAudioFrequency(); updateAudioLatency(); } auto Program::updateAudioDevice() -> void { audio.clear(); - if(!audio.hasDevice(settings["Audio/Device"].text())) { - settings["Audio/Device"].setValue(audio.device()); + if(!audio.hasDevice(settings.audio.device)) { + settings.audio.device = audio.device(); } - audio.setDevice(settings["Audio/Device"].text()); + audio.setDevice(settings.audio.device); updateAudioFrequency(); updateAudioLatency(); } auto Program::updateAudioBlocking() -> void { audio.clear(); - audio.setBlocking(settings["Audio/Blocking"].boolean()); + audio.setBlocking(settings.audio.blocking); } auto Program::updateAudioDynamic() -> void { - audio.setDynamic(settings["Audio/Dynamic"].boolean()); + audio.setDynamic(settings.audio.dynamic); } auto Program::updateAudioFrequency() -> void { audio.clear(); - if(!audio.hasFrequency(settings["Audio/Frequency"].real())) { - settings["Audio/Frequency"].setValue(audio.frequency()); + if(!audio.hasFrequency(settings.audio.frequency)) { + settings.audio.frequency = audio.frequency(); } - audio.setFrequency(settings["Audio/Frequency"].real()); - double frequency = settings["Audio/Frequency"].real() + settings["Audio/Skew"].integer(); - for(auto item : presentation.speedGroup.objects()) { - if(item.checked()) frequency *= item.property("multiplier").real(); + audio.setFrequency(settings.audio.frequency); + double frequency = settings.audio.frequency + settings.audio.skew; + if(!settings.video.blocking && settings.audio.blocking) { + for(auto item : presentation.speedGroup.objects()) { + if(item.checked()) frequency *= item.property("multiplier").real(); + } } Emulator::audio.setFrequency(frequency); } auto Program::updateAudioLatency() -> void { audio.clear(); - if(!audio.hasLatency(settings["Audio/Latency"].natural())) { - settings["Audio/Latency"].setValue(audio.latency()); + if(!audio.hasLatency(settings.audio.latency)) { + settings.audio.latency = audio.latency(); } - audio.setLatency(settings["Audio/Latency"].natural()); + audio.setLatency(settings.audio.latency); } auto Program::updateAudioEffects() -> void { - double volume = settings["Audio/Mute"].boolean() ? 0.0 : settings["Audio/Volume"].natural() * 0.01; + double volume = settings.audio.mute ? 0.0 : settings.audio.volume * 0.01; Emulator::audio.setVolume(volume); - double balance = max(-1.0, min(+1.0, (settings["Audio/Balance"].integer() - 50) / 50.0)); + double balance = max(-1.0, min(+1.0, (settings.audio.balance - 50) / 50.0)); Emulator::audio.setBalance(balance); } diff --git a/higan/target-bsnes/program/input.cpp b/higan/target-bsnes/program/input.cpp index f2d7533e..609cd0f7 100644 --- a/higan/target-bsnes/program/input.cpp +++ b/higan/target-bsnes/program/input.cpp @@ -1,6 +1,6 @@ auto Program::updateInputDriver(Window parent) -> void { auto changed = (bool)input; - input.create(settings["Input/Driver"].text()); + input.create(settings.input.driver); input.setContext(presentation.viewport.handle()); if(changed) { } @@ -11,9 +11,9 @@ auto Program::updateInputDriver(Window parent) -> void { if(!input.ready()) { MessageDialog({ - "Error: failed to initialize [", settings["Input/Driver"].text(), "] input driver." + "Error: failed to initialize [", settings.input.driver, "] input driver." }).setParent(parent).error(); - settings["Input/Driver"].setValue("None"); + settings.input.driver = "None"; return updateInputDriver(parent); } } diff --git a/higan/target-bsnes/program/paths.cpp b/higan/target-bsnes/program/paths.cpp index 743d0fd6..c177bc6f 100644 --- a/higan/target-bsnes/program/paths.cpp +++ b/higan/target-bsnes/program/paths.cpp @@ -5,39 +5,27 @@ auto Program::path(string type, string location, string extension) -> string { auto suffix = extension; if(type == "Games") { - if(auto path = settings["Path/Games"].text()) { - pathname = path; - } + if(auto path = settings.path.games) pathname = path; } if(type == "Patches") { - if(auto path = settings["Path/Patches"].text()) { - pathname = path; - } + if(auto path = settings.path.patches) pathname = path; } if(type == "Saves") { - if(auto path = settings["Path/Saves"].text()) { - pathname = path; - } + if(auto path = settings.path.saves) pathname = path; } if(type == "Cheats") { - if(auto path = settings["Path/Cheats"].text()) { - pathname = path; - } + if(auto path = settings.path.cheats) pathname = path; } if(type == "States") { - if(auto path = settings["Path/States"].text()) { - pathname = path; - } + if(auto path = settings.path.states) pathname = path; } if(type == "Screenshots") { - if(auto path = settings["Path/Screenshots"].text()) { - pathname = path; - } + if(auto path = settings.path.screenshots) pathname = path; } return {pathname, prefix, suffix}; diff --git a/higan/target-bsnes/program/platform.cpp b/higan/target-bsnes/program/platform.cpp index fc3a0fb1..ab86fd2f 100644 --- a/higan/target-bsnes/program/platform.cpp +++ b/higan/target-bsnes/program/platform.cpp @@ -109,12 +109,12 @@ auto Program::load(uint id, string name, string type, vector options) -> superFamicom.location = gameQueue.takeLeft(); } else { dialog.setTitle("Load Super Famicom"); - dialog.setPath(path("Games", settings["Path/Recent/SuperFamicom"].text())); + dialog.setPath(path("Games", settings.path.recent.superFamicom)); dialog.setFilters({string{"Super Famicom Games|*.sfc:*.smc:*.zip"}}); superFamicom.location = dialog.openObject(); } if(inode::exists(superFamicom.location)) { - settings["Path/Recent/SuperFamicom"].setValue(Location::dir(superFamicom.location)); + settings.path.recent.superFamicom = Location::dir(superFamicom.location); if(loadSuperFamicom(superFamicom.location)) { return {id, dialog.option()}; } @@ -126,12 +126,12 @@ auto Program::load(uint id, string name, string type, vector options) -> gameBoy.location = gameQueue.takeLeft(); } else { dialog.setTitle("Load Game Boy"); - dialog.setPath(path("Games", settings["Path/Recent/GameBoy"].text())); + dialog.setPath(path("Games", settings.path.recent.gameBoy)); dialog.setFilters({string{"Game Boy Games|*.gb:*.gbc:*.zip"}}); gameBoy.location = dialog.openObject(); } if(inode::exists(gameBoy.location)) { - settings["Path/Recent/GameBoy"].setValue(Location::dir(gameBoy.location)); + settings.path.recent.gameBoy = Location::dir(gameBoy.location); if(loadGameBoy(gameBoy.location)) { return {id, dialog.option()}; } @@ -143,12 +143,12 @@ auto Program::load(uint id, string name, string type, vector options) -> bsMemory.location = gameQueue.takeLeft(); } else { dialog.setTitle("Load BS Memory"); - dialog.setPath(path("Games", settings["Path/Recent/BSMemory"].text())); + dialog.setPath(path("Games", settings.path.recent.bsMemory)); dialog.setFilters({string{"BS Memory Games|*.bs:*.zip"}}); bsMemory.location = dialog.openObject(); } if(inode::exists(bsMemory.location)) { - settings["Path/Recent/BSMemory"].setValue(Location::dir(bsMemory.location)); + settings.path.recent.bsMemory = Location::dir(bsMemory.location); if(loadBSMemory(bsMemory.location)) { return {id, dialog.option()}; } @@ -160,12 +160,12 @@ auto Program::load(uint id, string name, string type, vector options) -> sufamiTurboA.location = gameQueue.takeLeft(); } else { dialog.setTitle("Load Sufami Turbo - Slot A"); - dialog.setPath(path("Games", settings["Path/Recent/SufamiTurboA"].text())); + dialog.setPath(path("Games", settings.path.recent.sufamiTurboA)); dialog.setFilters({string{"Sufami Turbo Games|*.st:*.zip"}}); sufamiTurboA.location = dialog.openObject(); } if(inode::exists(sufamiTurboA.location)) { - settings["Path/Recent/SufamiTurboA"].setValue(Location::dir(sufamiTurboA.location)); + settings.path.recent.sufamiTurboA = Location::dir(sufamiTurboA.location); if(loadSufamiTurboA(sufamiTurboA.location)) { return {id, dialog.option()}; } @@ -177,12 +177,12 @@ auto Program::load(uint id, string name, string type, vector options) -> sufamiTurboB.location = gameQueue.takeLeft(); } else { dialog.setTitle("Load Sufami Turbo - Slot B"); - dialog.setPath(path("Games", settings["Path/Recent/SufamiTurboB"].text())); + dialog.setPath(path("Games", settings.path.recent.sufamiTurboB)); dialog.setFilters({string{"Sufami Turbo Games|*.st:*.zip"}}); sufamiTurboB.location = dialog.openObject(); } if(inode::exists(sufamiTurboB.location)) { - settings["Path/Recent/SufamiTurboB"].setValue(Location::dir(sufamiTurboB.location)); + settings.path.recent.sufamiTurboB = Location::dir(sufamiTurboB.location); if(loadSufamiTurboB(sufamiTurboB.location)) { return {id, dialog.option()}; } @@ -205,7 +205,7 @@ auto Program::videoRefresh(uint display, const uint32* data, uint pitch, uint wi screenshot.height = height; pitch >>= 2; - if(presentation.overscanCropping.checked()) { + if(!presentation.showOverscanArea.checked()) { if(height == 240) data += 8 * pitch, height -= 16; if(height == 480) data += 16 * pitch, height -= 32; } diff --git a/higan/target-bsnes/program/program.cpp b/higan/target-bsnes/program/program.cpp index ef94d816..f67c4a16 100644 --- a/higan/target-bsnes/program/program.cpp +++ b/higan/target-bsnes/program/program.cpp @@ -37,22 +37,22 @@ auto Program::create(vector arguments) -> void { stateManager.create(); manifestViewer.create(); - if(settings["Crashed"].boolean()) { + if(settings.general.crashed) { MessageDialog( "Driver crash detected. Hardware drivers have been disabled.\n" "Please reconfigure drivers in the advanced settings panel." ).setParent(*presentation).information(); - settings["Video/Driver"].setValue("None"); - settings["Audio/Driver"].setValue("None"); - settings["Input/Driver"].setValue("None"); + settings.video.driver = "None"; + settings.audio.driver = "None"; + settings.input.driver = "None"; } - settings["Crashed"].setValue(true); + settings.general.crashed = true; settings.save(); updateVideoDriver(presentation); updateAudioDriver(presentation); updateInputDriver(presentation); - settings["Crashed"].setValue(false); + settings.general.crashed = false; settings.save(); driverSettings.videoDriverChanged(); @@ -89,7 +89,7 @@ auto Program::main() -> void { emulator->run(); if(emulatorSettings.autoSaveMemory.checked()) { auto currentTime = chrono::timestamp(); - if(currentTime - autoSaveTime >= settings["Emulator/AutoSaveMemory/Interval"].natural()) { + if(currentTime - autoSaveTime >= settings.emulator.autoSaveMemory.interval) { autoSaveTime = currentTime; emulator->save(); } diff --git a/higan/target-bsnes/program/utility.cpp b/higan/target-bsnes/program/utility.cpp index 72682f6c..97375540 100644 --- a/higan/target-bsnes/program/utility.cpp +++ b/higan/target-bsnes/program/utility.cpp @@ -46,7 +46,7 @@ auto Program::captureScreenshot() -> bool { auto width = capture.width(); auto height = capture.height(); - if(presentation.overscanCropping.checked()) { + if(!presentation.showOverscanArea.checked()) { if(height == 240) data += 8 * pitch, height -= 16; if(height == 480) data += 16 * pitch, height -= 32; } diff --git a/higan/target-bsnes/program/video.cpp b/higan/target-bsnes/program/video.cpp index 99366674..6bd94a3b 100644 --- a/higan/target-bsnes/program/video.cpp +++ b/higan/target-bsnes/program/video.cpp @@ -1,9 +1,9 @@ auto Program::updateVideoDriver(Window parent) -> void { auto changed = (bool)video; - video.create(settings["Video/Driver"].text()); + video.create(settings.video.driver); video.setContext(presentation.viewport.handle()); if(changed) { - settings["Video/Format"].setValue(video.format()); + settings.video.format = video.format(); } updateVideoExclusive(); updateVideoBlocking(); @@ -22,9 +22,9 @@ auto Program::updateVideoDriver(Window parent) -> void { if(!video.ready()) { MessageDialog({ - "Error: failed to initialize [", settings["Video/Driver"].text(), "] video driver." + "Error: failed to initialize [", settings.video.driver, "] video driver." }).setParent(parent).error(); - settings["Video/Driver"].setValue("None"); + settings.video.driver = "None"; return updateVideoDriver(parent); } @@ -37,40 +37,37 @@ auto Program::updateVideoExclusive() -> void { } auto Program::updateVideoBlocking() -> void { - video.setBlocking(settings["Video/Blocking"].boolean()); + video.setBlocking(settings.video.blocking); } auto Program::updateVideoFlush() -> void { - video.setFlush(settings["Video/Flush"].boolean()); + video.setFlush(settings.video.flush); } auto Program::updateVideoFormat() -> void { - if(!video.hasFormat(settings["Video/Format"].text())) { - settings["Video/Format"].setValue(video.format()); + if(!video.hasFormat(settings.video.format)) { + settings.video.format = video.format(); } - video.setFormat(settings["Video/Format"].text()); + video.setFormat(settings.video.format); } auto Program::updateVideoShader() -> void { - if(settings["Video/Driver"].text() == "OpenGL" - && settings["Video/Shader"].text() != "None" - && settings["Video/Shader"].text() != "Blur" + if(settings.video.driver == "OpenGL" + && settings.video.shader != "None" + && settings.video.shader != "Blur" ) { video.setSmooth(false); - video.setShader(settings["Video/Shader"].text()); + video.setShader(settings.video.shader); } else { - video.setSmooth(settings["Video/Shader"].text() == "Blur"); + video.setSmooth(settings.video.shader == "Blur"); video.setShader(""); } } auto Program::updateVideoPalette() -> void { - emulator->configure("video/colorEmulation", false); - double luminance = settings["Video/Luminance"].natural() / 100.0; - double saturation = settings["Video/Saturation"].natural() / 100.0; - double gamma = settings["Video/Gamma"].natural() / 100.0; - Emulator::video.setLuminance(luminance); - Emulator::video.setSaturation(saturation); - Emulator::video.setGamma(gamma); + emulator->configure("Video/ColorEmulation", false); + Emulator::video.setLuminance(settings.video.luminance / 100.0); + Emulator::video.setSaturation(settings.video.saturation / 100.0); + Emulator::video.setGamma(settings.video.gamma / 100.0); Emulator::video.setPalette(); } diff --git a/higan/target-bsnes/resource/locales/japanese.bml b/higan/target-bsnes/resource/locales/japanese.bml index 510f29a8..1712d7e2 100644 --- a/higan/target-bsnes/resource/locales/japanese.bml +++ b/higan/target-bsnes/resource/locales/japanese.bml @@ -45,7 +45,7 @@ namespace: Presentation input: Load Recent Game value: 最新ゲームを読み込み map - input: Empty + input: empty value: なし map input: Clear List diff --git a/higan/target-bsnes/settings/audio.cpp b/higan/target-bsnes/settings/audio.cpp index d2babee7..ab9e539e 100644 --- a/higan/target-bsnes/settings/audio.cpp +++ b/higan/target-bsnes/settings/audio.cpp @@ -7,27 +7,43 @@ auto AudioSettings::create() -> void { effectsLabel.setFont(Font().setBold()).setText("Effects"); effectsLayout.setSize({3, 3}); effectsLayout.column(0).setAlignment(1.0); - skewLabel.setText("Skew:"); - skewValue.setAlignment(0.5); - skewSlider.setLength(10001).setPosition(settings["Audio/Skew"].integer() + 5000).onChange([&] { + skewLabel.setText("Skew:").setToolTip( + "Adjusts the audio frequency by the skew amount (in hz.)\n\n" + "This is essentially static rate control:\n" + "First, enable both video and audio sync.\n" + "Then, raise or lower this value to try to reduce errors.\n" + "One direction will help video, but hurt audio.\n" + "The other direction will do the reverse.\n" + "The idea is to find the best middle ground.\n\n" + "You should leave this at 0 when using dynamic rate control." + ); + skewValue.setAlignment(0.5).setToolTip(skewLabel.toolTip()); + skewSlider.setLength(10001).setPosition(settings.audio.skew + 5000).onChange([&] { string value = {skewSlider.position() > 5000 ? "+" : "", (int)skewSlider.position() - 5000}; - settings["Audio/Skew"].setValue(value); + settings.audio.skew = value.integer(); skewValue.setText(value); program.updateAudioFrequency(); }).doChange(); - volumeLabel.setText("Volume:"); - volumeValue.setAlignment(0.5); - volumeSlider.setLength(201).setPosition(settings["Audio/Volume"].natural()).onChange([&] { + volumeLabel.setText("Volume:").setToolTip( + "Adjusts the audio output volume.\n\n" + "You should not use values above 100%, if possible!\n" + "If you do, audio clipping distortion can occur." + ); + volumeValue.setAlignment(0.5).setToolTip(volumeLabel.toolTip()); + volumeSlider.setLength(201).setPosition(settings.audio.volume).onChange([&] { string value = {volumeSlider.position(), "%"}; - settings["Audio/Volume"].setValue(value); + settings.audio.volume = value.natural(); volumeValue.setText(value); program.updateAudioEffects(); }).doChange(); - balanceLabel.setText("Balance:"); - balanceValue.setAlignment(0.5); - balanceSlider.setLength(101).setPosition(settings["Audio/Balance"].natural()).onChange([&] { + balanceLabel.setText("Balance:").setToolTip( + "Pans audio to the left (lower values) or right (higher values.)\n\n" + "50% (centered) is the recommended setting." + ); + balanceValue.setAlignment(0.5).setToolTip(balanceLabel.toolTip()); + balanceSlider.setLength(101).setPosition(settings.audio.balance).onChange([&] { string value = {balanceSlider.position(), "%"}; - settings["Audio/Balance"].setValue(value); + settings.audio.balance = value.natural(); balanceValue.setText(value); program.updateAudioEffects(); }).doChange(); diff --git a/higan/target-bsnes/settings/drivers.cpp b/higan/target-bsnes/settings/drivers.cpp index 0cb9fbd0..28ab3d12 100644 --- a/higan/target-bsnes/settings/drivers.cpp +++ b/higan/target-bsnes/settings/drivers.cpp @@ -13,17 +13,33 @@ auto DriverSettings::create() -> void { videoDriverUpdate.setText("Change").onActivate([&] { videoDriverChange(); }); videoFormatLabel.setText("Format:"); videoFormatOption.onChange([&] { videoFormatChange(); }); - videoExclusiveToggle.setText("Exclusive fullscreen").onToggle([&] { - settings["Video/Exclusive"].setValue(videoExclusiveToggle.checked()); + videoExclusiveToggle.setText("Exclusive").setToolTip( + "(Direct3D only)\n\n" + "Acquires exclusive access to the display in fullscreen mode.\n" + "Eliminates compositing issues such as video stuttering." + ).onToggle([&] { + settings.video.exclusive = videoExclusiveToggle.checked(); program.updateVideoExclusive(); }); - videoBlockingToggle.setText("Synchronize").onToggle([&] { - settings["Video/Blocking"].setValue(videoBlockingToggle.checked()); + videoBlockingToggle.setText("Synchronize").setToolTip( + "Waits for the video card to be ready before rendering frames.\n" + "Eliminates dropped or duplicated frames; but can distort audio.\n\n" + "With this option, it's recommended to disable audio sync,\n" + "and enable dynamic rate control. Or alternatively, adjust the\n" + "audio skew option to reduce buffer under/overflows." + ).onToggle([&] { + settings.video.blocking = videoBlockingToggle.checked(); program.updateVideoBlocking(); presentation.speedMenu.setEnabled(!videoBlockingToggle.checked() && audioBlockingToggle.checked()); }); - videoFlushToggle.setText("GPU sync").onToggle([&] { - settings["Video/Flush"].setValue(videoFlushToggle.checked()); + videoFlushToggle.setText("GPU sync").setToolTip({ + "(OpenGL only)\n\n" + "Causes the GPU to wait until frames are fully rendered.\n" + "In the best case, this can remove up to one frame of input lag.\n" + "However, it incurs a roughly 20% performance penalty.\n\n" + "You should disable this option unless you find it necessary." + }).onToggle([&] { + settings.video.flush = videoFlushToggle.checked(); program.updateVideoFlush(); }); videoSpacer.setColor({192, 192, 192}); @@ -41,17 +57,34 @@ auto DriverSettings::create() -> void { audioFrequencyOption.onChange([&] { audioFrequencyChange(); }); audioLatencyLabel.setText("Latency:"); audioLatencyOption.onChange([&] { audioLatencyChange(); }); - audioExclusiveToggle.setText("Exclusive").onToggle([&] { - settings["Audio/Exclusive"].setValue(audioExclusiveToggle.checked()); + audioExclusiveToggle.setText("Exclusive").setToolTip( + "(ASIO, WASAPI only)\n\n" + "Acquires exclusive control of the sound card device.\n" + "This can significantly reduce audio latency.\n" + "However, it will block sounds from all other applications." + ).onToggle([&] { + settings.audio.exclusive = audioExclusiveToggle.checked(); program.updateAudioExclusive(); }); - audioBlockingToggle.setText("Synchronize").onToggle([&] { - settings["Audio/Blocking"].setValue(audioBlockingToggle.checked()); + audioBlockingToggle.setText("Synchronize").setToolTip( + "Waits for the audio card to be ready before outputting samples.\n" + "Eliminates audio distortio; but can distort video.\n\n" + "With this option, it's recommended to disable video sync.\n" + "For best results, use this with an adaptive sync monitor." + ).onToggle([&] { + settings.audio.blocking = audioBlockingToggle.checked(); program.updateAudioBlocking(); presentation.speedMenu.setEnabled(!videoBlockingToggle.checked() && audioBlockingToggle.checked()); }); - audioDynamicToggle.setText("Dynamic rate").onToggle([&] { - settings["Audio/Dynamic"].setValue(audioDynamicToggle.checked()); + audioDynamicToggle.setText("Dynamic rate").setToolTip( + "(OSS only)\n\n" + "Dynamically adjusts the audio frequency by tiny amounts.\n" + "Use this with video sync enabled, and audio sync disabled.\n\n" + "This can produce perfectly smooth video and clean audio,\n" + "but only if your monitor refresh rate is set correctly:\n" + "60hz for NTSC games, and 50hz for PAL games." + ).onToggle([&] { + settings.audio.dynamic = audioDynamicToggle.checked(); program.updateAudioDynamic(); }); audioSpacer.setColor({192, 192, 192}); @@ -90,7 +123,7 @@ auto DriverSettings::videoDriverChanged() -> void { auto DriverSettings::videoDriverChange() -> void { auto item = videoDriverOption.selected(); - settings["Video/Driver"].setValue(item.text()); + settings.video.driver = item.text(); if(!emulator->loaded() || item.text() == "None" || MessageDialog( "Warning: incompatible drivers may cause bsnes to crash.\n" "It is highly recommended you unload your game first to be safe.\n" @@ -98,10 +131,10 @@ auto DriverSettings::videoDriverChange() -> void { ).setParent(*settingsWindow).question() == "Yes") { program.save(); program.saveUndoState(); - settings["Crashed"].setValue(true); + settings.general.crashed = true; settings.save(); program.updateVideoDriver(settingsWindow); - settings["Crashed"].setValue(false); + settings.general.crashed = false; settings.save(); videoDriverChanged(); } @@ -120,7 +153,7 @@ auto DriverSettings::videoFormatChanged() -> void { auto DriverSettings::videoFormatChange() -> void { auto item = videoFormatOption.selected(); - settings["Video/Format"].setValue(item.text()); + settings.video.format = item.text(); video.setFormat(item.text()); } @@ -146,7 +179,7 @@ auto DriverSettings::audioDriverChanged() -> void { auto DriverSettings::audioDriverChange() -> void { auto item = audioDriverOption.selected(); - settings["Audio/Driver"].setValue(item.text()); + settings.audio.driver = item.text(); if(!emulator->loaded() || item.text() == "None" || MessageDialog( "Warning: incompatible drivers may cause bsnes to crash.\n" "It is highly recommended you unload your game first to be safe.\n" @@ -154,10 +187,10 @@ auto DriverSettings::audioDriverChange() -> void { ).setParent(*settingsWindow).question() == "Yes") { program.save(); program.saveUndoState(); - settings["Crashed"].setValue(true); + settings.general.crashed = true; settings.save(); program.updateAudioDriver(settingsWindow); - settings["Crashed"].setValue(false); + settings.general.crashed = false; settings.save(); audioDriverChanged(); } @@ -176,7 +209,7 @@ auto DriverSettings::audioDeviceChanged() -> void { auto DriverSettings::audioDeviceChange() -> void { auto item = audioDeviceOption.selected(); - settings["Audio/Device"].setValue(item.text()); + settings.audio.device = item.text(); program.updateAudioDevice(); audioFrequencyChanged(); audioLatencyChanged(); @@ -186,7 +219,7 @@ auto DriverSettings::audioFrequencyChanged() -> void { audioFrequencyOption.reset(); for(auto& frequency : audio.hasFrequencies()) { ComboButtonItem item{&audioFrequencyOption}; - item.setText({(uint)frequency, "hz"}); + item.setText({frequency, "hz"}); if(frequency == audio.frequency()) item.setSelected(); } //audioFrequencyOption.setEnabled(audio->hasFrequency()); @@ -195,7 +228,7 @@ auto DriverSettings::audioFrequencyChanged() -> void { auto DriverSettings::audioFrequencyChange() -> void { auto item = audioFrequencyOption.selected(); - settings["Audio/Frequency"].setValue(item.text()); + settings.audio.frequency = item.text().natural(); program.updateAudioFrequency(); } @@ -212,7 +245,7 @@ auto DriverSettings::audioLatencyChanged() -> void { auto DriverSettings::audioLatencyChange() -> void { auto item = audioLatencyOption.selected(); - settings["Audio/Latency"].setValue(item.text()); + settings.audio.latency = item.text().natural(); program.updateAudioLatency(); } @@ -232,7 +265,7 @@ auto DriverSettings::inputDriverChanged() -> void { auto DriverSettings::inputDriverChange() -> void { auto item = inputDriverOption.selected(); - settings["Input/Driver"].setValue(item.text()); + settings.input.driver = item.text(); if(!emulator->loaded() || item.text() == "None" || MessageDialog( "Warning: incompatible drivers may cause bsnes to crash.\n" "It is highly recommended you unload your game first to be safe.\n" @@ -240,10 +273,10 @@ auto DriverSettings::inputDriverChange() -> void { ).setParent(*settingsWindow).question() == "Yes") { program.save(); program.saveUndoState(); - settings["Crashed"].setValue(true); + settings.general.crashed = true; settings.save(); program.updateInputDriver(settingsWindow); - settings["Crashed"].setValue(false); + settings.general.crashed = false; settings.save(); inputDriverChanged(); } diff --git a/higan/target-bsnes/settings/emulator.cpp b/higan/target-bsnes/settings/emulator.cpp index 3d35b1c4..5fb04ca3 100644 --- a/higan/target-bsnes/settings/emulator.cpp +++ b/higan/target-bsnes/settings/emulator.cpp @@ -7,43 +7,39 @@ auto EmulatorSettings::create() -> void { optionsLabel.setText("Options").setFont(Font().setBold()); inputFocusLabel.setText("When focus is lost:"); pauseEmulation.setText("Pause emulation").onActivate([&] { - settings["Input/Defocus"].setValue("Pause"); + settings.input.defocus = "Pause"; }); blockInput.setText("Block input").onActivate([&] { - settings["Input/Defocus"].setValue("Block"); + settings.input.defocus = "Block"; }); allowInput.setText("Allow input").onActivate([&] { - settings["Input/Defocus"].setValue("Allow"); + settings.input.defocus = "Allow"; }); - if(settings["Input/Defocus"].text() == "Pause") pauseEmulation.setChecked(); - if(settings["Input/Defocus"].text() == "Block") blockInput.setChecked(); - if(settings["Input/Defocus"].text() == "Allow") allowInput.setChecked(); - warnOnUnverifiedGames.setText("Warn when loading games that have not been verified").setChecked(settings["Emulator/WarnOnUnverifiedGames"].boolean()).onToggle([&] { - settings["Emulator/WarnOnUnverifiedGames"].setValue(warnOnUnverifiedGames.checked()); + if(settings.input.defocus == "Pause") pauseEmulation.setChecked(); + if(settings.input.defocus == "Block") blockInput.setChecked(); + if(settings.input.defocus == "Allow") allowInput.setChecked(); + warnOnUnverifiedGames.setText("Warn when loading games that have not been verified").setChecked(settings.emulator.warnOnUnverifiedGames).onToggle([&] { + settings.emulator.warnOnUnverifiedGames = warnOnUnverifiedGames.checked(); }); - autoSaveMemory.setText("Auto-save memory periodically").setChecked(settings["Emulator/AutoSaveMemory/Enable"].boolean()).onToggle([&] { - settings["Emulator/AutoSaveMemory/Enable"].setValue(autoSaveMemory.checked()); + autoSaveMemory.setText("Auto-save memory periodically").setChecked(settings.emulator.autoSaveMemory.enable).onToggle([&] { + settings.emulator.autoSaveMemory.enable = autoSaveMemory.checked(); }); - autoSaveStateOnUnload.setText("Auto-save undo state when unloading games").setChecked(settings["Emulator/AutoSaveStateOnUnload"].boolean()).onToggle([&] { - settings["Emulator/AutoSaveStateOnUnload"].setValue(autoSaveStateOnUnload.checked()); + autoSaveStateOnUnload.setText("Auto-save undo state when unloading games").setChecked(settings.emulator.autoSaveStateOnUnload).onToggle([&] { + settings.emulator.autoSaveStateOnUnload = autoSaveStateOnUnload.checked(); if(!autoSaveStateOnUnload.checked()) { autoLoadStateOnLoad.setEnabled(false).setChecked(false).doToggle(); } else { autoLoadStateOnLoad.setEnabled(true); } }).doToggle(); - autoLoadStateOnLoad.setText("Auto-resume on load").setChecked(settings["Emulator/AutoLoadStateOnLoad"].boolean()).onToggle([&] { - settings["Emulator/AutoLoadStateOnLoad"].setValue(autoLoadStateOnLoad.checked()); - }); - suppressScreenSaver.setText("Suppress screen saver").setChecked(settings["UserInterface/SuppressScreenSaver"].boolean()).onToggle([&] { - settings["UserInterface/SuppressScreenSaver"].setValue(suppressScreenSaver.checked()); - Application::setScreenSaver(!suppressScreenSaver.checked()); + autoLoadStateOnLoad.setText("Auto-resume on load").setChecked(settings.emulator.autoLoadStateOnLoad).onToggle([&] { + settings.emulator.autoLoadStateOnLoad = autoLoadStateOnLoad.checked(); }); optionsSpacer.setColor({192, 192, 192}); hacksLabel.setText("Hacks").setFont(Font().setBold()); - fastPPUOption.setText("Fast PPU").setChecked(settings["Emulator/Hack/FastPPU"].boolean()).onToggle([&] { - settings["Emulator/Hack/FastPPU"].setValue(fastPPUOption.checked()); + fastPPUOption.setText("Fast PPU").setChecked(settings.emulator.hack.fastPPU.enable).onToggle([&] { + settings.emulator.hack.fastPPU.enable = fastPPUOption.checked(); if(!fastPPUOption.checked()) { noSpriteLimit.setEnabled(false).setChecked(false).doToggle(); hiresMode7.setEnabled(false).setChecked(false).doToggle(); @@ -52,27 +48,27 @@ auto EmulatorSettings::create() -> void { hiresMode7.setEnabled(true); } }).doToggle(); - noSpriteLimit.setText("No sprite limit").setChecked(settings["Emulator/Hack/FastPPU/NoSpriteLimit"].boolean()).onToggle([&] { - settings["Emulator/Hack/FastPPU/NoSpriteLimit"].setValue(noSpriteLimit.checked()); + noSpriteLimit.setText("No sprite limit").setChecked(settings.emulator.hack.fastPPU.noSpriteLimit).onToggle([&] { + settings.emulator.hack.fastPPU.noSpriteLimit = noSpriteLimit.checked(); }); - hiresMode7.setText("Hires mode 7").setChecked(settings["Emulator/Hack/FastPPU/HiresMode7"].boolean()).onToggle([&] { - settings["Emulator/Hack/FastPPU/HiresMode7"].setValue(hiresMode7.checked()); + hiresMode7.setText("Hires mode 7").setChecked(settings.emulator.hack.fastPPU.hiresMode7).onToggle([&] { + settings.emulator.hack.fastPPU.hiresMode7 = hiresMode7.checked(); }); - fastDSPOption.setText("Fast DSP").setChecked(settings["Emulator/Hack/FastDSP"].boolean()).onToggle([&] { - settings["Emulator/Hack/FastDSP"].setValue(fastDSPOption.checked()); + fastDSPOption.setText("Fast DSP").setChecked(settings.emulator.hack.fastDSP.enable).onToggle([&] { + settings.emulator.hack.fastDSP.enable = fastDSPOption.checked(); }); superFXLabel.setText("SuperFX clock speed:"); superFXValue.setAlignment(0.5); - superFXClock.setLength(71).setPosition((settings["Emulator/Hack/FastSuperFX"].natural() - 100) / 10).onChange([&] { - settings["Emulator/Hack/FastSuperFX"].setValue({superFXClock.position() * 10 + 100, "%"}); - superFXValue.setText(settings["Emulator/Hack/FastSuperFX"].text()); + superFXClock.setLength(71).setPosition((settings.emulator.hack.fastSuperFX - 100) / 10).onChange([&] { + settings.emulator.hack.fastSuperFX = superFXClock.position() * 10 + 100; + superFXValue.setText({settings.emulator.hack.fastSuperFX, "%"}); }).doChange(); hacksNote.setForegroundColor({224, 0, 0}).setText("Note: some hack setting changes do not take effect until after reloading games."); } auto EmulatorSettings::updateConfiguration() -> void { - emulator->configure("hacks/ppuFast/enable", fastPPUOption.checked()); - emulator->configure("hacks/ppuFast/noSpriteLimit", noSpriteLimit.checked()); - emulator->configure("hacks/ppuFast/hiresMode7", hiresMode7.checked()); - emulator->configure("hacks/dspFast/enable", fastDSPOption.checked()); + emulator->configure("Hacks/FastPPU/Enable", fastPPUOption.checked()); + emulator->configure("Hacks/FastPPU/NoSpriteLimit", noSpriteLimit.checked()); + emulator->configure("Hacks/FastPPU/HiresMode7", hiresMode7.checked()); + emulator->configure("Hacks/FastDSP/Enable", fastDSPOption.checked()); } diff --git a/higan/target-bsnes/settings/hotkeys.cpp b/higan/target-bsnes/settings/hotkeys.cpp index c70ef97f..432e5c2d 100644 --- a/higan/target-bsnes/settings/hotkeys.cpp +++ b/higan/target-bsnes/settings/hotkeys.cpp @@ -13,6 +13,9 @@ auto HotkeySettings::create() -> void { assignButton.setEnabled(batched.size() == 1); clearButton.setEnabled(batched.size() >= 1); }); + mappingList.onSize([&] { + mappingList.resizeColumns(); + }); assignButton.setText("Assign").onActivate([&] { assignMapping(); }); diff --git a/higan/target-bsnes/settings/input.cpp b/higan/target-bsnes/settings/input.cpp index 993751e2..e5c01b2e 100644 --- a/higan/target-bsnes/settings/input.cpp +++ b/higan/target-bsnes/settings/input.cpp @@ -7,15 +7,17 @@ auto InputSettings::create() -> void { portList.onChange([&] { reloadDevices(); }); deviceLabel.setText("Device:"); deviceList.onChange([&] { reloadMappings(); }); - turboLabel.setText("Turbo rate:"); + turboLabel.setText("Turbo rate:").setToolTip( + "The number of frames to wait between toggling turbo buttons." + ); for(uint frequency : range(1, 9)) { ComboButtonItem item{&turboList}; item.setText(frequency); - if(frequency == settings["Input/Turbo/Frequency"].natural()) item.setSelected(); + if(frequency == settings.input.turbo.frequency) item.setSelected(); } - turboList.onChange([&] { + turboList.setToolTip(turboLabel.toolTip()).onChange([&] { uint frequency = turboList.selected().text().natural(); - settings["Input/Turbo/Frequency"].setValue(frequency); + settings.input.turbo.frequency = frequency; inputManager.turboCounter = 0; inputManager.turboFrequency = frequency; }); @@ -23,6 +25,7 @@ auto InputSettings::create() -> void { mappingList.setHeadered(); mappingList.onActivate([&] { if(assignButton.enabled()) assignButton.doActivate(); }); mappingList.onChange([&] { updateControls(); }); + mappingList.onSize([&] { mappingList.resizeColumns(); }); assignMouse1.onActivate([&] { assignMouseInput(0); }); assignMouse2.onActivate([&] { assignMouseInput(1); }); assignMouse3.onActivate([&] { assignMouseInput(2); }); diff --git a/higan/target-bsnes/settings/paths.cpp b/higan/target-bsnes/settings/paths.cpp index 2c06ed99..9869d659 100644 --- a/higan/target-bsnes/settings/paths.cpp +++ b/higan/target-bsnes/settings/paths.cpp @@ -10,12 +10,12 @@ auto PathSettings::create() -> void { gamesPath.setEditable(false); gamesAssign.setText("Assign ...").onActivate([&] { if(auto location = BrowserDialog().setParent(*settingsWindow).selectFolder()) { - settings["Path/Games"].setValue(location); + settings.path.games = location; refreshPaths(); } }); gamesReset.setText("Reset").onActivate([&] { - settings["Path/Games"].setValue(""); + settings.path.games = ""; refreshPaths(); }); @@ -23,12 +23,12 @@ auto PathSettings::create() -> void { patchesPath.setEditable(false); patchesAssign.setText("Assign ...").onActivate([&] { if(auto location = BrowserDialog().setParent(*settingsWindow).selectFolder()) { - settings["Path/Patches"].setValue(location); + settings.path.patches = location; refreshPaths(); } }); patchesReset.setText("Reset").onActivate([&] { - settings["Path/Patches"].setValue(""); + settings.path.patches = ""; refreshPaths(); }); @@ -36,12 +36,12 @@ auto PathSettings::create() -> void { savesPath.setEditable(false); savesAssign.setText("Assign ...").onActivate([&] { if(auto location = BrowserDialog().setParent(*settingsWindow).selectFolder()) { - settings["Path/Saves"].setValue(location); + settings.path.saves = location; refreshPaths(); } }); savesReset.setText("Reset").onActivate([&] { - settings["Path/Saves"].setValue(""); + settings.path.saves = ""; refreshPaths(); }); @@ -49,12 +49,12 @@ auto PathSettings::create() -> void { cheatsPath.setEditable(false); cheatsAssign.setText("Assign ...").onActivate([&] { if(auto location = BrowserDialog().setParent(*settingsWindow).selectFolder()) { - settings["Path/Cheats"].setValue(location); + settings.path.cheats = location; refreshPaths(); } }); cheatsReset.setText("Reset").onActivate([&] { - settings["Path/Cheats"].setValue(""); + settings.path.cheats = ""; refreshPaths(); }); @@ -62,12 +62,12 @@ auto PathSettings::create() -> void { statesPath.setEditable(false); statesAssign.setText("Assign ...").onActivate([&] { if(auto location = BrowserDialog().setParent(*settingsWindow).selectFolder()) { - settings["Path/States"].setValue(location); + settings.path.states = location; refreshPaths(); } }); statesReset.setText("Reset").onActivate([&] { - settings["Path/States"].setValue(""); + settings.path.states = ""; refreshPaths(); }); @@ -75,12 +75,12 @@ auto PathSettings::create() -> void { screenshotsPath.setEditable(false); screenshotsAssign.setText("Assign ...").onActivate([&] { if(auto location = BrowserDialog().setParent(*settingsWindow).selectFolder()) { - settings["Path/Screenshots"].setValue(location); + settings.path.screenshots = location; refreshPaths(); } }); screenshotsReset.setText("Reset").onActivate([&] { - settings["Path/Screenshots"].setValue(""); + settings.path.screenshots = ""; refreshPaths(); }); @@ -88,34 +88,34 @@ auto PathSettings::create() -> void { } auto PathSettings::refreshPaths() -> void { - if(auto location = settings["Path/Games"].text()) { + if(auto location = settings.path.games) { gamesPath.setText(location).setForegroundColor(); } else { - gamesPath.setText("").setForegroundColor({128, 128, 128}); + gamesPath.setText("(last recently used)").setForegroundColor({128, 128, 128}); } - if(auto location = settings["Path/Patches"].text()) { + if(auto location = settings.path.patches) { patchesPath.setText(location).setForegroundColor(); } else { - patchesPath.setText("").setForegroundColor({128, 128, 128}); + patchesPath.setText("(same as loaded game)").setForegroundColor({128, 128, 128}); } - if(auto location = settings["Path/Saves"].text()) { + if(auto location = settings.path.saves) { savesPath.setText(location).setForegroundColor(); } else { - savesPath.setText("").setForegroundColor({128, 128, 128}); + savesPath.setText("(same as loaded game)").setForegroundColor({128, 128, 128}); } - if(auto location = settings["Path/Cheats"].text()) { + if(auto location = settings.path.cheats) { cheatsPath.setText(location).setForegroundColor(); } else { - cheatsPath.setText("").setForegroundColor({128, 128, 128}); + cheatsPath.setText("(same as loaded game)").setForegroundColor({128, 128, 128}); } - if(auto location = settings["Path/States"].text()) { + if(auto location = settings.path.states) { statesPath.setText(location).setForegroundColor(); } else { - statesPath.setText("").setForegroundColor({128, 128, 128}); + statesPath.setText("(same as loaded game)").setForegroundColor({128, 128, 128}); } - if(auto location = settings["Path/Screenshots"].text()) { + if(auto location = settings.path.screenshots) { screenshotsPath.setText(location).setForegroundColor(); } else { - screenshotsPath.setText("").setForegroundColor({128, 128, 128}); + screenshotsPath.setText("(same as loaded game)").setForegroundColor({128, 128, 128}); } } diff --git a/higan/target-bsnes/settings/settings.cpp b/higan/target-bsnes/settings/settings.cpp index 2326f3e3..e44ac0d5 100644 --- a/higan/target-bsnes/settings/settings.cpp +++ b/higan/target-bsnes/settings/settings.cpp @@ -16,81 +16,100 @@ EmulatorSettings emulatorSettings; DriverSettings driverSettings; SettingsWindow settingsWindow; -Settings::Settings() { +auto Settings::load() -> void { Markup::Node::operator=(BML::unserialize(string::read(locate("settings.bml")), " ")); - - auto set = [&](string name, string value) { - //create node and set to default value only if it does not already exist - if(!operator[](name)) operator()(name).setValue(value); - }; - - set("Video/Driver", Video::safestDriver()); - set("Video/Exclusive", false); - set("Video/Blocking", false); - set("Video/Flush", false); - set("Video/Format", "Default"); - set("Video/Shader", "Blur"); - set("Video/Luminance", "100%"); - set("Video/Saturation", "100%"); - set("Video/Gamma", "150%"); - - set("Audio/Driver", Audio::safestDriver()); - set("Audio/Exclusive", false); - set("Audio/Device", ""); - set("Audio/Blocking", true); - set("Audio/Dynamic", false); - set("Audio/Frequency", "48000hz"); - set("Audio/Latency", 0); - set("Audio/Mute", false); - set("Audio/Skew", "0"); - set("Audio/Volume", "100%"); - set("Audio/Balance", "50%"); - - set("Input/Driver", Input::safestDriver()); - set("Input/Frequency", 5); - set("Input/Defocus", "Pause"); - set("Input/Turbo/Frequency", 4); - - set("View/Multiplier", "2"); - set("View/Output", "Scale"); - set("View/AspectCorrection", true); - set("View/OverscanCropping", true); - set("View/BlurEmulation", true); - - set("Path/Games", ""); - set("Path/Patches", ""); - set("Path/Saves", ""); - set("Path/Cheats", ""); - set("Path/States", ""); - set("Path/Screenshots", ""); - set("Path/Recent/SuperFamicom", Path::user()); - set("Path/Recent/GameBoy", Path::user()); - set("Path/Recent/BSMemory", Path::user()); - set("Path/Recent/SufamiTurboA", Path::user()); - set("Path/Recent/SufamiTurboB", Path::user()); - - set("UserInterface/ShowStatusBar", true); - set("UserInterface/SuppressScreenSaver", true); - - set("Emulator/WarnOnUnverifiedGames", false); - set("Emulator/AutoSaveMemory/Enable", true); - set("Emulator/AutoSaveMemory/Interval", 30); - set("Emulator/AutoSaveStateOnUnload", false); - set("Emulator/AutoLoadStateOnLoad", false); - set("Emulator/Hack/FastPPU", true); - set("Emulator/Hack/FastPPU/NoSpriteLimit", false); - set("Emulator/Hack/FastPPU/HiresMode7", false); - set("Emulator/Hack/FastDSP", true); - set("Emulator/Hack/FastSuperFX", "100%"); - set("Emulator/Cheats/Enable", true); - - set("Crashed", false); + process(true); + file::write(locate("settings.bml"), BML::serialize(*this, " ")); } auto Settings::save() -> void { + process(false); file::write(locate("settings.bml"), BML::serialize(*this, " ")); } +auto Settings::process(bool load) -> void { + if(load) { + //initialize non-static default settings + video.driver = ruby::Video::safestDriver(); + audio.driver = ruby::Audio::safestDriver(); + input.driver = ruby::Input::safestDriver(); + } + + #define bind(type, path, name) \ + if(load) { \ + if(auto node = operator[](path)) name = node.type(); \ + } else { \ + operator()(path).setValue(name); \ + } \ + + bind(text, "Video/Driver", video.driver); + bind(boolean, "Video/Exclusive", video.exclusive); + bind(boolean, "Video/Blocking", video.blocking); + bind(boolean, "Video/Flush", video.flush); + bind(text, "Video/Format", video.format); + bind(text, "Video/Shader", video.shader); + + bind(natural, "Video/Luminance", video.luminance); + bind(natural, "Video/Saturation", video.saturation); + bind(natural, "Video/Gamma", video.gamma); + + bind(text, "Video/Output", video.output); + bind(natural, "Video/Multiplier", video.multiplier); + bind(boolean, "Video/AspectCorrection", video.aspectCorrection); + bind(boolean, "Video/Overscan", video.overscan); + bind(boolean, "Video/Blur", video.blur); + + bind(text, "Audio/Driver", audio.driver); + bind(boolean, "Audio/Exclusive", audio.exclusive); + bind(text, "Audio/Device", audio.device); + bind(boolean, "Audio/Blocking", audio.blocking); + bind(boolean, "Audio/Dynamic", audio.dynamic); + bind(natural, "Audio/Frequency", audio.frequency); + bind(natural, "Audio/Latency", audio.latency); + + bind(boolean, "Audio/Mute", audio.mute); + bind(integer, "Audio/Skew", audio.skew); + bind(natural, "Audio/Volume", audio.volume); + bind(natural, "Audio/Balance", audio.balance); + + bind(text, "Input/Driver", input.driver); + bind(natural, "Input/Frequency", input.frequency); + bind(text, "Input/Defocus", input.defocus); + bind(natural, "Input/Turbo/Frequency", input.turbo.frequency); + + bind(text, "Path/Games", path.games); + bind(text, "Path/Patches", path.patches); + bind(text, "Path/Saves", path.saves); + bind(text, "Path/Cheats", path.cheats); + bind(text, "Path/States", path.states); + bind(text, "Path/Screenshots", path.screenshots); + + bind(text, "Path/Recent/SuperFamicom", path.recent.superFamicom); + bind(text, "Path/Recent/GameBoy", path.recent.gameBoy); + bind(text, "Path/Recent/BSMemory", path.recent.bsMemory); + bind(text, "Path/Recent/SufamiTurboA", path.recent.sufamiTurboA); + bind(text, "Path/Recent/SufamiTurboB", path.recent.sufamiTurboB); + + bind(boolean, "Emulator/WarnOnUnverifiedGames", emulator.warnOnUnverifiedGames); + bind(boolean, "Emulator/AutoSaveMemory/Enable", emulator.autoSaveMemory.enable); + bind(natural, "Emulator/AutoSaveMemory/Interval", emulator.autoSaveMemory.interval); + bind(boolean, "Emulator/AutoSaveStateOnUnload", emulator.autoSaveStateOnUnload); + bind(boolean, "Emulator/AutoLoadStateOnLoad", emulator.autoLoadStateOnLoad); + bind(boolean, "Emulator/Hack/FastPPU/Enable", emulator.hack.fastPPU.enable); + bind(boolean, "Emulator/Hack/FastPPU/NoSpriteLimit", emulator.hack.fastPPU.noSpriteLimit); + bind(boolean, "Emulator/Hack/FastPPU/HiresMode7", emulator.hack.fastPPU.hiresMode7); + bind(boolean, "Emulator/Hack/FastDSP/Enable", emulator.hack.fastDSP.enable); + bind(natural, "Emulator/Hack/FastSuperFX", emulator.hack.fastSuperFX); + bind(boolean, "Emulator/Cheats/Enable", emulator.cheats.enable); + + bind(boolean, "General/StatusBar", general.statusBar); + bind(boolean, "General/ScreenSaver", general.screenSaver); + bind(boolean, "General/ToolTips", general.toolTips); + bind(boolean, "General/Crashed", general.crashed); + + #undef bind +} + auto SettingsWindow::create() -> void { layout.setPadding(5); panel.append(videoSettings); @@ -107,11 +126,6 @@ auto SettingsWindow::create() -> void { setAlignment({0.0, 1.0}); setDismissable(); - onSize([&] { - inputSettings.mappingList.resizeColumns(); - hotkeySettings.mappingList.resizeColumns(); - }); - onClose([&] { if(inputSettings.activeMapping) inputSettings.cancelMapping(); if(hotkeySettings.activeMapping) hotkeySettings.cancelMapping(); @@ -124,7 +138,6 @@ auto SettingsWindow::setVisible(bool visible) -> SettingsWindow& { inputSettings.refreshMappings(); hotkeySettings.refreshMappings(); Application::processEvents(); - doSize(); } return Window::setVisible(visible), *this; } diff --git a/higan/target-bsnes/settings/settings.hpp b/higan/target-bsnes/settings/settings.hpp index a8f4f7cd..34fa2011 100644 --- a/higan/target-bsnes/settings/settings.hpp +++ b/higan/target-bsnes/settings/settings.hpp @@ -1,12 +1,106 @@ struct Settings : Markup::Node { - Settings(); + Settings() { load(); } + ~Settings() { save(); } + + auto load() -> void; auto save() -> void; + auto process(bool load) -> void; + + struct Video { + string driver; + bool exclusive = false; + bool blocking = false; + bool flush = false; + string format = "Default"; + string shader = "Blur"; + + uint luminance = 100; + uint saturation = 100; + uint gamma = 150; + + string output = "Scale"; + uint multiplier = 2; + bool aspectCorrection = true; + bool overscan = false; + bool blur = false; + } video; + + struct Audio { + string driver; + bool exclusive = false; + string device; + bool blocking = true; + bool dynamic = false; + uint frequency = 48000; + uint latency = 0; + + bool mute = false; + int skew = 0; + uint volume = 100; + uint balance = 50; + } audio; + + struct Input { + string driver; + uint frequency = 5; + string defocus = "Pause"; + struct Turbo { + uint frequency = 4; + } turbo; + } input; + + struct Path { + string games; + string patches; + string saves; + string cheats; + string states; + string screenshots; + struct Recent { + string superFamicom; + string gameBoy; + string bsMemory; + string sufamiTurboA; + string sufamiTurboB; + } recent; + } path; + + struct Emulator { + bool warnOnUnverifiedGames = false; + struct AutoSaveMemory { + bool enable = true; + uint interval = 30; + } autoSaveMemory; + bool autoSaveStateOnUnload = false; + bool autoLoadStateOnLoad = false; + struct Hack { + struct FastPPU { + bool enable = true; + bool noSpriteLimit = false; + bool hiresMode7 = false; + } fastPPU; + struct FastDSP { + bool enable = true; + } fastDSP; + uint fastSuperFX = 100; + } hack; + struct Cheats { + bool enable = true; + } cheats; + } emulator; + + struct General { + bool statusBar = true; + bool screenSaver = false; + bool toolTips = true; + bool crashed = false; + } general; }; struct VideoSettings : TabFrameItem { auto create() -> void; -public: +private: VerticalLayout layout{this}; Label colorAdjustmentLabel{&layout, Size{~0, 0}, 2}; TableLayout colorLayout{&layout, Size{~0, 0}}; @@ -26,7 +120,7 @@ public: struct AudioSettings : TabFrameItem { auto create() -> void; -public: +private: VerticalLayout layout{this}; Label effectsLabel{&layout, Size{~0, 0}, 2}; TableLayout effectsLayout{&layout, Size{~0, 0}}; @@ -57,8 +151,9 @@ struct InputSettings : TabFrameItem { auto assignMouseInput(uint id) -> void; auto inputEvent(shared_pointer device, uint group, uint input, int16 oldValue, int16 newValue, bool allowMouseInput = false) -> void; -public: maybe activeMapping; + +private: Timer timer; VerticalLayout layout{this}; @@ -87,8 +182,9 @@ struct HotkeySettings : TabFrameItem { auto cancelMapping() -> void; auto inputEvent(shared_pointer device, uint group, uint input, int16 oldValue, int16 newValue) -> void; -public: maybe activeMapping; + +private: Timer timer; VerticalLayout layout{this}; @@ -154,7 +250,6 @@ public: HorizontalLayout autoStateLayout{&layout, Size{~0, 0}}; CheckLabel autoSaveStateOnUnload{&autoStateLayout, Size{0, 0}}; CheckLabel autoLoadStateOnLoad{&autoStateLayout, Size{0, 0}}; - CheckLabel suppressScreenSaver{&layout, Size{~0, 0}}; Canvas optionsSpacer{&layout, Size{~0, 1}}; Label hacksLabel{&layout, Size{~0, 0}, 2}; HorizontalLayout fastPPULayout{&layout, Size{~0, 0}}; diff --git a/higan/target-bsnes/settings/video.cpp b/higan/target-bsnes/settings/video.cpp index e3868230..cf5debc3 100644 --- a/higan/target-bsnes/settings/video.cpp +++ b/higan/target-bsnes/settings/video.cpp @@ -9,25 +9,25 @@ auto VideoSettings::create() -> void { colorLayout.column(0).setAlignment(1.0); luminanceLabel.setText("Luminance:"); luminanceValue.setAlignment(0.5); - luminanceSlider.setLength(101).setPosition(settings["Video/Luminance"].natural()).onChange([&] { + luminanceSlider.setLength(101).setPosition(settings.video.luminance).onChange([&] { string value = {luminanceSlider.position(), "%"}; - settings["Video/Luminance"].setValue(value); + settings.video.luminance = value.natural(); luminanceValue.setText(value); program.updateVideoPalette(); }).doChange(); saturationLabel.setText("Saturation:"); saturationValue.setAlignment(0.5); - saturationSlider.setLength(201).setPosition(settings["Video/Saturation"].natural()).onChange([&] { + saturationSlider.setLength(201).setPosition(settings.video.saturation).onChange([&] { string value = {saturationSlider.position(), "%"}; - settings["Video/Saturation"].setValue(value); + settings.video.saturation = value.natural(); saturationValue.setText(value); program.updateVideoPalette(); }).doChange(); gammaLabel.setText("Gamma:"); gammaValue.setAlignment(0.5); - gammaSlider.setLength(101).setPosition(settings["Video/Gamma"].natural() - 100).onChange([&] { + gammaSlider.setLength(101).setPosition(settings.video.gamma - 100).onChange([&] { string value = {100 + gammaSlider.position(), "%"}; - settings["Video/Gamma"].setValue(value); + settings.video.gamma = value.natural(); gammaValue.setText(value); program.updateVideoPalette(); }).doChange(); diff --git a/higan/target-bsnes/tools/cheat-editor.cpp b/higan/target-bsnes/tools/cheat-editor.cpp index 96b26c3c..c7d338db 100644 --- a/higan/target-bsnes/tools/cheat-editor.cpp +++ b/higan/target-bsnes/tools/cheat-editor.cpp @@ -107,6 +107,8 @@ auto CheatEditor::create() -> void { layout.setPadding(5); cheatList.setBatchable(); + cheatList.setHeadered(); + cheatList.setSortable(); cheatList.onActivate([&] { editButton.doActivate(); }); @@ -121,11 +123,22 @@ auto CheatEditor::create() -> void { synchronizeCodes(); } }); + cheatList.onSort([&](TableViewColumn column) { + column.setSorting(column.sorting() == Sort::Ascending ? Sort::Descending : Sort::Ascending); + //cheatList.sort(); + }); + cheatList.onSize([&] { + cheatList.resizeColumns(); + }); findCheatsButton.setText("Find Cheats ...").onActivate([&] { cheatDatabase.findCheats(); }); - enableCheats.setText("Enable Cheats").setChecked(settings["Emulator/Cheats/Enable"].boolean()).onToggle([&] { - settings["Emulator/Cheats/Enable"].setValue(enableCheats.checked()); + enableCheats.setText("Enable Cheats").setToolTip( + "Master enable for all cheat codes.\n" + "When unchecked, no cheat codes will be active.\n\n" + "Use this to bypass game areas that have problems with cheats." + ).setChecked(settings.emulator.cheats.enable).onToggle([&] { + settings.emulator.cheats.enable = enableCheats.checked(); if(!enableCheats.checked()) { program.showMessage("All cheat codes disabled"); } else { @@ -152,12 +165,11 @@ auto CheatEditor::create() -> void { auto CheatEditor::refresh() -> void { cheatList.reset(); cheatList.append(TableViewColumn()); - cheatList.append(TableViewColumn().setExpandable()); + cheatList.append(TableViewColumn().setText("Name").setSorting(Sort::Ascending).setExpandable()); for(auto& cheat : cheats) { - cheatList.append(TableViewItem() - .append(TableViewCell().setCheckable().setChecked(cheat.enable)) - .append(TableViewCell().setText(cheat.name)) - ); + TableViewItem item{&cheatList}; + item.append(TableViewCell().setCheckable().setChecked(cheat.enable)); + item.append(TableViewCell().setText(cheat.name)); } cheatList.resizeColumns().doChange(); } diff --git a/higan/target-bsnes/tools/state-manager.cpp b/higan/target-bsnes/tools/state-manager.cpp index 2bbcf3d9..84317eac 100644 --- a/higan/target-bsnes/tools/state-manager.cpp +++ b/higan/target-bsnes/tools/state-manager.cpp @@ -59,6 +59,9 @@ auto StateManager::create() -> void { column.setSorting(column.sorting() == Sort::Ascending ? Sort::Descending : Sort::Ascending); stateList.sort(); }); + stateList.onSize([&] { + stateList.resizeColumns(); + }); categoryLabel.setText("Category:"); categoryOption.append(ComboButtonItem().setText("Managed States").setProperty("type", "Managed/")); categoryOption.append(ComboButtonItem().setText("Quick States").setProperty("type", "Quick/")); diff --git a/higan/target-bsnes/tools/tools.cpp b/higan/target-bsnes/tools/tools.cpp index 3ab245bb..f104a616 100644 --- a/higan/target-bsnes/tools/tools.cpp +++ b/higan/target-bsnes/tools/tools.cpp @@ -26,11 +26,6 @@ auto ToolsWindow::create() -> void { setAlignment({1.0, 1.0}); setDismissable(); - onSize([&] { - cheatEditor.cheatList.resizeColumns(); - stateManager.stateList.resizeColumns(); - }); - onClose([&] { setVisible(false); }); @@ -44,7 +39,6 @@ auto ToolsWindow::setVisible(bool visible) -> ToolsWindow& { stateWindow.setVisible(false); } else { Application::processEvents(); - doSize(); } return *this; } diff --git a/higan/target-higan/presentation/presentation.cpp b/higan/target-higan/presentation/presentation.cpp index 91af3b9a..a108229c 100644 --- a/higan/target-higan/presentation/presentation.cpp +++ b/higan/target-higan/presentation/presentation.cpp @@ -11,20 +11,37 @@ Presentation::Presentation() { systemMenu.setVisible(false); settingsMenu.setText("Settings"); - videoScaleMenu.setText("Video Scale"); - videoScaleSmall.setText("Small").onActivate([&] { - settings["Video/Windowed/Scale"].setValue("Small"); + sizeMenu.setText("Size"); + updateSizeMenu(); + outputMenu.setText("Output"); + centerViewport.setText("Center").onActivate([&] { + settings["View/Output"].setValue("Center"); resizeViewport(); }); - videoScaleMedium.setText("Medium").onActivate([&] { - settings["Video/Windowed/Scale"].setValue("Medium"); + scaleViewport.setText("Scale").onActivate([&] { + settings["View/Output"].setValue("Scale"); resizeViewport(); }); - videoScaleLarge.setText("Large").onActivate([&] { - settings["Video/Windowed/Scale"].setValue("Large"); + stretchViewport.setText("Stretch").onActivate([&] { + settings["View/Output"].setValue("Stretch"); resizeViewport(); }); - videoEmulationMenu.setText("Video Emulation"); + if(settings["View/Output"].text() == "Center") centerViewport.setChecked(); + if(settings["View/Output"].text() == "Scale") scaleViewport.setChecked(); + if(settings["View/Output"].text() == "Stretch") stretchViewport.setChecked(); + adaptiveSizing.setText("Adaptive Sizing").setChecked(settings["View/Adaptive"].boolean()).onToggle([&] { + settings["View/Adaptive"].setValue(adaptiveSizing.checked()); + resizeWindow(); + }); + aspectCorrection.setText("Aspect Correction").setChecked(settings["View/AspectCorrection"].boolean()).onToggle([&] { + settings["View/AspectCorrection"].setValue(aspectCorrection.checked()); + resizeWindow(); + }); + showOverscanArea.setText("Show Overscan Area").setChecked(settings["View/Overscan"].boolean()).onToggle([&] { + settings["View/Overscan"].setValue(showOverscanArea.checked()); + resizeWindow(); + }); + videoEmulationMenu.setText("Emulation"); blurEmulation.setText("Blurring").setChecked(settings["Video/BlurEmulation"].boolean()).onToggle([&] { settings["Video/BlurEmulation"].setValue(blurEmulation.checked()); if(emulator) emulator->set("Blur Emulation", blurEmulation.checked()); @@ -37,7 +54,7 @@ Presentation::Presentation() { settings["Video/ScanlineEmulation"].setValue(scanlineEmulation.checked()); if(emulator) emulator->set("Scanline Emulation", scanlineEmulation.checked()); }); - videoShaderMenu.setText("Video Shader"); + videoShaderMenu.setText("Shader"); videoShaderNone.setText("None").onActivate([&] { settings["Video/Shader"].setValue("None"); program->updateVideoShader(); @@ -59,10 +76,14 @@ Presentation::Presentation() { settings["Audio/Mute"].setValue(muteAudio.checked()); program->updateAudioEffects(); }); - showStatusBar.setText("Show Status Bar").setChecked(settings["UserInterface/ShowStatusBar"].boolean()).onToggle([&] { - settings["UserInterface/ShowStatusBar"].setValue(showStatusBar.checked()); - statusBar.setVisible(showStatusBar.checked()); - if(visible()) resizeViewport(); + showStatusBar.setText("Show Status Bar").setChecked(settings["View/StatusBar"].boolean()).onToggle([&] { + settings["View/StatusBar"].setValue(showStatusBar.checked()); + if(!showStatusBar.checked()) { + layout.remove(statusLayout); + } else { + layout.append(statusLayout, Size{~0, StatusHeight}); + } + if(visible()) resizeWindow(); }); showSystemSettings.setIcon(Icon::Device::Storage).setText("Systems ...").onActivate([&] { settingsManager->show(0); }); showVideoSettings.setIcon(Icon::Device::Display).setText("Video ...").onActivate([&] { settingsManager->show(1); }); @@ -113,9 +134,6 @@ Presentation::Presentation() { aboutWindow->setCentered(*this).setVisible().setFocused(); }); - statusBar.setFont(Font().setBold()); - statusBar.setVisible(settings["UserInterface/ShowStatusBar"].boolean()); - viewport.setDroppable().onDrop([&](vector locations) { if(!locations || !directory::exists(locations.first())) return; program->gameQueue.append(locations.first()); @@ -127,17 +145,42 @@ Presentation::Presentation() { icon.alphaBlend(0x000000); iconCanvas.setIcon(icon); + if(!settings["View/StatusBar"].boolean()) { + layout.remove(statusLayout); + } + + auto font = Font().setBold(); + auto back = Color{ 32, 32, 32}; + auto fore = Color{255, 255, 255}; + + spacerLeft.setBackgroundColor(back); + + statusMessage.setFont(font); + statusMessage.setAlignment(0.0); + statusMessage.setBackgroundColor(back); + statusMessage.setForegroundColor(fore); + + statusInfo.setFont(font); + statusInfo.setAlignment(1.0); + statusInfo.setBackgroundColor(back); + statusInfo.setForegroundColor(fore); + statusInfo.setText("Unloaded"); + + spacerRight.setBackgroundColor(back); + onSize([&] { - resizeViewport(false); + resizeViewport(); }); onClose([&] { program->quit(); }); + settings["View/Multiplier"].setValue(2); + setTitle({"higan v", Emulator::Version}); setBackgroundColor({0, 0, 0}); - resizeViewport(); + resizeWindow(); setCentered(); #if defined(PLATFORM_WINDOWS) @@ -238,13 +281,46 @@ auto Presentation::updateEmulatorDeviceSelections() -> void { } } -auto Presentation::clearViewport() -> void { - if(!video) return; +auto Presentation::updateSizeMenu() -> void { + assert(sizeMenu.actionCount() == 0); //should only be called once - if(!emulator || !emulator->loaded()) { - viewport.setGeometry({0, 0, geometry().width(), geometry().height()}); + //determine the largest multiplier that can be used by the largest monitor found + uint height = 1; + for(uint monitor : range(Monitor::count())) { + height = max(height, Monitor::workspace(monitor).height()); } + uint multipliers = max(1, height / 240); + for(uint multiplier : range(1, multipliers + 1)) { + MenuRadioItem item{&sizeMenu}; + item.setProperty("multiplier", multiplier); + item.setText({multiplier, "x (", 240 * multiplier, "p)"}); + item.onActivate([=] { + settings["View/Multiplier"].setValue(multiplier); + resizeWindow(); + }); + sizeGroup.append(item); + } + + for(auto item : sizeGroup.objects()) { + if(settings["View/Multiplier"].natural() == item.property("multiplier").natural()) { + item.setChecked(); + } + } + + sizeMenu.append(MenuSeparator()); + sizeMenu.append(MenuItem().setIcon(Icon::Action::Remove).setText("Shrink Window To Size").onActivate([&] { + resizeWindow(); + })); + sizeMenu.append(MenuItem().setIcon(Icon::Place::Settings).setText("Center Window").onActivate([&] { + setCentered(); + })); +} + +auto Presentation::clearViewport() -> void { + if(!emulator || !emulator->loaded()) viewportLayout.setPadding(); + if(!visible() || !video) return; + uint32_t* output; uint length = 0; uint width = 16; @@ -259,105 +335,126 @@ auto Presentation::clearViewport() -> void { } } -auto Presentation::resizeViewport(bool resizeWindow) -> void { - //clear video area before resizing to avoid seeing distorted video momentarily - clearViewport(); +auto Presentation::resizeViewport() -> void { + uint layoutWidth = viewportLayout.geometry().width(); + uint layoutHeight = viewportLayout.geometry().height(); - uint viewportWidth = geometry().width(); - uint viewportHeight = geometry().height(); + uint width = 320; + uint height = 240; - double emulatorWidth = 320; - double emulatorHeight = 240; - double aspectCorrection = 1.0; if(emulator) { - auto display = emulator->displays()[0]; - emulatorWidth = display.width; - emulatorHeight = display.height; - aspectCorrection = display.aspectCorrection; - if(display.type == Emulator::Interface::Display::Type::CRT) { - uint overscanHorizontal = settings["Video/Overscan/Horizontal"].natural(); - uint overscanVertical = settings["Video/Overscan/Vertical"].natural(); - emulatorWidth -= overscanHorizontal * 2; - emulatorHeight -= overscanVertical * 2; + auto display = emulator->displays().first(); + width = display.width; + height = display.height; + if(settings["View/AspectCorrection"].boolean()) width *= display.aspectCorrection; + if(!settings["View/Overscan"].boolean()) { + if(display.type == Emulator::Interface::Display::Type::CRT) { + uint overscanHorizontal = settings["View/Overscan/Horizontal"].natural(); + uint overscanVertical = settings["View/Overscan/Vertical"].natural(); + width -= overscanHorizontal * 2; + height -= overscanVertical * 2; + } } } - if(!fullScreen()) { - if(settings["Video/Windowed/AspectCorrection"].boolean()) emulatorWidth *= aspectCorrection; - - if(resizeWindow) { - string viewportScale = "640x480"; - if(settings["Video/Windowed/Scale"].text() == "Small") viewportScale = settings["Video/Windowed/Scale/Small"].text(); - if(settings["Video/Windowed/Scale"].text() == "Medium") viewportScale = settings["Video/Windowed/Scale/Medium"].text(); - if(settings["Video/Windowed/Scale"].text() == "Large") viewportScale = settings["Video/Windowed/Scale/Large"].text(); - auto resolution = viewportScale.isplit("x", 1L); - viewportWidth = resolution(0).natural(); - viewportHeight = resolution(1).natural(); - } - - if(settings["Video/Windowed/Adaptive"].boolean() && resizeWindow) { - uint multiplier = min(viewportWidth / emulatorWidth, viewportHeight / emulatorHeight); - emulatorWidth *= multiplier; - emulatorHeight *= multiplier; - setSize({viewportWidth = emulatorWidth, viewportHeight = emulatorHeight}); - } else if(settings["Video/Windowed/IntegralScaling"].boolean()) { - uint multiplier = min(viewportWidth / emulatorWidth, viewportHeight / emulatorHeight); - emulatorWidth *= multiplier; - emulatorHeight *= multiplier; - if(resizeWindow) setSize({viewportWidth, viewportHeight}); - } else { - double multiplier = min(viewportWidth / emulatorWidth, viewportHeight / emulatorHeight); - emulatorWidth *= multiplier; - emulatorHeight *= multiplier; - if(resizeWindow) setSize({viewportWidth, viewportHeight}); - } - } else { - if(settings["Video/Fullscreen/AspectCorrection"].boolean()) emulatorWidth *= aspectCorrection; - - if(settings["Video/Fullscreen/IntegralScaling"].boolean()) { - uint multiplier = min(viewportWidth / emulatorWidth, viewportHeight / emulatorHeight); - emulatorWidth *= multiplier; - emulatorHeight *= multiplier; - } else { - double multiplier = min(viewportWidth / emulatorWidth, viewportHeight / emulatorHeight); - emulatorWidth *= multiplier; - emulatorHeight *= multiplier; + if(visible() && !fullScreen()) { + uint widthMultiplier = layoutWidth / width; + uint heightMultiplier = layoutHeight / height; + uint multiplier = max(1, min(widthMultiplier, heightMultiplier)); + settings["View/Multiplier"].setValue(multiplier); + for(auto item : sizeGroup.objects()) { + if(auto property = item.property("multiplier")) { + if(property.natural() == multiplier) item.setChecked(); + } } } - if(emulator && emulator->loaded()) { - viewport.setGeometry({ - (viewportWidth - emulatorWidth) / 2, (viewportHeight - emulatorHeight) / 2, - emulatorWidth, emulatorHeight - }); - } else { - viewport.setGeometry({0, 0, geometry().width(), geometry().height()}); + if(!emulator || !emulator->loaded()) return clearViewport(); + if(!video) return; + + uint viewportWidth; + uint viewportHeight; + if(settings["View/Output"].text() == "Center") { + uint widthMultiplier = layoutWidth / width; + uint heightMultiplier = layoutHeight / height; + uint multiplier = min(widthMultiplier, heightMultiplier); + viewportWidth = width * multiplier; + viewportHeight = height * multiplier; + } else if(settings["View/Output"].text() == "Scale") { + double widthMultiplier = (double)layoutWidth / width; + double heightMultiplier = (double)layoutHeight / height; + double multiplier = min(widthMultiplier, heightMultiplier); + viewportWidth = width * multiplier; + viewportHeight = height * multiplier; + } else if(settings["View/Output"].text() == "Stretch" || 1) { + viewportWidth = layoutWidth; + viewportHeight = layoutHeight; } - //clear video area again to ensure entire viewport area has been painted in + //center viewport within viewportLayout by use of viewportLayout padding + uint paddingWidth = layoutWidth - viewportWidth; + uint paddingHeight = layoutHeight - viewportHeight; + viewportLayout.setPadding({ + paddingWidth / 2, paddingHeight / 2, + paddingWidth - paddingWidth / 2, paddingHeight - paddingHeight / 2 + }); + clearViewport(); } +auto Presentation::resizeWindow() -> void { + if(fullScreen()) return; + if(maximized()) setMaximized(false); + + uint width = 320; + uint height = 240; + uint multiplier = max(1, settings["View/Multiplier"].natural()); + uint statusHeight = settings["View/StatusBar"].boolean() ? StatusHeight : 0; + + if(emulator) { + auto display = emulator->displays().first(); + width = display.width; + height = display.height; + if(settings["View/AspectCorrection"].boolean()) width *= display.aspectCorrection; + if(!settings["View/Overscan"].boolean()) { + if(display.type == Emulator::Interface::Display::Type::CRT) { + uint overscanHorizontal = settings["View/Overscan/Horizontal"].natural(); + uint overscanVertical = settings["View/Overscan/Vertical"].natural(); + width -= overscanHorizontal * 2; + height -= overscanVertical * 2; + } + } + } + + setMinimumSize({width, height + statusHeight}); + setSize({width * multiplier, height * multiplier + statusHeight}); + layout.setGeometry(layout.geometry()); + resizeViewport(); +} + auto Presentation::toggleFullScreen() -> void { if(!fullScreen()) { - statusBar.setVisible(false); + if(settings["View/StatusBar"].boolean()) { + layout.remove(statusLayout); + } menuBar.setVisible(false); setFullScreen(true); - video->setExclusive(settings["Video/Fullscreen/Exclusive"].boolean()); + video->setExclusive(settings["Video/Exclusive"].boolean()); if(video->exclusive()) setVisible(false); if(!input->acquired()) input->acquire(); + resizeViewport(); } else { if(input->acquired()) input->release(); if(video->exclusive()) setVisible(true); video->setExclusive(false); setFullScreen(false); menuBar.setVisible(true); - statusBar.setVisible(settings["UserInterface/ShowStatusBar"].boolean()); + if(settings["View/StatusBar"].boolean()) { + layout.append(statusLayout, Size{~0, StatusHeight}); + } + resizeWindow(); + setCentered(); } - //hack: give window geometry time to update after toggling fullscreen and menu/status bars - usleep(20 * 1000); - Application::processEvents(); - resizeViewport(); } auto Presentation::loadSystems() -> void { diff --git a/higan/target-higan/presentation/presentation.hpp b/higan/target-higan/presentation/presentation.hpp index 747d24eb..8230febc 100644 --- a/higan/target-higan/presentation/presentation.hpp +++ b/higan/target-higan/presentation/presentation.hpp @@ -9,11 +9,15 @@ struct AboutWindow : Window { }; struct Presentation : Window { + enum : uint { StatusHeight = 24 }; + Presentation(); auto updateEmulatorMenu() -> void; auto updateEmulatorDeviceSelections() -> void; + auto updateSizeMenu() -> void; auto clearViewport() -> void; - auto resizeViewport(bool resizeWindow = true) -> void; + auto resizeViewport() -> void; + auto resizeWindow() -> void; auto toggleFullScreen() -> void; auto loadSystems() -> void; auto loadShaders() -> void; @@ -29,10 +33,17 @@ struct Presentation : Window { MenuItem powerSystem{&systemMenu}; MenuItem unloadSystem{&systemMenu}; Menu settingsMenu{&menuBar}; - Menu videoScaleMenu{&settingsMenu}; - MenuItem videoScaleSmall{&videoScaleMenu}; - MenuItem videoScaleMedium{&videoScaleMenu}; - MenuItem videoScaleLarge{&videoScaleMenu}; + Menu sizeMenu{&settingsMenu}; + Group sizeGroup; + Menu outputMenu{&settingsMenu}; + MenuRadioItem centerViewport{&outputMenu}; + MenuRadioItem scaleViewport{&outputMenu}; + MenuRadioItem stretchViewport{&outputMenu}; + Group outputGroup{¢erViewport, &scaleViewport, &stretchViewport}; + MenuSeparator outputSeparator{&outputMenu}; + MenuCheckItem adaptiveSizing{&outputMenu}; + MenuCheckItem aspectCorrection{&outputMenu}; + MenuCheckItem showOverscanArea{&outputMenu}; Menu videoEmulationMenu{&settingsMenu}; MenuCheckItem blurEmulation{&videoEmulationMenu}; MenuCheckItem colorEmulation{&videoEmulationMenu}; @@ -85,8 +96,11 @@ struct Presentation : Window { Widget iconBefore{&iconLayout, Size{128, ~0}, 0}; Canvas iconCanvas{&iconLayout, Size{112, 112}, 0}; Widget iconAfter{&iconLayout, Size{128, 8}, 0}; - - StatusBar statusBar{this}; + HorizontalLayout statusLayout{&layout, Size{~0, StatusHeight}, 0}; + Label spacerLeft{&statusLayout, Size{8, ~0}, 0}; + Label statusMessage{&statusLayout, Size{~0, ~0}, 0}; + Label statusInfo{&statusLayout, Size{100, ~0}, 0}; + Label spacerRight{&statusLayout, Size{8, ~0}, 0}; }; extern unique_pointer aboutWindow; diff --git a/higan/target-higan/program/game.cpp b/higan/target-higan/program/game.cpp index 6fa70529..72ec1dda 100644 --- a/higan/target-higan/program/game.cpp +++ b/higan/target-higan/program/game.cpp @@ -41,7 +41,11 @@ auto Program::load(Emulator::Interface& interface) -> void { updateAudioEffects(); presentation->viewportLayout.remove(presentation->iconLayout); - presentation->resizeViewport(); + if(settings["View/Adaptive"].boolean()) { + presentation->resizeWindow(); + } else { + presentation->resizeViewport(); + } presentation->setTitle(emulator->titles().merge(" + ")); presentation->systemMenu.setText(information.name).setVisible(true); presentation->toolsMenu.setVisible(true); @@ -62,7 +66,11 @@ auto Program::unload() -> void { gamePaths.reset(); presentation->viewportLayout.append(presentation->iconLayout, Size{0, ~0}); - presentation->resizeViewport(); + if(settings["View/Adaptive"].boolean()) { + presentation->resizeWindow(); + } else { + presentation->resizeViewport(); + } presentation->setTitle({"higan v", Emulator::Version}); presentation->systemMenu.setVisible(false); presentation->toolsMenu.setVisible(false); diff --git a/higan/target-higan/program/platform.cpp b/higan/target-higan/program/platform.cpp index 047d3fcc..20207a34 100644 --- a/higan/target-higan/program/platform.cpp +++ b/higan/target-higan/program/platform.cpp @@ -56,15 +56,17 @@ auto Program::videoRefresh(uint displayID, const uint32* data, uint pitch, uint pitch >>= 2; - auto display = emulator->displays()[displayID]; - if(display.type == Emulator::Interface::Display::Type::CRT) { - uint overscanHorizontal = settings["Video/Overscan/Horizontal"].natural(); - uint overscanVertical = settings["Video/Overscan/Vertical"].natural(); - overscanHorizontal *= display.internalWidth / display.width; - overscanVertical *= display.internalHeight / display.height; - data += overscanVertical * pitch + overscanHorizontal; - width -= overscanHorizontal * 2; - height -= overscanVertical * 2; + if(!settings["View/Overscan"].boolean()) { + auto display = emulator->displays()[displayID]; + if(display.type == Emulator::Interface::Display::Type::CRT) { + uint overscanHorizontal = settings["View/Overscan/Horizontal"].natural(); + uint overscanVertical = settings["View/Overscan/Vertical"].natural(); + overscanHorizontal *= display.internalWidth / display.width; + overscanVertical *= display.internalHeight / display.height; + data += overscanVertical * pitch + overscanHorizontal; + width -= overscanHorizontal * 2; + height -= overscanVertical * 2; + } } if(video->acquire(output, length, width, height)) { @@ -85,7 +87,7 @@ auto Program::videoRefresh(uint displayID, const uint32* data, uint pitch, uint current = chrono::timestamp(); if(current != previous) { previous = current; - statusText = {"FPS: ", frameCounter}; + statusInfo = {frameCounter, " FPS"}; frameCounter = 0; } } diff --git a/higan/target-higan/program/program.hpp b/higan/target-higan/program/program.hpp index 42032e49..5f857f40 100644 --- a/higan/target-higan/program/program.hpp +++ b/higan/target-higan/program/program.hpp @@ -50,11 +50,11 @@ struct Program : Emulator::Platform { vector gameQueue; //for command-line and drag-and-drop loading vector gamePaths; //for keeping track of loaded folder locations - time_t autoSaveTime = 0; //for automatically saving RAM periodically + uint64 autoSaveTime = 0; //for automatically saving RAM periodically - string statusText; + uint64 statusTime = 0; //for status message timeout after two seconds string statusMessage; - time_t statusTime = 0; + string statusInfo; }; extern unique_pointer program; diff --git a/higan/target-higan/program/utility.cpp b/higan/target-higan/program/utility.cpp index 187bcb72..a4436206 100644 --- a/higan/target-higan/program/utility.cpp +++ b/higan/target-higan/program/utility.cpp @@ -105,21 +105,24 @@ auto Program::showMessage(const string& text) -> void { } auto Program::updateStatusText() -> void { - time_t currentTime = time(nullptr); - - string text; - if((currentTime - statusTime) <= 2) { - text = statusMessage; - } else if(!emulator || emulator->loaded() == false) { - text = "No game loaded"; - } else if(pause || (!focused() && settingsManager->input.pauseEmulation.checked())) { - text = "Paused"; - } else { - text = statusText; + string message; + if(chrono::timestamp() - statusTime <= 2) { + message = statusMessage; + } + if(message != presentation->statusMessage.text()) { + presentation->statusMessage.setText(message); } - if(text != presentation->statusBar.text()) { - presentation->statusBar.setText(text); + string info; + if(!emulator || !emulator->loaded()) { + info = "Unloaded"; + } else if(pause || (!focused() && settingsManager->input.pauseEmulation.checked())) { + info = "Paused"; + } else { + info = statusInfo; + } + if(info != presentation->statusInfo.text()) { + presentation->statusInfo.setText(info); } } diff --git a/higan/target-higan/settings/settings.cpp b/higan/target-higan/settings/settings.cpp index 39db1ecc..d869aa1c 100644 --- a/higan/target-higan/settings/settings.cpp +++ b/higan/target-higan/settings/settings.cpp @@ -11,19 +11,18 @@ unique_pointer settingsManager; unique_pointer systemProperties; Settings::Settings() { - Markup::Node::operator=(BML::unserialize(string::read(locate("settings.bml")))); + Markup::Node::operator=(BML::unserialize(string::read(locate("settings.bml")), " ")); auto set = [&](const string& name, const string& value) { //create node and set to default value only if it does not already exist if(!operator[](name)) operator()(name).setValue(value); }; - set("UserInterface/ShowStatusBar", true); - set("Library/Location", {Path::user(), "Emulation/"}); set("Library/IgnoreManifests", false); set("Video/Driver", ruby::Video::safestDriver()); + set("Video/Exclusive", false); set("Video/Synchronize", false); set("Video/Shader", "Blur"); set("Video/BlurEmulation", true); @@ -34,21 +33,6 @@ Settings::Settings() { set("Video/Gamma", 100); set("Video/Luminance", 100); - set("Video/Overscan/Horizontal", 0); - set("Video/Overscan/Vertical", 0); - - set("Video/Windowed/AspectCorrection", true); - set("Video/Windowed/IntegralScaling", true); - set("Video/Windowed/Adaptive", true); - set("Video/Windowed/Scale", "Small"); - set("Video/Windowed/Scale/Small", "640x480"); - set("Video/Windowed/Scale/Medium", "960x720"); - set("Video/Windowed/Scale/Large", "1280x960"); - - set("Video/Fullscreen/AspectCorrection", true); - set("Video/Fullscreen/IntegralScaling", true); - set("Video/Fullscreen/Exclusive", false); - set("Audio/Driver", ruby::Audio::safestDriver()); set("Audio/Device", ""); set("Audio/Frequency", 48000); @@ -63,6 +47,15 @@ Settings::Settings() { set("Input/Frequency", 5); set("Input/Defocus", "Pause"); + set("View/Multiplier", "2"); + set("View/Output", "Scale"); + set("View/Adaptive", false); + set("View/AspectCorrection", true); + set("View/Overscan", true); + set("View/Overscan/Horizontal", 0); + set("View/Overscan/Vertical", 8); + set("View/StatusBar", true); + set("Emulation/AutoSaveMemory/Enable", true); set("Emulation/AutoSaveMemory/Interval", 30); @@ -72,7 +65,7 @@ Settings::Settings() { } auto Settings::save() -> void { - file::write(locate("settings.bml"), BML::serialize(*this)); + file::write(locate("settings.bml"), BML::serialize(*this, " ")); } // diff --git a/higan/target-higan/settings/settings.hpp b/higan/target-higan/settings/settings.hpp index d5511ef7..9fb7e446 100644 --- a/higan/target-higan/settings/settings.hpp +++ b/higan/target-higan/settings/settings.hpp @@ -48,6 +48,8 @@ struct SystemSettings : TabFrameItem { struct VideoSettings : TabFrameItem { VideoSettings(TabFrame*); + auto updateColor(bool initializing = false) -> void; + auto updateOverscan(bool initializing = false) -> void; VerticalLayout layout{this}; Label colorAdjustmentLabel{&layout, Size{~0, 0}, 2}; @@ -72,19 +74,8 @@ struct VideoSettings : TabFrameItem { Label verticalMaskLabel{&verticalMaskLayout, Size{80, 0}}; Label verticalMaskValue{&verticalMaskLayout, Size{50, 0}}; HorizontalSlider verticalMaskSlider{&verticalMaskLayout, Size{~0, 0}}; - Label windowedModeLabel{&layout, Size{~0, 0}, 2}; - HorizontalLayout windowedModeLayout{&layout, Size{~0, 0}}; - CheckLabel windowedModeAspectCorrection{&windowedModeLayout, Size{0, 0}}; - CheckLabel windowedModeIntegralScaling{&windowedModeLayout, Size{0, 0}}; - CheckLabel windowedModeAdaptive{&windowedModeLayout, Size{0, 0}}; - Label fullscreenModeLabel{&layout, Size{~0, 0}, 2}; - HorizontalLayout fullscreenModeLayout{&layout, Size{~0, 0}}; - CheckLabel fullscreenModeAspectCorrection{&fullscreenModeLayout, Size{0, 0}}; - CheckLabel fullscreenModeIntegralScaling{&fullscreenModeLayout, Size{0, 0}}; - CheckLabel fullscreenModeExclusive{&fullscreenModeLayout, Size{0, 0}}; - - auto updateColor(bool initializing = false) -> void; - auto updateViewport(bool initializing = false) -> void; + Label fullscreenLabel{&layout, Size{~0, 0}, 2}; + CheckLabel fullscreenExclusive{&layout, Size{0, 0}}; }; struct AudioSettings : TabFrameItem { diff --git a/higan/target-higan/settings/video.cpp b/higan/target-higan/settings/video.cpp index 7e56f62a..ee8cce79 100644 --- a/higan/target-higan/settings/video.cpp +++ b/higan/target-higan/settings/video.cpp @@ -15,26 +15,21 @@ VideoSettings::VideoSettings(TabFrame* parent) : TabFrameItem(parent) { luminanceValue.setAlignment(0.5); luminanceSlider.setLength(101).setPosition(settings["Video/Luminance"].natural()).onChange([&] { updateColor(); }); - overscanMaskLabel.setFont(Font().setBold()).setText("Overscan Mask"); + overscanMaskLabel.setFont(Font().setBold()).setText("Overscan Area"); horizontalMaskLabel.setText("Horizontal:"); horizontalMaskValue.setAlignment(0.5); - horizontalMaskSlider.setLength(25).setPosition(settings["Video/Overscan/Horizontal"].natural()).onChange([&] { updateViewport(); }); + horizontalMaskSlider.setLength(25).setPosition(settings["View/Overscan/Horizontal"].natural()).onChange([&] { updateOverscan(); }); verticalMaskLabel.setText("Vertical:"); verticalMaskValue.setAlignment(0.5); - verticalMaskSlider.setLength(25).setPosition(settings["Video/Overscan/Vertical"].natural()).onChange([&] { updateViewport(); }); + verticalMaskSlider.setLength(25).setPosition(settings["View/Overscan/Vertical"].natural()).onChange([&] { updateOverscan(); }); - windowedModeLabel.setFont(Font().setBold()).setText("Windowed Mode"); - windowedModeAspectCorrection.setText("Aspect correction").setChecked(settings["Video/Windowed/AspectCorrection"].boolean()).onToggle([&] { updateViewport(); }); - windowedModeIntegralScaling.setText("Integral scaling").setChecked(settings["Video/Windowed/IntegralScaling"].boolean()).onToggle([&] { updateViewport(); }); - windowedModeAdaptive.setText("Adaptive sizing").setChecked(settings["Video/Windowed/Adaptive"].boolean()).onToggle([&] { updateViewport(); }); - - fullscreenModeLabel.setFont(Font().setBold()).setText("Fullscreen Mode"); - fullscreenModeAspectCorrection.setText("Aspect correction").setChecked(settings["Video/Fullscreen/AspectCorrection"].boolean()).onToggle([&] { updateViewport(); }); - fullscreenModeIntegralScaling.setText("Integral scaling").setChecked(settings["Video/Fullscreen/IntegralScaling"].boolean()).onToggle([&] { updateViewport(); }); - fullscreenModeExclusive.setText("Exclusive mode").setChecked(settings["Video/Fullscreen/Exclusive"].boolean()).onToggle([&] { updateViewport(); }); + fullscreenLabel.setFont(Font().setBold()).setText("Fullscreen"); + fullscreenExclusive.setText("Exclusive mode").setChecked(settings["Video/Exclusive"].boolean()).onToggle([&] { + settings["Video/Exclusive"].setValue(fullscreenExclusive.checked()); + }); updateColor(true); - updateViewport(true); + updateOverscan(true); } auto VideoSettings::updateColor(bool initializing) -> void { @@ -48,20 +43,16 @@ auto VideoSettings::updateColor(bool initializing) -> void { if(!initializing) program->updateVideoPalette(); } -auto VideoSettings::updateViewport(bool initializing) -> void { - bool wasAdaptive = settings["Video/Windowed/Adaptive"].boolean(); - bool isAdaptive = windowedModeAdaptive.checked(); - - settings["Video/Overscan/Horizontal"].setValue(horizontalMaskSlider.position()); - settings["Video/Overscan/Vertical"].setValue(verticalMaskSlider.position()); - settings["Video/Windowed/AspectCorrection"].setValue(windowedModeAspectCorrection.checked()); - settings["Video/Windowed/IntegralScaling"].setValue(windowedModeIntegralScaling.checked()); - settings["Video/Windowed/Adaptive"].setValue(windowedModeAdaptive.checked()); - settings["Video/Fullscreen/AspectCorrection"].setValue(fullscreenModeAspectCorrection.checked()); - settings["Video/Fullscreen/IntegralScaling"].setValue(fullscreenModeIntegralScaling.checked()); - settings["Video/Fullscreen/Exclusive"].setValue(fullscreenModeExclusive.checked()); +auto VideoSettings::updateOverscan(bool initializing) -> void { + settings["View/Overscan/Horizontal"].setValue(horizontalMaskSlider.position()); + settings["View/Overscan/Vertical"].setValue(verticalMaskSlider.position()); horizontalMaskValue.setText({horizontalMaskSlider.position()}); verticalMaskValue.setText({verticalMaskSlider.position()}); - if(!initializing) presentation->resizeViewport(isAdaptive || wasAdaptive != isAdaptive); + if(initializing || settings["View/Overscan"].boolean()) return; + if(settings["View/Adaptive"].boolean()) { + presentation->resizeWindow(); + } else { + presentation->resizeViewport(); + } } diff --git a/hiro/cocoa/sizable.cpp b/hiro/cocoa/sizable.cpp index 4e793ab3..2274266d 100644 --- a/hiro/cocoa/sizable.cpp +++ b/hiro/cocoa/sizable.cpp @@ -13,6 +13,7 @@ auto pSizable::minimumSize() const -> Size { } auto pSizable::setGeometry(Geometry geometry) -> void { + self().doSize(); } } diff --git a/hiro/cocoa/widget/widget.cpp b/hiro/cocoa/widget/widget.cpp index 0d0357d8..d61e9834 100644 --- a/hiro/cocoa/widget/widget.cpp +++ b/hiro/cocoa/widget/widget.cpp @@ -14,6 +14,7 @@ auto pWidget::construct() -> void { if(auto p = window->self()) p->_append(self()); setEnabled(self().enabled(true)); setFont(self().font(true)); + setToolTip(self().toolTip()); setVisible(self().visible(true)); } } @@ -62,7 +63,11 @@ auto pWidget::setGeometry(Geometry geometry) -> void { [cocoaView setFrame:NSMakeRect(geometry.x(), windowHeight - geometry.y() - geometry.height(), geometry.width(), geometry.height())]; [[cocoaView superview] setNeedsDisplay:YES]; } - self().doSize(); + pSizable::setGeometry(geometry); +} + +auto pWidget::setToolTip(const string& toolTip) -> void { + //TODO } auto pWidget::setVisible(bool visible) -> void { diff --git a/hiro/cocoa/widget/widget.hpp b/hiro/cocoa/widget/widget.hpp index ca5e48e9..6f9f0904 100644 --- a/hiro/cocoa/widget/widget.hpp +++ b/hiro/cocoa/widget/widget.hpp @@ -10,6 +10,7 @@ struct pWidget : pSizable { auto setFocused() -> void override; auto setFont(const Font& font) -> void override; auto setGeometry(Geometry geometry) -> void override; + auto setToolTip(const string& toolTip) -> void; auto setVisible(bool visible) -> void override; NSView* cocoaView = nullptr; diff --git a/hiro/core/application.cpp b/hiro/core/application.cpp index b3cf0716..36a35fd5 100644 --- a/hiro/core/application.cpp +++ b/hiro/core/application.cpp @@ -72,7 +72,11 @@ auto Application::setScale(float scale) -> void { auto Application::setScreenSaver(bool screenSaver) -> void { state().screenSaver = screenSaver; - pApplication::setScreenSaver(screenSaver); + if(state().initialized) pApplication::setScreenSaver(screenSaver); +} + +auto Application::setToolTips(bool toolTips) -> void { + state().toolTips = toolTips; } //this ensures Application::state is initialized prior to use @@ -81,6 +85,10 @@ auto Application::state() -> State& { return state; } +auto Application::toolTips() -> bool { + return state().toolTips; +} + auto Application::unscale(float value) -> float { return value * (1.0 / state().scale); } @@ -135,11 +143,11 @@ auto Application::Cocoa::onQuit(const function& callback) -> void { //======== auto Application::initialize() -> void { - static bool initialized = false; - if(initialized == false) { - initialized = true; + if(!state().initialized) { hiro::initialize(); - return pApplication::initialize(); + pApplication::initialize(); + state().initialized = true; + pApplication::setScreenSaver(state().screenSaver); } } diff --git a/hiro/core/application.hpp b/hiro/core/application.hpp index 4f87f803..9e240be2 100644 --- a/hiro/core/application.hpp +++ b/hiro/core/application.hpp @@ -20,6 +20,8 @@ struct Application { static auto setName(const string& name = "") -> void; static auto setScale(float scale = 1.0) -> void; static auto setScreenSaver(bool screenSaver = true) -> void; + static auto setToolTips(bool toolTips = true) -> void; + static auto toolTips() -> bool; static auto unscale(float value) -> float; struct Windows { @@ -45,6 +47,7 @@ struct Application { //private: struct State { Font font; + bool initialized = false; Locale locale; int modal = 0; string name; @@ -52,6 +55,7 @@ struct Application { bool quit = false; float scale = 1.0; bool screenSaver = true; + bool toolTips = true; struct Windows { function onModalChange; diff --git a/hiro/core/core.hpp b/hiro/core/core.hpp index af391a21..68c7b22f 100644 --- a/hiro/core/core.hpp +++ b/hiro/core/core.hpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include @@ -25,7 +26,7 @@ using nall::set; using nall::shared_pointer; using nall::shared_pointer_weak; using nall::string; -using nall::string_pascal; +using nall::unique_pointer; using nall::vector; namespace hiro { @@ -1218,84 +1219,8 @@ struct mRadioLabel : mWidget { #endif #include "widget/source-edit.hpp" - -#if defined(Hiro_TabFrame) -struct mTabFrame : mWidget { - Declare(TabFrame) - using mObject::remove; - friend class mTabFrameItem; - - auto append(sTabFrameItem item) -> type&; - auto doChange() const -> void; - auto doClose(sTabFrameItem item) const -> void; - auto doMove(sTabFrameItem from, sTabFrameItem to) const -> void; - auto item(uint position) const -> TabFrameItem; - auto itemCount() const -> uint; - auto items() const -> vector; - auto navigation() const -> Navigation; - auto onChange(const function& callback = {}) -> type&; - auto onClose(const function& callback = {}) -> type&; - auto onMove(const function& callback = {}) -> type&; - auto remove(sTabFrameItem item) -> type&; - auto reset() -> type&; - auto selected() const -> TabFrameItem; - auto setEnabled(bool enabled = true) -> type& override; - auto setFont(const Font& font = {}) -> type& override; - auto setNavigation(Navigation navigation = Navigation::Top) -> type&; - auto setParent(mObject* object = nullptr, int offset = -1) -> type& override; - auto setVisible(bool visible = true) -> type& override; - -//private: - struct State { - vector items; - Navigation navigation = Navigation::Top; - function onChange; - function onClose; - function onMove; - } state; - - auto destruct() -> void override; -}; -#endif - -#if defined(Hiro_TabFrame) -struct mTabFrameItem : mObject { - Declare(TabFrameItem) - - auto append(sSizable sizable) -> type&; - auto closable() const -> bool; - auto icon() const -> image; - auto movable() const -> bool; - auto remove() -> type& override; - auto remove(sSizable sizable) -> type&; - auto reset() -> type&; - auto selected() const -> bool; - auto setClosable(bool closable = true) -> type&; - auto setEnabled(bool enabled = true) -> type& override; - auto setFont(const Font& font = {}) -> type& override; - auto setIcon(const image& icon = {}) -> type&; - auto setMovable(bool movable = true) -> type&; - auto setParent(mObject* object = nullptr, int offset = -1) -> type& override; - auto setSelected() -> type&; - auto setText(const string& text = "") -> type&; - auto setVisible(bool visible = true) -> type& override; - auto sizable() const -> Sizable; - auto text() const -> string; - -//private: - struct State { - bool closable = false; - image icon; - bool movable = false; - bool selected = false; - sSizable sizable; - string text; - } state; - - auto destruct() -> void override; -}; -#endif - +#include "widget/tab-frame.hpp" +#include "widget/tab-frame-item.hpp" #include "widget/table-view.hpp" #include "widget/table-view-column.hpp" #include "widget/table-view-item.hpp" diff --git a/hiro/core/object.hpp b/hiro/core/object.hpp index 788255be..5942053b 100644 --- a/hiro/core/object.hpp +++ b/hiro/core/object.hpp @@ -46,9 +46,9 @@ struct mObject { auto visible(bool recursive = false) const -> bool; //private: -//sizeof(mObject) == 72 +//sizeof(mObject) == 88 struct State { - Font font; //16 + Font font; //32 set properties; //16 mObject* parent = nullptr; // 8 int offset = -1; // 4 diff --git a/hiro/core/shared.hpp b/hiro/core/shared.hpp index 46042a55..c7e019ea 100644 --- a/hiro/core/shared.hpp +++ b/hiro/core/shared.hpp @@ -52,14 +52,16 @@ #define DeclareSharedSizable(Name) \ DeclareSharedObject(Name) \ + auto doSize() const { return self().doSize(); } \ auto geometry() const { return self().geometry(); } \ auto minimumSize() const { return self().minimumSize(); } \ + auto onSize(const function& callback = {}) { return self().onSize(callback), *this; } \ auto setGeometry(Geometry geometry) { return self().setGeometry(geometry), *this; } \ #define DeclareSharedWidget(Name) \ DeclareSharedSizable(Name) \ - auto doSize() const { return self().doSize(); } \ - auto onSize(const function& callback = {}) { return self().onSize(callback), *this; } \ + auto setToolTip(const string& toolTip = "") { return self().setToolTip(toolTip), *this; } \ + auto toolTip() const { return self().toolTip(); } \ #if defined(Hiro_Object) struct Object : sObject { @@ -75,9 +77,6 @@ struct Group : sGroup { auto append(sObject object) -> type& { return self().append(object), *this; } auto object(unsigned position) const { return self().object(position); } auto objectCount() const { return self().objectCount(); } -//auto objects() const { return self().objects(); } - auto remove(sObject object) -> type& { return self().remove(object), *this; } - template auto objects() const -> vector { vector objects; for(auto object : self().objects()) { @@ -85,6 +84,7 @@ struct Group : sGroup { } return objects; } + auto remove(sObject object) -> type& { return self().remove(object), *this; } private: auto _append() {} diff --git a/hiro/core/sizable.cpp b/hiro/core/sizable.cpp index b0a66c43..b4058ecf 100644 --- a/hiro/core/sizable.cpp +++ b/hiro/core/sizable.cpp @@ -4,6 +4,10 @@ auto mSizable::allocate() -> pObject* { return new pSizable(*this); } +auto mSizable::doSize() const -> void { + if(state.onSize) return state.onSize(); +} + auto mSizable::geometry() const -> Geometry { return state.geometry; } @@ -12,6 +16,11 @@ auto mSizable::minimumSize() const -> Size { return signal(minimumSize); } +auto mSizable::onSize(const function& callback) -> type& { + state.onSize = callback; + return *this; +} + auto mSizable::setGeometry(Geometry geometry) -> type& { state.geometry = geometry; signal(setGeometry, geometry); diff --git a/hiro/core/sizable.hpp b/hiro/core/sizable.hpp index 7cecda79..b67d3858 100644 --- a/hiro/core/sizable.hpp +++ b/hiro/core/sizable.hpp @@ -2,14 +2,17 @@ struct mSizable : mObject { Declare(Sizable) + auto doSize() const -> void; auto geometry() const -> Geometry; virtual auto minimumSize() const -> Size; + auto onSize(const function& callback = {}) -> type&; virtual auto setGeometry(Geometry geometry) -> type&; //private: -//sizeof(mSizable) == 16 +//sizeof(mSizable) == 24 struct State { Geometry geometry; + function onSize; } state; }; #endif diff --git a/hiro/core/widget/tab-frame-item.hpp b/hiro/core/widget/tab-frame-item.hpp new file mode 100644 index 00000000..b59d9dc1 --- /dev/null +++ b/hiro/core/widget/tab-frame-item.hpp @@ -0,0 +1,37 @@ +#if defined(Hiro_TabFrame) +struct mTabFrameItem : mObject { + Declare(TabFrameItem) + + auto append(sSizable sizable) -> type&; + auto closable() const -> bool; + auto icon() const -> image; + auto movable() const -> bool; + auto remove() -> type& override; + auto remove(sSizable sizable) -> type&; + auto reset() -> type&; + auto selected() const -> bool; + auto setClosable(bool closable = true) -> type&; + auto setEnabled(bool enabled = true) -> type& override; + auto setFont(const Font& font = {}) -> type& override; + auto setIcon(const image& icon = {}) -> type&; + auto setMovable(bool movable = true) -> type&; + auto setParent(mObject* object = nullptr, int offset = -1) -> type& override; + auto setSelected() -> type&; + auto setText(const string& text = "") -> type&; + auto setVisible(bool visible = true) -> type& override; + auto sizable() const -> Sizable; + auto text() const -> string; + +//private: + struct State { + bool closable = false; + image icon; + bool movable = false; + bool selected = false; + sSizable sizable; + string text; + } state; + + auto destruct() -> void override; +}; +#endif diff --git a/hiro/core/widget/tab-frame.hpp b/hiro/core/widget/tab-frame.hpp new file mode 100644 index 00000000..b6e7c92a --- /dev/null +++ b/hiro/core/widget/tab-frame.hpp @@ -0,0 +1,38 @@ +#if defined(Hiro_TabFrame) +struct mTabFrame : mWidget { + Declare(TabFrame) + using mObject::remove; + friend class mTabFrameItem; + + auto append(sTabFrameItem item) -> type&; + auto doChange() const -> void; + auto doClose(sTabFrameItem item) const -> void; + auto doMove(sTabFrameItem from, sTabFrameItem to) const -> void; + auto item(uint position) const -> TabFrameItem; + auto itemCount() const -> uint; + auto items() const -> vector; + auto navigation() const -> Navigation; + auto onChange(const function& callback = {}) -> type&; + auto onClose(const function& callback = {}) -> type&; + auto onMove(const function& callback = {}) -> type&; + auto remove(sTabFrameItem item) -> type&; + auto reset() -> type&; + auto selected() const -> TabFrameItem; + auto setEnabled(bool enabled = true) -> type& override; + auto setFont(const Font& font = {}) -> type& override; + auto setNavigation(Navigation navigation = Navigation::Top) -> type&; + auto setParent(mObject* object = nullptr, int offset = -1) -> type& override; + auto setVisible(bool visible = true) -> type& override; + +//private: + struct State { + vector items; + Navigation navigation = Navigation::Top; + function onChange; + function onClose; + function onMove; + } state; + + auto destruct() -> void override; +}; +#endif diff --git a/hiro/core/widget/widget.cpp b/hiro/core/widget/widget.cpp index 2292cfde..c13d1b84 100644 --- a/hiro/core/widget/widget.cpp +++ b/hiro/core/widget/widget.cpp @@ -6,15 +6,6 @@ auto mWidget::allocate() -> pObject* { // -auto mWidget::doSize() const -> void { - if(state.onSize) return state.onSize(); -} - -auto mWidget::onSize(const function& callback) -> type& { - state.onSize = callback; - return *this; -} - auto mWidget::remove() -> type& { //TODO: how to implement this after removing mLayout? //if(auto layout = parentLayout()) layout->remove(layout->sizable(offset())); @@ -22,4 +13,16 @@ auto mWidget::remove() -> type& { return *this; } +auto mWidget::setToolTip(const string& toolTip) -> type& { + state.toolTip = toolTip; + //TODO: allow this option to dynamically control tool tips + if(!Application::state().toolTips) return *this; + signal(setToolTip, toolTip); + return *this; +} + +auto mWidget::toolTip() const -> string { + return state.toolTip; +} + #endif diff --git a/hiro/core/widget/widget.hpp b/hiro/core/widget/widget.hpp index fa1e46c4..4412935f 100644 --- a/hiro/core/widget/widget.hpp +++ b/hiro/core/widget/widget.hpp @@ -2,14 +2,14 @@ struct mWidget : mSizable { Declare(Widget) - auto doSize() const -> void; - auto onSize(const function& callback = {}) -> type&; auto remove() -> type& override; + auto setToolTip(const string& toolTip = "") -> type&; + auto toolTip() const -> string; //private: -//sizeof(mWidget) == 8 +//sizeof(mWidget) == 32 struct State { - function onSize; + string toolTip; } state; }; #endif diff --git a/hiro/extension/vertical-layout.cpp b/hiro/extension/vertical-layout.cpp index ec967da0..3d2c1871 100644 --- a/hiro/extension/vertical-layout.cpp +++ b/hiro/extension/vertical-layout.cpp @@ -166,7 +166,7 @@ auto mVerticalLayout::setGeometry(Geometry geometry) -> type& { auto cell = this->cell(index); auto alignment = cell.alignment(); if(!alignment) alignment = this->alignment(); - if(!alignment) alignment = 0.5; + if(!alignment) alignment = 0.0; float cellWidth = cell.size().width(); float cellHeight = geometryHeight; if(cellWidth == Size::Minimum) cellWidth = cell.sizable()->minimumSize().width(); diff --git a/hiro/gtk/application.cpp b/hiro/gtk/application.cpp index bc06d0a5..0604fc76 100644 --- a/hiro/gtk/application.cpp +++ b/hiro/gtk/application.cpp @@ -2,13 +2,6 @@ namespace hiro { -vector pApplication::windows; - -#if defined(DISPLAY_XORG) -XlibDisplay* pApplication::display = nullptr; -bool pApplication::xdgScreenSaver = false; -#endif - auto pApplication::run() -> void { while(!Application::state().quit) { Application::doMain(); @@ -22,7 +15,7 @@ auto pApplication::pendingEvents() -> bool { auto pApplication::processEvents() -> void { while(pendingEvents()) gtk_main_iteration_do(false); - for(auto& window : windows) window->_synchronizeGeometry(); + for(auto& window : state().windows) window->_synchronizeGeometry(); } auto pApplication::quit() -> void { @@ -30,21 +23,54 @@ auto pApplication::quit() -> void { if(gtk_main_level()) gtk_main_quit(); #if defined(DISPLAY_XORG) - XCloseDisplay(display); - display = nullptr; + if(state().display) { + if(state().screenSaverXDG && state().screenSaverWindow) { + //this needs to run synchronously, so that XUnmapWindow() won't happen before xdg-screensaver is finished + execute("xdg-screensaver", "resume", string{"0x", hex(state().screenSaverWindow)}); + XUnmapWindow(state().display, state().screenSaverWindow); + state().screenSaverWindow = 0; + } + XCloseDisplay(state().display); + state().display = nullptr; + } #endif } auto pApplication::setScreenSaver(bool screenSaver) -> void { #if defined(DISPLAY_XORG) - for(auto& window : windows) window->_setScreenSaver(screenSaver); + if(state().screenSaverXDG && state().screenSaverWindow) { + invoke("xdg-screensaver", screenSaver ? "resume" : "suspend", string{"0x", hex(state().screenSaverWindow)}); + } #endif } +auto pApplication::state() -> State& { + static State state; + return state; +} + auto pApplication::initialize() -> void { #if defined(DISPLAY_XORG) - display = XOpenDisplay(nullptr); - xdgScreenSaver = (bool)execute("xdg-screensaver", "--version").output.find("xdg-screensaver"); + state().display = XOpenDisplay(nullptr); + state().screenSaverXDG = (bool)execute("xdg-screensaver", "--version").output.find("xdg-screensaver"); + + if(state().screenSaverXDG) { + auto screen = DefaultScreen(state().display); + auto rootWindow = RootWindow(state().display, screen); + XSetWindowAttributes attributes{}; + attributes.background_pixel = BlackPixel(state().display, screen); + attributes.border_pixel = 0; + attributes.override_redirect = true; + state().screenSaverWindow = XCreateWindow(state().display, rootWindow, + 0, 0, 1, 1, 0, DefaultDepth(state().display, screen), + InputOutput, DefaultVisual(state().display, screen), + CWBackPixel | CWBorderPixel | CWOverrideRedirect, &attributes + ); + //note: hopefully xdg-screensaver does not require the window to be mapped ... + //if it does, we're in trouble: a small 1x1 black pixel window will be visible in said case + XMapWindow(state().display, state().screenSaverWindow); + XFlush(state().display); + } #endif //set WM_CLASS to Application::name() diff --git a/hiro/gtk/application.hpp b/hiro/gtk/application.hpp index d8aea834..8520f333 100644 --- a/hiro/gtk/application.hpp +++ b/hiro/gtk/application.hpp @@ -11,12 +11,17 @@ struct pApplication { static auto initialize() -> void; - static vector windows; + struct State { + vector windows; - #if defined(DISPLAY_XORG) - static XlibDisplay* display; - static bool xdgScreenSaver; - #endif + #if defined(DISPLAY_XORG) + XlibDisplay* display = nullptr; + XlibWindow screenSaverWindow = 0; + bool screenSaverXDG = false; + #endif + }; + + static auto state() -> State&; }; } diff --git a/hiro/gtk/keyboard.cpp b/hiro/gtk/keyboard.cpp index 3be81caf..4e534c8f 100644 --- a/hiro/gtk/keyboard.cpp +++ b/hiro/gtk/keyboard.cpp @@ -8,7 +8,7 @@ auto pKeyboard::poll() -> vector { vector result; char state[256]; #if defined(DISPLAY_XORG) - XQueryKeymap(pApplication::display, state); + XQueryKeymap(pApplication::state().display, state); #endif for(auto& code : settings.keycodes) { result.append(_pressed(state, code)); @@ -19,7 +19,7 @@ auto pKeyboard::poll() -> vector { auto pKeyboard::pressed(unsigned code) -> bool { char state[256]; #if defined(DISPLAY_XORG) - XQueryKeymap(pApplication::display, state); + XQueryKeymap(pApplication::state().display, state); #endif return _pressed(state, code); } @@ -226,8 +226,8 @@ auto pKeyboard::_translate(unsigned code) -> signed { auto pKeyboard::initialize() -> void { auto append = [](unsigned lo, unsigned hi = 0) { #if defined(DISPLAY_XORG) - lo = lo ? (uint8_t)XKeysymToKeycode(pApplication::display, lo) : 0; - hi = hi ? (uint8_t)XKeysymToKeycode(pApplication::display, hi) : 0; + lo = lo ? (uint8_t)XKeysymToKeycode(pApplication::state().display, lo) : 0; + hi = hi ? (uint8_t)XKeysymToKeycode(pApplication::state().display, hi) : 0; #endif settings.keycodes.append(lo | (hi << 8)); }; diff --git a/hiro/gtk/mouse.cpp b/hiro/gtk/mouse.cpp index 645d9b7b..63ee3f9f 100644 --- a/hiro/gtk/mouse.cpp +++ b/hiro/gtk/mouse.cpp @@ -13,7 +13,7 @@ auto pMouse::position() -> Position { XlibWindow root, child; int rootx, rooty, winx, winy; unsigned int mask; - XQueryPointer(pApplication::display, DefaultRootWindow(pApplication::display), &root, &child, &rootx, &rooty, &winx, &winy, &mask); + XQueryPointer(pApplication::state().display, DefaultRootWindow(pApplication::state().display), &root, &child, &rootx, &rooty, &winx, &winy, &mask); return {rootx, rooty}; #endif } @@ -31,7 +31,7 @@ auto pMouse::pressed(Mouse::Button button) -> bool { XlibWindow root, child; int rootx, rooty, winx, winy; unsigned int mask; - XQueryPointer(pApplication::display, DefaultRootWindow(pApplication::display), &root, &child, &rootx, &rooty, &winx, &winy, &mask); + XQueryPointer(pApplication::state().display, DefaultRootWindow(pApplication::state().display), &root, &child, &rootx, &rooty, &winx, &winy, &mask); switch(button) { case Mouse::Button::Left: return mask & Button1Mask; case Mouse::Button::Middle: return mask & Button2Mask; diff --git a/hiro/gtk/sizable.cpp b/hiro/gtk/sizable.cpp index 4e793ab3..2274266d 100644 --- a/hiro/gtk/sizable.cpp +++ b/hiro/gtk/sizable.cpp @@ -13,6 +13,7 @@ auto pSizable::minimumSize() const -> Size { } auto pSizable::setGeometry(Geometry geometry) -> void { + self().doSize(); } } diff --git a/hiro/gtk/widget/widget.cpp b/hiro/gtk/widget/widget.cpp index 808edf23..069af87c 100644 --- a/hiro/gtk/widget/widget.cpp +++ b/hiro/gtk/widget/widget.cpp @@ -6,7 +6,9 @@ auto pWidget::construct() -> void { if(!gtkWidget) return; if(auto window = self().parentWindow(true)) { if(window->self()) window->self()->_append(self()); + setEnabled(self().enabled(true)); setFont(self().font(true)); + setToolTip(self().toolTip()); setVisible(self().visible(true)); } } @@ -58,7 +60,11 @@ auto pWidget::setGeometry(Geometry geometry) -> void { locked = false; } } - self().doSize(); + pSizable::setGeometry(geometry); +} + +auto pWidget::setToolTip(const string& toolTip) -> void { + gtk_widget_set_tooltip_text(gtkWidget, toolTip); } auto pWidget::setVisible(bool visible) -> void { diff --git a/hiro/gtk/widget/widget.hpp b/hiro/gtk/widget/widget.hpp index 831f0007..9058c128 100644 --- a/hiro/gtk/widget/widget.hpp +++ b/hiro/gtk/widget/widget.hpp @@ -11,6 +11,7 @@ struct pWidget : pSizable { auto setFocused() -> void override; auto setFont(const Font& font) -> void override; auto setGeometry(Geometry geometry) -> void override; + auto setToolTip(const string& toolTip) -> void; auto setVisible(bool visible) -> void override; GtkWidget* gtkWidget = nullptr; diff --git a/hiro/gtk/window.cpp b/hiro/gtk/window.cpp index ca62dc83..5e718625 100644 --- a/hiro/gtk/window.cpp +++ b/hiro/gtk/window.cpp @@ -100,7 +100,6 @@ static auto Window_keyRelease(GtkWidget* widget, GdkEventKey* event, pWindow* p) } static auto Window_realize(GtkWidget* widget, pWindow* p) -> void { - p->_setScreenSaver(Application::screenSaver()); } static auto Window_sizeAllocate(GtkWidget* widget, GtkAllocation* allocation, pWindow* p) -> void { @@ -130,7 +129,6 @@ static auto Window_stateEvent(GtkWidget* widget, GdkEvent* event, pWindow* p) -> } static auto Window_unrealize(GtkWidget* widget, pWindow* p) -> void { - p->_setScreenSaver(true); } auto pWindow::construct() -> void { @@ -209,13 +207,13 @@ auto pWindow::construct() -> void { g_object_set_data(G_OBJECT(widget), "hiro::window", (gpointer)this); g_object_set_data(G_OBJECT(formContainer), "hiro::window", (gpointer)this); - pApplication::windows.append(this); + pApplication::state().windows.append(this); } auto pWindow::destruct() -> void { - for(uint offset : range(pApplication::windows.size())) { - if(pApplication::windows[offset] == this) { - pApplication::windows.remove(offset); + for(uint offset : range(pApplication::state().windows.size())) { + if(pApplication::state().windows[offset] == this) { + pApplication::state().windows.remove(offset); break; } } @@ -495,19 +493,6 @@ auto pWindow::_setMenuVisible(bool visible) -> void { gtk_widget_set_visible(gtkMenu, visible); } -auto pWindow::_setScreenSaver(bool screenSaver) -> void { - if(!gtk_widget_get_realized(widget)) return; - - #if defined(DISPLAY_XORG) - if(pApplication::xdgScreenSaver) { - if(this->screenSaver != screenSaver) { - this->screenSaver = screenSaver; - invoke("xdg-screensaver", screenSaver ? "resume" : "suspend", string{"0x", hex(handle())}); - } - } - #endif -} - auto pWindow::_setStatusEnabled(bool enabled) -> void { gtk_widget_set_sensitive(gtkStatus, enabled); } diff --git a/hiro/gtk/window.hpp b/hiro/gtk/window.hpp index 95758452..74a24da4 100644 --- a/hiro/gtk/window.hpp +++ b/hiro/gtk/window.hpp @@ -35,7 +35,6 @@ struct pWindow : pObject { auto _append(mMenu& menu) -> void; auto _menuHeight() const -> int; auto _menuTextHeight() const -> int; - auto _setScreenSaver(bool screenSaver) -> void; auto _setIcon(const string& basename) -> bool; auto _setMenuEnabled(bool enabled) -> void; auto _setMenuFont(const Font& font) -> void; diff --git a/hiro/qt/application.cpp b/hiro/qt/application.cpp index 35f94727..3e607564 100644 --- a/hiro/qt/application.cpp +++ b/hiro/qt/application.cpp @@ -2,8 +2,6 @@ namespace hiro { -XlibDisplay* pApplication::display = nullptr; - auto pApplication::run() -> void { if(Application::state().onMain) { while(!Application::state().quit) { @@ -26,17 +24,37 @@ auto pApplication::processEvents() -> void { auto pApplication::quit() -> void { QApplication::quit(); qtApplication = nullptr; //note: deleting QApplication will crash libQtGui + + if(state().display) { + if(state().screenSaverXDG && state().screenSaverWindow) { + //this needs to run synchronously, so that XUnmapWindow() won't happen before xdg-screensaver is finished + execute("xdg-screensaver", "resume", string{"0x", hex(state().screenSaverWindow)}); + XUnmapWindow(state().display, state().screenSaverWindow); + state().screenSaverWindow = 0; + } + XCloseDisplay(state().display); + state().display = nullptr; + } } auto pApplication::setScreenSaver(bool screenSaver) -> void { - //TODO: not implemented + #if defined(DISPLAY_XORG) + if(state().screenSaverXDG && state().screenSaverWindow) { + invoke("xdg-screensaver", screenSaver ? "resume" : "suspend", string{"0x", hex(state().screenSaverWindow)}); + } + #endif +} + +auto pApplication::state() -> State& { + static State state; + return state; } //this is sadly necessary for things like determining window frame geometry //obviously, it is used as sparingly as possible -auto pApplication::syncX() -> void { +auto pApplication::synchronize() -> void { for(auto n : range(8)) { - #if HIRO_QT==4 + #if HIRO_QT==4 && defined(DISPLAY_XORG) QApplication::syncX(); #elif HIRO_QT==5 QApplication::sync(); @@ -51,7 +69,28 @@ auto pApplication::initialize() -> void { setenv("QTCOMPOSE", "/usr/local/lib/X11/locale/", 0); #endif - display = XOpenDisplay(0); + #if defined(DISPLAY_XORG) + state().display = XOpenDisplay(nullptr); + state().screenSaverXDG = (bool)execute("xdg-screensaver", "--version").output.find("xdg-screensaver"); + + if(state().screenSaverXDG) { + auto screen = DefaultScreen(state().display); + auto rootWindow = RootWindow(state().display, screen); + XSetWindowAttributes attributes{}; + attributes.background_pixel = BlackPixel(state().display, screen); + attributes.border_pixel = 0; + attributes.override_redirect = true; + state().screenSaverWindow = XCreateWindow(state().display, rootWindow, + 0, 0, 1, 1, 0, DefaultDepth(state().display, screen), + InputOutput, DefaultVisual(state().display, screen), + CWBackPixel | CWBorderPixel | CWOverrideRedirect, &attributes + ); + //note: hopefully xdg-screensaver does not require the window to be mapped ... + //if it does, we're in trouble: a small 1x1 black pixel window will be visible in said case + XMapWindow(state().display, state().screenSaverWindow); + XFlush(state().display); + } + #endif static auto name = Application::state().name ? Application::state().name : string{"hiro"}; diff --git a/hiro/qt/application.hpp b/hiro/qt/application.hpp index 2beae538..c292fb99 100644 --- a/hiro/qt/application.hpp +++ b/hiro/qt/application.hpp @@ -10,9 +10,17 @@ struct pApplication { static auto setScreenSaver(bool screenSaver) -> void; static auto initialize() -> void; - static auto syncX() -> void; + static auto synchronize() -> void; - static XlibDisplay* display; + struct State { + #if defined(DISPLAY_XORG) + XlibDisplay* display = nullptr; + XlibWindow screenSaverWindow = 0; + bool screenSaverXDG = false; + #endif + }; + + static auto state() -> State&; }; static QApplication* qtApplication = nullptr; diff --git a/hiro/qt/keyboard.cpp b/hiro/qt/keyboard.cpp index 4872be66..7cc68891 100644 --- a/hiro/qt/keyboard.cpp +++ b/hiro/qt/keyboard.cpp @@ -3,18 +3,29 @@ namespace hiro { auto pKeyboard::poll() -> vector { + if(Application::state().quit) return {}; + vector result; char state[256]; - XQueryKeymap(pApplication::display, state); + + #if defined(DISPLAY_XORG) + XQueryKeymap(pApplication::state().display, state); + #endif + for(auto& code : settings.keycodes) { result.append(_pressed(state, code)); } + return result; } auto pKeyboard::pressed(unsigned code) -> bool { char state[256]; - XQueryKeymap(pApplication::display, state); + + #if defined(DISPLAY_XORG) + XQueryKeymap(pApplication::state().display, state); + #endif + return _pressed(state, code); } @@ -22,22 +33,31 @@ auto pKeyboard::_pressed(const char* state, uint16_t code) -> bool { uint8_t lo = code >> 0; uint8_t hi = code >> 8; + #if defined(DISPLAY_XORG) if(lo && state[lo >> 3] & (1 << (lo & 7))) return true; if(hi && state[hi >> 3] & (1 << (hi & 7))) return true; + #endif return false; } auto pKeyboard::initialize() -> void { auto append = [](unsigned lo, unsigned hi = 0) { - lo = lo ? (uint8_t)XKeysymToKeycode(pApplication::display, lo) : 0; - hi = hi ? (uint8_t)XKeysymToKeycode(pApplication::display, hi) : 0; + #if defined(DISPLAY_XORG) + lo = lo ? (uint8_t)XKeysymToKeycode(pApplication::state().display, lo) : 0; + hi = hi ? (uint8_t)XKeysymToKeycode(pApplication::state().display, hi) : 0; + #endif settings.keycodes.append(lo << 0 | hi << 8); }; #define map(name, ...) if(key == name) { append(__VA_ARGS__); continue; } for(auto& key : Keyboard::keys) { - #include + #if defined(DISPLAY_XORG) + #include + #endif + + //print("[hiro/qt] warning: unhandled key: ", key, "\n"); + append(0); } #undef map } diff --git a/hiro/qt/sizable.cpp b/hiro/qt/sizable.cpp index 4e793ab3..2274266d 100644 --- a/hiro/qt/sizable.cpp +++ b/hiro/qt/sizable.cpp @@ -13,6 +13,7 @@ auto pSizable::minimumSize() const -> Size { } auto pSizable::setGeometry(Geometry geometry) -> void { + self().doSize(); } } diff --git a/hiro/qt/widget/widget.cpp b/hiro/qt/widget/widget.cpp index cf5242b3..05dc77ef 100644 --- a/hiro/qt/widget/widget.cpp +++ b/hiro/qt/widget/widget.cpp @@ -25,7 +25,9 @@ auto pWidget::construct() -> void { qtWidget->setParent(container); } + setEnabled(self().enabled(true)); setFont(self().font(true)); + setToolTip(self().toolTip()); setVisible(self().visible(true)); } @@ -55,7 +57,12 @@ auto pWidget::setFont(const Font& font) -> void { auto pWidget::setGeometry(Geometry geometry) -> void { if(!qtWidget) return; qtWidget->setGeometry(geometry.x(), geometry.y(), geometry.width(), geometry.height()); - self().doSize(); + pSizable::setGeometry(geometry); +} + +auto pWidget::setToolTip(const string& toolTip) -> void { + if(!qtWidget) return; + qtWidget->setToolTip(QString::fromUtf8(toolTip)); } auto pWidget::setVisible(bool visible) -> void { diff --git a/hiro/qt/widget/widget.hpp b/hiro/qt/widget/widget.hpp index 080c2a14..a55fb018 100644 --- a/hiro/qt/widget/widget.hpp +++ b/hiro/qt/widget/widget.hpp @@ -10,6 +10,7 @@ struct pWidget : pSizable { auto setFocused() -> void override; auto setFont(const Font& font) -> void override; auto setGeometry(Geometry geometry) -> void override; + auto setToolTip(const string& toolTip) -> void; auto setVisible(bool visible) -> void override; QWidget* qtWidget = nullptr; diff --git a/hiro/qt/window.cpp b/hiro/qt/window.cpp index 9028df78..2ed5a18e 100644 --- a/hiro/qt/window.cpp +++ b/hiro/qt/window.cpp @@ -269,7 +269,7 @@ auto pWindow::_statusTextHeight() const -> uint { } auto pWindow::_updateFrameGeometry() -> void { - pApplication::syncX(); + pApplication::synchronize(); QRect border = qtWindow->frameGeometry(); QRect client = qtWindow->geometry(); @@ -279,12 +279,12 @@ auto pWindow::_updateFrameGeometry() -> void { settings.geometry.frameHeight = border.height() - client.height(); if(qtMenuBar->isVisible()) { - pApplication::syncX(); + pApplication::synchronize(); settings.geometry.menuHeight = qtMenuBar->height() - _menuTextHeight(); } if(qtStatusBar->isVisible()) { - pApplication::syncX(); + pApplication::synchronize(); settings.geometry.statusHeight = qtStatusBar->height() - _statusTextHeight(); } } diff --git a/hiro/windows/application.cpp b/hiro/windows/application.cpp index 8078b79d..77b29b00 100644 --- a/hiro/windows/application.cpp +++ b/hiro/windows/application.cpp @@ -61,7 +61,7 @@ auto pApplication::initialize() -> void { CoInitialize(0); InitCommonControls(); - WNDCLASS wc; + WNDCLASS wc{}; #if defined(Hiro_Window) wc.cbClsExtra = 0; @@ -91,43 +91,29 @@ auto pApplication::initialize() -> void { RegisterClass(&wc); #endif - #if defined(Hiro_Canvas) + #if defined(Hiro_Widget) wc.cbClsExtra = 0; wc.cbWndExtra = 0; wc.hbrBackground = GetSysColorBrush(COLOR_3DFACE); wc.hCursor = LoadCursor(0, IDC_ARROW); wc.hIcon = LoadIcon(0, IDI_APPLICATION); wc.hInstance = GetModuleHandle(0); - wc.lpfnWndProc = Canvas_windowProc; - wc.lpszClassName = L"hiroCanvas"; + wc.lpfnWndProc = ToolTip_windowProc; + wc.lpszClassName = L"hiroToolTip"; wc.lpszMenuName = 0; wc.style = CS_HREDRAW | CS_VREDRAW; RegisterClass(&wc); #endif - #if defined(Hiro_Label) + #if defined(Hiro_Widget) wc.cbClsExtra = 0; wc.cbWndExtra = 0; wc.hbrBackground = GetSysColorBrush(COLOR_3DFACE); wc.hCursor = LoadCursor(0, IDC_ARROW); wc.hIcon = LoadIcon(0, IDI_APPLICATION); wc.hInstance = GetModuleHandle(0); - wc.lpfnWndProc = Label_windowProc; - wc.lpszClassName = L"hiroLabel"; - wc.lpszMenuName = 0; - wc.style = CS_HREDRAW | CS_VREDRAW; - RegisterClass(&wc); - #endif - - #if defined(Hiro_Viewport) - wc.cbClsExtra = 0; - wc.cbWndExtra = 0; - wc.hbrBackground = CreateSolidBrush(RGB(0, 0, 0)); - wc.hCursor = LoadCursor(0, IDC_ARROW); - wc.hIcon = LoadIcon(0, IDI_APPLICATION); - wc.hInstance = GetModuleHandle(0); - wc.lpfnWndProc = Viewport_windowProc; - wc.lpszClassName = L"hiroViewport"; + wc.lpfnWndProc = Default_windowProc; + wc.lpszClassName = L"hiroWidget"; wc.lpszMenuName = 0; wc.style = CS_HREDRAW | CS_VREDRAW; RegisterClass(&wc); @@ -137,6 +123,11 @@ auto pApplication::initialize() -> void { pWindow::initialize(); } +auto pApplication::state() -> State& { + static State state; + return state; +} + static auto Application_keyboardProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) -> bool { if(msg != WM_KEYDOWN && msg != WM_SYSKEYDOWN && msg != WM_KEYUP && msg != WM_SYSKEYUP) return false; diff --git a/hiro/windows/application.hpp b/hiro/windows/application.hpp index 97b29d7f..09f40855 100644 --- a/hiro/windows/application.hpp +++ b/hiro/windows/application.hpp @@ -10,6 +10,11 @@ struct pApplication { static auto setScreenSaver(bool screenSaver) -> void; static auto initialize() -> void; + + struct State { + pToolTip* toolTip = nullptr; //active toolTip + }; + static auto state() -> State&; }; } diff --git a/hiro/windows/header.hpp b/hiro/windows/header.hpp index a555e271..30cb3a64 100644 --- a/hiro/windows/header.hpp +++ b/hiro/windows/header.hpp @@ -80,3 +80,11 @@ #if !defined(TBS_TRANSPARENTBKGND) #define TBS_TRANSPARENTBKGND 0x1000 #endif + +#if !defined(TTP_STANDARD) + #define TTP_STANDARD 1 +#endif + +#if !defined(TTSS_NORMAL) + #define TTSS_NORMAL 1 +#endif diff --git a/hiro/windows/platform.cpp b/hiro/windows/platform.cpp index 466fa247..2172ac95 100644 --- a/hiro/windows/platform.cpp +++ b/hiro/windows/platform.cpp @@ -27,6 +27,8 @@ #include "sizable.cpp" +#include "tool-tip.cpp" + #include "widget/widget.cpp" #include "widget/button.cpp" #include "widget/canvas.cpp" diff --git a/hiro/windows/platform.hpp b/hiro/windows/platform.hpp index 6db6f485..37cb4f89 100644 --- a/hiro/windows/platform.hpp +++ b/hiro/windows/platform.hpp @@ -49,6 +49,8 @@ static vector windows; #include "sizable.hpp" +#include "tool-tip.hpp" + #include "widget/widget.hpp" #include "widget/button.hpp" #include "widget/canvas.hpp" diff --git a/hiro/windows/sizable.cpp b/hiro/windows/sizable.cpp index 4e793ab3..2274266d 100644 --- a/hiro/windows/sizable.cpp +++ b/hiro/windows/sizable.cpp @@ -13,6 +13,7 @@ auto pSizable::minimumSize() const -> Size { } auto pSizable::setGeometry(Geometry geometry) -> void { + self().doSize(); } } diff --git a/hiro/windows/tool-tip.cpp b/hiro/windows/tool-tip.cpp new file mode 100644 index 00000000..52f3e4cb --- /dev/null +++ b/hiro/windows/tool-tip.cpp @@ -0,0 +1,161 @@ +namespace hiro { + +static auto CALLBACK ToolTip_windowProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) -> LRESULT { + if(auto toolTip = (pToolTip*)GetWindowLongPtr(hwnd, GWLP_USERDATA)) { + if(auto result = toolTip->windowProc(hwnd, msg, wparam, lparam)) { + return result(); + } + } + return DefWindowProc(hwnd, msg, wparam, lparam); +} + +pToolTip::pToolTip(const string& toolTipText) { + text = toolTipText; + + htheme = OpenThemeData(hwnd, L"TOOLTIP"); + hwnd = CreateWindowEx( + WS_EX_TOOLWINDOW | WS_EX_TOPMOST | (htheme ? WS_EX_LAYERED : 0), L"hiroToolTip", L"", + WS_POPUP, 0, 0, 0, 0, + 0, 0, GetModuleHandle(0), 0 + ); + SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG_PTR)this); + tracking.x = -1; + tracking.y = -1; + + timeout.setInterval(5000); + timeout.onActivate([&] { hide(); }); +} + +pToolTip::~pToolTip() { + hide(); + if(htheme) { CloseThemeData(htheme); htheme = nullptr; } + if(hwnd) { DestroyWindow(hwnd); hwnd = nullptr; } +} + +auto pToolTip::drawLayered() -> void { + auto hdcOutput = GetDC(nullptr); + + uint32_t* below = nullptr; + auto hdcBelow = CreateCompatibleDC(hdcOutput); + auto hbmBelow = CreateBitmap(hdcBelow, size.cx, size.cy, below); + SelectObject(hdcBelow, hbmBelow); + RECT rc{}; + rc.left = 0, rc.top = 0, rc.right = size.cx, rc.bottom = size.cy; + DrawThemeBackground(htheme, hdcBelow, TTP_STANDARD, TTSS_NORMAL, &rc, nullptr); + + uint32_t* above = nullptr; + auto hdcAbove = CreateCompatibleDC(hdcOutput); + auto hbmAbove = CreateBitmap(hdcAbove, size.cx, size.cy, above); + SelectObject(hdcAbove, hbmAbove); + + memory::copy(above, below, size.cx * size.cy); + + auto hfont = pFont::create(Font()); + SelectObject(hdcAbove, hfont); + SetBkMode(hdcAbove, TRANSPARENT); + SetTextColor(hdcAbove, RGB(0, 0, 0)); + utf16_t drawText(text); + rc.left += 6, rc.top += 6, rc.right -= 6, rc.bottom -= 6; + DrawText(hdcAbove, drawText, -1, &rc, DT_LEFT | DT_TOP); + DeleteObject(hfont); + + for(uint n : range(size.cx * size.cy)) { + below[n] = (below[n] & 0xff000000) | (above[n] & 0x00ffffff); + } + + BLENDFUNCTION blend{}; + blend.BlendOp = AC_SRC_OVER; + blend.SourceConstantAlpha = 255; + blend.AlphaFormat = AC_SRC_ALPHA; + POINT zero{}; + zero.x = 0, zero.y = 0; + UpdateLayeredWindow(hwnd, hdcOutput, &position, &size, hdcBelow, &zero, RGB(0, 0, 0), &blend, ULW_ALPHA); + + DeleteObject(hbmAbove); + DeleteObject(hbmBelow); + DeleteDC(hdcAbove); + DeleteDC(hdcBelow); + ReleaseDC(nullptr, hdcOutput); +} + +auto pToolTip::drawOpaque() -> void { + PAINTSTRUCT ps; + BeginPaint(hwnd, &ps); + RECT rc{}; + GetClientRect(hwnd, &rc); + auto brush = CreateSolidBrush(RGB(0, 0, 0)); + FillRect(ps.hdc, &rc, brush); + DeleteObject(brush); + rc.left += 1, rc.top += 1, rc.right -= 1, rc.bottom -= 1; + brush = CreateSolidBrush(RGB(255, 255, 225)); + FillRect(ps.hdc, &rc, brush); + DeleteObject(brush); + rc.left += 5, rc.top += 5, rc.right -= 5, rc.bottom -= 5; + SetBkMode(ps.hdc, TRANSPARENT); + auto font = pFont::create(Font()); + SelectObject(ps.hdc, font); + SetTextColor(ps.hdc, RGB(0, 0, 0)); + DrawText(ps.hdc, utf16_t(text), -1, &rc, DT_LEFT | DT_TOP); + DeleteObject(font); + EndPaint(hwnd, &ps); +} + +auto pToolTip::show() -> void { + if(auto toolTip = pApplication::state().toolTip) { + if(toolTip != this) toolTip->hide(); + } + pApplication::state().toolTip = this; + + GetCursorPos(&position); + if(position.x == tracking.x && position.y == tracking.y) return; + tracking.x = position.x, tracking.y = position.y; + + position.y += 18; + auto textSize = pFont::size(Font(), text ? text : " "); + size.cx = 12 + textSize.width(); + size.cy = 12 + textSize.height(); + + //try to keep the tool-tip onscreen + auto desktop = pDesktop::size(); + if(position.x + size.cx >= desktop.width ()) position.x = desktop.width () - size.cx; + if(position.y + size.cy >= desktop.height()) position.y = desktop.height() - size.cy; + if(position.x < 0) position.x = 0; + if(position.y < 0) position.y = 0; + + SetWindowPos(hwnd, HWND_TOP, position.x, position.y, size.cx, size.cy, SWP_NOACTIVATE | SWP_SHOWWINDOW); + if(htheme) drawLayered(); + + timeout.setEnabled(true); +} + +auto pToolTip::hide() -> void { + pApplication::state().toolTip = nullptr; + timeout.setEnabled(false); + ShowWindow(hwnd, SW_HIDE); + GetCursorPos(&tracking); +} + +auto pToolTip::windowProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) -> maybe { + switch(msg) { + case WM_ERASEBKGND: + case WM_PAINT: + if(htheme) break; + drawOpaque(); + return msg == WM_ERASEBKGND; + case WM_MOUSEMOVE: + case WM_MOUSELEAVE: { + POINT point{}; + GetCursorPos(&point); + if(point.x != tracking.x || point.y != tracking.y) hide(); + } break; + case WM_LBUTTONDOWN: case WM_LBUTTONUP: + case WM_MBUTTONDOWN: case WM_MBUTTONUP: + case WM_RBUTTONDOWN: case WM_RBUTTONUP: + hide(); + break; + } + + return {}; +} + +} diff --git a/hiro/windows/tool-tip.hpp b/hiro/windows/tool-tip.hpp new file mode 100644 index 00000000..301bae97 --- /dev/null +++ b/hiro/windows/tool-tip.hpp @@ -0,0 +1,22 @@ +namespace hiro { + +struct pToolTip { + pToolTip(const string& text); + ~pToolTip(); + + auto drawLayered() -> void; + auto drawOpaque() -> void; + auto show() -> void; + auto hide() -> void; + auto windowProc(HWND, UINT, WPARAM, LPARAM) -> maybe; + + HWND hwnd = nullptr; + HTHEME htheme = nullptr; + POINT position{}; + SIZE size{}; + POINT tracking{}; + string text; + Timer timeout; +}; + +} diff --git a/hiro/windows/utility.cpp b/hiro/windows/utility.cpp index 4583ad3b..2b6c2f07 100644 --- a/hiro/windows/utility.cpp +++ b/hiro/windows/utility.cpp @@ -7,31 +7,37 @@ static const uint Windows7 = 0x0601; static auto Button_CustomDraw(HWND, PAINTSTRUCT&, bool, bool, bool, unsigned, const Font&, const image&, Orientation, const string&) -> void; -static auto OsVersion() -> unsigned { +static auto OsVersion() -> uint { OSVERSIONINFO versionInfo{0}; versionInfo.dwOSVersionInfoSize = sizeof(OSVERSIONINFO); GetVersionEx(&versionInfo); return (versionInfo.dwMajorVersion << 8) + (versionInfo.dwMajorVersion << 0); } +static auto CreateBitmap(HDC hdc, uint width, uint height, uint32_t*& data) -> HBITMAP { + BITMAPINFO info{}; + info.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); + info.bmiHeader.biWidth = width; + info.bmiHeader.biHeight = -(int)height; //bitmaps are stored upside down unless we negate height + info.bmiHeader.biPlanes = 1; + info.bmiHeader.biBitCount = 32; + info.bmiHeader.biCompression = BI_RGB; + info.bmiHeader.biSizeImage = width * height * sizeof(uint32_t); + void* bits = nullptr; + auto bitmap = CreateDIBSection(hdc, &info, DIB_RGB_COLORS, &bits, nullptr, 0); + data = (uint32_t*)bits; + return bitmap; +} + static auto CreateBitmap(image icon) -> HBITMAP { icon.alphaMultiply(); //Windows AlphaBlend() requires premultiplied image data icon.transform(); - HDC hdc = GetDC(0); - BITMAPINFO bitmapInfo; - memset(&bitmapInfo, 0, sizeof(BITMAPINFO)); - bitmapInfo.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); - bitmapInfo.bmiHeader.biWidth = icon.width(); - bitmapInfo.bmiHeader.biHeight = -(signed)icon.height(); //bitmaps are stored upside down unless we negate height - bitmapInfo.bmiHeader.biPlanes = 1; - bitmapInfo.bmiHeader.biBitCount = 32; - bitmapInfo.bmiHeader.biCompression = BI_RGB; - bitmapInfo.bmiHeader.biSizeImage = icon.size(); - void* bits = nullptr; - HBITMAP hbitmap = CreateDIBSection(hdc, &bitmapInfo, DIB_RGB_COLORS, &bits, NULL, 0); - if(bits) memory::copy(bits, icon.data(), icon.size()); - ReleaseDC(0, hdc); - return hbitmap; + uint32_t* data = nullptr; + auto hdc = GetDC(nullptr); + auto bitmap = CreateBitmap(hdc, icon.width(), icon.height(), data); + memory::copy(data, icon.data(), icon.size()); + ReleaseDC(nullptr, hdc); + return bitmap; } static auto CreateRGB(const Color& color) -> COLORREF { @@ -60,9 +66,21 @@ static auto DropPaths(WPARAM wparam) -> vector { return paths; } +static auto WINAPI EnumVisibleChildWindowsProc(HWND hwnd, LPARAM lparam) -> BOOL { + auto children = (vector*)lparam; + if(IsWindowVisible(hwnd)) children->append(hwnd); + return true; +} + +static auto EnumVisibleChildWindows(HWND hwnd) -> vector { + vector children; + EnumChildWindows(hwnd, EnumVisibleChildWindowsProc, (LPARAM)&children); + return children; +} + static auto GetWindowZOrder(HWND hwnd) -> unsigned { - unsigned z = 0; - for(HWND next = hwnd; next != NULL; next = GetWindow(next, GW_HWNDPREV)) z++; + uint z = 0; + for(HWND next = hwnd; next != nullptr; next = GetWindow(next, GW_HWNDPREV)) z++; return z; } @@ -111,6 +129,10 @@ static auto ScrollEvent(HWND hwnd, WPARAM wparam) -> unsigned { return info.nPos; } +static auto CALLBACK Default_windowProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) -> LRESULT { + return DefWindowProc(hwnd, msg, wparam, lparam); +} + //separate because PopupMenu HWND does not contain GWLP_USERDATA pointing at Window needed for Shared_windowProc static auto CALLBACK Menu_windowProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) -> LRESULT { switch(msg) { @@ -383,6 +405,47 @@ static auto CALLBACK Shared_windowProc(WindowProc windowProc, HWND hwnd, UINT ms break; } + //catch mouse events over disabled windows + case WM_MOUSEMOVE: + case WM_MOUSELEAVE: + case WM_MOUSEHOVER: { + POINT p{}; + GetCursorPos(&p); + ScreenToClient(hwnd, &p); + for(auto window : EnumVisibleChildWindows(hwnd)) { + if(auto widget = (mWidget*)GetWindowLongPtr(window, GWLP_USERDATA)) { + auto geometry = widget->geometry(); + if(p.x < geometry.x()) continue; + if(p.y < geometry.y()) continue; + if(p.x >= geometry.x() + geometry.width ()) continue; + if(p.y >= geometry.y() + geometry.height()) continue; + + if(msg == WM_MOUSEMOVE) { + TRACKMOUSEEVENT event{sizeof(TRACKMOUSEEVENT)}; + event.hwndTrack = hwnd; + event.dwFlags = TME_LEAVE | TME_HOVER; + event.dwHoverTime = 1500; + TrackMouseEvent(&event); + POINT p{}; + GetCursorPos(&p); + widget->self()->doMouseMove(p.x, p.y); + if(auto toolTip = pApplication::state().toolTip) { + toolTip->windowProc(hwnd, msg, wparam, lparam); + } + } + + if(msg == WM_MOUSELEAVE) { + widget->self()->doMouseLeave(); + } + + if(msg == WM_MOUSEHOVER) { + widget->self()->doMouseHover(); + } + } + } + break; + } + #if defined(Hiro_TableView) case AppMessage::TableView_doPaint: { if(auto tableView = (mTableView*)lparam) { @@ -402,7 +465,7 @@ static auto CALLBACK Shared_windowProc(WindowProc windowProc, HWND hwnd, UINT ms #endif } - return windowProc(hwnd, msg, wparam, lparam); + return CallWindowProc(windowProc, hwnd, msg, wparam, lparam); } } diff --git a/hiro/windows/widget/button.cpp b/hiro/windows/widget/button.cpp index 251cf184..c9caa139 100644 --- a/hiro/windows/widget/button.cpp +++ b/hiro/windows/widget/button.cpp @@ -2,49 +2,12 @@ namespace hiro { -static auto Button_paintProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam, - bool bordered, bool checked, bool enabled, const Font& font, const image& icon, Orientation orientation, const string& text -) -> LRESULT { - if(msg == WM_PAINT) { - PAINTSTRUCT ps; - BeginPaint(hwnd, &ps); - auto state = Button_GetState(hwnd); - Button_CustomDraw(hwnd, ps, bordered, checked, enabled, state, font, icon, orientation, text); - EndPaint(hwnd, &ps); - } - return DefWindowProc(hwnd, msg, wparam, lparam); -} - -//BUTTON cannot draw borderless buttons on its own -//BS_OWNERDRAW will send WM_DRAWITEM; but will disable hot-tracking notifications -//to gain hot-tracking plus borderless buttons; BUTTON is superclassed and WM_PAINT is hijacked -//note: letting hiro paint bordered buttons will lose the fade animations on Vista+; -//however, it will allow placing icons immediately next to text (original forces icon left alignment) -static auto CALLBACK Button_windowProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) -> LRESULT { - if(auto object = (mObject*)GetWindowLongPtr(hwnd, GWLP_USERDATA)) { - if(auto button = dynamic_cast(object)) { - if(auto self = button->self()) { - if(msg == WM_ERASEBKGND) return DefWindowProc(hwnd, msg, wparam, lparam); - if(msg == WM_PAINT) return Button_paintProc(hwnd, msg, wparam, lparam, - button->state.bordered, false, button->enabled(true), button->font(true), - button->state.icon, button->state.orientation, button->state.text - ); - return self->windowProc(hwnd, msg, wparam, lparam); - } - } - } - return DefWindowProc(hwnd, msg, wparam, lparam); -} - auto pButton::construct() -> void { hwnd = CreateWindow( L"BUTTON", L"", WS_CHILD | WS_TABSTOP, 0, 0, 0, 0, _parentHandle(), nullptr, GetModuleHandle(0), 0 ); - SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG_PTR)&reference); - windowProc = (WindowProc)GetWindowLongPtr(hwnd, GWLP_WNDPROC); - SetWindowLongPtr(hwnd, GWLP_WNDPROC, (LONG_PTR)Button_windowProc); - pWidget::_setState(); + pWidget::construct(); _setState(); } @@ -99,10 +62,35 @@ auto pButton::setVisible(bool visible) -> void { _setState(); } +// + auto pButton::onActivate() -> void { self().doActivate(); } +//BUTTON cannot draw borderless buttons on its own +//BS_OWNERDRAW will send WM_DRAWITEM; but will disable hot-tracking notifications +//to gain hot-tracking plus borderless buttons; BUTTON is superclassed and WM_PAINT is hijacked +//note: letting hiro paint bordered buttons will lose the fade animations on Vista+; +//however, it will allow placing icons immediately next to text (original forces icon left alignment) +auto pButton::windowProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) -> maybe { + if(msg == WM_PAINT) { + PAINTSTRUCT ps; + BeginPaint(hwnd, &ps); + auto buttonState = Button_GetState(hwnd); + Button_CustomDraw(hwnd, ps, + state().bordered, false, self().enabled(true), buttonState, + self().font(true), state().icon, state().orientation, state().text + ); + EndPaint(hwnd, &ps); + return false; + } + + return pWidget::windowProc(hwnd, msg, wparam, lparam); +} + +// + auto pButton::_setState() -> void { InvalidateRect(hwnd, 0, false); } diff --git a/hiro/windows/widget/button.hpp b/hiro/windows/widget/button.hpp index 44bd313b..3b5b3f80 100644 --- a/hiro/windows/widget/button.hpp +++ b/hiro/windows/widget/button.hpp @@ -15,10 +15,9 @@ struct pButton : pWidget { auto setVisible(bool visible) -> void override; auto onActivate() -> void; + auto windowProc(HWND, UINT, WPARAM, LPARAM) -> maybe override; auto _setState() -> void; - - WindowProc windowProc = nullptr; }; } diff --git a/hiro/windows/widget/canvas.cpp b/hiro/windows/widget/canvas.cpp index 3b6ab8ef..f6c84e45 100644 --- a/hiro/windows/widget/canvas.cpp +++ b/hiro/windows/widget/canvas.cpp @@ -2,64 +2,9 @@ namespace hiro { -static auto CALLBACK Canvas_windowProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) -> LRESULT { - auto object = (mObject*)GetWindowLongPtr(hwnd, GWLP_USERDATA); - if(!object) return DefWindowProc(hwnd, msg, wparam, lparam); - auto canvas = dynamic_cast(object); - if(!canvas) return DefWindowProc(hwnd, msg, wparam, lparam); - - if(msg == WM_DROPFILES) { - if(auto paths = DropPaths(wparam)) canvas->doDrop(paths); - return false; - } - - if(msg == WM_GETDLGCODE) { - return DLGC_STATIC | DLGC_WANTCHARS; - } - - if(msg == WM_ERASEBKGND) { - if(auto self = canvas->self()) self->_paint(); - return true; - } - - if(msg == WM_PAINT) { - if(auto self = canvas->self()) self->_paint(); - return false; - } - - if(msg == WM_MOUSEMOVE) { - TRACKMOUSEEVENT tracker{sizeof(TRACKMOUSEEVENT), TME_LEAVE, hwnd}; - TrackMouseEvent(&tracker); - canvas->doMouseMove({(int16_t)LOWORD(lparam), (int16_t)HIWORD(lparam)}); - } - - if(msg == WM_MOUSELEAVE) { - canvas->doMouseLeave(); - } - - if(msg == WM_LBUTTONDOWN || msg == WM_MBUTTONDOWN || msg == WM_RBUTTONDOWN) { - switch(msg) { - case WM_LBUTTONDOWN: canvas->doMousePress(Mouse::Button::Left); break; - case WM_MBUTTONDOWN: canvas->doMousePress(Mouse::Button::Middle); break; - case WM_RBUTTONDOWN: canvas->doMousePress(Mouse::Button::Right); break; - } - } - - if(msg == WM_LBUTTONUP || msg == WM_MBUTTONUP || msg == WM_RBUTTONUP) { - switch(msg) { - case WM_LBUTTONUP: canvas->doMouseRelease(Mouse::Button::Left); break; - case WM_MBUTTONUP: canvas->doMouseRelease(Mouse::Button::Middle); break; - case WM_RBUTTONUP: canvas->doMouseRelease(Mouse::Button::Right); break; - } - } - - return DefWindowProc(hwnd, msg, wparam, lparam); -} - auto pCanvas::construct() -> void { - hwnd = CreateWindow(L"hiroCanvas", L"", WS_CHILD, 0, 0, 0, 0, _parentHandle(), nullptr, GetModuleHandle(0), 0); - SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG_PTR)&reference); - pWidget::_setState(); + hwnd = CreateWindow(L"hiroWidget", L"", WS_CHILD, 0, 0, 0, 0, _parentHandle(), nullptr, GetModuleHandle(0), 0); + pWidget::construct(); setDroppable(state().droppable); update(); } @@ -99,31 +44,101 @@ auto pCanvas::update() -> void { _redraw(); } +// + +auto pCanvas::doMouseLeave() -> void { + return self().doMouseLeave(); +} + +auto pCanvas::doMouseMove(int x, int y) -> void { + return self().doMouseMove({x, y}); +} + +auto pCanvas::windowProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) -> maybe { + if(msg == WM_DROPFILES) { + if(auto paths = DropPaths(wparam)) self().doDrop(paths); + return false; + } + + if(msg == WM_GETDLGCODE) { + return DLGC_STATIC | DLGC_WANTCHARS; + } + + if(msg == WM_ERASEBKGND || msg == WM_PAINT) { + _paint(); + return msg == WM_ERASEBKGND; + } + + if(msg == WM_LBUTTONDOWN || msg == WM_MBUTTONDOWN || msg == WM_RBUTTONDOWN) { + switch(msg) { + case WM_LBUTTONDOWN: self().doMousePress(Mouse::Button::Left); break; + case WM_MBUTTONDOWN: self().doMousePress(Mouse::Button::Middle); break; + case WM_RBUTTONDOWN: self().doMousePress(Mouse::Button::Right); break; + } + } + + if(msg == WM_LBUTTONUP || msg == WM_MBUTTONUP || msg == WM_RBUTTONUP) { + switch(msg) { + case WM_LBUTTONUP: self().doMouseRelease(Mouse::Button::Left); break; + case WM_MBUTTONUP: self().doMouseRelease(Mouse::Button::Middle); break; + case WM_RBUTTONUP: self().doMouseRelease(Mouse::Button::Right); break; + } + } + + return pWidget::windowProc(hwnd, msg, wparam, lparam); +} + +// + auto pCanvas::_paint() -> void { PAINTSTRUCT ps; BeginPaint(hwnd, &ps); + int sx = 0, sy = 0, dx = 0, dy = 0; + int width = this->width; + int height = this->height; + auto geometry = self().geometry(); + + if(width <= geometry.width()) { + sx = 0; + dx = (geometry.width() - width) / 2; + } else { + sx = (width - geometry.width()) / 2; + dx = 0; + width = geometry.width(); + } + + if(height <= geometry.height()) { + sy = 0; + dy = (geometry.height() - height) / 2; + } else { + sy = (height - geometry.height()) / 2; + dy = 0; + height = geometry.height(); + } + HDC hdc = CreateCompatibleDC(ps.hdc); - BITMAPINFO bmi; - memset(&bmi, 0, sizeof(BITMAPINFO)); + BITMAPINFO bmi{}; bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); bmi.bmiHeader.biPlanes = 1; bmi.bmiHeader.biBitCount = 32; bmi.bmiHeader.biCompression = BI_RGB; bmi.bmiHeader.biWidth = width; bmi.bmiHeader.biHeight = -height; //GDI stores bitmaps upside now; negative height flips bitmap - bmi.bmiHeader.biSizeImage = pixels.size() * sizeof(uint32_t); + bmi.bmiHeader.biSizeImage = width * height * sizeof(uint32_t); void* bits = nullptr; HBITMAP bitmap = CreateDIBSection(hdc, &bmi, DIB_RGB_COLORS, &bits, nullptr, 0); if(bits) { - auto source = (const uint8_t*)pixels.data(); - auto target = (uint8_t*)bits; - for(auto n : range(width * height)) { - target[0] = (source[0] * source[3]) / 255; - target[1] = (source[1] * source[3]) / 255; - target[2] = (source[2] * source[3]) / 255; - target[3] = (source[3]); - source += 4, target += 4; + for(uint y : range(height)) { + auto source = (const uint8_t*)pixels.data() + (sy + y) * this->width * sizeof(uint32_t) + sx * sizeof(uint32_t); + auto target = (uint8_t*)bits + y * width * sizeof(uint32_t); + for(uint x : range(width)) { + target[0] = (source[0] * source[3]) / 255; + target[1] = (source[1] * source[3]) / 255; + target[2] = (source[2] * source[3]) / 255; + target[3] = (source[3]); + source += 4, target += 4; + } } } SelectObject(hdc, bitmap); @@ -133,7 +148,7 @@ auto pCanvas::_paint() -> void { DrawThemeParentBackground(hwnd, ps.hdc, &rc); BLENDFUNCTION bf{AC_SRC_OVER, 0, (BYTE)255, AC_SRC_ALPHA}; - AlphaBlend(ps.hdc, 0, 0, width, height, hdc, 0, 0, width, height, bf); + AlphaBlend(ps.hdc, dx, dy, width, height, hdc, 0, 0, width, height, bf); DeleteObject(bitmap); DeleteDC(hdc); diff --git a/hiro/windows/widget/canvas.hpp b/hiro/windows/widget/canvas.hpp index 1adcd9a7..1753ba7f 100644 --- a/hiro/windows/widget/canvas.hpp +++ b/hiro/windows/widget/canvas.hpp @@ -13,6 +13,10 @@ struct pCanvas : pWidget { auto setIcon(const image& icon) -> void; auto update() -> void; + auto doMouseLeave() -> void override; + auto doMouseMove(int x, int y) -> void override; + auto windowProc(HWND, UINT, WPARAM, LPARAM) -> maybe override; + auto _paint() -> void; auto _rasterize() -> void; auto _redraw() -> void; diff --git a/hiro/windows/widget/check-button.cpp b/hiro/windows/widget/check-button.cpp index ef5e93b5..3fd1dc1c 100644 --- a/hiro/windows/widget/check-button.cpp +++ b/hiro/windows/widget/check-button.cpp @@ -2,30 +2,11 @@ namespace hiro { -static auto CALLBACK CheckButton_windowProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) -> LRESULT { - if(auto object = (mObject*)GetWindowLongPtr(hwnd, GWLP_USERDATA)) { - if(auto button = dynamic_cast(object)) { - if(auto self = button->self()) { - if(msg == WM_ERASEBKGND) return DefWindowProc(hwnd, msg, wparam, lparam); - if(msg == WM_PAINT) return Button_paintProc(hwnd, msg, wparam, lparam, - button->state.bordered, button->state.checked, button->enabled(true), button->font(true), - button->state.icon, button->state.orientation, button->state.text - ); - return self->windowProc(hwnd, msg, wparam, lparam); - } - } - } - return DefWindowProc(hwnd, msg, wparam, lparam); -} - auto pCheckButton::construct() -> void { hwnd = CreateWindow(L"BUTTON", L"", WS_CHILD | WS_TABSTOP | BS_CHECKBOX | BS_PUSHLIKE, 0, 0, 0, 0, _parentHandle(), nullptr, GetModuleHandle(0), 0); - SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG_PTR)&reference); - windowProc = (WindowProc)GetWindowLongPtr(hwnd, GWLP_WNDPROC); - SetWindowLongPtr(hwnd, GWLP_WNDPROC, (LONG_PTR)CheckButton_windowProc); - pWidget::_setState(); + pWidget::construct(); _setState(); setChecked(state().checked); } @@ -85,12 +66,32 @@ auto pCheckButton::setVisible(bool visible) -> void { _setState(); } +// + auto pCheckButton::onToggle() -> void { state().checked = !state().checked; setChecked(state().checked); self().doToggle(); } +auto pCheckButton::windowProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) -> maybe { + if(msg == WM_PAINT) { + PAINTSTRUCT ps; + BeginPaint(hwnd, &ps); + auto buttonState = Button_GetState(hwnd); + Button_CustomDraw(hwnd, ps, + state().bordered, state().checked, self().enabled(true), buttonState, + self().font(true), state().icon, state().orientation, state().text + ); + EndPaint(hwnd, &ps); + return false; + } + + return pWidget::windowProc(hwnd, msg, wparam, lparam); +} + +// + auto pCheckButton::_setState() -> void { InvalidateRect(hwnd, 0, false); } diff --git a/hiro/windows/widget/check-button.hpp b/hiro/windows/widget/check-button.hpp index 1de6eed5..44927e5d 100644 --- a/hiro/windows/widget/check-button.hpp +++ b/hiro/windows/widget/check-button.hpp @@ -16,10 +16,9 @@ struct pCheckButton : pWidget { auto setVisible(bool visible) -> void override; auto onToggle() -> void; + auto windowProc(HWND, UINT, WPARAM, LPARAM) -> maybe override; auto _setState() -> void; - - WindowProc windowProc = nullptr; }; } diff --git a/hiro/windows/widget/check-label.cpp b/hiro/windows/widget/check-label.cpp index 7aa12508..d4c04527 100644 --- a/hiro/windows/widget/check-label.cpp +++ b/hiro/windows/widget/check-label.cpp @@ -8,8 +8,7 @@ auto pCheckLabel::construct() -> void { WS_CHILD | WS_TABSTOP | BS_CHECKBOX, 0, 0, 0, 0, _parentHandle(), nullptr, GetModuleHandle(0), 0 ); - SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG_PTR)&reference); - pWidget::_setState(); + pWidget::construct(); setChecked(state().checked); setText(state().text); } diff --git a/hiro/windows/widget/combo-button.cpp b/hiro/windows/widget/combo-button.cpp index 65cefb69..728a1a25 100644 --- a/hiro/windows/widget/combo-button.cpp +++ b/hiro/windows/widget/combo-button.cpp @@ -9,8 +9,7 @@ auto pComboButton::construct() -> void { 0, 0, 0, 0, _parentHandle(), nullptr, GetModuleHandle(0), 0 ); - SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG_PTR)&reference); - pWidget::_setState(); + pWidget::construct(); for(auto& item : state().items) append(item); } diff --git a/hiro/windows/widget/frame.cpp b/hiro/windows/widget/frame.cpp index 7375bd18..14eb17e4 100644 --- a/hiro/windows/widget/frame.cpp +++ b/hiro/windows/widget/frame.cpp @@ -6,8 +6,7 @@ auto pFrame::construct() -> void { hwnd = CreateWindow(L"BUTTON", L"", WS_CHILD | BS_GROUPBOX, 0, 0, 0, 0, _parentHandle(), nullptr, GetModuleHandle(0), 0); - SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG_PTR)&reference); - pWidget::_setState(); + pWidget::construct(); setText(state().text); } diff --git a/hiro/windows/widget/hex-edit.cpp b/hiro/windows/widget/hex-edit.cpp index 7ef58eca..60d4fc88 100644 --- a/hiro/windows/widget/hex-edit.cpp +++ b/hiro/windows/widget/hex-edit.cpp @@ -2,77 +2,20 @@ namespace hiro { -static auto CALLBACK HexEdit_windowProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) -> LRESULT { - auto object = (mObject*)GetWindowLongPtr(hwnd, GWLP_USERDATA); - if(!object) return DefWindowProc(hwnd, msg, wparam, lparam); - auto hexEdit = dynamic_cast(object); - if(!hexEdit) return DefWindowProc(hwnd, msg, wparam, lparam); - auto self = hexEdit->self(); - if(!self) return DefWindowProc(hwnd, msg, wparam, lparam); - - switch(msg) { - case WM_KEYDOWN: - if(self->keyPress(wparam)) return 0; - break; - - case WM_MOUSEWHEEL: { - signed offset = -((int16_t)HIWORD(wparam) / WHEEL_DELTA); - self->scrollTo(self->scrollPosition() + offset); - return true; - } - - case WM_SIZE: { - RECT rc; - GetClientRect(self->hwnd, &rc); - SetWindowPos(self->scrollBar, HWND_TOP, rc.right - 18, 0, 18, rc.bottom, SWP_SHOWWINDOW); - break; - } - - case WM_VSCROLL: { - SCROLLINFO info{sizeof(SCROLLINFO)}; - info.fMask = SIF_ALL; - GetScrollInfo((HWND)lparam, SB_CTL, &info); - switch(LOWORD(wparam)) { - case SB_LEFT: info.nPos = info.nMin; break; - case SB_RIGHT: info.nPos = info.nMax; break; - case SB_LINELEFT: info.nPos--; break; - case SB_LINERIGHT: info.nPos++; break; - case SB_PAGELEFT: info.nPos -= info.nMax >> 3; break; - case SB_PAGERIGHT: info.nPos += info.nMax >> 3; break; - case SB_THUMBTRACK: info.nPos = info.nTrackPos; break; - } - - info.fMask = SIF_POS; - SetScrollInfo((HWND)lparam, SB_CTL, &info, TRUE); - GetScrollInfo((HWND)lparam, SB_CTL, &info); //get clamped position - - self->scrollTo(info.nPos); - return true; - } - } - - return self->windowProc(hwnd, msg, wparam, lparam); -} - auto pHexEdit::construct() -> void { hwnd = CreateWindowEx( WS_EX_CLIENTEDGE, L"EDIT", L"", WS_CHILD | WS_TABSTOP | ES_AUTOHSCROLL | ES_READONLY | ES_MULTILINE | ES_WANTRETURN, 0, 0, 0, 0, _parentHandle(), nullptr, GetModuleHandle(0), 0 ); - SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG_PTR)&reference); - scrollBar = CreateWindowEx( 0, L"SCROLLBAR", L"", WS_VISIBLE | WS_CHILD | SBS_VERT, 0, 0, 0, 0, hwnd, nullptr, GetModuleHandle(0), 0 ); SetWindowLongPtr(scrollBar, GWLP_USERDATA, (LONG_PTR)&reference); + pWidget::construct(); - windowProc = (WindowProc)GetWindowLongPtr(hwnd, GWLP_WNDPROC); - SetWindowLongPtr(hwnd, GWLP_WNDPROC, (LONG_PTR)HexEdit_windowProc); - - pWidget::_setState(); setAddress(state().address); setBackgroundColor(state().backgroundColor); setLength(state().length); @@ -149,7 +92,7 @@ auto pHexEdit::update() -> void { Edit_SetSel(hwnd, LOWORD(cursorPosition), HIWORD(cursorPosition)); } -bool pHexEdit::keyPress(unsigned scancode) { +auto pHexEdit::keyPress(unsigned scancode) -> bool { if(!state().onRead) return false; signed position = LOWORD(Edit_GetSel(hwnd)); @@ -234,25 +177,67 @@ bool pHexEdit::keyPress(unsigned scancode) { return true; } -signed pHexEdit::rows() { +auto pHexEdit::rows() -> int { return (max(1u, state().length) + state().columns - 1) / state().columns; } -signed pHexEdit::rowsScrollable() { +auto pHexEdit::rowsScrollable() -> int { return max(0u, rows() - state().rows); } -signed pHexEdit::scrollPosition() { +auto pHexEdit::scrollPosition() -> int { return state().address / state().columns; } -void pHexEdit::scrollTo(signed position) { +auto pHexEdit::scrollTo(signed position) -> void { if(position > rowsScrollable()) position = rowsScrollable(); if(position < 0) position = 0; if(position == scrollPosition()) return; self().setAddress(position * state().columns); } +auto pHexEdit::windowProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) -> maybe { + if(msg == WM_KEYDOWN) { + if(keyPress(wparam)) return 0; + } + + if(msg == WM_MOUSEWHEEL) { + int offset = -((int16_t)HIWORD(wparam) / WHEEL_DELTA); + scrollTo(scrollPosition() + offset); + return true; + } + + if(msg == WM_SIZE) { + RECT rc; + GetClientRect(hwnd, &rc); + SetWindowPos(scrollBar, HWND_TOP, rc.right - 18, 0, 18, rc.bottom, SWP_SHOWWINDOW); + } + + if(msg == WM_VSCROLL) { + SCROLLINFO info{sizeof(SCROLLINFO)}; + info.fMask = SIF_ALL; + GetScrollInfo((HWND)lparam, SB_CTL, &info); + switch(LOWORD(wparam)) { + case SB_LEFT: info.nPos = info.nMin; break; + case SB_RIGHT: info.nPos = info.nMax; break; + case SB_LINELEFT: info.nPos--; break; + case SB_LINERIGHT: info.nPos++; break; + case SB_PAGELEFT: info.nPos -= info.nMax >> 3; break; + case SB_PAGERIGHT: info.nPos += info.nMax >> 3; break; + case SB_THUMBTRACK: info.nPos = info.nTrackPos; break; + } + + info.fMask = SIF_POS; + SetScrollInfo((HWND)lparam, SB_CTL, &info, TRUE); + GetScrollInfo((HWND)lparam, SB_CTL, &info); //get clamped position + + scrollTo(info.nPos); + return true; + } + + return pWidget::windowProc(hwnd, msg, wparam, lparam); +} + } #endif diff --git a/hiro/windows/widget/hex-edit.hpp b/hiro/windows/widget/hex-edit.hpp index ce2240a5..e6ac15aa 100644 --- a/hiro/windows/widget/hex-edit.hpp +++ b/hiro/windows/widget/hex-edit.hpp @@ -18,8 +18,8 @@ struct pHexEdit : pWidget { auto rowsScrollable() -> signed; auto scrollPosition() -> signed; auto scrollTo(signed position) -> void; + auto windowProc(HWND, UINT, WPARAM, LPARAM) -> maybe override; - WindowProc windowProc = nullptr; HWND scrollBar = nullptr; HBRUSH backgroundBrush = nullptr; }; diff --git a/hiro/windows/widget/horizontal-scroll-bar.cpp b/hiro/windows/widget/horizontal-scroll-bar.cpp index ab347542..b4200b81 100644 --- a/hiro/windows/widget/horizontal-scroll-bar.cpp +++ b/hiro/windows/widget/horizontal-scroll-bar.cpp @@ -7,8 +7,7 @@ auto pHorizontalScrollBar::construct() -> void { L"SCROLLBAR", L"", WS_CHILD | WS_TABSTOP | SBS_HORZ, 0, 0, 0, 0, _parentHandle(), nullptr, GetModuleHandle(0), 0 ); - SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG_PTR)&reference); - pWidget::_setState(); + pWidget::construct(); setLength(state().length); setPosition(state().position); } diff --git a/hiro/windows/widget/horizontal-slider.cpp b/hiro/windows/widget/horizontal-slider.cpp index 605104ef..5c578a43 100644 --- a/hiro/windows/widget/horizontal-slider.cpp +++ b/hiro/windows/widget/horizontal-slider.cpp @@ -7,8 +7,7 @@ auto pHorizontalSlider::construct() -> void { TRACKBAR_CLASS, L"", WS_CHILD | WS_TABSTOP | TBS_TRANSPARENTBKGND | TBS_NOTICKS | TBS_BOTH | TBS_HORZ, 0, 0, 0, 0, _parentHandle(), nullptr, GetModuleHandle(0), 0 ); - SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG_PTR)&reference); - pWidget::_setState(); + pWidget::construct(); setLength(state().length); setPosition(state().position); } diff --git a/hiro/windows/widget/label.cpp b/hiro/windows/widget/label.cpp index d0517a64..df3e2e6e 100644 --- a/hiro/windows/widget/label.cpp +++ b/hiro/windows/widget/label.cpp @@ -3,11 +3,10 @@ namespace hiro { auto pLabel::construct() -> void { - hwnd = CreateWindow(L"hiroLabel", L"", + hwnd = CreateWindow(L"hiroWidget", L"", WS_CHILD | WS_CLIPSIBLINGS, 0, 0, 0, 0, _parentHandle(), nullptr, GetModuleHandle(0), 0); - SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG_PTR)&reference); - pWidget::_setState(); + pWidget::construct(); setText(state().text); } @@ -33,20 +32,15 @@ auto pLabel::setForegroundColor(Color color) -> void { } auto pLabel::setText(const string& text) -> void { - SetWindowText(hwnd, utf16_t(text)); InvalidateRect(hwnd, 0, false); } -static auto CALLBACK Label_windowProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) -> LRESULT { - auto label = (mLabel*)GetWindowLongPtr(hwnd, GWLP_USERDATA); - if(!label) return DefWindowProc(hwnd, msg, wparam, lparam); - auto window = label->parentWindow(true); - if(!window) return DefWindowProc(hwnd, msg, wparam, lparam); +auto pLabel::windowProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) -> maybe { + if(msg == WM_GETDLGCODE) { + return DLGC_STATIC | DLGC_WANTCHARS; + } - switch(msg) { - case WM_GETDLGCODE: return DLGC_STATIC | DLGC_WANTCHARS; - case WM_ERASEBKGND: - case WM_PAINT: { + if(msg == WM_ERASEBKGND || msg == WM_PAINT) { PAINTSTRUCT ps; BeginPaint(hwnd, &ps); RECT rc; @@ -56,24 +50,25 @@ static auto CALLBACK Label_windowProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM auto hbmMemory = CreateCompatibleBitmap(ps.hdc, rc.right - rc.left, rc.bottom - rc.top); SelectObject(hdcMemory, hbmMemory); - uint length = GetWindowTextLength(hwnd); - wchar_t text[length + 1]; - GetWindowText(hwnd, text, length + 1); - text[length] = 0; - - //todo: use DrawThemeParentBackground if Label is inside TabFrame - if(auto color = label->backgroundColor()) { + if(auto color = state().backgroundColor) { auto brush = CreateSolidBrush(CreateRGB(color)); FillRect(hdcMemory, &rc, brush); DeleteObject(brush); - } else if(auto brush = window->self()->hbrush) { - FillRect(hdcMemory, &rc, brush); - } else { + } else if(self().parentTabFrame(true)) { DrawThemeParentBackground(hwnd, hdcMemory, &rc); + } else if(auto window = self().parentWindow(true)) { + if(auto color = window->backgroundColor()) { + auto brush = CreateSolidBrush(CreateRGB(color)); + FillRect(hdcMemory, &rc, brush); + DeleteObject(brush); + } else { + DrawThemeParentBackground(hwnd, hdcMemory, &rc); + } } + utf16_t text(state().text); SetBkMode(hdcMemory, TRANSPARENT); - SelectObject(hdcMemory, label->self()->hfont); + SelectObject(hdcMemory, hfont); DrawText(hdcMemory, text, -1, &rc, DT_CALCRECT | DT_END_ELLIPSIS); uint height = rc.bottom; @@ -81,12 +76,12 @@ static auto CALLBACK Label_windowProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM rc.top = (rc.bottom - height) / 2; rc.bottom = rc.top + height; uint horizontalAlignment = DT_CENTER; - if(label->alignment().horizontal() < 0.333) horizontalAlignment = DT_LEFT; - if(label->alignment().horizontal() > 0.666) horizontalAlignment = DT_RIGHT; + if(state().alignment.horizontal() < 0.333) horizontalAlignment = DT_LEFT; + if(state().alignment.horizontal() > 0.666) horizontalAlignment = DT_RIGHT; uint verticalAlignment = DT_VCENTER; - if(label->alignment().vertical() < 0.333) verticalAlignment = DT_TOP; - if(label->alignment().vertical() > 0.666) verticalAlignment = DT_BOTTOM; - if(auto color = label->foregroundColor()) { + if(state().alignment.vertical() < 0.333) verticalAlignment = DT_TOP; + if(state().alignment.vertical() > 0.666) verticalAlignment = DT_BOTTOM; + if(auto color = state().foregroundColor) { SetTextColor(hdcMemory, CreateRGB(color)); } DrawText(hdcMemory, text, -1, &rc, DT_END_ELLIPSIS | horizontalAlignment | verticalAlignment); @@ -99,9 +94,8 @@ static auto CALLBACK Label_windowProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM return msg == WM_ERASEBKGND; } - } - return DefWindowProc(hwnd, msg, wparam, lparam); + return pWidget::windowProc(hwnd, msg, wparam, lparam); } } diff --git a/hiro/windows/widget/label.hpp b/hiro/windows/widget/label.hpp index 39896d58..be78defd 100644 --- a/hiro/windows/widget/label.hpp +++ b/hiro/windows/widget/label.hpp @@ -10,6 +10,8 @@ struct pLabel : pWidget { auto setBackgroundColor(Color color) -> void; auto setForegroundColor(Color color) -> void; auto setText(const string& text) -> void; + + auto windowProc(HWND, UINT, WPARAM, LPARAM) -> maybe override; }; } diff --git a/hiro/windows/widget/line-edit.cpp b/hiro/windows/widget/line-edit.cpp index 21226fa8..297716b2 100644 --- a/hiro/windows/widget/line-edit.cpp +++ b/hiro/windows/widget/line-edit.cpp @@ -8,8 +8,7 @@ auto pLineEdit::construct() -> void { WS_CHILD | WS_TABSTOP | ES_AUTOHSCROLL | ES_AUTOVSCROLL, 0, 0, 0, 0, _parentHandle(), nullptr, GetModuleHandle(0), 0 ); - SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG_PTR)&reference); - pWidget::_setState(); + pWidget::construct(); setBackgroundColor(state().backgroundColor); setEditable(state().editable); setText(state().text); diff --git a/hiro/windows/widget/progress-bar.cpp b/hiro/windows/widget/progress-bar.cpp index deff42b7..63272e68 100644 --- a/hiro/windows/widget/progress-bar.cpp +++ b/hiro/windows/widget/progress-bar.cpp @@ -6,8 +6,7 @@ auto pProgressBar::construct() -> void { hwnd = CreateWindow(PROGRESS_CLASS, L"", WS_CHILD | PBS_SMOOTH, 0, 0, 0, 0, _parentHandle(), nullptr, GetModuleHandle(0), 0); - SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG_PTR)&reference); - pWidget::_setState(); + pWidget::construct(); SendMessage(hwnd, PBM_SETRANGE, 0, MAKELPARAM(0, 100)); SendMessage(hwnd, PBM_SETSTEP, MAKEWPARAM(1, 0), 0); setPosition(state().position); diff --git a/hiro/windows/widget/radio-button.cpp b/hiro/windows/widget/radio-button.cpp index d0840483..4d0bc41e 100644 --- a/hiro/windows/widget/radio-button.cpp +++ b/hiro/windows/widget/radio-button.cpp @@ -2,30 +2,11 @@ namespace hiro { -static auto CALLBACK RadioButton_windowProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) -> LRESULT { - if(auto object = (mObject*)GetWindowLongPtr(hwnd, GWLP_USERDATA)) { - if(auto button = dynamic_cast(object)) { - if(auto self = button->self()) { - if(msg == WM_ERASEBKGND) return DefWindowProc(hwnd, msg, wparam, lparam); - if(msg == WM_PAINT) return Button_paintProc(hwnd, msg, wparam, lparam, - button->state.bordered, button->state.checked, button->enabled(true), button->font(true), - button->state.icon, button->state.orientation, button->state.text - ); - return self->windowProc(hwnd, msg, wparam, lparam); - } - } - } - return DefWindowProc(hwnd, msg, wparam, lparam); -} - auto pRadioButton::construct() -> void { hwnd = CreateWindow(L"BUTTON", L"", WS_CHILD | WS_TABSTOP | BS_CHECKBOX | BS_PUSHLIKE, 0, 0, 0, 0, _parentHandle(), nullptr, GetModuleHandle(0), 0); - SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG_PTR)&reference); - windowProc = (WindowProc)GetWindowLongPtr(hwnd, GWLP_WNDPROC); - SetWindowLongPtr(hwnd, GWLP_WNDPROC, (LONG_PTR)RadioButton_windowProc); - pWidget::_setState(); + pWidget::construct(); setGroup(state().group); _setState(); } @@ -111,12 +92,32 @@ auto pRadioButton::setVisible(bool visible) -> void { _setState(); } +// + auto pRadioButton::onActivate() -> void { if(state().checked) return; self().setChecked(); self().doActivate(); } +auto pRadioButton::windowProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) -> maybe { + if(msg == WM_PAINT) { + PAINTSTRUCT ps; + BeginPaint(hwnd, &ps); + auto buttonState = Button_GetState(hwnd); + Button_CustomDraw(hwnd, ps, + state().bordered, state().checked, self().enabled(true), buttonState, + self().font(true), state().icon, state().orientation, state().text + ); + EndPaint(hwnd, &ps); + return false; + } + + return pWidget::windowProc(hwnd, msg, wparam, lparam); +} + +// + auto pRadioButton::_setState() -> void { InvalidateRect(hwnd, 0, false); } diff --git a/hiro/windows/widget/radio-button.hpp b/hiro/windows/widget/radio-button.hpp index dc0c5b3a..d9ddbe20 100644 --- a/hiro/windows/widget/radio-button.hpp +++ b/hiro/windows/widget/radio-button.hpp @@ -17,10 +17,9 @@ struct pRadioButton : pWidget { auto setVisible(bool visible) -> void override; auto onActivate() -> void; + auto windowProc(HWND, UINT, WPARAM, LPARAM) -> maybe override; auto _setState() -> void; - - WindowProc windowProc = nullptr; }; } diff --git a/hiro/windows/widget/radio-label.cpp b/hiro/windows/widget/radio-label.cpp index 90db50b7..2284b3d6 100644 --- a/hiro/windows/widget/radio-label.cpp +++ b/hiro/windows/widget/radio-label.cpp @@ -8,7 +8,7 @@ auto pRadioLabel::construct() -> void { 0, 0, 0, 0, _parentHandle(), nullptr, GetModuleHandle(0), 0 ); SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG_PTR)&reference); - pWidget::_setState(); + pWidget::construct(); setGroup(state().group); setText(state().text); } diff --git a/hiro/windows/widget/tab-frame.cpp b/hiro/windows/widget/tab-frame.cpp index cfd2fb51..d15449e5 100644 --- a/hiro/windows/widget/tab-frame.cpp +++ b/hiro/windows/widget/tab-frame.cpp @@ -3,14 +3,11 @@ namespace hiro { static auto CALLBACK TabFrame_windowProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) -> LRESULT { - if(auto object = (mObject*)GetWindowLongPtr(hwnd, GWLP_USERDATA)) { - if(auto tabFrame = dynamic_cast(object)) { - if(auto self = tabFrame->self()) { - return Shared_windowProc(self->windowProc, hwnd, msg, wparam, lparam); - } + if(auto tabFrame = (mTabFrame*)GetWindowLongPtr(hwnd, GWLP_USERDATA)) { + if(auto self = tabFrame->self()) { + return Shared_windowProc(self->defaultWindowProc, hwnd, msg, wparam, lparam); } } - return DefWindowProc(hwnd, msg, wparam, lparam); } @@ -19,10 +16,8 @@ auto pTabFrame::construct() -> void { WC_TABCONTROL, L"", WS_CHILD | WS_TABSTOP, 0, 0, 0, 0, _parentHandle(), nullptr, GetModuleHandle(0), 0 ); - SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG_PTR)&reference); - windowProc = (WindowProc)GetWindowLongPtr(hwnd, GWLP_WNDPROC); + pWidget::construct(); SetWindowLongPtr(hwnd, GWLP_WNDPROC, (LONG_PTR)TabFrame_windowProc); - pWidget::_setState(); for(auto& item : state().items) append(item); } @@ -88,6 +83,8 @@ auto pTabFrame::setVisible(bool visible) -> void { } } +// + auto pTabFrame::_buildImageList() -> void { unsigned size = pFont::size(hfont, " ").height(); diff --git a/hiro/windows/widget/tab-frame.hpp b/hiro/windows/widget/tab-frame.hpp index 59fe3b64..7c31d38e 100644 --- a/hiro/windows/widget/tab-frame.hpp +++ b/hiro/windows/widget/tab-frame.hpp @@ -17,7 +17,6 @@ struct pTabFrame : pWidget { auto _buildImageList() -> void; auto _synchronizeSizable() -> void; - WindowProc windowProc = nullptr; HIMAGELIST imageList = nullptr; }; diff --git a/hiro/windows/widget/table-view.cpp b/hiro/windows/widget/table-view.cpp index 5feb4e8d..0f53e44b 100644 --- a/hiro/windows/widget/table-view.cpp +++ b/hiro/windows/widget/table-view.cpp @@ -2,36 +2,14 @@ namespace hiro { -static auto CALLBACK TableView_windowProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) -> LRESULT { - if(auto object = (mObject*)GetWindowLongPtr(hwnd, GWLP_USERDATA)) { - if(auto tableView = dynamic_cast(object)) { - if(auto self = tableView->self()) { - if(!tableView->enabled(true)) { - if(msg == WM_KEYDOWN || msg == WM_KEYUP || msg == WM_SYSKEYDOWN || msg == WM_SYSKEYUP) { - //WC_LISTVIEW responds to key messages even when its HWND is disabled - //the control should be inactive when disabled; so we intercept the messages here - return false; - } - } - return self->windowProc(hwnd, msg, wparam, lparam); - } - } - } - - return DefWindowProc(hwnd, msg, wparam, lparam); -} - auto pTableView::construct() -> void { hwnd = CreateWindowEx( WS_EX_CLIENTEDGE | LVS_EX_DOUBLEBUFFER, WC_LISTVIEW, L"", WS_CHILD | WS_TABSTOP | LVS_REPORT | LVS_SHOWSELALWAYS, 0, 0, 0, 0, _parentHandle(), nullptr, GetModuleHandle(0), 0 ); - SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG_PTR)&reference); - windowProc = (WindowProc)GetWindowLongPtr(hwnd, GWLP_WNDPROC); - SetWindowLongPtr(hwnd, GWLP_WNDPROC, (LONG_PTR)&TableView_windowProc); ListView_SetExtendedListViewStyle(hwnd, LVS_EX_FULLROWSELECT | LVS_EX_SUBITEMIMAGES); - pWidget::_setState(); + pWidget::construct(); setBackgroundColor(state().backgroundColor); setBatchable(state().batchable); setBordered(state().bordered); @@ -301,6 +279,22 @@ auto pTableView::setSortable(bool sortable) -> void { SetWindowLong(hwnd, GWL_STYLE, style); } +// + +auto pTableView::windowProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) -> maybe { + if(msg == WM_KEYDOWN || msg == WM_KEYUP || msg == WM_SYSKEYDOWN || msg == WM_SYSKEYUP) { + if(!self().enabled(true)) { + //WC_LISTVIEW responds to key messages even when its HWND is disabled + //the control should be inactive when disabled; so we intercept the messages here + return false; + } + } + + return pWidget::windowProc(hwnd, msg, wparam, lparam); +} + +// + auto pTableView::_backgroundColor(unsigned _row, unsigned _column) -> Color { if(auto item = self().item(_row)) { if(auto cell = item->cell(_column)) { diff --git a/hiro/windows/widget/table-view.hpp b/hiro/windows/widget/table-view.hpp index 36a1732f..c8efe055 100644 --- a/hiro/windows/widget/table-view.hpp +++ b/hiro/windows/widget/table-view.hpp @@ -25,6 +25,7 @@ struct pTableView : pWidget { auto onCustomDraw(LPARAM lparam) -> LRESULT; auto onSort(LPARAM lparam) -> void; auto onToggle(LPARAM lparam) -> void; + auto windowProc(HWND, UINT, WPARAM, LPARAM) -> maybe override; auto _backgroundColor(unsigned row, unsigned column) -> Color; auto _cellWidth(unsigned row, unsigned column) -> unsigned; @@ -34,7 +35,6 @@ struct pTableView : pWidget { auto _setIcons() -> void; auto _width(unsigned column) -> unsigned; - WindowProc windowProc = nullptr; HIMAGELIST imageList = 0; vector icons; }; diff --git a/hiro/windows/widget/text-edit.cpp b/hiro/windows/widget/text-edit.cpp index fc04f47e..ed94fbbb 100644 --- a/hiro/windows/widget/text-edit.cpp +++ b/hiro/windows/widget/text-edit.cpp @@ -8,8 +8,7 @@ auto pTextEdit::construct() -> void { WS_CHILD | WS_TABSTOP | WS_VSCROLL | ES_AUTOVSCROLL | ES_MULTILINE | ES_WANTRETURN | (!state().wordWrap ? WS_HSCROLL | ES_AUTOHSCROLL : 0), 0, 0, 0, 0, _parentHandle(), nullptr, GetModuleHandle(0), 0 ); - SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG_PTR)&reference); - pWidget::_setState(); + pWidget::construct(); setBackgroundColor(state().backgroundColor); setEditable(state().editable); setText(state().text); diff --git a/hiro/windows/widget/vertical-scroll-bar.cpp b/hiro/windows/widget/vertical-scroll-bar.cpp index 9dd9370c..d2cfee70 100644 --- a/hiro/windows/widget/vertical-scroll-bar.cpp +++ b/hiro/windows/widget/vertical-scroll-bar.cpp @@ -7,8 +7,7 @@ auto pVerticalScrollBar::construct() -> void { L"SCROLLBAR", L"", WS_CHILD | SBS_VERT, 0, 0, 0, 0, _parentHandle(), nullptr, GetModuleHandle(0), 0 ); - SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG_PTR)&reference); - pWidget::_setState(); + pWidget::construct(); setLength(state().length); setPosition(state().position); } diff --git a/hiro/windows/widget/vertical-slider.cpp b/hiro/windows/widget/vertical-slider.cpp index 9a31e863..980ea3f8 100644 --- a/hiro/windows/widget/vertical-slider.cpp +++ b/hiro/windows/widget/vertical-slider.cpp @@ -7,8 +7,7 @@ auto pVerticalSlider::construct() -> void { TRACKBAR_CLASS, L"", WS_CHILD | WS_TABSTOP | TBS_TRANSPARENTBKGND | TBS_NOTICKS | TBS_BOTH | TBS_VERT, 0, 0, 0, 0, _parentHandle(), nullptr, GetModuleHandle(0), 0 ); - SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG_PTR)&reference); - pWidget::_setState(); + pWidget::construct(); setLength(state().length); setPosition(state().position); } diff --git a/hiro/windows/widget/viewport.cpp b/hiro/windows/widget/viewport.cpp index d93b076b..f0a7a04f 100644 --- a/hiro/windows/widget/viewport.cpp +++ b/hiro/windows/widget/viewport.cpp @@ -2,56 +2,11 @@ namespace hiro { -static auto CALLBACK Viewport_windowProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) -> LRESULT { - auto object = (mObject*)GetWindowLongPtr(hwnd, GWLP_USERDATA); - if(!object) return DefWindowProc(hwnd, msg, wparam, lparam); - auto viewport = dynamic_cast(object); - if(!viewport) return DefWindowProc(hwnd, msg, wparam, lparam); - - if(msg == WM_DROPFILES) { - if(auto paths = DropPaths(wparam)) viewport->doDrop(paths); - return false; - } - - if(msg == WM_GETDLGCODE) { - return DLGC_STATIC | DLGC_WANTCHARS; - } - - if(msg == WM_MOUSEMOVE) { - TRACKMOUSEEVENT tracker{sizeof(TRACKMOUSEEVENT), TME_LEAVE, hwnd}; - TrackMouseEvent(&tracker); - viewport->doMouseMove({(int16_t)LOWORD(lparam), (int16_t)HIWORD(lparam)}); - } - - if(msg == WM_MOUSELEAVE) { - viewport->doMouseLeave(); - } - - if(msg == WM_LBUTTONDOWN || msg == WM_MBUTTONDOWN || msg == WM_RBUTTONDOWN) { - switch(msg) { - case WM_LBUTTONDOWN: viewport->doMousePress(Mouse::Button::Left); break; - case WM_MBUTTONDOWN: viewport->doMousePress(Mouse::Button::Middle); break; - case WM_RBUTTONDOWN: viewport->doMousePress(Mouse::Button::Right); break; - } - } - - if(msg == WM_LBUTTONUP || msg == WM_MBUTTONUP || msg == WM_RBUTTONUP) { - switch(msg) { - case WM_LBUTTONUP: viewport->doMouseRelease(Mouse::Button::Left); break; - case WM_MBUTTONUP: viewport->doMouseRelease(Mouse::Button::Middle); break; - case WM_RBUTTONUP: viewport->doMouseRelease(Mouse::Button::Right); break; - } - } - - return DefWindowProc(hwnd, msg, wparam, lparam); -} - auto pViewport::construct() -> void { - hwnd = CreateWindow(L"hiroViewport", L"", + hwnd = CreateWindow(L"hiroWidget", L"", WS_CHILD | WS_DISABLED, 0, 0, 0, 0, _parentHandle(), nullptr, GetModuleHandle(0), 0); - SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG_PTR)&reference); - pWidget::_setState(); + pWidget::construct(); setDroppable(state().droppable); } @@ -67,6 +22,57 @@ auto pViewport::setDroppable(bool droppable) -> void { DragAcceptFiles(hwnd, droppable); } +// + +auto pViewport::doMouseLeave() -> void { + return self().doMouseLeave(); +} + +auto pViewport::doMouseMove(int x, int y) -> void { + return self().doMouseMove({x, y}); +} + +auto pViewport::windowProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) -> maybe { + if(msg == WM_DROPFILES) { + if(auto paths = DropPaths(wparam)) self().doDrop(paths); + return false; + } + + if(msg == WM_ERASEBKGND) { + PAINTSTRUCT ps; + BeginPaint(hwnd, &ps); + auto brush = CreateSolidBrush(RGB(0, 0, 0)); + RECT rc{}; + GetClientRect(hwnd, &rc); + FillRect(ps.hdc, &rc, brush); + DeleteObject(brush); + EndPaint(hwnd, &ps); + return true; + } + + if(msg == WM_GETDLGCODE) { + return DLGC_STATIC | DLGC_WANTCHARS; + } + + if(msg == WM_LBUTTONDOWN || msg == WM_MBUTTONDOWN || msg == WM_RBUTTONDOWN) { + switch(msg) { + case WM_LBUTTONDOWN: self().doMousePress(Mouse::Button::Left); break; + case WM_MBUTTONDOWN: self().doMousePress(Mouse::Button::Middle); break; + case WM_RBUTTONDOWN: self().doMousePress(Mouse::Button::Right); break; + } + } + + if(msg == WM_LBUTTONUP || msg == WM_MBUTTONUP || msg == WM_RBUTTONUP) { + switch(msg) { + case WM_LBUTTONUP: self().doMouseRelease(Mouse::Button::Left); break; + case WM_MBUTTONUP: self().doMouseRelease(Mouse::Button::Middle); break; + case WM_RBUTTONUP: self().doMouseRelease(Mouse::Button::Right); break; + } + } + + return pWidget::windowProc(hwnd, msg, wparam, lparam); +} + } #endif diff --git a/hiro/windows/widget/viewport.hpp b/hiro/windows/widget/viewport.hpp index ed0b17f7..ae917280 100644 --- a/hiro/windows/widget/viewport.hpp +++ b/hiro/windows/widget/viewport.hpp @@ -7,6 +7,10 @@ struct pViewport : pWidget { auto handle() const -> uintptr_t; auto setDroppable(bool droppable) -> void; + + auto doMouseLeave() -> void override; + auto doMouseMove(int x, int y) -> void override; + auto windowProc(HWND, UINT, WPARAM, LPARAM) -> maybe override; }; } diff --git a/hiro/windows/widget/widget.cpp b/hiro/windows/widget/widget.cpp index 5edde98b..c3d956b5 100644 --- a/hiro/windows/widget/widget.cpp +++ b/hiro/windows/widget/widget.cpp @@ -2,17 +2,35 @@ namespace hiro { +static auto CALLBACK Widget_windowProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) -> LRESULT { + if(auto object = (mObject*)GetWindowLongPtr(hwnd, GWLP_USERDATA)) { + if(auto widget = dynamic_cast(object)) { + if(auto self = widget->self()) { + if(auto result = self->windowProc(hwnd, msg, wparam, lparam)) { + return result(); + } + return CallWindowProc(self->defaultWindowProc, hwnd, msg, wparam, lparam); + } + } + } + return DefWindowProc(hwnd, msg, wparam, lparam); +} + auto pWidget::construct() -> void { - abstract = true; - //todo: create hiroWidget - hwnd = CreateWindow(L"hiroLabel", L"", WS_CHILD, 0, 0, 0, 0, _parentHandle(), nullptr, GetModuleHandle(0), 0); + if(!hwnd) { + abstract = true; + hwnd = CreateWindow(L"hiroWidget", L"", WS_CHILD, 0, 0, 0, 0, _parentHandle(), nullptr, GetModuleHandle(0), 0); + } SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG_PTR)&reference); + defaultWindowProc = (WindowProc)GetWindowLongPtr(hwnd, GWLP_WNDPROC); + SetWindowLongPtr(hwnd, GWLP_WNDPROC, (LONG_PTR)Widget_windowProc); _setState(); } auto pWidget::destruct() -> void { - DeleteObject(hfont); - DestroyWindow(hwnd); + toolTip.reset(); + if(hfont) { DeleteObject(hfont); hfont = nullptr; } + if(hwnd) { DestroyWindow(hwnd); hwnd = nullptr; } } auto pWidget::focused() const -> bool { @@ -48,7 +66,15 @@ auto pWidget::setGeometry(Geometry geometry) -> void { geometry.setY(geometry.y() - displacement.y()); } SetWindowPos(hwnd, nullptr, geometry.x(), geometry.y(), geometry.width(), geometry.height(), SWP_NOZORDER); - self().doSize(); + pSizable::setGeometry(geometry); +} + +auto pWidget::setToolTip(const string& toolTipText) -> void { + if(toolTipText) { + toolTip = new pToolTip{toolTipText}; + } else { + toolTip.reset(); + } } auto pWidget::setVisible(bool visible) -> void { @@ -60,6 +86,44 @@ auto pWidget::setVisible(bool visible) -> void { // +auto pWidget::doMouseHover() -> void { + if(toolTip) toolTip->show(); +} + +auto pWidget::doMouseLeave() -> void { +} + +auto pWidget::doMouseMove(int x, int y) -> void { +} + +auto pWidget::windowProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) -> maybe { + if(msg == WM_MOUSEMOVE) { + TRACKMOUSEEVENT event{sizeof(TRACKMOUSEEVENT)}; + event.hwndTrack = hwnd; + event.dwFlags = TME_LEAVE | TME_HOVER; + event.dwHoverTime = 1500; + TrackMouseEvent(&event); + POINT p{}; + GetCursorPos(&p); + doMouseMove(p.x, p.y); + if(auto toolTip = pApplication::state().toolTip) { + toolTip->windowProc(hwnd, msg, wparam, lparam); + } + } + + if(msg == WM_MOUSELEAVE) { + doMouseLeave(); + } + + if(msg == WM_MOUSEHOVER) { + doMouseHover(); + } + + return {}; +} + +// + auto pWidget::_parentHandle() -> HWND { if(auto parent = _parentWidget()) return parent->hwnd; if(auto parent = _parentWindow()) return parent->hwnd; @@ -72,19 +136,20 @@ auto pWidget::_parentWidget() -> maybe { if(auto self = parent->self()) return *self; } #endif - return nothing; + return {}; } auto pWidget::_parentWindow() -> maybe { if(auto parent = self().parentWindow(true)) { if(auto self = parent->self()) return *self; } - return nothing; + return {}; } auto pWidget::_setState() -> void { setEnabled(self().enabled()); setFont(self().font()); + setToolTip(self().toolTip()); setVisible(self().visible()); } diff --git a/hiro/windows/widget/widget.hpp b/hiro/windows/widget/widget.hpp index 2f2926a0..a00f9640 100644 --- a/hiro/windows/widget/widget.hpp +++ b/hiro/windows/widget/widget.hpp @@ -11,16 +11,24 @@ struct pWidget : pSizable { auto setFocused() -> void; auto setFont(const Font& font) -> void override; virtual auto setGeometry(Geometry geometry) -> void; + auto setToolTip(const string& toolTip) -> void; auto setVisible(bool visible) -> void override; + virtual auto doMouseHover() -> void; + virtual auto doMouseLeave() -> void; + virtual auto doMouseMove(int x, int y) -> void; + virtual auto windowProc(HWND, UINT, WPARAM, LPARAM) -> maybe; + auto _parentHandle() -> HWND; auto _parentWidget() -> maybe; auto _parentWindow() -> maybe; auto _setState() -> void; bool abstract = false; - HWND hwnd = 0; - HFONT hfont = 0; + WindowProc defaultWindowProc = nullptr; + HWND hwnd = nullptr; + HFONT hfont = nullptr; + unique_pointer toolTip; }; } diff --git a/icarus/heuristics/heuristics.cpp b/icarus/heuristics/heuristics.cpp index 89eb9ac9..c564ac95 100644 --- a/icarus/heuristics/heuristics.cpp +++ b/icarus/heuristics/heuristics.cpp @@ -24,4 +24,11 @@ auto Oscillator::text() const -> string { return output; } +auto Slot::text() const -> string { + string output; + output.append(" slot\n"); + output.append(" type: ", _type, "\n"); + return output; +} + } diff --git a/icarus/heuristics/heuristics.hpp b/icarus/heuristics/heuristics.hpp index f3f6ef2a..a65dafa0 100644 --- a/icarus/heuristics/heuristics.hpp +++ b/icarus/heuristics/heuristics.hpp @@ -27,4 +27,11 @@ struct Oscillator { natural _frequency; }; +struct Slot { + auto& type(string type) { _type = type; return *this; } + auto text() const -> string; + + string _type; +}; + } diff --git a/icarus/heuristics/mega-drive.cpp b/icarus/heuristics/mega-drive.cpp index f4cfaff6..7e5d33b5 100644 --- a/icarus/heuristics/mega-drive.cpp +++ b/icarus/heuristics/mega-drive.cpp @@ -85,9 +85,13 @@ auto MegaDrive::manifest() const -> string { output.append(" name: ", Location::prefix(location), "\n"); output.append(" region: ", regions.left(), "\n"); output.append(" board\n"); - if(domesticName == "SONIC & KNUCKLES") { + if(domesticName == "Game Genie") { + output.append(Memory{}.type("ROM").size(data.size()).content("Program").text()); + output.append(Slot{}.type("MegaDrive").text()); + } else if(domesticName == "SONIC & KNUCKLES") { output.append(Memory{}.type("ROM").size(0x200000).content("Program").text()); output.append(Memory{}.type("ROM").size( 0x40000).content("Patch").text()); + output.append(Slot{}.type("MegaDrive").text()); } else { output.append(Memory{}.type("ROM").size(data.size()).content("Program").text()); } diff --git a/nall/image.hpp b/nall/image.hpp index 34380859..c8f7768d 100644 --- a/nall/image.hpp +++ b/nall/image.hpp @@ -48,7 +48,7 @@ struct image { inline image(const string& filename); inline image(const void* data, uint size); inline image(const vector& buffer); - template inline image(Type (&Name)[Size]); + template inline image(const uint8_t (&Name)[Size]); inline image(); inline ~image(); diff --git a/nall/image/core.hpp b/nall/image/core.hpp index 933c9a5a..c2fae6b9 100644 --- a/nall/image/core.hpp +++ b/nall/image/core.hpp @@ -34,7 +34,7 @@ image::image(const void* data_, uint size) { image::image(const vector& buffer) : image(buffer.data(), buffer.size()) { } -template image::image(Type (&Name)[Size]) : image(Name, Size) { +template image::image(const uint8_t (&Name)[Size]) : image(Name, Size) { } image::image() { diff --git a/nall/queue.hpp b/nall/queue.hpp index 0b44daf6..0067c69d 100644 --- a/nall/queue.hpp +++ b/nall/queue.hpp @@ -16,9 +16,10 @@ struct queue { auto operator=(const queue& source) -> queue& { if(this == &source) return *this; reset(); + _capacity = source._capacity; _size = source._size; - _data = new T[_size]; - for(uint n : range(_size)) _data[n] = source._data[n]; + _data = new T[_capacity]; + for(uint n : range(_capacity)) _data[n] = source._data[n]; _read = source._read; _write = source._write; return *this; @@ -27,6 +28,7 @@ struct queue { auto operator=(queue&& source) -> queue& { if(this == &source) return *this; _data = source._data; + _capacity = source._capacity; _size = source._size; _read = source._read; _write = source._write; @@ -39,11 +41,13 @@ struct queue { reset(); } - explicit operator bool() const { return _size; } + template auto capacity() const -> uint { return _capacity * sizeof(T) / sizeof(U); } template auto size() const -> uint { return _size * sizeof(T) / sizeof(U); } - auto empty() const -> bool { return _read == _write; } - auto pending() const -> bool { return _read != _write; } - auto full() const -> bool { return _write == _size; } + auto empty() const -> bool { return _size == 0; } + auto pending() const -> bool { return _size > 0; } + auto full() const -> bool { return _size >= (int)_capacity; } + auto underflow() const -> bool { return _size < 0; } + auto overflow() const -> bool { return _size > (int)_capacity; } auto data() -> T* { return _data; } auto data() const -> const T* { return _data; } @@ -51,35 +55,41 @@ struct queue { auto reset() { delete[] _data; _data = nullptr; + _capacity = 0; _size = 0; _read = 0; _write = 0; } - auto resize(uint size, const T& value = {}) -> void { + auto resize(uint capacity, const T& value = {}) -> void { reset(); - _size = size; - _data = new T[_size]; - for(uint n : range(_size)) _data[n] = value; + _capacity = capacity; + _data = new T[_capacity]; + for(uint n : range(_capacity)) _data[n] = value; } auto flush() -> void { + _size = 0; _read = 0; _write = 0; } auto read() -> T { - if(_read >= _size) _read = 0; - return _data[_read++]; + T value = _data[_read++]; + if(_read >= _capacity) _read = 0; + _size--; + return value; } auto write(const T& value) -> void { - if(_write >= _size) _write = 0; _data[_write++] = value; + if(_write >= _capacity) _write = 0; + _size++; } auto serialize(serializer& s) -> void { - s.array(_data, _size); + s.array(_data, _capacity); + s.integer(_capacity); s.integer(_size); s.integer(_read); s.integer(_write); @@ -87,7 +97,8 @@ struct queue { private: T* _data = nullptr; - uint _size = 0; + uint _capacity = 0; + int _size = 0; uint _read = 0; uint _write = 0; }; diff --git a/nall/windows/utf8.hpp b/nall/windows/utf8.hpp index 8ee26aca..8ea49c60 100644 --- a/nall/windows/utf8.hpp +++ b/nall/windows/utf8.hpp @@ -24,55 +24,74 @@ using uint = unsigned; namespace nall { //UTF-8 to UTF-16 struct utf16_t { - utf16_t(const char* s = "") { + utf16_t(const char* s = "") { operator=(s); } + ~utf16_t() { reset(); } + + utf16_t(const utf16_t&) = delete; + auto operator=(const utf16_t&) -> utf16_t& = delete; + + auto operator=(const char* s) -> utf16_t& { + reset(); if(!s) s = ""; - uint length = MultiByteToWideChar(CP_UTF8, 0, s, -1, nullptr, 0); - buffer = new wchar_t[length + 1](); + length = MultiByteToWideChar(CP_UTF8, 0, s, -1, nullptr, 0); + buffer = new wchar_t[length + 1]; MultiByteToWideChar(CP_UTF8, 0, s, -1, buffer, length); + buffer[length] = 0; + return *this; } - ~utf16_t() { + operator wchar_t*() { return buffer; } + operator const wchar_t*() const { return buffer; } + + auto reset() -> void { delete[] buffer; + length = 0; } - operator wchar_t*() { - return buffer; - } + auto data() -> wchar_t* { return buffer; } + auto data() const -> const wchar_t* { return buffer; } - operator const wchar_t*() const { - return buffer; - } + auto size() const -> uint { return length; } private: wchar_t* buffer = nullptr; + uint length = 0; }; //UTF-16 to UTF-8 struct utf8_t { - utf8_t(const wchar_t* s = L"") { - if(!s) s = L""; - uint length = WideCharToMultiByte(CP_UTF8, 0, s, -1, nullptr, 0, nullptr, nullptr); - buffer = new char[length + 1](); - WideCharToMultiByte(CP_UTF8, 0, s, -1, buffer, length, nullptr, nullptr); - } - - ~utf8_t() { - delete[] buffer; - } + utf8_t(const wchar_t* s = L"") { operator=(s); } + ~utf8_t() { reset(); } utf8_t(const utf8_t&) = delete; - utf8_t& operator=(const utf8_t&) = delete; + auto operator=(const utf8_t&) -> utf8_t& = delete; - operator char*() { - return buffer; + auto operator=(const wchar_t* s) -> utf8_t& { + reset(); + if(!s) s = L""; + length = WideCharToMultiByte(CP_UTF8, 0, s, -1, nullptr, 0, nullptr, nullptr); + buffer = new char[length + 1]; + WideCharToMultiByte(CP_UTF8, 0, s, -1, buffer, length, nullptr, nullptr); + buffer[length] = 0; + return *this; } - operator const char*() const { - return buffer; + auto reset() -> void { + delete[] buffer; + length = 0; } + operator char*() { return buffer; } + operator const char*() const { return buffer; } + + auto data() -> char* { return buffer; } + auto data() const -> const char* { return buffer; } + + auto size() const -> uint { return length; } + private: char* buffer = nullptr; + uint length = 0; }; inline auto utf8_args(int& argc, char**& argv) -> void { diff --git a/ruby/audio/audio.cpp b/ruby/audio/audio.cpp index 6da2f2b7..6c060739 100644 --- a/ruby/audio/audio.cpp +++ b/ruby/audio/audio.cpp @@ -77,12 +77,14 @@ auto Audio::setDynamic(bool dynamic) -> bool { } auto Audio::setChannels(uint channels) -> bool { + if(resamplers.size() != channels) { + resamplers.reset(); + resamplers.resize(channels); + for(auto& resampler : resamplers) resampler.reset(instance->frequency); + } if(instance->channels == channels) return true; if(!instance->hasChannels(channels)) return false; if(!instance->setChannels(instance->channels = channels)) return false; - resamplers.reset(); - resamplers.resize(channels); - for(auto& resampler : resamplers) resampler.reset(instance->frequency); return true; } diff --git a/ruby/audio/oss.cpp b/ruby/audio/oss.cpp index eeb0f95b..41d297da 100644 --- a/ruby/audio/oss.cpp +++ b/ruby/audio/oss.cpp @@ -69,11 +69,12 @@ struct AudioOSS : AudioDriver { } auto output(const double samples[]) -> void override { - if(!ready()) return; - for(uint n : range(self.channels)) { buffer.write(sclamp<16>(samples[n] * 32767.0)); - if(buffer.full()) write(_fd, buffer.data(), buffer.size()); + if(buffer.full()) { + write(_fd, buffer.data(), buffer.capacity()); + buffer.flush(); + } } } diff --git a/ruby/audio/pulseaudio.cpp b/ruby/audio/pulseaudio.cpp index 9a5ccdad..01665fef 100644 --- a/ruby/audio/pulseaudio.cpp +++ b/ruby/audio/pulseaudio.cpp @@ -25,7 +25,7 @@ struct AudioPulseAudio : AudioDriver { } auto setBlocking(bool blocking) -> bool override { return true; } - auto setFrequency(double frequency) -> bool override { return initialize(); } + auto setFrequency(uint frequency) -> bool override { return initialize(); } auto setLatency(uint latency) -> bool override { return initialize(); } auto output(const double samples[]) -> void override { @@ -43,7 +43,7 @@ struct AudioPulseAudio : AudioDriver { } uint length = pa_stream_writable_size(_stream); if(length >= _offset * pa_frame_size(&_specification)) break; - if(!_blocking) { + if(!self.blocking) { _offset = 0; return; } diff --git a/ruby/audio/xaudio2.cpp b/ruby/audio/xaudio2.cpp index b8058e7e..a7a87653 100644 --- a/ruby/audio/xaudio2.cpp +++ b/ruby/audio/xaudio2.cpp @@ -1,5 +1,6 @@ #include "xaudio2.hpp" #include +#undef interface struct AudioXAudio2 : AudioDriver, public IXAudio2VoiceCallback { AudioXAudio2& self = *this; @@ -14,7 +15,7 @@ struct AudioXAudio2 : AudioDriver, public IXAudio2VoiceCallback { } auto driver() -> string override { return "XAudio2"; } - auto ready() -> bool override { return _ready; } + auto ready() -> bool override { return self.isReady; } auto hasBlocking() -> bool override { return true; } @@ -31,62 +32,62 @@ struct AudioXAudio2 : AudioDriver, public IXAudio2VoiceCallback { auto setLatency(uint latency) -> bool override { return initialize(); } auto clear() -> void override { - if(!_sourceVoice) return; - _sourceVoice->Stop(0); - _sourceVoice->FlushSourceBuffers(); //calls OnBufferEnd for all currently submitted buffers + if(!self.sourceVoice) return; + self.sourceVoice->Stop(0); + self.sourceVoice->FlushSourceBuffers(); //calls OnBufferEnd for all currently submitted buffers - _bufferIndex = 0; + self.bufferIndex = 0; - _bufferOffset = 0; - if(_buffer) memory::fill(_buffer, _period * _bufferCount); + self.bufferOffset = 0; + if(self.buffer) memory::fill(self.buffer, self.period * self.bufferCount); - _sourceVoice->Start(0); + self.sourceVoice->Start(0); } auto output(const double samples[]) -> void override { - _buffer[_bufferIndex * _period + _bufferOffset] = (uint16_t)sclamp<16>(samples[0] * 32767.0) << 0; - _buffer[_bufferIndex * _period + _bufferOffset] |= (uint16_t)sclamp<16>(samples[1] * 32767.0) << 16; - if(++_bufferOffset < _period) return; - _bufferOffset = 0; + self.buffer[self.bufferIndex * self.period + self.bufferOffset] = (uint16_t)sclamp<16>(samples[0] * 32767.0) << 0; + self.buffer[self.bufferIndex * self.period + self.bufferOffset] |= (uint16_t)sclamp<16>(samples[1] * 32767.0) << 16; + if(++self.bufferOffset < self.period) return; + self.bufferOffset = 0; - if(_bufferQueue == _bufferCount - 1) { + if(self.bufferQueue == self.bufferCount - 1) { if(self.blocking) { //wait until there is at least one other free buffer for the next sample - while(_bufferQueue == _bufferCount - 1); + while(self.bufferQueue == self.bufferCount - 1); } else { //we need one free buffer for the next sample, so ignore the current contents return; } } - pushBuffer(_period * 4, _buffer + _bufferIndex * _period); - _bufferIndex = (_bufferIndex + 1) % _bufferCount; + pushBuffer(self.period * 4, self.buffer + self.bufferIndex * self.period); + self.bufferIndex = (self.bufferIndex + 1) % self.bufferCount; } private: auto initialize() -> bool { terminate(); - _bufferCount = 8; - _period = self.frequency * self.latency / _bufferCount / 1000.0 + 0.5; - _buffer = new uint32_t[_period * _bufferCount]; - _bufferOffset = 0; - _bufferIndex = 0; - _bufferQueue = 0; + self.bufferCount = 8; + self.period = self.frequency * self.latency / self.bufferCount / 1000.0 + 0.5; + self.buffer = new uint32_t[self.period * self.bufferCount]; + self.bufferOffset = 0; + self.bufferIndex = 0; + self.bufferQueue = 0; - if(FAILED(XAudio2Create(&_interface, 0 , XAUDIO2_DEFAULT_PROCESSOR))) return false; + if(FAILED(XAudio2Create(&self.interface, 0 , XAUDIO2_DEFAULT_PROCESSOR))) return false; uint deviceCount = 0; - _interface->GetDeviceCount(&deviceCount); + self.interface->GetDeviceCount(&deviceCount); if(deviceCount == 0) return terminate(), false; uint deviceID = 0; for(uint deviceIndex : range(deviceCount)) { XAUDIO2_DEVICE_DETAILS deviceDetails = {}; - _interface->GetDeviceDetails(deviceIndex, &deviceDetails); + self.interface->GetDeviceDetails(deviceIndex, &deviceDetails); if(deviceDetails.Role & DefaultGameDevice) deviceID = deviceIndex; } - if(FAILED(_interface->CreateMasteringVoice(&_masterVoice, self.channels, self.frequency, 0, deviceID, nullptr))) return terminate(), false; + if(FAILED(self.interface->CreateMasteringVoice(&self.masterVoice, self.channels, self.frequency, 0, deviceID, nullptr))) return terminate(), false; WAVEFORMATEX waveFormat; waveFormat.wFormatTag = WAVE_FORMAT_PCM; @@ -97,56 +98,56 @@ private: waveFormat.nAvgBytesPerSec = waveFormat.nSamplesPerSec * waveFormat.nBlockAlign; waveFormat.cbSize = 0; - if(FAILED(_interface->CreateSourceVoice(&_sourceVoice, (WAVEFORMATEX*)&waveFormat, XAUDIO2_VOICE_NOSRC, XAUDIO2_DEFAULT_FREQ_RATIO, this, nullptr, nullptr))) return terminate(), false; + if(FAILED(self.interface->CreateSourceVoice(&self.sourceVoice, (WAVEFORMATEX*)&waveFormat, XAUDIO2_VOICE_NOSRC, XAUDIO2_DEFAULT_FREQ_RATIO, this, nullptr, nullptr))) return terminate(), false; clear(); - return _ready = true; + return self.isReady = true; } auto terminate() -> void { - _ready = false; + self.isReady = false; - if(_sourceVoice) { - _sourceVoice->Stop(0); - _sourceVoice->DestroyVoice(); - _sourceVoice = nullptr; + if(self.sourceVoice) { + self.sourceVoice->Stop(0); + self.sourceVoice->DestroyVoice(); + self.sourceVoice = nullptr; } - if(_masterVoice) { - _masterVoice->DestroyVoice(); - _masterVoice = nullptr; + if(self.masterVoice) { + self.masterVoice->DestroyVoice(); + self.masterVoice = nullptr; } - if(_interface) { - _interface->Release(); - _interface = nullptr; + if(self.interface) { + self.interface->Release(); + self.interface = nullptr; } - delete[] _buffer; - _buffer = nullptr; + delete[] self.buffer; + self.buffer = nullptr; } - auto pushBuffer(uint bytes, uint32_t* _audioData) -> void { + auto pushBuffer(uint bytes, uint32_t* audioData) -> void { XAUDIO2_BUFFER buffer = {}; buffer.AudioBytes = bytes; - buffer.pAudioData = reinterpret_cast(_audioData); + buffer.pAudioData = (BYTE*)audioData; buffer.pContext = 0; - InterlockedIncrement(&_bufferQueue); - _sourceVoice->SubmitSourceBuffer(&buffer); + InterlockedIncrement(&self.bufferQueue); + self.sourceVoice->SubmitSourceBuffer(&buffer); } - bool _ready = false; + bool isReady = false; - uint32_t* _buffer = nullptr; - uint _period = 0; - uint _bufferCount = 0; - uint _bufferOffset = 0; - uint _bufferIndex = 0; - volatile long _bufferQueue = 0; //how many buffers are queued and ready for playback + uint32_t* buffer = nullptr; + uint period = 0; + uint bufferCount = 0; + uint bufferOffset = 0; + uint bufferIndex = 0; + volatile long bufferQueue = 0; //how many buffers are queued and ready for playback - IXAudio2* _interface = nullptr; - IXAudio2MasteringVoice* _masterVoice = nullptr; - IXAudio2SourceVoice* _sourceVoice = nullptr; + IXAudio2* interface = nullptr; + IXAudio2MasteringVoice* masterVoice = nullptr; + IXAudio2SourceVoice* sourceVoice = nullptr; //inherited from IXAudio2VoiceCallback STDMETHODIMP_(void) OnBufferStart(void* pBufferContext){} @@ -157,6 +158,6 @@ private: STDMETHODIMP_(void) OnVoiceProcessingPassStart(UINT32 BytesRequired) {} STDMETHODIMP_(void) OnBufferEnd(void* pBufferContext) { - InterlockedDecrement(&_bufferQueue); + InterlockedDecrement(&self.bufferQueue); } };