diff --git a/bsnes/emulator/emulator.hpp b/bsnes/emulator/emulator.hpp index b29c17cd..7c0e10c6 100644 --- a/bsnes/emulator/emulator.hpp +++ b/bsnes/emulator/emulator.hpp @@ -29,7 +29,7 @@ using namespace nall; namespace Emulator { static const string Name = "bsnes"; - static const string Version = "109.3"; + static const string Version = "109.4"; static const string Author = "byuu"; static const string License = "GPLv3"; static const string Website = "https://byuu.org"; diff --git a/bsnes/target-bsnes/presentation/presentation.cpp b/bsnes/target-bsnes/presentation/presentation.cpp index 5b426142..94b8e204 100644 --- a/bsnes/target-bsnes/presentation/presentation.cpp +++ b/bsnes/target-bsnes/presentation/presentation.cpp @@ -115,29 +115,29 @@ auto Presentation::create() -> void { saveState.setIcon(Icon::Media::Record).setText("Save State"); for(uint index : range(QuickStates)) { MenuItem item{&saveState}; - item.setProperty("name", {"Quick/Slot ", 1 + index}); - item.setProperty("title", {"Slot ", 1 + index}); + item.setAttribute("name", {"Quick/Slot ", 1 + index}); + item.setAttribute("title", {"Slot ", 1 + index}); item.setText({"Slot ", 1 + index}); item.onActivate([=] { program.saveState({"Quick/Slot ", 1 + index}); }); } loadState.setIcon(Icon::Media::Rewind).setText("Load State"); for(uint index : range(QuickStates)) { MenuItem item{&loadState}; - item.setProperty("name", {"Quick/Slot ", 1 + index}); - item.setProperty("title", {"Slot ", 1 + index}); + item.setAttribute("name", {"Quick/Slot ", 1 + index}); + item.setAttribute("title", {"Slot ", 1 + index}); item.setText({"Slot ", 1 + index}); item.onActivate([=] { program.loadState({"Quick/Slot ", 1 + index}); }); } loadState.append(MenuSeparator()); loadState.append(MenuItem() - .setProperty("name", "Quick/Undo") - .setProperty("title", "Undo Last Save") + .setAttribute("name", "Quick/Undo") + .setAttribute("title", "Undo Last Save") .setIcon(Icon::Edit::Undo).setText("Undo Last Save").onActivate([&] { program.loadState("Quick/Undo"); })); loadState.append(MenuItem() - .setProperty("name", "Quick/Redo") - .setProperty("title", "Redo Last Undo") + .setAttribute("name", "Quick/Redo") + .setAttribute("title", "Redo Last Undo") .setIcon(Icon::Edit::Redo).setText("Redo Last Undo").onActivate([&] { program.loadState("Quick/Redo"); })); @@ -149,11 +149,11 @@ auto Presentation::create() -> void { } })); 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(); }); - speedFast.setText("150% (Fast)").setProperty("multiplier", "0.667").onActivate([&] { program.updateAudioFrequency(); }); - speedFastest.setText("200% (Fastest)").setProperty("multiplier", "0.5").onActivate([&] { program.updateAudioFrequency(); }); + speedSlowest.setText("50% (Slowest)").setAttribute("multiplier", "2.0").onActivate([&] { program.updateAudioFrequency(); }); + speedSlow.setText("75% (Slow)").setAttribute("multiplier", "1.333").onActivate([&] { program.updateAudioFrequency(); }); + speedNormal.setText("100% (Normal)").setAttribute("multiplier", "1.0").onActivate([&] { program.updateAudioFrequency(); }); + speedFast.setText("150% (Fast)").setAttribute("multiplier", "0.667").onActivate([&] { program.updateAudioFrequency(); }); + speedFastest.setText("200% (Fastest)").setAttribute("multiplier", "0.5").onActivate([&] { program.updateAudioFrequency(); }); runMenu.setIcon(Icon::Media::Play).setText("Run Mode"); runEmulation.setText("Normal").onActivate([&] { }); @@ -335,7 +335,7 @@ auto Presentation::updateDeviceMenu() -> void { if(port.name == "Expansion Port" && device.name == "21fx") continue; MenuRadioItem item{menu}; - item.setProperty("deviceID", device.id); + item.setAttribute("deviceID", device.id); item.setText(tr(device.name)); item.onActivate([=] { settings(path).setValue(device.name); @@ -363,7 +363,7 @@ auto Presentation::updateDeviceSelections() -> void { auto deviceID = emulator->connected(port.id); for(auto& action : menu->actions()) { if(auto item = action.cast()) { - if(item.property("deviceID").natural() == deviceID) { + if(item.attribute("deviceID").natural() == deviceID) { item.setChecked(); break; } @@ -385,7 +385,7 @@ auto Presentation::updateSizeMenu() -> void { uint multipliers = max(1, height / 240); for(uint multiplier : range(1, multipliers + 1)) { MenuRadioItem item{&sizeMenu}; - item.setProperty("multiplier", multiplier); + item.setAttribute("multiplier", multiplier); item.setText({multiplier, "x (", 240 * multiplier, "p)"}); item.onActivate([=] { settings.video.multiplier = multiplier; @@ -394,7 +394,7 @@ auto Presentation::updateSizeMenu() -> void { sizeGroup.append(item); } for(auto item : sizeGroup.objects()) { - if(settings.video.multiplier == item.property("multiplier").natural()) { + if(settings.video.multiplier == item.attribute("multiplier").natural()) { item.setChecked(); } } @@ -413,11 +413,11 @@ auto Presentation::updateStateMenus() -> void { for(auto& action : saveState.actions()) { if(auto item = action.cast()) { - if(auto name = item.property("name")) { + if(auto name = item.attribute("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.attribute("title"), " (", chrono::local::datetime(states[*offset].date), ")"}); } else { - item.setText({item.property("title"), " (empty)"}); + item.setText({item.attribute("title"), " (empty)"}); } } } @@ -425,13 +425,13 @@ auto Presentation::updateStateMenus() -> void { for(auto& action : loadState.actions()) { if(auto item = action.cast()) { - if(auto name = item.property("name")) { + if(auto name = item.attribute("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.attribute("title"), " (", chrono::local::datetime(states[*offset].date), ")"}); } else { item.setEnabled(false); - item.setText({item.property("title"), " (empty)"}); + item.setText({item.attribute("title"), " (empty)"}); } } } diff --git a/bsnes/target-bsnes/program/audio.cpp b/bsnes/target-bsnes/program/audio.cpp index 1644eb11..8950b0e1 100644 --- a/bsnes/target-bsnes/program/audio.cpp +++ b/bsnes/target-bsnes/program/audio.cpp @@ -56,7 +56,7 @@ auto Program::updateAudioFrequency() -> void { 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(); + if(item.checked()) frequency *= item.attribute("multiplier").real(); } } Emulator::audio.setFrequency(frequency); diff --git a/bsnes/target-bsnes/settings/enhancements.cpp b/bsnes/target-bsnes/settings/enhancements.cpp index 7904c834..aafa8d37 100644 --- a/bsnes/target-bsnes/settings/enhancements.cpp +++ b/bsnes/target-bsnes/settings/enhancements.cpp @@ -54,19 +54,19 @@ auto EnhancementSettings::create() -> void { mode7Label.setText("HD Mode 7 (fast PPU only)").setFont(Font().setBold()); mode7ScaleLabel.setText("Scale:"); - mode7Scale.append(ComboButtonItem().setText( "240p").setProperty("multiplier", 1)); - mode7Scale.append(ComboButtonItem().setText( "480p").setProperty("multiplier", 2)); - mode7Scale.append(ComboButtonItem().setText( "720p").setProperty("multiplier", 3)); - mode7Scale.append(ComboButtonItem().setText( "960p").setProperty("multiplier", 4)); - mode7Scale.append(ComboButtonItem().setText("1200p").setProperty("multiplier", 5)); - mode7Scale.append(ComboButtonItem().setText("1440p").setProperty("multiplier", 6)); - mode7Scale.append(ComboButtonItem().setText("1680p").setProperty("multiplier", 7)); - mode7Scale.append(ComboButtonItem().setText("1920p").setProperty("multiplier", 8)); + mode7Scale.append(ComboButtonItem().setText( "240p").setAttribute("multiplier", 1)); + mode7Scale.append(ComboButtonItem().setText( "480p").setAttribute("multiplier", 2)); + mode7Scale.append(ComboButtonItem().setText( "720p").setAttribute("multiplier", 3)); + mode7Scale.append(ComboButtonItem().setText( "960p").setAttribute("multiplier", 4)); + mode7Scale.append(ComboButtonItem().setText("1200p").setAttribute("multiplier", 5)); + mode7Scale.append(ComboButtonItem().setText("1440p").setAttribute("multiplier", 6)); + mode7Scale.append(ComboButtonItem().setText("1680p").setAttribute("multiplier", 7)); + mode7Scale.append(ComboButtonItem().setText("1920p").setAttribute("multiplier", 8)); for(uint n = 1; n <= 8; n++) { if(settings.emulator.hack.ppu.mode7.scale == n) mode7Scale.item(n - 1).setSelected(); } mode7Scale.onChange([&] { - settings.emulator.hack.ppu.mode7.scale = mode7Scale.selected().property("multiplier").natural(); + settings.emulator.hack.ppu.mode7.scale = mode7Scale.selected().attribute("multiplier").natural(); emulator->configure("Hacks/PPU/Mode7/Scale", settings.emulator.hack.ppu.mode7.scale); }); mode7Perspective.setText("Perspective correction").setChecked(settings.emulator.hack.ppu.mode7.perspective).onToggle([&] { diff --git a/bsnes/target-bsnes/settings/input.cpp b/bsnes/target-bsnes/settings/input.cpp index 211940b6..a056445a 100644 --- a/bsnes/target-bsnes/settings/input.cpp +++ b/bsnes/target-bsnes/settings/input.cpp @@ -87,7 +87,7 @@ auto InputSettings::activePort() -> InputPort& { } auto InputSettings::activeDevice() -> InputDevice& { - auto index = deviceList.selected().property("index").natural(); + auto index = deviceList.selected().attribute("index").natural(); return activePort().devices[index]; } @@ -105,7 +105,7 @@ auto InputSettings::reloadDevices() -> void { uint index = 0; for(auto& device : activePort().devices) { if(device.mappings) { //only display devices that have configurable inputs - deviceList.append(ComboButtonItem().setText(device.name).setProperty("index", index)); + deviceList.append(ComboButtonItem().setText(device.name).setAttribute("index", index)); } index++; } diff --git a/bsnes/target-bsnes/tools/cheat-editor.cpp b/bsnes/target-bsnes/tools/cheat-editor.cpp index 8214d5b3..06e7ca8f 100644 --- a/bsnes/target-bsnes/tools/cheat-editor.cpp +++ b/bsnes/target-bsnes/tools/cheat-editor.cpp @@ -38,7 +38,7 @@ auto CheatDatabase::findCheats() -> void { cheatList.append(ListViewItem() .setCheckable() .setText(cheat["description"].text()) - .setProperty("code", code) + .setAttribute("code", code) ); } @@ -53,7 +53,7 @@ auto CheatDatabase::findCheats() -> void { auto CheatDatabase::addCheats() -> void { for(auto item : cheatList.items()) { if(item.checked()) { - cheatEditor.addCheat({item.text(), item.property("code"), false}); + cheatEditor.addCheat({item.text(), item.attribute("code"), false}); } } setVisible(false); diff --git a/bsnes/target-bsnes/tools/manifest-viewer.cpp b/bsnes/target-bsnes/tools/manifest-viewer.cpp index c5cf3613..9477b19a 100644 --- a/bsnes/target-bsnes/tools/manifest-viewer.cpp +++ b/bsnes/target-bsnes/tools/manifest-viewer.cpp @@ -25,7 +25,7 @@ auto ManifestViewer::loadManifest() -> void { auto titles = emulator->titles(); for(uint offset : range(manifests.size())) { ComboButtonItem item{&manifestOption}; - item.setProperty("manifest", manifests[offset]); + item.setAttribute("manifest", manifests[offset]); item.setText(titles[offset]); bool verified = false; if(offset == 0) verified = program.superFamicom.verified; @@ -41,7 +41,7 @@ auto ManifestViewer::loadManifest() -> void { auto ManifestViewer::selectManifest() -> void { auto selected = manifestOption.selected(); uint offset = selected->offset(); - manifestView.setText(selected.property("manifest")); + manifestView.setText(selected.attribute("manifest")); string location; if(offset == 0) location = program.superFamicom.location; if(offset == 1 && program.gameBoy) location = program.gameBoy.location; diff --git a/bsnes/target-bsnes/tools/state-manager.cpp b/bsnes/target-bsnes/tools/state-manager.cpp index ab72067f..a12895e1 100644 --- a/bsnes/target-bsnes/tools/state-manager.cpp +++ b/bsnes/target-bsnes/tools/state-manager.cpp @@ -13,31 +13,31 @@ auto StateWindow::create() -> void { } auto StateWindow::show(string name) -> void { - setProperty("type", {name.split("/").first(), "/"}); - setProperty("name", {name.split("/").last()}); - nameValue.setText(property("name")); + setAttribute("type", {name.split("/").first(), "/"}); + setAttribute("name", {name.split("/").last()}); + nameValue.setText(attribute("name")); doChange(); - setTitle(!property("name") ? "Add State" : "Rename State"); + setTitle(!attribute("name") ? "Add State" : "Rename State"); setAlignment(*toolsWindow); setVisible(); setFocused(); nameValue.setFocused(); - acceptButton.setText(!property("name") ? "Add" : "Rename"); + acceptButton.setText(!attribute("name") ? "Add" : "Rename"); } auto StateWindow::doChange() -> void { auto name = nameValue.text().strip(); bool valid = name && !name.contains("\\\"\t/:*?<>|"); - if(property("name") && name != property("name")) { + if(attribute("name") && name != attribute("name")) { //don't allow a state to be renamed to the same name as an existing state (as this would overwrite it) - if(program.hasState({property("type"), name})) valid = false; + if(program.hasState({attribute("type"), name})) valid = false; } nameValue.setBackgroundColor(valid ? Color{} : Color{255, 224, 224}); acceptButton.setEnabled(valid); } auto StateWindow::doAccept() -> void { - string name = {property("type"), nameValue.text().strip()}; + string name = {attribute("type"), nameValue.text().strip()}; if(acceptButton.text() == "Add") { stateManager.createState(name); } else { @@ -64,23 +64,23 @@ auto StateManager::create() -> void { stateList.resizeColumns(); }); categoryLabel.setText("Category:"); - categoryOption.append(ComboButtonItem().setText("Managed States").setProperty("type", "Managed/")); - categoryOption.append(ComboButtonItem().setText("Quick States").setProperty("type", "Quick/")); + categoryOption.append(ComboButtonItem().setText("Managed States").setAttribute("type", "Managed/")); + categoryOption.append(ComboButtonItem().setText("Quick States").setAttribute("type", "Quick/")); categoryOption.onChange([&] { loadStates(); }); statePreviewSeparator1.setColor({192, 192, 192}); statePreviewLabel.setFont(Font().setBold()).setText("Preview"); statePreviewSeparator2.setColor({192, 192, 192}); loadButton.setText("Load").onActivate([&] { - if(auto item = stateList.selected()) program.loadState(item.property("name")); + if(auto item = stateList.selected()) program.loadState(item.attribute("name")); }); saveButton.setText("Save").onActivate([&] { - if(auto item = stateList.selected()) program.saveState(item.property("name")); + if(auto item = stateList.selected()) program.saveState(item.attribute("name")); }); addButton.setText("Add").onActivate([&] { stateWindow.show(type()); }); editButton.setText("Rename").onActivate([&] { - if(auto item = stateList.selected()) stateWindow.show(item.property("name")); + if(auto item = stateList.selected()) stateWindow.show(item.attribute("name")); }); removeButton.setText("Remove").onActivate([&] { removeStates(); @@ -88,7 +88,7 @@ auto StateManager::create() -> void { } auto StateManager::type() const -> string { - return categoryOption.selected().property("type"); + return categoryOption.selected().attribute("type"); } auto StateManager::loadStates() -> void { @@ -98,7 +98,7 @@ auto StateManager::loadStates() -> void { auto type = this->type(); for(auto& state : program.availableStates(type)) { TableViewItem item{&stateList}; - item.setProperty("name", state.name); + item.setAttribute("name", state.name); item.append(TableViewCell().setText(state.name.trimLeft(type, 1L))); item.append(TableViewCell().setText(chrono::local::datetime(state.date))); } @@ -111,19 +111,19 @@ auto StateManager::createState(string name) -> void { } program.saveState(name); for(auto& item : stateList.items()) { - item.setSelected(item.property("name") == name); + item.setSelected(item.attribute("name") == name); } stateList.doChange(); } auto StateManager::modifyState(string name) -> void { if(auto item = stateList.selected()) { - string from = item.property("name"); + string from = item.attribute("name"); string to = name; if(from != to) { program.renameState(from, to); for(auto& item : stateList.items()) { - item.setSelected(item.property("name") == to); + item.setSelected(item.attribute("name") == to); } stateList.doChange(); } @@ -135,7 +135,7 @@ auto StateManager::removeStates() -> void { if(MessageDialog("Are you sure you want to permanently remove the selected state(s)?") .setAlignment(*toolsWindow).question() == "Yes") { auto lock = acquire(); - for(auto& item : batched) program.removeState(item.property("name")); + for(auto& item : batched) program.removeState(item.attribute("name")); loadStates(); } } @@ -153,7 +153,7 @@ auto StateManager::updateSelection() -> void { statePreview.setColor({0, 0, 0}); if(batched.size() == 1) { - if(auto saveState = program.loadStateData(batched.first().property("name"))) { + if(auto saveState = program.loadStateData(batched.first().attribute("name"))) { if(saveState.size() >= 3 * sizeof(uint)) { uint signature = memory::readl(saveState.data() + 0 * sizeof(uint)); uint serializer = memory::readl(saveState.data() + 1 * sizeof(uint)); @@ -182,10 +182,10 @@ auto StateManager::updateSelection() -> void { auto StateManager::stateEvent(string name) -> void { if(locked() || !name.beginsWith(type())) return; - auto selected = stateList.selected().property("name"); + auto selected = stateList.selected().attribute("name"); loadStates(); for(auto& item : stateList.items()) { - item.setSelected(item.property("name") == selected); + item.setSelected(item.attribute("name") == selected); } stateList.doChange(); } diff --git a/hiro/components.hpp b/hiro/components.hpp index 86b66587..3f387181 100644 --- a/hiro/components.hpp +++ b/hiro/components.hpp @@ -27,7 +27,7 @@ #define Hiro_BrowserWindow #define Hiro_MessageWindow -#define Hiro_Property +#define Hiro_Attribute #define Hiro_Object #define Hiro_Group diff --git a/hiro/core/attribute.cpp b/hiro/core/attribute.cpp new file mode 100644 index 00000000..952b1665 --- /dev/null +++ b/hiro/core/attribute.cpp @@ -0,0 +1,29 @@ +#if defined(Hiro_Attribute) + +Attribute::Attribute(const string& name, const any& value) { + state.name = name; + state.value = value; +} + +auto Attribute::operator==(const Attribute& source) const -> bool { + return state.name == source.state.name; +} + +auto Attribute::operator<(const Attribute& source) const -> bool { + return state.name < source.state.name; +} + +auto Attribute::name() const -> string { + return state.name; +} + +auto Attribute::setValue(const any& value) -> type& { + state.value = value; + return *this; +} + +auto Attribute::value() const -> any& { + return state.value; +} + +#endif diff --git a/hiro/core/attribute.hpp b/hiro/core/attribute.hpp new file mode 100644 index 00000000..50dc3bea --- /dev/null +++ b/hiro/core/attribute.hpp @@ -0,0 +1,20 @@ +#if defined(Hiro_Attribute) +struct Attribute { + using type = Attribute; + + Attribute(const string& name, const any& value = {}); + + auto operator==(const Attribute& source) const -> bool; + auto operator< (const Attribute& source) const -> bool; + + auto name() const -> string; + auto setValue(const any& value = {}) -> type&; + auto value() const -> any&; + +private: + struct State { + string name; + mutable any value; + } state; +}; +#endif diff --git a/hiro/core/core.cpp b/hiro/core/core.cpp index 67f7a11c..84352228 100755 --- a/hiro/core/core.cpp +++ b/hiro/core/core.cpp @@ -48,7 +48,7 @@ namespace hiro { #include "browser-window.cpp" #include "message-window.cpp" - #include "property.cpp" + #include "attribute.cpp" #include "object.cpp" #include "group.cpp" diff --git a/hiro/core/core.hpp b/hiro/core/core.hpp index 3fef7e47..27d45276 100755 --- a/hiro/core/core.hpp +++ b/hiro/core/core.hpp @@ -352,6 +352,7 @@ struct Keyboard { #if defined(Hiro_Mouse) struct Mouse { enum class Button : uint { Left, Middle, Right }; + enum class Click : uint { Single, Double }; Mouse() = delete; @@ -410,7 +411,7 @@ struct MessageWindow { }; #endif -#include "property.hpp" +#include "attribute.hpp" #define Declare(Name) \ using type = m##Name; \ diff --git a/hiro/core/object.hpp b/hiro/core/object.hpp index a77d143f..ab56282b 100644 --- a/hiro/core/object.hpp +++ b/hiro/core/object.hpp @@ -43,45 +43,43 @@ struct mObject { virtual auto setVisible(bool visible = true) -> type&; auto visible(bool recursive = false) const -> bool; - template auto property(const string& name) const -> T { - if(auto property = state.properties.find(name)) { - if(property->value().is()) return property->value().get(); + template auto attribute(const string& name) const -> T { + if(auto attribute = state.attributes.find(name)) { + if(attribute->value().is()) return attribute->value().get(); } return {}; } //this template basically disables implicit template type deduction: - //if setProperty(name, value) is called without a type, the type will be a string, so property(name) will just work. - //if setProperty(name, value) is called, the type will be T. as such, U must be cast to T on assignment. + //if setAttribute(name, value) is called without a type, the type will be a string, so attribute(name) will just work. + //if setAttribute(name, value) is called, the type will be T. as such, U must be cast to T on assignment. //when T = string, value must be convertible to a string. - //U defaults to a string, so that setProperty(name, {values, ...}) will deduce U as a string. - template auto setProperty(const string& name, const U& value) -> type& { + //U defaults to a string, so that setAttribute(name, {values, ...}) will deduce U as a string. + template auto setAttribute(const string& name, const U& value) -> type& { if constexpr(std::is_same_v && !std::is_same_v) { - return setProperty(name, string{value}); + return setAttribute(name, string{value}); } - if(auto property = state.properties.find(name)) { - if((const T&)value) property->setValue((const T&)value); - else state.properties.remove(*property); + if(auto attribute = state.attributes.find(name)) { + if((const T&)value) attribute->setValue((const T&)value); + else state.attributes.remove(*attribute); } else { - if((const T&)value) state.properties.insert({name, (const T&)value}); + if((const T&)value) state.attributes.insert({name, (const T&)value}); } return *this; } //private: -//sizeof(mObject) == 88 struct State { - Font font; //32 - set properties; //16 - mObject* parent = nullptr; // 8 - int offset = -1; // 4 - char enabled = true; // 1+ - char visible = true; // 1=4 + set attributes; + Font font; + mObject* parent = nullptr; + int offset = -1; + char enabled = true; + char visible = true; } state; - wObject instance; // 8 - pObject* delegate = nullptr; // 8 - //vtable // 8 + wObject instance; + pObject* delegate = nullptr; virtual auto construct() -> void; virtual auto destruct() -> void; diff --git a/hiro/core/property.cpp b/hiro/core/property.cpp deleted file mode 100644 index 809fa2c1..00000000 --- a/hiro/core/property.cpp +++ /dev/null @@ -1,29 +0,0 @@ -#if defined(Hiro_Property) - -Property::Property(const string& name, const any& value) { - state.name = name; - state.value = value; -} - -auto Property::operator==(const Property& source) const -> bool { - return state.name == source.state.name; -} - -auto Property::operator<(const Property& source) const -> bool { - return state.name < source.state.name; -} - -auto Property::name() const -> string { - return state.name; -} - -auto Property::setValue(const any& value) -> type& { - state.value = value; - return *this; -} - -auto Property::value() const -> any& { - return state.value; -} - -#endif diff --git a/hiro/core/property.hpp b/hiro/core/property.hpp deleted file mode 100644 index c66cf480..00000000 --- a/hiro/core/property.hpp +++ /dev/null @@ -1,20 +0,0 @@ -#if defined(Hiro_Property) -struct Property { - using type = Property; - - Property(const string& name, const any& value = {}); - - auto operator==(const Property& source) const -> bool; - auto operator< (const Property& source) const -> bool; - - auto name() const -> string; - auto setValue(const any& value = {}) -> type&; - auto value() const -> any&; - -private: - struct State { - string name; - mutable any value; - } state; -}; -#endif diff --git a/hiro/core/shared.hpp b/hiro/core/shared.hpp index ed35228e..f376b0dd 100644 --- a/hiro/core/shared.hpp +++ b/hiro/core/shared.hpp @@ -28,6 +28,7 @@ } \ return T(); \ } \ + template auto attribute(const string& name) const { return self().attribute(name); } \ auto enabled(bool recursive = false) const { return self().enabled(recursive); } \ auto focused() const { return self().focused(); } \ auto font(bool recursive = false) const { return self().font(recursive); } \ @@ -38,12 +39,11 @@ } \ return Object(); \ } \ - template auto property(const string& name) const { return self().property(name); } \ auto remove() { return self().remove(), *this; } \ + template auto setAttribute(const string& name, const U& value = {}) { return self().setAttribute(name, value), *this; } \ auto setEnabled(bool enabled = true) { return self().setEnabled(enabled), *this; } \ auto setFocused() { return self().setFocused(), *this; } \ auto setFont(const Font& font = {}) { return self().setFont(font), *this; } \ - template auto setProperty(const string& name, const U& value = {}) { return self().setProperty(name, value), *this; } \ auto setVisible(bool visible = true) { return self().setVisible(visible), *this; } \ auto visible(bool recursive = false) const { return self().visible(recursive); } \ @@ -794,6 +794,7 @@ struct TreeViewItem : sTreeViewItem { struct TreeView : sTreeView { DeclareSharedWidget(TreeView) + auto activation() const { return self().activation(); } auto append(sTreeViewItem item) { return self().append(item), *this; } auto backgroundColor() const { return self().backgroundColor(); } auto collapse(bool recursive = true) { return self().collapse(recursive), *this; } @@ -814,6 +815,7 @@ struct TreeView : sTreeView { auto reset() { return self().reset(), *this; } auto selectNone() { return self().selectNone(), *this; } auto selected() const { return self().selected(); } + auto setActivation(Mouse::Click activation = Mouse::Click::Double) { return self().setActivation(activation), *this; } auto setBackgroundColor(Color color = {}) { return self().setBackgroundColor(color), *this; } auto setForegroundColor(Color color = {}) { return self().setForegroundColor(color), *this; } }; @@ -936,6 +938,7 @@ struct Window : sWindow { auto setFrameSize(Size size) { return self().setFrameSize(size), *this; } auto setFullScreen(bool fullScreen = true) { return self().setFullScreen(fullScreen), *this; } auto setGeometry(Geometry geometry) { return self().setGeometry(geometry), *this; } + auto setGeometry(Alignment alignment, Size size) { return self().setGeometry(alignment, size), *this; } auto setMaximized(bool maximized) { return self().setMaximized(maximized), *this; } auto setMaximumSize(Size size = {}) { return self().setMaximumSize(size), *this; } auto setMinimized(bool minimized) { return self().setMinimized(minimized), *this; } diff --git a/hiro/core/widget/tree-view.cpp b/hiro/core/widget/tree-view.cpp index aa781393..07ae5ab5 100755 --- a/hiro/core/widget/tree-view.cpp +++ b/hiro/core/widget/tree-view.cpp @@ -11,6 +11,10 @@ auto mTreeView::destruct() -> void { // +auto mTreeView::activation() const -> Mouse::Click { + return state.activation; +} + auto mTreeView::append(sTreeViewItem item) -> type& { state.items.append(item); item->setParent(this, itemCount() - 1); @@ -119,6 +123,12 @@ auto mTreeView::selected() const -> TreeViewItem { return item(state.selectedPath); } +auto mTreeView::setActivation(Mouse::Click activation) -> type& { + state.activation = activation; + signal(setActivation, activation); + return *this; +} + auto mTreeView::setBackgroundColor(Color color) -> type& { state.backgroundColor = color; signal(setBackgroundColor, color); diff --git a/hiro/core/widget/tree-view.hpp b/hiro/core/widget/tree-view.hpp index 83c6890f..782f8db7 100644 --- a/hiro/core/widget/tree-view.hpp +++ b/hiro/core/widget/tree-view.hpp @@ -3,6 +3,7 @@ struct mTreeView : mWidget { Declare(TreeView) using mObject::remove; + auto activation() const -> Mouse::Click; auto append(sTreeViewItem item) -> type&; auto backgroundColor() const -> Color; auto collapse(bool recursive = true) -> type&; @@ -23,12 +24,14 @@ struct mTreeView : mWidget { auto reset() -> type&; auto selectNone() -> type&; auto selected() const -> TreeViewItem; + auto setActivation(Mouse::Click activation = Mouse::Click::Double) -> type&; auto setBackgroundColor(Color color = {}) -> type&; auto setForegroundColor(Color color = {}) -> type&; auto setParent(mObject* parent = nullptr, int offset = -1) -> type&; //private: struct State { + Mouse::Click activation = Mouse::Click::Double; Color backgroundColor; Color foregroundColor; vector items; diff --git a/hiro/core/window.cpp b/hiro/core/window.cpp index 4ed9523f..7e37ab3d 100755 --- a/hiro/core/window.cpp +++ b/hiro/core/window.cpp @@ -279,6 +279,17 @@ auto mWindow::setGeometry(Geometry geometry) -> type& { return *this; } +auto mWindow::setGeometry(Alignment alignment, Size size) -> type& { + auto margin = signal(frameMargin); + auto width = margin.width() + size.width(); + auto height = margin.height() + size.height(); + auto workspace = Monitor::workspace(); + auto x = workspace.x() + alignment.horizontal() * (workspace.width() - width); + auto y = workspace.y() + alignment.vertical() * (workspace.height() - height); + setFrameGeometry({(int)x, (int)y, (int)width, (int)height}); + return *this; +} + auto mWindow::setMaximized(bool maximized) -> type& { state.maximized = maximized; signal(setMaximized, maximized); diff --git a/hiro/core/window.hpp b/hiro/core/window.hpp index 45000235..07b6e4c1 100644 --- a/hiro/core/window.hpp +++ b/hiro/core/window.hpp @@ -49,6 +49,7 @@ struct mWindow : mObject { auto setFrameSize(Size size) -> type&; auto setFullScreen(bool fullScreen = true) -> type&; auto setGeometry(Geometry geometry) -> type&; + auto setGeometry(Alignment, Size) -> type&; auto setMaximized(bool maximized = true) -> type&; auto setMaximumSize(Size size = {}) -> type&; auto setMinimized(bool minimized = true) -> type&; diff --git a/hiro/extension/browser-dialog.cpp b/hiro/extension/browser-dialog.cpp index 1dbca658..2f958c5e 100755 --- a/hiro/extension/browser-dialog.cpp +++ b/hiro/extension/browser-dialog.cpp @@ -359,7 +359,7 @@ auto BrowserDialogWindow::run() -> BrowserDialog::Response { window.setAlignment(state.relativeTo, state.alignment); window.setDismissable(); window.setVisible(); - fileName.setFocused(); + fileName.setText(state.name).setFocused().doChange(); Application::processEvents(); view->resizeColumns(); window.setModal(); @@ -480,6 +480,11 @@ auto BrowserDialog::setFilters(const vector& filters) -> type& { return *this; } +auto BrowserDialog::setName(const string& name) -> type& { + state.name = name; + return *this; +} + auto BrowserDialog::setOptions(const vector& options) -> type& { state.options = options; return *this; diff --git a/hiro/extension/browser-dialog.hpp b/hiro/extension/browser-dialog.hpp index dbba5c39..00343909 100755 --- a/hiro/extension/browser-dialog.hpp +++ b/hiro/extension/browser-dialog.hpp @@ -17,6 +17,7 @@ struct BrowserDialog { auto setAlignment(Alignment = Alignment::Center) -> type&; auto setAlignment(sWindow relativeTo, Alignment = Alignment::Center) -> type&; auto setFilters(const vector& filters = {}) -> type&; + auto setName(const string& name = "") -> type&; auto setOptions(const vector& options = {}) -> type&; auto setPath(const string& path = "") -> type&; auto setTitle(const string& title = "") -> type&; @@ -26,6 +27,7 @@ private: string action; Alignment alignment = Alignment::Center; vector filters = {"*"}; + string name; vector options; string path; sWindow relativeTo; diff --git a/hiro/gtk/widget/table-view.cpp b/hiro/gtk/widget/table-view.cpp index 750038e3..38b8354f 100755 --- a/hiro/gtk/widget/table-view.cpp +++ b/hiro/gtk/widget/table-view.cpp @@ -200,13 +200,13 @@ auto pTableView::_cellWidth(unsigned _row, unsigned _column) -> unsigned { } auto pTableView::_columnWidth(unsigned _column) -> unsigned { - unsigned width = 8; + unsigned width = 6; if(auto column = self().column(_column)) { if(auto& icon = column->state.icon) { width += icon.width() + 2; } if(auto& text = column->state.text) { - width += pFont::size(column->font(true), text).width(); + width += pFont::size(column->font(true), text).width() + 8; } if(column->state.sorting != Sort::None) { width += 20; diff --git a/hiro/gtk/widget/tree-view.cpp b/hiro/gtk/widget/tree-view.cpp index 0accd4e4..fd0d6e4c 100755 --- a/hiro/gtk/widget/tree-view.cpp +++ b/hiro/gtk/widget/tree-view.cpp @@ -12,6 +12,7 @@ static auto TreeView_buttonEvent(GtkTreeView*, GdkEventButton* gdkEvent, pTreeVi static auto TreeView_change(GtkTreeSelection*, pTreeView* p) -> void { p->_updateSelected(); } static auto TreeView_context(GtkTreeView*, pTreeView* p) -> void { p->self().doContext(); } static auto TreeView_dataFunc(GtkTreeViewColumn* column, GtkCellRenderer* renderer, GtkTreeModel* model, GtkTreeIter* iter, pTreeView* p) -> void { return p->_doDataFunc(column, renderer, iter); } +static auto TreeView_keyPress(GtkWidget*, GdkEventKey*, pTreeView* p) -> int { p->suppressActivate = false; return false; } static auto TreeView_toggle(GtkCellRendererToggle*, char* path, pTreeView* p) -> void { p->_togglePath(path); } auto pTreeView::construct() -> void { @@ -52,11 +53,13 @@ auto pTreeView::construct() -> void { gtk_tree_view_append_column(gtkTreeView, gtkTreeViewColumn); gtk_tree_view_set_search_column(gtkTreeView, 2); + setActivation(state().activation); setBackgroundColor(state().backgroundColor); setForegroundColor(state().foregroundColor); g_signal_connect(G_OBJECT(gtkWidgetChild), "button-press-event", G_CALLBACK(TreeView_buttonEvent), (gpointer)this); g_signal_connect(G_OBJECT(gtkWidgetChild), "button-release-event", G_CALLBACK(TreeView_buttonEvent), (gpointer)this); + g_signal_connect(G_OBJECT(gtkWidgetChild), "key-press-event", G_CALLBACK(TreeView_keyPress), (gpointer)this); g_signal_connect(G_OBJECT(gtkWidgetChild), "popup-menu", G_CALLBACK(TreeView_context), (gpointer)this); g_signal_connect(G_OBJECT(gtkWidgetChild), "row-activated", G_CALLBACK(TreeView_activate), (gpointer)this); g_signal_connect(G_OBJECT(gtkTreeSelection), "changed", G_CALLBACK(TreeView_change), (gpointer)this); @@ -87,6 +90,10 @@ auto pTreeView::append(sTreeViewItem item) -> void { auto pTreeView::remove(sTreeViewItem item) -> void { } +auto pTreeView::setActivation(Mouse::Click activation) -> void { + //handled by callbacks +} + auto pTreeView::setBackgroundColor(Color color) -> void { auto gdkColor = CreateColor(color); gtk_widget_modify_base(gtkWidgetChild, GTK_STATE_NORMAL, color ? &gdkColor : nullptr); @@ -115,6 +122,11 @@ auto pTreeView::setGeometry(Geometry geometry) -> void { // auto pTreeView::_activatePath(GtkTreePath* gtkPath) -> void { + if(suppressActivate) { + suppressActivate = false; + return; + } + char* path = gtk_tree_path_to_string(gtkPath); if(auto item = self().item(string{path}.transform(":", "/"))) { if(!locked()) self().doActivate(); @@ -140,6 +152,25 @@ auto pTreeView::_buttonEvent(GdkEventButton* gdkEvent) -> signed { } } + if(gdkEvent->button == 1) { + //emulate activate-on-single-click, which is only available in GTK+ 3.8 and later + if(gtkPath && self().activation() == Mouse::Click::Single) { + //selectedPath must be updated for TreeView::doActivate() to act on the correct TreeViewItem. + //as this will then cause "changed" to not see that the path has changed, we must handle that case here as well. + char* path = gtk_tree_path_to_string(gtkPath); + string selectedPath = string{path}.transform(":", "/"); + g_free(path); + if(state().selectedPath != selectedPath) { + state().selectedPath = selectedPath; + self().doChange(); + } + self().doActivate(); + //"row-activated" is sent before "button-press-event" (GDK_2BUTTON_PRESS); + //so stop a double-click from calling TreeView::doActivate() twice by setting a flag after single-clicks + suppressActivate = true; //key presses will clear this flag to allow key-activations to work correctly + } + } + if(gdkEvent->button == 3) { //multi-selection mode: (not implemented in TreeView yet ... but code is here anyway for future use) //if multiple items are selected, and one item is right-clicked on (for a context menu), GTK clears selection on all other items diff --git a/hiro/gtk/widget/tree-view.hpp b/hiro/gtk/widget/tree-view.hpp index 96002895..fb9e3449 100755 --- a/hiro/gtk/widget/tree-view.hpp +++ b/hiro/gtk/widget/tree-view.hpp @@ -7,6 +7,7 @@ struct pTreeView : pWidget { auto append(sTreeViewItem item) -> void; auto remove(sTreeViewItem item) -> void; + auto setActivation(Mouse::Click activation) -> void; auto setBackgroundColor(Color color) -> void; auto setFocused() -> void override; auto setForegroundColor(Color color) -> void; @@ -32,6 +33,7 @@ struct pTreeView : pWidget { GtkCellRenderer* gtkCellPixbuf = nullptr; GtkCellRenderer* gtkCellText = nullptr; GtkEntry* gtkEntry = nullptr; + bool suppressActivate = false; bool suppressChange = false; }; diff --git a/hiro/gtk/window.cpp b/hiro/gtk/window.cpp index a04ab17a..eaad2000 100755 --- a/hiro/gtk/window.cpp +++ b/hiro/gtk/window.cpp @@ -317,13 +317,15 @@ auto pWindow::setFullScreen(bool fullScreen) -> void { } else { gtk_window_unfullscreen(GTK_WINDOW(widget)); } + auto time = chrono::millisecond(); - while(chrono::millisecond() - time < 20) Application::processEvents(); + while(chrono::millisecond() - time < 20) { + Application::processEvents(); + } } auto pWindow::setGeometry(Geometry geometry) -> void { auto margin = frameMargin(); - gtk_window_move(GTK_WINDOW(widget), geometry.x() - margin.x(), geometry.y() - margin.y()); setMaximumSize(state().maximumSize); setMinimumSize(state().minimumSize); @@ -333,10 +335,13 @@ auto pWindow::setGeometry(Geometry geometry) -> void { Application::processEvents(); } + gtk_window_move(GTK_WINDOW(widget), geometry.x() - margin.x(), geometry.y() - margin.y()); gtk_window_resize(GTK_WINDOW(widget), geometry.width(), geometry.height() + _menuHeight() + _statusHeight()); auto time2 = chrono::millisecond(); - while(chrono::millisecond() - time2 < 20) Application::processEvents(); + while(chrono::millisecond() - time2 < 20) { + Application::processEvents(); + } } auto pWindow::setMaximized(bool maximized) -> void { diff --git a/hiro/windows/widget/combo-button.cpp b/hiro/windows/widget/combo-button.cpp index 65ef68d4..f219e977 100755 --- a/hiro/windows/widget/combo-button.cpp +++ b/hiro/windows/widget/combo-button.cpp @@ -44,7 +44,13 @@ auto pComboButton::reset() -> void { SendMessage(hwnd, CB_RESETCONTENT, 0, 0); } +//Windows overrides the height parameter for a ComboButton's SetWindowPos to be the drop-down list height. +//the canonical way to set the actual height is through CB_SETITEMHEIGHT. However, doing so is bugged. +//the ComboButton will end up not being painted for ~500ms after calling ShowWindow(hwnd, SW_NORMAL) on it. +//thus, implementing windows that use multiple pages of controls via toggling visibility will flicker heavily. +//as a result, the best we can do is center the actual widget within the requested space. auto pComboButton::setGeometry(Geometry geometry) -> void { + //since the ComboButton has a fixed height, it will always be the same, even before calling setGeometry() once. RECT rc; GetWindowRect(hwnd, &rc); geometry.setY(geometry.y() + (geometry.height() - (rc.bottom - rc.top))); diff --git a/nall/random.hpp b/nall/random.hpp index 66571e4d..5b7b3c2c 100755 --- a/nall/random.hpp +++ b/nall/random.hpp @@ -5,7 +5,9 @@ #include #include #include +#if !defined(PLATFORM_ANDROID) #include +#endif #if defined(PLATFORM_LINUX) && __has_include() #include @@ -125,6 +127,7 @@ private: } +#if !defined(PLATFORM_ANDROID) namespace CSPRNG { //XChaCha20 cryptographically secure pseudo-random number generator @@ -153,6 +156,7 @@ private: }; } +#endif // diff --git a/ruby/GNUmakefile b/ruby/GNUmakefile index d67aa3ef..1450daf5 100755 --- a/ruby/GNUmakefile +++ b/ruby/GNUmakefile @@ -6,7 +6,7 @@ ifeq ($(ruby),) else ifeq ($(platform),macos) ruby += video.cgl ruby += audio.openal - ruby += input.quartz input.carbon + ruby += input.quartz #input.carbon else ifeq ($(platform),linux) ruby += video.glx video.glx2 video.xvideo video.xshm ruby += audio.oss audio.alsa audio.openal audio.pulseaudio audio.pulseaudiosimple audio.ao diff --git a/ruby/input/joypad/iokit.cpp b/ruby/input/joypad/iokit.cpp new file mode 100644 index 00000000..0df2e570 --- /dev/null +++ b/ruby/input/joypad/iokit.cpp @@ -0,0 +1,278 @@ +#pragma once + +#include + +auto deviceMatchingCallback(void* context, IOReturn result, void* sender, IOHIDDeviceRef device) -> void; +auto deviceRemovalCallback(void* context, IOReturn result, void* sender, IOHIDDeviceRef device) -> void; + +struct InputJoypadIOKit { + Input& input; + InputJoypadIOKit(Input& input) : input(input) {} + + struct Joypad { + auto appendElements(CFArrayRef elements) -> void { + for(uint n : range(CFArrayGetCount(elements))) { + IOHIDElementRef element = (IOHIDElementRef)CFArrayGetValueAtIndex(elements, n); + IOHIDElementType type = IOHIDElementGetType(element); + uint32_t usage = IOHIDElementGetUsage(element); + switch(type) { + case kIOHIDElementTypeInput_Button: + appendButton(element); + break; + case kIOHIDElementTypeInput_Axis: + case kIOHIDElementTypeInput_Misc: + if(usage == kHIDUsage_Sim_Accelerator || usage == kHIDUsage_Sim_Brake + || usage == kHIDUsage_Sim_Rudder || usage == kHIDUsage_Sim_Throttle + || usage == kHIDUsage_GD_X || usage == kHIDUsage_GD_Y || usage == kHIDUsage_GD_Z + || usage == kHIDUsage_GD_Rx || usage == kHIDUsage_GD_Ry || usage == kHIDUsage_GD_Rz + || usage == kHIDUsage_GD_Slider || usage == kHIDUsage_GD_Dial || usage == kHIDUsage_GD_Wheel + ) appendAxis(element); + if(usage == kHIDUsage_GD_DPadUp || usage == kHIDUsage_GD_DPadDown + || usage == kHIDUsage_GD_DPadLeft || usage == kHIDUsage_GD_DPadRight + || usage == kHIDUsage_GD_Start || usage == kHIDUsage_GD_Select + || usage == kHIDUsage_GD_SystemMainMenu + ) appendButton(element); + if(usage == kHIDUsage_GD_Hatswitch + ) appendHat(element); + break; + case kIOHIDElementTypeCollection: + if(CFArrayRef children = IOHIDElementGetChildren(element)) appendElements(children); + break; + } + } + } + + auto appendAxis(IOHIDElementRef element) -> void { + IOHIDElementCookie cookie = IOHIDElementGetCookie(element); + if(auto duplicate = axes.find([cookie](auto axis) { return IOHIDElementGetCookie(axis) == cookie; })) { + return; + } + + int min = IOHIDElementGetLogicalMin(element); + int max = IOHIDElementGetLogicalMax(element); + int range = max - min; + if(range == 0) return; + + hid->axes().append(axes.size()); + axes.append(element); + } + + auto appendHat(IOHIDElementRef element) -> void { + IOHIDElementCookie cookie = IOHIDElementGetCookie(element); + if(auto duplicate = hats.find([cookie](auto hat) { return IOHIDElementGetCookie(hat) == cookie; })) { + return; + } + + uint n = hats.size() * 2; + hid->hats().append(n + 0); + hid->hats().append(n + 1); + hats.append(element); + } + + auto appendButton(IOHIDElementRef element) -> void { + IOHIDElementCookie cookie = IOHIDElementGetCookie(element); + if(auto duplicate = buttons.find([cookie](auto button) { return IOHIDElementGetCookie(button) == cookie; })) { + return; + } + + hid->buttons().append(buttons.size()); + buttons.append(element); + } + + shared_pointer hid{new HID::Joypad}; + + IOHIDDeviceRef device = nullptr; + vector axes; + vector hats; + vector buttons; + }; + vector joypads; + IOHIDManagerRef manager = nullptr; + + enum : int { Center = 0, Up = -1, Down = +1, Left = -1, Right = +1 }; + + auto assign(shared_pointer hid, uint groupID, uint inputID, int16_t value) -> void { + auto& group = hid->group(groupID); + if(group.input(inputID).value() == value) return; + input.doChange(hid, groupID, inputID, group.input(inputID).value(), value); + group.input(inputID).setValue(value); + } + + auto poll(vector>& devices) -> void { + detectDevices(); //hotplug support + + for(auto& jp : joypads) { + IOHIDDeviceRef device = jp.device; + + for(uint n : range(jp.axes.size())) { + int value = 0; + IOHIDValueRef valueRef; + if(IOHIDDeviceGetValue(device, jp.axes[n], &valueRef) == kIOReturnSuccess) { + int min = IOHIDElementGetLogicalMin(jp.axes[n]); + int max = IOHIDElementGetLogicalMax(jp.axes[n]); + int range = max - min; + value = (IOHIDValueGetIntegerValue(valueRef) - min) * 65535LL / range - 32767; + } + assign(jp.hid, HID::Joypad::GroupID::Axis, n, sclamp<16>(value)); + } + + for(uint n : range(jp.hats.size())) { + int x = Center; + int y = Center; + IOHIDValueRef valueRef; + if(IOHIDDeviceGetValue(device, jp.hats[n], &valueRef) == kIOReturnSuccess) { + int position = IOHIDValueGetIntegerValue(valueRef); + int min = IOHIDElementGetLogicalMin(jp.hats[n]); + int max = IOHIDElementGetLogicalMax(jp.hats[n]); + if(position >= min && position <= max) { + position -= min; + int range = max - min + 1; + if(range == 4) { + position *= 2; + } + if(range == 8) { + switch(position) { + case 0: x = Up; y = Center; break; + case 1: x = Up; y = Right; break; + case 2: x = Center; y = Right; break; + case 3: x = Down; y = Right; break; + case 4: x = Down; y = Center; break; + case 5: x = Down; y = Left; break; + case 6: x = Center; y = Left; break; + case 7: x = Up; y = Left; break; + } + } + } + } + assign(jp.hid, HID::Joypad::GroupID::Hat, n * 2 + 0, x * 32767); + assign(jp.hid, HID::Joypad::GroupID::Hat, n * 2 + 1, x * 32767); + } + + for(uint n : range(jp.buttons.size())) { + int value = 0; + IOHIDValueRef valueRef; + if(IOHIDDeviceGetValue(device, jp.buttons[n], &valueRef) == kIOReturnSuccess) { + value = IOHIDValueGetIntegerValue(valueRef); + } + assign(jp.hid, HID::Joypad::GroupID::Button, n, (bool)value); + } + + devices.append(jp.hid); + } + } + + auto rumble(uint64_t id, bool enable) -> bool { + //todo + return false; + } + + auto initialize() -> bool { + manager = IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDOptionsTypeNone); + if(!manager) return false; + + CFArrayRef matcher = createMatcher(); + if(!matcher) { + releaseManager(); + return false; + } + + IOHIDManagerSetDeviceMatchingMultiple(manager, matcher); + IOHIDManagerRegisterDeviceMatchingCallback(manager, deviceMatchingCallback, this); + IOHIDManagerRegisterDeviceRemovalCallback(manager, deviceRemovalCallback, this); + + if(IOHIDManagerOpen(manager, kIOHIDOptionsTypeNone) != kIOReturnSuccess) { + releaseManager(); + return false; + } + + detectDevices(); + return true; + } + + auto terminate() -> void { + if(!manager) return; + IOHIDManagerClose(manager, kIOHIDOptionsTypeNone); + releaseManager(); + } + + auto appendJoypad(IOHIDDeviceRef device) -> void { + Joypad jp; + jp.device = device; + + int32_t vendorID, productID; + CFNumberGetValue((CFNumberRef)IOHIDDeviceGetProperty(device, CFSTR(kIOHIDVendorIDKey)), kCFNumberSInt32Type, &vendorID); + CFNumberGetValue((CFNumberRef)IOHIDDeviceGetProperty(device, CFSTR(kIOHIDProductIDKey)), kCFNumberSInt32Type, &productID); + jp.hid->setVendorID(vendorID); + jp.hid->setProductID(productID); + + CFArrayRef elements = IOHIDDeviceCopyMatchingElements(device, nullptr, kIOHIDOptionsTypeNone); + if(elements) { + jp.appendElements(elements); + CFRelease(elements); + joypads.append(jp); + } + } + + auto removeJoypad(IOHIDDeviceRef device) -> void { + for(uint n : range(joypads.size())) { + if(joypads[n].device == device) { + joypads.remove(n); + return; + } + } + } + +private: + auto releaseManager() -> void { + CFRelease(manager); + manager = nullptr; + } + + auto createMatcher() -> CFArrayRef { + CFDictionaryRef dict1 = createMatcherCriteria(kHIDPage_GenericDesktop, kHIDUsage_GD_Joystick); + if(!dict1) return nullptr; + CFDictionaryRef dict2 = createMatcherCriteria(kHIDPage_GenericDesktop, kHIDUsage_GD_GamePad); + if(!dict2) return CFRelease(dict1), nullptr; + CFDictionaryRef dict3 = createMatcherCriteria(kHIDPage_GenericDesktop, kHIDUsage_GD_MultiAxisController); + if(!dict3) return CFRelease(dict1), CFRelease(dict2), nullptr; + + const void* values[] = {dict1, dict2, dict3}; + CFArrayRef array = CFArrayCreate(kCFAllocatorDefault, values, 3, &kCFTypeArrayCallBacks); + CFRelease(dict1), CFRelease(dict2), CFRelease(dict3); + return array; + } + + auto createMatcherCriteria(uint32_t page, uint32_t usage) -> CFDictionaryRef { + CFNumberRef pageNumber = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &page); + if(!pageNumber) return nullptr; + + CFNumberRef usageNumber = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &usage); + if(!usageNumber) return nullptr; + + const void* keys[] = {CFSTR(kIOHIDDeviceUsagePageKey), CFSTR(kIOHIDDeviceUsageKey)}; + const void* values[] = {pageNumber, usageNumber}; + + CFDictionaryRef dict = CFDictionaryCreate( + kCFAllocatorDefault, keys, values, 2, + &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks + ); + CFRelease(pageNumber), CFRelease(usageNumber); + return dict; + } + + auto detectDevices() -> void { + CFRunLoopRef runLoop = CFRunLoopGetCurrent(); + CFStringRef runLoopMode = CFSTR("rubyJoypadIOKit"); + IOHIDManagerScheduleWithRunLoop(manager, runLoop, runLoopMode); + while(CFRunLoopRunInMode(runLoopMode, 0, true) == kCFRunLoopRunHandledSource); + IOHIDManagerUnscheduleFromRunLoop(manager, runLoop, runLoopMode); + } +}; + +auto deviceMatchingCallback(void* context, IOReturn result, void* sender, IOHIDDeviceRef device) -> void { + ((InputJoypadIOKit*)context)->appendJoypad(device); +} + +auto deviceRemovalCallback(void* context, IOReturn result, void* sender, IOHIDDeviceRef device) -> void { + ((InputJoypadIOKit*)context)->removeJoypad(device); +} diff --git a/ruby/input/quartz.cpp b/ruby/input/quartz.cpp index e20b73f0..2c07a5b7 100644 --- a/ruby/input/quartz.cpp +++ b/ruby/input/quartz.cpp @@ -1,8 +1,9 @@ #include "keyboard/quartz.cpp" +#include "joypad/iokit.cpp" struct InputQuartz : InputDriver { InputQuartz& self = *this; - InputQuartz(Input& super) : InputDriver(super), keyboard(super) {} + InputQuartz(Input& super) : InputDriver(super), keyboard(super), joypad(super) {} ~InputQuartz() { terminate(); } auto create() -> bool override { @@ -19,6 +20,7 @@ struct InputQuartz : InputDriver { auto poll() -> vector> override { vector> devices; keyboard.poll(devices); + joypad.poll(devices); return devices; } @@ -30,14 +32,17 @@ private: auto initialize() -> bool { terminate(); if(!keyboard.initialize()) return false; + if(!joypad.initialize()) return false; return isReady = true; } auto terminate() -> void { isReady = false; keyboard.terminate(); + joypad.terminate(); } bool isReady = false; InputKeyboardQuartz keyboard; + InputJoypadIOKit joypad; };