File indexing completed on 2025-03-02 05:11:58
0001 /* 0002 SPDX-FileCopyrightText: 2005-2007 Fredrik Höglund <fredrik@kde.org> 0003 0004 SPDX-License-Identifier: GPL-2.0-only 0005 */ 0006 0007 #include <KConfig> 0008 #include <KConfigGroup> 0009 #include <KLocalizedString> 0010 0011 #include <QDir> 0012 #include <QLoggingCategory> 0013 #include <QRegularExpression> 0014 #include <private/qtx11extras_p.h> 0015 0016 #include "thememodel.h" 0017 #include "xcursortheme.h" 0018 0019 #include <X11/Xcursor/Xcursor.h> 0020 #include <X11/Xlib.h> 0021 0022 using namespace Qt::StringLiterals; 0023 0024 // Check for older version 0025 #if !defined(XCURSOR_LIB_MAJOR) && defined(XCURSOR_MAJOR) 0026 #define XCURSOR_LIB_MAJOR XCURSOR_MAJOR 0027 #define XCURSOR_LIB_MINOR XCURSOR_MINOR 0028 #endif 0029 0030 Q_LOGGING_CATEGORY(KCM_CURSORTHEME, "kcm_cursortheme", QtWarningMsg) 0031 0032 CursorThemeModel::CursorThemeModel(QObject *parent) 0033 : QAbstractListModel(parent) 0034 { 0035 insertThemes(); 0036 } 0037 0038 CursorThemeModel::~CursorThemeModel() 0039 { 0040 qDeleteAll(list); 0041 list.clear(); 0042 } 0043 0044 QHash<int, QByteArray> CursorThemeModel::roleNames() const 0045 { 0046 QHash<int, QByteArray> roleNames = QAbstractListModel::roleNames(); 0047 roleNames[CursorTheme::DisplayDetailRole] = "description"; 0048 roleNames[CursorTheme::IsWritableRole] = "isWritable"; 0049 roleNames[CursorTheme::PendingDeletionRole] = "pendingDeletion"; 0050 0051 return roleNames; 0052 } 0053 0054 void CursorThemeModel::refreshList() 0055 { 0056 beginResetModel(); 0057 qDeleteAll(list); 0058 list.clear(); 0059 defaultName.clear(); 0060 endResetModel(); 0061 insertThemes(); 0062 } 0063 0064 QVariant CursorThemeModel::data(const QModelIndex &index, int role) const 0065 { 0066 if (!index.isValid() || index.row() < 0 || index.row() >= list.count()) 0067 return QVariant(); 0068 0069 CursorTheme *theme = list.at(index.row()); 0070 0071 // Text label 0072 if (role == Qt::DisplayRole) { 0073 return theme->title(); 0074 } 0075 0076 // Description for the first name column 0077 if (role == CursorTheme::DisplayDetailRole) 0078 return theme->description(); 0079 0080 // Icon for the name column 0081 if (role == Qt::DecorationRole) 0082 return theme->icon(); 0083 0084 if (role == CursorTheme::IsWritableRole) { 0085 return theme->isWritable(); 0086 } 0087 0088 if (role == CursorTheme::PendingDeletionRole) { 0089 return pendingDeletions.contains(theme); 0090 } 0091 0092 return QVariant(); 0093 } 0094 0095 bool CursorThemeModel::setData(const QModelIndex &index, const QVariant &value, int role) 0096 { 0097 if (!checkIndex(index, CheckIndexOption::IndexIsValid | CheckIndexOption::ParentIsInvalid)) { 0098 return false; 0099 } 0100 if (role == CursorTheme::PendingDeletionRole) { 0101 const bool shouldRemove = value.toBool(); 0102 if (shouldRemove) { 0103 pendingDeletions.push_back(list[index.row()]); 0104 } else { 0105 pendingDeletions.removeAll(list[index.row()]); 0106 } 0107 Q_EMIT dataChanged(index, index, {role}); 0108 return true; 0109 } 0110 return false; 0111 } 0112 0113 void CursorThemeModel::sort(int column, Qt::SortOrder order) 0114 { 0115 Q_UNUSED(column); 0116 Q_UNUSED(order); 0117 0118 // Sorting of the model isn't implemented, as the KCM currently uses 0119 // a sorting proxy model. 0120 } 0121 0122 const CursorTheme *CursorThemeModel::theme(const QModelIndex &index) 0123 { 0124 if (!index.isValid()) 0125 return nullptr; 0126 0127 if (index.row() < 0 || index.row() >= list.count()) 0128 return nullptr; 0129 0130 return list.at(index.row()); 0131 } 0132 0133 QModelIndex CursorThemeModel::findIndex(const QString &name) 0134 { 0135 uint hash = qHash(name); 0136 0137 for (int i = 0; i < list.count(); i++) { 0138 const CursorTheme *theme = list.at(i); 0139 if (theme->hash() == hash) 0140 return index(i, 0); 0141 } 0142 0143 return QModelIndex(); 0144 } 0145 0146 QModelIndex CursorThemeModel::defaultIndex() 0147 { 0148 return findIndex(defaultName); 0149 } 0150 0151 const QStringList CursorThemeModel::searchPaths() 0152 { 0153 if (!baseDirs.isEmpty()) 0154 return baseDirs; 0155 0156 #if XCURSOR_LIB_MAJOR == 1 && XCURSOR_LIB_MINOR < 1 0157 // These are the default paths Xcursor will scan for cursor themes 0158 QString path("~/.icons:/usr/share/icons:/usr/share/pixmaps:/usr/X11R6/lib/X11/icons"); 0159 0160 // If XCURSOR_PATH is set, use that instead of the default path 0161 char *xcursorPath = std::getenv("XCURSOR_PATH"); 0162 if (xcursorPath) 0163 path = xcursorPath; 0164 #else 0165 // Get the search path from Xcursor 0166 QString path = XcursorLibraryPath(); 0167 qCDebug(KCM_CURSORTHEME) << "XcursorLibraryPath:" << path; 0168 #endif 0169 0170 // Separate the paths 0171 baseDirs = path.split(':', Qt::SkipEmptyParts); 0172 0173 // Remove duplicates 0174 QMutableStringListIterator i(baseDirs); 0175 while (i.hasNext()) { 0176 const QString path = i.next(); 0177 QMutableStringListIterator j(i); 0178 while (j.hasNext()) 0179 if (j.next() == path) 0180 j.remove(); 0181 } 0182 0183 // Expand all occurrences of ~/ to the home dir 0184 baseDirs.replaceInStrings(QRegularExpression("^~\\/"), QDir::home().path() + '/'); 0185 return baseDirs; 0186 } 0187 0188 bool CursorThemeModel::hasTheme(const QString &name) const 0189 { 0190 const uint hash = qHash(name); 0191 0192 foreach (const CursorTheme *theme, list) 0193 if (theme->hash() == hash) 0194 return true; 0195 0196 return false; 0197 } 0198 0199 bool CursorThemeModel::isCursorTheme(const QString &theme, const int depth) 0200 { 0201 // Prevent infinite recursion 0202 if (depth > 10) 0203 return false; 0204 0205 // Search each icon theme directory for 'theme' 0206 foreach (const QString &baseDir, searchPaths()) { 0207 QDir dir(baseDir); 0208 if (!dir.exists() || !dir.cd(theme)) 0209 continue; 0210 0211 // If there's a cursors subdir, we'll assume this is a cursor theme 0212 if (dir.exists(QStringLiteral("cursors"))) 0213 return true; 0214 0215 // If the theme doesn't have an index.theme file, it can't inherit any themes. 0216 if (!dir.exists(QStringLiteral("index.theme"))) 0217 continue; 0218 0219 // Open the index.theme file, so we can get the list of inherited themes 0220 KConfig config(dir.path() + "/index.theme", KConfig::NoGlobals); 0221 KConfigGroup cg(&config, u"Icon Theme"_s); 0222 0223 // Recurse through the list of inherited themes, to check if one of them 0224 // is a cursor theme. 0225 const QStringList inherits = cg.readEntry("Inherits", QStringList()); 0226 for (const QString &inherit : inherits) { 0227 // Avoid possible DoS 0228 if (inherit == theme) 0229 continue; 0230 0231 if (isCursorTheme(inherit, depth + 1)) 0232 return true; 0233 } 0234 } 0235 0236 return false; 0237 } 0238 0239 bool CursorThemeModel::handleDefault(const QDir &themeDir) 0240 { 0241 QFileInfo info(themeDir.path()); 0242 0243 // If "default" is a symlink 0244 if (info.isSymLink()) { 0245 QFileInfo target(info.symLinkTarget()); 0246 if (target.exists() && (target.isDir() || target.isSymLink())) 0247 defaultName = target.fileName(); 0248 0249 return true; 0250 } 0251 0252 // If there's no cursors subdir, or if it's empty 0253 if (!themeDir.exists(QStringLiteral("cursors")) || QDir(themeDir.path() + "/cursors").entryList(QDir::Files | QDir::NoDotAndDotDot).isEmpty()) { 0254 if (themeDir.exists(QStringLiteral("index.theme"))) { 0255 XCursorTheme theme(themeDir); 0256 if (!theme.inherits().isEmpty()) 0257 defaultName = theme.inherits().at(0); 0258 } 0259 return true; 0260 } 0261 0262 defaultName = QStringLiteral("default"); 0263 return false; 0264 } 0265 0266 void CursorThemeModel::processThemeDir(const QDir &themeDir) 0267 { 0268 qCDebug(KCM_CURSORTHEME) << "Searching in" << themeDir; 0269 bool haveCursors = themeDir.exists(QStringLiteral("cursors")); 0270 0271 // Special case handling of "default", since it's usually either a 0272 // symlink to another theme, or an empty theme that inherits another 0273 // theme. 0274 if (defaultName.isNull() && themeDir.dirName() == QLatin1String("default")) { 0275 if (handleDefault(themeDir)) 0276 return; 0277 } 0278 0279 // If the directory doesn't have a cursors subdir and lacks an 0280 // index.theme file it can't be a cursor theme. 0281 if (!themeDir.exists(QStringLiteral("index.theme")) && !haveCursors) 0282 return; 0283 0284 // Create a cursor theme object for the theme dir 0285 XCursorTheme *theme = new XCursorTheme(themeDir); 0286 0287 // Skip this theme if it's hidden. 0288 if (theme->isHidden()) { 0289 delete theme; 0290 return; 0291 } 0292 0293 // If there's no cursors subdirectory we'll do a recursive scan 0294 // to check if the theme inherits a theme with one. 0295 if (!haveCursors) { 0296 bool foundCursorTheme = false; 0297 0298 foreach (const QString &name, theme->inherits()) 0299 if ((foundCursorTheme = isCursorTheme(name))) 0300 break; 0301 0302 if (!foundCursorTheme) { 0303 delete theme; 0304 return; 0305 } 0306 } 0307 0308 // Append the theme to the list 0309 beginInsertRows(QModelIndex(), list.size(), list.size()); 0310 list.append(theme); 0311 endInsertRows(); 0312 } 0313 0314 void CursorThemeModel::insertThemes() 0315 { 0316 // Scan each base dir for Xcursor themes and add them to the list. 0317 const QStringList paths{searchPaths()}; 0318 qCDebug(KCM_CURSORTHEME) << "searchPaths:" << paths; 0319 for (const QString &baseDir : paths) { 0320 QDir dir(baseDir); 0321 if (!dir.exists()) 0322 continue; 0323 0324 // Process each subdir in the directory 0325 for (const auto list{dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot)}; const QString &name : list) { 0326 // Don't process the theme if a theme with the same name already exists 0327 // in the list. Xcursor will pick the first one it finds in that case, 0328 // and since we use the same search order, the one Xcursor picks should 0329 // be the one already in the list. 0330 if (hasTheme(name) || !dir.cd(name)) 0331 continue; 0332 0333 processThemeDir(dir); 0334 dir.cdUp(); // Return to the base dir 0335 } 0336 } 0337 0338 // The theme Xcursor will end up using if no theme is configured 0339 if (defaultName.isNull() || !hasTheme(defaultName)) 0340 defaultName = QStringLiteral("KDE_Classic"); 0341 } 0342 0343 bool CursorThemeModel::addTheme(const QDir &dir) 0344 { 0345 XCursorTheme *theme = new XCursorTheme(dir); 0346 0347 // Don't add the theme to the list if it's hidden 0348 if (theme->isHidden()) { 0349 delete theme; 0350 return false; 0351 } 0352 0353 // ### If the theme is hidden, the user will probably find it strange that it 0354 // doesn't appear in the list view. There also won't be a way for the user 0355 // to delete the theme using the KCM. Perhaps a warning about this should 0356 // be issued, and the user be given a chance to undo the installation. 0357 0358 // If an item with the same name already exists in the list, 0359 // we'll remove it before inserting the new one. 0360 for (int i = 0; i < list.count(); i++) { 0361 if (list.at(i)->hash() == theme->hash()) { 0362 removeTheme(index(i, 0)); 0363 break; 0364 } 0365 } 0366 0367 // Append the theme to the list 0368 beginInsertRows(QModelIndex(), rowCount(), rowCount()); 0369 list.append(theme); 0370 endInsertRows(); 0371 0372 return true; 0373 } 0374 0375 void CursorThemeModel::removeTheme(const QModelIndex &index) 0376 { 0377 if (!index.isValid()) 0378 return; 0379 0380 beginRemoveRows(QModelIndex(), index.row(), index.row()); 0381 pendingDeletions.removeAll(list[index.row()]); 0382 delete list.takeAt(index.row()); 0383 endRemoveRows(); 0384 }