Something went wrong. Try again.
Bring Windows XP themes to your KDE Plasma environment!
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797// SPDX-License-Identifier: GPL-3.0-or-later#include "app/apply.h"
#include "app/colors.h"#include "app/controls.h"#include "app/decoration.h"#include "app/forks.h"#include "app/gtk.h"#include "app/icons.h"#include "app/overrides.h"#include "app/panel.h"#include "app/pointers.h"#include "app/project.h"#include "app/paths.h"#include "app/qttarget.h"#include "app/sounds.h"#include "app/startmenu.h"#include "app/state.h"#include "app/xptext.h"#include "core/schema.h"
#include <QCoreApplication>#include <QDir>#include <QDirIterator>#include <QElapsedTimer>#include <QFile>#include <QFileInfo>#include <QStandardPaths>#include <QJsonArray>#include <QJsonDocument>#include <QJsonObject>#include <QMap>#include <QRegularExpression>#include <QSaveFile>#include <QSet>#include <QThread>#include <QUrl>
#include <cstdio>#include <fcntl.h>
namespace xpl {
namespace {
bool fail(QString *error, const QString &message){ if (error) *error = message; return false;}
QString share(const QString &relative){ return paths::localShare() + QLatin1Char('/') + relative;}
// A package directory built under `from` moved into place at `to`.// Copied in beside the old one, then swapped for it in one step: whatever// is watching (every app, for an icon theme in use) never finds it missing// or half there.bool install(const QString &from, const QString &to, QString *error){ const QString fresh = to + QLatin1String(".xplasma-new"); QDir(fresh).removeRecursively(); QDir().mkpath(fresh); QDirIterator it(from, QDir::Files, QDirIterator::Subdirectories); while (it.hasNext()) { const QString file = it.next(); const QString target = fresh + file.mid(from.size()); QDir().mkpath(QFileInfo(target).absolutePath()); if (!QFile::copy(file, target)) { QDir(fresh).removeRecursively(); return fail(error, QStringLiteral("can't install %1").arg(to + file.mid(from.size()))); } } const QByteArray a = QFile::encodeName(fresh), b = QFile::encodeName(to); if (QFileInfo(to).isDir() && ::renameat2(AT_FDCWD, a.constData(), AT_FDCWD, b.constData(), RENAME_EXCHANGE) == 0) { QDir(fresh).removeRecursively(); // (the old one, now) return true; } QDir(to).removeRecursively(); // (none there, or a filesystem that can't swap) if (!QDir().rename(fresh, to)) { QDir(fresh).removeRecursively(); return fail(error, QStringLiteral("can't install %1").arg(to)); } return true;}
bool ours(const QString &metadata){ QFile f(metadata); if (!f.open(QIODevice::ReadOnly)) return false; const QByteArray text = f.readAll().toLower(); return text.contains("xplasma") || text.contains("xp2plasma"); // (X-Xp2plasma-Icon-Theme, too)}
QString resourceText(const QString &path){ QFile f(path); return f.open(QIODevice::ReadOnly) ? QString::fromUtf8(f.readAll()) : QString();}
void report(const ApplyOptions &o, const QString &line){ if (o.progress) o.progress(line);}
// KWin: Aurorae, with the look's theme. (Aurorae keeps one QML renderer for// every SVG theme, with its shadow palette cached: switching straight from// one of ours to another kept the old one's, so it goes by Breeze first.)void applyDecoration(Session &session, const QString &id){ const QStringList group{QStringLiteral("org.kde.kdecoration2")}; if (session.readConfig(QStringLiteral("kwinrc"), group, QStringLiteral("theme")).startsWith(QLatin1String("__aurorae__svg__"))) { session.writeConfig(QStringLiteral("kwinrc"), group, QStringLiteral("library"), QStringLiteral("org.kde.breeze")); session.writeConfig(QStringLiteral("kwinrc"), group, QStringLiteral("theme"), QStringLiteral("Breeze")); // Long enough for KWin to drop every Aurorae border: only then does // it let go of its QML engine, and so of the aurorae.qml it compiled // (a new title-shadow override isn't read otherwise). 500 ms wasn't. session.reconfigureKWin(); QThread::msleep(2000); } session.writeConfig(QStringLiteral("kwinrc"), group, QStringLiteral("library"), QStringLiteral("org.kde.kwin.aurorae")); session.writeConfig(QStringLiteral("kwinrc"), group, QStringLiteral("theme"), QStringLiteral("__aurorae__svg__") + id); session.reconfigureKWin(); // Until KWin says it has it: what comes next (the shell's restart, a // look's colours applied again) mustn't catch it half-way -- once, on a // fresh install's first apply, windows came out with their titles but // without borders or buttons (KWin: an Aurorae theme with no SVG), and // applying again mended them. const QString theme = QStringLiteral("Theme: __aurorae__svg__") + id; QElapsedTimer clock; clock.start(); while (clock.elapsed() < 10000) { const auto info = session.query(QStringLiteral("org.kde.KWin"), QStringLiteral("/KWin"), QStringLiteral("org.kde.KWin"), QStringLiteral("supportInformation")); if (!info) return; // (no KWin to ask) const QString text = info->toString(); const qsizetype at = text.indexOf(theme); if (at >= 0 && (at + theme.size() == text.size() || text.at(at + theme.size()) == QLatin1Char('\n'))) { QThread::msleep(500); // (and its windows' borders made anew) return; } QThread::msleep(200); }}
// KWin rules for XPlasma's own windows that XP gave no minimize or maximize// box: the copy dialog (plasmashell's, titled "Copying..." and the like) and// the editor (XPlasma Properties and its dialogs, and Date and Time// Properties, property sheets on XP).// Wayland has no way for a window to say so itself. With neither box, the// decoration shows Close alone, as XP does. Other rules are left as they are.const QString kRulesFile = QStringLiteral("kwinrulesrc");using Rule = QList<std::pair<QString, QString>>;const Rule kNoBoxes{ {QStringLiteral("types"), QStringLiteral("33")}, // normal, dialog {QStringLiteral("minimize"), QStringLiteral("false")}, {QStringLiteral("minimizerule"), QStringLiteral("2")}, // force {QStringLiteral("maximizehoriz"), QStringLiteral("false")}, {QStringLiteral("maximizehorizrule"), QStringLiteral("2")}, {QStringLiteral("maximizevert"), QStringLiteral("false")}, {QStringLiteral("maximizevertrule"), QStringLiteral("2")},};const QList<std::pair<QString, Rule>> kRules{ {QStringLiteral("xplasma-copy-dialog"), Rule{{QStringLiteral("Description"), QStringLiteral("XPlasma: XP's copy dialog (Close only)")}, {QStringLiteral("wmclass"), QStringLiteral("org.kde.plasmashell")}, {QStringLiteral("wmclassmatch"), QStringLiteral("1")}, // exact {QStringLiteral("title"), QStringLiteral("\\.\\.\\.$")}, {QStringLiteral("titlematch"), QStringLiteral("3")}} // regular expression + kNoBoxes}, {QStringLiteral("xplasma-display-properties"), Rule{{QStringLiteral("Description"), QStringLiteral("XPlasma: XPlasma Properties, Date and Time Properties (Close only)")}, {QStringLiteral("wmclass"), QStringLiteral("^fyi\\.hotsocket\\.xplasma(\\.datetime)?$")}, {QStringLiteral("wmclassmatch"), QStringLiteral("3")}} // regular expression + kNoBoxes},};
QStringList ruleIds(Session &session){ const QString list = session.readConfig(kRulesFile, {QStringLiteral("General")}, QStringLiteral("rules")); return list.isEmpty() ? QStringList() : list.split(QLatin1Char(','));}
void setRuleIds(Session &session, const QStringList &ids){ session.writeConfig(kRulesFile, {QStringLiteral("General")}, QStringLiteral("rules"), ids.join(QLatin1Char(',')), false); session.writeConfig(kRulesFile, {QStringLiteral("General")}, QStringLiteral("count"), QString::number(ids.size()), false);}
void installWindowRules(Session &session){ QStringList ids = ruleIds(session); for (const auto &[id, rule] : kRules) { for (const auto &[key, value] : rule) session.writeConfig(kRulesFile, {id}, key, value, false); if (!ids.contains(id)) ids << id; } setRuleIds(session, ids);}
void removeWindowRules(Session &session){ QStringList ids = ruleIds(session); for (const auto &[id, rule] : kRules) { ids.removeAll(id); for (const auto &[key, value] : rule) session.deleteConfig(kRulesFile, {id}, key, false); } setRuleIds(session, ids);}
// The fonts, and the running session told: the platform theme only re-reads// them on refreshFonts, the signal the Fonts settings send.void applyFonts(Session &session, const QList<ConfigValue> &fonts){ for (const ConfigValue &v : fonts) session.writeConfig(v.file, v.group.split(QLatin1Char('/')), v.key, v.value); session.signal(QStringLiteral("/KDEPlatformTheme"), QStringLiteral("org.kde.KDEPlatformTheme"), QStringLiteral("refreshFonts")); session.reconfigureKWin();}
// This XPlasma's editor (XPlasma Properties, Date and Time Properties),// beside this program; or whichever is on the PATH.QString editorPath(){ const QString beside = QCoreApplication::applicationDirPath() + QLatin1String("/xplasma-edit"); return QFileInfo(beside).isExecutable() ? beside : QStringLiteral("xplasma-edit");}
// Plasma's helper that switches the icon theme: in its libexec, wherever the// distribution keeps that -- found beside Plasma's own tools first.QString iconHelper(){ QStringList candidates; const QString tool = QStandardPaths::findExecutable(QStringLiteral("plasma-apply-colorscheme")); if (!tool.isEmpty()) { const QDir prefix(QFileInfo(tool).canonicalPath() + QLatin1String("/..")); candidates << prefix.filePath(QStringLiteral("libexec/plasma-changeicons")) << prefix.filePath(QStringLiteral("lib/libexec/plasma-changeicons")) << prefix.filePath(QStringLiteral("lib64/libexec/plasma-changeicons")); // (Debian's multiarch: lib/x86_64-linux-gnu/libexec) for (const QString &triplet : QDir(prefix.filePath(QStringLiteral("lib"))).entryList({QStringLiteral("*-linux-*")}, QDir::Dirs)) candidates << prefix.filePath(QStringLiteral("lib/%1/libexec/plasma-changeicons").arg(triplet)); } candidates << QStringLiteral("/usr/lib/plasma-changeicons") << QStringLiteral("/usr/lib64/plasma-changeicons") << QStringLiteral("/usr/libexec/plasma-changeicons"); for (const QString &p : std::as_const(candidates)) if (QFileInfo(p).isExecutable()) return p; return QString();}
} // namespace
// The system's applications, as desktop file ids (a subfolder's joined on// with "-", as the spec has it) to their files -- the first found, the// user's own before the system's.QVariantMap desktopFiles(){ QVariantMap files; for (const QString &root : QStandardPaths::standardLocations(QStandardPaths::ApplicationsLocation)) { QDirIterator it(root, {QStringLiteral("*.desktop")}, QDir::Files, QDirIterator::Subdirectories); while (it.hasNext()) { const QString path = it.next(); const QString id = path.mid(root.size() + 1).replace(QLatin1Char('/'), QLatin1Char('-')); if (!files.contains(id)) files.insert(id, path); } } return files;}
QString scriptFrom(const QString &name, const QVariantMap &values){ QString text = resourceText(QStringLiteral(":/scripts/") + name + QLatin1String(".js")); for (auto it = values.begin(); it != values.end(); ++it) { const QJsonValue v = QJsonValue::fromVariant(it.value()); QString json; if (v.isObject()) json = QString::fromUtf8(QJsonDocument(v.toObject()).toJson(QJsonDocument::Compact)); else if (v.isArray()) json = QString::fromUtf8(QJsonDocument(v.toArray()).toJson(QJsonDocument::Compact)); else // (a scalar, as JSON: wrap it in an array and take it out again) json = QString::fromUtf8(QJsonDocument(QJsonArray{v}).toJson(QJsonDocument::Compact)).mid(1).chopped(1); text.replace(QLatin1String("${") + it.key() + QLatin1Char('}'), json); } return text;}
QStringList generatedPackages(const QStringList &keep){ QStringList found; const struct { QString dir; QStringList meta; } kinds[] = { {share(QStringLiteral("aurorae/themes")), {QStringLiteral("metadata.json"), QStringLiteral("metadata.desktop")}}, {share(QStringLiteral("plasma/desktoptheme")), {QStringLiteral("metadata.json"), QStringLiteral("metadata.desktop")}}, {share(QStringLiteral("icons")), {QStringLiteral("xplasma-icons.json"), QStringLiteral("xp2plasma-icons.json")}}, }; for (const auto &k : kinds) for (const QFileInfo &pkg : QDir(k.dir).entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot)) { if (keep.contains(pkg.absoluteFilePath()) || pkg.isSymLink()) continue; for (const QString &m : k.meta) if (ours(pkg.filePath() + QLatin1Char('/') + m)) { found << pkg.absoluteFilePath(); break; } } for (const QFileInfo &f : QDir(share(QStringLiteral("color-schemes"))).entryInfoList({QStringLiteral("*.colors")}, QDir::Files)) if (!keep.contains(f.absoluteFilePath()) && ours(f.absoluteFilePath())) found << f.absoluteFilePath(); return found;}
// KSvg's elements cache (~/.cache/ksvg-elements) started with the size// variants ("16-16-shadow-center") of the theme's SVGs in `widgets`: KSvg// learns them only as it parses an SVG, after the first process to draw one// has already chosen what to draw -- and after an apply that process is the// shell (the check box's box, button.svg's 16px shadow, came up without it).// Each SVG's group carries its file's time, as KSvg keeps it.static void primeSizeHints(const QString &widgets, const QString &cacheFile){ static const QRegularExpression element(QStringLiteral("<image id=\"([^\"]+)\"[^>]*? width=\"(\\d+)\" height=\"(\\d+)\"")); static const QRegularExpression variant(QStringLiteral("^(\\d+)-(\\d+)-(.+)$")); QString out; for (const QFileInfo &svg : QDir(widgets).entryInfoList({QStringLiteral("*.svg")}, QDir::Files, QDir::Name)) { QFile f(svg.absoluteFilePath()); if (!f.open(QIODevice::ReadOnly)) continue; const QString text = QString::fromUtf8(f.readAll()); QMap<QString, QString> sizes; // element -> "WxH," for it and each variant QSet<QString> varied; for (auto it = element.globalMatch(text); it.hasNext();) { const auto m = it.next(); QString id = m.captured(1); if (const auto v = variant.match(id); v.hasMatch()) { id = v.captured(3); varied << id; } sizes[id] += m.captured(2) + QLatin1Char('x') + m.captured(3) + QLatin1Char(','); } if (varied.isEmpty()) continue; out += QLatin1Char('[') + svg.absoluteFilePath() + QStringLiteral("]\nLastModified=") + QString::number(svg.lastModified().toSecsSinceEpoch()) + QLatin1Char('\n'); for (const QString &id : std::as_const(varied)) out += id + QLatin1Char('=') + sizes.value(id) + QLatin1Char('\n'); out += QLatin1Char('\n'); } if (out.isEmpty()) return; QSaveFile f(cacheFile); if (f.open(QIODevice::WriteOnly) && f.write(out.toUtf8()) >= 0) f.commit(); // (a cache: failing to prime it isn't failing)}
bool applyStartButton(Session &session, const Look &look, QString *error){ const StartButtonExtras extras = StartButtonExtras::fromJson(State::load().settings.value(QLatin1String(kStartButtonKey)).toObject()); const QString startDir = paths::data() + QLatin1String("/start/") + look.id; QDir(startDir).removeRecursively(); const QJsonObject theme = writeStartTheme(look, startDir, error, extras); if (theme.isEmpty()) return false; session.script(scriptFrom(QStringLiteral("start-theme"), {{QStringLiteral("START"), QLatin1String(kStartPlasmoidId)}, {QStringLiteral("THEME"), theme.toVariantMap()}})); return true;}
bool applyWidgets(Session &session, const Look &look, const std::function<void(const QString &)> &progress, QString *error){ State state = State::load(); QStringList log; const bool ok = qtstyle::apply(session, look, state, &log, error); for (const QString &line : std::as_const(log)) if (progress) progress(QStringLiteral(" ") + line); state.save(); return ok;}
// "[A][B C][D]" -> {"A", "B C", "D"}static QStringList configGroups(const QString &header){ return header.trimmed().mid(1).chopped(1).split(QLatin1String("]["));}
void settleTaskbars(Session &session, int height){ auto lines = [](const QString &name) { QFile f(paths::config() + QLatin1Char('/') + name); return f.open(QIODevice::ReadOnly) ? QString::fromUtf8(f.readAll()).split(QLatin1Char('\n')) : QStringList(); }; // The bottom panels (location 4), from the layout. QSet<QString> bottom; QString containment; bool panel = false, atBottom = false; auto finish = [&] { if (!containment.isEmpty() && panel && atBottom) bottom.insert(containment); }; for (const QString &line : lines(QStringLiteral("plasma-org.kde.plasma.desktop-appletsrc"))) { if (line.startsWith(QLatin1Char('['))) { finish(); const QStringList g = configGroups(line); containment = g.size() == 2 && g.at(0) == QLatin1String("Containments") ? g.at(1) : QString(); panel = atBottom = false; } else if (!containment.isEmpty()) { panel |= line.trimmed() == QLatin1String("plugin=org.kde.panel"); atBottom |= line.trimmed() == QLatin1String("location=4"); } } finish(); // Their views, as the shell keeps them: per panel, its defaults, and per // screen size (HorizontalNNNN). const QString file = QStringLiteral("plasmashellrc"); QSet<QString> seen; for (const QString &line : lines(file)) { if (!line.startsWith(QLatin1String("[PlasmaViews][Panel "))) continue; const QStringList g = configGroups(line); if (g.size() < 2 || !bottom.contains(g.at(1).mid(6)) || seen.contains(g.join(QLatin1Char('/')))) continue; seen.insert(g.join(QLatin1Char('/'))); const QString sub = g.value(2); if (sub == QLatin1String("Defaults")) { session.writeConfig(file, g, QStringLiteral("thickness"), QString::number(height), false); continue; } if (!sub.isEmpty() && !sub.startsWith(QLatin1String("Horizontal"))) continue; session.writeConfig(file, g, QStringLiteral("floating"), QStringLiteral("0"), false); session.writeConfig(file, g, QStringLiteral("alignment"), QStringLiteral("1"), false); // Qt::AlignLeft if (!sub.isEmpty()) { session.writeConfig(file, g, QStringLiteral("panelLengthMode"), QStringLiteral("0"), false); // fill session.writeConfig(file, g, QStringLiteral("panelOpacity"), QStringLiteral("1"), false); // opaque } }}
QStringList cannotTake(Session &session, bool taskbar, std::optional<forks::Choice> *chosen){ // (Widgets this Plasma builds in come from XPlasma's builds of them, if // one for this Plasma loads here.) QStringList cannot; QString why; if (taskbar && !overrides::checkPanelWidgets(share(QStringLiteral("plasma/plasmoids")), &why)) cannot << why.split(QLatin1Char('\n')); if (const QStringList builtIn = overrides::builtInPanelWidgets(); taskbar && !builtIn.isEmpty()) { const auto builds = forks::choose(forks::runningPlasma(session), builtIn, &why); if (!builds) cannot << why; if (chosen) *chosen = builds; } if (!overrides::checkDesktopMenu(share(QStringLiteral("plasma/plasmoids")), &why)) cannot << why.split(QLatin1Char('\n')); if (!overrides::checkTitleShadows(share(QStringLiteral("kwin/aurorae/aurorae.qml")), &why)) cannot << why; return cannot;}
bool lookOutdated(const State &state){ return !state.appliedKind.isEmpty() && state.settings.value(QLatin1String(kAppliedWithKey)).toInt() < project::kVersion;}
bool applyLook(Session &session, const Look &look, const ApplyOptions &o, QString *error){ // The whole XP look or none of it: what this Plasma can't take is known // before anything changes. std::optional<forks::Choice> builds; if (const QStringList cannot = cannotTake(session, o.taskbar, &builds); !cannot.isEmpty()) return fail(error, QStringLiteral("this Plasma can't take the whole XP look yet, so nothing was changed:\n ") + cannot.join(QStringLiteral("\n "))); State state = State::load(); const QString build = paths::build() + QLatin1Char('/') + look.id; QDir(build).removeRecursively();
// --- generate ------------------------------------------------------------ report(o, QStringLiteral("Generating %1 ...").arg(look.name)); const QString decoration = writeDecoration(look, build + QLatin1String("/aurorae"), error); if (decoration.isEmpty()) return false; const QString panel = o.taskbar ? writePanelTheme(look, build + QLatin1String("/desktoptheme"), error) : QString(); if (o.taskbar && panel.isEmpty()) return false; const QString startDir = paths::data() + QLatin1String("/start/") + look.id; QJsonObject startTheme; if (o.taskbar) { QDir(startDir).removeRecursively(); startTheme = writeStartTheme(look, startDir, error, StartButtonExtras::fromJson(state.settings.value(QLatin1String(kStartButtonKey)).toObject())); if (startTheme.isEmpty()) return false; } QString iconTheme; if (o.icons) { QString previous = state.settings.value(QStringLiteral("previousIconTheme")).toString(); const QString current = session.readConfig(QStringLiteral("kdeglobals"), {QStringLiteral("Icons")}, QStringLiteral("Theme")); if (previous.isEmpty() && current != QLatin1String(kIconThemeId) && current != QLatin1String("xp2plasma-icons")) previous = current.isEmpty() ? QStringLiteral("breeze") : current; if (!previous.isEmpty()) state.settings.insert(QStringLiteral("previousIconTheme"), previous); QString why; // (the tray's icons XP has no art for, in the tray's text colour) iconTheme = writeIconTheme(build + QLatin1String("/icons"), previous, panelArt(look).trayTextColour, &why, spinIcons(look)); if (iconTheme.isEmpty()) report(o, QStringLiteral(" no XP icons: %1").arg(why)); }
// --- install --------------------------------------------------------------- report(o, QStringLiteral("Installing ...")); const QString auroraeDest = share(QStringLiteral("aurorae/themes/")) + look.id; const QString colorsDest = share(QStringLiteral("color-schemes/")) + look.id + QLatin1String(".colors"); const QString panelDest = share(QStringLiteral("plasma/desktoptheme/")) + look.id + QLatin1String("-taskbar"); const QString iconsDest = share(QStringLiteral("icons/")) + QLatin1String(kIconThemeId); if (!install(decoration, auroraeDest, error)) return false; QDir().mkpath(QFileInfo(colorsDest).absolutePath()); { QSaveFile f(colorsDest); if (!f.open(QIODevice::WriteOnly) || f.write(colorScheme(look.system, look.id, {panelArt(look).panelColour, panelArt(look).textColour})) < 0 || !f.commit()) return fail(error, QStringLiteral("%1: %2").arg(colorsDest, f.errorString())); } if (o.taskbar) { if (!install(panel, panelDest, error) || !installStartPlasmoid(share(QStringLiteral("plasma/plasmoids")), error) || !installClockPlasmoid(share(QStringLiteral("plasma/plasmoids")), error)) return false; QString why; if (!overrides::installPanelWidgets(share(QStringLiteral("plasma/plasmoids")), &why)) for (const QString &line : why.split(QLatin1Char('\n'))) report(o, QStringLiteral(" left as Plasma's own: %1").arg(line)); if (builds) { if (!forks::install(session, *builds, state, error)) return false; report(o, QStringLiteral(" Plasma's %1 as XPlasma builds them (Plasma %2)") .arg(QStringList(builds->plugins.keys()).join(QStringLiteral(", ")), builds->series)); } else { forks::remove(state); // (none wanted: an older Plasma's, if any, go) } } if (!iconTheme.isEmpty() && !install(iconTheme, iconsDest, error)) return false; QString why; // XP's Properties on the desktop's menu: this XPlasma's editor. const QString editor = QCoreApplication::applicationDirPath() + QLatin1String("/xplasma-edit"); if (!QFileInfo(editor).isExecutable()) report(o, QStringLiteral(" no XPlasma Properties on the desktop: %1 isn't there").arg(editor)); else if (!overrides::installDesktopMenu(share(QStringLiteral("plasma/plasmoids")), editor, &why)) report(o, QStringLiteral(" no XPlasma Properties on the desktop: %1").arg(why)); if (!overrides::installTitleShadows(share(QStringLiteral("aurorae/themes")), share(QStringLiteral("kwin/aurorae/aurorae.qml")), &why)) report(o, QStringLiteral(" no title shadows: %1").arg(why)); // Other looks' packages go: one of each at a time. for (const QString &stale : generatedPackages({auroraeDest, colorsDest, panelDest, iconsDest})) { if (QFileInfo(stale).isDir()) QDir(stale).removeRecursively(); else QFile::remove(stale); }
// --- apply ------------------------------------------------------------------- report(o, QStringLiteral("Applying ...")); // XP's text first: the shell restarts below, and reads fontconfig anew. if (!xptext::apply(session, state, &why)) report(o, QStringLiteral(" text stays as it was: %1").arg(why)); installWindowRules(session); // (Its name forgotten first: plasma-apply-colorscheme won't apply the // scheme that's already current -- a look applied again, regenerated.) session.writeConfig(QStringLiteral("kdeglobals"), {QStringLiteral("General")}, QStringLiteral("ColorScheme"), QString(), false); if (!session.run({QStringLiteral("plasma-apply-colorscheme"), look.id}).ok()) report(o, QStringLiteral(" plasma-apply-colorscheme failed")); applyFonts(session, fontSettings(look.system)); // The window borders after the colours and fonts, which have KWin make // every window's anew: its last change, waited for. applyDecoration(session, look.id); if (o.widgets) { QStringList log; if (!qtstyle::apply(session, look, state, &log, &why)) report(o, QStringLiteral(" Qt apps keep their style: %1").arg(why)); // (GTK's too: a failure there isn't the look's.) if (!gtk::apply(session, look, state, &log, &why)) report(o, QStringLiteral(" GTK apps keep their theme: %1").arg(why)); for (const QString &line : std::as_const(log)) report(o, QStringLiteral(" ") + line); } if (o.taskbar) { const QString panelId = look.id + QLatin1String("-taskbar"); session.run({QStringLiteral("plasma-apply-desktoptheme"), panelId}); // A running plasmashell neither finds a theme package that appeared // after it started nor re-reads the current one's SVGs, and caches // them by the theme's name (another colour of the same style came up // in the old one's art): it restarts, its caches gone while it's down. report(o, QStringLiteral("Restarting Plasma's shell for the taskbar ...")); session.restartPlasmaShell([&] { const QString cache = paths::home() + QLatin1String("/.cache/"); QFile::remove(cache + QLatin1String("plasma_theme_") + panelId + QLatin1String(".kcache")); QFile::remove(cache + QLatin1String("ksvg-elements")); primeSizeHints(panelDest + QLatin1String("/widgets"), cache + QLatin1String("ksvg-elements")); settleTaskbars(session, panelArt(look).height); }); // (Again through the running shell, for panels it hadn't saved yet.) session.script(scriptFrom(QStringLiteral("panel"), {{QStringLiteral("HEIGHT"), panelArt(look).height}})); // The clock in the style's clock font (Classic: the status font). LogFont clock = look.system.metrics.statusFont; if (!look.classic()) { const QString spec = look.renderer->property({QStringLiteral("clock"), 0, 0, QStringLiteral("traynotify")}, u"font") .value_or(QStringLiteral("Tahoma, 8")); const QStringList parts = spec.split(QLatin1Char(',')); clock = LogFont::fromPoints(parts.value(0).trimmed(), parts.value(1).trimmed().toInt(), spec.contains(QLatin1String("bold"), Qt::CaseInsensitive)); } const auto layout = session.script(scriptFrom(QStringLiteral("layout"), {{QStringLiteral("START"), QLatin1String(kStartPlasmoidId)}, {QStringLiteral("THEME"), startTheme.toVariantMap()}, {QStringLiteral("CLOCK_FAMILY"), clock.face}, {QStringLiteral("CLOCK_POINTS"), clock.points()}, {QStringLiteral("CLOCK_WEIGHT"), clock.weight}, {QStringLiteral("CLOCK"), QLatin1String(kClockPlasmoidId)}, {QStringLiteral("DESKTOP_FILES"), desktopFiles()}, {QStringLiteral("CLOCK_COLOR"), panelArt(look).trayTextColour.name()}, {QStringLiteral("CLOCK_COMMAND"), overrides::launchCommand(editorPath(), {QStringLiteral("--date-time")})}}), &why); if (!layout) { report(o, QStringLiteral(" the panel wasn't rearranged: %1").arg(why)); } else { const QJsonObject result = QJsonDocument::fromJson(layout->toUtf8()).object(); for (const QJsonValue &line : result.value(QStringLiteral("log")).toArray()) report(o, QStringLiteral(" ") + line.toString()); // Plasma's scripting can't reorder a panel: its AppletOrder is // written with the shell down (it saves its own on the way out). QList<std::pair<QString, QString>> orders; for (const QJsonValue &p : result.value(QStringLiteral("panels")).toArray()) { QStringList ids; for (const QJsonValue &id : p.toObject().value(QStringLiteral("order")).toArray()) ids << QString::number(id.toInt()); const QString panelNumber = QString::number(p.toObject().value(QStringLiteral("panel")).toInt()); const QString wanted = ids.join(QLatin1Char(';')); if (session.readConfig(QStringLiteral("plasma-org.kde.plasma.desktop-appletsrc"), {QStringLiteral("Containments"), panelNumber, QStringLiteral("General")}, QStringLiteral("AppletOrder")) != wanted) orders.append({panelNumber, wanted}); } if (!orders.isEmpty()) { report(o, QStringLiteral("Restarting Plasma's shell for the taskbar's order ...")); session.restartPlasmaShell([&] { for (const auto &[panelNumber, order] : orders) session.writeConfig(QStringLiteral("plasma-org.kde.plasma.desktop-appletsrc"), {QStringLiteral("Containments"), panelNumber, QStringLiteral("General")}, QStringLiteral("AppletOrder"), order, false); }); } } } if (o.wallpaper) applyWallpaper(session, *o.wallpaper, o.progress); if (!iconTheme.isEmpty()) { const QString helper = iconHelper(); if (helper.isEmpty()) report(o, QStringLiteral(" Plasma's icon helper isn't here; the XP icons are installed but not switched on")); else session.run({helper, QLatin1String(kIconThemeId)}); }
// XP's sounds, as XP had them from the start: the first time, unless // they've been turned off. (A failure there isn't the look's.) if (o.sounds && sounds::wantedWithLook(state)) { if (sounds::apply(session, state, sounds::defaultScheme(), &why)) report(o, QStringLiteral("Sounds: Windows Default")); else report(o, QStringLiteral(" XP's sounds stay off: %1").arg(why)); }
state.appliedKind = look.classic() ? QStringLiteral("classic") : QStringLiteral("style"); state.style = look.stylePath; state.variant = look.variant; state.scheme = look.classic() ? look.system.name : QString(); state.themeFile = o.themeFile; state.settings.insert(QLatin1String(kAppliedWithKey), project::kVersion); state.save(); report(o, QStringLiteral("Done.")); return true;}
void applyWallpaper(Session &session, const Wallpaper &w, const std::function<void(const QString &)> &progress){ session.script(scriptFrom(QStringLiteral("wallpaper"), {{QStringLiteral("IMAGE"), w.image.isEmpty() ? QVariant::fromValue(nullptr) : QVariant(QUrl::fromLocalFile(w.image).toString())}, {QStringLiteral("FILL"), w.fill ? QVariant(*w.fill) : QVariant::fromValue(nullptr)}, {QStringLiteral("COLOUR"), w.colour.name()}})); if (progress) progress(w.image.isEmpty() ? QStringLiteral("Desktop: %1").arg(w.colour.name()) : QStringLiteral("Wallpaper: %1").arg(QFileInfo(w.image).fileName()));}
bool resetDesktop(Session &session, const std::function<void(const QString &)> &progress, QString *error){ auto say = [&](const QString &line) { if (progress) progress(line); }; State state = State::load(); QStringList log; say(QStringLiteral("Taking XPlasma's widget style off ...")); qtstyle::revert(session, state, &log); gtk::revert(session, state, &log); for (const QString &line : std::as_const(log)) say(QStringLiteral(" ") + line); overrides::removeTitleShadows(share(QStringLiteral("kwin/aurorae/aurorae.qml"))); removeWindowRules(session); // Plasma's own widgets back, in a shell of their own, before Plasma's // layout is: the running shell has XPlasma's loaded, and given the // default panel it would load Plasma's beside them -- with builds of // them, the same QML module registered twice, which Qt won't survive. session.restartPlasmaShell([&] { overrides::removePanelWidgets(share(QStringLiteral("plasma/plasmoids"))); overrides::removeDesktopMenu(share(QStringLiteral("plasma/plasmoids"))); forks::remove(state); });
say(QStringLiteral("Clearing the XP fonts ...")); xptext::revert(session, state); pointers::revert(session, state); sounds::revert(session, state); for (const ConfigValue &v : fontSettings(ClassicScheme())) session.deleteConfig(v.file, v.group.split(QLatin1Char('/')), v.key); session.signal(QStringLiteral("/KDEPlatformTheme"), QStringLiteral("org.kde.KDEPlatformTheme"), QStringLiteral("refreshFonts"));
// Plasma's own tool does the heavy lifting: the global theme (borders, // colours, Plasma style, widget style, icons) and the default panel. const QString lookAndFeel = QStringLiteral("org.kde.breeze.desktop"); say(QStringLiteral("Applying %1 with the default panel ...").arg(lookAndFeel)); const host::Result r = session.run({QStringLiteral("plasma-apply-lookandfeel"), QStringLiteral("-a"), lookAndFeel, QStringLiteral("--resetLayout")}); if (!r.ok()) return fail(error, QStringLiteral("plasma-apply-lookandfeel failed: %1").arg(QString::fromUtf8(r.err).trimmed())); session.waitForPlasmaShell(); // (it may restart the shell itself)
// The wallpaper from the global theme's own defaults, set outright. for (const QString &root : paths::systemData()) { QFile defaults(root + QLatin1String("/plasma/look-and-feel/") + lookAndFeel + QLatin1String("/contents/defaults")); if (!defaults.open(QIODevice::ReadOnly)) continue; QString section, image; for (const QString &line : QString::fromUtf8(defaults.readAll()).split(QLatin1Char('\n'))) { if (line.startsWith(QLatin1Char('['))) section = line.trimmed(); else if (section == QLatin1String("[Wallpaper]") && line.startsWith(QLatin1String("Image="))) image = line.mid(6).trimmed(); } const QString path = image.startsWith(QLatin1Char('/')) ? image : root + QLatin1String("/wallpapers/") + (image.isEmpty() ? QStringLiteral("Next") : image); if (QFileInfo::exists(path)) { session.script(scriptFrom(QStringLiteral("wallpaper"), {{QStringLiteral("IMAGE"), path}, {QStringLiteral("FILL"), QVariant::fromValue(nullptr)}, {QStringLiteral("COLOUR"), QStringLiteral("#000000")}})); say(QStringLiteral("Wallpaper: %1").arg(QFileInfo(path).fileName())); } break; }
// The icons the user had. const QString previousIcons = state.settings.value(QStringLiteral("previousIconTheme")).toString(); if (const QString helper = iconHelper(); !helper.isEmpty() && !previousIcons.isEmpty()) session.run({helper, previousIcons}); state.settings.remove(QStringLiteral("previousIconTheme"));
say(QStringLiteral("Removing XPlasma's packages ...")); const QStringList packages = generatedPackages(); for (const QString &p : packages) { if (QFileInfo(p).isDir()) QDir(p).removeRecursively(); else QFile::remove(p); } const QString start = share(QStringLiteral("plasma/plasmoids/")) + QLatin1String(kStartPlasmoidId); QDir(start).removeRecursively(); QDir(share(QStringLiteral("plasma/plasmoids/")) + QLatin1String(kClockPlasmoidId)).removeRecursively(); QDir(paths::data() + QLatin1String("/start")).removeRecursively(); say(QStringLiteral(" %1 removed").arg(packages.size() + 1));
state.appliedKind.clear(); state.style.clear(); state.variant.clear(); state.scheme.clear(); state.themeFile.clear(); state.settings.remove(QLatin1String(kAppliedWithKey)); state.save(); session.reconfigureKWin(); say(QStringLiteral("The desktop is Plasma's default again.")); return true;}
} // namespace xpl