1 #include <QVariant>
2 #include <QSettings>
3 #include <QGuiApplication>
4 #include <QScreen>
5 #include <QFont>
6 #include <QPalette>
7 #include <QTimer>
8 #include <QIcon>
9 #include <QRegExp>
10 #include <QWindow>
11 
12 #ifdef QT_WIDGETS_LIB
13 #include <QStyle>
14 #include <QStyleFactory>
15 #include <QApplication>
16 #include <QWidget>
17 #endif
18 
19 #include <QFile>
20 #include <QFileSystemWatcher>
21 #include <QDir>
22 #include <QTextStream>
23 
24 #include <stdlib.h>
25 
26 #include <lthemeengine/lthemeengine.h>
27 #include "lthemeengineplatformtheme.h"
28 #if (QT_VERSION >= QT_VERSION_CHECK(5, 7, 0)) && !defined(QT_NO_DBUS)
29 #include <private/qdbusmenubar_p.h>
30 #endif
31 #if !defined(QT_NO_DBUS) && !defined(QT_NO_SYSTEMTRAYICON)
32 #include <QDBusArgument>
33 #include <private/qdbustrayicon_p.h>
34 #endif
35 
36 #include <QX11Info>
37 #include <QCursor>
38 //Need access to the private QCursor header so we can refresh the mouse cursor cache
39 //#include <private/qcursor_p.h> //Does not work - looks like we need to use X11 stuff instead
40 #include <X11/Xcursor/Xcursor.h>
41 
42 Q_LOGGING_CATEGORY(llthemeengine, "lthemeengine")
43 
44 //QT_QPA_PLATFORMTHEME=lthemeengine
45 
lthemeenginePlatformTheme()46 lthemeenginePlatformTheme::lthemeenginePlatformTheme(){
47   if(QGuiApplication::desktopSettingsAware()){
48     readSettings();
49 #ifdef QT_WIDGETS_LIB
50     QMetaObject::invokeMethod(this, "createFSWatcher", Qt::QueuedConnection);
51 #endif
52     QMetaObject::invokeMethod(this, "applySettings", Qt::QueuedConnection);
53 
54     QGuiApplication::setFont(m_generalFont);
55     }
56   //qCDebug(llthemeengine) << "using lthemeengine plugin";
57 #ifdef QT_WIDGETS_LIB
58   if(!QStyleFactory::keys().contains("lthemeengine-style"))
59     qCCritical(llthemeengine) << "unable to find lthemeengine proxy style";
60 #endif
61 }
62 
~lthemeenginePlatformTheme()63 lthemeenginePlatformTheme::~lthemeenginePlatformTheme(){
64   if(m_customPalette)
65     delete m_customPalette;
66 }
67 
68 #if (QT_VERSION >= QT_VERSION_CHECK(5, 7, 0)) && !defined(QT_NO_DBUS)
createPlatformMenuBar() const69 QPlatformMenuBar *lthemeenginePlatformTheme::createPlatformMenuBar() const{
70   if(m_checkDBusGlobalMenu){
71     QDBusConnection conn = QDBusConnection::sessionBus();
72     m_dbusGlobalMenuAvailable = conn.interface()->isServiceRegistered("com.canonical.AppMenu.Registrar");
73     //qCDebug(llthemeengine) << "D-Bus global menu:" << (m_dbusGlobalMenuAvailable ? "yes" : "no");
74     }
75   return (m_dbusGlobalMenuAvailable ? new QDBusMenuBar() : nullptr);
76 }
77 #endif
78 
79 #if !defined(QT_NO_DBUS) && !defined(QT_NO_SYSTEMTRAYICON)
createPlatformSystemTrayIcon() const80 QPlatformSystemTrayIcon *lthemeenginePlatformTheme::createPlatformSystemTrayIcon() const{
81   if(m_checkDBusTray){
82     QDBusMenuConnection conn;
83     m_dbusTrayAvailable = conn.isStatusNotifierHostRegistered();
84     m_checkDBusTray = false;
85     //qCDebug(llthemeengine) << "D-Bus system tray:" << (m_dbusTrayAvailable ? "yes" : "no");
86     }
87   return (m_dbusTrayAvailable ? new QDBusTrayIcon() : nullptr);
88 }
89 #endif
90 
palette(QPlatformTheme::Palette type) const91 const QPalette *lthemeenginePlatformTheme::palette(QPlatformTheme::Palette type) const{
92   Q_UNUSED(type);
93   return (m_usePalette ? m_customPalette : nullptr);
94 }
95 
font(QPlatformTheme::Font type) const96 const QFont *lthemeenginePlatformTheme::font(QPlatformTheme::Font type) const{
97   if(type == QPlatformTheme::FixedFont){ return &m_fixedFont; }
98   return &m_generalFont;
99 }
100 
themeHint(QPlatformTheme::ThemeHint hint) const101 QVariant lthemeenginePlatformTheme::themeHint(QPlatformTheme::ThemeHint hint) const{
102   switch (hint){
103     case QPlatformTheme::CursorFlashTime: return m_cursorFlashTime;
104     case MouseDoubleClickInterval: return m_doubleClickInterval;
105     case QPlatformTheme::ToolButtonStyle: return m_toolButtonStyle;
106     case QPlatformTheme::SystemIconThemeName: return m_iconTheme;
107     case QPlatformTheme::StyleNames: return QStringList() << "lthemeengine-style";
108     case QPlatformTheme::IconThemeSearchPaths: return lthemeengine::iconPaths();
109     case DialogButtonBoxLayout: return m_buttonBoxLayout;
110     case QPlatformTheme::UiEffects: return m_uiEffects;
111 #if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0))
112     case QPlatformTheme::WheelScrollLines: return m_wheelScrollLines;
113 #endif
114     default: return QPlatformTheme::themeHint(hint);
115     }
116 }
117 
applySettings()118 void lthemeenginePlatformTheme::applySettings(){
119   if(!QGuiApplication::desktopSettingsAware()){ return; }
120 #if (QT_VERSION >= QT_VERSION_CHECK(5, 5, 0))
121  if(!m_update){
122    //do not override application palette
123    if(QCoreApplication::testAttribute(Qt::AA_SetPalette)){
124      m_usePalette = false;
125      qCDebug(llthemeengine) << "palette support is disabled";
126      }
127    }
128 #endif
129 #ifdef QT_WIDGETS_LIB
130   if(hasWidgets()){
131     qApp->setFont(m_generalFont);
132     //Qt 5.6 or higher should be use themeHint function on application startup.
133     //So, there is no need to call this function first time.
134 #if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0))
135     if(m_update)
136       qApp->setWheelScrollLines(m_wheelScrollLines);
137 #else
138       qApp->setWheelScrollLines(m_wheelScrollLines);
139 #endif
140       if(m_update && qApp->style()->objectName() == "lthemeengine-style") /* ignore application style */ { qApp->setStyle("lthemeengine-style"); } //recreate style object
141       if(m_update && m_usePalette){
142         if(m_customPalette){ qApp->setPalette(*m_customPalette); }
143         else{ qApp->setPalette(qApp->style()->standardPalette()); }
144         }
145         //do not override application style if one is already set by the app itself
146       QString orig = qApp->styleSheet();
147       if(orig.startsWith(m_oldStyleSheet)){ orig = orig.remove(m_oldStyleSheet); }
148       qApp->setStyleSheet(m_userStyleSheet+orig); //make sure the app style has higher priority than ours
149       m_oldStyleSheet = m_userStyleSheet;
150 
151     }
152 #endif
153   QGuiApplication::setFont(m_generalFont); //apply font
154   bool ithemechange = m_iconTheme != QIcon::themeName();
155   QIcon::setThemeName(m_iconTheme); //apply icons
156   //See if we need to reload the application icon from the new theme
157   if(ithemechange){
158     QString appIcon = qApp->windowIcon().name();
159     if(!appIcon.isEmpty() && QIcon::hasThemeIcon(appIcon)){ qApp->setWindowIcon(QIcon::fromTheme(appIcon)); }
160     QWindowList wins = qApp->topLevelWindows();
161     for(int i=0; i<wins.length(); i++){
162      QString winIcon = wins[i]->icon().name();
163       if(!winIcon.isEmpty() && QIcon::hasThemeIcon(winIcon)){ wins[i]->setIcon(QIcon::fromTheme(winIcon)); }
164     }
165   }
166   bool cthemechange = m_cursorTheme != QString(getenv("X_CURSOR_THEME"));
167   setenv("X_CURSOR_THEME", m_cursorTheme.toLocal8Bit().data(), 1);
168   //qDebug() << "Icon Theme Change:" << m_iconTheme << QIcon::themeSearchPaths();
169   if(m_customPalette && m_usePalette){ QGuiApplication::setPalette(*m_customPalette); } //apply palette
170 #ifdef QT_WIDGETS_LIB
171   if(hasWidgets()){
172     QEvent et(QEvent::ThemeChange);
173     QEvent ec(QEvent::CursorChange);
174     foreach (QWidget *w, qApp->allWidgets()){
175       if(ithemechange){ QApplication::sendEvent(w, &et); }
176       if(cthemechange){ QApplication::sendEvent(w, &ec); }
177       }
178     }
179 #endif
180   if(!m_update){ m_update = true; }
181 
182   //Mouse Cursor syncronization
183   /*QString mthemefile = QDir::homePath()+"/.icons/default/index.theme";
184   if(!watcher->files().contains(mthemefile) && QFile::exists(mthemefile)){
185     watcher->addPath(mthemefile); //X11 mouse cursor theme file
186     //qDebug() << "Add Mouse Cursor File to Watcher";
187     syncMouseCursorTheme(mthemefile);
188   }*/
189   //Now cleanup any old palette as needed
190   if(outgoingpalette != 0){
191     QCoreApplication::processEvents(); //make sure everything switches to the new palette first
192     delete outgoingpalette;
193   }
194 }
195 #ifdef QT_WIDGETS_LIB
196 
createFSWatcher()197 void lthemeenginePlatformTheme::createFSWatcher(){
198   watcher = new QFileSystemWatcher(this);
199   watcher->addPath(lthemeengine::configPath()); //theme engine settings directory
200   watcher->addPath(QDir::homePath()+"/.icons/default/index.theme"); //X11 mouse cursor theme file
201   QTimer *timer = new QTimer(this);
202   timer->setSingleShot(true);
203   timer->setInterval(500);
204   connect(watcher, SIGNAL(directoryChanged(QString)), timer, SLOT(start()));
205   connect(watcher, SIGNAL(fileChanged(QString)), this, SLOT(fileChanged(QString)) );
206   connect(timer, SIGNAL(timeout()), SLOT(updateSettings()));
207 }
208 
updateSettings()209 void lthemeenginePlatformTheme::updateSettings(){
210   //qCDebug(llthemeengine) << "updating settings..";
211   readSettings();
212   applySettings();
213 }
214 #endif
215 
fileChanged(QString path)216 void lthemeenginePlatformTheme::fileChanged(QString path){
217   if(path.endsWith("default/index.theme")){
218     //qDebug() << "Mouse Cursor File Changed";
219     syncMouseCursorTheme(path);
220   }
221 }
222 
readSettings()223 void lthemeenginePlatformTheme::readSettings(){
224   outgoingpalette = m_customPalette;
225   if(m_customPalette){
226     m_customPalette = 0;
227   }
228   QSettings settings(lthemeengine::configFile(), QSettings::IniFormat);
229   settings.beginGroup("Appearance");
230   m_style = settings.value("style", "Fusion").toString();
231   if(settings.value("custom_palette", false).toBool()){
232     QString schemePath = settings.value("color_scheme_path","airy").toString();
233     m_customPalette = new QPalette(loadColorScheme(schemePath));
234   }
235   m_cursorTheme = settings.value("cursor_theme","").toString();
236   m_iconTheme = settings.value("icon_theme", "material-design-light").toString();
237   settings.endGroup();
238   settings.beginGroup("Fonts");
239   m_generalFont = settings.value("general", QPlatformTheme::font(QPlatformTheme::SystemFont)).value<QFont>();
240   m_fixedFont = settings.value("fixed", QPlatformTheme::font(QPlatformTheme::FixedFont)).value<QFont>();
241   settings.endGroup();
242   settings.beginGroup("Interface");
243   m_doubleClickInterval = QPlatformTheme::themeHint(QPlatformTheme::MouseDoubleClickInterval).toInt();
244   m_doubleClickInterval = settings.value("double_click_interval", m_doubleClickInterval).toInt();
245   m_cursorFlashTime = QPlatformTheme::themeHint(QPlatformTheme::CursorFlashTime).toInt();
246   m_cursorFlashTime = settings.value("cursor_flash_time", m_cursorFlashTime).toInt();
247   m_buttonBoxLayout = QPlatformTheme::themeHint(QPlatformTheme::DialogButtonBoxLayout).toInt();
248   m_buttonBoxLayout = settings.value("buttonbox_layout", m_buttonBoxLayout).toInt();
249   QCoreApplication::setAttribute(Qt::AA_DontShowIconsInMenus, !settings.value("menus_have_icons", true).toBool());
250   m_toolButtonStyle = settings.value("toolbutton_style", Qt::ToolButtonFollowStyle).toInt();
251   m_wheelScrollLines = settings.value("wheel_scroll_lines", 3).toInt();
252   //load effects
253   m_uiEffects = QPlatformTheme::themeHint(QPlatformTheme::UiEffects).toInt();
254     if(settings.childKeys().contains("gui_effects")){
255       QStringList effectList = settings.value("gui_effects").toStringList();
256       m_uiEffects = 0;
257       if(effectList.contains("General")){ m_uiEffects |= QPlatformTheme::GeneralUiEffect; }
258       if(effectList.contains("AnimateMenu")){ m_uiEffects |= QPlatformTheme::AnimateMenuUiEffect; }
259       if(effectList.contains("FadeMenu")){ m_uiEffects |= QPlatformTheme::FadeMenuUiEffect; }
260       if(effectList.contains("AnimateCombo")){ m_uiEffects |= QPlatformTheme::AnimateComboUiEffect; }
261       if(effectList.contains("AnimateTooltip")){ m_uiEffects |= QPlatformTheme::AnimateTooltipUiEffect; }
262       if(effectList.contains("FadeTooltip")){ m_uiEffects |= QPlatformTheme::FadeTooltipUiEffect; }
263       if(effectList.contains("AnimateToolBox")){ m_uiEffects |= QPlatformTheme::AnimateToolBoxUiEffect; }
264       }
265     //load style sheets
266 #ifdef QT_WIDGETS_LIB
267     QStringList qssPaths;
268     if(qApp->applicationFilePath().section("/",-1).startsWith("lumina-desktop") ){ qssPaths << settings.value("desktop_stylesheets").toStringList(); }
269     qssPaths << settings.value("stylesheets").toStringList();
270     //qDebug() << "Loaded Stylesheets:" << qApp->applicationName() << qssPaths;
271     m_userStyleSheet = loadStyleSheets(qssPaths);
272 #endif
273     settings.endGroup();
274 }
275 
276 #ifdef QT_WIDGETS_LIB
hasWidgets()277 bool lthemeenginePlatformTheme::hasWidgets(){
278   return qobject_cast<QApplication *> (qApp) != nullptr;
279 }
280 #endif
281 
loadStyleSheets(const QStringList & paths)282 QString lthemeenginePlatformTheme::loadStyleSheets(const QStringList &paths){
283   //qDebug() << "Loading Stylesheets:" << paths;
284   QString content;
285   foreach (QString path, paths){
286     if(!QFile::exists(path)){ continue; }
287     QFile file(path);
288     file.open(QIODevice::ReadOnly);
289     content.append(file.readAll());
290     }
291   QRegExp regExp("//.*(\\n|$)");
292   regExp.setMinimal(true);
293   content.remove(regExp);
294   return content;
295 }
296 
loadColorScheme(QString filePath)297 QPalette lthemeenginePlatformTheme::loadColorScheme(QString filePath){
298   if(!filePath.contains("/") && !filePath.endsWith(".conf") && !filePath.isEmpty()){
299     //relative theme name, auto-complete it
300     QStringList dirs;
301     dirs << getenv("XDG_CONFIG_HOME");
302     dirs << QString(getenv("XDG_CONFIG_DIRS")).split(":");
303     dirs << QString(getenv("XDG_DATA_DIRS")).split(":");
304     QString relpath = "/lthemeengine/colors/%1.conf";
305     relpath = relpath.arg(filePath);
306     for(int i=0; i<dirs.length(); i++){
307       if(QFile::exists(dirs[i]+relpath)){ filePath = dirs[i]+relpath; break; }
308     }
309   }
310 
311   QPalette customPalette;
312   QSettings settings(filePath, QSettings::IniFormat);
313   settings.beginGroup("ColorScheme");
314   QStringList activeColors = settings.value("active_colors").toStringList();
315   QStringList inactiveColors = settings.value("inactive_colors").toStringList();
316   QStringList disabledColors = settings.value("disabled_colors").toStringList();
317   settings.endGroup();
318   if(activeColors.count() <= QPalette::NColorRoles && inactiveColors.count() <= QPalette::NColorRoles && disabledColors.count() <= QPalette::NColorRoles){
319     for (int i = 0; i < QPalette::NColorRoles && i<activeColors.count(); i++){
320       QPalette::ColorRole role = QPalette::ColorRole(i);
321       customPalette.setColor(QPalette::Active, role, QColor(activeColors.at(i)));
322       customPalette.setColor(QPalette::Inactive, role, QColor(inactiveColors.at(i)));
323       customPalette.setColor(QPalette::Disabled, role, QColor(disabledColors.at(i)));
324       }
325     }
326   else{ customPalette = *QPlatformTheme::palette(SystemPalette); } //load fallback palette
327   return customPalette;
328 }
329 
syncMouseCursorTheme(QString indexfile)330 void lthemeenginePlatformTheme::syncMouseCursorTheme(QString indexfile){
331   //Read the index file and pull out the theme name
332   QFile file(indexfile);
333   QString newtheme;
334   if(file.open(QIODevice::ReadOnly)){
335     QTextStream stream(&file);
336     QString tmp;
337     while(!stream.atEnd()){
338       tmp = stream.readLine().simplified();
339       if(tmp.startsWith("Inherits=")){ newtheme = tmp.section("=",1,-1).simplified(); break; }
340     }
341     file.close();
342   }
343   if(newtheme.isEmpty()){ return; } //nothing to do
344   QString curtheme = QString(XcursorGetTheme(QX11Info::display()) ); //currently-used theme
345   //qDebug() << "Sync Mouse Cursur Theme:" << curtheme << newtheme;
346   if(curtheme!=newtheme){
347     qDebug() << " - Setting new cursor theme:" << newtheme;
348     XcursorSetTheme(QX11Info::display(), newtheme.toLocal8Bit().data()); //save the new theme name
349   }else{
350     return;
351   }
352   //qDebug() << "Qt Stats:";
353   //qDebug() << " TopLevelWindows:" << QGuiApplication::topLevelWindows().length();
354   //qDebug() << " AllWindows:" << QGuiApplication::allWindows().length();
355   //qDebug() << " AllWidgets:" << QApplication::allWidgets().length();
356 
357   //XcursorSetThemeCore( QX11Info::display(), XcursorGetThemeCore(QX11Info::display()) ); //reset the theme core
358   //Load the cursors from the new theme
359   int defsize = XcursorGetDefaultSize(QX11Info::display());
360   //qDebug() << "Default cursor size:" << defsize;
361   XcursorImages *imgs = XcursorLibraryLoadImages("left_ptr", NULL, defsize);
362   //qDebug() << "imgs:" << imgs << imgs->nimage;
363   XcursorCursors *curs = XcursorImagesLoadCursors(QX11Info::display(), imgs);
364   if(curs==0){ return; } //not found
365   //qDebug() << "Got Cursors:" << curs->ncursor;
366   //Now re-set the cursors for the current top-level X windows
367   QWindowList wins = QGuiApplication::allWindows(); //QGuiApplication::topLevelWindows();
368   //qDebug() << "Got Windows:" << wins.length();
369   for(int i=0; i<curs->ncursor; i++){
370     for(int w=0; w<wins.length(); w++){
371       XDefineCursor(curs->dpy, wins[w]->winId(), curs->cursors[i]);
372     }
373   }
374   XcursorCursorsDestroy(curs); //finished with this temporary structure
375 
376   /*QWidgetList wlist = QApplication::allWidgets();
377   qDebug() << "Widget List:" << wlist.length();
378   for(int i=0; i<wlist.length(); i++){
379     QCursor cur(wlist[i]->cursor().shape());
380     wlist[i]->cursor().swap( cur );
381   }*/
382 }
383