desktop_capture_choose_source.cpp 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618
  1. /*
  2. This file is part of Telegram Desktop,
  3. the official desktop application for the Telegram messaging service.
  4. For license and copyright information please follow this link:
  5. https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
  6. */
  7. #include "calls/group/ui/desktop_capture_choose_source.h"
  8. #include "ui/widgets/rp_window.h"
  9. #include "ui/widgets/scroll_area.h"
  10. #include "ui/widgets/labels.h"
  11. #include "ui/widgets/buttons.h"
  12. #include "ui/widgets/checkbox.h"
  13. #include "ui/effects/ripple_animation.h"
  14. #include "ui/image/image.h"
  15. #include "ui/platform/ui_platform_window_title.h"
  16. #include "ui/painter.h"
  17. #include "base/platform/base_platform_info.h"
  18. #include "webrtc/webrtc_video_track.h"
  19. #include "lang/lang_keys.h"
  20. #include "styles/style_calls.h"
  21. #include <tgcalls/desktop_capturer/DesktopCaptureSourceManager.h>
  22. #include <tgcalls/desktop_capturer/DesktopCaptureSourceHelper.h>
  23. #include <QtGui/QWindow>
  24. namespace Calls::Group::Ui::DesktopCapture {
  25. namespace {
  26. constexpr auto kColumns = 3;
  27. constexpr auto kRows = 2;
  28. struct Preview {
  29. explicit Preview(tgcalls::DesktopCaptureSource source);
  30. tgcalls::DesktopCaptureSourceHelper helper;
  31. Webrtc::VideoTrack track;
  32. rpl::lifetime lifetime;
  33. };
  34. class SourceButton final : public RippleButton {
  35. public:
  36. using RippleButton::RippleButton;
  37. private:
  38. QImage prepareRippleMask() const override;
  39. };
  40. QImage SourceButton::prepareRippleMask() const {
  41. return RippleAnimation::RoundRectMask(size(), st::roundRadiusLarge);
  42. }
  43. class Source final {
  44. public:
  45. Source(
  46. not_null<QWidget*> parent,
  47. tgcalls::DesktopCaptureSource source,
  48. const QString &title);
  49. void setGeometry(QRect geometry);
  50. void clearHelper();
  51. [[nodiscard]] rpl::producer<> activations() const;
  52. void setActive(bool active);
  53. [[nodiscard]] QString deviceIdKey() const;
  54. [[nodiscard]] rpl::lifetime &lifetime();
  55. private:
  56. void paint();
  57. void setupPreview();
  58. SourceButton _widget;
  59. FlatLabel _label;
  60. Ui::RoundRect _selectedRect;
  61. Ui::RoundRect _activeRect;
  62. tgcalls::DesktopCaptureSource _source;
  63. std::unique_ptr<Preview> _preview;
  64. rpl::event_stream<> _activations;
  65. QImage _frame;
  66. bool _active = false;
  67. };
  68. class ChooseSourceProcess final {
  69. public:
  70. static void Start(not_null<ChooseSourceDelegate*> delegate);
  71. explicit ChooseSourceProcess(not_null<ChooseSourceDelegate*> delegate);
  72. void activate();
  73. private:
  74. void setupPanel();
  75. void setupSources();
  76. void setupGeometryWithParent(not_null<QWidget*> parent);
  77. void fillSources();
  78. void setupSourcesGeometry();
  79. void updateButtonsVisibility();
  80. void destroy();
  81. static base::flat_map<
  82. not_null<ChooseSourceDelegate*>,
  83. std::unique_ptr<ChooseSourceProcess>> &Map();
  84. const not_null<ChooseSourceDelegate*> _delegate;
  85. const std::unique_ptr<RpWindow> _window;
  86. const std::unique_ptr<ScrollArea> _scroll;
  87. const not_null<RpWidget*> _inner;
  88. const not_null<RpWidget*> _bottom;
  89. const not_null<RoundButton*> _submit;
  90. const not_null<RoundButton*> _finish;
  91. const not_null<Checkbox*> _withAudio;
  92. QSize _fixedSize;
  93. std::vector<std::unique_ptr<Source>> _sources;
  94. Source *_selected = nullptr;
  95. QString _selectedId;
  96. };
  97. [[nodiscard]] tgcalls::DesktopCaptureSourceData SourceData() {
  98. const auto factor = style::DevicePixelRatio();
  99. const auto size = st::desktopCaptureSourceSize * factor;
  100. return {
  101. .aspectSize = { size.width(), size.height() },
  102. .fps = 1,
  103. .captureMouse = false,
  104. };
  105. }
  106. Preview::Preview(tgcalls::DesktopCaptureSource source)
  107. : helper(source, SourceData())
  108. , track(Webrtc::VideoState::Active) {
  109. helper.setOutput(track.sink());
  110. helper.start();
  111. }
  112. Source::Source(
  113. not_null<QWidget*> parent,
  114. tgcalls::DesktopCaptureSource source,
  115. const QString &title)
  116. : _widget(parent, st::groupCallRipple)
  117. , _label(&_widget, title, st::desktopCaptureLabel)
  118. , _selectedRect(ImageRoundRadius::Large, st::groupCallMembersBgOver)
  119. , _activeRect(ImageRoundRadius::Large, st::groupCallMuted1)
  120. , _source(source) {
  121. _widget.paintRequest(
  122. ) | rpl::start_with_next([=] {
  123. paint();
  124. }, _widget.lifetime());
  125. _label.setAttribute(Qt::WA_TransparentForMouseEvents);
  126. _widget.sizeValue(
  127. ) | rpl::start_with_next([=](QSize size) {
  128. const auto padding = st::desktopCapturePadding;
  129. _label.resizeToNaturalWidth(
  130. size.width() - padding.left() - padding.right());
  131. _label.move(
  132. (size.width() - _label.width()) / 2,
  133. size.height() - _label.height() - st::desktopCaptureLabelBottom);
  134. }, _label.lifetime());
  135. _widget.setClickedCallback([=] {
  136. setActive(true);
  137. });
  138. }
  139. rpl::producer<> Source::activations() const {
  140. return _activations.events();
  141. }
  142. QString Source::deviceIdKey() const {
  143. return QString::fromStdString(_source.deviceIdKey());
  144. }
  145. void Source::setActive(bool active) {
  146. if (_active != active) {
  147. _active = active;
  148. _widget.update();
  149. if (active) {
  150. _activations.fire({});
  151. }
  152. }
  153. }
  154. void Source::setGeometry(QRect geometry) {
  155. _widget.setGeometry(geometry);
  156. }
  157. void Source::clearHelper() {
  158. _preview = nullptr;
  159. }
  160. void Source::paint() {
  161. auto p = QPainter(&_widget);
  162. if (_frame.isNull() && !_preview) {
  163. setupPreview();
  164. }
  165. if (_active) {
  166. _activeRect.paint(p, _widget.rect());
  167. } else if (_widget.isOver() || _widget.isDown()) {
  168. _selectedRect.paint(p, _widget.rect());
  169. }
  170. _widget.paintRipple(
  171. p,
  172. { 0, 0 },
  173. _active ? &st::shadowFg->c : nullptr);
  174. const auto size = _preview ? _preview->track.frameSize() : QSize();
  175. const auto factor = style::DevicePixelRatio();
  176. const auto padding = st::desktopCapturePadding;
  177. const auto rect = _widget.rect();
  178. const auto inner = rect.marginsRemoved(padding);
  179. if (!size.isEmpty()) {
  180. const auto scaled = size.scaled(inner.size(), Qt::KeepAspectRatio);
  181. const auto request = Webrtc::FrameRequest{
  182. .resize = scaled * factor,
  183. .outer = scaled * factor,
  184. };
  185. _frame = _preview->track.frame(request);
  186. _preview->track.markFrameShown();
  187. }
  188. if (!_frame.isNull()) {
  189. clearHelper();
  190. const auto size = _frame.size() / factor;
  191. const auto x = inner.x() + (inner.width() - size.width()) / 2;
  192. const auto y = inner.y() + (inner.height() - size.height()) / 2;
  193. auto hq = PainterHighQualityEnabler(p);
  194. p.drawImage(QRect(x, y, size.width(), size.height()), _frame);
  195. }
  196. }
  197. void Source::setupPreview() {
  198. _preview = std::make_unique<Preview>(_source);
  199. _preview->track.renderNextFrame(
  200. ) | rpl::start_with_next([=] {
  201. if (_preview->track.frameSize().isEmpty()) {
  202. _preview->track.markFrameShown();
  203. }
  204. _widget.update();
  205. }, _preview->lifetime);
  206. }
  207. rpl::lifetime &Source::lifetime() {
  208. return _widget.lifetime();
  209. }
  210. ChooseSourceProcess::ChooseSourceProcess(
  211. not_null<ChooseSourceDelegate*> delegate)
  212. : _delegate(delegate)
  213. , _window(std::make_unique<RpWindow>())
  214. , _scroll(std::make_unique<ScrollArea>(_window->body()))
  215. , _inner(_scroll->setOwnedWidget(object_ptr<RpWidget>(_scroll.get())))
  216. , _bottom(CreateChild<RpWidget>(_window->body().get()))
  217. , _submit(
  218. CreateChild<RoundButton>(
  219. _bottom.get(),
  220. tr::lng_group_call_screen_share_start(),
  221. st::desktopCaptureSubmit))
  222. , _finish(
  223. CreateChild<RoundButton>(
  224. _bottom.get(),
  225. tr::lng_group_call_screen_share_stop(),
  226. st::desktopCaptureFinish))
  227. , _withAudio(
  228. CreateChild<Checkbox>(
  229. _bottom.get(),
  230. tr::lng_group_call_screen_share_audio(tr::now),
  231. false,
  232. st::desktopCaptureWithAudio)) {
  233. setupPanel();
  234. setupSources();
  235. activate();
  236. }
  237. void ChooseSourceProcess::Start(not_null<ChooseSourceDelegate*> delegate) {
  238. auto &map = Map();
  239. auto i = map.find(delegate);
  240. if (i == end(map)) {
  241. i = map.emplace(delegate, nullptr).first;
  242. delegate->chooseSourceInstanceLifetime().add([=] {
  243. Map().erase(delegate);
  244. });
  245. }
  246. if (!i->second) {
  247. i->second = std::make_unique<ChooseSourceProcess>(delegate);
  248. } else {
  249. i->second->activate();
  250. }
  251. }
  252. void ChooseSourceProcess::activate() {
  253. if (_window->windowState() & Qt::WindowMinimized) {
  254. _window->showNormal();
  255. } else {
  256. _window->show();
  257. }
  258. _window->raise();
  259. _window->activateWindow();
  260. }
  261. [[nodiscard]] base::flat_map<
  262. not_null<ChooseSourceDelegate*>,
  263. std::unique_ptr<ChooseSourceProcess>> &ChooseSourceProcess::Map() {
  264. static auto result = base::flat_map<
  265. not_null<ChooseSourceDelegate*>,
  266. std::unique_ptr<ChooseSourceProcess>>();
  267. return result;
  268. }
  269. void ChooseSourceProcess::setupPanel() {
  270. #ifndef Q_OS_LINUX
  271. //_window->setAttribute(Qt::WA_OpaquePaintEvent);
  272. #endif // Q_OS_LINUX
  273. //_window->setAttribute(Qt::WA_NoSystemBackground);
  274. _window->setWindowIcon(QIcon(
  275. QPixmap::fromImage(Image::Empty()->original(), Qt::ColorOnly)));
  276. _window->setTitleStyle(st::desktopCaptureSourceTitle);
  277. const auto skips = st::desktopCaptureSourceSkips;
  278. const auto margins = st::desktopCaptureMargins;
  279. const auto padding = st::desktopCapturePadding;
  280. const auto bottomSkip = margins.right() + padding.right();
  281. const auto bottomHeight = 2 * bottomSkip
  282. + st::desktopCaptureCancel.height;
  283. const auto width = margins.left()
  284. + kColumns * st::desktopCaptureSourceSize.width()
  285. + (kColumns - 1) * skips.width()
  286. + margins.right();
  287. const auto height = margins.top()
  288. + kRows * st::desktopCaptureSourceSize.height()
  289. + (kRows - 1) * skips.height()
  290. + (st::desktopCaptureSourceSize.height() / 2)
  291. + bottomHeight;
  292. _fixedSize = QSize(width, height);
  293. _window->setStaysOnTop(true);
  294. _window->body()->paintRequest(
  295. ) | rpl::start_with_next([=](QRect clip) {
  296. QPainter(_window->body()).fillRect(clip, st::groupCallMembersBg);
  297. }, _window->lifetime());
  298. _bottom->setGeometry(0, height - bottomHeight, width, bottomHeight);
  299. _submit->setClickedCallback([=] {
  300. if (_selectedId.isEmpty()) {
  301. return;
  302. }
  303. const auto weak = MakeWeak(_window.get());
  304. _delegate->chooseSourceAccepted(
  305. _selectedId,
  306. !_withAudio->isHidden() && _withAudio->checked());
  307. if (const auto strong = weak.data()) {
  308. strong->close();
  309. }
  310. });
  311. _finish->setClickedCallback([=] {
  312. const auto weak = MakeWeak(_window.get());
  313. _delegate->chooseSourceStop();
  314. if (const auto strong = weak.data()) {
  315. strong->close();
  316. }
  317. });
  318. const auto cancel = CreateChild<RoundButton>(
  319. _bottom.get(),
  320. tr::lng_cancel(),
  321. st::desktopCaptureCancel);
  322. cancel->setClickedCallback([=] {
  323. _window->close();
  324. });
  325. rpl::combine(
  326. _submit->widthValue(),
  327. _submit->shownValue(),
  328. _finish->widthValue(),
  329. _finish->shownValue(),
  330. cancel->widthValue()
  331. ) | rpl::start_with_next([=](
  332. int submitWidth,
  333. bool submitShown,
  334. int finishWidth,
  335. bool finishShown,
  336. int cancelWidth) {
  337. _finish->moveToRight(bottomSkip, bottomSkip);
  338. _submit->moveToRight(bottomSkip, bottomSkip);
  339. cancel->moveToRight(
  340. bottomSkip * 2 + (submitShown ? submitWidth : finishWidth),
  341. bottomSkip);
  342. }, _bottom->lifetime());
  343. _withAudio->widthValue(
  344. ) | rpl::start_with_next([=](int width) {
  345. const auto top = (bottomHeight - _withAudio->heightNoMargins()) / 2;
  346. _withAudio->moveToLeft(bottomSkip, top);
  347. }, _withAudio->lifetime());
  348. _withAudio->setChecked(_delegate->chooseSourceActiveWithAudio());
  349. _withAudio->checkedChanges(
  350. ) | rpl::start_with_next([=] {
  351. updateButtonsVisibility();
  352. }, _withAudio->lifetime());
  353. const auto sharing = !_delegate->chooseSourceActiveDeviceId().isEmpty();
  354. _finish->setVisible(sharing);
  355. _submit->setVisible(!sharing);
  356. _window->body()->sizeValue(
  357. ) | rpl::start_with_next([=](QSize size) {
  358. _scroll->setGeometry(
  359. 0,
  360. 0,
  361. size.width(),
  362. size.height() - _bottom->height());
  363. }, _scroll->lifetime());
  364. _scroll->widthValue(
  365. ) | rpl::start_with_next([=](int width) {
  366. const auto rows = int(std::ceil(_sources.size() / float(kColumns)));
  367. const auto innerHeight = margins.top()
  368. + rows * st::desktopCaptureSourceSize.height()
  369. + (rows - 1) * skips.height()
  370. + margins.bottom();
  371. _inner->resize(width, innerHeight);
  372. }, _inner->lifetime());
  373. if (const auto parent = _delegate->chooseSourceParent()) {
  374. setupGeometryWithParent(parent);
  375. }
  376. _window->events(
  377. ) | rpl::filter([=](not_null<QEvent*> e) {
  378. return e->type() == QEvent::Close;
  379. }) | rpl::start_with_next([=] {
  380. destroy();
  381. }, _window->lifetime());
  382. }
  383. void ChooseSourceProcess::setupSources() {
  384. fillSources();
  385. setupSourcesGeometry();
  386. }
  387. void ChooseSourceProcess::fillSources() {
  388. using Type = tgcalls::DesktopCaptureType;
  389. auto screensManager = tgcalls::DesktopCaptureSourceManager(Type::Screen);
  390. auto windowsManager = tgcalls::DesktopCaptureSourceManager(Type::Window);
  391. _withAudio->setVisible(_delegate->chooseSourceWithAudioSupported());
  392. auto screenIndex = 0;
  393. auto windowIndex = 0;
  394. auto firstScreenSelected = false;
  395. const auto active = _delegate->chooseSourceActiveDeviceId();
  396. const auto append = [&](const tgcalls::DesktopCaptureSource &source) {
  397. const auto firstScreen = !source.isWindow() && !screenIndex;
  398. const auto title = !source.isWindow()
  399. ? tr::lng_group_call_screen_title(
  400. tr::now,
  401. lt_index,
  402. QString::number(++screenIndex))
  403. : !source.title().empty()
  404. ? QString::fromStdString(source.title())
  405. : "Window " + QString::number(++windowIndex);
  406. const auto id = source.deviceIdKey();
  407. _sources.push_back(std::make_unique<Source>(_inner, source, title));
  408. const auto raw = _sources.back().get();
  409. if (!active.isEmpty() && active.toStdString() == id) {
  410. _selected = raw;
  411. raw->setActive(true);
  412. } else if (active.isEmpty() && firstScreen) {
  413. _selected = raw;
  414. raw->setActive(true);
  415. firstScreenSelected = true;
  416. }
  417. _sources.back()->activations(
  418. ) | rpl::filter([=] {
  419. return (_selected != raw);
  420. }) | rpl::start_with_next([=]{
  421. if (_selected) {
  422. _selected->setActive(false);
  423. }
  424. _selected = raw;
  425. updateButtonsVisibility();
  426. }, raw->lifetime());
  427. };
  428. for (const auto &source : screensManager.sources()) {
  429. append(source);
  430. }
  431. for (const auto &source : windowsManager.sources()) {
  432. append(source);
  433. }
  434. if (firstScreenSelected) {
  435. updateButtonsVisibility();
  436. }
  437. }
  438. void ChooseSourceProcess::updateButtonsVisibility() {
  439. const auto selectedId = _selected
  440. ? _selected->deviceIdKey()
  441. : QString();
  442. if (selectedId == _delegate->chooseSourceActiveDeviceId()
  443. && (!_delegate->chooseSourceWithAudioSupported()
  444. || (_withAudio->checked()
  445. == _delegate->chooseSourceActiveWithAudio()))) {
  446. _selectedId = QString();
  447. _finish->setVisible(true);
  448. _submit->setVisible(false);
  449. } else {
  450. _selectedId = selectedId;
  451. _finish->setVisible(false);
  452. _submit->setVisible(true);
  453. }
  454. }
  455. void ChooseSourceProcess::setupSourcesGeometry() {
  456. if (_sources.empty()) {
  457. destroy();
  458. return;
  459. }
  460. _inner->widthValue(
  461. ) | rpl::start_with_next([=](int width) {
  462. const auto rows = int(std::ceil(_sources.size() / float(kColumns)));
  463. const auto margins = st::desktopCaptureMargins;
  464. const auto skips = st::desktopCaptureSourceSkips;
  465. const auto single = (width
  466. - margins.left()
  467. - margins.right()
  468. - (kColumns - 1) * skips.width()) / kColumns;
  469. const auto height = st::desktopCaptureSourceSize.height();
  470. auto top = margins.top();
  471. auto index = 0;
  472. for (auto row = 0; row != rows; ++row) {
  473. auto left = margins.left();
  474. for (auto column = 0; column != kColumns; ++column) {
  475. _sources[index]->setGeometry({ left, top, single, height });
  476. if (++index == _sources.size()) {
  477. break;
  478. }
  479. left += single + skips.width();
  480. }
  481. if (index >= _sources.size()) {
  482. break;
  483. }
  484. top += height + skips.height();
  485. }
  486. }, _inner->lifetime());
  487. rpl::combine(
  488. _scroll->scrollTopValue(),
  489. _scroll->heightValue()
  490. ) | rpl::start_with_next([=](int scrollTop, int scrollHeight) {
  491. const auto rows = int(std::ceil(_sources.size() / float(kColumns)));
  492. const auto margins = st::desktopCaptureMargins;
  493. const auto skips = st::desktopCaptureSourceSkips;
  494. const auto height = st::desktopCaptureSourceSize.height();
  495. auto top = margins.top();
  496. auto index = 0;
  497. for (auto row = 0; row != rows; ++row) {
  498. const auto hidden = (top + height <= scrollTop)
  499. || (top >= scrollTop + scrollHeight);
  500. if (hidden) {
  501. for (auto column = 0; column != kColumns; ++column) {
  502. _sources[index]->clearHelper();
  503. if (++index == _sources.size()) {
  504. break;
  505. }
  506. }
  507. } else {
  508. index += kColumns;
  509. }
  510. if (index >= _sources.size()) {
  511. break;
  512. }
  513. top += height + skips.height();
  514. }
  515. }, _inner->lifetime());
  516. }
  517. void ChooseSourceProcess::setupGeometryWithParent(
  518. not_null<QWidget*> parent) {
  519. const auto parentScreen = parent->screen();
  520. const auto myScreen = _window->screen();
  521. if (parentScreen && myScreen != parentScreen) {
  522. #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
  523. _window->setScreen(parentScreen);
  524. #else // Qt >= 6.0.0
  525. _window->createWinId();
  526. _window->windowHandle()->setScreen(parentScreen);
  527. #endif // Qt < 6.0.0
  528. }
  529. _window->setFixedSize(_fixedSize);
  530. _window->move(
  531. parent->x() + (parent->width() - _window->width()) / 2,
  532. parent->y() + (parent->height() - _window->height()) / 2);
  533. }
  534. void ChooseSourceProcess::destroy() {
  535. auto &map = Map();
  536. if (const auto i = map.find(_delegate); i != end(map)) {
  537. if (i->second.get() == this) {
  538. base::take(i->second);
  539. }
  540. }
  541. }
  542. } // namespace
  543. void ChooseSource(not_null<ChooseSourceDelegate*> delegate) {
  544. ChooseSourceProcess::Start(delegate);
  545. }
  546. } // namespace Calls::Group::Ui::DesktopCapture