properties-view.cpp 66 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482
  1. #include <QFormLayout>
  2. #include <QScrollBar>
  3. #include <QLabel>
  4. #include <QCheckBox>
  5. #include <QFont>
  6. #include <QFontDialog>
  7. #include <QLineEdit>
  8. #include <QSpinBox>
  9. #include <QSlider>
  10. #include <QDoubleSpinBox>
  11. #include <QComboBox>
  12. #include <QListWidget>
  13. #include <QPushButton>
  14. #include <QRadioButton>
  15. #include <QButtonGroup>
  16. #include <QStandardItem>
  17. #include <QFileDialog>
  18. #include <QColorDialog>
  19. #include <QDialogButtonBox>
  20. #include <QMenu>
  21. #include <QMessageBox>
  22. #include <QStackedWidget>
  23. #include <QDir>
  24. #include <QGroupBox>
  25. #include <QObject>
  26. #include <QDesktopServices>
  27. #include <QUuid>
  28. #include "double-slider.hpp"
  29. #include "spinbox-ignorewheel.hpp"
  30. #include "moc_properties-view.cpp"
  31. #include "properties-view.moc.hpp"
  32. #include <qt-wrappers.hpp>
  33. #include <plain-text-edit.hpp>
  34. #include <slider-ignorewheel.hpp>
  35. #include <icon-label.hpp>
  36. #include <cstdlib>
  37. #include <initializer_list>
  38. #include <obs-data.h>
  39. #include <obs.h>
  40. #include <qtimer.h>
  41. #include <string>
  42. #include <obs-frontend-api.h>
  43. using namespace std;
  44. static inline QColor color_from_int(long long val)
  45. {
  46. return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff,
  47. (val >> 24) & 0xff);
  48. }
  49. static inline long long color_to_int(QColor color)
  50. {
  51. auto shift = [&](unsigned val, int shift) {
  52. return ((val & 0xff) << shift);
  53. };
  54. return shift(color.red(), 0) | shift(color.green(), 8) |
  55. shift(color.blue(), 16) | shift(color.alpha(), 24);
  56. }
  57. namespace {
  58. struct frame_rate_tag {
  59. enum tag_type {
  60. SIMPLE,
  61. RATIONAL,
  62. USER,
  63. } type = SIMPLE;
  64. const char *val = nullptr;
  65. frame_rate_tag() = default;
  66. explicit frame_rate_tag(tag_type type) : type(type) {}
  67. explicit frame_rate_tag(const char *val) : type(USER), val(val) {}
  68. static frame_rate_tag simple() { return frame_rate_tag{SIMPLE}; }
  69. static frame_rate_tag rational() { return frame_rate_tag{RATIONAL}; }
  70. };
  71. struct common_frame_rate {
  72. const char *fps_name;
  73. media_frames_per_second fps;
  74. };
  75. } // namespace
  76. Q_DECLARE_METATYPE(frame_rate_tag);
  77. Q_DECLARE_METATYPE(media_frames_per_second);
  78. void OBSPropertiesView::ReloadProperties()
  79. {
  80. if (weakObj || rawObj) {
  81. OBSObject strongObj = GetObject();
  82. void *obj = strongObj ? strongObj.Get() : rawObj;
  83. if (obj)
  84. properties.reset(reloadCallback(obj));
  85. } else {
  86. properties.reset(reloadCallback((void *)type.c_str()));
  87. obs_properties_apply_settings(properties.get(), settings);
  88. }
  89. uint32_t flags = obs_properties_get_flags(properties.get());
  90. deferUpdate = enableDefer && (flags & OBS_PROPERTIES_DEFER_UPDATE) != 0;
  91. RefreshProperties();
  92. }
  93. #define NO_PROPERTIES_STRING QObject::tr("Basic.PropertiesWindow.NoProperties")
  94. void OBSPropertiesView::RefreshProperties()
  95. {
  96. int h, v, hend, vend;
  97. GetScrollPos(h, v, hend, vend);
  98. children.clear();
  99. if (widget)
  100. widget->deleteLater();
  101. widget = new QWidget();
  102. widget->setObjectName("PropertiesContainer");
  103. QFormLayout *layout = new QFormLayout;
  104. layout->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow);
  105. widget->setLayout(layout);
  106. QSizePolicy mainPolicy(QSizePolicy::MinimumExpanding,
  107. QSizePolicy::MinimumExpanding);
  108. layout->setLabelAlignment(Qt::AlignRight);
  109. obs_property_t *property = obs_properties_first(properties.get());
  110. bool hasNoProperties = !property;
  111. while (property) {
  112. AddProperty(property, layout);
  113. obs_property_next(&property);
  114. }
  115. setWidgetResizable(true);
  116. setWidget(widget);
  117. setSizePolicy(mainPolicy);
  118. adjustSize();
  119. SetScrollPos(h, v, hend, vend);
  120. if (disableScrolling)
  121. setMinimumHeight(widget->minimumSizeHint().height());
  122. lastFocused.clear();
  123. if (lastWidget) {
  124. lastWidget->setFocus(Qt::OtherFocusReason);
  125. lastWidget = nullptr;
  126. }
  127. if (hasNoProperties) {
  128. QLabel *noPropertiesLabel = new QLabel(NO_PROPERTIES_STRING);
  129. layout->addWidget(noPropertiesLabel);
  130. }
  131. emit PropertiesRefreshed();
  132. }
  133. void OBSPropertiesView::SetScrollPos(int h, int v, int old_hend, int old_vend)
  134. {
  135. QScrollBar *scroll = horizontalScrollBar();
  136. if (scroll) {
  137. int hend = scroll->maximum() + scroll->pageStep();
  138. scroll->setValue(h * hend / old_hend);
  139. }
  140. scroll = verticalScrollBar();
  141. if (scroll) {
  142. int vend = scroll->maximum() + scroll->pageStep();
  143. scroll->setValue(v * vend / old_vend);
  144. }
  145. }
  146. void OBSPropertiesView::GetScrollPos(int &h, int &v, int &hend, int &vend)
  147. {
  148. h = v = 0;
  149. QScrollBar *scroll = horizontalScrollBar();
  150. if (scroll) {
  151. h = scroll->value();
  152. hend = scroll->maximum() + scroll->pageStep();
  153. }
  154. scroll = verticalScrollBar();
  155. if (scroll) {
  156. v = scroll->value();
  157. vend = scroll->maximum() + scroll->pageStep();
  158. }
  159. }
  160. OBSPropertiesView::OBSPropertiesView(OBSData settings_, obs_object_t *obj,
  161. PropertiesReloadCallback reloadCallback,
  162. PropertiesUpdateCallback callback_,
  163. PropertiesVisualUpdateCb visUpdateCb_,
  164. int minSize_)
  165. : VScrollArea(nullptr),
  166. properties(nullptr, obs_properties_destroy),
  167. settings(settings_),
  168. weakObj(obs_object_get_weak_object(obj)),
  169. reloadCallback(reloadCallback),
  170. callback(callback_),
  171. visUpdateCb(visUpdateCb_),
  172. minSize(minSize_)
  173. {
  174. setFrameShape(QFrame::NoFrame);
  175. QMetaObject::invokeMethod(this, "ReloadProperties",
  176. Qt::QueuedConnection);
  177. }
  178. OBSPropertiesView::OBSPropertiesView(OBSData settings_, void *obj,
  179. PropertiesReloadCallback reloadCallback,
  180. PropertiesUpdateCallback callback_,
  181. PropertiesVisualUpdateCb visUpdateCb_,
  182. int minSize_)
  183. : VScrollArea(nullptr),
  184. properties(nullptr, obs_properties_destroy),
  185. settings(settings_),
  186. rawObj(obj),
  187. reloadCallback(reloadCallback),
  188. callback(callback_),
  189. visUpdateCb(visUpdateCb_),
  190. minSize(minSize_)
  191. {
  192. setFrameShape(QFrame::NoFrame);
  193. QMetaObject::invokeMethod(this, "ReloadProperties",
  194. Qt::QueuedConnection);
  195. }
  196. OBSPropertiesView::OBSPropertiesView(OBSData settings_, const char *type_,
  197. PropertiesReloadCallback reloadCallback_,
  198. int minSize_)
  199. : VScrollArea(nullptr),
  200. properties(nullptr, obs_properties_destroy),
  201. settings(settings_),
  202. type(type_),
  203. reloadCallback(reloadCallback_),
  204. minSize(minSize_)
  205. {
  206. setFrameShape(QFrame::NoFrame);
  207. QMetaObject::invokeMethod(this, "ReloadProperties",
  208. Qt::QueuedConnection);
  209. }
  210. void OBSPropertiesView::SetDisabled(bool disabled)
  211. {
  212. for (auto child : findChildren<QWidget *>()) {
  213. child->setDisabled(disabled);
  214. }
  215. }
  216. void OBSPropertiesView::resizeEvent(QResizeEvent *event)
  217. {
  218. emit PropertiesResized();
  219. VScrollArea::resizeEvent(event);
  220. }
  221. template<typename Sender, typename SenderParent, typename... Args>
  222. QWidget *OBSPropertiesView::NewWidget(obs_property_t *prop, Sender *widget,
  223. void (SenderParent::*signal)(Args...))
  224. {
  225. const char *long_desc = obs_property_long_description(prop);
  226. WidgetInfo *info = new WidgetInfo(this, prop, widget);
  227. QObject::connect(widget, signal, info, &WidgetInfo::ControlChanged);
  228. children.emplace_back(info);
  229. widget->setToolTip(QT_UTF8(long_desc));
  230. return widget;
  231. }
  232. QWidget *OBSPropertiesView::AddCheckbox(obs_property_t *prop)
  233. {
  234. const char *name = obs_property_name(prop);
  235. const char *desc = obs_property_description(prop);
  236. const char *long_desc = obs_property_long_description(prop);
  237. bool val = obs_data_get_bool(settings, name);
  238. QCheckBox *checkbox = new QCheckBox(QT_UTF8(desc));
  239. checkbox->setCheckState(val ? Qt::Checked : Qt::Unchecked);
  240. #if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)
  241. QWidget *widget =
  242. NewWidget(prop, checkbox, &QCheckBox::checkStateChanged);
  243. #else
  244. QWidget *widget = NewWidget(prop, checkbox, &QCheckBox::stateChanged);
  245. #endif
  246. if (!long_desc) {
  247. return widget;
  248. }
  249. QString file = !obs_frontend_is_theme_dark()
  250. ? ":/res/images/help.svg"
  251. : ":/res/images/help_light.svg";
  252. IconLabel *help = new IconLabel(checkbox);
  253. help->setIcon(QIcon(file));
  254. help->setToolTip(long_desc);
  255. #ifdef __APPLE__
  256. checkbox->setAttribute(Qt::WA_LayoutUsesWidgetRect);
  257. #endif
  258. widget = new QWidget();
  259. QHBoxLayout *layout = new QHBoxLayout(widget);
  260. layout->setContentsMargins(0, 0, 0, 0);
  261. layout->setAlignment(Qt::AlignLeft);
  262. layout->setSpacing(0);
  263. layout->addWidget(checkbox);
  264. layout->addWidget(help);
  265. widget->setLayout(layout);
  266. return widget;
  267. }
  268. QWidget *OBSPropertiesView::AddText(obs_property_t *prop, QFormLayout *layout,
  269. QLabel *&label)
  270. {
  271. const char *name = obs_property_name(prop);
  272. const char *val = obs_data_get_string(settings, name);
  273. bool monospace = obs_property_text_monospace(prop);
  274. obs_text_type type = obs_property_text_type(prop);
  275. if (type == OBS_TEXT_MULTILINE) {
  276. OBSPlainTextEdit *edit = new OBSPlainTextEdit(this, monospace);
  277. edit->setPlainText(QT_UTF8(val));
  278. edit->setTabStopDistance(40);
  279. return NewWidget(prop, edit, &OBSPlainTextEdit::textChanged);
  280. } else if (type == OBS_TEXT_PASSWORD) {
  281. QLayout *subLayout = new QHBoxLayout();
  282. QLineEdit *edit = new QLineEdit();
  283. QPushButton *show = new QPushButton();
  284. show->setText(tr("Show"));
  285. show->setCheckable(true);
  286. edit->setText(QT_UTF8(val));
  287. edit->setEchoMode(QLineEdit::Password);
  288. subLayout->addWidget(edit);
  289. subLayout->addWidget(show);
  290. WidgetInfo *info = new WidgetInfo(this, prop, edit);
  291. connect(show, &QAbstractButton::toggled, info,
  292. &WidgetInfo::TogglePasswordText);
  293. connect(show, &QAbstractButton::toggled, [=](bool hide) {
  294. show->setText(hide ? tr("Hide") : tr("Show"));
  295. });
  296. children.emplace_back(info);
  297. label = new QLabel(QT_UTF8(obs_property_description(prop)));
  298. layout->addRow(label, subLayout);
  299. edit->setToolTip(QT_UTF8(obs_property_long_description(prop)));
  300. connect(edit, &QLineEdit::textEdited, info,
  301. &WidgetInfo::ControlChanged);
  302. return nullptr;
  303. } else if (type == OBS_TEXT_INFO) {
  304. QString desc = QT_UTF8(obs_property_description(prop));
  305. const char *long_desc = obs_property_long_description(prop);
  306. obs_text_info_type info_type =
  307. obs_property_text_info_type(prop);
  308. QLabel *info_label = new QLabel(QT_UTF8(val));
  309. if (info_label->text().isEmpty() && long_desc == NULL) {
  310. label = nullptr;
  311. info_label->setText(desc);
  312. } else
  313. label = new QLabel(desc);
  314. if (long_desc != NULL && !info_label->text().isEmpty()) {
  315. QString file = !obs_frontend_is_theme_dark()
  316. ? ":/res/images/help.svg"
  317. : ":/res/images/help_light.svg";
  318. QString lStr = "<html>%1 <img src='%2' style=' \
  319. vertical-align: bottom; ' /></html>";
  320. info_label->setText(lStr.arg(info_label->text(), file));
  321. info_label->setToolTip(QT_UTF8(long_desc));
  322. } else if (long_desc != NULL) {
  323. info_label->setText(QT_UTF8(long_desc));
  324. }
  325. info_label->setOpenExternalLinks(true);
  326. info_label->setWordWrap(obs_property_text_info_word_wrap(prop));
  327. if (info_type == OBS_TEXT_INFO_WARNING)
  328. info_label->setProperty("class", "text-warning");
  329. else if (info_type == OBS_TEXT_INFO_ERROR)
  330. info_label->setProperty("class", "text-danger");
  331. if (label)
  332. label->setObjectName(info_label->objectName());
  333. WidgetInfo *info = new WidgetInfo(this, prop, info_label);
  334. children.emplace_back(info);
  335. layout->addRow(label, info_label);
  336. return nullptr;
  337. }
  338. QLineEdit *edit = new QLineEdit();
  339. edit->setText(QT_UTF8(val));
  340. edit->setToolTip(QT_UTF8(obs_property_long_description(prop)));
  341. return NewWidget(prop, edit, &QLineEdit::textEdited);
  342. }
  343. void OBSPropertiesView::AddPath(obs_property_t *prop, QFormLayout *layout,
  344. QLabel **label)
  345. {
  346. const char *name = obs_property_name(prop);
  347. const char *val = obs_data_get_string(settings, name);
  348. QLayout *subLayout = new QHBoxLayout();
  349. QLineEdit *edit = new QLineEdit();
  350. QPushButton *button = new QPushButton(tr("Browse"));
  351. if (!obs_property_enabled(prop)) {
  352. edit->setEnabled(false);
  353. button->setEnabled(false);
  354. }
  355. edit->setText(QT_UTF8(val));
  356. edit->setReadOnly(true);
  357. edit->setToolTip(QT_UTF8(obs_property_long_description(prop)));
  358. subLayout->addWidget(edit);
  359. subLayout->addWidget(button);
  360. WidgetInfo *info = new WidgetInfo(this, prop, edit);
  361. connect(button, &QPushButton::clicked, info,
  362. &WidgetInfo::ControlChanged);
  363. children.emplace_back(info);
  364. *label = new QLabel(QT_UTF8(obs_property_description(prop)));
  365. layout->addRow(*label, subLayout);
  366. }
  367. void OBSPropertiesView::AddInt(obs_property_t *prop, QFormLayout *layout,
  368. QLabel **label)
  369. {
  370. obs_number_type type = obs_property_int_type(prop);
  371. QLayout *subLayout = new QHBoxLayout();
  372. const char *name = obs_property_name(prop);
  373. int val = (int)obs_data_get_int(settings, name);
  374. QSpinBox *spin = new SpinBoxIgnoreScroll();
  375. spin->setEnabled(obs_property_enabled(prop));
  376. int minVal = obs_property_int_min(prop);
  377. int maxVal = obs_property_int_max(prop);
  378. int stepVal = obs_property_int_step(prop);
  379. const char *suffix = obs_property_int_suffix(prop);
  380. spin->setMinimum(minVal);
  381. spin->setMaximum(maxVal);
  382. spin->setSingleStep(stepVal);
  383. spin->setValue(val);
  384. spin->setToolTip(QT_UTF8(obs_property_long_description(prop)));
  385. spin->setSuffix(QT_UTF8(suffix));
  386. WidgetInfo *info = new WidgetInfo(this, prop, spin);
  387. children.emplace_back(info);
  388. if (type == OBS_NUMBER_SLIDER) {
  389. QSlider *slider = new SliderIgnoreScroll();
  390. slider->setMinimum(minVal);
  391. slider->setMaximum(maxVal);
  392. slider->setPageStep(stepVal);
  393. slider->setValue(val);
  394. slider->setOrientation(Qt::Horizontal);
  395. slider->setEnabled(obs_property_enabled(prop));
  396. subLayout->addWidget(slider);
  397. connect(slider, &QSlider::valueChanged, spin,
  398. &QSpinBox::setValue);
  399. connect(spin, &QSpinBox::valueChanged, slider,
  400. &QSlider::setValue);
  401. }
  402. connect(spin, &QSpinBox::valueChanged, info,
  403. &WidgetInfo::ControlChanged);
  404. subLayout->addWidget(spin);
  405. *label = new QLabel(QT_UTF8(obs_property_description(prop)));
  406. layout->addRow(*label, subLayout);
  407. }
  408. void OBSPropertiesView::AddFloat(obs_property_t *prop, QFormLayout *layout,
  409. QLabel **label)
  410. {
  411. obs_number_type type = obs_property_float_type(prop);
  412. QLayout *subLayout = new QHBoxLayout();
  413. const char *name = obs_property_name(prop);
  414. double val = obs_data_get_double(settings, name);
  415. QDoubleSpinBox *spin = new QDoubleSpinBox();
  416. if (!obs_property_enabled(prop))
  417. spin->setEnabled(false);
  418. double minVal = obs_property_float_min(prop);
  419. double maxVal = obs_property_float_max(prop);
  420. double stepVal = obs_property_float_step(prop);
  421. const char *suffix = obs_property_float_suffix(prop);
  422. if (stepVal < 1.0) {
  423. constexpr int sane_limit = 8;
  424. const int decimals =
  425. std::min<int>(log10(1.0 / stepVal) + 0.99, sane_limit);
  426. if (decimals > spin->decimals())
  427. spin->setDecimals(decimals);
  428. }
  429. spin->setMinimum(minVal);
  430. spin->setMaximum(maxVal);
  431. spin->setSingleStep(stepVal);
  432. spin->setValue(val);
  433. spin->setToolTip(QT_UTF8(obs_property_long_description(prop)));
  434. spin->setSuffix(QT_UTF8(suffix));
  435. WidgetInfo *info = new WidgetInfo(this, prop, spin);
  436. children.emplace_back(info);
  437. if (type == OBS_NUMBER_SLIDER) {
  438. DoubleSlider *slider = new DoubleSlider();
  439. slider->setDoubleConstraints(minVal, maxVal, stepVal, val);
  440. slider->setOrientation(Qt::Horizontal);
  441. subLayout->addWidget(slider);
  442. connect(slider, &DoubleSlider::doubleValChanged, spin,
  443. &QDoubleSpinBox::setValue);
  444. connect(spin, &QDoubleSpinBox::valueChanged, slider,
  445. &DoubleSlider::setDoubleVal);
  446. }
  447. connect(spin, &QDoubleSpinBox::valueChanged, info,
  448. &WidgetInfo::ControlChanged);
  449. subLayout->addWidget(spin);
  450. *label = new QLabel(QT_UTF8(obs_property_description(prop)));
  451. layout->addRow(*label, subLayout);
  452. }
  453. static QVariant propertyListToQVariant(obs_property_t *prop, size_t idx)
  454. {
  455. obs_combo_format format = obs_property_list_format(prop);
  456. QVariant var;
  457. if (format == OBS_COMBO_FORMAT_INT) {
  458. long long val = obs_property_list_item_int(prop, idx);
  459. var = QVariant::fromValue<long long>(val);
  460. } else if (format == OBS_COMBO_FORMAT_FLOAT) {
  461. double val = obs_property_list_item_float(prop, idx);
  462. var = QVariant::fromValue<double>(val);
  463. } else if (format == OBS_COMBO_FORMAT_STRING) {
  464. var = QByteArray(obs_property_list_item_string(prop, idx));
  465. } else if (format == OBS_COMBO_FORMAT_BOOL) {
  466. bool val = obs_property_list_item_bool(prop, idx);
  467. var = QVariant::fromValue<bool>(val);
  468. }
  469. return var;
  470. }
  471. static void AddComboItem(QComboBox *combo, obs_property_t *prop, size_t idx)
  472. {
  473. const char *name = obs_property_list_item_name(prop, idx);
  474. QVariant var = propertyListToQVariant(prop, idx);
  475. combo->addItem(QT_UTF8(name), var);
  476. if (!obs_property_list_item_disabled(prop, idx))
  477. return;
  478. int index = combo->findText(QT_UTF8(name));
  479. if (index < 0)
  480. return;
  481. QStandardItemModel *model =
  482. dynamic_cast<QStandardItemModel *>(combo->model());
  483. if (!model)
  484. return;
  485. QStandardItem *item = model->item(index);
  486. item->setFlags(Qt::NoItemFlags);
  487. }
  488. static void AddRadioItem(QButtonGroup *buttonGroup, QFormLayout *layout,
  489. obs_property_t *prop, QVariant value, size_t idx)
  490. {
  491. const char *name = obs_property_list_item_name(prop, idx);
  492. QVariant var = propertyListToQVariant(prop, idx);
  493. QRadioButton *button = new QRadioButton(name);
  494. button->setChecked(value == var);
  495. button->setProperty("value", var);
  496. buttonGroup->addButton(button);
  497. layout->addRow(button);
  498. }
  499. template<long long get_int(obs_data_t *, const char *),
  500. double get_double(obs_data_t *, const char *),
  501. const char *get_string(obs_data_t *, const char *),
  502. bool get_bool(obs_data_t *, const char *)>
  503. static QVariant from_obs_data(obs_data_t *data, const char *name,
  504. obs_combo_format format)
  505. {
  506. switch (format) {
  507. case OBS_COMBO_FORMAT_INT:
  508. return QVariant::fromValue(get_int(data, name));
  509. case OBS_COMBO_FORMAT_FLOAT:
  510. return QVariant::fromValue(get_double(data, name));
  511. case OBS_COMBO_FORMAT_STRING:
  512. return QByteArray(get_string(data, name));
  513. case OBS_COMBO_FORMAT_BOOL:
  514. return QVariant::fromValue(get_bool(data, name));
  515. default:
  516. return QVariant();
  517. }
  518. }
  519. static QVariant from_obs_data(obs_data_t *data, const char *name,
  520. obs_combo_format format)
  521. {
  522. return from_obs_data<obs_data_get_int, obs_data_get_double,
  523. obs_data_get_string, obs_data_get_bool>(data, name,
  524. format);
  525. }
  526. static QVariant from_obs_data_autoselect(obs_data_t *data, const char *name,
  527. obs_combo_format format)
  528. {
  529. return from_obs_data<
  530. obs_data_get_autoselect_int, obs_data_get_autoselect_double,
  531. obs_data_get_autoselect_string, obs_data_get_autoselect_bool>(
  532. data, name, format);
  533. }
  534. QWidget *OBSPropertiesView::AddList(obs_property_t *prop, bool &warning)
  535. {
  536. const char *name = obs_property_name(prop);
  537. obs_combo_type type = obs_property_list_type(prop);
  538. obs_combo_format format = obs_property_list_format(prop);
  539. size_t count = obs_property_list_item_count(prop);
  540. QVariant value = from_obs_data(settings, name, format);
  541. if (type == OBS_COMBO_TYPE_RADIO) {
  542. QButtonGroup *buttonGroup = new QButtonGroup();
  543. QFormLayout *subLayout = new QFormLayout();
  544. subLayout->setContentsMargins(0, 0, 0, 0);
  545. for (size_t idx = 0; idx < count; idx++)
  546. AddRadioItem(buttonGroup, subLayout, prop, value, idx);
  547. if (count > 0) {
  548. buttonGroup->setExclusive(true);
  549. WidgetInfo *info = new WidgetInfo(
  550. this, prop, buttonGroup->buttons()[0]);
  551. children.emplace_back(info);
  552. connect(buttonGroup, &QButtonGroup::buttonClicked, info,
  553. &WidgetInfo::ControlChanged);
  554. }
  555. QWidget *widget = new QWidget();
  556. widget->setLayout(subLayout);
  557. return widget;
  558. }
  559. int idx = -1;
  560. QComboBox *combo = new QComboBox();
  561. for (size_t i = 0; i < count; i++)
  562. AddComboItem(combo, prop, i);
  563. if (type == OBS_COMBO_TYPE_EDITABLE)
  564. combo->setEditable(true);
  565. combo->setMaxVisibleItems(40);
  566. combo->setToolTip(QT_UTF8(obs_property_long_description(prop)));
  567. if (format == OBS_COMBO_FORMAT_STRING &&
  568. type == OBS_COMBO_TYPE_EDITABLE) {
  569. combo->lineEdit()->setText(value.toString());
  570. } else {
  571. idx = combo->findData(value);
  572. }
  573. if (type == OBS_COMBO_TYPE_EDITABLE)
  574. return NewWidget(prop, combo, &QComboBox::editTextChanged);
  575. if (idx != -1)
  576. combo->setCurrentIndex(idx);
  577. if (obs_data_has_autoselect_value(settings, name)) {
  578. QVariant autoselect =
  579. from_obs_data_autoselect(settings, name, format);
  580. int id = combo->findData(autoselect);
  581. if (id != -1 && id != idx) {
  582. QString actual = combo->itemText(id);
  583. QString selected = combo->itemText(idx);
  584. QString combined =
  585. tr("Basic.PropertiesWindow.AutoSelectFormat");
  586. combo->setItemText(idx,
  587. combined.arg(selected).arg(actual));
  588. }
  589. }
  590. QAbstractItemModel *model = combo->model();
  591. warning = idx != -1 &&
  592. model->flags(model->index(idx, 0)) == Qt::NoItemFlags;
  593. WidgetInfo *info = new WidgetInfo(this, prop, combo);
  594. connect(combo, &QComboBox::currentIndexChanged, info,
  595. &WidgetInfo::ControlChanged);
  596. children.emplace_back(info);
  597. /* trigger a settings update if the index was not found */
  598. if (count && idx == -1)
  599. info->ControlChanged();
  600. return combo;
  601. }
  602. static void NewButton(QLayout *layout, WidgetInfo *info, const char *themeIcon,
  603. void (WidgetInfo::*method)())
  604. {
  605. QPushButton *button = new QPushButton();
  606. button->setProperty("class", themeIcon);
  607. button->setFlat(true);
  608. button->setProperty("toolButton", true);
  609. QObject::connect(button, &QPushButton::clicked, info, method);
  610. layout->addWidget(button);
  611. }
  612. void OBSPropertiesView::AddEditableList(obs_property_t *prop,
  613. QFormLayout *layout, QLabel *&label)
  614. {
  615. const char *name = obs_property_name(prop);
  616. OBSDataArrayAutoRelease array = obs_data_get_array(settings, name);
  617. QListWidget *list = new QListWidget();
  618. size_t count = obs_data_array_count(array);
  619. if (!obs_property_enabled(prop))
  620. list->setEnabled(false);
  621. list->setSortingEnabled(false);
  622. list->setSelectionMode(QAbstractItemView::ExtendedSelection);
  623. list->setToolTip(QT_UTF8(obs_property_long_description(prop)));
  624. list->setSpacing(1);
  625. for (size_t i = 0; i < count; i++) {
  626. OBSDataAutoRelease item = obs_data_array_item(array, i);
  627. list->addItem(QT_UTF8(obs_data_get_string(item, "value")));
  628. QListWidgetItem *const list_item = list->item((int)i);
  629. list_item->setSelected(obs_data_get_bool(item, "selected"));
  630. list_item->setHidden(obs_data_get_bool(item, "hidden"));
  631. QString uuid = QT_UTF8(obs_data_get_string(item, "uuid"));
  632. /* for backwards compatibility */
  633. if (uuid.isEmpty()) {
  634. uuid = QUuid::createUuid().toString(
  635. QUuid::WithoutBraces);
  636. obs_data_set_string(item, "uuid", uuid.toUtf8());
  637. }
  638. list_item->setData(Qt::UserRole, uuid);
  639. }
  640. WidgetInfo *info = new WidgetInfo(this, prop, list);
  641. list->setDragDropMode(QAbstractItemView::InternalMove);
  642. connect(list->model(), &QAbstractItemModel::rowsMoved,
  643. [info]() { info->EditableListChanged(); });
  644. QVBoxLayout *sideLayout = new QVBoxLayout();
  645. NewButton(sideLayout, info, "icon-plus", &WidgetInfo::EditListAdd);
  646. NewButton(sideLayout, info, "icon-trash", &WidgetInfo::EditListRemove);
  647. NewButton(sideLayout, info, "icon-gear", &WidgetInfo::EditListEdit);
  648. NewButton(sideLayout, info, "icon-up", &WidgetInfo::EditListUp);
  649. NewButton(sideLayout, info, "icon-down", &WidgetInfo::EditListDown);
  650. sideLayout->addStretch(0);
  651. QHBoxLayout *subLayout = new QHBoxLayout();
  652. subLayout->addWidget(list);
  653. subLayout->addLayout(sideLayout);
  654. children.emplace_back(info);
  655. label = new QLabel(QT_UTF8(obs_property_description(prop)));
  656. layout->addRow(label, subLayout);
  657. }
  658. QWidget *OBSPropertiesView::AddButton(obs_property_t *prop)
  659. {
  660. const char *desc = obs_property_description(prop);
  661. QPushButton *button = new QPushButton(QT_UTF8(desc));
  662. button->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum);
  663. return NewWidget(prop, button, &QPushButton::clicked);
  664. }
  665. void OBSPropertiesView::AddColorInternal(obs_property_t *prop,
  666. QFormLayout *layout, QLabel *&label,
  667. bool supportAlpha)
  668. {
  669. QPushButton *button = new QPushButton;
  670. QLabel *colorLabel = new QLabel;
  671. const char *name = obs_property_name(prop);
  672. long long val = obs_data_get_int(settings, name);
  673. QColor color = color_from_int(val);
  674. QColor::NameFormat format;
  675. if (!obs_property_enabled(prop)) {
  676. button->setEnabled(false);
  677. colorLabel->setEnabled(false);
  678. }
  679. button->setText(tr("Basic.PropertiesWindow.SelectColor"));
  680. button->setToolTip(QT_UTF8(obs_property_long_description(prop)));
  681. if (supportAlpha) {
  682. format = QColor::HexArgb;
  683. } else {
  684. format = QColor::HexRgb;
  685. color.setAlpha(255);
  686. }
  687. QPalette palette = QPalette(color);
  688. colorLabel->setFrameStyle(QFrame::Sunken | QFrame::Panel);
  689. colorLabel->setText(color.name(format));
  690. colorLabel->setPalette(palette);
  691. colorLabel->setStyleSheet(
  692. QString("background-color :%1; color: %2;")
  693. .arg(palette.color(QPalette::Window).name(format))
  694. .arg(palette.color(QPalette::WindowText).name(format)));
  695. colorLabel->setAutoFillBackground(true);
  696. colorLabel->setAlignment(Qt::AlignCenter);
  697. colorLabel->setToolTip(QT_UTF8(obs_property_long_description(prop)));
  698. QHBoxLayout *subLayout = new QHBoxLayout;
  699. subLayout->setContentsMargins(0, 0, 0, 0);
  700. subLayout->addWidget(colorLabel);
  701. subLayout->addWidget(button);
  702. WidgetInfo *info = new WidgetInfo(this, prop, colorLabel);
  703. connect(button, &QPushButton::clicked, info,
  704. &WidgetInfo::ControlChanged);
  705. children.emplace_back(info);
  706. label = new QLabel(QT_UTF8(obs_property_description(prop)));
  707. layout->addRow(label, subLayout);
  708. }
  709. void OBSPropertiesView::AddColor(obs_property_t *prop, QFormLayout *layout,
  710. QLabel *&label)
  711. {
  712. AddColorInternal(prop, layout, label, false);
  713. }
  714. void OBSPropertiesView::AddColorAlpha(obs_property_t *prop, QFormLayout *layout,
  715. QLabel *&label)
  716. {
  717. AddColorInternal(prop, layout, label, true);
  718. }
  719. void MakeQFont(obs_data_t *font_obj, QFont &font, bool limit = false)
  720. {
  721. const char *face = obs_data_get_string(font_obj, "face");
  722. const char *style = obs_data_get_string(font_obj, "style");
  723. int size = (int)obs_data_get_int(font_obj, "size");
  724. uint32_t flags = (uint32_t)obs_data_get_int(font_obj, "flags");
  725. if (face) {
  726. font.setFamily(face);
  727. font.setStyleName(style);
  728. }
  729. if (size) {
  730. if (limit) {
  731. int max_size = font.pointSize();
  732. if (max_size < 28)
  733. max_size = 28;
  734. if (size > max_size)
  735. size = max_size;
  736. }
  737. font.setPointSize(size);
  738. }
  739. if (flags & OBS_FONT_BOLD)
  740. font.setBold(true);
  741. if (flags & OBS_FONT_ITALIC)
  742. font.setItalic(true);
  743. if (flags & OBS_FONT_UNDERLINE)
  744. font.setUnderline(true);
  745. if (flags & OBS_FONT_STRIKEOUT)
  746. font.setStrikeOut(true);
  747. }
  748. void OBSPropertiesView::AddFont(obs_property_t *prop, QFormLayout *layout,
  749. QLabel *&label)
  750. {
  751. const char *name = obs_property_name(prop);
  752. OBSDataAutoRelease font_obj = obs_data_get_obj(settings, name);
  753. const char *face = obs_data_get_string(font_obj, "face");
  754. const char *style = obs_data_get_string(font_obj, "style");
  755. QPushButton *button = new QPushButton;
  756. QLabel *fontLabel = new QLabel;
  757. QFont font;
  758. if (!obs_property_enabled(prop)) {
  759. button->setEnabled(false);
  760. fontLabel->setEnabled(false);
  761. }
  762. font = fontLabel->font();
  763. MakeQFont(font_obj, font, true);
  764. button->setText(tr("Basic.PropertiesWindow.SelectFont"));
  765. button->setToolTip(QT_UTF8(obs_property_long_description(prop)));
  766. fontLabel->setFrameStyle(QFrame::Sunken | QFrame::Panel);
  767. fontLabel->setFont(font);
  768. fontLabel->setText(QString("%1 %2").arg(face, style));
  769. fontLabel->setAlignment(Qt::AlignCenter);
  770. fontLabel->setToolTip(QT_UTF8(obs_property_long_description(prop)));
  771. QHBoxLayout *subLayout = new QHBoxLayout;
  772. subLayout->setContentsMargins(0, 0, 0, 0);
  773. subLayout->addWidget(fontLabel);
  774. subLayout->addWidget(button);
  775. WidgetInfo *info = new WidgetInfo(this, prop, fontLabel);
  776. connect(button, &QPushButton::clicked, info,
  777. &WidgetInfo::ControlChanged);
  778. children.emplace_back(info);
  779. label = new QLabel(QT_UTF8(obs_property_description(prop)));
  780. layout->addRow(label, subLayout);
  781. }
  782. namespace std {
  783. template<> struct default_delete<obs_data_t> {
  784. void operator()(obs_data_t *data) { obs_data_release(data); }
  785. };
  786. template<> struct default_delete<obs_data_item_t> {
  787. void operator()(obs_data_item_t *item) { obs_data_item_release(&item); }
  788. };
  789. } // namespace std
  790. template<typename T> static double make_epsilon(T val)
  791. {
  792. return val * 0.00001;
  793. }
  794. static bool matches_range(media_frames_per_second &match,
  795. media_frames_per_second fps,
  796. const frame_rate_range_t &pair)
  797. {
  798. auto val = media_frames_per_second_to_frame_interval(fps);
  799. auto max_ = media_frames_per_second_to_frame_interval(pair.first);
  800. auto min_ = media_frames_per_second_to_frame_interval(pair.second);
  801. if (min_ <= val && val <= max_) {
  802. match = fps;
  803. return true;
  804. }
  805. return false;
  806. }
  807. static bool matches_ranges(media_frames_per_second &best_match,
  808. media_frames_per_second fps,
  809. const frame_rate_ranges_t &fps_ranges,
  810. bool exact = false)
  811. {
  812. auto convert_fn = media_frames_per_second_to_frame_interval;
  813. auto val = convert_fn(fps);
  814. auto epsilon = make_epsilon(val);
  815. bool match = false;
  816. auto best_dist = numeric_limits<double>::max();
  817. for (auto &pair : fps_ranges) {
  818. auto max_ = convert_fn(pair.first);
  819. auto min_ = convert_fn(pair.second);
  820. /*blog(LOG_INFO, "%lg <= %lg <= %lg? %s %s %s",
  821. min_, val, max_,
  822. fabsl(min_ - val) < epsilon ? "true" : "false",
  823. min_ <= val && val <= max_ ? "true" : "false",
  824. fabsl(min_ - val) < epsilon ? "true" :
  825. "false");*/
  826. if (matches_range(best_match, fps, pair))
  827. return true;
  828. if (exact)
  829. continue;
  830. auto min_dist = fabsl(min_ - val);
  831. auto max_dist = fabsl(max_ - val);
  832. if (min_dist < epsilon && min_dist < best_dist) {
  833. best_match = pair.first;
  834. match = true;
  835. continue;
  836. }
  837. if (max_dist < epsilon && max_dist < best_dist) {
  838. best_match = pair.second;
  839. match = true;
  840. continue;
  841. }
  842. }
  843. return match;
  844. }
  845. static media_frames_per_second make_fps(uint32_t num, uint32_t den)
  846. {
  847. media_frames_per_second fps{};
  848. fps.numerator = num;
  849. fps.denominator = den;
  850. return fps;
  851. }
  852. static const common_frame_rate common_fps[] = {
  853. {"240", {240, 1}}, {"144", {144, 1}},
  854. {"120", {120, 1}}, {"119.88", {120000, 1001}},
  855. {"60", {60, 1}}, {"59.94", {60000, 1001}},
  856. {"50", {50, 1}}, {"48", {48, 1}},
  857. {"30", {30, 1}}, {"29.97", {30000, 1001}},
  858. {"25", {25, 1}}, {"24", {24, 1}},
  859. {"23.976", {24000, 1001}},
  860. };
  861. static void UpdateSimpleFPSSelection(OBSFrameRatePropertyWidget *fpsProps,
  862. const media_frames_per_second *current_fps)
  863. {
  864. if (!current_fps || !media_frames_per_second_is_valid(*current_fps)) {
  865. fpsProps->simpleFPS->setCurrentIndex(0);
  866. return;
  867. }
  868. auto combo = fpsProps->simpleFPS;
  869. auto num = combo->count();
  870. for (int i = 0; i < num; i++) {
  871. auto variant = combo->itemData(i);
  872. if (!variant.canConvert<media_frames_per_second>())
  873. continue;
  874. auto fps = variant.value<media_frames_per_second>();
  875. if (fps != *current_fps)
  876. continue;
  877. combo->setCurrentIndex(i);
  878. return;
  879. }
  880. combo->setCurrentIndex(0);
  881. }
  882. static void AddFPSRanges(vector<common_frame_rate> &items,
  883. const frame_rate_ranges_t &ranges)
  884. {
  885. auto InsertFPS = [&](media_frames_per_second fps) {
  886. auto fps_val = media_frames_per_second_to_fps(fps);
  887. auto end_ = end(items);
  888. auto i = begin(items);
  889. for (; i != end_; i++) {
  890. auto i_fps_val = media_frames_per_second_to_fps(i->fps);
  891. if (fabsl(i_fps_val - fps_val) < 0.01)
  892. return;
  893. if (i_fps_val > fps_val)
  894. continue;
  895. break;
  896. }
  897. items.insert(i, {nullptr, fps});
  898. };
  899. for (auto &range : ranges) {
  900. InsertFPS(range.first);
  901. InsertFPS(range.second);
  902. }
  903. }
  904. static QWidget *
  905. CreateSimpleFPSValues(OBSFrameRatePropertyWidget *fpsProps, bool &selected,
  906. const media_frames_per_second *current_fps)
  907. {
  908. auto widget = new QWidget{};
  909. widget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
  910. auto layout = new QVBoxLayout{};
  911. layout->setContentsMargins(0, 0, 0, 0);
  912. auto items = vector<common_frame_rate>{};
  913. items.reserve(sizeof(common_fps) / sizeof(common_frame_rate));
  914. auto combo = fpsProps->simpleFPS = new QComboBox();
  915. combo->addItem("", QVariant::fromValue(make_fps(0, 0)));
  916. for (const auto &fps : common_fps) {
  917. media_frames_per_second best_match{};
  918. if (!matches_ranges(best_match, fps.fps, fpsProps->fps_ranges))
  919. continue;
  920. items.push_back({fps.fps_name, best_match});
  921. }
  922. AddFPSRanges(items, fpsProps->fps_ranges);
  923. for (const auto &item : items) {
  924. auto var = QVariant::fromValue(item.fps);
  925. auto name = item.fps_name
  926. ? QString(item.fps_name)
  927. : QString("%1").arg(
  928. media_frames_per_second_to_fps(
  929. item.fps));
  930. combo->addItem(name, var);
  931. bool select = current_fps && *current_fps == item.fps;
  932. if (select) {
  933. combo->setCurrentIndex(combo->count() - 1);
  934. selected = true;
  935. }
  936. }
  937. layout->addWidget(combo, 0, Qt::AlignTop);
  938. widget->setLayout(layout);
  939. return widget;
  940. }
  941. static void UpdateRationalFPSWidgets(OBSFrameRatePropertyWidget *fpsProps,
  942. const media_frames_per_second *current_fps)
  943. {
  944. if (!current_fps || !media_frames_per_second_is_valid(*current_fps)) {
  945. fpsProps->numEdit->setValue(0);
  946. fpsProps->denEdit->setValue(0);
  947. return;
  948. }
  949. auto combo = fpsProps->fpsRange;
  950. auto num = combo->count();
  951. for (int i = 0; i < num; i++) {
  952. auto variant = combo->itemData(i);
  953. if (!variant.canConvert<size_t>())
  954. continue;
  955. auto idx = variant.value<size_t>();
  956. if (fpsProps->fps_ranges.size() < idx)
  957. continue;
  958. media_frames_per_second match{};
  959. if (!matches_range(match, *current_fps,
  960. fpsProps->fps_ranges[idx]))
  961. continue;
  962. combo->setCurrentIndex(i);
  963. break;
  964. }
  965. fpsProps->numEdit->setValue(current_fps->numerator);
  966. fpsProps->denEdit->setValue(current_fps->denominator);
  967. }
  968. static QWidget *CreateRationalFPS(OBSFrameRatePropertyWidget *fpsProps,
  969. bool &selected,
  970. const media_frames_per_second *current_fps)
  971. {
  972. auto widget = new QWidget{};
  973. widget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
  974. auto layout = new QFormLayout{};
  975. layout->setContentsMargins(0, 0, 0, 0);
  976. layout->setSpacing(4);
  977. auto str = QObject::tr("Basic.PropertiesView.FPS.ValidFPSRanges");
  978. auto rlabel = new QLabel{str};
  979. auto combo = fpsProps->fpsRange = new QComboBox();
  980. auto convert_fps = media_frames_per_second_to_fps;
  981. //auto convert_fi = media_frames_per_second_to_frame_interval;
  982. for (size_t i = 0; i < fpsProps->fps_ranges.size(); i++) {
  983. auto &pair = fpsProps->fps_ranges[i];
  984. combo->addItem(QString{"%1 - %2"}
  985. .arg(convert_fps(pair.first))
  986. .arg(convert_fps(pair.second)),
  987. QVariant::fromValue(i));
  988. media_frames_per_second match;
  989. if (!current_fps || !matches_range(match, *current_fps, pair))
  990. continue;
  991. combo->setCurrentIndex(combo->count() - 1);
  992. selected = true;
  993. }
  994. layout->addRow(rlabel, combo);
  995. auto num_edit = fpsProps->numEdit = new SpinBoxIgnoreScroll{};
  996. auto den_edit = fpsProps->denEdit = new SpinBoxIgnoreScroll{};
  997. num_edit->setRange(0, INT_MAX);
  998. den_edit->setRange(0, INT_MAX);
  999. if (current_fps) {
  1000. num_edit->setValue(current_fps->numerator);
  1001. den_edit->setValue(current_fps->denominator);
  1002. }
  1003. layout->addRow(QObject::tr("Basic.Settings.Video.Numerator"), num_edit);
  1004. layout->addRow(QObject::tr("Basic.Settings.Video.Denominator"),
  1005. den_edit);
  1006. widget->setLayout(layout);
  1007. return widget;
  1008. }
  1009. static OBSFrameRatePropertyWidget *
  1010. CreateFrameRateWidget(obs_property_t *prop, bool &warning, const char *option,
  1011. media_frames_per_second *current_fps,
  1012. frame_rate_ranges_t &fps_ranges)
  1013. {
  1014. auto widget = new OBSFrameRatePropertyWidget{};
  1015. auto hlayout = new QHBoxLayout{};
  1016. hlayout->setContentsMargins(0, 0, 0, 0);
  1017. swap(widget->fps_ranges, fps_ranges);
  1018. auto combo = widget->modeSelect = new QComboBox();
  1019. combo->addItem(QObject::tr("Basic.PropertiesView.FPS.Simple"),
  1020. QVariant::fromValue(frame_rate_tag::simple()));
  1021. combo->addItem(QObject::tr("Basic.PropertiesView.FPS.Rational"),
  1022. QVariant::fromValue(frame_rate_tag::rational()));
  1023. combo->setToolTip(QT_UTF8(obs_property_long_description(prop)));
  1024. auto num = obs_property_frame_rate_options_count(prop);
  1025. if (num)
  1026. combo->insertSeparator(combo->count());
  1027. bool option_found = false;
  1028. for (size_t i = 0; i < num; i++) {
  1029. auto name = obs_property_frame_rate_option_name(prop, i);
  1030. auto desc = obs_property_frame_rate_option_description(prop, i);
  1031. combo->addItem(desc, QVariant::fromValue(frame_rate_tag{name}));
  1032. if (!name || !option || string(name) != option)
  1033. continue;
  1034. option_found = true;
  1035. combo->setCurrentIndex(combo->count() - 1);
  1036. }
  1037. hlayout->addWidget(combo, 0, Qt::AlignTop);
  1038. auto stack = widget->modeDisplay = new QStackedWidget{};
  1039. bool match_found = option_found;
  1040. auto AddWidget = [&](decltype(CreateRationalFPS) func) {
  1041. bool selected = false;
  1042. stack->addWidget(func(widget, selected, current_fps));
  1043. if (match_found || !selected)
  1044. return;
  1045. match_found = true;
  1046. stack->setCurrentIndex(stack->count() - 1);
  1047. combo->setCurrentIndex(stack->count() - 1);
  1048. };
  1049. AddWidget(CreateSimpleFPSValues);
  1050. AddWidget(CreateRationalFPS);
  1051. stack->addWidget(new QWidget{});
  1052. if (option_found)
  1053. stack->setCurrentIndex(stack->count() - 1);
  1054. else if (!match_found) {
  1055. int idx = current_fps ? 1 : 0; // Rational for "unsupported"
  1056. // Simple as default
  1057. stack->setCurrentIndex(idx);
  1058. combo->setCurrentIndex(idx);
  1059. warning = true;
  1060. }
  1061. hlayout->addWidget(stack, 0, Qt::AlignTop);
  1062. auto label_area = widget->labels = new QWidget{};
  1063. label_area->setSizePolicy(QSizePolicy::Expanding,
  1064. QSizePolicy::Expanding);
  1065. auto vlayout = new QVBoxLayout{};
  1066. vlayout->setContentsMargins(0, 0, 0, 0);
  1067. auto fps_label = widget->currentFPS = new QLabel{"FPS: 22"};
  1068. auto time_label = widget->timePerFrame =
  1069. new QLabel{"Frame Interval: 0.123 ms"};
  1070. auto min_label = widget->minLabel = new QLabel{"Min FPS: 1/1"};
  1071. auto max_label = widget->maxLabel = new QLabel{"Max FPS: 2/1"};
  1072. min_label->setHidden(true);
  1073. max_label->setHidden(true);
  1074. auto flags = Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard;
  1075. min_label->setTextInteractionFlags(flags);
  1076. max_label->setTextInteractionFlags(flags);
  1077. vlayout->addWidget(fps_label);
  1078. vlayout->addWidget(time_label);
  1079. vlayout->addWidget(min_label);
  1080. vlayout->addWidget(max_label);
  1081. label_area->setLayout(vlayout);
  1082. hlayout->addWidget(label_area, 0, Qt::AlignTop);
  1083. widget->setLayout(hlayout);
  1084. return widget;
  1085. }
  1086. static void UpdateMinMaxLabels(OBSFrameRatePropertyWidget *w)
  1087. {
  1088. auto Hide = [&](bool hide) {
  1089. w->minLabel->setHidden(hide);
  1090. w->maxLabel->setHidden(hide);
  1091. };
  1092. auto variant = w->modeSelect->currentData();
  1093. if (!variant.canConvert<frame_rate_tag>() ||
  1094. variant.value<frame_rate_tag>().type != frame_rate_tag::RATIONAL) {
  1095. Hide(true);
  1096. return;
  1097. }
  1098. variant = w->fpsRange->currentData();
  1099. if (!variant.canConvert<size_t>()) {
  1100. Hide(true);
  1101. return;
  1102. }
  1103. auto idx = variant.value<size_t>();
  1104. if (idx >= w->fps_ranges.size()) {
  1105. Hide(true);
  1106. return;
  1107. }
  1108. Hide(false);
  1109. auto min = w->fps_ranges[idx].first;
  1110. auto max = w->fps_ranges[idx].second;
  1111. w->minLabel->setText(QString("Min FPS: %1/%2")
  1112. .arg(min.numerator)
  1113. .arg(min.denominator));
  1114. w->maxLabel->setText(QString("Max FPS: %1/%2")
  1115. .arg(max.numerator)
  1116. .arg(max.denominator));
  1117. }
  1118. static void UpdateFPSLabels(OBSFrameRatePropertyWidget *w)
  1119. {
  1120. UpdateMinMaxLabels(w);
  1121. unique_ptr<obs_data_item_t> obj{
  1122. obs_data_item_byname(w->settings, w->name)};
  1123. media_frames_per_second fps{};
  1124. media_frames_per_second *valid_fps = nullptr;
  1125. if (obs_data_item_get_autoselect_frames_per_second(obj.get(), &fps,
  1126. nullptr) ||
  1127. obs_data_item_get_frames_per_second(obj.get(), &fps, nullptr))
  1128. valid_fps = &fps;
  1129. const char *option = nullptr;
  1130. obs_data_item_get_frames_per_second(obj.get(), nullptr, &option);
  1131. if (!valid_fps) {
  1132. w->currentFPS->setHidden(true);
  1133. w->timePerFrame->setHidden(true);
  1134. if (!option)
  1135. w->warningLabel->setProperty("class", "text-danger");
  1136. return;
  1137. }
  1138. w->currentFPS->setHidden(false);
  1139. w->timePerFrame->setHidden(false);
  1140. media_frames_per_second match{};
  1141. if (!option && !matches_ranges(match, *valid_fps, w->fps_ranges, true))
  1142. w->warningLabel->setProperty("class", "text-danger");
  1143. else
  1144. w->warningLabel->setProperty("class", "");
  1145. auto convert_to_fps = media_frames_per_second_to_fps;
  1146. auto convert_to_frame_interval =
  1147. media_frames_per_second_to_frame_interval;
  1148. w->currentFPS->setText(
  1149. QString("FPS: %1").arg(convert_to_fps(*valid_fps)));
  1150. w->timePerFrame->setText(
  1151. QString("Frame Interval: %1 ms")
  1152. .arg(convert_to_frame_interval(*valid_fps) * 1000));
  1153. }
  1154. void OBSPropertiesView::AddFrameRate(obs_property_t *prop, bool &warning,
  1155. QFormLayout *layout, QLabel *&label)
  1156. {
  1157. const char *name = obs_property_name(prop);
  1158. bool enabled = obs_property_enabled(prop);
  1159. unique_ptr<obs_data_item_t> obj{obs_data_item_byname(settings, name)};
  1160. const char *option = nullptr;
  1161. obs_data_item_get_frames_per_second(obj.get(), nullptr, &option);
  1162. media_frames_per_second fps{};
  1163. media_frames_per_second *valid_fps = nullptr;
  1164. if (obs_data_item_get_frames_per_second(obj.get(), &fps, nullptr))
  1165. valid_fps = &fps;
  1166. frame_rate_ranges_t fps_ranges;
  1167. size_t num = obs_property_frame_rate_fps_ranges_count(prop);
  1168. fps_ranges.reserve(num);
  1169. for (size_t i = 0; i < num; i++)
  1170. fps_ranges.emplace_back(
  1171. obs_property_frame_rate_fps_range_min(prop, i),
  1172. obs_property_frame_rate_fps_range_max(prop, i));
  1173. auto widget = CreateFrameRateWidget(prop, warning, option, valid_fps,
  1174. fps_ranges);
  1175. auto info = new WidgetInfo(this, prop, widget);
  1176. widget->setToolTip(QT_UTF8(obs_property_long_description(prop)));
  1177. widget->name = name;
  1178. widget->settings = settings;
  1179. widget->modeSelect->setEnabled(enabled);
  1180. widget->simpleFPS->setEnabled(enabled);
  1181. widget->fpsRange->setEnabled(enabled);
  1182. widget->numEdit->setEnabled(enabled);
  1183. widget->denEdit->setEnabled(enabled);
  1184. label = widget->warningLabel =
  1185. new QLabel{obs_property_description(prop)};
  1186. layout->addRow(label, widget);
  1187. children.emplace_back(info);
  1188. UpdateFPSLabels(widget);
  1189. auto stack = widget->modeDisplay;
  1190. auto combo = widget->modeSelect;
  1191. stack->setToolTip(QT_UTF8(obs_property_long_description(prop)));
  1192. combo->setToolTip(QT_UTF8(obs_property_long_description(prop)));
  1193. auto comboIndexChanged = static_cast<void (QComboBox::*)(int)>(
  1194. &QComboBox::currentIndexChanged);
  1195. connect(combo, comboIndexChanged, stack, [=](int index) {
  1196. bool out_of_bounds = index >= stack->count();
  1197. auto idx = out_of_bounds ? stack->count() - 1 : index;
  1198. stack->setCurrentIndex(idx);
  1199. if (widget->updating)
  1200. return;
  1201. UpdateFPSLabels(widget);
  1202. emit info->ControlChanged();
  1203. });
  1204. connect(widget->simpleFPS, comboIndexChanged, [=](int) {
  1205. if (widget->updating)
  1206. return;
  1207. emit info->ControlChanged();
  1208. });
  1209. connect(widget->fpsRange, comboIndexChanged, [=](int) {
  1210. if (widget->updating)
  1211. return;
  1212. UpdateFPSLabels(widget);
  1213. });
  1214. auto sbValueChanged =
  1215. static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged);
  1216. connect(widget->numEdit, sbValueChanged, [=](int) {
  1217. if (widget->updating)
  1218. return;
  1219. emit info->ControlChanged();
  1220. });
  1221. connect(widget->denEdit, sbValueChanged, [=](int) {
  1222. if (widget->updating)
  1223. return;
  1224. emit info->ControlChanged();
  1225. });
  1226. }
  1227. void OBSPropertiesView::AddGroup(obs_property_t *prop, QFormLayout *layout)
  1228. {
  1229. const char *name = obs_property_name(prop);
  1230. bool val = obs_data_get_bool(settings, name);
  1231. const char *desc = obs_property_description(prop);
  1232. enum obs_group_type type = obs_property_group_type(prop);
  1233. // Create GroupBox
  1234. QGroupBox *groupBox = new QGroupBox(QT_UTF8(desc));
  1235. groupBox->setCheckable(type == OBS_GROUP_CHECKABLE);
  1236. groupBox->setChecked(groupBox->isCheckable() ? val : true);
  1237. groupBox->setAccessibleName("group");
  1238. groupBox->setEnabled(obs_property_enabled(prop));
  1239. // Create Layout and build content
  1240. QFormLayout *subLayout = new QFormLayout();
  1241. subLayout->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow);
  1242. groupBox->setLayout(subLayout);
  1243. obs_properties_t *content = obs_property_group_content(prop);
  1244. obs_property_t *el = obs_properties_first(content);
  1245. while (el != nullptr) {
  1246. AddProperty(el, subLayout);
  1247. obs_property_next(&el);
  1248. }
  1249. // Insert into UI
  1250. layout->setWidget(layout->rowCount(),
  1251. QFormLayout::ItemRole::SpanningRole, groupBox);
  1252. // Register Group Widget
  1253. WidgetInfo *info = new WidgetInfo(this, prop, groupBox);
  1254. children.emplace_back(info);
  1255. // Signals
  1256. connect(groupBox, &QGroupBox::toggled, info,
  1257. &WidgetInfo::ControlChanged);
  1258. }
  1259. void OBSPropertiesView::AddProperty(obs_property_t *property,
  1260. QFormLayout *layout)
  1261. {
  1262. const char *name = obs_property_name(property);
  1263. obs_property_type type = obs_property_get_type(property);
  1264. if (!obs_property_visible(property))
  1265. return;
  1266. QLabel *label = nullptr;
  1267. QWidget *widget = nullptr;
  1268. bool warning = false;
  1269. switch (type) {
  1270. case OBS_PROPERTY_INVALID:
  1271. return;
  1272. case OBS_PROPERTY_BOOL:
  1273. widget = AddCheckbox(property);
  1274. break;
  1275. case OBS_PROPERTY_INT:
  1276. AddInt(property, layout, &label);
  1277. break;
  1278. case OBS_PROPERTY_FLOAT:
  1279. AddFloat(property, layout, &label);
  1280. break;
  1281. case OBS_PROPERTY_TEXT:
  1282. widget = AddText(property, layout, label);
  1283. break;
  1284. case OBS_PROPERTY_PATH:
  1285. AddPath(property, layout, &label);
  1286. break;
  1287. case OBS_PROPERTY_LIST:
  1288. widget = AddList(property, warning);
  1289. break;
  1290. case OBS_PROPERTY_COLOR:
  1291. AddColor(property, layout, label);
  1292. break;
  1293. case OBS_PROPERTY_FONT:
  1294. AddFont(property, layout, label);
  1295. break;
  1296. case OBS_PROPERTY_BUTTON:
  1297. widget = AddButton(property);
  1298. break;
  1299. case OBS_PROPERTY_EDITABLE_LIST:
  1300. AddEditableList(property, layout, label);
  1301. break;
  1302. case OBS_PROPERTY_FRAME_RATE:
  1303. AddFrameRate(property, warning, layout, label);
  1304. break;
  1305. case OBS_PROPERTY_GROUP:
  1306. AddGroup(property, layout);
  1307. break;
  1308. case OBS_PROPERTY_COLOR_ALPHA:
  1309. AddColorAlpha(property, layout, label);
  1310. }
  1311. if (!widget && !label)
  1312. return;
  1313. if (!label && type != OBS_PROPERTY_BOOL &&
  1314. type != OBS_PROPERTY_BUTTON && type != OBS_PROPERTY_GROUP)
  1315. label = new QLabel(QT_UTF8(obs_property_description(property)));
  1316. if (label) {
  1317. if (warning)
  1318. label->setObjectName("errorLabel");
  1319. if (minSize) {
  1320. label->setMinimumWidth(minSize);
  1321. label->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
  1322. }
  1323. if (!obs_property_enabled(property))
  1324. label->setEnabled(false);
  1325. }
  1326. if (!widget)
  1327. return;
  1328. if (!obs_property_enabled(property))
  1329. widget->setEnabled(false);
  1330. QWidget *leftWidget = label;
  1331. if (obs_property_long_description(property) && label) {
  1332. QString file = !obs_frontend_is_theme_dark()
  1333. ? ":/res/images/help.svg"
  1334. : ":/res/images/help_light.svg";
  1335. QWidget *newWidget = new QWidget();
  1336. newWidget->setToolTip(obs_property_long_description(property));
  1337. QHBoxLayout *boxLayout = new QHBoxLayout(newWidget);
  1338. boxLayout->setContentsMargins(0, 0, 0, 0);
  1339. boxLayout->setAlignment(Qt::AlignLeft);
  1340. boxLayout->setSpacing(0);
  1341. IconLabel *help = new IconLabel(newWidget);
  1342. help->setIcon(QIcon(file));
  1343. help->setToolTip(obs_property_long_description(property));
  1344. boxLayout->addWidget(label);
  1345. boxLayout->addWidget(help);
  1346. leftWidget = newWidget;
  1347. }
  1348. layout->addRow(leftWidget, widget);
  1349. if (!lastFocused.empty())
  1350. if (lastFocused.compare(name) == 0)
  1351. lastWidget = widget;
  1352. }
  1353. void OBSPropertiesView::SignalChanged()
  1354. {
  1355. emit Changed();
  1356. }
  1357. static bool FrameRateChangedVariant(const QVariant &variant,
  1358. media_frames_per_second &fps,
  1359. obs_data_item_t *&obj,
  1360. const media_frames_per_second *valid_fps)
  1361. {
  1362. if (!variant.canConvert<media_frames_per_second>())
  1363. return false;
  1364. fps = variant.value<media_frames_per_second>();
  1365. if (valid_fps && fps == *valid_fps)
  1366. return false;
  1367. obs_data_item_set_frames_per_second(&obj, fps, nullptr);
  1368. return true;
  1369. }
  1370. static bool FrameRateChangedCommon(OBSFrameRatePropertyWidget *w,
  1371. obs_data_item_t *&obj,
  1372. const media_frames_per_second *valid_fps)
  1373. {
  1374. media_frames_per_second fps{};
  1375. if (!FrameRateChangedVariant(w->simpleFPS->currentData(), fps, obj,
  1376. valid_fps))
  1377. return false;
  1378. UpdateRationalFPSWidgets(w, &fps);
  1379. return true;
  1380. }
  1381. static bool FrameRateChangedRational(OBSFrameRatePropertyWidget *w,
  1382. obs_data_item_t *&obj,
  1383. const media_frames_per_second *valid_fps)
  1384. {
  1385. auto num = w->numEdit->value();
  1386. auto den = w->denEdit->value();
  1387. auto fps = make_fps(num, den);
  1388. if (valid_fps && media_frames_per_second_is_valid(fps) &&
  1389. fps == *valid_fps)
  1390. return false;
  1391. obs_data_item_set_frames_per_second(&obj, fps, nullptr);
  1392. UpdateSimpleFPSSelection(w, &fps);
  1393. return true;
  1394. }
  1395. static bool FrameRateChanged(QWidget *widget, const char *name,
  1396. OBSData &settings)
  1397. {
  1398. auto w = qobject_cast<OBSFrameRatePropertyWidget *>(widget);
  1399. if (!w)
  1400. return false;
  1401. auto variant = w->modeSelect->currentData();
  1402. if (!variant.canConvert<frame_rate_tag>())
  1403. return false;
  1404. auto StopUpdating = [&](void *) {
  1405. w->updating = false;
  1406. };
  1407. unique_ptr<void, decltype(StopUpdating)> signalGuard(
  1408. static_cast<void *>(w), StopUpdating);
  1409. w->updating = true;
  1410. if (!obs_data_has_user_value(settings, name))
  1411. obs_data_set_obj(settings, name, nullptr);
  1412. unique_ptr<obs_data_item_t> obj{obs_data_item_byname(settings, name)};
  1413. auto obj_ptr = obj.get();
  1414. auto CheckObj = [&]() {
  1415. if (!obj_ptr)
  1416. obj.release();
  1417. };
  1418. const char *option = nullptr;
  1419. obs_data_item_get_frames_per_second(obj.get(), nullptr, &option);
  1420. media_frames_per_second fps{};
  1421. media_frames_per_second *valid_fps = nullptr;
  1422. if (obs_data_item_get_frames_per_second(obj.get(), &fps, nullptr))
  1423. valid_fps = &fps;
  1424. auto tag = variant.value<frame_rate_tag>();
  1425. switch (tag.type) {
  1426. case frame_rate_tag::SIMPLE:
  1427. if (!FrameRateChangedCommon(w, obj_ptr, valid_fps))
  1428. return false;
  1429. break;
  1430. case frame_rate_tag::RATIONAL:
  1431. if (!FrameRateChangedRational(w, obj_ptr, valid_fps))
  1432. return false;
  1433. break;
  1434. case frame_rate_tag::USER:
  1435. if (tag.val && option && strcmp(tag.val, option) == 0)
  1436. return false;
  1437. obs_data_item_set_frames_per_second(&obj_ptr, {}, tag.val);
  1438. break;
  1439. }
  1440. UpdateFPSLabels(w);
  1441. CheckObj();
  1442. return true;
  1443. }
  1444. void WidgetInfo::BoolChanged(const char *setting)
  1445. {
  1446. QCheckBox *checkbox = static_cast<QCheckBox *>(widget);
  1447. obs_data_set_bool(view->settings, setting,
  1448. checkbox->checkState() == Qt::Checked);
  1449. }
  1450. void WidgetInfo::IntChanged(const char *setting)
  1451. {
  1452. QSpinBox *spin = static_cast<QSpinBox *>(widget);
  1453. obs_data_set_int(view->settings, setting, spin->value());
  1454. }
  1455. void WidgetInfo::FloatChanged(const char *setting)
  1456. {
  1457. QDoubleSpinBox *spin = static_cast<QDoubleSpinBox *>(widget);
  1458. obs_data_set_double(view->settings, setting, spin->value());
  1459. }
  1460. void WidgetInfo::TextChanged(const char *setting)
  1461. {
  1462. obs_text_type type = obs_property_text_type(property);
  1463. if (type == OBS_TEXT_MULTILINE) {
  1464. OBSPlainTextEdit *edit =
  1465. static_cast<OBSPlainTextEdit *>(widget);
  1466. obs_data_set_string(view->settings, setting,
  1467. QT_TO_UTF8(edit->toPlainText()));
  1468. return;
  1469. }
  1470. QLineEdit *edit = static_cast<QLineEdit *>(widget);
  1471. obs_data_set_string(view->settings, setting, QT_TO_UTF8(edit->text()));
  1472. }
  1473. bool WidgetInfo::PathChanged(const char *setting)
  1474. {
  1475. const char *desc = obs_property_description(property);
  1476. obs_path_type type = obs_property_path_type(property);
  1477. const char *filter = obs_property_path_filter(property);
  1478. const char *default_path = obs_property_path_default_path(property);
  1479. QLineEdit *edit = static_cast<QLineEdit *>(widget);
  1480. QString startDir = edit->text();
  1481. if (startDir.isEmpty())
  1482. startDir = default_path;
  1483. QString path;
  1484. if (type == OBS_PATH_DIRECTORY)
  1485. path = SelectDirectory(view, QT_UTF8(desc), startDir);
  1486. else if (type == OBS_PATH_FILE)
  1487. path = OpenFile(view, QT_UTF8(desc), startDir, QT_UTF8(filter));
  1488. else if (type == OBS_PATH_FILE_SAVE)
  1489. path = SaveFile(view, QT_UTF8(desc), startDir, QT_UTF8(filter));
  1490. #ifdef __APPLE__
  1491. // TODO: Revisit when QTBUG-42661 is fixed
  1492. widget->window()->raise();
  1493. #endif
  1494. if (path.isEmpty())
  1495. return false;
  1496. edit->setText(path);
  1497. obs_data_set_string(view->settings, setting, QT_TO_UTF8(path));
  1498. return true;
  1499. }
  1500. void WidgetInfo::ListChanged(const char *setting)
  1501. {
  1502. obs_combo_format format = obs_property_list_format(property);
  1503. obs_combo_type type = obs_property_list_type(property);
  1504. QVariant data;
  1505. if (type == OBS_COMBO_TYPE_RADIO) {
  1506. QButtonGroup *group =
  1507. static_cast<QAbstractButton *>(widget)->group();
  1508. QAbstractButton *button = group->checkedButton();
  1509. data = button->property("value");
  1510. } else if (type == OBS_COMBO_TYPE_EDITABLE) {
  1511. data = static_cast<QComboBox *>(widget)->currentText().toUtf8();
  1512. } else {
  1513. QComboBox *combo = static_cast<QComboBox *>(widget);
  1514. int index = combo->currentIndex();
  1515. if (index != -1)
  1516. data = combo->itemData(index);
  1517. else
  1518. return;
  1519. }
  1520. switch (format) {
  1521. case OBS_COMBO_FORMAT_INVALID:
  1522. return;
  1523. case OBS_COMBO_FORMAT_INT:
  1524. obs_data_set_int(view->settings, setting,
  1525. data.value<long long>());
  1526. break;
  1527. case OBS_COMBO_FORMAT_FLOAT:
  1528. obs_data_set_double(view->settings, setting,
  1529. data.value<double>());
  1530. break;
  1531. case OBS_COMBO_FORMAT_STRING:
  1532. obs_data_set_string(view->settings, setting,
  1533. data.toByteArray().constData());
  1534. break;
  1535. case OBS_COMBO_FORMAT_BOOL:
  1536. obs_data_set_bool(view->settings, setting,
  1537. data.value<double>());
  1538. break;
  1539. }
  1540. }
  1541. bool WidgetInfo::ColorChangedInternal(const char *setting, bool supportAlpha)
  1542. {
  1543. const char *desc = obs_property_description(property);
  1544. long long val = obs_data_get_int(view->settings, setting);
  1545. QColor color = color_from_int(val);
  1546. QColor::NameFormat format;
  1547. QColorDialog::ColorDialogOptions options;
  1548. if (supportAlpha) {
  1549. options |= QColorDialog::ShowAlphaChannel;
  1550. }
  1551. #ifdef __linux__
  1552. // TODO: Revisit hang on Ubuntu with native dialog
  1553. options |= QColorDialog::DontUseNativeDialog;
  1554. #endif
  1555. color = QColorDialog::getColor(color, view, QT_UTF8(desc), options);
  1556. #ifdef __APPLE__
  1557. // TODO: Revisit when QTBUG-42661 is fixed
  1558. widget->window()->raise();
  1559. #endif
  1560. if (!color.isValid())
  1561. return false;
  1562. if (supportAlpha) {
  1563. format = QColor::HexArgb;
  1564. } else {
  1565. color.setAlpha(255);
  1566. format = QColor::HexRgb;
  1567. }
  1568. QLabel *label = static_cast<QLabel *>(widget);
  1569. label->setText(color.name(format));
  1570. QPalette palette = QPalette(color);
  1571. label->setPalette(palette);
  1572. label->setStyleSheet(
  1573. QString("background-color :%1; color: %2;")
  1574. .arg(palette.color(QPalette::Window).name(format))
  1575. .arg(palette.color(QPalette::WindowText).name(format)));
  1576. obs_data_set_int(view->settings, setting, color_to_int(color));
  1577. return true;
  1578. }
  1579. bool WidgetInfo::ColorChanged(const char *setting)
  1580. {
  1581. return ColorChangedInternal(setting, false);
  1582. }
  1583. bool WidgetInfo::ColorAlphaChanged(const char *setting)
  1584. {
  1585. return ColorChangedInternal(setting, true);
  1586. }
  1587. bool WidgetInfo::FontChanged(const char *setting)
  1588. {
  1589. OBSDataAutoRelease font_obj = obs_data_get_obj(view->settings, setting);
  1590. bool success;
  1591. uint32_t flags;
  1592. QFont font;
  1593. QFontDialog::FontDialogOptions options;
  1594. #ifndef _WIN32
  1595. options = QFontDialog::DontUseNativeDialog;
  1596. #endif
  1597. if (!font_obj) {
  1598. QFont initial;
  1599. font = QFontDialog::getFont(
  1600. &success, initial, view,
  1601. tr("Basic.PropertiesWindow.SelectFont.WindowTitle"),
  1602. options);
  1603. } else {
  1604. MakeQFont(font_obj, font);
  1605. font = QFontDialog::getFont(
  1606. &success, font, view,
  1607. tr("Basic.PropertiesWindow.SelectFont.WindowTitle"),
  1608. options);
  1609. }
  1610. if (!success)
  1611. return false;
  1612. font_obj = obs_data_create();
  1613. obs_data_set_string(font_obj, "face", QT_TO_UTF8(font.family()));
  1614. obs_data_set_string(font_obj, "style", QT_TO_UTF8(font.styleName()));
  1615. obs_data_set_int(font_obj, "size", font.pointSize());
  1616. flags = font.bold() ? OBS_FONT_BOLD : 0;
  1617. flags |= font.italic() ? OBS_FONT_ITALIC : 0;
  1618. flags |= font.underline() ? OBS_FONT_UNDERLINE : 0;
  1619. flags |= font.strikeOut() ? OBS_FONT_STRIKEOUT : 0;
  1620. obs_data_set_int(font_obj, "flags", flags);
  1621. QLabel *label = static_cast<QLabel *>(widget);
  1622. QFont labelFont;
  1623. MakeQFont(font_obj, labelFont, true);
  1624. label->setFont(labelFont);
  1625. label->setText(QString("%1 %2").arg(font.family(), font.styleName()));
  1626. obs_data_set_obj(view->settings, setting, font_obj);
  1627. return true;
  1628. }
  1629. void WidgetInfo::GroupChanged(const char *setting)
  1630. {
  1631. QGroupBox *groupbox = static_cast<QGroupBox *>(widget);
  1632. obs_data_set_bool(view->settings, setting,
  1633. groupbox->isCheckable() ? groupbox->isChecked()
  1634. : true);
  1635. }
  1636. void WidgetInfo::EditableListChanged()
  1637. {
  1638. const char *setting = obs_property_name(property);
  1639. QListWidget *list = reinterpret_cast<QListWidget *>(widget);
  1640. OBSDataArrayAutoRelease array = obs_data_array_create();
  1641. for (int i = 0; i < list->count(); i++) {
  1642. QListWidgetItem *item = list->item(i);
  1643. OBSDataAutoRelease arrayItem = obs_data_create();
  1644. obs_data_set_string(arrayItem, "value",
  1645. QT_TO_UTF8(item->text()));
  1646. obs_data_set_string(
  1647. arrayItem, "uuid",
  1648. QT_TO_UTF8(item->data(Qt::UserRole).toString()));
  1649. obs_data_set_bool(arrayItem, "selected", item->isSelected());
  1650. obs_data_set_bool(arrayItem, "hidden", item->isHidden());
  1651. obs_data_array_push_back(array, arrayItem);
  1652. }
  1653. obs_data_set_array(view->settings, setting, array);
  1654. ControlChanged();
  1655. }
  1656. void WidgetInfo::ButtonClicked()
  1657. {
  1658. obs_button_type type = obs_property_button_type(property);
  1659. const char *savedUrl = obs_property_button_url(property);
  1660. if (type == OBS_BUTTON_URL && strcmp(savedUrl, "") != 0) {
  1661. QUrl url(savedUrl, QUrl::StrictMode);
  1662. if (url.isValid() && (url.scheme().compare("http") == 0 ||
  1663. url.scheme().compare("https") == 0)) {
  1664. QString msg(tr("Basic.PropertiesView.UrlButton.Text"));
  1665. msg += "\n\n";
  1666. msg += QString(tr("Basic.PropertiesView.UrlButton.Text.Url"))
  1667. .arg(savedUrl);
  1668. QMessageBox::StandardButton button = OBSMessageBox::question(
  1669. view->window(),
  1670. tr("Basic.PropertiesView.UrlButton.OpenUrl"),
  1671. msg, QMessageBox::Yes | QMessageBox::No,
  1672. QMessageBox::No);
  1673. if (button == QMessageBox::Yes)
  1674. QDesktopServices::openUrl(url);
  1675. }
  1676. return;
  1677. }
  1678. OBSObject strongObj = view->GetObject();
  1679. void *obj = strongObj ? strongObj.Get() : view->rawObj;
  1680. if (obs_property_button_clicked(property, obj)) {
  1681. QMetaObject::invokeMethod(view, "RefreshProperties",
  1682. Qt::QueuedConnection);
  1683. }
  1684. }
  1685. void WidgetInfo::TogglePasswordText(bool show)
  1686. {
  1687. reinterpret_cast<QLineEdit *>(widget)->setEchoMode(
  1688. show ? QLineEdit::Normal : QLineEdit::Password);
  1689. }
  1690. void WidgetInfo::ControlChanged()
  1691. {
  1692. const char *setting = obs_property_name(property);
  1693. obs_property_type type = obs_property_get_type(property);
  1694. if (!recently_updated) {
  1695. old_settings_cache = obs_data_create();
  1696. obs_data_apply(old_settings_cache, view->settings);
  1697. obs_data_release(old_settings_cache);
  1698. }
  1699. switch (type) {
  1700. case OBS_PROPERTY_INVALID:
  1701. return;
  1702. case OBS_PROPERTY_BOOL:
  1703. BoolChanged(setting);
  1704. break;
  1705. case OBS_PROPERTY_INT:
  1706. IntChanged(setting);
  1707. break;
  1708. case OBS_PROPERTY_FLOAT:
  1709. FloatChanged(setting);
  1710. break;
  1711. case OBS_PROPERTY_TEXT:
  1712. TextChanged(setting);
  1713. break;
  1714. case OBS_PROPERTY_LIST:
  1715. ListChanged(setting);
  1716. break;
  1717. case OBS_PROPERTY_BUTTON:
  1718. ButtonClicked();
  1719. return;
  1720. case OBS_PROPERTY_COLOR:
  1721. if (!ColorChanged(setting))
  1722. return;
  1723. break;
  1724. case OBS_PROPERTY_FONT:
  1725. if (!FontChanged(setting))
  1726. return;
  1727. break;
  1728. case OBS_PROPERTY_PATH:
  1729. if (!PathChanged(setting))
  1730. return;
  1731. break;
  1732. case OBS_PROPERTY_EDITABLE_LIST:
  1733. break;
  1734. case OBS_PROPERTY_FRAME_RATE:
  1735. if (!FrameRateChanged(widget, setting, view->settings))
  1736. return;
  1737. break;
  1738. case OBS_PROPERTY_GROUP:
  1739. GroupChanged(setting);
  1740. break;
  1741. case OBS_PROPERTY_COLOR_ALPHA:
  1742. if (!ColorAlphaChanged(setting))
  1743. return;
  1744. break;
  1745. }
  1746. if (!recently_updated) {
  1747. recently_updated = true;
  1748. update_timer = new QTimer;
  1749. connect(update_timer, &QTimer::timeout,
  1750. [this, &ru = recently_updated]() {
  1751. OBSObject strongObj = view->GetObject();
  1752. void *obj = strongObj ? strongObj.Get()
  1753. : view->rawObj;
  1754. if (obj && view->callback &&
  1755. !view->deferUpdate) {
  1756. view->callback(obj, old_settings_cache,
  1757. view->settings);
  1758. }
  1759. ru = false;
  1760. });
  1761. connect(update_timer, &QTimer::timeout, &QTimer::deleteLater);
  1762. update_timer->setSingleShot(true);
  1763. }
  1764. if (update_timer) {
  1765. update_timer->stop();
  1766. update_timer->start(500);
  1767. } else {
  1768. blog(LOG_DEBUG, "No update timer or no callback!");
  1769. }
  1770. if (view->visUpdateCb && !view->deferUpdate) {
  1771. OBSObject strongObj = view->GetObject();
  1772. void *obj = strongObj ? strongObj.Get() : view->rawObj;
  1773. if (obj)
  1774. view->visUpdateCb(obj, view->settings);
  1775. }
  1776. view->SignalChanged();
  1777. if (obs_property_modified(property, view->settings)) {
  1778. view->lastFocused = setting;
  1779. QMetaObject::invokeMethod(view, "RefreshProperties",
  1780. Qt::QueuedConnection);
  1781. }
  1782. }
  1783. class EditableItemDialog : public QDialog {
  1784. QLineEdit *edit;
  1785. QString filter;
  1786. QString default_path;
  1787. void BrowseClicked()
  1788. {
  1789. QString curPath = QFileInfo(edit->text()).absoluteDir().path();
  1790. if (curPath.isEmpty())
  1791. curPath = default_path;
  1792. QString path = OpenFile(this, tr("Browse"), curPath, filter);
  1793. if (path.isEmpty())
  1794. return;
  1795. edit->setText(path);
  1796. }
  1797. public:
  1798. EditableItemDialog(QWidget *parent, const QString &text, bool browse,
  1799. const char *filter_ = nullptr,
  1800. const char *default_path_ = nullptr)
  1801. : QDialog(parent),
  1802. filter(QT_UTF8(filter_)),
  1803. default_path(QT_UTF8(default_path_))
  1804. {
  1805. QHBoxLayout *topLayout = new QHBoxLayout();
  1806. QVBoxLayout *mainLayout = new QVBoxLayout();
  1807. edit = new QLineEdit();
  1808. edit->setText(text);
  1809. topLayout->addWidget(edit);
  1810. topLayout->setAlignment(edit, Qt::AlignVCenter);
  1811. if (browse) {
  1812. QPushButton *browseButton =
  1813. new QPushButton(tr("Browse"));
  1814. topLayout->addWidget(browseButton);
  1815. topLayout->setAlignment(browseButton, Qt::AlignVCenter);
  1816. connect(browseButton, &QPushButton::clicked, this,
  1817. &EditableItemDialog::BrowseClicked);
  1818. }
  1819. QDialogButtonBox::StandardButtons buttons =
  1820. QDialogButtonBox::Ok | QDialogButtonBox::Cancel;
  1821. QDialogButtonBox *buttonBox = new QDialogButtonBox(buttons);
  1822. buttonBox->setCenterButtons(true);
  1823. mainLayout->addLayout(topLayout);
  1824. mainLayout->addWidget(buttonBox);
  1825. setLayout(mainLayout);
  1826. resize(QSize(400, 80));
  1827. connect(buttonBox, &QDialogButtonBox::accepted, this,
  1828. &EditableItemDialog::accept);
  1829. connect(buttonBox, &QDialogButtonBox::rejected, this,
  1830. &EditableItemDialog::reject);
  1831. }
  1832. inline QString GetText() const { return edit->text(); }
  1833. };
  1834. void WidgetInfo::EditListAdd()
  1835. {
  1836. enum obs_editable_list_type type =
  1837. obs_property_editable_list_type(property);
  1838. if (type == OBS_EDITABLE_LIST_TYPE_STRINGS) {
  1839. EditListAddText();
  1840. return;
  1841. }
  1842. /* Files and URLs */
  1843. QMenu popup(view->window());
  1844. QAction *action;
  1845. action = new QAction(tr("Basic.PropertiesWindow.AddFiles"), this);
  1846. connect(action, &QAction::triggered, this,
  1847. &WidgetInfo::EditListAddFiles);
  1848. popup.addAction(action);
  1849. action = new QAction(tr("Basic.PropertiesWindow.AddDir"), this);
  1850. connect(action, &QAction::triggered, this, &WidgetInfo::EditListAddDir);
  1851. popup.addAction(action);
  1852. if (type == OBS_EDITABLE_LIST_TYPE_FILES_AND_URLS) {
  1853. action = new QAction(tr("Basic.PropertiesWindow.AddURL"), this);
  1854. connect(action, &QAction::triggered, this,
  1855. &WidgetInfo::EditListAddText);
  1856. popup.addAction(action);
  1857. }
  1858. popup.exec(QCursor::pos());
  1859. }
  1860. void WidgetInfo::EditListAddText()
  1861. {
  1862. QListWidget *list = reinterpret_cast<QListWidget *>(widget);
  1863. const char *desc = obs_property_description(property);
  1864. EditableItemDialog dialog(widget->window(), QString(), false);
  1865. auto title = tr("Basic.PropertiesWindow.AddEditableListEntry")
  1866. .arg(QT_UTF8(desc));
  1867. dialog.setWindowTitle(title);
  1868. if (dialog.exec() == QDialog::Rejected)
  1869. return;
  1870. QString text = dialog.GetText();
  1871. if (text.isEmpty())
  1872. return;
  1873. QListWidgetItem *item = new QListWidgetItem(text);
  1874. item->setData(Qt::UserRole,
  1875. QUuid::createUuid().toString(QUuid::WithoutBraces));
  1876. list->addItem(item);
  1877. EditableListChanged();
  1878. }
  1879. void WidgetInfo::EditListAddFiles()
  1880. {
  1881. QListWidget *list = reinterpret_cast<QListWidget *>(widget);
  1882. const char *desc = obs_property_description(property);
  1883. const char *filter = obs_property_editable_list_filter(property);
  1884. const char *default_path =
  1885. obs_property_editable_list_default_path(property);
  1886. QString title = tr("Basic.PropertiesWindow.AddEditableListFiles")
  1887. .arg(QT_UTF8(desc));
  1888. QStringList files =
  1889. OpenFiles(list, title, QT_UTF8(default_path), QT_UTF8(filter));
  1890. #ifdef __APPLE__
  1891. // TODO: Revisit when QTBUG-42661 is fixed
  1892. widget->window()->raise();
  1893. #endif
  1894. if (files.count() == 0)
  1895. return;
  1896. for (QString file : files) {
  1897. QListWidgetItem *item = new QListWidgetItem(file);
  1898. item->setData(Qt::UserRole, QUuid::createUuid().toString(
  1899. QUuid::WithoutBraces));
  1900. list->addItem(item);
  1901. }
  1902. EditableListChanged();
  1903. }
  1904. void WidgetInfo::EditListAddDir()
  1905. {
  1906. QListWidget *list = reinterpret_cast<QListWidget *>(widget);
  1907. const char *desc = obs_property_description(property);
  1908. const char *default_path =
  1909. obs_property_editable_list_default_path(property);
  1910. QString title = tr("Basic.PropertiesWindow.AddEditableListDir")
  1911. .arg(QT_UTF8(desc));
  1912. QString dir = SelectDirectory(list, title, QT_UTF8(default_path));
  1913. #ifdef __APPLE__
  1914. // TODO: Revisit when QTBUG-42661 is fixed
  1915. widget->window()->raise();
  1916. #endif
  1917. if (dir.isEmpty())
  1918. return;
  1919. QListWidgetItem *item = new QListWidgetItem(dir);
  1920. item->setData(Qt::UserRole,
  1921. QUuid::createUuid().toString(QUuid::WithoutBraces));
  1922. list->addItem(item);
  1923. EditableListChanged();
  1924. }
  1925. void WidgetInfo::EditListRemove()
  1926. {
  1927. QListWidget *list = reinterpret_cast<QListWidget *>(widget);
  1928. QList<QListWidgetItem *> items = list->selectedItems();
  1929. for (QListWidgetItem *item : items)
  1930. delete item;
  1931. EditableListChanged();
  1932. }
  1933. void WidgetInfo::EditListEdit()
  1934. {
  1935. QListWidget *list = reinterpret_cast<QListWidget *>(widget);
  1936. enum obs_editable_list_type type =
  1937. obs_property_editable_list_type(property);
  1938. const char *desc = obs_property_description(property);
  1939. const char *filter = obs_property_editable_list_filter(property);
  1940. QList<QListWidgetItem *> selectedItems = list->selectedItems();
  1941. if (!selectedItems.count())
  1942. return;
  1943. QListWidgetItem *item = selectedItems[0];
  1944. if (type == OBS_EDITABLE_LIST_TYPE_FILES) {
  1945. QDir pathDir(item->text());
  1946. QString path;
  1947. if (pathDir.exists())
  1948. path = SelectDirectory(list, tr("Browse"),
  1949. item->text());
  1950. else
  1951. path = OpenFile(list, tr("Browse"), item->text(),
  1952. QT_UTF8(filter));
  1953. if (path.isEmpty())
  1954. return;
  1955. item->setText(path);
  1956. EditableListChanged();
  1957. return;
  1958. }
  1959. EditableItemDialog dialog(widget->window(), item->text(),
  1960. type != OBS_EDITABLE_LIST_TYPE_STRINGS,
  1961. filter);
  1962. auto title = tr("Basic.PropertiesWindow.EditEditableListEntry")
  1963. .arg(QT_UTF8(desc));
  1964. dialog.setWindowTitle(title);
  1965. if (dialog.exec() == QDialog::Rejected)
  1966. return;
  1967. QString text = dialog.GetText();
  1968. if (text.isEmpty())
  1969. return;
  1970. item->setText(text);
  1971. EditableListChanged();
  1972. }
  1973. void WidgetInfo::EditListUp()
  1974. {
  1975. QListWidget *list = reinterpret_cast<QListWidget *>(widget);
  1976. int lastItemRow = -1;
  1977. for (int i = 0; i < list->count(); i++) {
  1978. QListWidgetItem *item = list->item(i);
  1979. if (!item->isSelected())
  1980. continue;
  1981. int row = list->row(item);
  1982. if ((row - 1) != lastItemRow) {
  1983. lastItemRow = row - 1;
  1984. list->takeItem(row);
  1985. list->insertItem(lastItemRow, item);
  1986. item->setSelected(true);
  1987. } else {
  1988. lastItemRow = row;
  1989. }
  1990. }
  1991. EditableListChanged();
  1992. }
  1993. void WidgetInfo::EditListDown()
  1994. {
  1995. QListWidget *list = reinterpret_cast<QListWidget *>(widget);
  1996. int lastItemRow = list->count();
  1997. for (int i = list->count() - 1; i >= 0; i--) {
  1998. QListWidgetItem *item = list->item(i);
  1999. if (!item->isSelected())
  2000. continue;
  2001. int row = list->row(item);
  2002. if ((row + 1) != lastItemRow) {
  2003. lastItemRow = row + 1;
  2004. list->takeItem(row);
  2005. list->insertItem(lastItemRow, item);
  2006. item->setSelected(true);
  2007. } else {
  2008. lastItemRow = row;
  2009. }
  2010. }
  2011. EditableListChanged();
  2012. }