mirror of
https://github.com/cemu-project/Cemu.git
synced 2025-07-06 23:11:18 +12:00
Add camera settings window and save camera config
This commit is contained in:
parent
791e358bf7
commit
b9000bd667
10 changed files with 284 additions and 59 deletions
|
@ -234,8 +234,7 @@ namespace camera
|
|||
return CAM_STATUS_UNINITIALIZED;
|
||||
if (s_instance.isOpen)
|
||||
return CAM_STATUS_DEVICE_IN_USE;
|
||||
if (!CameraManager::instance().Open(false))
|
||||
return CAM_STATUS_UVC_ERROR;
|
||||
CameraManager::instance().Open();
|
||||
s_instance.isOpen = true;
|
||||
coreinit::OSSignalEvent(s_cameraOpenEvent);
|
||||
s_instance.inTargetBuffers.Clear();
|
||||
|
|
|
@ -8,4 +8,8 @@ add_library(CemuCamera
|
|||
set_property(TARGET CemuCamera PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
|
||||
|
||||
target_include_directories(CemuCamera PUBLIC "../")
|
||||
target_link_libraries(CemuCamera PRIVATE CemuCommon CemuUtil PUBLIC openpnp-capture)
|
||||
target_link_libraries(CemuCamera PRIVATE CemuCommon CemuUtil PUBLIC openpnp-capture)
|
||||
|
||||
if (ENABLE_WXWIDGETS)
|
||||
target_link_libraries(CemuCamera PRIVATE wx::base)
|
||||
endif()
|
|
@ -1,5 +1,6 @@
|
|||
#include "CameraManager.h"
|
||||
#include "Rgb2Nv12.h"
|
||||
#include "config/CemuConfig.h"
|
||||
#include "util/helpers/helpers.h"
|
||||
|
||||
constexpr unsigned CAMERA_WIDTH = 640;
|
||||
|
@ -10,93 +11,157 @@ CameraManager::CameraManager()
|
|||
: m_ctx(Cap_createContext()),
|
||||
m_rgbBuffer(CAMERA_WIDTH * CAMERA_HEIGHT * 3),
|
||||
m_nv12Buffer(CAMERA_PITCH * CAMERA_HEIGHT * 3 / 2),
|
||||
m_refCount(0)
|
||||
m_refCount(0), m_capturing(false), m_running(true)
|
||||
{
|
||||
// Set default device if it exists
|
||||
if (Cap_getDeviceCount(m_ctx) > 0)
|
||||
m_device = 0;
|
||||
m_captureThread = std::thread(&CameraManager::CaptureWorker, this);
|
||||
|
||||
const auto uniqueId = GetConfig().camera_id.GetValue();
|
||||
if (!uniqueId.empty())
|
||||
{
|
||||
const auto devices = EnumerateDevices();
|
||||
for (CapDeviceID deviceId = 0; deviceId < devices.size(); ++deviceId)
|
||||
{
|
||||
if (devices[deviceId].uniqueId == uniqueId)
|
||||
{
|
||||
m_device = deviceId;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
ResetBuffers();
|
||||
}
|
||||
CameraManager::~CameraManager()
|
||||
{
|
||||
Close();
|
||||
m_running = false;
|
||||
CloseStream();
|
||||
Cap_releaseContext(m_ctx);
|
||||
}
|
||||
|
||||
void CameraManager::SetDevice(uint32 deviceNo)
|
||||
{
|
||||
std::scoped_lock lock(m_mutex);
|
||||
if (m_device == deviceNo)
|
||||
CloseStream();
|
||||
if (deviceNo == DEVICE_NONE)
|
||||
{
|
||||
m_device = std::nullopt;
|
||||
ResetBuffers();
|
||||
return;
|
||||
if (m_stream)
|
||||
Cap_closeStream(m_ctx, *m_stream);
|
||||
}
|
||||
m_device = deviceNo;
|
||||
if (m_refCount != 0)
|
||||
OpenStream();
|
||||
}
|
||||
bool CameraManager::Open(bool weak)
|
||||
std::vector<CameraManager::DeviceInfo> CameraManager::EnumerateDevices()
|
||||
{
|
||||
std::scoped_lock lock(m_mutex);
|
||||
if (!m_device)
|
||||
return false;
|
||||
if (m_refCount == 0)
|
||||
std::vector<DeviceInfo> infos;
|
||||
const auto deviceCount = Cap_getDeviceCount(m_ctx);
|
||||
for (CapDeviceID deviceNo = 0; deviceNo < deviceCount; ++deviceNo)
|
||||
{
|
||||
const auto formatCount = Cap_getNumFormats(m_ctx, *m_device);
|
||||
CapFormatID formatId = 0;
|
||||
for (; formatId < formatCount; ++formatId)
|
||||
{
|
||||
CapFormatInfo formatInfo;
|
||||
if (Cap_getFormatInfo(m_ctx, *m_device, formatId, &formatInfo) != CAPRESULT_OK)
|
||||
continue;
|
||||
if (formatInfo.width == CAMERA_WIDTH && formatInfo.height == CAMERA_HEIGHT)
|
||||
break;
|
||||
}
|
||||
if (formatId == formatCount)
|
||||
return false;
|
||||
auto stream = Cap_openStream(m_ctx, *m_device, formatId);
|
||||
if (stream == -1)
|
||||
return false;
|
||||
m_capturing = true;
|
||||
m_stream = stream;
|
||||
m_captureThread = std::thread(&CameraManager::CaptureWorker, this);
|
||||
const auto uniqueId = Cap_getDeviceUniqueID(m_ctx, deviceNo);
|
||||
const auto name = Cap_getDeviceName(m_ctx, deviceNo);
|
||||
|
||||
if (name)
|
||||
infos.emplace_back(uniqueId, fmt::format("{}: {}", deviceNo + 1, name));
|
||||
else
|
||||
infos.emplace_back(uniqueId, fmt::format("{}: Unknown", deviceNo + 1));
|
||||
}
|
||||
if (!weak)
|
||||
m_refCount += 1;
|
||||
return true;
|
||||
return infos;
|
||||
}
|
||||
void CameraManager::SaveDevice()
|
||||
{
|
||||
std::scoped_lock lock(m_mutex);
|
||||
if (m_device)
|
||||
GetConfig().camera_id = Cap_getDeviceUniqueID(m_ctx, *m_device);
|
||||
else
|
||||
GetConfig().camera_id = "";
|
||||
}
|
||||
void CameraManager::Open()
|
||||
{
|
||||
std::scoped_lock lock(m_mutex);
|
||||
if (m_device && m_refCount == 0)
|
||||
{
|
||||
OpenStream();
|
||||
}
|
||||
m_refCount += 1;
|
||||
}
|
||||
void CameraManager::Close()
|
||||
{
|
||||
std::scoped_lock lock(m_mutex);
|
||||
if (m_refCount == 0)
|
||||
return;
|
||||
m_refCount -= 1;
|
||||
if (m_refCount != 0)
|
||||
return;
|
||||
CloseStream();
|
||||
}
|
||||
|
||||
std::optional<CapFormatID> CameraManager::FindCorrectFormat()
|
||||
{
|
||||
const auto formatCount = Cap_getNumFormats(m_ctx, *m_device);
|
||||
for (CapFormatID formatId = 0; formatId < formatCount; ++formatId)
|
||||
{
|
||||
std::scoped_lock lock(m_mutex);
|
||||
if (m_refCount == 0)
|
||||
return;
|
||||
m_refCount -= 1;
|
||||
if (m_refCount != 0)
|
||||
return;
|
||||
Cap_closeStream(m_ctx, *m_stream);
|
||||
m_stream = std::nullopt;
|
||||
m_capturing = false;
|
||||
CapFormatInfo formatInfo;
|
||||
if (Cap_getFormatInfo(m_ctx, *m_device, formatId, &formatInfo) != CAPRESULT_OK)
|
||||
continue;
|
||||
if (formatInfo.width == CAMERA_WIDTH && formatInfo.height == CAMERA_HEIGHT)
|
||||
return formatId;
|
||||
}
|
||||
m_captureThread.join();
|
||||
return std::nullopt;
|
||||
}
|
||||
void CameraManager::ResetBuffers()
|
||||
{
|
||||
std::ranges::fill(m_rgbBuffer, 0);
|
||||
std::ranges::fill_n(m_nv12Buffer.begin(), CAMERA_WIDTH * CAMERA_PITCH, 16);
|
||||
std::ranges::fill_n(m_nv12Buffer.begin() + CAMERA_WIDTH * CAMERA_PITCH, (CAMERA_WIDTH / 2), 128);
|
||||
}
|
||||
|
||||
void CameraManager::FillNV12Buffer(uint8* nv12Buffer) const
|
||||
{
|
||||
std::shared_lock lock(m_mutex);
|
||||
std::scoped_lock lock(m_mutex);
|
||||
std::ranges::copy(m_nv12Buffer, nv12Buffer);
|
||||
}
|
||||
|
||||
void CameraManager::FillRGBBuffer(uint8* rgbBuffer) const
|
||||
{
|
||||
std::scoped_lock lock(m_mutex);
|
||||
std::ranges::copy(m_rgbBuffer, rgbBuffer);
|
||||
}
|
||||
void CameraManager::CaptureWorker()
|
||||
{
|
||||
SetThreadName("CameraManager");
|
||||
while (m_capturing)
|
||||
while (m_running)
|
||||
{
|
||||
while (m_capturing)
|
||||
{
|
||||
std::scoped_lock lock(m_mutex);
|
||||
bool frameAvailable = Cap_hasNewFrame(m_ctx, *m_stream);
|
||||
if (frameAvailable &&
|
||||
m_mutex.lock();
|
||||
if (m_stream && Cap_hasNewFrame(m_ctx, *m_stream) &&
|
||||
Cap_captureFrame(m_ctx, *m_stream, m_rgbBuffer.data(), m_rgbBuffer.size()) == CAPRESULT_OK)
|
||||
{
|
||||
Rgb2Nv12(m_rgbBuffer.data(), CAMERA_WIDTH, CAMERA_HEIGHT, m_nv12Buffer.data(), CAMERA_PITCH);
|
||||
}
|
||||
m_mutex.unlock();
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(30));
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(16));
|
||||
std::this_thread::sleep_for(std::chrono::seconds(1));
|
||||
std::this_thread::yield();
|
||||
}
|
||||
}
|
||||
void CameraManager::OpenStream()
|
||||
{
|
||||
const auto formatId = FindCorrectFormat();
|
||||
if (!formatId)
|
||||
return;
|
||||
const auto stream = Cap_openStream(m_ctx, *m_device, *formatId);
|
||||
if (stream == -1)
|
||||
return;
|
||||
m_capturing = true;
|
||||
m_stream = stream;
|
||||
}
|
||||
void CameraManager::CloseStream()
|
||||
{
|
||||
m_capturing = false;
|
||||
if (m_stream)
|
||||
{
|
||||
Cap_closeStream(m_ctx, *m_stream);
|
||||
m_stream = std::nullopt;
|
||||
}
|
||||
}
|
|
@ -1,5 +1,7 @@
|
|||
#pragma once
|
||||
#include <shared_mutex>
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
#include <openpnp-capture.h>
|
||||
#include "util/helpers/Singleton.h"
|
||||
|
||||
|
@ -13,19 +15,32 @@ class CameraManager : public Singleton<CameraManager>
|
|||
int m_refCount;
|
||||
std::thread m_captureThread;
|
||||
std::atomic_bool m_capturing;
|
||||
mutable std::shared_mutex m_mutex;
|
||||
std::atomic_bool m_running;
|
||||
mutable std::recursive_mutex m_mutex;
|
||||
|
||||
public:
|
||||
constexpr static uint32 DEVICE_NONE = std::numeric_limits<uint32>::max();
|
||||
struct DeviceInfo
|
||||
{
|
||||
std::string uniqueId;
|
||||
std::string name;
|
||||
};
|
||||
CameraManager();
|
||||
~CameraManager();
|
||||
|
||||
void SetDevice(uint32 deviceNo);
|
||||
std::vector<DeviceInfo> EnumerateDevices();
|
||||
void SaveDevice();
|
||||
|
||||
bool Open(bool weak);
|
||||
void Open();
|
||||
void Close();
|
||||
|
||||
void FillNV12Buffer(uint8* nv12Buffer) const;
|
||||
void FillRGBBuffer(uint8* rgbBuffer) const;
|
||||
|
||||
private:
|
||||
std::optional<CapFormatID> FindCorrectFormat();
|
||||
void ResetBuffers();
|
||||
void CaptureWorker();
|
||||
};
|
||||
void OpenStream();
|
||||
void CloseStream();
|
||||
};
|
||||
|
|
|
@ -343,6 +343,9 @@ void CemuConfig::Load(XMLConfigParser& parser)
|
|||
dsu_client.host = dsuc.get_attribute("host", dsu_client.host);
|
||||
dsu_client.port = dsuc.get_attribute("port", dsu_client.port);
|
||||
|
||||
auto camera = parser.get("Camera");
|
||||
camera_id = camera.get_attribute("Id", "");
|
||||
|
||||
// emulatedusbdevices
|
||||
auto usbdevices = parser.get("EmulatedUsbDevices");
|
||||
emulated_usb_devices.emulate_skylander_portal = usbdevices.get("EmulateSkylanderPortal", emulated_usb_devices.emulate_skylander_portal);
|
||||
|
@ -544,6 +547,9 @@ void CemuConfig::Save(XMLConfigParser& parser)
|
|||
dsuc.set_attribute("host", dsu_client.host);
|
||||
dsuc.set_attribute("port", dsu_client.port);
|
||||
|
||||
auto camera = config.set("Camera");
|
||||
camera.set("Id", camera_id);
|
||||
|
||||
// emulated usb devices
|
||||
auto usbdevices = config.set("EmulatedUsbDevices");
|
||||
usbdevices.set("EmulateSkylanderPortal", emulated_usb_devices.emulate_skylander_portal.GetValue());
|
||||
|
|
|
@ -499,6 +499,9 @@ struct CemuConfig
|
|||
ConfigValue<uint16> port{ 26760 };
|
||||
}dsu_client{};
|
||||
|
||||
// camera
|
||||
ConfigValue<std::string> camera_id;
|
||||
|
||||
// debug
|
||||
ConfigValueBounds<CrashDump> crash_dump{ CrashDump::Disabled };
|
||||
ConfigValue<uint16> gdb_port{ 1337 };
|
||||
|
|
|
@ -127,6 +127,8 @@ add_library(CemuGui
|
|||
wxcomponents/unchecked.xpm
|
||||
wxgui.h
|
||||
wxHelper.h
|
||||
CameraSettingsWindow.cpp
|
||||
CameraSettingsWindow.h
|
||||
)
|
||||
|
||||
set_property(TARGET CemuGui PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
|
||||
|
@ -139,6 +141,7 @@ target_include_directories(CemuGui PUBLIC ${RAPIDJSON_INCLUDE_DIRS})
|
|||
target_link_libraries(CemuGui PRIVATE
|
||||
CemuAudio
|
||||
CemuCafe
|
||||
CemuCamera
|
||||
CemuCommon
|
||||
CemuComponents
|
||||
CemuConfig
|
||||
|
|
98
src/gui/CameraSettingsWindow.cpp
Normal file
98
src/gui/CameraSettingsWindow.cpp
Normal file
|
@ -0,0 +1,98 @@
|
|||
#include "CameraSettingsWindow.h"
|
||||
|
||||
#include "camera/CameraManager.h"
|
||||
|
||||
#include <wx/sizer.h>
|
||||
#include <wx/dcclient.h>
|
||||
#include <wx/dcbuffer.h>
|
||||
#include <wx/rawbmp.h>
|
||||
|
||||
constexpr unsigned CAMERA_WIDTH = 640;
|
||||
constexpr unsigned CAMERA_HEIGHT = 480;
|
||||
|
||||
CameraSettingsWindow::CameraSettingsWindow(wxWindow* parent)
|
||||
: wxDialog(parent, wxID_ANY, _("Camera settings"), wxDefaultPosition),
|
||||
m_imageBitmap(CAMERA_WIDTH, CAMERA_HEIGHT, 24), m_imageBuffer(CAMERA_WIDTH * CAMERA_HEIGHT * 3)
|
||||
{
|
||||
|
||||
|
||||
auto* rootSizer = new wxBoxSizer(wxVERTICAL);
|
||||
{
|
||||
auto* topSizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
{
|
||||
wxString choices[] = {_("None")};
|
||||
m_cameraChoice = new wxChoice(this, wxID_ANY, wxDefaultPosition, {300, -1}, 1, choices);
|
||||
m_cameraChoice->Bind(wxEVT_CHOICE, &CameraSettingsWindow::OnSelectCameraChoice, this);
|
||||
|
||||
m_refreshButton = new wxButton(this, wxID_ANY, wxString::FromUTF8("⟳"));
|
||||
m_refreshButton->Fit();
|
||||
m_refreshButton->Bind(wxEVT_BUTTON, &CameraSettingsWindow::OnRefreshPressed, this);
|
||||
wxQueueEvent(m_refreshButton, new wxCommandEvent{wxEVT_BUTTON});
|
||||
|
||||
topSizer->Add(m_cameraChoice);
|
||||
topSizer->Add(m_refreshButton);
|
||||
}
|
||||
|
||||
m_imageWindow = new wxWindow(this, wxID_ANY, wxDefaultPosition, {CAMERA_WIDTH, CAMERA_HEIGHT});
|
||||
rootSizer->Add(topSizer);
|
||||
rootSizer->Add(m_imageWindow, wxEXPAND);
|
||||
}
|
||||
SetSizerAndFit(rootSizer);
|
||||
CameraManager::instance().Open();
|
||||
m_imageUpdateTimer.Bind(wxEVT_TIMER, &CameraSettingsWindow::UpdateImage, this);
|
||||
m_imageUpdateTimer.Start(33, wxTIMER_CONTINUOUS);
|
||||
this->Bind(wxEVT_CLOSE_WINDOW, &CameraSettingsWindow::OnClose, this);
|
||||
}
|
||||
void CameraSettingsWindow::OnSelectCameraChoice(wxCommandEvent&)
|
||||
{
|
||||
const auto selection = m_cameraChoice->GetSelection();
|
||||
if (selection < 0)
|
||||
return;
|
||||
if (selection == 0)
|
||||
CameraManager::instance().SetDevice(CameraManager::DEVICE_NONE);
|
||||
else
|
||||
CameraManager::instance().SetDevice(selection - 1);
|
||||
}
|
||||
void CameraSettingsWindow::OnRefreshPressed(wxCommandEvent&)
|
||||
{
|
||||
wxArrayString choices = {_("None")};
|
||||
for (const auto& entry : CameraManager::instance().EnumerateDevices())
|
||||
{
|
||||
choices.push_back(entry.name);
|
||||
}
|
||||
m_cameraChoice->Set(choices);
|
||||
wxArrayString str;
|
||||
|
||||
}
|
||||
void CameraSettingsWindow::UpdateImage(const wxTimerEvent&)
|
||||
{
|
||||
CameraManager::instance().FillRGBBuffer(m_imageBuffer.data());
|
||||
|
||||
wxNativePixelData data{m_imageBitmap};
|
||||
if (!data)
|
||||
return;
|
||||
wxNativePixelData::Iterator p{data};
|
||||
for (auto row = 0u; row < CAMERA_HEIGHT; ++row)
|
||||
{
|
||||
const auto* rowPtr = m_imageBuffer.data() + row * CAMERA_WIDTH * 3;
|
||||
wxNativePixelData::Iterator rowStart = p;
|
||||
for (auto col = 0u; col < CAMERA_WIDTH; ++col, ++p)
|
||||
{
|
||||
auto* colour = rowPtr + col * 3;
|
||||
p.Red() = colour[0];
|
||||
p.Green() = colour[1];
|
||||
p.Blue() = colour[2];
|
||||
}
|
||||
p = rowStart;
|
||||
p.OffsetY(data, 1);
|
||||
}
|
||||
|
||||
wxClientDC dc{m_imageWindow};
|
||||
dc.DrawBitmap(m_imageBitmap, 0, 0);
|
||||
}
|
||||
void CameraSettingsWindow::OnClose(wxCloseEvent& event)
|
||||
{
|
||||
CameraManager::instance().Close();
|
||||
CameraManager::instance().SaveDevice();
|
||||
event.Skip();
|
||||
}
|
22
src/gui/CameraSettingsWindow.h
Normal file
22
src/gui/CameraSettingsWindow.h
Normal file
|
@ -0,0 +1,22 @@
|
|||
#pragma once
|
||||
#include <wx/dialog.h>
|
||||
#include <wx/timer.h>
|
||||
#include <wx/choice.h>
|
||||
#include <wx/bmpbuttn.h>
|
||||
|
||||
class CameraSettingsWindow : public wxDialog
|
||||
{
|
||||
wxChoice* m_cameraChoice;
|
||||
wxButton* m_refreshButton;
|
||||
wxWindow* m_imageWindow;
|
||||
wxBitmap m_imageBitmap;
|
||||
wxTimer m_imageUpdateTimer;
|
||||
std::vector<uint8> m_imageBuffer;
|
||||
public:
|
||||
explicit CameraSettingsWindow(wxWindow* parent);
|
||||
void OnSelectCameraChoice(wxCommandEvent&);
|
||||
void OnRefreshPressed(wxCommandEvent&);
|
||||
void UpdateImage(const wxTimerEvent&);
|
||||
void OnClose(wxCloseEvent& event);
|
||||
};
|
||||
|
|
@ -61,6 +61,7 @@
|
|||
#include "gamemode_client.h"
|
||||
#endif
|
||||
|
||||
#include "CameraSettingsWindow.h"
|
||||
#include "Cafe/TitleList/TitleInfo.h"
|
||||
#include "Cafe/TitleList/TitleList.h"
|
||||
#include "wxHelper.h"
|
||||
|
@ -91,6 +92,7 @@ enum
|
|||
MAINFRAME_MENU_ID_OPTIONS_GENERAL2,
|
||||
MAINFRAME_MENU_ID_OPTIONS_AUDIO,
|
||||
MAINFRAME_MENU_ID_OPTIONS_INPUT,
|
||||
MAINFRAME_MENU_ID_OPTIONS_CAMERA,
|
||||
// options -> account
|
||||
MAINFRAME_MENU_ID_OPTIONS_ACCOUNT_1 = 20350,
|
||||
MAINFRAME_MENU_ID_OPTIONS_ACCOUNT_12 = 20350 + 11,
|
||||
|
@ -186,6 +188,7 @@ EVT_MENU(MAINFRAME_MENU_ID_OPTIONS_GENERAL, MainWindow::OnOptionsInput)
|
|||
EVT_MENU(MAINFRAME_MENU_ID_OPTIONS_GENERAL2, MainWindow::OnOptionsInput)
|
||||
EVT_MENU(MAINFRAME_MENU_ID_OPTIONS_AUDIO, MainWindow::OnOptionsInput)
|
||||
EVT_MENU(MAINFRAME_MENU_ID_OPTIONS_INPUT, MainWindow::OnOptionsInput)
|
||||
EVT_MENU(MAINFRAME_MENU_ID_OPTIONS_CAMERA, MainWindow::OnOptionsInput)
|
||||
// tools menu
|
||||
EVT_MENU(MAINFRAME_MENU_ID_TOOLS_MEMORY_SEARCHER, MainWindow::OnToolsInput)
|
||||
EVT_MENU(MAINFRAME_MENU_ID_TOOLS_TITLE_MANAGER, MainWindow::OnToolsInput)
|
||||
|
@ -921,6 +924,12 @@ void MainWindow::OnOptionsInput(wxCommandEvent& event)
|
|||
frame->Destroy();
|
||||
break;
|
||||
}
|
||||
case MAINFRAME_MENU_ID_OPTIONS_CAMERA:
|
||||
{
|
||||
auto* frame = new CameraSettingsWindow(this);
|
||||
frame->ShowModal();
|
||||
frame->Destroy();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -2159,6 +2168,7 @@ void MainWindow::RecreateMenu()
|
|||
optionsMenu->AppendSeparator();
|
||||
optionsMenu->Append(MAINFRAME_MENU_ID_OPTIONS_GENERAL2, _("&General settings"));
|
||||
optionsMenu->Append(MAINFRAME_MENU_ID_OPTIONS_INPUT, _("&Input settings"));
|
||||
optionsMenu->Append(MAINFRAME_MENU_ID_OPTIONS_CAMERA, _("&Camera settings"));
|
||||
|
||||
optionsMenu->AppendSeparator();
|
||||
optionsMenu->AppendSubMenu(m_optionsAccountMenu, _("&Active account"));
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue