From c35458d26231289036580352479d1dd389322b94 Mon Sep 17 00:00:00 2001 From: Martin Koller Date: Fri, 3 Nov 2017 07:18:30 +0100 Subject: [PATCH] Applets on the desktop First applet is a weather applet --- CMakeLists.txt | 11 +- ClockWidget.cxx | 4 +- ...alog.cxx => ClockWidgetConfigureDialog.cxx | 6 +- ...alog.hxx => ClockWidgetConfigureDialog.hxx | 10 +- DesktopApplet.cxx | 136 ++++++ DesktopApplet.hxx | 60 +++ DesktopWidget.cxx | 100 ++++- DesktopWidget.hxx | 9 + SysTray.cxx | 1 + WeatherApplet.cxx | 398 ++++++++++++++++++ WeatherApplet.hxx | 88 ++++ WeatherAppletConfigureDialog.cxx | 172 ++++++++ WeatherAppletConfigureDialog.hxx | 48 +++ WeatherAppletConfigureDialog.ui | 181 ++++++++ stylesheet.css | 15 + 15 files changed, 1217 insertions(+), 22 deletions(-) rename ConfigureClockWidgetDialog.cxx => ClockWidgetConfigureDialog.cxx (95%) rename ConfigureClockWidgetDialog.hxx => ClockWidgetConfigureDialog.hxx (81%) create mode 100644 DesktopApplet.cxx create mode 100644 DesktopApplet.hxx create mode 100644 WeatherApplet.cxx create mode 100644 WeatherApplet.hxx create mode 100644 WeatherAppletConfigureDialog.cxx create mode 100644 WeatherAppletConfigureDialog.hxx create mode 100644 WeatherAppletConfigureDialog.ui diff --git a/CMakeLists.txt b/CMakeLists.txt index 4162203..af0f1b1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -15,7 +15,7 @@ find_package(Qt5 CONFIG REQUIRED COMPONENTS ) find_package(KF5 REQUIRED COMPONENTS - WindowSystem WidgetsAddons ConfigWidgets Config KIO IconThemes ItemViews + WindowSystem WidgetsAddons ConfigWidgets Config KIO IconThemes ItemViews Archive Notifications I18n NetworkManagerQt Service Solid BluezQt KCMUtils Crash DBusAddons ) @@ -32,7 +32,7 @@ set(SOURCES PagerButton.cxx WindowList.cxx ClockWidget.cxx - ConfigureClockWidgetDialog.cxx + ClockWidgetConfigureDialog.cxx TaskBar.cxx TaskBarButton.cxx LockLogout.cxx @@ -49,9 +49,13 @@ set(SOURCES DeviceList.cxx Battery.cxx Bluetooth.cxx + + DesktopApplet.cxx + WeatherApplet.cxx + WeatherAppletConfigureDialog.cxx ) -ki18n_wrap_ui(UI_FILES ConfigureDesktopDialog.ui) +ki18n_wrap_ui(UI_FILES ConfigureDesktopDialog.ui WeatherAppletConfigureDialog.ui) set(statusnotifieritem_xml ${KNOTIFICATIONS_DBUS_INTERFACES_DIR}/kf5_org.kde.StatusNotifierItem.xml) set_source_files_properties(${statusnotifieritem_xml} PROPERTIES @@ -90,6 +94,7 @@ target_link_libraries(${TARGET} KF5::Crash KF5::DBusAddons KF5::ItemViews + KF5::Archive ) install(TARGETS ${TARGET} ${INSTALL_TARGETS_DEFAULT_ARGS}) diff --git a/ClockWidget.cxx b/ClockWidget.cxx index e4b283c..5d6647e 100644 --- a/ClockWidget.cxx +++ b/ClockWidget.cxx @@ -18,7 +18,7 @@ */ #include -#include +#include #include #include @@ -112,7 +112,7 @@ ClockWidget::ClockWidget(DesktopPanel *parent) connect(action, &QAction::triggered, [this]() { - ConfigureClockWidgetDialog dialog(parentWidget(), timeZoneIds); + ClockWidgetConfigureDialog dialog(parentWidget(), timeZoneIds); dialog.setWindowTitle(i18n("Select Timezones")); dialog.resize(600, 400); if ( dialog.exec() == QDialog::Accepted ) diff --git a/ConfigureClockWidgetDialog.cxx b/ClockWidgetConfigureDialog.cxx similarity index 95% rename from ConfigureClockWidgetDialog.cxx rename to ClockWidgetConfigureDialog.cxx index c3b1e63..3829a81 100644 --- a/ConfigureClockWidgetDialog.cxx +++ b/ClockWidgetConfigureDialog.cxx @@ -17,7 +17,7 @@ along with liquidshell. If not, see . */ -#include +#include #include #include @@ -32,7 +32,7 @@ //-------------------------------------------------------------------------------- -ConfigureClockWidgetDialog::ConfigureClockWidgetDialog(QWidget *parent, const QVector &timeZoneIds) +ClockWidgetConfigureDialog::ClockWidgetConfigureDialog(QWidget *parent, const QVector &timeZoneIds) : QDialog(parent) { tree = new QTreeWidget; @@ -84,7 +84,7 @@ ConfigureClockWidgetDialog::ConfigureClockWidgetDialog(QWidget *parent, const QV //-------------------------------------------------------------------------------- -QVector ConfigureClockWidgetDialog::getSelectedTimeZoneIds() const +QVector ClockWidgetConfigureDialog::getSelectedTimeZoneIds() const { QVector timeZoneIds; diff --git a/ConfigureClockWidgetDialog.hxx b/ClockWidgetConfigureDialog.hxx similarity index 81% rename from ConfigureClockWidgetDialog.hxx rename to ClockWidgetConfigureDialog.hxx index 0b0c221..da2f86e 100644 --- a/ConfigureClockWidgetDialog.hxx +++ b/ClockWidgetConfigureDialog.hxx @@ -17,23 +17,21 @@ along with liquidshell. If not, see . */ -#ifndef _ConfigureClockWidgetDialog_H_ -#define _ConfigureClockWidgetDialog_H_ +#ifndef _ClockWidgetConfigureDialog_H_ +#define _ClockWidgetConfigureDialog_H_ #include class QTreeWidget; -class ConfigureClockWidgetDialog : public QDialog +class ClockWidgetConfigureDialog : public QDialog { Q_OBJECT public: - ConfigureClockWidgetDialog(QWidget *parent, const QVector &timeZoneIds); + ClockWidgetConfigureDialog(QWidget *parent, const QVector &timeZoneIds); QVector getSelectedTimeZoneIds() const; - private slots: - private: QTreeWidget *tree; }; diff --git a/DesktopApplet.cxx b/DesktopApplet.cxx new file mode 100644 index 0000000..4cfe7e7 --- /dev/null +++ b/DesktopApplet.cxx @@ -0,0 +1,136 @@ +/* + Copyright 2017 Martin Koller, kollix@aon.at + + This file is part of liquidshell. + + liquidshell 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 3 of the License, or + (at your option) any later version. + + liquidshell 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 liquidshell. If not, see . +*/ + +#include + +#include +#include +#include +#include +#include + +#include +#include +#include + +//-------------------------------------------------------------------------------- + +DesktopApplet::DesktopApplet(QWidget *parent, const QString &theId) + : QFrame(parent), id(theId) +{ + setFrameShape(QFrame::StyledPanel); + setContextMenuPolicy(Qt::ActionsContextMenu); + + QAction *action = new QAction(this); + action->setIcon(QIcon::fromTheme("preferences-system-windows-move")); + action->setText(i18n("Change Size && Position")); + addAction(action); + connect(action, &QAction::triggered, this, &DesktopApplet::startGeometryChange); + + action = new QAction(this); + action->setIcon(QIcon::fromTheme("window-close")); + action->setText(i18n("Remove this Applet")); + addAction(action); + connect(action, &QAction::triggered, this, &DesktopApplet::removeThisApplet); + + buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); + buttons->adjustSize(); + buttons->hide(); + connect(buttons, &QDialogButtonBox::clicked, this, &DesktopApplet::finishGeometryChange); +} + +//-------------------------------------------------------------------------------- + +void DesktopApplet::loadConfig() +{ + KConfig config; + KConfigGroup group = config.group(id); + setGeometry(group.readEntry("rect", QRect(30, 30, 600, 400))); + onDesktop = group.readEntry("onDesktop", int(NET::OnAllDesktops)); +} + +//-------------------------------------------------------------------------------- + +void DesktopApplet::saveConfig() +{ + KConfig config; + KConfigGroup group = config.group(id); + group.writeEntry("rect", geometry()); + group.writeEntry("onDesktop", onDesktop); +} + +//-------------------------------------------------------------------------------- + +void DesktopApplet::startGeometryChange() +{ + buttons->raise(); + buttons->show(); + + oldRect = geometry(); + setWindowFlags(Qt::Window); + setWindowTitle(i18n("%1: Change Size & Position").arg(id)); + + if ( onDesktop == NET::OnAllDesktops ) + KWindowSystem::setOnAllDesktops(winId(), true); + + show(); + setGeometry(oldRect); +} + +//-------------------------------------------------------------------------------- + +void DesktopApplet::finishGeometryChange(QAbstractButton *clicked) +{ + KWindowInfo info(winId(), NET::WMDesktop); + if ( info.valid() ) + onDesktop = info.desktop(); + + buttons->hide(); + QRect rect = geometry(); + setWindowFlags(Qt::Widget); + show(); + + if ( buttons->buttonRole(clicked) == QDialogButtonBox::AcceptRole ) + { + setGeometry(rect); + KConfig config; + KConfigGroup group = config.group(id); + group.writeEntry("rect", rect); + group.writeEntry("onDesktop", onDesktop); + } + else + { + setGeometry(oldRect); + } +} + +//-------------------------------------------------------------------------------- + +void DesktopApplet::removeThisApplet() +{ + if ( QMessageBox::question(this, i18n("Remove Applet"), + i18n("Really remove this applet?")) == QMessageBox::Yes ) + { + KConfig config; + config.deleteGroup(id); + emit removeThis(this); + } +} + +//-------------------------------------------------------------------------------- diff --git a/DesktopApplet.hxx b/DesktopApplet.hxx new file mode 100644 index 0000000..28e26cf --- /dev/null +++ b/DesktopApplet.hxx @@ -0,0 +1,60 @@ +/* + Copyright 2017 Martin Koller, kollix@aon.at + + This file is part of liquidshell. + + liquidshell 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 3 of the License, or + (at your option) any later version. + + liquidshell 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 liquidshell. If not, see . +*/ + +#ifndef _DesktopApplet_H_ +#define _DesktopApplet_H_ + +#include +#include +#include + +class DesktopApplet : public QFrame +{ + Q_OBJECT + + public: + DesktopApplet(QWidget *parent, const QString &theId); + + virtual void loadConfig(); + virtual void saveConfig(); + + bool isConfiguring() const { return buttons->isVisible(); } + + const QString &getId() const { return id; } + int isOnDesktop(int d) const { return (onDesktop == NET::OnAllDesktops) || (onDesktop == d); } + + signals: + void geometryChanged(); + void removeThis(DesktopApplet *); + + protected: + QString id; + + private slots: + void startGeometryChange(); + void finishGeometryChange(QAbstractButton *clicked); + void removeThisApplet(); + + private: + QDialogButtonBox *buttons = nullptr; + QRect oldRect; + int onDesktop = NET::OnAllDesktops; +}; + +#endif diff --git a/DesktopWidget.cxx b/DesktopWidget.cxx index 603db78..25db65b 100644 --- a/DesktopWidget.cxx +++ b/DesktopWidget.cxx @@ -19,6 +19,7 @@ #include #include +#include #include #include @@ -29,6 +30,8 @@ #include #include #include +#include +#include #include #include @@ -38,6 +41,10 @@ //-------------------------------------------------------------------------------- +int DesktopWidget::appletNum = 0; + +//-------------------------------------------------------------------------------- + DesktopWidget::DesktopWidget() : QWidget(nullptr, Qt::WindowDoesNotAcceptFocus) { @@ -62,10 +69,40 @@ DesktopWidget::DesktopWidget() connect(action, &QAction::triggered, this, &DesktopWidget::configureWallpaper); addAction(action); + action = new QAction(i18n("Add Applet"), this); + addAction(action); + QMenu *menu = new QMenu; + action->setMenu(menu); + action = menu->addAction(QIcon::fromTheme("weather-clouds"), i18n("Weather")); + connect(action, &QAction::triggered, [this]() { addApplet("Weather"); }); + action = new QAction(QIcon::fromTheme("preferences-desktop-display"), i18n("Configure Display..."), this); connect(action, &QAction::triggered, this, &DesktopWidget::configureDisplay); addAction(action); + // restore aapplets + KConfig config; + KConfigGroup group = config.group("DesktopApplets"); + QStringList appletNames = group.readEntry("applets", QStringList()); + for (const QString &appletName : appletNames) + { + DesktopApplet *applet = nullptr; + + if ( appletName.startsWith("Weather_") ) + applet = new WeatherApplet(this, appletName); + + if ( applet ) + { + int num = appletName.mid(appletName.indexOf('_') + 1).toInt(); + if ( num > appletNum ) + appletNum = num; + + applet->loadConfig(); + applets.append(applet); + connect(applet, &DesktopApplet::removeThis, this, &DesktopWidget::removeApplet); + } + } + loadSettings(); connect(qApp->primaryScreen(), &QScreen::geometryChanged, @@ -81,7 +118,7 @@ void DesktopWidget::loadSettings() "wallpapers", QStandardPaths::LocateDirectory); QStringList defaultFiles; - const QString wantedPrefix = QString("%1x%2").arg(width()).arg(height()); + const QString geometryString = QString("%1x%2").arg(width()).arg(height()); for (const QString &dirName : dirNames) { for (const QString &subdir : QDir(dirName).entryList(QDir::Dirs | QDir::NoDotAndDotDot | QDir::Readable)) @@ -89,7 +126,7 @@ void DesktopWidget::loadSettings() QDir dir(dirName + '/' + subdir + "/contents/images"); for (const QString &fileName : dir.entryList(QDir::Files | QDir::Readable)) { - if ( fileName.startsWith(wantedPrefix) ) + if ( fileName.startsWith(geometryString) ) defaultFiles.append(dir.absoluteFilePath(fileName)); } } @@ -102,11 +139,11 @@ void DesktopWidget::loadSettings() Wallpaper wallpaper; - wallpaper.color = group.readEntry("Color", QColor(Qt::black)); - wallpaper.mode = group.readEntry("WallpaperMode", QString()); + wallpaper.color = group.readEntry("color", QColor(Qt::black)); + wallpaper.mode = group.readEntry("wallpaperMode", QString()); int idx = defaultFiles.count() ? ((i - 1) % defaultFiles.count()) : -1; - wallpaper.fileName = group.readEntry(QString("Wallpaper"), (i != -1) ? defaultFiles[idx] : QString()); + wallpaper.fileName = group.readEntry("wallpaper", (i != -1) ? defaultFiles[idx] : QString()); wallpapers.append(wallpaper); } @@ -141,9 +178,9 @@ void DesktopWidget::configureWallpaper() Wallpaper &wallpaper = wallpapers[currentDesktop]; KConfig config; KConfigGroup group = config.group(QString("Desktop_%1").arg(currentDesktop + 1)); - group.writeEntry("Color", wallpaper.color); - group.writeEntry(QString("Wallpaper"), wallpaper.fileName); - group.writeEntry("WallpaperMode", wallpaper.mode); + group.writeEntry("color", wallpaper.color); + group.writeEntry("wallpaper", wallpaper.fileName); + group.writeEntry("wallpaperMode", wallpaper.mode); } KWindowSystem::setShowingDesktop(showingDesktop); // restore } @@ -210,6 +247,13 @@ void DesktopWidget::desktopChanged() } update(); + + // show applets for new desktop + for (DesktopApplet *applet : applets) + { + if ( !applet->isConfiguring() ) + applet->setVisible(applet->isOnDesktop(currentDesktop + 1)); + } } //-------------------------------------------------------------------------------- @@ -233,3 +277,43 @@ void DesktopWidget::paintEvent(QPaintEvent *event) } //-------------------------------------------------------------------------------- + +void DesktopWidget::addApplet(const QString &type) +{ + if ( type == "Weather" ) + { + DesktopApplet *applet = new WeatherApplet(this, QString("%1_%2").arg(type).arg(++appletNum)); + applets.append(applet); + applet->loadConfig(); // defaults + size + applet->move(QCursor::pos()); + applet->saveConfig(); + applet->show(); + connect(applet, &DesktopApplet::removeThis, this, &DesktopWidget::removeApplet); + } + saveAppletsList(); +} + +//-------------------------------------------------------------------------------- + +void DesktopWidget::removeApplet(DesktopApplet *applet) +{ + applets.removeOne(applet); + applet->deleteLater(); + saveAppletsList(); +} + +//-------------------------------------------------------------------------------- + +void DesktopWidget::saveAppletsList() +{ + KConfig config; + KConfigGroup group = config.group("DesktopApplets"); + + QStringList appletNames; + for (DesktopApplet *applet : applets) + appletNames.append(applet->getId()); + + group.writeEntry("applets", appletNames); +} + +//-------------------------------------------------------------------------------- diff --git a/DesktopWidget.hxx b/DesktopWidget.hxx index bbf66a3..3b45479 100644 --- a/DesktopWidget.hxx +++ b/DesktopWidget.hxx @@ -24,6 +24,7 @@ #include #include class DesktopPanel; +class DesktopApplet; class DesktopWidget : public QWidget { @@ -48,12 +49,20 @@ class DesktopWidget : public QWidget void desktopChanged(); void configureWallpaper(); void configureDisplay(); + void addApplet(const QString &type); + void removeApplet(DesktopApplet *applet); + + private: + void saveAppletsList(); private: DesktopPanel *panel; QVector wallpapers; int currentDesktop = -1; + + QVector applets; + static int appletNum; }; #endif diff --git a/SysTray.cxx b/SysTray.cxx index aa58832..7650ba1 100644 --- a/SysTray.cxx +++ b/SysTray.cxx @@ -217,6 +217,7 @@ void SysTray::itemRegistered(QString item) void SysTray::itemInitialized(SysTrayNotifyItem *item) { + // TODO count only visible items int lowestCount = 0; int lowestCountAt = 0; for (int i = 0; i < appsRows.count(); i++) diff --git a/WeatherApplet.cxx b/WeatherApplet.cxx new file mode 100644 index 0000000..9486da1 --- /dev/null +++ b/WeatherApplet.cxx @@ -0,0 +1,398 @@ +/* + Copyright 2017 Martin Koller, kollix@aon.at + + This file is part of liquidshell. + + liquidshell 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 3 of the License, or + (at your option) any later version. + + liquidshell 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 liquidshell. If not, see . +*/ + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +//-------------------------------------------------------------------------------- + +QString WeatherApplet::apiKey; + +//-------------------------------------------------------------------------------- + +WeatherApplet::WeatherApplet(QWidget *parent, const QString &theId) + : DesktopApplet(parent, theId) +{ + setAutoFillBackground(true); + + timer.setInterval(600000); // 10min smallest update interval for free data + + connect(&timer, &QTimer::timeout, this, &WeatherApplet::fetchData); + + QVBoxLayout *vbox = new QVBoxLayout(this); + cityLabel = new QLabel(this); + cityLabel->setObjectName("city"); + + QFont f = font(); + f.setPointSizeF(fontInfo().pointSizeF() * 2); + f.setBold(true); + cityLabel->setFont(f); + + vbox->addWidget(cityLabel); + + QGridLayout *grid = new QGridLayout; + vbox->addLayout(grid); + + grid->addWidget(new QLabel(i18n("Temperature:"), this), 0, 0); + grid->addWidget(tempLabel = new QLabel, 0, 1); + + grid->addWidget(new QLabel(i18n("Pressure:"), this), 1, 0); + grid->addWidget(pressureLabel = new QLabel, 1, 1); + + grid->addWidget(new QLabel(i18n("Humidity:"), this), 2, 0); + grid->addWidget(humidityLabel = new QLabel, 2, 1); + + grid->addWidget(new QLabel(i18n("Wind Speed:"), this), 3, 0); + grid->addWidget(windSpeedLabel = new QLabel, 3, 1); + + grid->addWidget(new QLabel(i18n("Wind Direction:"), this), 4, 0); + grid->addWidget(windDirectionLabel = new QLabel, 4, 1); + + for (int i = 0; i < 4; i++) + { + shortForecast[i] = new ForecastWidget(this, false); + grid->addWidget(shortForecast[i], 0, 2 + i, 5, 1, Qt::AlignCenter); + } + + QHBoxLayout *hbox = new QHBoxLayout; + vbox->addLayout(hbox); + + for (int i = 0; i < 5; i++) + { + forecast[i] = new ForecastWidget(this); + hbox->addWidget(forecast[i]); + + if ( i < 4 ) + hbox->addStretch(); + } + + if ( !apiKey.isEmpty() && !cityId.isEmpty() ) + { + fetchData(); + timer.start(); + } + else + { + cityLabel->setText(i18n("Not configured")); + } + + QAction *action = new QAction(this); + action->setText(i18n("Configure...")); + action->setIcon(QIcon::fromTheme("configure")); + insertAction(actions()[0], action); + connect(action, &QAction::triggered, this, &WeatherApplet::configure); +} + +//-------------------------------------------------------------------------------- + +void WeatherApplet::loadConfig() +{ + KConfig config; + KConfigGroup group = config.group("Weather"); + apiKey = group.readEntry("apiKey", QString()); + group = config.group(id); + cityId = group.readEntry("cityId", QString()); + units = group.readEntry("units", QString("metric")); + + QColor textCol = group.readEntry("textCol", QColor(Qt::white)); + QColor backCol = group.readEntry("backCol", QColor(32, 56, 92, 190)); + QPalette pal; + pal.setColor(foregroundRole(), textCol); + pal.setColor(backgroundRole(), backCol); + setPalette(pal); + + DesktopApplet::loadConfig(); +} + +//-------------------------------------------------------------------------------- + +void WeatherApplet::showEvent(QShowEvent *) +{ + fetchData(); +} + +//-------------------------------------------------------------------------------- + +void WeatherApplet::fetchData() +{ + if ( !isVisible() || apiKey.isEmpty() || cityId.isEmpty() ) + return; + + QString url = QString("http://api.openweathermap.org/data/2.5/weather?APPID=%1&units=%2&id=%3") + .arg(apiKey, units, cityId); + + KIO::StoredTransferJob *job = KIO::storedGet(QUrl(url), KIO::Reload, KIO::HideProgressInfo); + connect(job, &KIO::Job::result, this, &WeatherApplet::gotData); + + url = QString("http://api.openweathermap.org/data/2.5/forecast?APPID=%1&units=%2&id=%3") + .arg(apiKey, units, cityId); + + job = KIO::storedGet(QUrl(url), KIO::Reload, KIO::HideProgressInfo); + connect(job, &KIO::Job::result, this, &WeatherApplet::gotData); +} + +//-------------------------------------------------------------------------------- + +void WeatherApplet::gotData(KJob *job) +{ + if ( job->error() ) + return; + + QJsonDocument doc = QJsonDocument::fromJson(static_cast(job)->data()); + if ( doc.isNull() || !doc.isObject() ) + return; + + QString tempUnit = "°K"; + if ( units == "metric" ) tempUnit = "°C"; + else if ( units == "imperial" ) tempUnit = "°F"; + + QJsonObject data = doc.object(); + + if ( data.contains("city") && data["city"].isObject() ) + cityLabel->setText(data["city"].toObject()["name"].toString()); + + // current + if ( data.contains("main") && data["main"].isObject() ) + { + QJsonObject mainData = data["main"].toObject(); + double temp = mainData["temp"].toDouble(); + + tempLabel->setText(i18n("%1 %2").arg(temp, 0, 'f', 1).arg(tempUnit)); + + double pressure = mainData["pressure"].toDouble(); + pressureLabel->setText(i18n("%1 hPa").arg(pressure, 0, 'f', 1)); + + double humidity = mainData["humidity"].toDouble(); + humidityLabel->setText(i18n("%1 %").arg(humidity, 0, 'f', 1)); + } + + if ( data.contains("wind") && data["wind"].isObject() ) + { + QJsonObject windData = data["wind"].toObject(); + + QString speedUnit = "m/s"; + if ( units == "imperial" ) speedUnit = "mi/h"; + + double speed = windData["speed"].toDouble(); + windSpeedLabel->setText(i18n("%1 %2").arg(speed, 0, 'f', 0).arg(speedUnit)); + + double deg = windData["deg"].toDouble(); + windDirectionLabel->setText(i18n("%1 °").arg(deg, 0, 'f', 0)); + } + + if ( data.contains("weather") && data["weather"].isArray() ) + { + QDateTime dt = QDateTime::fromSecsSinceEpoch(data["dt"].toInt()); + shortForecast[0]->day->setText(dt.time().toString(Qt::SystemLocaleShortDate)); + setIcon(shortForecast[0]->icon, data["weather"].toArray()[0].toObject()["icon"].toString()); + } + + // forecast + if ( data.contains("list") && data["list"].isArray() ) + { + for (int i = 0; i < 5; i++) + forecast[i]->hide(); + + QJsonArray array = data["list"].toArray(); + + // 3 hours short forecast + for (int i = 0; i < 3; i++) + { + setIcon(shortForecast[1 + i]->icon, array[i].toObject()["weather"].toArray()[0].toObject()["icon"].toString()); + QDateTime dt = QDateTime::fromSecsSinceEpoch(array[i].toObject()["dt"].toInt()); + shortForecast[1 + i]->day->setText(dt.time().toString(Qt::SystemLocaleShortDate)); + shortForecast[1 + i]->show(); + } + + QHash minTemp, maxTemp; // key = day + + for (QJsonValue value : array) + { + QJsonObject obj = value.toObject(); + + int day = QDateTime::fromSecsSinceEpoch(obj["dt"].toInt()).date().dayOfWeek(); + double temp = obj["main"].toObject()["temp"].toDouble(); + + if ( !minTemp.contains(day) ) + { + minTemp.insert(day, temp); + maxTemp.insert(day, temp); + } + else + { + if ( temp < minTemp[day] ) minTemp[day] = temp; + if ( temp > maxTemp[day] ) maxTemp[day] = temp; + } + } + + int idx = 0; + for (QJsonValue value : array) + { + QJsonObject obj = value.toObject(); + + if ( obj["dt_txt"].toString().contains("12:00") ) + { + QString icon = obj["weather"].toArray()[0].toObject()["icon"].toString(); + setIcon(forecast[idx]->icon, icon); + + int day = QDateTime::fromSecsSinceEpoch(obj["dt"].toInt()).date().dayOfWeek(); + forecast[idx]->day->setText(QDate::shortDayName(day)); + forecast[idx]->min->setText(i18n("%1 %2").arg(minTemp[day], 0, 'f', 1).arg(tempUnit)); + forecast[idx]->max->setText(i18n("%1 %2").arg(maxTemp[day], 0, 'f', 1).arg(tempUnit)); + forecast[idx]->show(); + idx++; + if ( idx == 5 ) break; + } + } + } + + timer.start(); // after showEvent make sure to wait another full timeout phase +} + +//-------------------------------------------------------------------------------- + +void WeatherApplet::setIcon(QLabel *label, const QString &icon) +{ + QString cacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + + "/api.openweathermap.org/"; + QDir dir; + dir.mkpath(cacheDir); + QString filePath = cacheDir + icon + ".png"; + + if ( QFile::exists(filePath) ) + { + QPixmap pixmap(filePath); + if ( !pixmap.isNull() ) + label->setPixmap(pixmap); + } + else + { + KIO::StoredTransferJob *job = + KIO::storedGet(QUrl("http://api.openweathermap.org/img/w/" + icon), KIO::Reload, KIO::HideProgressInfo); + + connect(job, &KIO::Job::result, + [label, filePath](KJob *job) + { + if ( job->error() ) + return; + + QPixmap pixmap; + pixmap.loadFromData(static_cast(job)->data()); + if ( !pixmap.isNull() ) + { + label->setPixmap(pixmap); + pixmap.save(filePath, "PNG"); + } + }); + } +} + +//-------------------------------------------------------------------------------- +//-------------------------------------------------------------------------------- +//-------------------------------------------------------------------------------- + +ForecastWidget::ForecastWidget(QWidget *parent, bool showMinMax) + : QWidget(parent) +{ + QGridLayout *grid = new QGridLayout(this); + + if ( showMinMax ) + { + min = new QLabel(this); + max = new QLabel(this); + + min->setAlignment(Qt::AlignRight); + max->setAlignment(Qt::AlignRight); + + grid->addWidget(max, 0, 1); + grid->addWidget(min, 1, 1); + } + + day = new QLabel(this); + icon = new QLabel(this); + + day->setAlignment(Qt::AlignCenter); + + icon->setFixedSize(64, 64); + icon->setScaledContents(true); + + grid->addWidget(day, 2, 0, 1, 2); + grid->addWidget(icon, 0, 0, 2, 1); + + setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum); +} + +//-------------------------------------------------------------------------------- + +void WeatherApplet::configure() +{ + if ( dialog ) + { + dialog->raise(); + dialog->activateWindow(); + return; + } + + dialog = new WeatherAppletConfigureDialog(this); + dialog->setWindowTitle(i18n("Configure Weather Applet")); + + dialog->setAttribute(Qt::WA_DeleteOnClose); + dialog->show(); + + connect(dialog, &QDialog::accepted, + [this]() + { + KConfig config; + KConfigGroup group = config.group("Weather"); + group.writeEntry("apiKey", apiKey); + group = config.group(id); + group.writeEntry("cityId", cityId); + group.writeEntry("units", units); + + group.writeEntry("textCol", palette().color(foregroundRole())); + group.writeEntry("backCol", palette().color(backgroundRole())); + + if ( !apiKey.isEmpty() && !cityId.isEmpty() ) + { + fetchData(); + timer.start(); + } + else + { + cityLabel->setText(i18n("Not configured")); + } + }); +} + +//-------------------------------------------------------------------------------- diff --git a/WeatherApplet.hxx b/WeatherApplet.hxx new file mode 100644 index 0000000..8f7322b --- /dev/null +++ b/WeatherApplet.hxx @@ -0,0 +1,88 @@ +/* + Copyright 2017 Martin Koller, kollix@aon.at + + This file is part of liquidshell. + + liquidshell 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 3 of the License, or + (at your option) any later version. + + liquidshell 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 liquidshell. If not, see . +*/ + +#ifndef _WeatherApplet_H_ +#define _WeatherApplet_H_ + +#include +#include + +#include +#include +#include + +#include + +// applet using data from http://api.openweathermap.org + +class WeatherApplet : public DesktopApplet +{ + Q_OBJECT + + public: + WeatherApplet(QWidget *parent, const QString &theId); + + void loadConfig() override; + + protected: + void showEvent(QShowEvent *event) override; + + private slots: + void fetchData(); + void gotData(KJob *job); + void configure(); + + private: // methods + void setIcon(QLabel *label, const QString &icon); + + private: // members + static QString apiKey; // see http://openweathermap.org/api + QString cityId, units; + QTimer timer; + QLabel *cityLabel = nullptr; + QLabel *tempLabel = nullptr; + QLabel *pressureLabel = nullptr; + QLabel *humidityLabel = nullptr; + QLabel *windSpeedLabel = nullptr; + QLabel *windDirectionLabel = nullptr; + + class ForecastWidget *shortForecast[4] = { nullptr }; + class ForecastWidget *forecast[5] = { nullptr }; + + QPointer dialog; + + friend WeatherAppletConfigureDialog; +}; + +//-------------------------------------------------------------------------------- + +class ForecastWidget : public QWidget +{ + Q_OBJECT + + public: + ForecastWidget(QWidget *parent, bool showMinMax = true); + + QLabel *min = nullptr; + QLabel *max = nullptr; + QLabel *day = nullptr; + QLabel *icon = nullptr; +}; + +#endif diff --git a/WeatherAppletConfigureDialog.cxx b/WeatherAppletConfigureDialog.cxx new file mode 100644 index 0000000..93f2b40 --- /dev/null +++ b/WeatherAppletConfigureDialog.cxx @@ -0,0 +1,172 @@ +/* + Copyright 2017 Martin Koller, kollix@aon.at + + This file is part of liquidshell. + + liquidshell 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 3 of the License, or + (at your option) any later version. + + liquidshell 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 liquidshell. If not, see . +*/ + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +//-------------------------------------------------------------------------------- + +WeatherAppletConfigureDialog::WeatherAppletConfigureDialog(WeatherApplet *parent) + : QDialog(parent), applet(parent) +{ + ui.setupUi(this); + connect(ui.city, &QLineEdit::textEdited, [this](const QString &txt) + { auto found = ui.cityList->findItems(txt, Qt::MatchStartsWith); + if ( found.count() ) ui.cityList->setCurrentItem(found[0]); }); + + connect(ui.cityList, &QTreeWidget::itemClicked, + [this](QTreeWidgetItem *current) { ui.city->setText(current->text(0)); }); + + ui.apiKey->setText(applet->apiKey); + if ( applet->units == "metric" ) + ui.metric->setChecked(true); + else + ui.imperial->setChecked(true); + + connect(ui.getApiKey, &QPushButton::clicked, []() { new KRun(QUrl("http://openweathermap.org/appid"), nullptr); }); + + ui.textColor->setColor(applet->palette().color(applet->foregroundRole())); + ui.backgroundColor->setColor(applet->palette().color(applet->backgroundRole())); + + // for city selection, we need the json file + QString cacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + + "/api.openweathermap.org/"; + + QString filePath = cacheDir + "city.list.json.gz"; + + if ( QFile::exists(filePath) ) + QTimer::singleShot(0, [this, filePath]() { readJsonFile(filePath); }); + else + { + KIO::CopyJob *job = KIO::copy(QUrl("http://bulk.openweathermap.org/sample/city.list.json.gz"), + QUrl::fromLocalFile(filePath)); + + connect(job, &KIO::Job::result, this, &WeatherAppletConfigureDialog::gotJsonFile); + + ui.city->setText(i18n("Downloading city list...")); + ui.city->setReadOnly(true); + } +} + +//-------------------------------------------------------------------------------- + +void WeatherAppletConfigureDialog::gotJsonFile(KJob *job) +{ + ui.city->setText(QString()); + ui.city->setReadOnly(false); + + if ( job->error() ) + { + QMessageBox::warning(this, i18n("Download Error"), + i18n("Error on downloading city list: %1").arg(job->errorString())); + return; + } + readJsonFile(static_cast(job)->destUrl().toLocalFile()); +} + +//-------------------------------------------------------------------------------- + +void WeatherAppletConfigureDialog::readJsonFile(const QString &filePath) +{ + ui.city->setFocus(); + + KFilterDev jsonFile(filePath); + if ( !jsonFile.open(QIODevice::ReadOnly) ) + return; + + QJsonArray array = QJsonDocument::fromJson(jsonFile.readAll()).array(); + + QApplication::setOverrideCursor(QCursor(Qt::WaitCursor)); + + int i = 0; + for (const QJsonValue &value : array) + { + if ( value.isObject() ) + { + QJsonObject obj = value.toObject(); + QString city = obj["name"].toString(); + QString country = obj["country"].toString(); + + if ( !city.isEmpty() && (city != "-") && !country.isEmpty() ) + { + QTreeWidgetItem *item = new QTreeWidgetItem(ui.cityList); + item->setText(0, city); + item->setText(1, country); + item->setData(0, Qt::UserRole, obj["id"].toInt()); + } + } + if ( (i++ % 40000) == 0 ) + QApplication::processEvents(); + } + + ui.cityList->setSortingEnabled(true); + ui.cityList->sortByColumn(0, Qt::AscendingOrder); + ui.cityList->header()->resizeSections(QHeaderView::ResizeToContents); + + for (int i = 0; i < ui.cityList->topLevelItemCount(); i++) + { + if ( ui.cityList->topLevelItem(i)->data(0, Qt::UserRole).toString() == applet->cityId ) + { + ui.cityList->setCurrentItem(ui.cityList->topLevelItem(i)); + ui.city->setText(ui.cityList->topLevelItem(i)->text(0)); + break; + } + } + + QApplication::restoreOverrideCursor(); +} + +//-------------------------------------------------------------------------------- + +void WeatherAppletConfigureDialog::accept() +{ + applet->apiKey = ui.apiKey->text(); + + auto selected = ui.cityList->selectedItems(); + if ( selected.count() ) + applet->cityId = selected[0]->data(0, Qt::UserRole).toString(); + + if ( ui.metric->isChecked() ) + applet->units = "metric"; + else + applet->units = "imperial"; + + QPalette pal = applet->palette(); + pal.setColor(applet->foregroundRole(), ui.textColor->color()); + pal.setColor(applet->backgroundRole(), ui.backgroundColor->color()); + applet->setPalette(pal); + + QDialog::accept(); +} + +//-------------------------------------------------------------------------------- diff --git a/WeatherAppletConfigureDialog.hxx b/WeatherAppletConfigureDialog.hxx new file mode 100644 index 0000000..d33448f --- /dev/null +++ b/WeatherAppletConfigureDialog.hxx @@ -0,0 +1,48 @@ +/* + Copyright 2017 Martin Koller, kollix@aon.at + + This file is part of liquidshell. + + liquidshell 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 3 of the License, or + (at your option) any later version. + + liquidshell 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 liquidshell. If not, see . +*/ + +#ifndef _WeatherAppletConfigureDialog_H_ +#define _WeatherAppletConfigureDialog_H_ + +#include +#include +#include +class WeatherApplet; + +class WeatherAppletConfigureDialog : public QDialog +{ + Q_OBJECT + + public: + WeatherAppletConfigureDialog(WeatherApplet *parent); + + public slots: + void accept() override; + + private slots: + void gotJsonFile(KJob *job); + void readJsonFile(const QString &filePath); + + private: + WeatherApplet *applet; + Ui::WeatherAppletConfigureDialog ui; + QString cityId; +}; + +#endif diff --git a/WeatherAppletConfigureDialog.ui b/WeatherAppletConfigureDialog.ui new file mode 100644 index 0000000..33697d8 --- /dev/null +++ b/WeatherAppletConfigureDialog.ui @@ -0,0 +1,181 @@ + + + WeatherAppletConfigureDialog + + + + 0 + 0 + 638 + 555 + + + + Dialog + + + + + + API Key + + + + + + + The API key is needed to be allowed to get data from openweathermap.org + + + + + + + Get your personal API key + + + + + + + Units + + + + + + + &metric + + + true + + + + + + + &imperial + + + + + + + City + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + false + + + true + + + 2 + + + + City + + + + + Country + + + + + + + + true + + + + + + + Text + + + + + + + Background + + + + + + + true + + + + + + + true + + + + + + + + KColorButton + QPushButton +
kcolorbutton.h
+
+
+ + + + buttonBox + accepted() + WeatherAppletConfigureDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + WeatherAppletConfigureDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + +
diff --git a/stylesheet.css b/stylesheet.css index 15d3c55..71c2985 100644 --- a/stylesheet.css +++ b/stylesheet.css @@ -38,3 +38,18 @@ StartMenu::menu-indicator { image: url() } + +/* +WeatherApplet +{ + background: rgba(32, 56, 92, 75%); + border: none; + color: white; + font-size: 20px; +} + +WeatherApplet QLabel#city +{ + font-size: 28px; +} +*/