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 &currentConfig)
66 {
67     m_currentConfig = currentConfig;
68 }
69 
idealConfig(const KScreen::ConfigPtr & currentConfig)70 KScreen::ConfigPtr Generator::idealConfig(const KScreen::ConfigPtr &currentConfig)
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