From 2d24bce243cc9aa931101ea48d0f84da08e1256c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Gr=C3=A4=C3=9Flin?= Date: Fri, 19 Jun 2009 09:18:07 +0000 Subject: [PATCH] Initial import of Aurorae Kwin decoration theme engine. This theme engine uses SVG files to theme the decoration like the themes for Plasma desktop shell. svn path=/trunk/playground/artwork/aurorae/; revision=983819 --- clients/aurorae/AUTHORS | 1 + clients/aurorae/CMakeLists.txt | 14 + clients/aurorae/README | 6 + clients/aurorae/TODO | 3 + clients/aurorae/src/CMakeLists.txt | 24 + clients/aurorae/src/aurorae.cpp | 714 ++++++++++++++++++ clients/aurorae/src/aurorae.desktop | 3 + clients/aurorae/src/aurorae.h | 132 ++++ clients/aurorae/src/config/config.cpp | 570 ++++++++++++++ clients/aurorae/src/config/config.h | 132 ++++ clients/aurorae/src/config/config.ui | 98 +++ clients/aurorae/src/themeconfig.cpp | 85 +++ clients/aurorae/src/themeconfig.h | 166 ++++ clients/aurorae/theme-description | 122 +++ .../themes/example-deco/CMakeLists.txt | 4 + .../aurorae/themes/example-deco/close.svgz | Bin 0 -> 2124 bytes .../themes/example-deco/decoration.svgz | Bin 0 -> 8257 bytes .../themes/example-deco/example-decorc | 27 + .../aurorae/themes/example-deco/maximize.svgz | Bin 0 -> 1980 bytes .../themes/example-deco/metadata.desktop | 12 + .../aurorae/themes/example-deco/minimize.svgz | Bin 0 -> 1822 bytes .../aurorae/themes/example-deco/restore.svgz | Bin 0 -> 1980 bytes 22 files changed, 2113 insertions(+) create mode 100644 clients/aurorae/AUTHORS create mode 100644 clients/aurorae/CMakeLists.txt create mode 100644 clients/aurorae/README create mode 100644 clients/aurorae/TODO create mode 100644 clients/aurorae/src/CMakeLists.txt create mode 100644 clients/aurorae/src/aurorae.cpp create mode 100644 clients/aurorae/src/aurorae.desktop create mode 100644 clients/aurorae/src/aurorae.h create mode 100644 clients/aurorae/src/config/config.cpp create mode 100644 clients/aurorae/src/config/config.h create mode 100644 clients/aurorae/src/config/config.ui create mode 100644 clients/aurorae/src/themeconfig.cpp create mode 100644 clients/aurorae/src/themeconfig.h create mode 100644 clients/aurorae/theme-description create mode 100644 clients/aurorae/themes/example-deco/CMakeLists.txt create mode 100644 clients/aurorae/themes/example-deco/close.svgz create mode 100644 clients/aurorae/themes/example-deco/decoration.svgz create mode 100644 clients/aurorae/themes/example-deco/example-decorc create mode 100644 clients/aurorae/themes/example-deco/maximize.svgz create mode 100644 clients/aurorae/themes/example-deco/metadata.desktop create mode 100644 clients/aurorae/themes/example-deco/minimize.svgz create mode 100644 clients/aurorae/themes/example-deco/restore.svgz diff --git a/clients/aurorae/AUTHORS b/clients/aurorae/AUTHORS new file mode 100644 index 0000000000..4d113408ee --- /dev/null +++ b/clients/aurorae/AUTHORS @@ -0,0 +1 @@ +Martin Gräßlin kde [at] martin [minus] graesslin [dot] com Developer and Maintainer \ No newline at end of file diff --git a/clients/aurorae/CMakeLists.txt b/clients/aurorae/CMakeLists.txt new file mode 100644 index 0000000000..d8b5ce01e6 --- /dev/null +++ b/clients/aurorae/CMakeLists.txt @@ -0,0 +1,14 @@ +project(aurorae) +set(CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake/modules ) + +find_package(KDE4 4.2.92 REQUIRED) +find_package(KDE4Workspace REQUIRED) +include (KDE4Defaults) +include (MacroLibrary) + +add_definitions(${QT_DEFINITIONS} ${KDE4_DEFINITIONS}) +include_directories (${CMAKE_SOURCE_DIR} ${CMAKE_BINARY_DIR} ${KDE4_INCLUDES}) + +add_subdirectory(src) +add_subdirectory(themes/example-deco) + diff --git a/clients/aurorae/README b/clients/aurorae/README new file mode 100644 index 0000000000..72e833a10b --- /dev/null +++ b/clients/aurorae/README @@ -0,0 +1,6 @@ +Aurorae is a themeable window decoration for KWin. + +It supports theme files consisting of several SVG files for decoration and buttons. Themes can be +installed and selected directly in the configuration module of KWin decorations. + +Please have a look at theme-description on how to write a theme file. \ No newline at end of file diff --git a/clients/aurorae/TODO b/clients/aurorae/TODO new file mode 100644 index 0000000000..4e30caa0dc --- /dev/null +++ b/clients/aurorae/TODO @@ -0,0 +1,3 @@ + * Button positions are not updated after theme change + * Delete themes from selection + * Get Hot New Stuff support \ No newline at end of file diff --git a/clients/aurorae/src/CMakeLists.txt b/clients/aurorae/src/CMakeLists.txt new file mode 100644 index 0000000000..6b580b96d4 --- /dev/null +++ b/clients/aurorae/src/CMakeLists.txt @@ -0,0 +1,24 @@ +########### decoration ############### + +set(kwin3_aurorae_PART_SRCS aurorae.cpp themeconfig.cpp) + +kde4_add_plugin(kwin3_aurorae ${kwin3_aurorae_PART_SRCS}) + +target_link_libraries(kwin3_aurorae ${KDE4_KDEUI_LIBS} ${KDE4_PLASMA_LIBS} ${KDE4WORKSPACE_KDECORATIONS_LIBS}) + +install(TARGETS kwin3_aurorae DESTINATION ${PLUGIN_INSTALL_DIR} ) + +########### install files ############### + +install( FILES aurorae.desktop DESTINATION ${DATA_INSTALL_DIR}/kwin ) + +########### config ############### +set(kwin_aurorae_config_PART_SRCS config/config.cpp themeconfig.cpp ) + +kde4_add_ui_files(kwin_aurorae_config_PART_SRCS config/config.ui) + +kde4_add_plugin(kwin_aurorae_config ${kwin_aurorae_config_PART_SRCS}) + +target_link_libraries(kwin_aurorae_config ${KDE4_KDEUI_LIBS} ${KDE4_KIO_LIBS} ${KDE4_PLASMA_LIBS} ${QT_QTGUI_LIBRARY}) + +install(TARGETS kwin_aurorae_config DESTINATION ${PLUGIN_INSTALL_DIR} ) diff --git a/clients/aurorae/src/aurorae.cpp b/clients/aurorae/src/aurorae.cpp new file mode 100644 index 0000000000..9b574d8d75 --- /dev/null +++ b/clients/aurorae/src/aurorae.cpp @@ -0,0 +1,714 @@ +/******************************************************************** +Copyright (C) 2009 Martin Gräßlin + +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 "aurorae.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include + +namespace Aurorae +{ + +AuroraeFactory::AuroraeFactory() + : QObject() + , KDecorationFactoryUnstable() +{ + init(); +} + +void AuroraeFactory::init() +{ + KConfig conf("auroraerc"); + KConfigGroup group(&conf, "Engine"); + + m_themeName = group.readEntry("ThemeName", "example-deco"); + + QString file("aurorae/themes/" + m_themeName + "/decoration.svg"); + QString path = KGlobal::dirs()->findResource("data", file); + if (path.isEmpty()) { + file += "z"; + path = KGlobal::dirs()->findResource("data", file); + } + if (path.isEmpty()) { + kDebug(1216) << "Could not find decoration svg: aborting"; + abort(); + } + m_frame.setImagePath(path); + m_frame.setCacheAllRenderedFrames(true); + m_frame.setEnabledBorders(Plasma::FrameSvg::AllBorders); + + // load the buttons + initButtonFrame("minimize"); + initButtonFrame("maximize"); + initButtonFrame("restore"); + initButtonFrame("close"); + initButtonFrame("alldesktops"); + initButtonFrame("keepabove"); + initButtonFrame("keepbelow"); + initButtonFrame("shade"); + initButtonFrame("help"); + + readThemeConfig(); +} + +void AuroraeFactory::readThemeConfig() +{ + // read config values + KConfig conf("aurorae/themes/" + m_themeName + "/" + m_themeName + "rc", KConfig::FullConfig, "data"); + m_themeConfig.load(&conf); +} + +AuroraeFactory::~AuroraeFactory() +{ + s_instance = NULL; +} + +AuroraeFactory *AuroraeFactory::instance() +{ + if (!s_instance) { + s_instance = new AuroraeFactory; + } + + return s_instance; +} + +bool AuroraeFactory::reset(unsigned long changed) +{ + // re-read config + m_frame.clearCache(); + m_buttons.clear(); + init(); + resetDecorations(changed); + return false; // need hard reset +} + +bool AuroraeFactory::supports(Ability ability) const +{ + switch (ability) { + case AbilityAnnounceButtons: + case AbilityUsesAlphaChannel: + case AbilityButtonMenu: + case AbilityButtonSpacer: + return true; + case AbilityButtonMinimize: + return m_buttons.contains("minimize"); + case AbilityButtonMaximize: + return m_buttons.contains("maximize") || m_buttons.contains("restore"); + case AbilityButtonClose: + return m_buttons.contains("close"); + case AbilityButtonAboveOthers: + return m_buttons.contains("keepabove"); + case AbilityButtonBelowOthers: + return m_buttons.contains("keepbelow"); + case AbilityButtonShade: + return m_buttons.contains("shade"); + case AbilityButtonOnAllDesktops: + return m_buttons.contains("alldesktops"); + case AbilityButtonHelp: + return m_buttons.contains("help"); + case AbilityProvidesShadow: + return m_themeConfig.shadow(); + default: + return false; + } +} + +KDecoration *AuroraeFactory::createDecoration(KDecorationBridge *bridge) +{ + AuroraeClient *client = new AuroraeClient(bridge, this); + return client->decoration(); +} + +void AuroraeFactory::initButtonFrame(const QString &button) +{ + QString file("aurorae/themes/" + m_themeName + "/" + button + ".svg"); + QString path = KGlobal::dirs()->findResource("data", file); + if (path.isEmpty()) { + // let's look for svgz + file.append("z"); + path = KGlobal::dirs()->findResource("data", file); + } + if (!path.isEmpty()) { + Plasma::FrameSvg *frame = new Plasma::FrameSvg(this); + frame->setImagePath(path); + frame->setCacheAllRenderedFrames(true); + frame->setEnabledBorders(Plasma::FrameSvg::NoBorder); + m_buttons[ button ] = frame; + } else { + kDebug(1216) << "No button for: " << button; + } +} + +Plasma::FrameSvg *AuroraeFactory::button(const QString &b) +{ + if (hasButton(b)) { + return m_buttons[ b ]; + } else { + return NULL; + } +} + + +AuroraeFactory *AuroraeFactory::s_instance = NULL; + +/******************************************************* +* Button +*******************************************************/ +AuroraeButton::AuroraeButton(ButtonType type, KCommonDecoration *parent) + : KCommonDecorationButton(type, parent) + , m_animationId(0) + , m_animationProgress(0.0) + , m_pressed(false) +{ + setAttribute(Qt::WA_NoSystemBackground); + connect(Plasma::Animator::self(), SIGNAL(customAnimationFinished(int)), this, SLOT(animationFinished(int))); +} + +void AuroraeButton::reset(unsigned long changed) +{ + Q_UNUSED(changed) +} + +void AuroraeButton::enterEvent(QEvent *event) +{ + Q_UNUSED(event) + if (m_animationId != 0) { + Plasma::Animator::self()->stopCustomAnimation(m_animationId); + } + m_animationProgress = 0.0; + int time = AuroraeFactory::instance()->themeConfig().animationTime(); + if (time != 0) { + m_animationId = Plasma::Animator::self()->customAnimation(40 / (1000.0 / qreal(time)), + time, Plasma::Animator::EaseInCurve, this, "animationUpdate"); + } + update(); +} + +void AuroraeButton::leaveEvent(QEvent *event) +{ + Q_UNUSED(event) + if (m_animationId != 0) { + Plasma::Animator::self()->stopCustomAnimation(m_animationId); + } + m_animationProgress = 0.0; + int time = AuroraeFactory::instance()->themeConfig().animationTime(); + if (time != 0) { + m_animationId = Plasma::Animator::self()->customAnimation(40 / (1000.0 / qreal(time)), + time, Plasma::Animator::EaseOutCurve, this, "animationUpdate"); + } + update(); +} + +void AuroraeButton::mousePressEvent(QMouseEvent *e) +{ + m_pressed = true; + update(); + KCommonDecorationButton::mousePressEvent(e); +} + +void AuroraeButton::mouseReleaseEvent(QMouseEvent *e) +{ + m_pressed = false; + update(); + KCommonDecorationButton::mouseReleaseEvent(e); +} + +void AuroraeButton::animationUpdate(double progress, int id) +{ + Q_UNUSED(id) + m_animationProgress = progress; + update(); +} + +void AuroraeButton::animationFinished(int id) +{ + if (m_animationId == id) { + m_animationId = 0; + update(); + } +} + +void AuroraeButton::paintEvent(QPaintEvent *event) +{ + Q_UNUSED(event) + if (decoration()->isPreview()) { + return; + } + + QPainter painter(this); + painter.setRenderHint(QPainter::Antialiasing); + + bool active = decoration()->isActive(); + + if (type() == MenuButton) { + const QIcon icon = decoration()->icon(); + const QSize size = icon.actualSize(QSize(16, 16)); + QPixmap iconPix = icon.pixmap(size); + KIconEffect *effect = KIconLoader::global()->iconEffect(); + if (active) { + if (underMouse()) { + iconPix = effect->apply(iconPix, KIconLoader::Desktop, KIconLoader::ActiveState); + } + } else { + iconPix = effect->apply(iconPix, KIconLoader::Desktop, KIconLoader::DisabledState); + } + painter.drawPixmap(0, 0, iconPix); + return; + } + + ButtonStates states; + if (active) { + states |= Active; + } + if (underMouse()) { + states |= Hover; + } + if (m_pressed) { + states |= Pressed; + } + QString buttonName = ""; + switch (type()) { + case MinButton: + if (!decoration()->isMinimizable()) { + states |= Deactivated; + } + buttonName = "minimize"; + break; + case CloseButton: + if (!decoration()->isCloseable()) { + states |= Deactivated; + } + buttonName = "close"; + break; + case MaxButton: { + if (!decoration()->isMaximizable()) { + states |= Deactivated; + } + buttonName = "maximize"; + if (decoration()->maximizeMode() == KDecorationDefines::MaximizeFull && + !decoration()->options()->moveResizeMaximizedWindows()) { + buttonName = "restore"; + if (!AuroraeFactory::instance()->hasButton(buttonName)) { + buttonName = "maximize"; + } + } + break; + } + case OnAllDesktopsButton: + if (decoration()->isOnAllDesktops()) { + states |= Hover; + } + buttonName = "alldesktops"; + break; + case AboveButton: + buttonName = "keepabove"; + break; + case BelowButton: + buttonName = "keepbelow"; + break; + case ShadeButton: + if (!decoration()->isShadeable()) { + states |= Deactivated; + } + if (decoration()->isShade()) { + states |= Hover; + } + buttonName = "shade"; + break; + case HelpButton: + buttonName = "help"; + break; + default: + buttonName = QString(); + } + + if (!buttonName.isEmpty()) { + if (AuroraeFactory::instance()->hasButton(buttonName)) { + Plasma::FrameSvg *button = AuroraeFactory::instance()->button(buttonName); + paintButton(painter, button, states); + } + } +} + +void AuroraeButton::paintButton(QPainter &painter, Plasma::FrameSvg *frame, ButtonStates states) +{ + QString prefix = "active"; + QString animationPrefix = "active"; + bool hasInactive = false; + // check for inactive prefix + if (!states.testFlag(Active) && frame->hasElementPrefix("inactive")) { + // we have inactive, so we use it + hasInactive = true; + prefix = "inactive"; + animationPrefix = "inactive"; + } + + if (states.testFlag(Hover)) { + if (states.testFlag(Active)) { + if (frame->hasElementPrefix("hover")) { + prefix = "hover"; + } + } else { + if (hasInactive) { + if (frame->hasElementPrefix("hover-inactive")) { + prefix = "hover-inactive"; + } + } else { + if (frame->hasElementPrefix("hover")) { + prefix = "hover"; + } + } + } + } + if (states.testFlag(Pressed)) { + if (states.testFlag(Active)) { + if (frame->hasElementPrefix("pressed")) { + prefix = "pressed"; + } + } else { + if (hasInactive) { + if (frame->hasElementPrefix("pressed-inactive")) { + prefix = "pressed-inactive"; + } + } else { + if (frame->hasElementPrefix("pressed")) { + prefix = "pressed"; + } + } + } + } + if (states.testFlag(Deactivated)) { + if (states.testFlag(Active)) { + if (frame->hasElementPrefix("deactivated")) { + prefix = "deactivated"; + } + } else { + if (hasInactive) { + if (frame->hasElementPrefix("deactivated-inactive")) { + prefix = "deactivated-inactive"; + } + } else { + if (frame->hasElementPrefix("deactivated")) { + prefix = "deactivated"; + } + } + } + } + frame->setElementPrefix(prefix); + frame->resizeFrame(size()); + if (m_animationId != 0) { + // there is an animation so we have to use it + // the animation is definately a hover animation as currently nothing else is supported + if (!states.testFlag(Hover)) { + // only have to set for not hover state as animationPrefix is set to (in)active by default + if (states.testFlag(Active)) { + if (frame->hasElementPrefix("hover")) { + animationPrefix = "hover"; + } + } else { + if (hasInactive) { + if (frame->hasElementPrefix("hover-inactive")) { + animationPrefix = "hover-inactive"; + } + } else { + if (frame->hasElementPrefix("hover")) { + animationPrefix = "hover"; + } + } + } + } + QPixmap target = frame->framePixmap(); + frame->setElementPrefix(animationPrefix); + frame->resizeFrame(size()); + QPixmap result = Plasma::PaintUtils::transition(frame->framePixmap(), + target, m_animationProgress); + painter.drawPixmap(rect(), result); + } else { + frame->paintFrame(&painter); + } +} + +/******************************************************* +* Client +*******************************************************/ + +AuroraeClient::AuroraeClient(KDecorationBridge *bridge, KDecorationFactory *factory) + : KCommonDecorationUnstable(bridge, factory) +{ +} + +AuroraeClient::~AuroraeClient() +{ +} + +void AuroraeClient::init() +{ + KCommonDecoration::init(); +} + +void AuroraeClient::reset(unsigned long changed) +{ + widget()->update(); + + KCommonDecoration::reset(changed); +} + +QString AuroraeClient::visibleName() const +{ + return QString("Aurorae Theme Engine"); +} + +QString AuroraeClient::defaultButtonsLeft() const +{ + return AuroraeFactory::instance()->themeConfig().defaultButtonsLeft(); +} + +QString AuroraeClient::defaultButtonsRight() const +{ + return AuroraeFactory::instance()->themeConfig().defaultButtonsRight(); +} + +bool AuroraeClient::decorationBehaviour(DecorationBehaviour behavior) const +{ + switch (behavior) { + case DB_MenuClose: + return true; // Close on double click + + case DB_WindowMask: + case DB_ButtonHide: + return false; + default: + return false; + } +} + +int AuroraeClient::layoutMetric(LayoutMetric lm, bool respectWindowState, + const KCommonDecorationButton *button) const +{ + bool maximized = maximizeMode() == MaximizeFull && + !options()->moveResizeMaximizedWindows(); + const ThemeConfig &conf = AuroraeFactory::instance()->themeConfig(); + switch (lm) { + case LM_BorderLeft: + return maximized && respectWindowState ? 0 : conf.borderLeft(); + case LM_BorderRight: + return maximized && respectWindowState ? 0 : conf.borderRight(); + case LM_BorderBottom: + return maximized && respectWindowState ? 0 : conf.borderBottom(); + + case LM_OuterPaddingLeft: + return conf.paddingLeft(); + case LM_OuterPaddingRight: + return conf.paddingRight(); + case LM_OuterPaddingTop: + return conf.paddingTop(); + case LM_OuterPaddingBottom: + return conf.paddingBottom(); + + case LM_TitleEdgeLeft: + return conf.titleEdgeLeft(); + case LM_TitleEdgeRight: + return conf.titleBorderRight(); + case LM_TitleEdgeTop: + return conf.titleEdgeTop(); + case LM_TitleEdgeBottom: + return conf.titleEdgeBottom(); + + case LM_ButtonMarginTop: + return conf.buttonMarginTop(); + + case LM_TitleBorderLeft: + return conf.titleBorderLeft(); + case LM_TitleBorderRight: + return conf.titleBorderRight(); + case LM_TitleHeight: + return conf.titleHeight(); + + case LM_ButtonWidth: + return conf.buttonWidth(); + case LM_ButtonHeight: + return conf.buttonHeight(); + case LM_ButtonSpacing: + return conf.buttonSpacing(); + case LM_ExplicitButtonSpacer: + return conf.explicitButtonSpacer(); + + default: + return KCommonDecoration::layoutMetric(lm, respectWindowState, button); + } +} + +KCommonDecorationButton *AuroraeClient::createButton(ButtonType type) +{ + AuroraeFactory *factory = AuroraeFactory::instance(); + switch (type) { + case MenuButton: + return new AuroraeButton(type, this); + case MinButton: + if (factory->hasButton("minimize")) { + return new AuroraeButton(type, this); + } else { + return NULL; + } + case MaxButton: + if (factory->hasButton("maximize") || factory->hasButton("restore")) { + return new AuroraeButton(type, this); + } else { + return NULL; + } + case CloseButton: + if (factory->hasButton("close")) { + return new AuroraeButton(type, this); + } else { + return NULL; + } + case OnAllDesktopsButton: + if (factory->hasButton("alldesktops")) { + return new AuroraeButton(type, this); + } else { + return NULL; + } + case HelpButton: + if (factory->hasButton("help")) { + return new AuroraeButton(type, this); + } else { + return NULL; + } + case AboveButton: + if (factory->hasButton("keepabove")) { + return new AuroraeButton(type, this); + } else { + return NULL; + } + case BelowButton: + if (factory->hasButton("keepbelow")) { + return new AuroraeButton(type, this); + } else { + return NULL; + } + case ShadeButton: + if (factory->hasButton("shade")) { + return new AuroraeButton(type, this); + } else { + return NULL; + } + default: + return NULL; + } +} + +void AuroraeClient::paintEvent(QPaintEvent *event) +{ + Q_UNUSED(event); + if (isPreview()) { + return; + } + bool maximized = maximizeMode() == MaximizeFull && + !options()->moveResizeMaximizedWindows(); + + QPainter painter(widget()); + painter.setCompositionMode(QPainter::CompositionMode_Source); + + const ThemeConfig &conf = AuroraeFactory::instance()->themeConfig(); + Plasma::FrameSvg *frame = AuroraeFactory::instance()->frame(); + + frame->setElementPrefix("decoration"); + if (!isActive() && frame->hasElementPrefix("decoration-inactive")) { + frame->setElementPrefix("decoration-inactive"); + } + if (!compositingActive() && frame->hasElementPrefix("decoration-opaque")) { + frame->setElementPrefix("decoration-opaque"); + if (!isActive() && frame->hasElementPrefix("decoration-opaque-inactive")) { + frame->setElementPrefix("decoration-opaque-inactive"); + } + } + + // top + if (maximized) { + frame->setEnabledBorders(Plasma::FrameSvg::NoBorder); + } else { + frame->setEnabledBorders(Plasma::FrameSvg::AllBorders); + } + QRectF rect = QRectF(0.0, 0.0, widget()->width(), widget()->height()); + QRectF sourceRect = rect; + if (!compositingActive()) { + rect = QRectF(conf.paddingLeft(), conf.paddingTop(), + widget()->width()-conf.paddingRight()-conf.paddingLeft(), + widget()->height()-conf.paddingBottom()-conf.paddingTop()); + sourceRect = QRectF(0.0, 0.0, rect.width(), rect.height()); + } + frame->resizeFrame(rect.size()); + frame->paintFrame(&painter, rect, sourceRect); + + if (isActive()) { + painter.setPen(conf.activeTextColor()); + } else { + painter.setPen(conf.inactiveTextColor()); + } + painter.setFont(options()->font(isActive())); + painter.drawText(titleRect(), conf.alignment() | conf.verticalAlignment() | Qt::TextSingleLine, + caption()); +} + +void AuroraeClient::updateWindowShape() +{ + bool maximized = maximizeMode()==KDecorationDefines::MaximizeFull && !options()->moveResizeMaximizedWindows(); + int w=widget()->width(); + int h=widget()->height(); + + if (maximized) { + QRegion mask(0,0,w,h); + setMask(mask); + return; + } + + const ThemeConfig &conf = AuroraeFactory::instance()->themeConfig(); + Plasma::FrameSvg *deco = AuroraeFactory::instance()->frame(); + if (!deco->hasElementPrefix("decoration-opaque")) { + // opaque element is missing: set generic mask + QRegion mask(0,0,w,h); + setMask(mask); + return; + } + deco->setElementPrefix("decoration-opaque"); + deco->resizeFrame(QSize(w-conf.paddingLeft()-conf.paddingRight(), + h-conf.paddingTop()-conf.paddingBottom())); + QRegion mask = deco->mask().translated(conf.paddingLeft(), conf.paddingTop()); + setMask(mask); +} + +} // namespace Aurorae + +extern "C" +{ + KDE_EXPORT KDecorationFactory *create_factory() { + return Aurorae::AuroraeFactory::instance(); + } +} + + +#include "aurorae.moc" diff --git a/clients/aurorae/src/aurorae.desktop b/clients/aurorae/src/aurorae.desktop new file mode 100644 index 0000000000..fed257190f --- /dev/null +++ b/clients/aurorae/src/aurorae.desktop @@ -0,0 +1,3 @@ +[Desktop Entry] +Name=Aurorae Decoration Theme Engine +X-KDE-Library=kwin3_aurorae diff --git a/clients/aurorae/src/aurorae.h b/clients/aurorae/src/aurorae.h new file mode 100644 index 0000000000..e9485975c5 --- /dev/null +++ b/clients/aurorae/src/aurorae.h @@ -0,0 +1,132 @@ +/******************************************************************** +Copyright (C) 2009 Martin Gräßlin + +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 . +*********************************************************************/ + +#ifndef AURORAE_H +#define AURORAE_H + +#include "themeconfig.h" + +#include +#include +#include + +class KComponentData; + +namespace Aurorae +{ + +class AuroraeFactory : public QObject, public KDecorationFactoryUnstable +{ +public: + ~AuroraeFactory(); + + static AuroraeFactory* instance(); + bool reset(unsigned long changed); + KDecoration *createDecoration(KDecorationBridge*); + bool supports(Ability ability) const; + + Plasma::FrameSvg *frame() { + return &m_frame; + } + Plasma::FrameSvg *button(const QString& b); + bool hasButton(const QString& button) { + return m_buttons.contains(button); + } + ThemeConfig &themeConfig() { + return m_themeConfig; + } + +private: + AuroraeFactory(); + void init(); + void initButtonFrame(const QString& button); + void readThemeConfig(); + +private: + static AuroraeFactory *s_instance; + + // theme name + QString m_themeName; + ThemeConfig m_themeConfig; + // deco + Plasma::FrameSvg m_frame; + + // buttons + QHash< QString, Plasma::FrameSvg* > m_buttons; +}; + +class AuroraeButton : public KCommonDecorationButton +{ + Q_OBJECT + +public: + AuroraeButton(ButtonType type, KCommonDecoration *parent); + void reset(unsigned long changed); + void enterEvent(QEvent *event); + void leaveEvent(QEvent *event); + void paintEvent(QPaintEvent *event); + +public slots: + void animationUpdate(qreal progress, int id); + void animationFinished(int id); + +protected: + void mousePressEvent(QMouseEvent *e); + void mouseReleaseEvent(QMouseEvent *e); + +private: + enum ButtonState { + Active = 0x1, + Hover = 0x2, + Pressed = 0x4, + Deactivated = 0x8 + }; + Q_DECLARE_FLAGS(ButtonStates, ButtonState); + void paintButton(QPainter& painter, Plasma::FrameSvg* frame, ButtonStates states); + +private: + int m_animationId; + qreal m_animationProgress; + bool m_pressed; +}; + + +class AuroraeClient : public KCommonDecorationUnstable +{ +public: + AuroraeClient(KDecorationBridge *bridge, KDecorationFactory *factory); + ~AuroraeClient(); + + virtual void init(); + + virtual QString visibleName() const; + virtual QString defaultButtonsLeft() const; + virtual QString defaultButtonsRight() const; + virtual bool decorationBehaviour(DecorationBehaviour behaviour) const; + virtual int layoutMetric(LayoutMetric lm, bool respectWindowState = true, + const KCommonDecorationButton * = 0) const; + virtual KCommonDecorationButton *createButton(ButtonType type); + virtual void updateWindowShape(); + +protected: + void reset(unsigned long changed); + void paintEvent(QPaintEvent *event); +}; + +} + +#endif diff --git a/clients/aurorae/src/config/config.cpp b/clients/aurorae/src/config/config.cpp new file mode 100644 index 0000000000..293fe613ff --- /dev/null +++ b/clients/aurorae/src/config/config.cpp @@ -0,0 +1,570 @@ +/******************************************************************** +Copyright (C) 2009 Martin Gräßlin + +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 "config.h" +#include "themeconfig.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +extern "C" +{ + KDE_EXPORT QObject *allocate_config(KConfig *conf, QWidget *parent) + { + return (new Aurorae::AuroraeConfig(conf, parent)); + } +} + +namespace Aurorae +{ + +//Theme selector code by Andre Duffeck (modified to add package description) +ThemeModel::ThemeModel(QObject *parent) + : QAbstractListModel(parent) +{ + reload(); +} + +ThemeModel::~ThemeModel() +{ + clearThemeList(); +} + +void ThemeModel::clearThemeList() +{ + foreach(const ThemeInfo &themeInfo, m_themes) { + delete themeInfo.svg; + } + m_themes.clear(); +} + +void ThemeModel::reload() +{ + reset(); + clearThemeList(); + + // get all desktop themes + QStringList themes = KGlobal::dirs()->findAllResources("data", + "aurorae/themes/*/metadata.desktop", + KStandardDirs::NoDuplicates); + foreach(const QString &theme, themes) { + int themeSepIndex = theme.lastIndexOf('/', -1); + QString themeRoot = theme.left(themeSepIndex); + int themeNameSepIndex = themeRoot.lastIndexOf('/', -1); + QString packageName = themeRoot.right(themeRoot.length() - themeNameSepIndex - 1); + + KDesktopFile df(theme); + QString name = df.readName(); + if (name.isEmpty()) { + name = packageName; + } + QString comment = df.readComment(); + QString author = df.desktopGroup().readEntry("X-KDE-PluginInfo-Author", QString()); + QString email = df.desktopGroup().readEntry("X-KDE-PluginInfo-Email", QString()); + QString version = df.desktopGroup().readEntry("X-KDE-PluginInfo-Version", QString()); + QString license = df.desktopGroup().readEntry("X-KDE-PluginInfo-License", QString()); + QString website = df.desktopGroup().readEntry("X-KDE-PluginInfo-Website", QString()); + + + Plasma::FrameSvg *svg = new Plasma::FrameSvg(this); + QString svgFile = themeRoot + "/decoration.svg"; + if (QFile::exists(svgFile)) { + svg->setImagePath(svgFile); + } else { + svg->setImagePath(svgFile + "z"); + } + svg->setEnabledBorders(Plasma::FrameSvg::AllBorders); + + ThemeConfig *config = new ThemeConfig(); + KConfig conf("aurorae/themes/" + packageName + "/" + packageName + "rc", KConfig::FullConfig, "data"); + config->load(&conf); + + // buttons + QHash *buttons = new QHash(); + initButtonFrame("minimize", packageName, buttons); + initButtonFrame("maximize", packageName, buttons); + initButtonFrame("restore", packageName, buttons); + initButtonFrame("close", packageName, buttons); + initButtonFrame("alldesktops", packageName, buttons); + initButtonFrame("keepabove", packageName, buttons); + initButtonFrame("keepbelow", packageName, buttons); + initButtonFrame("shade", packageName, buttons); + initButtonFrame("help", packageName, buttons); + + ThemeInfo info; + info.package = packageName; + info.description = comment; + info.author = author; + info.email = email; + info.version = version; + info.website = website; + info.license = license; + info.svg = svg; + info.themeRoot = themeRoot; + info.themeConfig = config; + info.buttons = buttons; + m_themes[name] = info; + } + + beginInsertRows(QModelIndex(), 0, m_themes.size()); + endInsertRows(); +} + +int ThemeModel::rowCount(const QModelIndex &) const +{ + return m_themes.size(); +} + +QVariant ThemeModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) { + return QVariant(); + } + + if (index.row() >= m_themes.size()) { + return QVariant(); + } + + QMap::const_iterator it = m_themes.constBegin(); + for (int i = 0; i < index.row(); ++i) { + ++it; + } + + switch (role) { + case Qt::DisplayRole: + return it.key(); + case PackageNameRole: + return (*it).package; + case SvgRole: + return qVariantFromValue((void*)(*it).svg); + case PackageDescriptionRole: + return (*it).description; + case PackageAuthorRole: + return (*it).author; + case PackageVersionRole: + return (*it).version; + case PackageEmailRole: + return (*it).email; + case PackageLicenseRole: + return (*it).license; + case PackageWebsiteRole: + return (*it).website; + case ThemeConfigRole: + return qVariantFromValue((void*)(*it).themeConfig); + case ButtonsRole: + return qVariantFromValue((void*)(*it).buttons); + default: + return QVariant(); + } +} + +int ThemeModel::indexOf(const QString &name) const +{ + QMapIterator it(m_themes); + int i = -1; + while (it.hasNext()) { + ++i; + if (it.next().value().package == name) { + return i; + } + } + + return -1; +} + +void ThemeModel::initButtonFrame(const QString &button, const QString &themeName, QHash *buttons) +{ + QString file("aurorae/themes/" + themeName + "/" + button + ".svg"); + QString path = KGlobal::dirs()->findResource("data", file); + if (path.isEmpty()) { + // let's look for svgz + file.append("z"); + path = KGlobal::dirs()->findResource("data", file); + } + if (!path.isEmpty()) { + Plasma::FrameSvg *frame = new Plasma::FrameSvg(this); + frame->setImagePath(path); + frame->setCacheAllRenderedFrames(true); + frame->setEnabledBorders(Plasma::FrameSvg::NoBorder); + buttons->insert(button, frame); + } +} + +/////////////////////////////////////////////////// +// ThemeDelegate +////////////////////////////////////////////////// +ThemeDelegate::ThemeDelegate(QObject* parent) + : QAbstractItemDelegate(parent) +{ +} + +void ThemeDelegate::paint(QPainter *painter, + const QStyleOptionViewItem &option, + const QModelIndex &index) const +{ + QString title = index.model()->data(index, Qt::DisplayRole).toString(); + + // highlight selected item + painter->save(); + if (option.state & QStyle::State_Selected) { + painter->setBrush(option.palette.color(QPalette::Highlight)); + } else { + painter->setBrush(Qt::gray); + } + painter->drawRect(option.rect); + painter->restore(); + + ThemeConfig *themeConfig = static_cast( + index.model()->data(index, ThemeModel::ThemeConfigRole).value()); + painter->save(); + paintDeco(painter, false, option, index, 5 + themeConfig->paddingLeft() + themeConfig->borderLeft(), + 5, 5, 5 + themeConfig->paddingBottom() + themeConfig->borderBottom() ); + painter->restore(); + painter->save(); + int activeLeft = 5; + int activeTop = 5 + themeConfig->paddingTop() + themeConfig->titleEdgeTop() + + themeConfig->titleEdgeBottom() + themeConfig->titleHeight(); + int activeRight = 5 + themeConfig->paddingRight() + themeConfig->borderRight(); + int activeBottom = 5; + paintDeco(painter, true, option, index, activeLeft, activeTop, activeRight, activeBottom); + painter->restore(); + + // paint title + painter->save(); + QFont font = painter->font(); + font.setWeight(QFont::Bold); + painter->setPen(themeConfig->activeTextColor()); + painter->setFont(font); + painter->drawText(QRect(option.rect.topLeft() + QPoint(activeLeft, activeTop), + option.rect.bottomRight() - QPoint(activeRight, activeBottom)), + Qt::AlignCenter | Qt::TextWordWrap, title); + painter->restore(); +} + +void ThemeDelegate::paintDeco(QPainter *painter, bool active, const QStyleOptionViewItem &option, const QModelIndex &index, + int leftMargin, int topMargin, + int rightMargin, int bottomMargin) const +{ + Plasma::FrameSvg *svg = static_cast( + index.model()->data(index, ThemeModel::SvgRole).value()); + svg->setElementPrefix("decoration"); + if (!active && svg->hasElementPrefix("decoration-inactive")) { + svg->setElementPrefix("decoration-inactive"); + } + svg->resizeFrame(QSize(option.rect.width() - leftMargin - rightMargin, option.rect.height() - topMargin - bottomMargin)); + svg->paintFrame(painter, option.rect.topLeft() + QPoint(leftMargin, topMargin)); + + ThemeConfig *themeConfig = static_cast( + index.model()->data(index, ThemeModel::ThemeConfigRole).value()); + + QHash *buttons = static_cast *>( + index.model()->data(index, ThemeModel::ButtonsRole).value()); + int y = option.rect.top() + topMargin + themeConfig->paddingTop() + themeConfig->titleEdgeTop() + themeConfig->buttonMarginTop(); + int x = option.rect.left() + leftMargin + themeConfig->paddingLeft() + themeConfig->titleEdgeLeft(); + int buttonWidth = themeConfig->buttonWidth(); + int buttonHeight = themeConfig->buttonHeight(); + foreach (const QChar &character, themeConfig->defaultButtonsLeft()) { + QString buttonName; + if (character == '_'){ + x += themeConfig->explicitButtonSpacer() + themeConfig->buttonSpacing(); + continue; + } + else if (character == 'M') { + KIcon icon = KIcon( "xorg" ); + QSize buttonSize(buttonWidth,buttonHeight); + painter->drawPixmap(QPoint(x,y), icon.pixmap(buttonSize)); + x += buttonWidth; + } + else if (character == 'S') { + buttonName = "alldesktops"; + } + else if (character == 'H') { + buttonName = "help"; + } + else if (character == 'I') { + buttonName = "minimize"; + } + else if (character == 'A') { + buttonName = "restore"; + if (!buttons->contains(buttonName)) { + buttonName = "maximize"; + } + } + else if (character == 'X') { + buttonName = "close"; + } + else if (character == 'F') { + buttonName = "keepabove"; + } + else if (character == 'B') { + buttonName = "keepbelow"; + } + else if (character == 'L') { + buttonName = "shade"; + } + if (!buttonName.isEmpty() && buttons->contains(buttonName)) { + Plasma::FrameSvg *frame = buttons->value(buttonName); + frame->setElementPrefix("active"); + if (!active && frame->hasElementPrefix("inactive")) { + frame->setElementPrefix("inactive"); + } + frame->resizeFrame(QSize(buttonWidth,buttonHeight)); + frame->paintFrame(painter, QPoint(x, y)); + x += buttonWidth; + } + x += themeConfig->buttonSpacing(); + } + if (!themeConfig->defaultButtonsLeft().isEmpty()) { + x -= themeConfig->buttonSpacing(); + } + int titleLeft = x; + + x = option.rect.right() - rightMargin - themeConfig->paddingRight() - themeConfig->titleEdgeRight() - buttonWidth; + QString rightButtons; + foreach (const QChar &character, themeConfig->defaultButtonsRight()) { + rightButtons.prepend(character); + } + foreach (const QChar &character, rightButtons) { + QString buttonName; + if (character == '_'){ + x -= themeConfig->explicitButtonSpacer() + themeConfig->buttonSpacing(); + continue; + } + else if (character == 'M') { + KIcon icon = KIcon( "xorg" ); + QSize buttonSize(buttonWidth,buttonHeight); + painter->drawPixmap(QPoint(x,y), icon.pixmap(buttonSize)); + x -= buttonWidth; + } + else if (character == 'S') { + buttonName = "alldesktops"; + } + else if (character == 'H') { + buttonName = "help"; + } + else if (character == 'I') { + buttonName = "minimize"; + } + else if (character == 'A') { + buttonName = "restore"; + if (!buttons->contains(buttonName)) { + buttonName = "maximize"; + } + } + else if (character == 'X') { + buttonName = "close"; + } + else if (character == 'F') { + buttonName = "keepabove"; + } + else if (character == 'B') { + buttonName = "keepbelow"; + } + else if (character == 'L') { + buttonName = "shade"; + } + if (!buttonName.isEmpty() && buttons->contains(buttonName)) { + Plasma::FrameSvg *frame = buttons->value(buttonName); + frame->setElementPrefix("active"); + if (!active && frame->hasElementPrefix("inactive")) { + frame->setElementPrefix("inactive"); + } + frame->resizeFrame(QSize(buttonWidth,buttonHeight)); + frame->paintFrame(painter, QPoint(x, y)); + x -= buttonWidth; + } + x -= themeConfig->buttonSpacing(); + } + x += buttonWidth; + if (!rightButtons.isEmpty()){ + x += themeConfig->buttonSpacing(); + } + int titleRight = x; + + // draw text + painter->save(); + if (active) { + painter->setPen(themeConfig->activeTextColor()); + } + else { + painter->setPen(themeConfig->inactiveTextColor()); + } + y = option.rect.top() + topMargin + themeConfig->paddingTop() + themeConfig->titleEdgeTop(); + QRectF titleRect(QPointF(titleLeft, y), QPointF(titleRight, y + themeConfig->titleHeight())); + QString caption = i18n("Active Window"); + if (!active) { + caption = i18n("Inactive Window"); + } + painter->drawText(titleRect, + themeConfig->alignment() | themeConfig->verticalAlignment() | Qt::TextSingleLine, + caption); + painter->restore(); +} + +QSize ThemeDelegate::sizeHint(const QStyleOptionViewItem &, const QModelIndex &) const +{ + return QSize(400, 200); +} + +/////////////////////////////////////////////////// +// AuroraeConfig +////////////////////////////////////////////////// + +AuroraeConfig::AuroraeConfig(KConfig* conf, QWidget* parent) + : QObject(parent) + , m_parent(parent) +{ + Q_UNUSED(conf) + m_ui = new AuroraeConfigUI(parent); + m_ui->aboutPushButton->setIcon(KIcon("dialog-information")); + + m_themeModel = new ThemeModel(this); + m_ui->theme->setModel(m_themeModel); + m_ui->theme->setItemDelegate(new ThemeDelegate(m_ui->theme->view())); + m_ui->theme->setMinimumSize(400, m_ui->theme->sizeHint().height()); + + m_config = new KConfig("auroraerc"); + KConfigGroup group(m_config, "Engine"); + load(group); + + connect(m_ui->theme, SIGNAL(currentIndexChanged(int)), this, SIGNAL(changed())); + connect(m_ui->installNewThemeButton, SIGNAL(clicked(bool)), this, SLOT(slotInstallNewTheme())); + connect(m_ui->aboutPushButton, SIGNAL(clicked(bool)), this, SLOT(slotAboutClicked())); + m_ui->show(); +} + +AuroraeConfig::AuroraeConfig::~AuroraeConfig() +{ + delete m_ui; + delete m_config; +} + +void AuroraeConfig::defaults() +{ + m_ui->theme->setCurrentIndex(m_themeModel->indexOf("example-deco")); +} + +void AuroraeConfig::load(const KConfigGroup &conf) +{ + QString theme = conf.readEntry("ThemeName", "example-deco"); + m_ui->theme->setCurrentIndex(m_themeModel->indexOf(theme)); +} + +void AuroraeConfig::save(KConfigGroup &conf) +{ + Q_UNUSED(conf) + KConfigGroup group(m_config, "Engine"); + int index = m_ui->theme->currentIndex(); + QString theme = m_ui->theme->itemData(index, ThemeModel::PackageNameRole).toString(); + group.writeEntry("ThemeName", theme); + group.sync(); +} + +void AuroraeConfig::slotAboutClicked() +{ + int index = m_ui->theme->currentIndex(); + const QString name = m_ui->theme->itemData(index, Qt::DisplayRole).toString(); + const QString comment = m_ui->theme->itemData(index, ThemeModel::PackageDescriptionRole).toString(); + const QString author = m_ui->theme->itemData(index, ThemeModel::PackageAuthorRole).toString(); + const QString email = m_ui->theme->itemData(index, ThemeModel::PackageEmailRole).toString(); + const QString website = m_ui->theme->itemData(index, ThemeModel::PackageWebsiteRole).toString(); + const QString version = m_ui->theme->itemData(index, ThemeModel::PackageVersionRole).toString(); + const QString license = m_ui->theme->itemData(index, ThemeModel::PackageLicenseRole).toString(); + + KAboutData aboutData(name.toUtf8(), name.toUtf8(), ki18n(name.toUtf8()), version.toUtf8(), ki18n(comment.toUtf8()), KAboutLicense::byKeyword(license).key(), ki18n(QByteArray()), ki18n(QByteArray()), website.toLatin1()); + aboutData.setProgramIconName("preferences-system-windows-action"); + const QStringList authors = author.split(','); + const QStringList emails = email.split(','); + int i = 0; + if (authors.count() == emails.count()) { + foreach(const QString &author, authors) { + if (!author.isEmpty()) { + aboutData.addAuthor(ki18n(author.toUtf8()), ki18n(QByteArray()), emails[i].toUtf8(), 0); + } + i++; + } + } + KAboutApplicationDialog aboutPlugin(&aboutData, m_parent); + aboutPlugin.exec(); +} + +void AuroraeConfig::slotInstallNewTheme() +{ + KUrl themeURL = KUrlRequesterDialog::getUrl(QString(), m_parent, + i18n("Drag or Type Theme URL")); + if (themeURL.url().isEmpty()) { + return; + } + + // themeTmpFile contains the name of the downloaded file + QString themeTmpFile; + + if (!KIO::NetAccess::download(themeURL, themeTmpFile, m_parent)) { + QString sorryText; + if (themeURL.isLocalFile()) { + sorryText = i18n("Unable to find the theme archive %1.", themeURL.prettyUrl()); + } + else { + sorryText = i18n("Unable to download theme archive;\n" + "please check that address %1 is correct.", themeURL.prettyUrl()); + } + KMessageBox::sorry(m_parent, sorryText); + return ; + } + + // TODO: check if archive contains a valid theme + // TODO: show a progress dialog + KTar archive(themeTmpFile); + archive.open(QIODevice::ReadOnly); + const KArchiveDirectory* themeDir = archive.directory(); + QString localThemesDir = KStandardDirs::locateLocal("data", "aurorae/themes/"); + foreach(const QString& entry, themeDir->entries()) { + // entry has to be a directory to contain a theme + const KArchiveEntry* possibleEntry = themeDir->entry(entry); + if (possibleEntry->isDirectory()) { + const KArchiveDirectory* dir = dynamic_cast(possibleEntry); + if (dir) { + dir->copyTo(localThemesDir + dir->name()); + } + } + } + // and reload + int index = m_ui->theme->currentIndex(); + const QString themeName = m_ui->theme->itemData(index, ThemeModel::PackageNameRole).toString(); + m_themeModel->reload(); + m_ui->theme->setCurrentIndex(m_themeModel->indexOf(themeName)); +} + +} // namespace + +#include "config.moc" diff --git a/clients/aurorae/src/config/config.h b/clients/aurorae/src/config/config.h new file mode 100644 index 0000000000..e1e3002903 --- /dev/null +++ b/clients/aurorae/src/config/config.h @@ -0,0 +1,132 @@ +/******************************************************************** +Copyright (C) 2009 Martin Gräßlin + +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 . +*********************************************************************/ +#ifndef AURORAE_CONFIG_H +#define AURORAE_CONFIG_H + +#include +#include +#include +#include + +#include "ui_config.h" + +namespace Plasma +{ +class FrameSvg; +} + +namespace Aurorae +{ +class ThemeConfig; + +//Theme selector code by Andre Duffeck (modified to add package description) +class ThemeInfo +{ +public: + QString package; + Plasma::FrameSvg *svg; + QString description; + QString author; + QString email; + QString version; + QString website; + QString license; + QString themeRoot; + ThemeConfig *themeConfig; + QHash *buttons; +}; + +class ThemeModel : public QAbstractListModel +{ +public: + enum { PackageNameRole = Qt::UserRole, + SvgRole = Qt::UserRole + 1, + PackageDescriptionRole = Qt::UserRole + 2, + PackageAuthorRole = Qt::UserRole + 3, + PackageVersionRole = Qt::UserRole + 4, + PackageLicenseRole = Qt::UserRole + 5, + PackageEmailRole = Qt::UserRole + 6, + PackageWebsiteRole = Qt::UserRole + 7, + ThemeConfigRole = Qt::UserRole + 8, + ButtonsRole = Qt::UserRole + 9 + }; + + ThemeModel(QObject *parent = 0); + virtual ~ThemeModel(); + + virtual int rowCount(const QModelIndex &parent = QModelIndex()) const; + virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const; + int indexOf(const QString &name) const; + void reload(); + void clearThemeList(); + void initButtonFrame(const QString &button, const QString &path, QHash *buttons); +private: + QMap m_themes; +}; + +class ThemeDelegate : public QAbstractItemDelegate +{ +public: + ThemeDelegate(QObject *parent = 0); + + virtual void paint(QPainter *painter, + const QStyleOptionViewItem &option, + const QModelIndex &index) const; + virtual QSize sizeHint(const QStyleOptionViewItem &option, + const QModelIndex &index) const; +private: + void paintDeco(QPainter *painter, bool active, const QStyleOptionViewItem &option, const QModelIndex &index, + int leftMargin, int topMargin, + int rightMargin, int bottomMargin) const; +}; + +class AuroraeConfigUI : public QWidget, public Ui::ConfigUI +{ +public: + AuroraeConfigUI(QWidget *parent) : QWidget(parent) + { + setupUi(this); + } +}; + +class AuroraeConfig: public QObject +{ + Q_OBJECT +public: + AuroraeConfig(KConfig *conf, QWidget *parent); + ~AuroraeConfig(); + +signals: + void changed(); +public slots: + void load(const KConfigGroup &conf); + void save(KConfigGroup &conf); + void defaults(); +private slots: + void slotAboutClicked(); + void slotInstallNewTheme(); + +private: + QWidget *m_parent; + ThemeModel *m_themeModel; + KConfig *m_config; + AuroraeConfigUI *m_ui; +}; + +} // namespace + +#endif diff --git a/clients/aurorae/src/config/config.ui b/clients/aurorae/src/config/config.ui new file mode 100644 index 0000000000..5c6ec0b196 --- /dev/null +++ b/clients/aurorae/src/config/config.ui @@ -0,0 +1,98 @@ + + + ConfigUI + + + + 0 + 0 + 400 + 300 + + + + Form + + + + + + + + Theme: + + + theme + + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + Install New Theme + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + KPushButton + QPushButton +
kpushbutton.h
+
+
+ + +
diff --git a/clients/aurorae/src/themeconfig.cpp b/clients/aurorae/src/themeconfig.cpp new file mode 100644 index 0000000000..75b693fc28 --- /dev/null +++ b/clients/aurorae/src/themeconfig.cpp @@ -0,0 +1,85 @@ +/******************************************************************** +Copyright (C) 2009 Martin Gräßlin + +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 "themeconfig.h" + +#include +#include + +namespace Aurorae +{ + +ThemeConfig::ThemeConfig() +{ +} + +void ThemeConfig::load(KConfig *conf) +{ + KConfigGroup general(conf, "General"); + m_activeTextColor = general.readEntry("ActiveTextColor", QColor(Qt::black)); + m_inactiveTextColor = general.readEntry("InactiveTextColor", QColor(Qt::black)); + QString alignment = (general.readEntry("TitleAlignment", "Left")).toLower(); + if (alignment == "left") { + m_alignment = Qt::AlignLeft; + } + else if (alignment == "center") { + m_alignment = Qt::AlignCenter; + } + else { + m_alignment = Qt::AlignRight; + } + alignment = (general.readEntry("TitleVerticalAlignment", "Center")).toLower(); + if (alignment == "top") { + m_verticalAlignment = Qt::AlignTop; + } + else if (alignment == "center") { + m_verticalAlignment = Qt::AlignVCenter; + } + else { + m_verticalAlignment = Qt::AlignBottom; + } + m_animationTime = general.readEntry("Animation", 0); + m_defaultButtonsLeft = general.readEntry("LeftButtons", "MS"); + m_defaultButtonsRight = general.readEntry("RightButtons", "HIA__X"); + m_shadow = general.readEntry("Shadow", true); + + KConfigGroup border(conf, "Layout"); + // default values taken from KCommonDecoration::layoutMetric() in kcommondecoration.cpp + m_borderLeft = border.readEntry("BorderLeft", 5); + m_borderRight = border.readEntry("BorderRight", 5); + m_borderBottom = border.readEntry("BorderBottom", 5); + + m_titleEdgeTop = border.readEntry("TitleEdgeTop", 5); + m_titleEdgeBottom = border.readEntry("TitleEdgeBottom", 5); + m_titleEdgeLeft = border.readEntry("TitleEdgeLeft", 5); + m_titleEdgeRight = border.readEntry("TitleEdgeRight", 5); + m_titleBorderLeft = border.readEntry("TitleBorderLeft", 5); + m_titleBorderRight = border.readEntry("TitleBorderRight", 5); + m_titleHeight = border.readEntry("TitleHeight", 20); + + m_buttonWidth = border.readEntry("ButtonWidth", 20); + m_buttonHeight = border.readEntry("ButtonHeight", 20); + m_buttonSpacing = border.readEntry("ButtonSpacing", 5); + m_buttonMarginTop = border.readEntry("ButtonMarginTop", 0); + m_explicitButtonSpacer = border.readEntry("ExplicitButtonSpacer", 10); + + m_paddingLeft = border.readEntry("PaddingLeft", 0); + m_paddingRight = border.readEntry("PaddingRight", 0); + m_paddingTop = border.readEntry("PaddingTop", 0); + m_paddingBottom = border.readEntry("PaddingBottom", 0); +} + +} //namespace diff --git a/clients/aurorae/src/themeconfig.h b/clients/aurorae/src/themeconfig.h new file mode 100644 index 0000000000..1540694a85 --- /dev/null +++ b/clients/aurorae/src/themeconfig.h @@ -0,0 +1,166 @@ +/******************************************************************** +Copyright (C) 2009 Martin Gräßlin + +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 . +*********************************************************************/ +#ifndef THEMECONFIG_H +#define THEMECONFIG_H +// This class encapsulates all theme config values +// it's a seperate class as it's needed by both deco and config dialog + +#include +#include + +class KConfig; + +namespace Aurorae +{ +class ThemeConfig +{ +public: + ThemeConfig(); + void load(KConfig *conf); + ~ThemeConfig() {}; + // active window + QColor activeTextColor() const { + return m_activeTextColor; + } + // inactive window + QColor inactiveTextColor() const { + return m_inactiveTextColor; + } + // Alignment + Qt::Alignment alignment() const { + return m_alignment; + }; + Qt::Alignment verticalAlignment() const { + return m_verticalAlignment; + } + int animationTime() const { + return m_animationTime; + } + // Borders + int borderLeft() const { + return m_borderLeft; + } + int borderRight() const { + return m_borderRight; + } + int borderBottom() const { + return m_borderBottom; + } + + int titleEdgeTop() const { + return m_titleEdgeTop; + } + int titleEdgeBottom() const { + return m_titleEdgeBottom; + } + int titleEdgeLeft() const { + return m_titleEdgeLeft; + } + int titleEdgeRight() const { + return m_titleEdgeRight; + } + int titleBorderLeft() const { + return m_titleBorderLeft; + } + int titleBorderRight() const { + return m_titleBorderRight; + } + int titleHeight() const { + return m_titleHeight; + } + + int buttonWidth() const { + return m_buttonWidth; + } + int buttonHeight() const { + return m_buttonHeight; + } + int buttonSpacing() const { + return m_buttonSpacing; + } + int buttonMarginTop() const { + return m_buttonMarginTop; + } + int explicitButtonSpacer() const { + return m_explicitButtonSpacer; + } + + int paddingLeft() const { + return m_paddingLeft; + } + int paddingRight() const { + return m_paddingRight; + } + int paddingTop() const { + return m_paddingTop; + } + int paddingBottom() const { + return m_paddingBottom; + } + + QString defaultButtonsLeft() const { + return m_defaultButtonsLeft; + } + QString defaultButtonsRight() const { + return m_defaultButtonsRight; + } + bool shadow() const { + return m_shadow; + } + +private: + QColor m_activeTextColor; + QColor m_inactiveTextColor; + Qt::Alignment m_alignment; + Qt::Alignment m_verticalAlignment; + // borders + int m_borderLeft; + int m_borderRight; + int m_borderBottom; + + // title + int m_titleEdgeTop; + int m_titleEdgeBottom; + int m_titleEdgeLeft; + int m_titleEdgeRight; + int m_titleBorderLeft; + int m_titleBorderRight; + int m_titleHeight; + + // buttons + int m_buttonWidth; + int m_buttonHeight; + int m_buttonSpacing; + int m_buttonMarginTop; + int m_explicitButtonSpacer; + + // padding + int m_paddingLeft; + int m_paddingRight; + int m_paddingTop; + int m_paddingBottom; + + int m_animationTime; + + QString m_defaultButtonsLeft; + QString m_defaultButtonsRight; + bool m_shadow; +}; + +} + +#endif diff --git a/clients/aurorae/theme-description b/clients/aurorae/theme-description new file mode 100644 index 0000000000..54693be016 --- /dev/null +++ b/clients/aurorae/theme-description @@ -0,0 +1,122 @@ +DESCRIPTION OF AURORAE +====================== + +Aurorae is a theme engine for KWin window decorations. It is built against the unstable API of KWin +in KDE 4.3. Aurorae uses SVG to render the decoration and buttons and there is a simple config file +for configuring the theme details. + +This theme engine uses Plasma technologie to render the window decoration. Every detail can be +themed by the usage of SVG. The theme engine uses Plasma's FrameSvg, so you can provide SVG files +containing borders. This is described in more detail in techbase: +http://techbase.kde.org/Projects/Plasma/Theme#Backgrounds + +The theme consists of one folder containing svgz files for decoration and buttons, one KConfig file +for the theme details and one metadata.desktop file which you can use to name your theme, author +information, etc. + +Although the engine uses Plasma technology, it isn't Plasma. So it does not know anything about +Plasmoids and you will never be able to put Plasmoids into the decoration. That is out of scope of +this engine. + +Aurorae uses the features provided by KWin 4.3. So the themes can provide their own decoration +shadows and it is recommended that your themes provide those. The engine supports ARGB decoration +which is enabled by default. If you provide a theme using translucency, please make sure, that it +works without compositing as well. + +Window Decoration +================= +The window decoration has to be provided in file "decoration.svgz". This svg has to contain all the +elements required for a Plasma theme background. The decoration has to use the element prefix +"decoration". + +If you want to provide a different style for inactive windows you can add it to the same svg. The +inactive elements must have the element prefix "decoration-inactive". The theme engine tests for +this prefix and if not provided inactive windows will be rendered with the same style as active +windows. + +You have to provide a special decoration for opaque mode, that is when compositing is not active. +This opaque decoration is used for generating the window mask. The element prefix is +"decoration-opaque" for active and "decoration-opaque-inactive" for inactive windows. The mask is +generated from the active window. + +Buttons +======= +You have to provide a svgz file for each button your theme should contain. If you do not provide a +file for a button type the engine will not include that button, so your decoration will miss it. +There is no fallback to a default theme. The buttons are rendered using Plasma's FrameSvg as well. +So you have to provide the "center" element. Borders are not supported + +You can provide the following buttons: + * close + * minimize + * maximize + * restore + * alldesktops + * keepabove + * keepbelow + * shade + * resize + * help + +Each button can have different states. So a button could be hovered, pressed, deactivated and you +might want to provide different styles for active and inactive windows. You can use the following +element prefix to provide styles for the buttons: + * active (normal button for active window) + * inactive (normal button for inactive window) + * hover (hover state for active window) + * hover-inactive (hover state for inactive window) + * pressed (button is pressed) + * pressed-inactive (pressed inactive button) + * deactivated (button cannot be clicked, e.g. window cannot be closed) + * deactivated-inactive (same for inactive windows) + +You have at least to provide the active element. All other elements are optional and the active +element is always used as a fallback. If you provide the inactive element, this is used as a +fallback for the inactive window. That is, if you provide a hover element, but none for inactive, +the inactive window will not have a hover effect. Same is true for pressed and deactivated. +Reasonable that means if you provide a deactivated and an inactive element you want to provide a +deactivated-inactive element as well. + +Configuration file +================== +The configuration file is a normal KConfig file. You have to give it the name of your decoration +with suffix "rc". So if your theme has the name "deco", your config file will be named "decorc". +The following section shows the possible options with their default values. + +[General] +TitleAlignment=Left # vorizontal alignment of window title +TitleVerticalAlignment=Center # vertical alignment of window title +Animation=0 # animation duration in msec when hovering a button +ActiveTextColor=0,0,0,255 # title text color of active window +InactiveTextColor=0,0,0,255 # title text color of inactive window +LeftButtons=MS # buttons in left button group (see http://api.kde.org/4.x-api/kdebase-workspace-apidocs/kwin/lib/html/classKDecorationOptions.html#8ad12d76c93c5f1a12ea07b30f92d2fa) +RightButtons=HIA__X # buttons in right button group +Shadow=true # decoration provides shadows: you have to add padding + +[Layout] # uses Layout Manager (see http://api.kde.org/4.x-api/kdebase-workspace-apidocs/kwin/lib/html/classKCommonDecoration.html#7932f74c28432ad8de232f1c6e8751ce) +BorderLeft=5 +BorderRight=5 +BorderBottom=5 +TitleEdgeTop=5 +TitleEdgeBottom=5 +TitleEdgeLeft=5 +TitleEdgeRight=5 +TitleBorderLeft=5 +TitleBorderRight=5 +TitleHeight=20 +ButtonWidth=20 +ButtonHeight=20 +ButtonSpacing=5 +ButtonMarginTop=0 +ExplicitButtonSpacer=10 +PaddingTop=0 # Padding added to provide shadows +PaddingBottom=0 # Padding added to provide shadows +PaddingRight=0 # Padding added to provide shadows +PaddingLeft=0 # Padding added to provide shadows + +Packaging +========= +All theme files (decoration, buttons, metadata.desktop and configuration file) have to be stored in +one directory with the name of the theme (this has to be identical to the one used for the config +file). You have to create a tar.gz archive from that directory. This archive is the theme, which +can be installed in the kcm for window decorations. diff --git a/clients/aurorae/themes/example-deco/CMakeLists.txt b/clients/aurorae/themes/example-deco/CMakeLists.txt new file mode 100644 index 0000000000..5a738a3496 --- /dev/null +++ b/clients/aurorae/themes/example-deco/CMakeLists.txt @@ -0,0 +1,4 @@ + + +FILE(GLOB deco *.svgz) +install( FILES ${deco} example-decorc metadata.desktop DESTINATION ${DATA_INSTALL_DIR}/aurorae/themes/example-deco/ ) diff --git a/clients/aurorae/themes/example-deco/close.svgz b/clients/aurorae/themes/example-deco/close.svgz new file mode 100644 index 0000000000000000000000000000000000000000..91288e3dce2201546df0321aac6a4e1a30a4b09b GIT binary patch literal 2124 zcmV-S2($MeiwFP!000000PR{!bK5u)zUNnH=}TN|Qh1RBWGi#n>}<_xt9B-}$0?eG zBvvR=MN*NSUq20qZ$S@Ba^gzHsgx*m5pG+ZjFjENTVRhQaQEK%=+cSyN7?+_UK=^3`!Y|HgUNa{g!?%!eAvw?-ymc znvBPr&BlqVMkmYXL_qr*AU5Lx)333F(*!P%!Z;i&sr*#d};^WgIG?=!X?P1)v z|HW`2|2SMoyd18X{u{#OBbReRPlF5Nw+0u*?q0Zz7#J4Qahb!Q)A&fxLcS&znyU_B!SRWU{6>gv7n65p9YH~=LivkyhSV?#;OR=PfcE-YLDnkEH`bX z@E9};TLp8e(;$Fgn}Sw?$1IOzUW+{a*ArEl(zd96*GF89+Q^YvUMzwr+kl}%;nyr% zPA%d10%6jW0PSuna0pE5dtz4z^9cli_?Xe55DKxis=Mv=cBfWPJKfiLt`w5sS>~#u z3RKRbZUO8#)z8unK|$tK&tj2n=DEUj79=56)0qCJ9tzo zV7ATn$iQlw);_S5We^2r(C&BLQs?3ic{H2+@8hpciX4WM|7ZE)3} zRlylfpgLa$<%f6)Td1x9{t4AMxLqUYDXW~e9dtWvF1KaA@n$3nm$B+J{=JNoX?=-rsKyTf{|%|`ZGHTg3K7>x7>QBm#!|NGqI{ee0_K@8^c(|hE2E)tya zJE+NAfq)e6?DnxEzC*X2EivG?J2(vF3FH$!IH4FbUk~0s)?zjI&;+8+FyS(PXtm9d z!v*sP*qKo>Bih*j*ZPJ|(B-Ik%}(WmP-TgwD8``2aM*s{yOdoIGEW!Z9c;PpHfVAKng;AN7!y=~gMgnG!oBLsrc6`0-jR&A~X4D3Es{65K_$FL#!+dT*kCxE&Z z+sOpaKN*MF9!03T;8)A`%oIW>VTAOp>-^wy1ug;mF5;5Ev%sS$9X{$P!sxuC?z+x# zW_Gx(9QV>qQ-SW&Fa{KJ1R??M+i50*Sq zM9z$;>UDs(l006#ls)K>O9=mMC69;GBnlDWQlE4kar+iZduMtqK{DumrwcO3bk59? z)K5gOnj_I}zS%w&@>1;3;=P<{R7t^?VG%v zYh$SYu}7I8Lr`l@@wpM*UT`<)>OkPs1hVU3ghbEvv_}EUqrOLKj}i`?@0jdI=z3|L zCMJd*D*C^HDJt`@gBeFIIpQ}a|LXkgc~Tv7-5)(G2Gk!9>Yb1e@BRYc9}W|EGXMaD CFB`J} literal 0 HcmV?d00001 diff --git a/clients/aurorae/themes/example-deco/decoration.svgz b/clients/aurorae/themes/example-deco/decoration.svgz new file mode 100644 index 0000000000000000000000000000000000000000..e79bf862f92e7b261c29122c89e9eb650ff342a5 GIT binary patch literal 8257 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 MzfnEt1&bvB0G9B9mjD0& literal 0 HcmV?d00001 diff --git a/clients/aurorae/themes/example-deco/restore.svgz b/clients/aurorae/themes/example-deco/restore.svgz new file mode 100644 index 0000000000000000000000000000000000000000..633de7bc3a6dca4dd48e338c8772a5304aaa5f5c GIT binary patch literal 1980 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 OSAPP9pBt(qCIA3VIluG( literal 0 HcmV?d00001