File indexing completed on 2024-12-08 13:22:28

0001 /*
0002  *  Copyright 2016 Marco Martin <mart@kde.org>
0003  *
0004  *   This program is free software; you can redistribute it and/or modify
0005  *   it under the terms of the GNU Library General Public License as
0006  *   published by the Free Software Foundation; either version 2, or
0007  *   (at your option) any later version.
0008  *
0009  *   This program is distributed in the hope that it will be useful,
0010  *   but WITHOUT ANY WARRANTY; without even the implied warranty of
0011  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
0012  *   GNU General Public License for more details
0013  *
0014  *   You should have received a copy of the GNU Library General Public
0015  *   License along with this program; if not, write to the
0016  *   Free Software Foundation, Inc.,
0017  *   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
0018  */
0019 
0020 #include "screenpool.h"
0021 
0022 // local
0023 #include <config-latte.h>
0024 
0025 // Qt
0026 #include <QDebug>
0027 #include <QFile>
0028 #include <QGuiApplication>
0029 #include <QScreen>
0030 
0031 // KDE
0032 #include <KLocalizedString>
0033 
0034 // X11
0035 #if HAVE_X11
0036     #include <QtX11Extras/QX11Info>
0037     #include <xcb/xcb.h>
0038     #include <xcb/randr.h>
0039     #include <xcb/xcb_event.h>
0040 #endif
0041 
0042 namespace Latte {
0043 
0044 ScreenPool::ScreenPool(KSharedConfig::Ptr config, QObject *parent)
0045     : QObject(parent),
0046       m_configGroup(KConfigGroup(config, QStringLiteral("ScreenConnectors")))
0047 {
0048     qApp->installNativeEventFilter(this);
0049 
0050     m_configSaveTimer.setSingleShot(true);
0051     connect(&m_configSaveTimer, &QTimer::timeout, this, [this]() {
0052         m_configGroup.sync();
0053     });
0054 }
0055 
0056 void ScreenPool::load()
0057 {
0058     m_primaryConnector = QString();
0059     m_connectorForId.clear();
0060     m_idForConnector.clear();
0061 
0062     QScreen *primary = qGuiApp->primaryScreen();
0063 
0064     if (primary) {
0065         m_primaryConnector = primary->name();
0066 
0067         if (!m_primaryConnector.isEmpty()) {
0068             //m_connectorForId[0] = m_primaryConnector;
0069             //m_idForConnector[m_primaryConnector] = 0;
0070         }
0071     }
0072 
0073     //restore the known ids to connector mappings
0074     for (const QString &key : m_configGroup.keyList()) {
0075         QString connector = m_configGroup.readEntry(key, QString());
0076         qDebug() << "connector :" << connector << " - " << key;
0077 
0078         if (!key.isEmpty() && !connector.isEmpty() &&
0079             !m_connectorForId.contains(key.toInt()) &&
0080             !m_idForConnector.contains(connector)) {
0081             m_connectorForId[key.toInt()] = connector;
0082             m_idForConnector[connector] = key.toInt();
0083             qDebug() << "Known Screen - " << connector << " - " << key.toInt();
0084         } else if (m_idForConnector.value(connector) != key.toInt()) {
0085             m_configGroup.deleteEntry(key);
0086         }
0087     }
0088 
0089     // if there are already connected unknown screens, map those
0090     // all needs to be populated as soon as possible, otherwise
0091     // containment->screen() will return an incorrect -1
0092     // at startup, if it' asked before corona::addOutput()
0093     // is performed, driving to the creation of a new containment
0094     for (QScreen *screen : qGuiApp->screens()) {
0095         if (!m_idForConnector.contains(screen->name())) {
0096             insertScreenMapping(firstAvailableId(), screen->name());
0097         }
0098     }
0099 }
0100 
0101 ScreenPool::~ScreenPool()
0102 {
0103     m_configGroup.sync();
0104 }
0105 
0106 
0107 QString ScreenPool::reportHtml(const QList<int> &assignedScreens) const
0108 {
0109     QString report;
0110 
0111     report += "<table cellspacing='8'>";
0112     report += "<tr><td align='center'><b>" + i18nc("screen id","ID") + "</b></td>" +
0113             "<td align='center'><b>" + i18nc("screen name", "Name") + "</b></td>" +
0114             "<td align='center'><b>" + i18nc("screen type", "Type") + "</b></td>" +
0115             "<td align='center'><b>" + i18n("Docks/Panels") + "</b></td></tr>";
0116 
0117     report += "<tr><td colspan='4'><hr></td></tr>";
0118 
0119     for(const QString &connector : m_connectorForId) {
0120         int scrId = id(connector);
0121         bool hasViews = assignedScreens.contains(scrId);
0122         bool primary = m_primaryConnector == connector;
0123         bool secondary = !primary && screenExists(scrId);
0124 
0125         report += "<tr>";
0126 
0127         //! ScreenId
0128         QString idStr = "[" + QString::number(scrId) + "]";
0129         if (primary || secondary) {
0130             idStr = "<b>" + idStr +"</b>";
0131         }
0132         report += "<td align='center'>" + idStr + "</td>";
0133 
0134         //! ScreenName and Primary flag
0135         QString connectorStr = connector;
0136         if (primary || secondary) {
0137             connectorStr = "<b>"+ connector + "</b>";
0138         }
0139         report += "<td align='center'>" + connectorStr + "</td>";
0140 
0141         //! ScreenType
0142         QString typeStr = "";
0143         if (primary) {
0144             typeStr = "<b><font color='green'>[" + i18nc("primary screen","Primary") + "]</font></b>";
0145         } else if (secondary) {
0146             typeStr = "<b>[" + i18nc("secondary screen","Secondary") + "]</b>";
0147         } else {
0148             typeStr = "<i>" + i18nc("inactive screen","inactive") + "</i>";
0149         }
0150 
0151         report += "<td align='center'>" + typeStr +"</td>";
0152 
0153         //! Screen has not assigned any docks/panels
0154         QString notAssignedStr = "";
0155         if (!hasViews) {
0156             notAssignedStr = "<font color='red'><i>[" + i18nc("it has not latte docks/panels", "none") + "]</i></font>";
0157         } else {
0158             notAssignedStr = " ✔ ";
0159         }
0160 
0161         report += "<td align='center'>" + notAssignedStr +"</td>";
0162 
0163         report += "</tr>";
0164     }
0165 
0166     report += "</table>";
0167 
0168     return report;
0169 }
0170 
0171 void ScreenPool::reload(QString path)
0172 {
0173     QFile rcfile(QString(path + "/lattedockrc"));
0174 
0175     if (rcfile.exists()) {
0176         qDebug() << "load screen connectors from ::: " << rcfile.fileName();
0177         KSharedConfigPtr newFile = KSharedConfig::openConfig(rcfile.fileName());
0178         m_configGroup = KConfigGroup(newFile, QStringLiteral("ScreenConnectors"));
0179         load();
0180     }
0181 
0182 
0183 
0184 }
0185 
0186 int ScreenPool::primaryScreenId() const
0187 {
0188     return id(qGuiApp->primaryScreen()->name());
0189 }
0190 
0191 QString ScreenPool::primaryConnector() const
0192 {
0193     return m_primaryConnector;
0194 }
0195 
0196 void ScreenPool::setPrimaryConnector(const QString &primary)
0197 {
0198     //the ":" check fixes the strange plasma/qt issues when changing layouts
0199     //there are case that the QScreen instead of the correct screen name
0200     //returns "0:0", this check prevents from breaking the screens database
0201     //from garbage ids
0202     if ((m_primaryConnector == primary) || primary.startsWith(":")) {
0203         return;
0204     }
0205 
0206     Q_ASSERT(m_idForConnector.contains(primary));
0207 
0208     /* int oldIdForPrimary = m_idForConnector.value(primary);
0209 
0210      m_idForConnector[primary] = 0;
0211      m_connectorForId[0] = primary;
0212      m_idForConnector[m_primaryConnector] = oldIdForPrimary;
0213      m_connectorForId[oldIdForPrimary] = m_primaryConnector;
0214      m_primaryConnector = primary;
0215     */
0216     save();
0217 }
0218 
0219 void ScreenPool::save()
0220 {
0221     QMap<int, QString>::const_iterator i;
0222 
0223     for (i = m_connectorForId.constBegin(); i != m_connectorForId.constEnd(); ++i) {
0224         m_configGroup.writeEntry(QString::number(i.key()), i.value());
0225     }
0226 
0227     //write to disck every 30 seconds at most
0228     m_configSaveTimer.start(30000);
0229 }
0230 
0231 void ScreenPool::insertScreenMapping(int id, const QString &connector)
0232 {
0233     //Q_ASSERT(!m_connectorForId.contains(id) || m_connectorForId.value(id) == connector);
0234     //Q_ASSERT(!m_idForConnector.contains(connector) || m_idForConnector.value(connector) == id);
0235 
0236     //the ":" check fixes the strange plasma/qt issues when changing layouts
0237     //there are case that the QScreen instead of the correct screen name
0238     //returns "0:0", this check prevents from breaking the screens database
0239     //from garbage ids
0240     if (connector.startsWith(":"))
0241         return;
0242 
0243     qDebug() << "add connector..." << connector;
0244 
0245     if (id == 0) {
0246         m_primaryConnector = connector;
0247     } else {
0248         m_connectorForId[id] = connector;
0249         m_idForConnector[connector] = id;
0250     }
0251 
0252     save();
0253 }
0254 
0255 int ScreenPool::id(const QString &connector) const
0256 {
0257     if (!m_idForConnector.contains(connector)) {
0258         return -1;
0259     }
0260 
0261     return m_idForConnector.value(connector);
0262 }
0263 
0264 QString ScreenPool::connector(int id) const
0265 {   
0266     Q_ASSERT(m_connectorForId.contains(id));
0267 
0268     return m_connectorForId.value(id);
0269 }
0270 
0271 int ScreenPool::firstAvailableId() const
0272 {
0273     //start counting from 10, first numbers will
0274     //be used for special cases.
0275     //e.g primaryScreen, id=0
0276     int i = 10;
0277 
0278     //find the first integer not stored in m_connectorForId
0279     //m_connectorForId is the only map, so the ids are sorted
0280     for (const int &existingId : m_connectorForId.keys()) {
0281         if (i != existingId) {
0282             return i;
0283         }
0284 
0285         ++i;
0286     }
0287 
0288     return i;
0289 }
0290 
0291 QList <int> ScreenPool::knownIds() const
0292 {
0293     return m_connectorForId.keys();
0294 }
0295 
0296 bool ScreenPool::hasId(int id) const
0297 {
0298     return ((id!=-1) && m_connectorForId.keys().contains(id));
0299 }
0300 
0301 bool ScreenPool::screenExists(int id) const
0302 {
0303     if (id != -1 && knownIds().contains(id)) {
0304         QString scrName = connector(id);
0305 
0306         for (const auto scr : qGuiApp->screens()) {
0307             if (scr->name() == scrName) {
0308                 return true;
0309             }
0310         }
0311     }
0312 
0313     return false;
0314 }
0315 
0316 QScreen *ScreenPool::screenForId(int id)
0317 {
0318     const auto screens = qGuiApp->screens();
0319     QScreen *screen{qGuiApp->primaryScreen()};
0320 
0321     if (id != -1 && knownIds().contains(id)) {
0322         QString scrName = connector(id);
0323 
0324         for (const auto scr : screens) {
0325             if (scr->name() == scrName) {
0326                 return scr;
0327             }
0328         }
0329     }
0330 
0331     return screen;
0332 }
0333 
0334 
0335 bool ScreenPool::nativeEventFilter(const QByteArray &eventType, void *message, long int *result)
0336 {
0337     Q_UNUSED(result);
0338 #if HAVE_X11
0339 
0340     // a particular edge case: when we switch the only enabled screen
0341     // we don't have any signal about it, the primary screen changes but we have the same old QScreen* getting recycled
0342     // see https://bugs.kde.org/show_bug.cgi?id=373880
0343     // if this slot will be invoked many times, their//second time on will do nothing as name and primaryconnector will be the same by then
0344     if (eventType != "xcb_generic_event_t") {
0345         return false;
0346     }
0347 
0348     xcb_generic_event_t *ev = static_cast<xcb_generic_event_t *>(message);
0349 
0350     const auto responseType = XCB_EVENT_RESPONSE_TYPE(ev);
0351 
0352     const xcb_query_extension_reply_t *reply = xcb_get_extension_data(QX11Info::connection(), &xcb_randr_id);
0353 
0354     if (responseType == reply->first_event + XCB_RANDR_SCREEN_CHANGE_NOTIFY) {
0355         if (qGuiApp->primaryScreen()->name() != primaryConnector()) {
0356             //new screen?
0357             if (id(qGuiApp->primaryScreen()->name()) < 0) {
0358                 insertScreenMapping(firstAvailableId(), qGuiApp->primaryScreen()->name());
0359             }
0360 
0361             //switch the primary screen in the pool
0362             setPrimaryConnector(qGuiApp->primaryScreen()->name());
0363 
0364             emit primaryPoolChanged();
0365         }
0366     }
0367 
0368 #endif
0369     return false;
0370 }
0371 
0372 }
0373 
0374 #include "moc_screenpool.cpp"