diff --git a/kcm/kcm.cpp b/kcm/kcm.cpp index 6c74f5f..e6883e8 100644 --- a/kcm/kcm.cpp +++ b/kcm/kcm.cpp @@ -1,393 +1,393 @@ /******************************************************************** Copyright © 2019 Roman Gilg This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . *********************************************************************/ #include "kcm.h" #include "config_handler.h" #include "kcm_screen_debug.h" #include "output_identifier.h" #include "output_model.h" #include "../common/control.h" #include #include #include #include #include #include #include #include #include #include #include #include K_PLUGIN_FACTORY_WITH_JSON(KCMDisplayConfigurationFactory, "kcm_kscreen.json", registerPlugin();) using namespace KScreen; KCMKScreen::KCMKScreen(QObject *parent, const QVariantList &args) : KQuickAddons::ConfigModule(parent, args) { qmlRegisterType(); qmlRegisterType("org.kde.private.kcm.kscreen", 1, 0, "Output"); qmlRegisterUncreatableType("org.kde.private.kcm.kscreen", 1, 0, "Control", QStringLiteral("Provides only the OutputRetention enum class")); Log::instance(); KAboutData *about = new KAboutData(QStringLiteral("kcm_kscreen"), i18n("Display Configuration"), QStringLiteral(KSCREEN_VERSION), - i18n("Configuration for displays"), + i18n("Manage and configure monitors and displays"), KAboutLicense::GPL, i18n("Copyright © 2019 Roman Gilg")); about->addAuthor(i18n("Roman Gilg"), i18n("Maintainer"), QStringLiteral("subdiff@gmail.com")); setAboutData(about); setButtons(Apply); } void KCMKScreen::configReady(ConfigOperation *op) { qCDebug(KSCREEN_KCM) << "Reading in config now."; if (op->hasError()) { m_config.reset(); setBackendError(true); return; } m_config->setConfig(qobject_cast(op)->config()); Q_EMIT perOutputScalingChanged(); Q_EMIT primaryOutputSupportedChanged(); Q_EMIT outputReplicationSupportedChanged(); } void KCMKScreen::forceSave() { doSave(true); } void KCMKScreen::save() { doSave(false); } void KCMKScreen::doSave(bool force) { if (!m_config) { Q_EMIT errorOnSave(); return; } auto config = m_config->config(); bool atLeastOneEnabledOutput = false; for (const KScreen::OutputPtr &output : config->outputs()) { KScreen::ModePtr mode = output->currentMode(); atLeastOneEnabledOutput |= output->isEnabled(); qCDebug(KSCREEN_KCM) << output->name() << output->id() << output.data() << "\n" << " Connected:" << output->isConnected() << "\n" << " Enabled:" << output->isEnabled() << "\n" << " Primary:" << output->isPrimary() << "\n" << " Rotation:" << output->rotation() << "\n" << " Mode:" << (mode ? mode->name() : QStringLiteral("unknown")) << "@" << (mode ? mode->refreshRate() : 0.0) << "Hz" << "\n" << " Position:" << output->pos().x() << "x" << output->pos().y() << "\n" << " Scale:" << (perOutputScaling() ? QString::number(output->scale()) : QStringLiteral("global")) << "\n" << " Replicates:" << (output->replicationSource() == 0 ? "no" : "yes"); } if (!atLeastOneEnabledOutput && !force) { Q_EMIT dangerousSave(); m_config->checkNeedsSave(); return; } if (!Config::canBeApplied(config)) { Q_EMIT errorOnSave(); m_config->checkNeedsSave(); return; } if (!perOutputScaling()) { writeGlobalScale(); } m_config->writeControl(); // Store the current config, apply settings. Block until operation is // completed, otherwise ConfigModule might terminate before we get to // execute the Operation. auto *op = new SetConfigOperation(config); op->exec(); // The 1000ms is a legacy value tested to work for randr having // enough time to change configuration. QTimer::singleShot(1000, this, [this] () { if (!m_config) { setNeedsSave(false); return; } m_config->updateInitialConfig(); } ); } bool KCMKScreen::backendError() const { return m_backendError; } void KCMKScreen::setBackendError(bool error) { if (m_backendError == error) { return; } m_backendError = error; Q_EMIT backendErrorChanged(); } OutputModel* KCMKScreen::outputModel() const { if (!m_config) { return nullptr; } return m_config->outputModel(); } void KCMKScreen::identifyOutputs() { if (!m_config || !m_config->config() || m_outputIdentifier) { return; } m_outputIdentifier.reset(new OutputIdentifier(m_config->config(), this)); connect(m_outputIdentifier.get(), &OutputIdentifier::identifiersFinished, this, [this]() { m_outputIdentifier.reset(); }); } QSize KCMKScreen::normalizeScreen() const { if (!m_config) { return QSize(); } return m_config->normalizeScreen(); } bool KCMKScreen::screenNormalized() const { return m_screenNormalized; } bool KCMKScreen::perOutputScaling() const { if (!m_config || !m_config->config()) { return false; } return m_config->config()->supportedFeatures().testFlag(Config::Feature:: PerOutputScaling); } bool KCMKScreen::primaryOutputSupported() const { if (!m_config || !m_config->config()) { return false; } return m_config->config()->supportedFeatures().testFlag(Config::Feature:: PrimaryDisplay); } bool KCMKScreen::outputReplicationSupported() const { if (!m_config || !m_config->config()) { return false; } return m_config->config()->supportedFeatures().testFlag(Config::Feature:: OutputReplication); } void KCMKScreen::setScreenNormalized(bool normalized) { if (m_screenNormalized == normalized) { return; } m_screenNormalized = normalized; Q_EMIT screenNormalizedChanged(); } void KCMKScreen::defaults() { qCDebug(KSCREEN_KCM) << "Applying defaults."; load(); } void KCMKScreen::load() { qCDebug(KSCREEN_KCM) << "About to read in config."; setBackendError(false); setNeedsSave(false); if (!screenNormalized()) { Q_EMIT screenNormalizedChanged(); } fetchGlobalScale(); m_config.reset(new ConfigHandler(this)); Q_EMIT perOutputScalingChanged(); connect (m_config.get(), &ConfigHandler::outputModelChanged, this, &KCMKScreen::outputModelChanged); connect (m_config.get(), &ConfigHandler::outputConnect, this, [this](bool connected) { load(); Q_EMIT outputConnect(connected); }); connect (m_config.get(), &ConfigHandler::screenNormalizationUpdate, this, &KCMKScreen::setScreenNormalized); connect (m_config.get(), &ConfigHandler::retentionChanged, this, &KCMKScreen::outputRetentionChanged); // This is a queued connection so that we can fire the event from // within the save() call in case it failed. connect (m_config.get(), &ConfigHandler::needsSaveChecked, this, &KCMKScreen::continueNeedsSaveCheck, Qt::QueuedConnection); connect (m_config.get(), &ConfigHandler::changed, this, &KCMKScreen::changed); connect(new GetConfigOperation(), &GetConfigOperation::finished, this, &KCMKScreen::configReady); Q_EMIT changed(); } void KCMKScreen::continueNeedsSaveCheck(bool needs) { if (needs || m_globalScale != m_initialGlobalScale) { setNeedsSave(true); } else { setNeedsSave(false); } } void KCMKScreen::fetchGlobalScale() { const auto config = KSharedConfig::openConfig(QStringLiteral("kdeglobals")); const qreal scale = config->group("KScreen").readEntry("ScaleFactor", 1.0); m_initialGlobalScale = scale; setGlobalScale(scale); } void KCMKScreen::writeGlobalScale() { if (qFuzzyCompare(m_initialGlobalScale, m_globalScale)) { return; } auto config = KSharedConfig::openConfig(QStringLiteral("kdeglobals")); config->group("KScreen").writeEntry("ScaleFactor", m_globalScale); // Write env var to be used by session startup scripts to populate the QT_SCREEN_SCALE_FACTORS // env var. // We use QT_SCREEN_SCALE_FACTORS as opposed to QT_SCALE_FACTOR as we need to use one that will // NOT scale fonts according to the scale. // Scaling the fonts makes sense if you don't also set a font DPI, but we NEED to set a font // DPI for both PlasmaShell which does it's own thing, and for KDE4/GTK2 applications. QString screenFactors; for (const KScreen::OutputPtr output : m_config->config()->outputs()) { screenFactors.append(output->name() + QLatin1Char('=') + QString::number(m_globalScale) + QLatin1Char(';')); } config->group("KScreen").writeEntry("ScreenScaleFactors", screenFactors); KConfig fontConfig(QStringLiteral("kcmfonts")); auto fontConfigGroup = fontConfig.group("General"); if (qFuzzyCompare(m_globalScale, 1.0)) { //if dpi is the default (96) remove the entry rather than setting it QProcess proc; proc.start(QStringLiteral("xrdb -quiet -remove -nocpp")); if (proc.waitForStarted()) { proc.write(QByteArray("Xft.dpi\n")); proc.closeWriteChannel(); proc.waitForFinished(); } fontConfigGroup.writeEntry("forceFontDPI", 0); } else { const int scaleDpi = qRound(m_globalScale * 96.0); QProcess proc; proc.start(QStringLiteral("xrdb -quiet -merge -nocpp")); if (proc.waitForStarted()) { proc.write(QByteArray("Xft.dpi: " + QString::number(scaleDpi).toLatin1())); proc.closeWriteChannel(); proc.waitForFinished(); } fontConfigGroup.writeEntry("forceFontDPI", scaleDpi); } m_initialGlobalScale = m_globalScale; Q_EMIT globalScaleWritten(); } qreal KCMKScreen::globalScale() const { return m_globalScale; } void KCMKScreen::setGlobalScale(qreal scale) { if (qFuzzyCompare(m_globalScale, scale)) { return; } m_globalScale = scale; if (m_config) { m_config->checkNeedsSave(); } else { continueNeedsSaveCheck(false); } Q_EMIT changed(); Q_EMIT globalScaleChanged(); } int KCMKScreen::outputRetention() const { if (!m_config) { return -1; } return m_config->retention(); } void KCMKScreen::setOutputRetention(int retention) { if (!m_config) { return; } return m_config->setRetention(retention); } #include "kcm.moc" diff --git a/kcm/kcm_kscreen.desktop b/kcm/kcm_kscreen.desktop index b1c06e0..293bc2e 100644 --- a/kcm/kcm_kscreen.desktop +++ b/kcm/kcm_kscreen.desktop @@ -1,122 +1,122 @@ [Desktop Entry] Exec=kcmshell5 kcm_kscreen Icon=preferences-desktop-display-randr Type=Service X-KDE-ServiceTypes=KCModule X-KDE-Library=kcm_kscreen X-KDE-ParentApp=kcontrol X-KDE-System-Settings-Parent-Category=display X-KDE-Weight=40 -Name=Displays +Name=Display Configuration Name[ca]=Pantalles Name[ca@valencia]=Pantalles Name[cs]=Obrazovky Name[da]=Skærme Name[de]=Anzeigen Name[el]=Οθόνες Name[en_GB]=Displays Name[es]=Pantallas Name[et]=Monitorid Name[eu]=Bistaratzaileak Name[fi]=Näytöt Name[fr]=Affichages Name[gl]=Pantallas Name[he]=תצוגות Name[hu]=Megjelenítők Name[ia]=Monstratores Name[id]=Displai Name[it]=Schermi Name[ko]=디스플레이 Name[lt]=Ekranai Name[nl]=Schermen Name[nn]=Skjermar Name[pl]=Ekrany Name[pt]=Ecrãs Name[pt_BR]=Telas Name[ru]=Экраны Name[sk]=Obrazovky Name[sl]=Zasloni Name[sr]=Екрани Name[sr@ijekavian]=Екрани Name[sr@ijekavianlatin]=Ekrani Name[sr@latin]=Ekrani Name[sv]=Bildskärmar Name[tr]=Ekranlar Name[uk]=Дисплеї Name[x-test]=xxDisplaysxx Name[zh_CN]=显示 Name[zh_TW]=顯示 Comment=Manage and configure monitors and displays Comment[ar]=أدر واضبط الشّاشات والعروض Comment[bs]=Upravljanje i konfiguracija monitora i ekrana Comment[ca]=Gestiona i configura els monitors i pantalles Comment[ca@valencia]=Gestiona i configura els monitors i pantalles Comment[cs]=Spravovat a nastavit monitory a zobrazení Comment[da]=Håndtér og indstil monitorer og skærme Comment[de]=Verwaltung und Einrichtung vom Monitoren und Anzeigen Comment[el]=Διαχείριση και διαμόρφωση οθονών και απεικονίσεων Comment[en_GB]=Manage and configure monitors and displays Comment[es]=Gestionar y configurar monitores y pantallas Comment[et]=Monitoride ja ekraanide haldamine ja seadistamine Comment[eu]=Kudeatu eta konfiguratu monitoreak eta bistaratzaileak Comment[fi]=Näyttöjen asetusten hallinta Comment[fr]=Gère et configure les moniteurs et les écrans Comment[gl]=Xestiona a configura os monitores e a resolución Comment[he]=נהל את הגדרות המסכים והתצוגות שלך Comment[hu]=Monitorok és kijelzők kezelése és beállítása Comment[ia]=Gere e configura monitors e monstratores Comment[id]=Kelola dan konfigurasi monitor dan displai Comment[it]=Gestisce e configura i monitor e gli schermi Comment[ko]=모니터와 디스플레이 설정 및 관리 Comment[lt]=Tvarkyti ir konfigūruoti monitorius ir ekranus Comment[nb]=Håndter og sett opp monitorer og skjermer Comment[nl]=Monitoren en schermen beheren en instellen Comment[nn]=Set opp skjermar og skjermbilete Comment[pa]=ਮਾਨੀਟਰ ਤੇ ਡਿਸਪਲੇਅ ਦਾ ਪਰਬੰਧ ਤੇ ਸੰਰਚਨਾ ਕਰੋ Comment[pl]=Zarządzanie i ustawienia ekranów Comment[pt]=Faz a gestão e configuração dos monitores e ecrãs Comment[pt_BR]=Gerencia e configura monitores e telas Comment[ro]=Gestionează și configurează monitoare și afișaje Comment[ru]=Настройка экранов и видеовыходов Comment[sk]=Správa a nastavenie monitorov a obrazoviek Comment[sl]=Upravljajte in nastavljajte zaslone in prikazovalnike Comment[sr]=Управљање и подешавање монитора̂ и екрана̂ Comment[sr@ijekavian]=Управљање и подешавање монитора̂ и екрана̂ Comment[sr@ijekavianlatin]=Upravljanje i podešavanje monitorâ̂ i ekranâ̂ Comment[sr@latin]=Upravljanje i podešavanje monitorâ̂ i ekranâ̂ Comment[sv]=Hantera och ställ in bildskärmar Comment[tr]=Ekranları ve görüntüleri yönet, yapılandır Comment[uk]=Керування і налаштовування моніторів і дисплеїв Comment[x-test]=xxManage and configure monitors and displaysxx Comment[zh_CN]=管理和配置显示和监视器 Comment[zh_TW]=管理與設定螢幕與顯示 X-KDE-Keywords=display,monitor,scale,scaling,resolution,orientation,outputs,screen,refresh X-KDE-Keywords[ca]=pantalla,monitor,escala,escalat,resolució,orientació,sortides,pantalla,refresc X-KDE-Keywords[ca@valencia]=pantalla,monitor,escala,escalat,resolució,orientació,eixides,pantalla,refresc X-KDE-Keywords[da]=skærm,monitor,skaler,opløsning,orientering,output,genopfriskning X-KDE-Keywords[de]=Anzeige,Monitor,Bildschirm,Skalierung,Auflösung,Ausrichtung,Ausgabe,Aktualisieren X-KDE-Keywords[el]=απεικόνιση,οθόνη,κλίμακα,κλιμάκωση,ανάλυση,προσανατολισμός,έξοδοι,οθόνη,ανανέωση X-KDE-Keywords[en_GB]=display,monitor,scale,scaling,resolution,orientation,outputs,screen,refresh X-KDE-Keywords[es]=pantalla,monitor,escala,escalado,resolución,orientación,salidas,refresco X-KDE-Keywords[eu]=bistaratzailea,monitorea,eskalatu,eskalatze,bereizmen,orientazioa,irteerak,pantaila,freskatu X-KDE-Keywords[fi]=näyttö,monitori,mittakaava,skaalaus,tarkkuus,suunta,ulostulot,virkistys X-KDE-Keywords[fr]=affichage,moniteur,échelle,mise à l'échelle,résolution,orientation,sorties,écran,rafraîchissement X-KDE-Keywords[gl]=display,pantalla,monitor,scale,cambiar de escala,scaling,cambio de escala,resolution,resolución,orientation,orientación,outputs,saída,saídas,pantalla,actualizar,actualización X-KDE-Keywords[hu]=megjelenítés,monitor,skála,skálázás,felbontás,tájolás,kimenetek,képernyő,frissítés X-KDE-Keywords[id]=displai,monitor,skala,penyekalaan,resolusi,orientasi,output,layar,segarkan X-KDE-Keywords[it]=schermo,monitor,scala,riscalatura,risoluzione,orientazione,uscite,schermo,aggiornamento X-KDE-Keywords[ko]=display,monitor,scale,scaling,resolution,orientation,outputs,screen,refresh,디스플레이,모니터,비율,배율,해상도,출력,방향,주사율,화면 X-KDE-Keywords[lt]=ekranas,vaizduoklis,monitorius,mastelis,skiriamoji geba,raiška,išvestis,atnaujinti,padėtis,orientacija,išvedimas X-KDE-Keywords[nl]=scherm,monitor,schaal,schalen,resolutie,oriëntatie,uitvoer,scherm,vernieuwen X-KDE-Keywords[nn]=skjermbilete,skjerm,skala,skalering,oppløysing,skjermoppløysing,retning,utgang,oppdatering,oppfrisking X-KDE-Keywords[pl]=wyświetlacz,monitor,skala,skalowanie,rozdzielczość,kierunek,orientacja,wyjścia,ekran,odświeżanie X-KDE-Keywords[pt]=ecrã,monitor,escala,resolução,orientação,saídas,monitor,actualização X-KDE-Keywords[pt_BR]=tela,monitor,escala,ajuste,resolução,orientação,saídas,atualizar X-KDE-Keywords[ru]=display,monitor,scale,scaling,resolution,orientation,outputs,экран,монитор,масштаб,разрешение,ориентация,выводы,частота кадров X-KDE-Keywords[sk]=displej,monitor,mierka,škálovanie,rozlíšenie,orientácia,výstupy,obrazovka,obnoviť X-KDE-Keywords[sv]=skärm,bildskärm,skala,skalning,upplösning,orientering,utgångar,uppdatera X-KDE-Keywords[uk]=display,monitor,scale,scaling,resolution,orientation,outputs,screen,refresh,дисплей,монітор,масштаб,масштабування,роздільність,здатність,орієнтація,виведення,виходи,екран,оновлення X-KDE-Keywords[x-test]=xxdisplayxx,xxmonitorxx,xxscalexx,xxscalingxx,xxresolutionxx,xxorientationxx,xxoutputsxx,xxscreenxx,xxrefreshxx X-KDE-Keywords[zh_CN]=display,monitor,scale,scaling,resolution,orientation,outputs,显示,监视器,缩放,分辨率,旋转,方位,输出,屏幕,刷新 X-KDE-Keywords[zh_TW]=display,monitor,scale,scaling,resolution,orientation,outputs,screen,refresh diff --git a/kcm/output_model.cpp b/kcm/output_model.cpp index c839e77..785ea75 100644 --- a/kcm/output_model.cpp +++ b/kcm/output_model.cpp @@ -1,869 +1,871 @@ /******************************************************************** Copyright © 2019 Roman Gilg This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . *********************************************************************/ #include "output_model.h" +#include "../common/utils.h" + #include "config_handler.h" #include #include OutputModel::OutputModel(ConfigHandler *configHandler) : QAbstractListModel(configHandler) , m_config(configHandler) { connect(this, &OutputModel::dataChanged, this, &OutputModel::changed); } int OutputModel::rowCount(const QModelIndex &parent) const { Q_UNUSED(parent) return m_outputs.count(); } QVariant OutputModel::data(const QModelIndex &index, int role) const { if (index.row() < 0 || index.row() >= m_outputs.count()) { return QVariant(); } const KScreen::OutputPtr &output = m_outputs[index.row()].ptr; switch (role) { case Qt::DisplayRole: - return output->name(); + return Utils::outputName(output); case EnabledRole: return output->isEnabled(); case PrimaryRole: return output->isPrimary(); case SizeRole: return output->geometry().size(); case PositionRole: return m_outputs[index.row()].pos; case NormalizedPositionRole: return output->geometry().topLeft(); case RotationRole: return output->rotation(); case ScaleRole: return output->scale(); case ResolutionIndexRole: return resolutionIndex(output); case ResolutionsRole: return resolutionsStrings(output); case RefreshRateIndexRole: return refreshRateIndex(output); case ReplicationSourceModelRole: return replicationSourceModel(output); case ReplicationSourceIndexRole: return replicationSourceIndex(index.row(), output->replicationSource()); case ReplicasModelRole: return replicasModel(output); case RefreshRatesRole: QVariantList ret; for (const auto rate : refreshRates(output)) { ret << i18n("%1 Hz", int(rate + 0.5)); } return ret; } return QVariant(); } bool OutputModel::setData(const QModelIndex &index, const QVariant &value, int role) { if (index.row() < 0 || index.row() >= m_outputs.count()) { return false; } const Output &output = m_outputs[index.row()]; switch (role) { case PositionRole: if (value.canConvert()) { QPoint val = value.toPoint(); if (output.pos == val) { return false; } snap(output, val); m_outputs[index.row()].pos = val; updatePositions(); Q_EMIT positionChanged(); Q_EMIT dataChanged(index, index, {role}); return true; } break; case EnabledRole: if (value.canConvert()) { bool enable = value.toBool(); if (output.ptr->isEnabled() == enable) { return false; } output.ptr->setEnabled(enable); reposition(); Q_EMIT dataChanged(index, index, {role}); return true; } break; case PrimaryRole: if (value.canConvert()) { bool primary = value.toBool(); if (output.ptr->isPrimary() == primary) { return false; } m_config->config()->setPrimaryOutput(output.ptr); Q_EMIT dataChanged(index, index, {role}); return true; } break; case ResolutionIndexRole: if (value.canConvert()) { return setResolution(index.row(), value.toInt()); } break; case RefreshRateIndexRole: if (value.canConvert()) { return setRefreshRate(index.row(), value.toInt()); } break; case RotationRole: if (value.canConvert()) { return setRotation(index.row(), value.value()); } break; case ReplicationSourceIndexRole: if (value.canConvert()) { return setReplicationSourceIndex(index.row(), value.toInt() - 1); } break; case ScaleRole: bool ok; const qreal scale = value.toReal(&ok); if (ok && !qFuzzyCompare(output.ptr->scale(), scale)) { output.ptr->setScale(scale); Q_EMIT sizeChanged(); Q_EMIT dataChanged(index, index, {role, SizeRole}); return true; } break; } return false; } QHash OutputModel::roleNames() const { QHash roles = QAbstractItemModel::roleNames(); roles[EnabledRole] = "enabled"; roles[PrimaryRole] = "primary"; roles[SizeRole] = "size"; roles[PositionRole] = "position"; roles[NormalizedPositionRole] = "normalizedPosition"; roles[RotationRole] = "rotation"; roles[ScaleRole] = "scale"; roles[ResolutionIndexRole] = "resolutionIndex"; roles[ResolutionsRole] = "resolutions"; roles[RefreshRateIndexRole] = "refreshRateIndex"; roles[RefreshRatesRole] = "refreshRates"; roles[ReplicationSourceModelRole] = "replicationSourceModel"; roles[ReplicationSourceIndexRole] = "replicationSourceIndex"; roles[ReplicasModelRole] = "replicasModel"; return roles; } void OutputModel::add(const KScreen::OutputPtr &output) { const int insertPos = m_outputs.count(); Q_EMIT beginInsertRows(QModelIndex(), insertPos, insertPos); int i = 0; while (i < m_outputs.size()) { const QPoint pos = m_outputs[i].ptr->pos(); if (output->pos().x() < pos.x()) { break; } if (output->pos().x() == pos.x() && output->pos().y() < pos.y()) { break; } i++; } // Set the initial non-normalized position to be the normalized // position plus the current delta. QPoint pos = output->pos(); if (!m_outputs.isEmpty()) { const QPoint delta = m_outputs[0].pos - m_outputs[0].ptr->pos(); pos = output->pos() + delta; } m_outputs.insert(i, Output(output, pos)); connect(output.data(), &KScreen::Output::isPrimaryChanged, this, [this, output](){ roleChanged(output->id(), {PrimaryRole}); }); Q_EMIT endInsertRows(); // Update replications. for (int j = 0; j < m_outputs.size(); j++) { if (i == j) { continue; } QModelIndex index = createIndex(j, 0); // Calling this directly ignores possible optimization when the // refresh rate hasn't changed in fact. But that's ok. Q_EMIT dataChanged(index, index, {ReplicationSourceModelRole, ReplicationSourceIndexRole}); } } void OutputModel::remove(int outputId) { auto it = std::find_if(m_outputs.begin(), m_outputs.end(), [outputId](const Output &output) { return output.ptr->id() == outputId; }); if (it != m_outputs.end()) { const int index = it - m_outputs.begin(); Q_EMIT beginRemoveRows(QModelIndex(), index, index); m_outputs.erase(it); Q_EMIT endRemoveRows(); } } inline bool refreshRateCompare(float rate1, float rate2) { return qAbs(rate1 - rate2) < 0.5; } bool OutputModel::setResolution(int outputIndex, int resIndex) { const Output &output = m_outputs[outputIndex]; const auto resolutionList = resolutions(output.ptr); if (resIndex < 0 || resIndex >= resolutionList.size()) { return false; } const QSize size = resolutionList[resIndex]; const float oldRefreshRate = output.ptr->currentMode()->refreshRate(); const auto modes = output.ptr->modes(); auto modeIt = std::find_if(modes.begin(), modes.end(), [size, oldRefreshRate](const KScreen::ModePtr &mode) { // TODO: we don't want to compare against old refresh rate if // refresh rate selection is auto. return mode->size() == size && refreshRateCompare(mode->refreshRate(), oldRefreshRate); }); if (modeIt == modes.end()) { // New resolution does not support previous refresh rate. // Get the highest one instead. float bestRefreshRate = 0; auto it = modes.begin(); while (it != modes.end()) { if ((*it)->size() == size && (*it)->refreshRate() > bestRefreshRate) { modeIt = it; } it++; } } Q_ASSERT(modeIt != modes.end()); const auto id = (*modeIt)->id(); if (output.ptr->currentModeId() == id) { return false; } output.ptr->setCurrentModeId(id); QModelIndex index = createIndex(outputIndex, 0); // Calling this directly ignores possible optimization when the // refresh rate hasn't changed in fact. But that's ok. Q_EMIT dataChanged(index, index, {ResolutionIndexRole, SizeRole, RefreshRateIndexRole}); Q_EMIT sizeChanged(); return true; } bool OutputModel::setRefreshRate(int outputIndex, int refIndex) { const Output &output = m_outputs[outputIndex]; const auto rates = refreshRates(output.ptr); if (refIndex < 0 || refIndex >= rates.size()) { return false; } const float refreshRate = rates[refIndex]; const auto modes = output.ptr->modes(); const auto oldMode = output.ptr->currentMode(); auto modeIt = std::find_if(modes.begin(), modes.end(), [oldMode, refreshRate](const KScreen::ModePtr &mode) { // TODO: we don't want to compare against old refresh rate if // refresh rate selection is auto. return mode->size() == oldMode->size() && refreshRateCompare(mode->refreshRate(), refreshRate); }); Q_ASSERT(modeIt != modes.end()); if (refreshRateCompare(oldMode->refreshRate(), (*modeIt)->refreshRate())) { // no change return false; } output.ptr->setCurrentModeId((*modeIt)->id()); QModelIndex index = createIndex(outputIndex, 0); Q_EMIT dataChanged(index, index, {RefreshRateIndexRole}); return true; } bool OutputModel::setRotation(int outputIndex, KScreen::Output::Rotation rotation) { const Output &output = m_outputs[outputIndex]; if (rotation != KScreen::Output::None && rotation != KScreen::Output::Left && rotation != KScreen::Output::Inverted && rotation != KScreen::Output::Right) { return false; } if (output.ptr->rotation() == rotation) { return false; } output.ptr->setRotation(rotation); QModelIndex index = createIndex(outputIndex, 0); Q_EMIT dataChanged(index, index, {RotationRole, SizeRole}); Q_EMIT sizeChanged(); return true; } int OutputModel::resolutionIndex(const KScreen::OutputPtr &output) const { if (!output->currentMode()) { return 0; } const auto sizes = resolutions(output); const QSize currentResolution = output->currentMode()->size(); const auto it = std::find_if(sizes.begin(), sizes.end(), [currentResolution](const QSize &size) { return size == currentResolution; }); if (it == sizes.end()) { return -1; } return it - sizes.begin(); } int OutputModel::refreshRateIndex(const KScreen::OutputPtr &output) const { if (!output->currentMode()) { return 0; } const auto rates = refreshRates(output); const float currentRate = output->currentMode()->refreshRate(); const auto it = std::find_if(rates.begin(), rates.end(), [currentRate](float rate) { return refreshRateCompare(rate, currentRate); }); if (it == rates.end()) { return 0; } return it - rates.begin(); } QVariantList OutputModel::resolutionsStrings(const KScreen::OutputPtr &output) const { QVariantList ret; for (const QSize &size : resolutions(output)) { const QString text = QString::number(size.width()) + QStringLiteral("x") + QString::number(size.height()); ret << text; } return ret; } QVector OutputModel::resolutions(const KScreen::OutputPtr &output) const { QVector hits; for (const auto &mode : output->modes()) { const QSize size = mode->size(); if (!hits.contains(size)) { hits << size; } } std::sort(hits.begin(), hits.end(), [](const QSize &a, const QSize &b) { if (a.width() > b.width()) { return true; } if (a.width() == b.width() && a.height() > b.height()) { return true; } return false; }); return hits; } QVector OutputModel::refreshRates(const KScreen::OutputPtr &output) const { QVector hits; if (!output->currentMode()) { return hits; } const auto baseSize = output->currentMode()->size(); for (const auto &mode : output->modes()) { if (mode->size() != baseSize) { continue; } const float rate = mode->refreshRate(); if (std::find_if(hits.begin(), hits.end(), [rate](float r) { return refreshRateCompare(r, rate); }) != hits.end()) { continue; } hits << rate; } return hits; } QStringList OutputModel::replicationSourceModel(const KScreen::OutputPtr &output) const { QStringList ret = { i18n("None") }; for (const auto &out : m_outputs) { if (out.ptr->id() != output->id()) { if (out.ptr->replicationSource() == output->id()) { // 'output' is already source for replication, can't be replica itself return { i18n("Replicated by other output") }; } if (out.ptr->replicationSource()) { // This 'out' is a replica. Can't be a replication source. continue; } ret.append(out.ptr->name()); } } return ret; } bool OutputModel::setReplicationSourceIndex(int outputIndex, int sourceIndex) { if (outputIndex <= sourceIndex) { sourceIndex++; } if (sourceIndex >= m_outputs.count()) { return false; } Output &output = m_outputs[outputIndex]; const int oldSourceId = output.ptr->replicationSource(); if (sourceIndex < 0) { if (oldSourceId == 0) { // no change return false; } output.ptr->setReplicationSource(0); if (output.replicaReset.isNull()) { // KCM was closed in between. for (const Output &out : m_outputs) { if (out.ptr->id() == output.ptr->id()) { continue; } if (out.ptr->geometry().right() > output.ptr->pos().x()) { output.ptr->setPos(out.ptr->geometry().topRight()); } } } else { output.ptr->setPos(output.ptr->pos() - output.replicaReset); } } else { const int sourceId = m_outputs[sourceIndex].ptr->id(); if (oldSourceId == sourceId) { // no change return false; } output.ptr->setReplicationSource(sourceId); output.replicaReset = m_outputs[sourceIndex].ptr->pos() - output.ptr->pos(); output.ptr->setPos(m_outputs[sourceIndex].ptr->pos()); } reposition(); QModelIndex index = createIndex(outputIndex, 0); Q_EMIT dataChanged(index, index, {ReplicationSourceIndexRole}); if (oldSourceId != 0) { auto it = std::find_if(m_outputs.begin(), m_outputs.end(), [oldSourceId](const Output &out) { return out.ptr->id() == oldSourceId; }); if (it != m_outputs.end()) { QModelIndex index = createIndex(it - m_outputs.begin(), 0); Q_EMIT dataChanged(index, index, {ReplicationSourceModelRole, ReplicasModelRole}); } } if (sourceIndex >= 0) { QModelIndex index = createIndex(sourceIndex, 0); Q_EMIT dataChanged(index, index, {ReplicationSourceModelRole, ReplicasModelRole}); } return true; } int OutputModel::replicationSourceIndex(int outputIndex, int sourceId) const { for (int i = 0; i < m_outputs.size(); i++) { const Output &output = m_outputs[i]; if (output.ptr->id() == sourceId) { return i + (outputIndex > i ? 1 : 0); } } return 0; } QVariantList OutputModel::replicasModel(const KScreen::OutputPtr &output) const { QVariantList ret; for (int i = 0; i < m_outputs.size(); i++) { const Output &out = m_outputs[i]; if (out.ptr->id() != output->id()) { if (out.ptr->replicationSource() == output->id()) { ret << i; } } } return ret; } void OutputModel::roleChanged(int outputId, OutputRoles role) { for (int i = 0; i < m_outputs.size(); i++) { Output &output = m_outputs[i]; if (output.ptr->id() == outputId) { QModelIndex index = createIndex(i, 0); Q_EMIT dataChanged(index, index, {role}); return; } } } bool OutputModel::positionable(const Output &output) const { return output.ptr->isPositionable(); } void OutputModel::reposition() { int x = 0; int y = 0; // Find first valid output. for (const auto &out : m_outputs) { if (positionable(out)) { x = out.ptr->pos().x(); y = out.ptr->pos().y(); break; } } for (int i = 0; i < m_outputs.size(); i++) { if (!positionable(m_outputs[i])) { continue; } const QPoint &cmp = m_outputs[i].ptr->pos(); if (cmp.x() < x) { x = cmp.x(); } if (cmp.y() < y) { y = cmp.y(); } } if (x == 0 && y == 0) { return; } for (int i = 0; i < m_outputs.size(); i++) { auto &out = m_outputs[i]; out.ptr->setPos(out.ptr->pos() - QPoint(x, y)); QModelIndex index = createIndex(i, 0); Q_EMIT dataChanged(index, index, {NormalizedPositionRole}); } m_config->normalizeScreen(); } QPoint OutputModel::originDelta() const { int x = 0; int y = 0; // Find first valid output. for (const auto &out : m_outputs) { if (positionable(out)) { x = out.pos.x(); y = out.pos.y(); break; } } for (int i = 1; i < m_outputs.size(); i++) { if (!positionable(m_outputs[i])) { continue; } const QPoint &cmp = m_outputs[i].pos; if (cmp.x() < x) { x = cmp.x(); } if (cmp.y() < y) { y = cmp.y(); } } return QPoint(x, y); } void OutputModel::updatePositions() { const QPoint delta = originDelta(); for (int i = 0; i < m_outputs.size(); i++) { const auto &out = m_outputs[i]; if (!positionable(out)) { continue; } const QPoint set = out.pos - delta; if (out.ptr->pos() != set) { out.ptr->setPos(set); QModelIndex index = createIndex(i, 0); Q_EMIT dataChanged(index, index, {NormalizedPositionRole}); } } updateOrder(); } void OutputModel::updateOrder() { auto order = m_outputs; std::sort(order.begin(), order.end(), [](const Output &a, const Output &b) { const int xDiff = b.ptr->pos().x() - a.ptr->pos().x(); const int yDiff = b.ptr->pos().y() - a.ptr->pos().y(); if (xDiff > 0) { return true; } if (xDiff == 0 && yDiff > 0) { return true; } return false; }); for (int i = 0; i < order.size(); i++) { for (int j = 0; j < m_outputs.size(); j++) { if (order[i].ptr->id() != m_outputs[j].ptr->id()) { continue; } if (i != j) { beginMoveRows(QModelIndex(), j, j, QModelIndex(), i); m_outputs.remove(j); m_outputs.insert(i, order[i]); endMoveRows(); } break; } } // TODO: Could this be optimized by only outputs updating where replica indices changed? for (int i = 0; i < m_outputs.size(); i++) { QModelIndex index = createIndex(i, 0); Q_EMIT dataChanged(index, index, { ReplicasModelRole }); } } bool OutputModel::normalizePositions() { bool changed = false; for (int i = 0; i < m_outputs.size(); i++) { auto &output = m_outputs[i]; if (output.pos == output.ptr->pos()) { continue; } if (!positionable(output)) { continue; } changed = true; auto index = createIndex(i, 0); output.pos = output.ptr->pos(); Q_EMIT dataChanged(index, index, {PositionRole}); } return changed; } bool OutputModel::positionsNormalized() const { // There might be slight deviations because of snapping. return originDelta().manhattanLength() < 5; } const int s_snapArea = 80; bool isVerticalClose(const QRect &rect1, const QRect &rect2) { if (rect2.top() - rect1.bottom() > s_snapArea ) { return false; } if (rect1.top() - rect2.bottom() > s_snapArea ) { return false; } return true; } bool snapToRight(const QRect &target, const QSize &size, QPoint &dest) { if (qAbs(target.right() - dest.x()) < s_snapArea) { // In snap zone for left to right snap. dest.setX(target.right() + 1); return true; } if (qAbs(target.right() - (dest.x() + size.width())) < s_snapArea) { // In snap zone for right to right snap. dest.setX(target.right() - size.width()); return true; } return false; } bool snapToLeft(const QRect &target, const QSize &size, QPoint &dest) { if (qAbs(target.left() - dest.x()) < s_snapArea) { // In snap zone for left to left snap. dest.setX(target.left()); return true; } if (qAbs(target.left() - (dest.x() + size.width())) < s_snapArea) { // In snap zone for right to left snap. dest.setX(target.left() - size.width()); return true; } return false; } bool snapToMiddle(const QRect &target, const QSize &size, QPoint &dest) { const int outputMid = dest.y() + size.height() / 2; const int targetMid = target.top() + target.height() / 2; if (qAbs(targetMid - outputMid) < s_snapArea) { // In snap zone for middle to middle snap. dest.setY(targetMid - size.height() / 2); return true; } return false; } bool snapToTop(const QRect &target, const QSize &size, QPoint &dest) { if (qAbs(target.top() - dest.y()) < s_snapArea) { // In snap zone for bottom to top snap. dest.setY(target.top()); return true; } if (qAbs(target.top() - (dest.y() + size.height())) < s_snapArea) { // In snap zone for top to top snap. dest.setY(target.top() - size.height()); return true; } return false; } bool snapToBottom(const QRect &target, const QSize &size, QPoint &dest) { if (qAbs(target.bottom() - dest.y()) < s_snapArea) { // In snap zone for top to bottom snap. dest.setY(target.bottom() + 1); return true; } if (qAbs(target.bottom() - (dest.y() + size.height())) < s_snapArea) { // In snap zone for bottom to bottom snap. dest.setY(target.bottom() - size.height() + 1); return true; } return false; } bool snapVertical(const QRect &target, const QSize &size, QPoint &dest) { if (snapToMiddle(target, size, dest)) { return true; } if (snapToBottom(target, size, dest)) { return true; } if (snapToTop(target, size, dest)) { return true; } return false; } void OutputModel::snap(const Output &output, QPoint &dest) { const QSize size = output.ptr->geometry().size(); for (const Output &out : m_outputs) { if (out.ptr->id() == output.ptr->id()) { // Can not snap to itself. continue; } if (!positionable(out)) { continue; } const QRect target(out.pos, out.ptr->geometry().size()); if (!isVerticalClose(target, QRect(dest, size))) { continue; } // try snap left to right first if (snapToRight(target, size, dest)) { snapVertical(target, size, dest); continue; } if (snapToLeft(target, size, dest)) { snapVertical(target, size, dest); continue; } if (snapVertical(target, size, dest)) { continue; } } } diff --git a/kcm/package/contents/ui/Output.qml b/kcm/package/contents/ui/Output.qml index 452eab8..7c36f1a 100644 --- a/kcm/package/contents/ui/Output.qml +++ b/kcm/package/contents/ui/Output.qml @@ -1,268 +1,269 @@ /******************************************************************** Copyright © 2019 Roman Gilg Copyright © 2012 Dan Vratil This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . *********************************************************************/ import QtQuick 2.12 import QtQuick.Layouts 1.1 import QtQuick.Controls 2.3 as Controls import QtGraphicalEffects 1.0 Rectangle { id: output property bool isSelected: root.selectedOutput === model.index onIsSelectedChanged: { if (isSelected) { z = 89; } else { z = 0; } } function getAbsolutePosition(pos) { return Qt.point((pos.x - screen.xOffset) * screen.relativeFactor, (pos.y - screen.yOffset) * screen.relativeFactor) ; } visible: model.enabled && model.replicationSourceIndex === 0 onVisibleChanged: screen.resetTotalSize() x: model.position.x / screen.relativeFactor + screen.xOffset y: model.position.y / screen.relativeFactor + screen.yOffset width: model.size.width / screen.relativeFactor height: model.size.height / screen.relativeFactor SystemPalette { id: palette } radius: 4 color: palette.window smooth: true clip: true border { color: isSelected ? palette.highlight : palette.shadow width: 1 Behavior on color { PropertyAnimation { duration: 150 } } } Item { anchors.fill: parent ColumnLayout { anchors.centerIn: parent spacing: units.smallSpacing width: parent.width Layout.maximumHeight: parent.height Controls.Label { Layout.fillWidth: true Layout.margins: units.smallSpacing text: model.display horizontalAlignment: Text.AlignHCenter elide: Text.ElideRight color: palette.text } Controls.Label { Layout.fillWidth: true Layout.margins: units.smallSpacing text: "(" + model.size.width + "x" + model.size.height + ")" horizontalAlignment: Text.AlignHCenter elide: Text.ElideRight color: palette.text } } } Rectangle { id: posLabel y: 4 x: 4 width: childrenRect.width + 5 height: childrenRect.height + 2 radius: 4 opacity: model.enabled && (tapHandler.isLongPressed || dragHandler.active) ? 0.9 : 0.0 color: palette.shadow Text { id: posLabelText y: 2 x: 2 text: model.normalizedPosition.x + "," + model.normalizedPosition.y color: "white" } Behavior on opacity { PropertyAnimation { duration: 100; } } } Controls.ToolButton { id: replicas property int selectedReplica: -1 height: output.height / 4 width: output.width / 5 anchors.top: output.top anchors.right: output.right anchors.margins: 5 visible: model.replicasModel.length > 0 icon.name: "osd-duplicate" Controls.ToolTip { text: i18n("Replicas") } onClicked: { var index = selectedReplica + 1; if (index >= model.replicasModel.length) { index = 0; } if (root.selectedOutput !== model.replicasModel[index]) { root.selectedOutput = model.replicasModel[index]; } } } Item { id: orientationPanelContainer anchors.fill: output visible: false Rectangle { id: orientationPanel anchors { left: parent.left right: parent.right bottom: parent.bottom } height: 10 color: isSelected ? palette.highlight : palette.shadow smooth: true Behavior on color { PropertyAnimation { duration: 150 } } } } states: [ State { name: "rot90" when: model.rotation === 2 PropertyChanges { target: orientationPanel height: undefined width: 10 } AnchorChanges { target: orientationPanel anchors.right: undefined anchors.top: orientationPanelContainer.top } }, State { name: "rot180" when: model.rotation === 4 AnchorChanges { target: orientationPanel anchors.top: orientationPanelContainer.top anchors.bottom: undefined } }, State { name: "rot270" when: model.rotation === 8 PropertyChanges { target: orientationPanel height: undefined width: 10 } AnchorChanges { target: orientationPanel anchors.left: undefined anchors.top: orientationPanelContainer.top } } ] OpacityMask { anchors.fill: orientationPanelContainer source: orientationPanelContainer maskSource: output } property point dragStartPosition TapHandler { id: tapHandler property bool isLongPressed: false function bindSelectedOutput() { root.selectedOutput = Qt.binding(function() { return model.index; }); } onPressedChanged: { if (pressed) { bindSelectedOutput(); dragStartPosition = Qt.point(output.x, output.y) } else { isLongPressed = false; } } onLongPressed: isLongPressed = true; longPressThreshold: 0.3 } DragHandler { id: dragHandler + enabled: kcm.outputModel && kcm.outputModel.rowCount() > 1 acceptedButtons: Qt.LeftButton target: null onTranslationChanged: { var newX = dragStartPosition.x + translation.x; var newY = dragStartPosition.y + translation.y; model.position = getAbsolutePosition(Qt.point(newX, newY)); } } } diff --git a/kcm/package/contents/ui/OutputIdentifier.qml b/kcm/package/contents/ui/OutputIdentifier.qml index 8a22118..21a16ae 100644 --- a/kcm/package/contents/ui/OutputIdentifier.qml +++ b/kcm/package/contents/ui/OutputIdentifier.qml @@ -1,57 +1,57 @@ /* Copyright (C) 2012 Dan Vratil This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ import QtQuick 2.1 import org.kde.plasma.core 2.0 as PlasmaCore import org.kde.plasma.components 2.0 as PlasmaComponents Rectangle { id: root; property string outputName; property string modeName; color: theme.backgroundColor border { - color: theme.textColor - width: Math.ceil(units.gridUnit / 20) + color: "red" + width: units.smallSpacing * 2 } width: childrenRect.width + 2 * childrenRect.x height: childrenRect.height + 2 * childrenRect.y PlasmaComponents.Label { id: displayName x: units.largeSpacing * 2 y: units.largeSpacing font.pointSize: theme.defaultFont.pointSize * 3 text: root.outputName; wrapMode: Text.WordWrap; horizontalAlignment: Text.AlignHCenter; } PlasmaComponents.Label { id: modeLabel; anchors { horizontalCenter: displayName.horizontalCenter top: displayName.bottom } text: root.modeName; horizontalAlignment: Text.AlignHCenter; } } diff --git a/kcm/package/contents/ui/OutputPanel.qml b/kcm/package/contents/ui/OutputPanel.qml index 275a8dd..7ee3bb2 100644 --- a/kcm/package/contents/ui/OutputPanel.qml +++ b/kcm/package/contents/ui/OutputPanel.qml @@ -1,118 +1,126 @@ /******************************************************************** Copyright © 2019 Roman Gilg This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . *********************************************************************/ import QtQuick 2.9 import QtQuick.Layouts 1.1 import QtQuick.Controls 2.3 as Controls import org.kde.kirigami 2.4 as Kirigami +import org.kde.kcm 1.2 as KCM + ColumnLayout { id: outputPanel property var element: model + Kirigami.Heading { + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + level: 2 + text: i18n("Settings for %1", element.display) + visible: kcm.outputModel.rowCount() > 1 + } + Kirigami.FormLayout { - Controls.Label { -// Kirigami.FormData.label: i18n("Name") - text: element.display - } + twinFormLayouts: globalSettingsLayout Controls.CheckBox { text: i18n("Enabled") checked: element.enabled onClicked: element.enabled = checked + visible: kcm.outputModel.rowCount() > 1 } Controls.CheckBox { text: i18n("Primary") checked: element.primary onClicked: element.primary = checked - visible: kcm.primaryOutputSupported + visible: kcm.primaryOutputSupported && kcm.outputModel.rowCount() > 1 } Controls.ComboBox { Kirigami.FormData.label: i18n("Resolution:") model: element.resolutions currentIndex: element.resolutionIndex !== undefined ? element.resolutionIndex : -1 onActivated: element.resolutionIndex = currentIndex } - ColumnLayout { + RowLayout { Layout.fillWidth: true visible: kcm.perOutputScaling Kirigami.FormData.label: i18n("Scale:") + Controls.Slider { id: scaleSlider Layout.fillWidth: true from: 0.5 to: 3 stepSize: 0.1 live: true value: element.scale onMoved: element.scale = value } Controls.Label { Layout.alignment: Qt.AlignHCenter - text: scaleSlider.value.toLocaleString(Qt.locale(), "f", 1) + text: i18nc("Scale factor (e.g. 1.0x, 1.5x, 2.0x)","%1x", scaleSlider.value.toLocaleString(Qt.locale(), "f", 1)) } } Controls.ButtonGroup { buttons: orientation.children } RowLayout { id: orientation Kirigami.FormData.label: i18n("Orientation:") - Layout.fillWidth: true RotationButton { value: 0 } RotationButton { value: 90 } RotationButton { value: 180 } RotationButton { value: 270 } } Controls.ComboBox { Kirigami.FormData.label: i18n("Refresh rate:") model: element.refreshRates currentIndex: element.refreshRateIndex ? element.refreshRateIndex : 0 onActivated: element.refreshRateIndex = currentIndex } Controls.ComboBox { Kirigami.FormData.label: i18n("Replica of:") model: element.replicationSourceModel - visible: kcm.outputReplicationSupported + visible: kcm.outputReplicationSupported && kcm.outputModel && kcm.outputModel.rowCount() > 1 onModelChanged: enabled = (count > 1); onCountChanged: enabled = (count > 1); currentIndex: element.replicationSourceIndex onActivated: element.replicationSourceIndex = currentIndex } } } diff --git a/kcm/package/contents/ui/Panel.qml b/kcm/package/contents/ui/Panel.qml index 2f3c486..3b66464 100644 --- a/kcm/package/contents/ui/Panel.qml +++ b/kcm/package/contents/ui/Panel.qml @@ -1,128 +1,109 @@ /******************************************************************** Copyright © 2019 Roman Gilg This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . *********************************************************************/ import QtQuick 2.9 import QtQuick.Layouts 1.1 import QtQuick.Controls 2.3 as Controls import org.kde.plasma.extras 2.0 as PlasmaExtras import org.kde.kirigami 2.4 as Kirigami import org.kde.private.kcm.kscreen 1.0 as KScreen ColumnLayout { - Kirigami.FormLayout { - twinFormLayouts: globalSettingsLayout - Item { - Kirigami.FormData.isSection: true - Kirigami.FormData.label: i18n("Output settings") - - Rectangle { - anchors.fill: parent - opacity: 0.5 - } - } - } - Controls.SwipeView { id: panelView currentIndex: root.selectedOutput onCurrentIndexChanged: root.selectedOutput = Qt.binding(function() { return currentIndex; }); Layout.fillWidth: true Repeater { model: kcm.outputModel OutputPanel {} } } Controls.PageIndicator { id: indicator Layout.alignment: Qt.AlignHCenter - opacity: count > 1 ? 1 : 0 + visible: count > 1 count: panelView.count currentIndex: panelView.currentIndex interactive: true onCurrentIndexChanged: root.selectedOutput = currentIndex } Kirigami.FormLayout { id: globalSettingsLayout Layout.fillWidth: true - Layout.topMargin: 20 + Kirigami.Separator { Layout.fillWidth: true Kirigami.FormData.isSection: true } - Item { - Layout.fillWidth: true - Kirigami.FormData.isSection: true - Kirigami.FormData.label: i18n("Arrangement settings") - } - ColumnLayout { + RowLayout { Layout.fillWidth: true Kirigami.FormData.label: i18n("Global scale:") visible: !kcm.perOutputScaling Controls.Slider { id: globalScaleSlider Layout.fillWidth: true from: 1 to: 3 stepSize: 0.1 live: true value: kcm.globalScale onMoved: kcm.globalScale = value } Controls.Label { - Layout.alignment: Qt.AlignHCenter - text: globalScaleSlider.value.toLocaleString(Qt.locale(), "f", 1) + text: i18nc("Scale factor (e.g. 1.0x, 1.5x, 2.0x)","%1x", globalScaleSlider.value.toLocaleString(Qt.locale(), "f", 1)) } } Controls.ButtonGroup { buttons: retentionSelector.children } ColumnLayout { id: retentionSelector - Kirigami.FormData.label: i18n("Save values of an output:") + Kirigami.FormData.label: i18n("Save displays' properties:") Kirigami.FormData.buddyFor: globalRetentionRadio spacing: Kirigami.Units.smallSpacing Controls.RadioButton { id: globalRetentionRadio - text: i18n("For this and other combination of outputs") + text: i18n("For any display arrangement") checked: !individualRetentionRadio.checked onClicked: kcm.outputRetention = KScreen.Control.Global } Controls.RadioButton { id: individualRetentionRadio - text: i18n("For this specific setup independently of others") + text: i18n("For only this specific display arrangement") checked: kcm.outputRetention === KScreen.Control.Individual onClicked: kcm.outputRetention = KScreen.Control.Individual } } } } diff --git a/kcm/package/contents/ui/Screen.qml b/kcm/package/contents/ui/Screen.qml index 25a7edf..3ac369e 100644 --- a/kcm/package/contents/ui/Screen.qml +++ b/kcm/package/contents/ui/Screen.qml @@ -1,78 +1,86 @@ /******************************************************************** Copyright © 2019 Roman Gilg This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . *********************************************************************/ import QtQuick 2.9 import QtQuick.Layouts 1.1 import QtQuick.Controls 2.3 as Controls +import org.kde.kirigami 2.5 as Kirigami Rectangle { property var outputs property size totalSize function resetTotalSize() { totalSize = kcm.normalizeScreen(); } onWidthChanged: resetTotalSize() onHeightChanged: resetTotalSize() property real relativeFactor: { - var relativeSize = Qt.size(totalSize.width / (0.6 * width), - totalSize.height / (0.6 * height)); + var relativeSize = Qt.size(totalSize.width / (0.35 * width), + totalSize.height / (0.35 * height)); if (relativeSize.width > relativeSize.height) { // Available width smaller than height, optimize for width (we have // '>' because the available width, height is in the denominator). return relativeSize.width; } else { return relativeSize.height; } } property int xOffset: (width - totalSize.width / relativeFactor) / 2; property int yOffset: (height - totalSize.height / relativeFactor) / 2; - implicitHeight: root.height * 0.45 + implicitHeight: Math.max(root.height * 0.4, units.gridUnit * 13) + radius: units.smallSpacing color: "white" + border.color: Qt.tint(Kirigami.Theme.textColor, Qt.rgba(Kirigami.Theme.backgroundColor.r, Kirigami.Theme.backgroundColor.g, Kirigami.Theme.backgroundColor.b, 0.8)) + border.width: 1 clip: true Row { z: 90 anchors { bottom: parent.bottom horizontalCenter: parent.horizontalCenter margins: units.smallSpacing } spacing: units.smallSpacing Controls.Button { onClicked: kcm.identifyOutputs() text: i18n("Identify") + icon.name: "documentinfo" focusPolicy: Qt.NoFocus + visible: kcm.outputModel && kcm.outputModel.rowCount() > 1 } Controls.Button { enabled: !kcm.screenNormalized onClicked: resetTotalSize() - text: i18n("Center view") + text: i18n("Center View") + icon.name: "zoom-original" focusPolicy: Qt.NoFocus + visible: kcm.outputModel && kcm.outputModel.rowCount() > 1 } } Repeater { model: kcm.outputModel delegate: Output {} onCountChanged: resetTotalSize() } } diff --git a/kcm/package/contents/ui/main.qml b/kcm/package/contents/ui/main.qml index caa98f7..2649b1a 100644 --- a/kcm/package/contents/ui/main.qml +++ b/kcm/package/contents/ui/main.qml @@ -1,128 +1,132 @@ /******************************************************************** Copyright © 2019 Roman Gilg This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . *********************************************************************/ import QtQuick 2.9 import QtQuick.Layouts 1.1 import QtQuick.Controls 2.3 as Controls import org.kde.kirigami 2.4 as Kirigami import org.kde.kcm 1.2 as KCM KCM.SimpleKCM { id: root - implicitWidth: units.gridUnit * 40 - implicitHeight: units.gridUnit * 50 + implicitWidth: units.gridUnit * 30 + implicitHeight: units.gridUnit * 38 property int selectedOutput: 0 ColumnLayout { Kirigami.InlineMessage { // Note1: There is an implicit height binding loop error on // first invokation. Seems to be an issue in Kirigami. // Note2: This should maybe go in header component of the KCM, // but there seems to be another issue in Kirigami then // being always hidden. Compare Night Color KCM with // the same issue. id: dangerousSaveMsg Layout.fillWidth: true type: Kirigami.MessageType.Warning text: i18n("Are you sure you want to disable all outputs? " + "This might render the device unusable.") showCloseButton: true actions: [ Kirigami.Action { iconName: "dialog-ok" text: i18n("Disable all outputs") onTriggered: { dangerousSaveMsg.visible = false; kcm.forceSave(); } } ] } Kirigami.InlineMessage { id: errBackendMsg Layout.fillWidth: true type: Kirigami.MessageType.Error text: i18n("No KScreen backend found. Please check your KScreen installation.") visible: false showCloseButton: false } Kirigami.InlineMessage { id: errSaveMsg Layout.fillWidth: true type: Kirigami.MessageType.Error text: i18n("Outputs could not be saved due to error.") visible: false showCloseButton: true } Kirigami.InlineMessage { id: scaleMsg Layout.fillWidth: true type: Kirigami.MessageType.Positive text: i18n("New global scale applied. " + "Change will come into effect after restart.") visible: false showCloseButton: true } Kirigami.InlineMessage { id: connectMsg Layout.fillWidth: true type: Kirigami.MessageType.Information visible: false showCloseButton: true } Connections { target: kcm onDangerousSave: dangerousSaveMsg.visible = true; onErrorOnSave: errSaveMsg.visible = true; onGlobalScaleWritten: scaleMsg.visible = true; onOutputConnect: { if (connected) { connectMsg.text = i18n("A new output has been added. " + " Settings have been reloaded."); } else { connectMsg.text = i18n("An output has been removed. " + " Settings have been reloaded."); } connectMsg.visible = true; } onBackendErrorChanged: errBackendMsg.visible = kcm.backendError onChanged: { dangerousSaveMsg.visible = false; errSaveMsg.visible = false; scaleMsg.visible = false; } } Screen { id: screen - Layout.fillWidth: true + + Layout.alignment: Qt.AlignHCenter + Layout.preferredWidth: Math.max(root.width * 0.8, units.gridUnit * 26) + Layout.topMargin: Kirigami.Units.smallSpacing + Layout.bottomMargin: Kirigami.Units.largeSpacing * 2 enabled: kcm.outputModel && !kcm.backendError outputs: kcm.outputModel } Panel { enabled: kcm.outputModel && !kcm.backendError Layout.fillWidth: true } } }