1
0
mirror of https://github.com/telegramdesktop/tdesktop synced 2025-03-30 23:38:25 +00:00

Refactor calls settings panel.

This commit is contained in:
John Preston 2019-01-11 14:07:56 +04:00
parent 8711830f66
commit 65430d92ea
10 changed files with 284 additions and 193 deletions

View File

@ -13,27 +13,48 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/widgets/checkbox.h"
#include "styles/style_boxes.h"
SingleChoiceBox::SingleChoiceBox(
QWidget*,
LangKey title,
const std::vector<QString> &optionTexts,
int initialSelection,
Fn<void(int)> callback)
: _title(title)
, _optionTexts(optionTexts)
, _initialSelection(initialSelection)
, _callback(callback) {
}
void SingleChoiceBox::prepare() {
setTitle(langFactory(_title));
addButton(langFactory(lng_box_ok), [this] { closeBox(); });
addButton(langFactory(lng_box_ok), [=] { closeBox(); });
auto group = std::make_shared<Ui::RadiobuttonGroup>(_initialSelection);
auto y = st::boxOptionListPadding.top() + st::autolockButton.margin.top();
auto count = int(_optionTexts.size());
_options.reserve(count);
const auto group = std::make_shared<Ui::RadiobuttonGroup>(_initialSelection);
auto y = st::boxOptionListPadding.top()
+ st::autolockButton.margin.top();
_options.reserve(_optionTexts.size());
auto i = 0;
for (const auto &text : _optionTexts) {
_options.emplace_back(this, group, i, text, st::autolockButton);
_options.back()->moveToLeft(st::boxPadding.left() + st::boxOptionListPadding.left(), y);
_options.back()->moveToLeft(
st::boxPadding.left() + st::boxOptionListPadding.left(),
y);
y += _options.back()->heightNoMargins() + st::boxOptionListSkip;
i++;
}
group->setChangedCallback([this](int value) {
group->setChangedCallback([=](int value) {
const auto weak = make_weak(this);
_callback(value);
closeBox();
if (weak) {
closeBox();
}
});
setDimensions(st::autolockWidth, st::boxOptionListPadding.top() + count * _options.back()->heightNoMargins() + (count - 1) * st::boxOptionListSkip + st::boxOptionListPadding.bottom() + st::boxPadding.bottom());
const auto height = y
- st::boxOptionListSkip
+ st::boxOptionListPadding.bottom()
+ st::boxPadding.bottom();
setDimensions(st::autolockWidth, height);
}

View File

@ -18,8 +18,12 @@ class Radiobutton;
class SingleChoiceBox : public BoxContent {
public:
SingleChoiceBox(QWidget*, LangKey title, std::vector<QString> optionTexts, int initialSelection, Fn<void(int)> callback) : _title(title), _optionTexts(optionTexts), _initialSelection(initialSelection), _callback(callback) {
}
SingleChoiceBox(
QWidget*,
LangKey title,
const std::vector<QString> &optionTexts,
int initialSelection,
Fn<void(int)> callback);
protected:
void prepare() override;

View File

@ -491,24 +491,23 @@ void OpenSystemSettingsForPermission(PermissionType type) {
bool OpenSystemSettings(SystemSettingsType type) {
if (type == SystemSettingsType::Audio) {
bool succeeded = false;
auto options = std::vector<QString>();
const auto add = [&](const char *option) {
options.emplace_back(option);
};
if (DesktopEnvironment::IsUnity()) {
succeeded |= QProcess::startDetached(qsl("unity-control-center sound"));
add("unity-control-center sound");
} else if (DesktopEnvironment::IsKDE()) {
succeeded |= QProcess::startDetached(qsl("kcmshell5 kcm_pulseaudio"));
if (!succeeded) {
succeeded |= QProcess::startDetached(qsl("kcmshell4 phonon"));
}
add("kcmshell5 kcm_pulseaudio");
add("kcmshell4 phonon");
} else if (DesktopEnvironment::IsGnome()) {
succeeded |= QProcess::startDetached(qsl("gnome-control-center sound"));
add("gnome-control-center sound");
}
if (!succeeded) {
succeeded |= QProcess::startDetached(qsl("pavucontrol"));
}
if (!succeeded) {
succeeded |= QProcess::startDetached(qsl("alsamixergui"));
}
return succeeded;
add("pavucontrol");
add("alsamixergui");
return ranges::find_if(options, [](const QString &command) {
return QProcess::startDetached(command);
}) != end(options);
}
return true;
}

View File

@ -334,9 +334,9 @@ void OpenSystemSettingsForPermission(PermissionType type) {
bool OpenSystemSettings(SystemSettingsType type) {
switch (type) {
case SystemSettingsType::Audio:
[[NSWorkspace sharedWorkspace] openFile:@"/System/Library/PreferencePanes/Sound.prefPane"];
break;
case SystemSettingsType::Audio:
[[NSWorkspace sharedWorkspace] openFile:@"/System/Library/PreferencePanes/Sound.prefPane"];
break;
}
return true;
}

View File

@ -652,14 +652,24 @@ void RequestPermission(PermissionType type, Fn<void(PermissionStatus)> resultCal
}
void OpenSystemSettingsForPermission(PermissionType type) {
if (type==PermissionType::Microphone) {
ShellExecute(NULL, L"open", L"ms-settings:privacy-microphone", NULL, NULL, SW_SHOWDEFAULT);
if (type == PermissionType::Microphone) {
crl::on_main([] {
ShellExecute(
nullptr,
L"open",
L"ms-settings:privacy-microphone",
nullptr,
nullptr,
SW_SHOWDEFAULT);
});
}
}
bool OpenSystemSettings(SystemSettingsType type) {
if (type == SystemSettingsType::Audio) {
WinExec("control.exe mmsys.cpl", SW_SHOW);
crl::on_main([] {
WinExec("control.exe mmsys.cpl", SW_SHOW);
});
}
return true;
}

View File

@ -43,13 +43,13 @@ Calls::Calls(QWidget *parent, UserData *self)
setupContent();
}
Calls::~Calls(){
Calls::~Calls() {
if (_needWriteSettings) {
Local::writeUserSettings();
}
}
void Calls::sectionSaveChanges(FnMut<void()> done){
void Calls::sectionSaveChanges(FnMut<void()> done) {
if (_micTester) {
_micTester.reset();
}
@ -57,181 +57,219 @@ void Calls::sectionSaveChanges(FnMut<void()> done){
}
void Calls::setupContent() {
using namespace tgvoip;
const auto content = Ui::CreateChild<Ui::VerticalLayout>(this);
const auto getId = [](const auto &device) {
return QString::fromStdString(device.id);
};
const auto getName = [](const auto &device) {
return QString::fromStdString(device.displayName);
};
QString currentOutputName;
if (Global::CallOutputDeviceID() == qsl("default")) {
currentOutputName = lang(lng_settings_call_device_default);
} else {
std::vector<tgvoip::AudioOutputDevice> outputDevices = tgvoip::VoIPController::EnumerateAudioOutputs();
currentOutputName=Global::CallOutputDeviceID();
for (auto &dev : outputDevices) {
if (QString::fromUtf8(dev.id.c_str()) == Global::CallOutputDeviceID()) {
currentOutputName = QString::fromUtf8(dev.displayName.c_str());
break;
}
const auto currentOutputName = [&] {
if (Global::CallOutputDeviceID() == qsl("default")) {
return lang(lng_settings_call_device_default);
}
}
const auto &list = VoIPController::EnumerateAudioOutputs();
const auto i = ranges::find(
list,
Global::CallOutputDeviceID(),
getId);
return (i != end(list))
? getName(*i)
: Global::CallOutputDeviceID();
}();
QString currentInputName;
if (Global::CallInputDeviceID() == qsl("default")) {
currentInputName = lang(lng_settings_call_device_default);
} else {
std::vector<tgvoip::AudioInputDevice> inputDevices = tgvoip::VoIPController::EnumerateAudioInputs();
currentInputName = Global::CallInputDeviceID();
for (auto &dev : inputDevices) {
if (QString::fromUtf8(dev.id.c_str()) == Global::CallInputDeviceID()) {
currentInputName = QString::fromUtf8(dev.displayName.c_str());
break;
}
const auto currentInputName = [&] {
if (Global::CallInputDeviceID() == qsl("default")) {
return lang(lng_settings_call_device_default);
}
}
const auto &list = VoIPController::EnumerateAudioInputs();
const auto i = ranges::find(
list,
Global::CallInputDeviceID(),
getId);
return (i != end(list))
? getName(*i)
: Global::CallInputDeviceID();
}();
AddSkip(content);
AddSubsectionTitle(content, lng_settings_call_section_output);
const auto outputButton = AddButtonWithLabel(
AddButtonWithLabel(
content,
lng_settings_call_output_device,
rpl::single(currentOutputName) | rpl::then(_outputNameStream.events()),
st::settingsButton);
outputButton->addClickHandler([this] {
int selectedOption = 0;
std::vector<tgvoip::AudioOutputDevice> devices = tgvoip::VoIPController::EnumerateAudioOutputs();
std::vector<QString> options;
options.push_back(lang(lng_settings_call_device_default));
int i = 1;
for (auto &device : devices) {
QString displayName = QString::fromUtf8(device.displayName.c_str());
options.push_back(displayName);
if (QString::fromUtf8(device.id.c_str()) == Global::CallOutputDeviceID()) {
selectedOption = i;
}
i++;
}
const auto save = crl::guard(this, [=](int selectedOption) {
QString name = options[selectedOption];
_outputNameStream.fire(std::move(name));
std::string selectedDeviceID;
if (selectedOption == 0) {
selectedDeviceID = "default";
} else {
selectedDeviceID = devices[selectedOption-1].id;
}
Global::SetCallOutputDeviceID(QString::fromStdString(selectedDeviceID));
rpl::single(
currentOutputName
) | rpl::then(
_outputNameStream.events()
),
st::settingsButton
)->addClickHandler([=] {
const auto &devices = VoIPController::EnumerateAudioOutputs();
const auto options = ranges::view::concat(
ranges::view::single(lang(lng_settings_call_device_default)),
devices | ranges::view::transform(getName)
) | ranges::to_vector;
const auto i = ranges::find(
devices,
Global::CallOutputDeviceID(),
getId);
const auto currentOption = (i != end(devices))
? int(i - begin(devices) + 1)
: 0;
const auto save = crl::guard(this, [=](int option) {
_outputNameStream.fire_copy(options[option]);
const auto deviceId = option
? devices[option - 1].id
: "default";
Global::SetCallOutputDeviceID(QString::fromStdString(deviceId));
Local::writeUserSettings();
::Calls::Call *currentCall = ::Calls::Current().currentCall();
if (currentCall) {
currentCall->setCurrentAudioDevice(false, selectedDeviceID);
if (const auto call = ::Calls::Current().currentCall()) {
call->setCurrentAudioDevice(false, deviceId);
}
});
Ui::show(Box<SingleChoiceBox>(lng_settings_call_output_device, options, selectedOption, save));
Ui::show(Box<SingleChoiceBox>(
lng_settings_call_output_device,
options,
currentOption,
save));
});
const auto outputLabel = content->add(object_ptr<Ui::LabelSimple>(content, st::settingsAudioVolumeLabel), st::settingsAudioVolumeLabelPadding);
const auto outputSlider = content->add(object_ptr<Ui::MediaSlider>(content, st::settingsAudioVolumeSlider), st::settingsAudioVolumeSliderPadding);
auto updateOutputLabel = [outputLabel](int value){
QString percent = QString::number(value);
outputLabel->setText(lng_settings_call_output_volume(lt_percent, percent));
const auto outputLabel = content->add(
object_ptr<Ui::LabelSimple>(
content,
st::settingsAudioVolumeLabel),
st::settingsAudioVolumeLabelPadding);
const auto outputSlider = content->add(
object_ptr<Ui::MediaSlider>(
content,
st::settingsAudioVolumeSlider),
st::settingsAudioVolumeSliderPadding);
const auto updateOutputLabel = [=](int value) {
const auto percent = QString::number(value);
outputLabel->setText(
lng_settings_call_output_volume(lt_percent, percent));
};
const auto updateOutputVolume = [=](int value) {
_needWriteSettings = true;
updateOutputLabel(value);
Global::SetCallOutputVolume(value);
if (const auto call = ::Calls::Current().currentCall()) {
call->setAudioVolume(false, value / 100.0f);
}
};
outputSlider->resize(st::settingsAudioVolumeSlider.seekSize);
outputSlider->setPseudoDiscrete(
201,
[](int val){
return val;
},
[](int val) { return val; },
Global::CallOutputVolume(),
[updateOutputLabel, this](int value) {
_needWriteSettings = true;
updateOutputLabel(value);
Global::SetCallOutputVolume(value);
::Calls::Call* currentCall = ::Calls::Current().currentCall();
if (currentCall) {
currentCall->setAudioVolume(false, value/100.0f);
}
});
updateOutputVolume);
updateOutputLabel(Global::CallOutputVolume());
AddSkip(content);
AddDivider(content);
AddSkip(content);
AddSubsectionTitle(content, lng_settings_call_section_input);
const auto inputButton = AddButtonWithLabel(
AddButtonWithLabel(
content,
lng_settings_call_input_device,
rpl::single(currentInputName) | rpl::then(_inputNameStream.events()),
st::settingsButton);
inputButton->addClickHandler([this] {
int selectedOption = 0;
std::vector<tgvoip::AudioInputDevice> devices = tgvoip::VoIPController::EnumerateAudioInputs();
std::vector<QString> options;
options.push_back(lang(lng_settings_call_device_default));
int i = 1;
for (auto &device : devices) {
QString displayName = QString::fromUtf8(device.displayName.c_str());
options.push_back(displayName);
if(QString::fromUtf8(device.id.c_str()) == Global::CallInputDeviceID())
selectedOption = i;
i++;
}
const auto save = crl::guard(this, [=](int selectedOption) {
QString name=options[selectedOption];
_inputNameStream.fire(std::move(name));
std::string selectedDeviceID;
if (selectedOption == 0) {
selectedDeviceID = "default";
} else {
selectedDeviceID = devices[selectedOption - 1].id;
}
Global::SetCallInputDeviceID(QString::fromUtf8(selectedDeviceID.c_str()));
rpl::single(
currentInputName
) | rpl::then(
_inputNameStream.events()
),
st::settingsButton
)->addClickHandler([=] {
const auto &devices = VoIPController::EnumerateAudioInputs();
const auto options = ranges::view::concat(
ranges::view::single(lang(lng_settings_call_device_default)),
devices | ranges::view::transform(getName)
) | ranges::to_vector;
const auto i = ranges::find(
devices,
Global::CallInputDeviceID(),
getId);
const auto currentOption = (i != end(devices))
? int(i - begin(devices) + 1)
: 0;
const auto save = crl::guard(this, [=](int option) {
_inputNameStream.fire_copy(options[option]);
const auto deviceId = option
? devices[option - 1].id
: "default";
Global::SetCallInputDeviceID(QString::fromStdString(deviceId));
Local::writeUserSettings();
if (_micTester) {
stopTestingMicrophone();
}
::Calls::Call *currentCall = ::Calls::Current().currentCall();
if(currentCall){
currentCall->setCurrentAudioDevice(true, selectedDeviceID);
if (const auto call = ::Calls::Current().currentCall()) {
call->setCurrentAudioDevice(true, deviceId);
}
});
Ui::show(Box<SingleChoiceBox>(lng_settings_call_input_device, options, selectedOption, save));
Ui::show(Box<SingleChoiceBox>(
lng_settings_call_input_device,
options,
currentOption,
save));
});
const auto inputLabel = content->add(object_ptr<Ui::LabelSimple>(content, st::settingsAudioVolumeLabel), st::settingsAudioVolumeLabelPadding);
const auto inputSlider = content->add(object_ptr<Ui::MediaSlider>(content, st::settingsAudioVolumeSlider), st::settingsAudioVolumeSliderPadding);
auto updateInputLabel = [inputLabel](int value){
QString percent = QString::number(value);
inputLabel->setText(lng_settings_call_input_volume(lt_percent, percent));
const auto inputLabel = content->add(
object_ptr<Ui::LabelSimple>(
content,
st::settingsAudioVolumeLabel),
st::settingsAudioVolumeLabelPadding);
const auto inputSlider = content->add(
object_ptr<Ui::MediaSlider>(
content,
st::settingsAudioVolumeSlider),
st::settingsAudioVolumeSliderPadding);
const auto updateInputLabel = [=](int value) {
const auto percent = QString::number(value);
inputLabel->setText(
lng_settings_call_input_volume(lt_percent, percent));
};
const auto updateInputVolume = [=](int value) {
_needWriteSettings = true;
updateInputLabel(value);
Global::SetCallInputVolume(value);
::Calls::Call *currentCall = ::Calls::Current().currentCall();
if (currentCall) {
currentCall->setAudioVolume(true, value / 100.0f);
}
};
inputSlider->resize(st::settingsAudioVolumeSlider.seekSize);
inputSlider->setPseudoDiscrete(101,
[](int val){
return val;
},
[](int val) { return val; },
Global::CallInputVolume(),
[updateInputLabel, this](int value) {
_needWriteSettings = true;
updateInputLabel(value);
Global::SetCallInputVolume(value);
::Calls::Call *currentCall = ::Calls::Current().currentCall();
if (currentCall) {
currentCall->setAudioVolume(true, value / 100.0f);
}
});
updateInputVolume);
updateInputLabel(Global::CallInputVolume());
_micTestButton=AddButton(content, rpl::single(lang(lng_settings_call_test_mic)) | rpl::then(_micTestTextStream.events()), st::settingsButton);
_micTestLevel=content->add(object_ptr<Ui::LevelMeter>(content, st::defaultLevelMeter), st::settingsLevelMeterPadding);
_micTestLevel->resize(QSize(0, st::defaultLevelMeter.height));
_micTestButton->addClickHandler([this]{
AddButton(
content,
rpl::single(
lang(lng_settings_call_test_mic)
) | rpl::then(
_micTestTextStream.events()
),
st::settingsButton
)->addClickHandler([=] {
if (!_micTester) {
requestPermissionAndStartTestingMicrophone();
} else {
stopTestingMicrophone();
}
});
_levelUpdateTimer.setCallback([this](){
_micTestLevel = content->add(
object_ptr<Ui::LevelMeter>(
content,
st::defaultLevelMeter),
st::settingsLevelMeterPadding);
_micTestLevel->resize(QSize(0, st::defaultLevelMeter.height));
_levelUpdateTimer.setCallback([=] {
_micTestLevel->setValue(_micTester->GetAndResetLevel());
});
@ -252,16 +290,20 @@ void Calls::setupContent() {
}) | rpl::start_with_next([](bool enabled) {
Global::SetCallAudioDuckingEnabled(enabled);
Local::writeUserSettings();
::Calls::Call *currentCall = ::Calls::Current().currentCall();
if (currentCall) {
currentCall->setAudioDuckingEnabled(enabled);
if (const auto call = ::Calls::Current().currentCall()) {
call->setAudioDuckingEnabled(enabled);
}
}, content->lifetime());
#endif // Q_OS_MAC
const auto systemSettingsButton=AddButton(content, lng_settings_call_open_system_prefs, st::settingsButton);
systemSettingsButton->addClickHandler([]{
if (!Platform::OpenSystemSettings(Platform::SystemSettingsType::Audio)) {
AddButton(
content,
lng_settings_call_open_system_prefs,
st::settingsButton
)->addClickHandler([] {
const auto opened = Platform::OpenSystemSettings(
Platform::SystemSettingsType::Audio);
if (!opened) {
Ui::show(Box<InformBox>(lang(lng_linux_no_audio_prefs)));
}
});
@ -270,37 +312,48 @@ void Calls::setupContent() {
Ui::ResizeFitChild(this, content);
}
void Calls::requestPermissionAndStartTestingMicrophone(){
Platform::PermissionStatus status = Platform::GetPermissionStatus(Platform::PermissionType::Microphone);
void Calls::requestPermissionAndStartTestingMicrophone() {
const auto status = Platform::GetPermissionStatus(
Platform::PermissionType::Microphone);
if (status == Platform::PermissionStatus::Granted) {
startTestingMicrophone();
} else if (status == Platform::PermissionStatus::CanRequest) {
Platform::RequestPermission(Platform::PermissionType::Microphone, crl::guard(this, [this](Platform::PermissionStatus status) {
const auto startTestingChecked = crl::guard(this, [=](
Platform::PermissionStatus status) {
if (status == Platform::PermissionStatus::Granted) {
crl::on_main(crl::guard(this, [this]{
crl::on_main(crl::guard(this, [=] {
startTestingMicrophone();
}));
}
}));
});
Platform::RequestPermission(
Platform::PermissionType::Microphone,
startTestingChecked);
} else {
Ui::show(Box<ConfirmBox>(lang(lng_no_mic_permission), lang(lng_menu_settings), crl::guard(this, [] {
Platform::OpenSystemSettingsForPermission(Platform::PermissionType::Microphone);
const auto showSystemSettings = [] {
Platform::OpenSystemSettingsForPermission(
Platform::PermissionType::Microphone);
Ui::hideLayer();
})));
};
Ui::show(Box<ConfirmBox>(
lang(lng_no_mic_permission),
lang(lng_menu_settings),
showSystemSettings));
}
}
void Calls::startTestingMicrophone(){
void Calls::startTestingMicrophone() {
_micTestTextStream.fire(lang(lng_settings_call_stop_mic_test));
_levelUpdateTimer.callEach(50);
_micTester = std::make_unique<tgvoip::AudioInputTester>(Global::CallInputDeviceID().toStdString());
_micTester = std::make_unique<tgvoip::AudioInputTester>(
Global::CallInputDeviceID().toStdString());
if (_micTester->Failed()) {
Ui::show(Box<InformBox>(lang(lng_call_error_audio_io)));
stopTestingMicrophone();
}
}
void Calls::stopTestingMicrophone(){
void Calls::stopTestingMicrophone() {
_micTestTextStream.fire(lang(lng_settings_call_test_mic));
_levelUpdateTimer.cancel();
_micTester.reset();

View File

@ -41,7 +41,6 @@ private:
rpl::event_stream<QString> _micTestTextStream;
bool _needWriteSettings = false;
std::unique_ptr<tgvoip::AudioInputTester> _micTester;
Button *_micTestButton = nullptr;
Ui::LevelMeter *_micTestLevel = nullptr;
base::Timer _levelUpdateTimer;
};

View File

@ -603,11 +603,11 @@ void RadiobuttonGroup::setValue(int value) {
}
_hasValue = true;
_value = value;
for (auto button : _buttons) {
for (const auto button : _buttons) {
button->handleNewGroupValue(_value);
}
if (_changedCallback) {
_changedCallback(_value);
if (const auto callback = _changedCallback) {
callback(_value);
}
}

View File

@ -9,33 +9,36 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace Ui {
LevelMeter::LevelMeter(QWidget *parent, const style::LevelMeter &st) : RpWidget(parent), _st(st){
LevelMeter::LevelMeter(QWidget *parent, const style::LevelMeter &st)
: RpWidget(parent)
, _st(st) {
}
void LevelMeter::setValue(float value){
void LevelMeter::setValue(float value) {
_value = value;
repaint();
}
void LevelMeter::paintEvent(QPaintEvent* event){
void LevelMeter::paintEvent(QPaintEvent* event) {
Painter p(this);
PainterHighQualityEnabler hq(p);
p.setPen(Qt::NoPen);
auto activeFg = _st.activeFg;
auto inactiveFg = _st.inactiveFg;
auto radius = _st.lineWidth / 2;
QRect rect(0, 0, _st.lineWidth, height());
const auto activeFg = _st.activeFg;
const auto inactiveFg = _st.inactiveFg;
const auto radius = _st.lineWidth / 2;
const auto rect = QRect(0, 0, _st.lineWidth, height());
p.setBrush(activeFg);
for (auto i = 0; i < _st.lineCount; ++i) {
float valueAtLine = (float)(i + 1) / _st.lineCount;
const auto valueAtLine = (float)(i + 1) / _st.lineCount;
if (valueAtLine > _value) {
p.setBrush(inactiveFg);
}
rect.moveLeft((_st.lineWidth + _st.lineSpacing) * i);
p.drawRoundedRect(rect, radius, radius);
p.drawRoundedRect(
rect.translated((_st.lineWidth + _st.lineSpacing) * i, 0),
radius,
radius);
}
}

View File

@ -15,6 +15,7 @@ namespace Ui {
class LevelMeter : public RpWidget {
public:
LevelMeter(QWidget *parent, const style::LevelMeter& st);
void setValue(float value);
protected:
@ -22,7 +23,8 @@ protected:
private:
const style::LevelMeter &_st;
float _value=0.0f;
float _value = 0.0f;
};
} // namespace Ui