1 /*
2 SPDX-FileCopyrightText: 2012 Alejandro Fiestas Olivares <afiestas@kde.org>
3 SPDX-FileCopyrightText: 2021 David Redondo <kde@david-redondo.de>
4
5 SPDX-License-Identifier: GPL-2.0-or-later
6 */
7
8 #include "generator.h"
9 #include "device.h"
10 #include "kscreen_daemon_debug.h"
11 #include "output.h"
12 #include <QRect>
13
14 #include <kscreen/config.h>
15
16 #if defined(QT_NO_DEBUG)
17 #define ASSERT_OUTPUTS(outputs)
18 #else
19 #define ASSERT_OUTPUTS(outputs) \
20 while (true) { \
21 Q_ASSERT(!outputs.isEmpty()); \
22 Q_FOREACH (const KScreen::OutputPtr &output, outputs) { \
23 Q_ASSERT(output); \
24 Q_ASSERT(output->isConnected()); \
25 } \
26 break; \
27 }
28 #endif
29
30 Generator *Generator::instance = nullptr;
31
operator <(const QSize & s1,const QSize & s2)32 bool operator<(const QSize &s1, const QSize &s2)
33 {
34 return s1.width() * s1.height() < s2.width() * s2.height();
35 }
36
self()37 Generator *Generator::self()
38 {
39 if (!Generator::instance) {
40 Generator::instance = new Generator();
41 }
42 return Generator::instance;
43 }
44
Generator()45 Generator::Generator()
46 : QObject()
47 , m_forceLaptop(false)
48 , m_forceLidClosed(false)
49 , m_forceNotLaptop(false)
50 , m_forceDocked(false)
51 {
52 connect(Device::self(), &Device::ready, this, &Generator::ready);
53 }
54
destroy()55 void Generator::destroy()
56 {
57 delete Generator::instance;
58 Generator::instance = nullptr;
59 }
60
~Generator()61 Generator::~Generator()
62 {
63 }
64
setCurrentConfig(const KScreen::ConfigPtr & currentConfig)65 void Generator::setCurrentConfig(const KScreen::ConfigPtr ¤tConfig)
66 {
67 m_currentConfig = currentConfig;
68 }
69
idealConfig(const KScreen::ConfigPtr & currentConfig)70 KScreen::ConfigPtr Generator::idealConfig(const KScreen::ConfigPtr ¤tConfig)
71 {
72 Q_ASSERT(currentConfig);
73
74 // KDebug::Block idealBlock("Ideal Config");
75 KScreen::ConfigPtr config = currentConfig->clone();
76
77 disableAllDisconnectedOutputs(config->outputs());
78
79 KScreen::OutputList connectedOutputs = config->connectedOutputs();
80 qCDebug(KSCREEN_KDED) << "Connected outputs: " << connectedOutputs.count();
81
82 if (connectedOutputs.isEmpty()) {
83 return config;
84 }
85
86 for (const auto output : connectedOutputs) {
87 initializeOutput(output, config->supportedFeatures());
88 }
89
90 if (connectedOutputs.count() == 1) {
91 singleOutput(connectedOutputs);
92 return config;
93 }
94
95 if (isLaptop()) {
96 laptop(connectedOutputs);
97 return fallbackIfNeeded(config);
98 }
99
100 qCDebug(KSCREEN_KDED) << "Extend to Right";
101 extendToRight(connectedOutputs);
102
103 return fallbackIfNeeded(config);
104 }
105
fallbackIfNeeded(const KScreen::ConfigPtr & config)106 KScreen::ConfigPtr Generator::fallbackIfNeeded(const KScreen::ConfigPtr &config)
107 {
108 qCDebug(KSCREEN_KDED) << "fallbackIfNeeded()";
109
110 KScreen::ConfigPtr newConfig;
111
112 // If the ideal config can't be applied, try clonning
113 if (!KScreen::Config::canBeApplied(config)) {
114 if (isLaptop()) {
115 newConfig = displaySwitch(Generator::Clone); // Try to clone at our best
116 } else {
117 newConfig = config;
118 KScreen::OutputList connectedOutputs = config->connectedOutputs();
119 if (connectedOutputs.isEmpty()) {
120 return config;
121 }
122 connectedOutputs.value(connectedOutputs.keys().first())->setPrimary(true);
123 cloneScreens(connectedOutputs);
124 }
125 } else {
126 newConfig = config;
127 }
128
129 // If after trying to clone at our best, we fail... return current
130 if (!KScreen::Config::canBeApplied(newConfig)) {
131 qCDebug(KSCREEN_KDED) << "Config cannot be applied";
132 newConfig = config;
133 }
134
135 return config;
136 }
137
displaySwitch(DisplaySwitchAction action)138 KScreen::ConfigPtr Generator::displaySwitch(DisplaySwitchAction action)
139 {
140 // KDebug::Block switchBlock("Display Switch");
141 KScreen::ConfigPtr config = m_currentConfig;
142 Q_ASSERT(config);
143
144 KScreen::OutputList connectedOutputs = config->connectedOutputs();
145
146 for (const auto output : connectedOutputs) {
147 initializeOutput(output, config->supportedFeatures());
148 }
149
150 // There's not much else we can do with only one output
151 if (connectedOutputs.count() < 2) {
152 singleOutput(connectedOutputs);
153 return config;
154 }
155
156 // We cannot try all possible combinations with two and more outputs
157 if (connectedOutputs.count() > 2) {
158 extendToRight(connectedOutputs);
159 return config;
160 }
161
162 KScreen::OutputPtr embedded, external;
163 embedded = embeddedOutput(connectedOutputs);
164 // If we don't have an embedded output (desktop with two external screens
165 // for instance), then pretend one of them is embedded
166 if (!embedded) {
167 embedded = connectedOutputs.value(connectedOutputs.keys().first());
168 }
169 // Just to be sure
170 if (embedded->modes().isEmpty()) {
171 return config;
172 }
173
174 if (action == Generator::Clone) {
175 qCDebug(KSCREEN_KDED) << "Cloning";
176 embedded->setPrimary(true);
177 cloneScreens(connectedOutputs);
178 return config;
179 }
180
181 connectedOutputs.remove(embedded->id());
182 external = connectedOutputs.value(connectedOutputs.keys().first());
183 // Just to be sure
184 if (external->modes().isEmpty()) {
185 return config;
186 }
187
188 Q_ASSERT(embedded->currentMode());
189 Q_ASSERT(external->currentMode());
190
191 switch (action) {
192 case Generator::ExtendToLeft: {
193 qCDebug(KSCREEN_KDED) << "Extend to left";
194 external->setPos(QPoint(0, 0));
195 external->setEnabled(true);
196
197 const QSize size = external->geometry().size();
198 embedded->setPos(QPoint(size.width(), 0));
199 embedded->setEnabled(true);
200 embedded->setPrimary(true);
201
202 return config;
203 }
204 case Generator::TurnOffEmbedded: {
205 qCDebug(KSCREEN_KDED) << "Turn off embedded (laptop)";
206 embedded->setEnabled(false);
207 embedded->setPrimary(false);
208
209 external->setEnabled(true);
210 external->setPrimary(true);
211 return config;
212 }
213 case Generator::TurnOffExternal: {
214 qCDebug(KSCREEN_KDED) << "Turn off external screen";
215 embedded->setPos(QPoint(0, 0));
216 embedded->setEnabled(true);
217 embedded->setPrimary(true);
218
219 external->setEnabled(false);
220 external->setPrimary(false);
221 return config;
222 }
223 case Generator::ExtendToRight: {
224 qCDebug(KSCREEN_KDED) << "Extend to the right";
225 embedded->setPos(QPoint(0, 0));
226 embedded->setEnabled(true);
227 embedded->setPrimary(true);
228
229 Q_ASSERT(embedded->currentMode()); // we must have a mode now
230 const QSize size = embedded->geometry().size();
231 external->setPos(QPoint(size.width(), 0));
232 external->setEnabled(true);
233 external->setPrimary(false);
234
235 return config;
236 }
237 case Generator::None: // just return config
238 case Generator::Clone: // handled above
239 break;
240 } // switch
241
242 return config;
243 }
244
qHash(const QSize & size)245 uint qHash(const QSize &size)
246 {
247 return size.width() * size.height();
248 }
249
cloneScreens(KScreen::OutputList & connectedOutputs)250 void Generator::cloneScreens(KScreen::OutputList &connectedOutputs)
251 {
252 ASSERT_OUTPUTS(connectedOutputs);
253 if (connectedOutputs.isEmpty()) {
254 return;
255 }
256
257 QSet<QSize> commonSizes;
258 const QSize maxScreenSize = m_currentConfig->screen()->maxSize();
259
260 Q_FOREACH (const KScreen::OutputPtr &output, connectedOutputs) {
261 QSet<QSize> modeSizes;
262 Q_FOREACH (const KScreen::ModePtr &mode, output->modes()) {
263 const QSize size = mode->size();
264 if (size.width() > maxScreenSize.width() || size.height() > maxScreenSize.height()) {
265 continue;
266 }
267 modeSizes.insert(mode->size());
268 }
269
270 // If we have nothing to compare against
271 if (commonSizes.isEmpty()) {
272 commonSizes = modeSizes;
273 } else {
274 commonSizes.intersect(modeSizes);
275 }
276
277 // If there's already nothing in common, bail
278 if (commonSizes.isEmpty()) {
279 break;
280 }
281 }
282
283 qCDebug(KSCREEN_KDED) << "Common sizes: " << commonSizes;
284 // fallback to biggestMode if no commonSizes have been found
285 if (commonSizes.isEmpty()) {
286 Q_FOREACH (KScreen::OutputPtr output, connectedOutputs) {
287 if (output->modes().isEmpty()) {
288 continue;
289 }
290 output->setEnabled(true);
291 output->setPos(QPoint(0, 0));
292 const KScreen::ModePtr mode = biggestMode(output->modes());
293 Q_ASSERT(mode);
294 output->setCurrentModeId(mode->id());
295 }
296 return;
297 }
298
299 // At this point, we know we have common sizes, let's get the biggest on
300 QList<QSize> commonSizeList = commonSizes.values();
301 std::sort(commonSizeList.begin(), commonSizeList.end());
302 const QSize biggestSize = commonSizeList.last();
303
304 // Finally, look for the mode with biggestSize and biggest refreshRate and set it
305 qCDebug(KSCREEN_KDED) << "Biggest Size: " << biggestSize;
306 KScreen::ModePtr bestMode;
307 Q_FOREACH (KScreen::OutputPtr output, connectedOutputs) {
308 if (output->modes().isEmpty()) {
309 continue;
310 }
311 bestMode = bestModeForSize(output->modes(), biggestSize);
312 Q_ASSERT(bestMode); // we resolved this mode previously, so it better works
313 output->setEnabled(true);
314 output->setPos(QPoint(0, 0));
315 output->setCurrentModeId(bestMode->id());
316 }
317 }
318
singleOutput(KScreen::OutputList & connectedOutputs)319 void Generator::singleOutput(KScreen::OutputList &connectedOutputs)
320 {
321 ASSERT_OUTPUTS(connectedOutputs);
322 if (connectedOutputs.isEmpty()) {
323 return;
324 }
325
326 KScreen::OutputPtr output = connectedOutputs.take(connectedOutputs.keys().first());
327 if (output->modes().isEmpty()) {
328 return;
329 }
330 output->setEnabled(true);
331 output->setPrimary(true);
332 output->setPos(QPoint(0, 0));
333 }
334
laptop(KScreen::OutputList & connectedOutputs)335 void Generator::laptop(KScreen::OutputList &connectedOutputs)
336 {
337 ASSERT_OUTPUTS(connectedOutputs)
338 if (connectedOutputs.isEmpty()) {
339 return;
340 }
341
342 // KDebug::Block laptopBlock("Laptop config");
343
344 KScreen::OutputPtr embedded = embeddedOutput(connectedOutputs);
345 /* Apparently older laptops use "VGA-*" as embedded output ID, so embeddedOutput()
346 * will fail, because it looks only for modern "LVDS", "EDP", etc. If we
347 * fail to detect which output is embedded, just use the one with the lowest
348 * ID. It's a wild guess, but I think it's highly probable that it will work.
349 * See bug #318907 for further reference. -- dvratil
350 */
351 if (!embedded) {
352 QList<int> keys = connectedOutputs.keys();
353 std::sort(keys.begin(), keys.end());
354 embedded = connectedOutputs.value(keys.first());
355 }
356 connectedOutputs.remove(embedded->id());
357
358 if (connectedOutputs.isEmpty() || embedded->modes().isEmpty()) {
359 qCWarning(KSCREEN_KDED) << "No external outputs found, going for singleOutput()";
360 connectedOutputs.insert(embedded->id(), embedded);
361 return singleOutput(connectedOutputs);
362 }
363
364 if (isLidClosed() && connectedOutputs.count() == 1) {
365 qCDebug(KSCREEN_KDED) << "With lid closed";
366 embedded->setEnabled(false);
367 embedded->setPrimary(false);
368
369 KScreen::OutputPtr external = connectedOutputs.value(connectedOutputs.keys().first());
370 if (external->modes().isEmpty()) {
371 return;
372 }
373 external->setEnabled(true);
374 external->setPrimary(true);
375 external->setPos(QPoint(0, 0));
376
377 return;
378 }
379
380 if (isLidClosed() && connectedOutputs.count() > 1) {
381 qCDebug(KSCREEN_KDED) << "Lid is closed, and more than one output";
382 embedded->setEnabled(false);
383 embedded->setPrimary(false);
384
385 extendToRight(connectedOutputs);
386 return;
387 }
388
389 qCDebug(KSCREEN_KDED) << "Lid is open";
390 // If lid is open, laptop screen should be primary
391 embedded->setPos(QPoint(0, 0));
392 embedded->setPrimary(true);
393 embedded->setEnabled(true);
394
395 int globalWidth = embedded->geometry().width();
396 KScreen::OutputPtr biggest = biggestOutput(connectedOutputs);
397 Q_ASSERT(biggest);
398 connectedOutputs.remove(biggest->id());
399
400 biggest->setPos(QPoint(globalWidth, 0));
401 biggest->setEnabled(true);
402 biggest->setPrimary(false);
403
404 globalWidth += biggest->geometry().width();
405 Q_FOREACH (KScreen::OutputPtr output, connectedOutputs) {
406 output->setEnabled(true);
407 output->setPrimary(false);
408 output->setPos(QPoint(globalWidth, 0));
409
410 globalWidth += output->geometry().width();
411 }
412
413 if (isDocked()) {
414 qCDebug(KSCREEN_KDED) << "Docked";
415 embedded->setPrimary(false);
416 biggest->setPrimary(true);
417 }
418 }
419
extendToRight(KScreen::OutputList & connectedOutputs)420 void Generator::extendToRight(KScreen::OutputList &connectedOutputs)
421 {
422 ASSERT_OUTPUTS(connectedOutputs);
423 if (connectedOutputs.isEmpty()) {
424 return;
425 }
426
427 qCDebug(KSCREEN_KDED) << "Extending to the right";
428 KScreen::OutputPtr biggest = biggestOutput(connectedOutputs);
429 Q_ASSERT(biggest);
430
431 connectedOutputs.remove(biggest->id());
432
433 biggest->setEnabled(true);
434 biggest->setPrimary(true);
435 biggest->setPos(QPoint(0, 0));
436
437 int globalWidth = biggest->geometry().width();
438
439 Q_FOREACH (KScreen::OutputPtr output, connectedOutputs) {
440 output->setEnabled(true);
441 output->setPrimary(false);
442 output->setPos(QPoint(globalWidth, 0));
443
444 globalWidth += output->geometry().width();
445 }
446 }
447
initializeOutput(const KScreen::OutputPtr & output,KScreen::Config::Features features)448 void Generator::initializeOutput(const KScreen::OutputPtr &output, KScreen::Config::Features features)
449 {
450 Output::GlobalConfig config = Output::readGlobal(output);
451 output->setCurrentModeId(config.modeId.value_or(bestModeForOutput(output)->id()));
452 output->setRotation(config.rotation.value_or(output->rotation()));
453 if (features & KScreen::Config::Feature::PerOutputScaling) {
454 output->setScale(config.scale.value_or(bestScaleForOutput(output)));
455 }
456 }
457
biggestMode(const KScreen::ModeList & modes)458 KScreen::ModePtr Generator::biggestMode(const KScreen::ModeList &modes)
459 {
460 Q_ASSERT(!modes.isEmpty());
461
462 int modeArea, biggestArea = 0;
463 KScreen::ModePtr biggestMode;
464 Q_FOREACH (const KScreen::ModePtr &mode, modes) {
465 modeArea = mode->size().width() * mode->size().height();
466 if (modeArea < biggestArea) {
467 continue;
468 }
469 if (modeArea == biggestArea && mode->refreshRate() < biggestMode->refreshRate()) {
470 continue;
471 }
472 if (modeArea == biggestArea && mode->refreshRate() > biggestMode->refreshRate()) {
473 biggestMode = mode;
474 continue;
475 }
476
477 biggestArea = modeArea;
478 biggestMode = mode;
479 }
480
481 return biggestMode;
482 }
483
bestModeForSize(const KScreen::ModeList & modes,const QSize & size)484 KScreen::ModePtr Generator::bestModeForSize(const KScreen::ModeList &modes, const QSize &size)
485 {
486 KScreen::ModePtr bestMode;
487 Q_FOREACH (const KScreen::ModePtr &mode, modes) {
488 if (mode->size() != size) {
489 continue;
490 }
491
492 if (!bestMode) {
493 bestMode = mode;
494 continue;
495 }
496
497 if (mode->refreshRate() > bestMode->refreshRate()) {
498 bestMode = mode;
499 }
500 }
501
502 return bestMode;
503 }
504
bestScaleForOutput(const KScreen::OutputPtr & output)505 qreal Generator::bestScaleForOutput(const KScreen::OutputPtr &output)
506 {
507 // if we have no physical size, we can't determine the DPI properly. Fallback to scale 1
508 if (output->sizeMm().height() <= 0) {
509 return 1.0;
510 }
511 const auto mode = output->currentMode();
512 const qreal dpi = mode->size().height() / (output->sizeMm().height() / 25.4);
513
514 // if reported DPI is closer to two times normal DPI, followed by a sanity check of having the sort of vertical resolution
515 // you'd find in a high res screen
516 if (dpi > 96 * 1.5 && mode->size().height() >= 1440) {
517 return 2.0;
518 }
519 return 1.0;
520 }
521
bestModeForOutput(const KScreen::OutputPtr & output)522 KScreen::ModePtr Generator::bestModeForOutput(const KScreen::OutputPtr &output)
523 {
524 if (KScreen::ModePtr outputMode = output->preferredMode()) {
525 return outputMode;
526 }
527
528 return biggestMode(output->modes());
529 }
530
biggestOutput(const KScreen::OutputList & outputs)531 KScreen::OutputPtr Generator::biggestOutput(const KScreen::OutputList &outputs)
532 {
533 ASSERT_OUTPUTS(outputs)
534
535 int area, total = 0;
536 KScreen::OutputPtr biggest;
537 Q_FOREACH (const KScreen::OutputPtr &output, outputs) {
538 const KScreen::ModePtr mode = bestModeForOutput(output);
539 if (!mode) {
540 continue;
541 }
542 area = mode->size().width() * mode->size().height();
543 if (area <= total) {
544 continue;
545 }
546
547 total = area;
548 biggest = output;
549 }
550
551 return biggest;
552 }
553
disableAllDisconnectedOutputs(const KScreen::OutputList & outputs)554 void Generator::disableAllDisconnectedOutputs(const KScreen::OutputList &outputs)
555 {
556 // KDebug::Block disableBlock("Disabling disconnected screens");
557 Q_FOREACH (KScreen::OutputPtr output, outputs) {
558 if (!output->isConnected()) {
559 qCDebug(KSCREEN_KDED) << output->name() << " Disabled";
560 output->setEnabled(false);
561 output->setPrimary(false);
562 }
563 }
564 }
565
embeddedOutput(const KScreen::OutputList & outputs)566 KScreen::OutputPtr Generator::embeddedOutput(const KScreen::OutputList &outputs)
567 {
568 Q_FOREACH (const KScreen::OutputPtr &output, outputs) {
569 if (output->type() != KScreen::Output::Panel) {
570 continue;
571 }
572
573 return output;
574 }
575
576 return KScreen::OutputPtr();
577 }
578
isLaptop() const579 bool Generator::isLaptop() const
580 {
581 if (m_forceLaptop) {
582 return true;
583 }
584 if (m_forceNotLaptop) {
585 return false;
586 }
587
588 return Device::self()->isLaptop();
589 }
590
isLidClosed() const591 bool Generator::isLidClosed() const
592 {
593 if (m_forceLidClosed) {
594 return true;
595 }
596 if (m_forceNotLaptop) {
597 return false;
598 }
599
600 return Device::self()->isLidClosed();
601 }
602
isDocked() const603 bool Generator::isDocked() const
604 {
605 if (m_forceDocked) {
606 return true;
607 }
608
609 return Device::self()->isDocked();
610 }
611
setForceLaptop(bool force)612 void Generator::setForceLaptop(bool force)
613 {
614 m_forceLaptop = force;
615 }
616
setForceLidClosed(bool force)617 void Generator::setForceLidClosed(bool force)
618 {
619 m_forceLidClosed = force;
620 }
621
setForceDocked(bool force)622 void Generator::setForceDocked(bool force)
623 {
624 m_forceDocked = force;
625 }
626
setForceNotLaptop(bool force)627 void Generator::setForceNotLaptop(bool force)
628 {
629 m_forceNotLaptop = force;
630 }
631