diff --git a/applets/appmenu/lib/appmenuapplet.cpp b/applets/appmenu/lib/appmenuapplet.cpp index 8b25ebc2b..5d9afd9d1 100644 --- a/applets/appmenu/lib/appmenuapplet.cpp +++ b/applets/appmenu/lib/appmenuapplet.cpp @@ -1,296 +1,296 @@ /* * Copyright 2016 Kai Uwe Broulik * * 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) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * 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 "appmenuapplet.h" #include "../plugin/appmenumodel.h" #include #include #include #include #include #include #include #include #include #include int AppMenuApplet::s_refs = 0; namespace { QString viewService() { return QStringLiteral("org.kde.kappmenuview"); } } AppMenuApplet::AppMenuApplet(QObject *parent, const QVariantList &data) : Plasma::Applet(parent, data) { ++s_refs; - //if we're the first, regster the service + //if we're the first, register the service if (s_refs == 1) { QDBusConnection::sessionBus().interface()->registerService(viewService(), QDBusConnectionInterface::QueueService, QDBusConnectionInterface::DontAllowReplacement); } /*it registers or unregisters the service when the destroyed value of the applet change, and not in the dtor, because: when we "delete" an applet, it just hides it for about a minute setting its status to destroyed, in order to be able to do a clean undo: if we undo, there will be another destroyedchanged and destroyed will be false. When this happens, if we are the only appmenu applet existing, the dbus interface will have to be registered again*/ connect(this, &Applet::destroyedChanged, this, [](bool destroyed) { if (destroyed) { //if we were the last, unregister if (--s_refs == 0) { QDBusConnection::sessionBus().interface()->unregisterService(viewService()); } } else { - //if we're the first, regster the service + //if we're the first, register the service if (++s_refs == 1) { QDBusConnection::sessionBus().interface()->registerService(viewService(), QDBusConnectionInterface::QueueService, QDBusConnectionInterface::DontAllowReplacement); } } }); } AppMenuApplet::~AppMenuApplet() = default; void AppMenuApplet::init() { } AppMenuModel *AppMenuApplet::model() const { return m_model; } void AppMenuApplet::setModel(AppMenuModel *model) { if (m_model != model) { m_model = model; emit modelChanged(); } } int AppMenuApplet::view() const { return m_viewType; } void AppMenuApplet::setView(int type) { if (m_viewType != type) { m_viewType = type; emit viewChanged(); } } int AppMenuApplet::currentIndex() const { return m_currentIndex; } void AppMenuApplet::setCurrentIndex(int currentIndex) { if (m_currentIndex != currentIndex) { m_currentIndex = currentIndex; emit currentIndexChanged(); } } QQuickItem *AppMenuApplet::buttonGrid() const { return m_buttonGrid; } void AppMenuApplet::setButtonGrid(QQuickItem *buttonGrid) { if (m_buttonGrid != buttonGrid) { m_buttonGrid = buttonGrid; emit buttonGridChanged(); } } QMenu *AppMenuApplet::createMenu(int idx) const { QMenu *menu = nullptr; QAction *action = nullptr; if (view() == CompactView) { menu = new QMenu(); for (int i=0; irowCount(); i++) { const QModelIndex index = m_model->index(i, 0); const QVariant data = m_model->data(index, AppMenuModel::ActionRole); action = (QAction *)data.value(); menu->addAction(action); } menu->setAttribute(Qt::WA_DeleteOnClose); } else if (view() == FullView) { const QModelIndex index = m_model->index(idx, 0); const QVariant data = m_model->data(index, AppMenuModel::ActionRole); action = (QAction *)data.value(); if (action) { menu = action->menu(); } } return menu; } void AppMenuApplet::onMenuAboutToHide() { setCurrentIndex(-1); } void AppMenuApplet::trigger(QQuickItem *ctx, int idx) { if (m_currentIndex == idx) { return; } if (!ctx || !ctx->window() || !ctx->window()->screen()) { return; } QMenu *actionMenu = createMenu(idx); if (actionMenu) { //this is a workaround where Qt will fail to realize a mouse has been released // this happens if a window which does not accept focus spawns a new window that takes focus and X grab // whilst the mouse is depressed // https://bugreports.qt.io/browse/QTBUG-59044 // this causes the next click to go missing //by releasing manually we avoid that situation auto ungrabMouseHack = [ctx]() { if (ctx && ctx->window() && ctx->window()->mouseGrabberItem()) { // FIXME event forge thing enters press and hold move mode :/ ctx->window()->mouseGrabberItem()->ungrabMouse(); } }; QTimer::singleShot(0, ctx, ungrabMouseHack); //end workaround const auto &geo = ctx->window()->screen()->availableVirtualGeometry(); QPoint pos = ctx->window()->mapToGlobal(ctx->mapToScene(QPointF()).toPoint()); if (location() == Plasma::Types::TopEdge) { pos.setY(pos.y() + ctx->height()); } actionMenu->adjustSize(); pos = QPoint(qBound(geo.x(), pos.x(), geo.x() + geo.width() - actionMenu->width()), qBound(geo.y(), pos.y(), geo.y() + geo.height() - actionMenu->height())); if (view() == FullView) { actionMenu->installEventFilter(this); } actionMenu->winId();//create window handle actionMenu->windowHandle()->setTransientParent(ctx->window()); actionMenu->popup(pos); if (view() == FullView) { // hide the old menu only after showing the new one to avoid brief flickering // in other windows as they briefly re-gain focus QMenu *oldMenu = m_currentMenu; m_currentMenu = actionMenu; if (oldMenu && oldMenu != actionMenu) { //don't initialize the currentIndex when another menu is already shown disconnect(oldMenu, &QMenu::aboutToHide, this, &AppMenuApplet::onMenuAboutToHide); oldMenu->hide(); } } setCurrentIndex(idx); // FIXME TODO connect only once connect(actionMenu, &QMenu::aboutToHide, this, &AppMenuApplet::onMenuAboutToHide, Qt::UniqueConnection); } else { // is it just an action without a menu? const QVariant data = m_model->index(idx, 0).data(AppMenuModel::ActionRole); QAction *action = static_cast(data.value()); if (action) { Q_ASSERT(!action->menu()); action->trigger(); } } } // FIXME TODO doesn't work on submenu bool AppMenuApplet::eventFilter(QObject *watched, QEvent *event) { auto *menu = qobject_cast(watched); if (!menu) { return false; } if (event->type() == QEvent::KeyPress) { auto *e = static_cast(event); // TODO right to left languages if (e->key() == Qt::Key_Left) { int desiredIndex = m_currentIndex - 1; emit requestActivateIndex(desiredIndex); return true; } else if (e->key() == Qt::Key_Right) { if (menu->activeAction() && menu->activeAction()->menu()) { return false; } int desiredIndex = m_currentIndex + 1; emit requestActivateIndex(desiredIndex); return true; } } else if (event->type() == QEvent::MouseMove) { auto *e = static_cast(event); if (!m_buttonGrid || !m_buttonGrid->window()) { return false; } // FIXME the panel margin breaks Fitt's law :( const QPointF &windowLocalPos = m_buttonGrid->window()->mapFromGlobal(e->globalPos()); const QPointF &buttonGridLocalPos = m_buttonGrid->mapFromScene(windowLocalPos); auto *item = m_buttonGrid->childAt(buttonGridLocalPos.x(), buttonGridLocalPos.y()); if (!item) { return false; } bool ok; const int buttonIndex = item->property("buttonIndex").toInt(&ok); if (!ok) { return false; } emit requestActivateIndex(buttonIndex); } return false; } K_EXPORT_PLASMA_APPLET_WITH_JSON(appmenu, AppMenuApplet, "metadata.json") #include "appmenuapplet.moc" diff --git a/dataengines/weather/ions/envcan/ion_envcan.cpp b/dataengines/weather/ions/envcan/ion_envcan.cpp index 0b1a141ab..ba66d2c82 100644 --- a/dataengines/weather/ions/envcan/ion_envcan.cpp +++ b/dataengines/weather/ions/envcan/ion_envcan.cpp @@ -1,1674 +1,1674 @@ /*************************************************************************** * Copyright (C) 2007-2011,2019 by Shawn Starr * * * * 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, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA * ***************************************************************************/ /* Ion for Environment Canada XML data */ #include "ion_envcan.h" #include "ion_envcandebug.h" #include #include #include #include #include WeatherData::WeatherData() : stationLatitude(qQNaN()) , stationLongitude(qQNaN()) , temperature(qQNaN()) , dewpoint(qQNaN()) , windchill(qQNaN()) , pressure(qQNaN()) , visibility(qQNaN()) , humidity(qQNaN()) , windSpeed(qQNaN()) , windGust(qQNaN()) , normalHigh(qQNaN()) , normalLow(qQNaN()) , prevHigh(qQNaN()) , prevLow(qQNaN()) , recordHigh(qQNaN()) , recordLow(qQNaN()) , recordRain(qQNaN()) , recordSnow(qQNaN()) { } WeatherData::ForecastInfo::ForecastInfo() : tempHigh(qQNaN()) , tempLow(qQNaN()) , popPrecent(qQNaN()) { } // ctor, dtor EnvCanadaIon::EnvCanadaIon(QObject *parent, const QVariantList &args) : IonInterface(parent, args) { // Get the real city XML URL so we can parse this getXMLSetup(); } void EnvCanadaIon::deleteForecasts() { QMutableHashIterator it(m_weatherData); while (it.hasNext()) { it.next(); WeatherData &item = it.value(); qDeleteAll(item.warnings); item.warnings.clear(); qDeleteAll(item.watches); item.watches.clear(); qDeleteAll(item.forecasts); item.forecasts.clear(); } } void EnvCanadaIon::reset() { deleteForecasts(); emitWhenSetup = true; m_sourcesToReset = sources(); getXMLSetup(); } EnvCanadaIon::~EnvCanadaIon() { // Destroy each watch/warning stored in a QVector deleteForecasts(); } QMap EnvCanadaIon::setupConditionIconMappings() const { return QMap { // Explicit periods { QStringLiteral("mainly sunny"), FewCloudsDay }, { QStringLiteral("mainly clear"), FewCloudsNight }, { QStringLiteral("sunny"), ClearDay }, { QStringLiteral("clear"), ClearNight }, // Available conditions { QStringLiteral("blowing snow"), Snow }, { QStringLiteral("cloudy"), Overcast }, { QStringLiteral("distant precipitation"), LightRain }, { QStringLiteral("drifting snow"), Flurries }, { QStringLiteral("drizzle"), LightRain }, { QStringLiteral("dust"), NotAvailable }, { QStringLiteral("dust devils"), NotAvailable }, { QStringLiteral("fog"), Mist }, { QStringLiteral("fog bank near station"), Mist }, { QStringLiteral("fog depositing ice"), Mist }, { QStringLiteral("fog patches"), Mist }, { QStringLiteral("freezing drizzle"), FreezingDrizzle }, { QStringLiteral("freezing rain"), FreezingRain }, { QStringLiteral("funnel cloud"), NotAvailable }, { QStringLiteral("hail"), Hail }, { QStringLiteral("haze"), Haze }, { QStringLiteral("heavy blowing snow"), Snow }, { QStringLiteral("heavy drifting snow"), Snow }, { QStringLiteral("heavy drizzle"), LightRain }, { QStringLiteral("heavy hail"), Hail }, { QStringLiteral("heavy mixed rain and drizzle"), LightRain }, { QStringLiteral("heavy mixed rain and snow shower"), RainSnow }, { QStringLiteral("heavy rain"), Rain }, { QStringLiteral("heavy rain and snow"), RainSnow }, { QStringLiteral("heavy rainshower"), Rain }, { QStringLiteral("heavy snow"), Snow }, { QStringLiteral("heavy snow pellets"), Snow }, { QStringLiteral("heavy snowshower"), Snow }, { QStringLiteral("heavy thunderstorm with hail"), Thunderstorm }, { QStringLiteral("heavy thunderstorm with rain"), Thunderstorm }, { QStringLiteral("ice crystals"), Flurries }, { QStringLiteral("ice pellets"), Hail }, { QStringLiteral("increasing cloud"), Overcast }, { QStringLiteral("light drizzle"), LightRain }, { QStringLiteral("light freezing drizzle"), FreezingRain }, { QStringLiteral("light freezing rain"), FreezingRain }, { QStringLiteral("light rain"), LightRain }, { QStringLiteral("light rainshower"), LightRain }, { QStringLiteral("light snow"), LightSnow }, { QStringLiteral("light snow pellets"), LightSnow }, { QStringLiteral("light snowshower"), Flurries }, { QStringLiteral("lightning visible"), Thunderstorm }, { QStringLiteral("mist"), Mist }, { QStringLiteral("mixed rain and drizzle"), LightRain }, { QStringLiteral("mixed rain and snow shower"), RainSnow }, { QStringLiteral("not reported"), NotAvailable }, { QStringLiteral("rain"), Rain }, { QStringLiteral("rain and snow"), RainSnow }, { QStringLiteral("rainshower"), LightRain }, { QStringLiteral("recent drizzle"), LightRain }, { QStringLiteral("recent dust or sand storm"), NotAvailable }, { QStringLiteral("recent fog"), Mist }, { QStringLiteral("recent freezing precipitation"), FreezingDrizzle }, { QStringLiteral("recent hail"), Hail }, { QStringLiteral("recent rain"), Rain }, { QStringLiteral("recent rain and snow"), RainSnow }, { QStringLiteral("recent rainshower"), Rain }, { QStringLiteral("recent snow"), Snow }, { QStringLiteral("recent snowshower"), Flurries }, { QStringLiteral("recent thunderstorm"), Thunderstorm }, { QStringLiteral("recent thunderstorm with hail"), Thunderstorm }, { QStringLiteral("recent thunderstorm with heavy hail"), Thunderstorm }, { QStringLiteral("recent thunderstorm with heavy rain"), Thunderstorm }, { QStringLiteral("recent thunderstorm with rain"), Thunderstorm }, { QStringLiteral("sand or dust storm"), NotAvailable }, { QStringLiteral("severe sand or dust storm"), NotAvailable }, { QStringLiteral("shallow fog"), Mist }, { QStringLiteral("smoke"), NotAvailable }, { QStringLiteral("snow"), Snow }, { QStringLiteral("snow crystals"), Flurries }, { QStringLiteral("snow grains"), Flurries }, { QStringLiteral("squalls"), Snow }, { QStringLiteral("thunderstorm"), Thunderstorm }, { QStringLiteral("thunderstorm with hail"), Thunderstorm }, { QStringLiteral("thunderstorm with rain"), Thunderstorm }, { QStringLiteral("thunderstorm with light rainshowers"), Thunderstorm }, { QStringLiteral("thunderstorm with heavy rainshowers"), Thunderstorm }, { QStringLiteral("thunderstorm with sand or dust storm"), Thunderstorm }, { QStringLiteral("thunderstorm without precipitation"), Thunderstorm }, { QStringLiteral("tornado"), NotAvailable }, }; } QMap EnvCanadaIon::setupForecastIconMappings() const { return QMap { // Abbreviated forecast descriptions { QStringLiteral("a few flurries"), Flurries }, { QStringLiteral("a few flurries mixed with ice pellets"), RainSnow }, { QStringLiteral("a few flurries or rain showers"), RainSnow }, { QStringLiteral("a few flurries or thundershowers"), RainSnow }, { QStringLiteral("a few rain showers or flurries"), RainSnow }, { QStringLiteral("a few rain showers or wet flurries"), RainSnow }, { QStringLiteral("a few showers"), LightRain }, { QStringLiteral("a few showers or drizzle"), LightRain }, { QStringLiteral("a few showers or thundershowers"), Thunderstorm }, { QStringLiteral("a few showers or thunderstorms"), Thunderstorm }, { QStringLiteral("a few thundershowers"), Thunderstorm }, { QStringLiteral("a few thunderstorms"), Thunderstorm }, { QStringLiteral("a few wet flurries"), RainSnow }, { QStringLiteral("a few wet flurries or rain showers"), RainSnow }, { QStringLiteral("a mix of sun and cloud"), PartlyCloudyDay }, { QStringLiteral("cloudy with sunny periods"), PartlyCloudyDay }, { QStringLiteral("partly cloudy"), PartlyCloudyDay }, { QStringLiteral("mainly cloudy"), PartlyCloudyDay }, { QStringLiteral("mainly sunny"), FewCloudsDay }, { QStringLiteral("sunny"), ClearDay }, { QStringLiteral("blizzard"), Snow }, { QStringLiteral("clear"), ClearNight }, { QStringLiteral("cloudy"), Overcast }, { QStringLiteral("drizzle"), LightRain }, { QStringLiteral("drizzle mixed with freezing drizzle"), FreezingDrizzle }, { QStringLiteral("drizzle mixed with rain"), LightRain }, { QStringLiteral("drizzle or freezing drizzle"), LightRain }, { QStringLiteral("drizzle or rain"), LightRain }, { QStringLiteral("flurries"), Flurries }, { QStringLiteral("flurries at times heavy"), Flurries }, { QStringLiteral("flurries at times heavy or rain snowers"), RainSnow }, { QStringLiteral("flurries mixed with ice pellets"), FreezingRain }, { QStringLiteral("flurries or ice pellets"), FreezingRain }, { QStringLiteral("flurries or rain showers"), RainSnow }, { QStringLiteral("flurries or thundershowers"), Flurries }, { QStringLiteral("fog"), Mist }, { QStringLiteral("fog developing"), Mist }, { QStringLiteral("fog dissipating"), Mist }, { QStringLiteral("fog patches"), Mist }, { QStringLiteral("freezing drizzle"), FreezingDrizzle }, { QStringLiteral("freezing rain"), FreezingRain }, { QStringLiteral("freezing rain mixed with rain"), FreezingRain }, { QStringLiteral("freezing rain mixed with snow"), FreezingRain }, { QStringLiteral("freezing rain or ice pellets"), FreezingRain }, { QStringLiteral("freezing rain or rain"), FreezingRain }, { QStringLiteral("freezing rain or snow"), FreezingRain }, { QStringLiteral("ice fog"), Mist }, { QStringLiteral("ice fog developing"), Mist }, { QStringLiteral("ice fog dissipating"), Mist }, { QStringLiteral("ice pellets"), Hail }, { QStringLiteral("ice pellets mixed with freezing rain"), Hail }, { QStringLiteral("ice pellets mixed with snow"), Hail }, { QStringLiteral("ice pellets or snow"), RainSnow }, { QStringLiteral("light snow"), LightSnow }, { QStringLiteral("light snow and blizzard"), LightSnow }, { QStringLiteral("light snow and blizzard and blowing snow"), Snow }, { QStringLiteral("light snow and blowing snow"), LightSnow }, { QStringLiteral("light snow mixed with freezing drizzle"), FreezingDrizzle }, { QStringLiteral("light snow mixed with freezing rain"), FreezingRain }, { QStringLiteral("light snow or ice pellets"), LightSnow }, { QStringLiteral("light snow or rain"), RainSnow }, { QStringLiteral("light wet snow"), RainSnow }, { QStringLiteral("light wet snow or rain"), RainSnow }, { QStringLiteral("local snow squalls"), Snow }, { QStringLiteral("near blizzard"), Snow }, { QStringLiteral("overcast"), Overcast }, { QStringLiteral("increasing cloudiness"), Overcast }, { QStringLiteral("increasing clouds"), Overcast }, { QStringLiteral("periods of drizzle"), LightRain }, { QStringLiteral("periods of drizzle mixed with freezing drizzle"), FreezingDrizzle }, { QStringLiteral("periods of drizzle mixed with rain"), LightRain }, { QStringLiteral("periods of drizzle or freezing drizzle"), FreezingDrizzle }, { QStringLiteral("periods of drizzle or rain"), LightRain }, { QStringLiteral("periods of freezing drizzle"), FreezingDrizzle }, { QStringLiteral("periods of freezing drizzle or drizzle"), FreezingDrizzle }, { QStringLiteral("periods of freezing drizzle or rain"), FreezingDrizzle }, { QStringLiteral("periods of freezing rain"), FreezingRain }, { QStringLiteral("periods of freezing rain mixed with ice pellets"), FreezingRain }, { QStringLiteral("periods of freezing rain mixed with rain"), FreezingRain }, { QStringLiteral("periods of freezing rain mixed with snow"), FreezingRain }, { QStringLiteral("periods of freezing rain mixed with freezing drizzle"), FreezingRain }, { QStringLiteral("periods of freezing rain or ice pellets"), FreezingRain }, { QStringLiteral("periods of freezing rain or rain"), FreezingRain }, { QStringLiteral("periods of freezing rain or snow"), FreezingRain }, { QStringLiteral("periods of ice pellets"), Hail }, { QStringLiteral("periods of ice pellets mixed with freezing rain"), Hail }, { QStringLiteral("periods of ice pellets mixed with snow"), Hail }, { QStringLiteral("periods of ice pellets or freezing rain"), Hail }, { QStringLiteral("periods of ice pellets or snow"), Hail }, { QStringLiteral("periods of light snow"), LightSnow }, { QStringLiteral("periods of light snow and blizzard"), Snow }, { QStringLiteral("periods of light snow and blizzard and blowing snow"), Snow }, { QStringLiteral("periods of light snow and blowing snow"), LightSnow }, { QStringLiteral("periods of light snow mixed with freezing drizzle"), RainSnow }, { QStringLiteral("periods of light snow mixed with freezing rain"), RainSnow }, { QStringLiteral("periods of light snow mixed with ice pellets"), LightSnow }, { QStringLiteral("periods of light snow mixed with rain"), RainSnow }, { QStringLiteral("periods of light snow or freezing drizzle"), RainSnow }, { QStringLiteral("periods of light snow or freezing rain"), RainSnow }, { QStringLiteral("periods of light snow or ice pellets"), LightSnow }, { QStringLiteral("periods of light snow or rain"), RainSnow }, { QStringLiteral("periods of light wet snow"), LightSnow }, { QStringLiteral("periods of light wet snow mixed with rain"), RainSnow }, { QStringLiteral("periods of light wet snow or rain"), RainSnow }, { QStringLiteral("periods of rain"), Rain }, { QStringLiteral("periods of rain mixed with freezing rain"), Rain }, { QStringLiteral("periods of rain mixed with snow"), RainSnow }, { QStringLiteral("periods of rain or drizzle"), Rain }, { QStringLiteral("periods of rain or freezing rain"), Rain }, { QStringLiteral("periods of rain or thundershowers"), Showers }, { QStringLiteral("periods of rain or thunderstorms"), Thunderstorm }, { QStringLiteral("periods of rain or snow"), RainSnow }, { QStringLiteral("periods of snow"), Snow }, { QStringLiteral("periods of snow and blizzard"), Snow }, { QStringLiteral("periods of snow and blizzard and blowing snow"), Snow }, { QStringLiteral("periods of snow and blowing snow"), Snow }, { QStringLiteral("periods of snow mixed with freezing drizzle"), RainSnow }, { QStringLiteral("periods of snow mixed with freezing rain"), RainSnow }, { QStringLiteral("periods of snow mixed with ice pellets"), Snow }, { QStringLiteral("periods of snow mixed with rain"), RainSnow }, { QStringLiteral("periods of snow or freezing drizzle"), RainSnow }, { QStringLiteral("periods of snow or freezing rain"), RainSnow }, { QStringLiteral("periods of snow or ice pellets"), Snow }, { QStringLiteral("periods of snow or rain"), RainSnow }, { QStringLiteral("periods of rain or snow"), RainSnow }, { QStringLiteral("periods of wet snow"), Snow }, { QStringLiteral("periods of wet snow mixed with rain"), RainSnow }, { QStringLiteral("periods of wet snow or rain"), RainSnow }, { QStringLiteral("rain"), Rain }, { QStringLiteral("rain at times heavy"), Rain }, { QStringLiteral("rain at times heavy mixed with freezing rain"), FreezingRain }, { QStringLiteral("rain at times heavy mixed with snow"), RainSnow }, { QStringLiteral("rain at times heavy or drizzle"), Rain }, { QStringLiteral("rain at times heavy or freezing rain"), Rain }, { QStringLiteral("rain at times heavy or snow"), RainSnow }, { QStringLiteral("rain at times heavy or thundershowers"), Showers }, { QStringLiteral("rain at times heavy or thunderstorms"), Thunderstorm }, { QStringLiteral("rain mixed with freezing rain"), FreezingRain }, { QStringLiteral("rain mixed with snow"), RainSnow }, { QStringLiteral("rain or drizzle"), Rain }, { QStringLiteral("rain or freezing rain"), Rain }, { QStringLiteral("rain or snow"), RainSnow }, { QStringLiteral("rain or thundershowers"), Showers }, { QStringLiteral("rain or thunderstorms"), Thunderstorm }, { QStringLiteral("rain showers or flurries"), RainSnow }, { QStringLiteral("rain showers or wet flurries"), RainSnow }, { QStringLiteral("showers"), Showers }, { QStringLiteral("showers at times heavy"), Showers }, { QStringLiteral("showers at times heavy or thundershowers"), Showers }, { QStringLiteral("showers at times heavy or thunderstorms"), Thunderstorm }, { QStringLiteral("showers or drizzle"), Showers }, { QStringLiteral("showers or thundershowers"), Thunderstorm }, { QStringLiteral("showers or thunderstorms"), Thunderstorm }, { QStringLiteral("smoke"), NotAvailable }, { QStringLiteral("snow"), Snow }, { QStringLiteral("snow and blizzard"), Snow }, { QStringLiteral("snow and blizzard and blowing snow"), Snow }, { QStringLiteral("snow and blowing snow"), Snow }, { QStringLiteral("snow at times heavy"), Snow }, { QStringLiteral("snow at times heavy and blizzard"), Snow }, { QStringLiteral("snow at times heavy and blowing snow"), Snow }, { QStringLiteral("snow at times heavy mixed with freezing drizzle"), RainSnow }, { QStringLiteral("snow at times heavy mixed with freezing rain"), RainSnow }, { QStringLiteral("snow at times heavy mixed with ice pellets"), Snow }, { QStringLiteral("snow at times heavy mixed with rain"), RainSnow }, { QStringLiteral("snow at times heavy or freezing rain"), RainSnow }, { QStringLiteral("snow at times heavy or ice pellets"), Snow }, { QStringLiteral("snow at times heavy or rain"), RainSnow }, { QStringLiteral("snow mixed with freezing drizzle"), RainSnow }, { QStringLiteral("snow mixed with freezing rain"), RainSnow }, { QStringLiteral("snow mixed with ice pellets"), Snow }, { QStringLiteral("snow mixed with rain"), RainSnow }, { QStringLiteral("snow or freezing drizzle"), RainSnow }, { QStringLiteral("snow or freezing rain"), RainSnow }, { QStringLiteral("snow or ice pellets"), Snow }, { QStringLiteral("snow or rain"), RainSnow }, { QStringLiteral("snow squalls"), Snow }, { QStringLiteral("sunny"), ClearDay }, { QStringLiteral("sunny with cloudy periods"), PartlyCloudyDay }, { QStringLiteral("thunderstorms"), Thunderstorm }, { QStringLiteral("thunderstorms and possible hail"), Thunderstorm }, { QStringLiteral("wet flurries"), Flurries }, { QStringLiteral("wet flurries at times heavy"), Flurries }, { QStringLiteral("wet flurries at times heavy or rain snowers"), RainSnow }, { QStringLiteral("wet flurries or rain showers"), RainSnow }, { QStringLiteral("wet snow"), Snow }, { QStringLiteral("wet snow at times heavy"), Snow }, { QStringLiteral("wet snow at times heavy mixed with rain"), RainSnow }, { QStringLiteral("wet snow mixed with rain"), RainSnow }, { QStringLiteral("wet snow or rain"), RainSnow }, { QStringLiteral("windy"), NotAvailable }, { QStringLiteral("chance of drizzle mixed with freezing drizzle"), LightRain }, { QStringLiteral("chance of flurries mixed with ice pellets"), Flurries }, { QStringLiteral("chance of flurries or ice pellets"), Flurries }, { QStringLiteral("chance of flurries or rain showers"), RainSnow }, { QStringLiteral("chance of flurries or thundershowers"), RainSnow }, { QStringLiteral("chance of freezing drizzle"), FreezingDrizzle }, { QStringLiteral("chance of freezing rain"), FreezingRain }, { QStringLiteral("chance of freezing rain mixed with snow"), RainSnow }, { QStringLiteral("chance of freezing rain or rain"), FreezingRain }, { QStringLiteral("chance of freezing rain or snow"), RainSnow }, { QStringLiteral("chance of light snow and blowing snow"), LightSnow }, { QStringLiteral("chance of light snow mixed with freezing drizzle"), LightSnow }, { QStringLiteral("chance of light snow mixed with ice pellets"), LightSnow }, { QStringLiteral("chance of light snow mixed with rain"), RainSnow }, { QStringLiteral("chance of light snow or freezing rain"), RainSnow }, { QStringLiteral("chance of light snow or ice pellets"), LightSnow }, { QStringLiteral("chance of light snow or rain"), RainSnow }, { QStringLiteral("chance of light wet snow"), Snow }, { QStringLiteral("chance of rain"), Rain }, { QStringLiteral("chance of rain at times heavy"), Rain }, { QStringLiteral("chance of rain mixed with snow"), RainSnow }, { QStringLiteral("chance of rain or drizzle"), Rain }, { QStringLiteral("chance of rain or freezing rain"), Rain }, { QStringLiteral("chance of rain or snow"), RainSnow }, { QStringLiteral("chance of rain showers or flurries"), RainSnow }, { QStringLiteral("chance of rain showers or wet flurries"), RainSnow }, { QStringLiteral("chance of severe thunderstorms"), Thunderstorm }, { QStringLiteral("chance of showers at times heavy"), Rain }, { QStringLiteral("chance of showers at times heavy or thundershowers"), Thunderstorm }, { QStringLiteral("chance of showers at times heavy or thunderstorms"), Thunderstorm }, { QStringLiteral("chance of showers or thundershowers"), Thunderstorm }, { QStringLiteral("chance of showers or thunderstorms"), Thunderstorm }, { QStringLiteral("chance of snow"), Snow }, { QStringLiteral("chance of snow and blizzard"), Snow }, { QStringLiteral("chance of snow mixed with freezing drizzle"), Snow }, { QStringLiteral("chance of snow mixed with freezing rain"), RainSnow }, { QStringLiteral("chance of snow mixed with rain"), RainSnow }, { QStringLiteral("chance of snow or rain"), RainSnow }, { QStringLiteral("chance of snow squalls"), Snow }, { QStringLiteral("chance of thundershowers"), Showers }, { QStringLiteral("chance of thunderstorms"), Thunderstorm }, { QStringLiteral("chance of thunderstorms and possible hail"), Thunderstorm }, { QStringLiteral("chance of wet flurries"), Flurries }, { QStringLiteral("chance of wet flurries at times heavy"), Flurries }, { QStringLiteral("chance of wet flurries or rain showers"), RainSnow }, { QStringLiteral("chance of wet snow"), Snow }, { QStringLiteral("chance of wet snow mixed with rain"), RainSnow }, { QStringLiteral("chance of wet snow or rain"), RainSnow }, }; } QMap const& EnvCanadaIon::conditionIcons() const { static QMap const condval = setupConditionIconMappings(); return condval; } QMap const& EnvCanadaIon::forecastIcons() const { static QMap const foreval = setupForecastIconMappings(); return foreval; } QStringList EnvCanadaIon::validate(const QString& source) const { QStringList placeList; QString sourceNormalized = source.toUpper(); QHash::const_iterator it = m_places.constBegin(); while (it != m_places.constEnd()) { if (it.key().toUpper().contains(sourceNormalized)) { placeList.append(QStringLiteral("place|") + it.key()); } ++it; } placeList.sort(); return placeList; } // Get a specific Ion's data bool EnvCanadaIon::updateIonSource(const QString& source) { //qCDebug(IONENGINE_ENVCAN) << "updateIonSource()" << source; // We expect the applet to send the source in the following tokenization: // ionname|validate|place_name - Triggers validation of place // ionname|weather|place_name - Triggers receiving weather of place const QStringList sourceAction = source.split(QLatin1Char('|')); // Guard: if the size of array is not 2 then we have bad data, return an error if (sourceAction.size() < 2) { setData(source, QStringLiteral("validate"), QStringLiteral("envcan|malformed")); return true; } if (sourceAction[1] == QLatin1String("validate") && sourceAction.size() > 2) { const QStringList result = validate(sourceAction[2]); const QString reply = (result.size() == 1) ? QStringLiteral("envcan|valid|single|") + result[0] : (result.size() > 1) ? QStringLiteral("envcan|valid|multiple|") + result.join(QLatin1Char('|')) : /*else*/ QStringLiteral("envcan|invalid|single|") + sourceAction[2]; setData(source, QStringLiteral("validate"), reply); return true; } if (sourceAction[1] == QLatin1String("weather") && sourceAction.size() > 2) { getXMLData(source); return true; } setData(source, QStringLiteral("validate"), QStringLiteral("envcan|malformed")); return true; } // Parses city list and gets the correct city based on ID number void EnvCanadaIon::getXMLSetup() { //qCDebug(IONENGINE_ENVCAN) << "getXMLSetup()"; // If network is down, we need to spin and wait const QUrl url(QStringLiteral("http://dd.weatheroffice.ec.gc.ca/citypage_weather/xml/siteList.xml")); KIO::TransferJob* getJob = KIO::get(url, KIO::NoReload, KIO::HideProgressInfo); m_xmlSetup.clear(); connect(getJob, &KIO::TransferJob::data, this, &EnvCanadaIon::setup_slotDataArrived); connect(getJob, &KJob::result, this, &EnvCanadaIon::setup_slotJobFinished); } // Gets specific city XML data void EnvCanadaIon::getXMLData(const QString& source) { for (const QString& fetching : qAsConst(m_jobList)) { if (fetching == source) { // already getting this source and awaiting the data return; } } //qCDebug(IONENGINE_ENVCAN) << source; // Demunge source name for key only. QString dataKey = source; dataKey.remove(QStringLiteral("envcan|weather|")); const XMLMapInfo& place = m_places[dataKey]; const QUrl url(QLatin1String("http://dd.weatheroffice.ec.gc.ca/citypage_weather/xml/") + place.territoryName + QLatin1Char('/') + place.cityCode + QStringLiteral("_e.xml")); //url="file:///home/spstarr/Desktop/s0000649_e.xml"; //qCDebug(IONENGINE_ENVCAN) << "Will Try URL: " << url; if (place.territoryName.isEmpty() && place.cityCode.isEmpty()) { setData(source, QStringLiteral("validate"), QStringLiteral("envcan|malformed")); return; } KIO::TransferJob* getJob = KIO::get(url, KIO::Reload, KIO::HideProgressInfo); m_jobXml.insert(getJob, new QXmlStreamReader); m_jobList.insert(getJob, source); connect(getJob, &KIO::TransferJob::data, this, &EnvCanadaIon::slotDataArrived); connect(getJob, &KJob::result, this, &EnvCanadaIon::slotJobFinished); } void EnvCanadaIon::setup_slotDataArrived(KIO::Job *job, const QByteArray &data) { Q_UNUSED(job) if (data.isEmpty()) { //qCDebug(IONENGINE_ENVCAN) << "done!"; return; } // Send to xml. //qCDebug(IONENGINE_ENVCAN) << data; m_xmlSetup.addData(data); } void EnvCanadaIon::slotDataArrived(KIO::Job *job, const QByteArray &data) { if (data.isEmpty() || !m_jobXml.contains(job)) { return; } // Send to xml. m_jobXml[job]->addData(data); } void EnvCanadaIon::slotJobFinished(KJob *job) { // Dual use method, if we're fetching location data to parse we need to do this first const QString source = m_jobList.value(job); //qCDebug(IONENGINE_ENVCAN) << source << m_sourcesToReset.contains(source); setData(source, Data()); QXmlStreamReader *reader = m_jobXml.value(job); if (reader) { readXMLData(m_jobList[job], *reader); } m_jobList.remove(job); delete m_jobXml[job]; m_jobXml.remove(job); if (m_sourcesToReset.contains(source)) { m_sourcesToReset.removeAll(source); // so the weather engine updates it's data forceImmediateUpdateOfAllVisualizations(); // update the clients of our engine emit forceUpdate(this, source); } } void EnvCanadaIon::setup_slotJobFinished(KJob *job) { Q_UNUSED(job) const bool success = readXMLSetup(); m_xmlSetup.clear(); //qCDebug(IONENGINE_ENVCAN) << success << m_sourcesToReset; setInitialized(success); } // Parse the city list and store into a QMap bool EnvCanadaIon::readXMLSetup() { bool success = false; QString territory; QString code; QString cityName; //qCDebug(IONENGINE_ENVCAN) << "readXMLSetup()"; while (!m_xmlSetup.atEnd()) { m_xmlSetup.readNext(); const QStringRef elementName = m_xmlSetup.name(); if (m_xmlSetup.isStartElement()) { // XML ID code to match filename if (elementName == QLatin1String("site")) { code = m_xmlSetup.attributes().value(QStringLiteral("code")).toString(); } if (elementName == QLatin1String("nameEn")) { cityName = m_xmlSetup.readElementText(); // Name of cities } if (elementName == QLatin1String("provinceCode")) { territory = m_xmlSetup.readElementText(); // Provinces/Territory list } } if (m_xmlSetup.isEndElement() && elementName == QLatin1String("site")) { EnvCanadaIon::XMLMapInfo info; QString tmp = cityName + QStringLiteral(", ") + territory; // Build the key name. // Set the mappings info.cityCode = code; info.territoryName = territory; info.cityName = cityName; // Set the string list, we will use for the applet to display the available cities. m_places[tmp] = info; success = true; } } return (success && !m_xmlSetup.error()); } void EnvCanadaIon::parseWeatherSite(WeatherData& data, QXmlStreamReader& xml) { while (!xml.atEnd()) { xml.readNext(); const QStringRef elementName = xml.name(); if (xml.isStartElement()) { if (elementName == QLatin1String("license")) { data.creditUrl = xml.readElementText(); } else if (elementName == QLatin1String("location")) { parseLocations(data, xml); } else if (elementName == QLatin1String("warnings")) { // Cleanup warning list on update data.warnings.clear(); data.watches.clear(); parseWarnings(data, xml); } else if (elementName == QLatin1String("currentConditions")) { parseConditions(data, xml); } else if (elementName == QLatin1String("forecastGroup")) { // Clean up forecast list on update data.forecasts.clear(); parseWeatherForecast(data, xml); } else if (elementName == QLatin1String("yesterdayConditions")) { parseYesterdayWeather(data, xml); } else if (elementName == QLatin1String("riseSet")) { parseAstronomicals(data, xml); } else if (elementName == QLatin1String("almanac")) { parseWeatherRecords(data, xml); } else { parseUnknownElement(xml); } } } } -// Parse Weather data main loop, from here we have to decend into each tag pair +// Parse Weather data main loop, from here we have to descend into each tag pair bool EnvCanadaIon::readXMLData(const QString& source, QXmlStreamReader& xml) { WeatherData data; //qCDebug(IONENGINE_ENVCAN) << "readXMLData()"; QString dataKey = source; dataKey.remove(QStringLiteral("envcan|weather|")); data.shortTerritoryName = m_places[dataKey].territoryName; while (!xml.atEnd()) { xml.readNext(); if (xml.isEndElement()) { break; } if (xml.isStartElement()) { if (xml.name() == QLatin1String("siteData")) { parseWeatherSite(data, xml); } else { parseUnknownElement(xml); } } } bool solarDataSourceNeedsConnect = false; Plasma::DataEngine* timeEngine = dataEngine(QStringLiteral("time")); if (timeEngine) { const bool canCalculateElevation = (data.observationDateTime.isValid() && (!qIsNaN(data.stationLatitude) && !qIsNaN(data.stationLongitude))); if (canCalculateElevation) { data.solarDataTimeEngineSourceName = QStringLiteral("%1|Solar|Latitude=%2|Longitude=%3|DateTime=%4") .arg(QString::fromUtf8(data.observationDateTime.timeZone().id())) .arg(data.stationLatitude) .arg(data.stationLongitude) .arg(data.observationDateTime.toString(Qt::ISODate)); solarDataSourceNeedsConnect = true; } // check any previous data const auto it = m_weatherData.constFind(source); if (it != m_weatherData.constEnd()) { const QString& oldSolarDataTimeEngineSource = it.value().solarDataTimeEngineSourceName; if (oldSolarDataTimeEngineSource == data.solarDataTimeEngineSourceName) { // can reuse elevation source (if any), copy over data data.isNight = it.value().isNight; solarDataSourceNeedsConnect = false; } else if (!oldSolarDataTimeEngineSource.isEmpty()) { // drop old elevation source timeEngine->disconnectSource(oldSolarDataTimeEngineSource, this); } } } m_weatherData[source] = data; // connect only after m_weatherData has the data, so the instant data push handling can see it if (solarDataSourceNeedsConnect) { timeEngine->connectSource(data.solarDataTimeEngineSourceName, this); } else { updateWeather(source); } return !xml.error(); } void EnvCanadaIon::parseFloat(float& value, QXmlStreamReader& xml) { bool ok = false; const float result = xml.readElementText().toFloat(&ok); if (ok) { value = result; } } void EnvCanadaIon::parseDateTime(WeatherData& data, QXmlStreamReader& xml, WeatherData::WeatherEvent *event) { Q_ASSERT(xml.isStartElement() && xml.name() == QLatin1String("dateTime")); // What kind of date info is this? const QString dateType = xml.attributes().value(QStringLiteral("name")).toString(); const QString dateZone = xml.attributes().value(QStringLiteral("zone")).toString(); const QString dateUtcOffset = xml.attributes().value(QStringLiteral("UTCOffset")).toString(); QString selectTimeStamp; while (!xml.atEnd()) { xml.readNext(); if (xml.isEndElement()) { break; } const QStringRef elementName = xml.name(); if (xml.isStartElement()) { if (dateType == QLatin1String("xmlCreation")) { return; } if (dateZone == QLatin1String("UTC")) { return; } if (elementName == QLatin1String("year")) { xml.readElementText(); } else if (elementName == QLatin1String("month")) { xml.readElementText(); } else if (elementName == QLatin1String("day")) { xml.readElementText(); } else if (elementName == QLatin1String("hour")) xml.readElementText(); else if (elementName == QLatin1String("minute")) xml.readElementText(); else if (elementName == QLatin1String("timeStamp")) selectTimeStamp = xml.readElementText(); else if (elementName == QLatin1String("textSummary")) { if (dateType == QLatin1String("eventIssue")) { if (event) { event->timestamp = xml.readElementText(); } } else if (dateType == QLatin1String("observation")) { xml.readElementText(); QDateTime observationDateTime = QDateTime::fromString(selectTimeStamp, QStringLiteral("yyyyMMddHHmmss")); QTimeZone timeZone = QTimeZone(dateZone.toUtf8()); // if timezone id not recognized, fallback to utcoffset if (!timeZone.isValid()) { timeZone = QTimeZone(dateUtcOffset.toInt() * 3600); } if (observationDateTime.isValid() && timeZone.isValid()) { data.observationDateTime = observationDateTime; data.observationDateTime.setTimeZone(timeZone); } data.obsTimestamp = observationDateTime.toString(QStringLiteral("dd.MM.yyyy @ hh:mm")); } else if (dateType == QLatin1String("forecastIssue")) { data.forecastTimestamp = xml.readElementText(); } else if (dateType == QLatin1String("sunrise")) { data.sunriseTimestamp = xml.readElementText(); } else if (dateType == QLatin1String("sunset")) { data.sunsetTimestamp = xml.readElementText(); } else if (dateType == QLatin1String("moonrise")) { data.moonriseTimestamp = xml.readElementText(); } else if (dateType == QLatin1String("moonset")) { data.moonsetTimestamp = xml.readElementText(); } } } } } void EnvCanadaIon::parseLocations(WeatherData& data, QXmlStreamReader& xml) { Q_ASSERT(xml.isStartElement() && xml.name() == QLatin1String("location")); while (!xml.atEnd()) { xml.readNext(); if (xml.isEndElement()) { break; } const QStringRef elementName = xml.name(); if (xml.isStartElement()) { if (elementName == QLatin1String("country")) { data.countryName = xml.readElementText(); } else if (elementName == QLatin1String("province") || elementName == QLatin1String("territory")) { data.longTerritoryName = xml.readElementText(); } else if (elementName == QLatin1String("name")) { data.cityName = xml.readElementText(); } else if (elementName == QLatin1String("region")) { data.regionName = xml.readElementText(); } else { parseUnknownElement(xml); } } } } void EnvCanadaIon::parseWindInfo(WeatherData& data, QXmlStreamReader& xml) { Q_ASSERT(xml.isStartElement() && xml.name() == QLatin1String("wind")); while (!xml.atEnd()) { xml.readNext(); if (xml.isEndElement()) { break; } const QStringRef elementName = xml.name(); if (xml.isStartElement()) { if (elementName == QLatin1String("speed")) { parseFloat(data.windSpeed, xml); } else if (elementName == QLatin1String("gust")) { parseFloat(data.windGust, xml); } else if (elementName == QLatin1String("direction")) { data.windDirection = xml.readElementText(); } else if (elementName == QLatin1String("bearing")) { data.windDegrees = xml.attributes().value(QStringLiteral("degrees")).toString(); } else { parseUnknownElement(xml); } } } } void EnvCanadaIon::parseConditions(WeatherData& data, QXmlStreamReader& xml) { Q_ASSERT(xml.isStartElement() && xml.name() == QLatin1String("currentConditions")); data.temperature = qQNaN(); data.dewpoint = qQNaN(); data.condition = i18n("N/A"); data.humidex.clear(); data.stationID = i18n("N/A"); data.stationLatitude = qQNaN(); data.stationLongitude = qQNaN(); data.pressure = qQNaN(); data.visibility = qQNaN(); data.humidity = qQNaN(); while (!xml.atEnd()) { xml.readNext(); const QStringRef elementName = xml.name(); if (xml.isEndElement() && elementName == QLatin1String("currentConditions")) break; if (xml.isStartElement()) { if (elementName == QLatin1String("station")) { data.stationID = xml.attributes().value(QStringLiteral("code")).toString(); QRegularExpression dumpDirection(QStringLiteral("[^0-9.]")); data.stationLatitude = xml.attributes().value(QStringLiteral("lat")).toString().remove(dumpDirection).toDouble(); data.stationLongitude = xml.attributes().value(QStringLiteral("lon")).toString().remove(dumpDirection).toDouble(); } else if (elementName == QLatin1String("dateTime")) { parseDateTime(data, xml); } else if (elementName == QLatin1String("condition")) { data.condition = xml.readElementText().trimmed(); } else if (elementName == QLatin1String("temperature")) { // prevent N/A text to result in 0.0 value parseFloat(data.temperature, xml); } else if (elementName == QLatin1String("dewpoint")) { // prevent N/A text to result in 0.0 value parseFloat(data.dewpoint, xml); } else if (elementName == QLatin1String("humidex")) { data.humidex = xml.readElementText(); } else if (elementName == QLatin1String("windChill")) { // prevent N/A text to result in 0.0 value parseFloat(data.windchill, xml); } else if (elementName == QLatin1String("pressure")) { data.pressureTendency = xml.attributes().value(QStringLiteral("tendency")).toString(); if (data.pressureTendency.isEmpty()) { data.pressureTendency = QStringLiteral("steady"); } parseFloat(data.pressure, xml); } else if (elementName == QLatin1String("visibility")) { parseFloat(data.visibility, xml); } else if (elementName == QLatin1String("relativeHumidity")) { parseFloat(data.humidity, xml); } else if (elementName == QLatin1String("wind")) { parseWindInfo(data, xml); } //} else { // parseUnknownElement(xml); //} } } } void EnvCanadaIon::parseWarnings(WeatherData &data, QXmlStreamReader& xml) { WeatherData::WeatherEvent *watch = new WeatherData::WeatherEvent; WeatherData::WeatherEvent *warning = new WeatherData::WeatherEvent; Q_ASSERT(xml.isStartElement() && xml.name() == QLatin1String("warnings")); QString eventURL = xml.attributes().value(QStringLiteral("url")).toString(); int flag = 0; while (!xml.atEnd()) { xml.readNext(); const QStringRef elementName = xml.name(); if (xml.isEndElement() && elementName == QLatin1String("warnings")) { break; } if (xml.isStartElement()) { if (elementName == QLatin1String("dateTime")) { if (flag == 1) { parseDateTime(data, xml, watch); } if (flag == 2) { parseDateTime(data, xml, warning); } if (!warning->timestamp.isEmpty() && !warning->url.isEmpty()) { data.warnings.append(warning); warning = new WeatherData::WeatherEvent; } if (!watch->timestamp.isEmpty() && !watch->url.isEmpty()) { data.watches.append(watch); watch = new WeatherData::WeatherEvent; } } else if (elementName == QLatin1String("event")) { // Append new event to list. QString eventType = xml.attributes().value(QStringLiteral("type")).toString(); if (eventType == QLatin1String("watch")) { watch->url = eventURL; watch->type = eventType; watch->priority = xml.attributes().value(QStringLiteral("priority")).toString(); watch->description = xml.attributes().value(QStringLiteral("description")).toString(); flag = 1; } if (eventType == QLatin1String("warning")) { warning->url = eventURL; warning->type = eventType; warning->priority = xml.attributes().value(QStringLiteral("priority")).toString(); warning->description = xml.attributes().value(QStringLiteral("description")).toString(); flag = 2; } } else { if (xml.name() != QLatin1String("dateTime")) { parseUnknownElement(xml); } } } } delete watch; delete warning; } void EnvCanadaIon::parseWeatherForecast(WeatherData& data, QXmlStreamReader& xml) { WeatherData::ForecastInfo* forecast = new WeatherData::ForecastInfo; Q_ASSERT(xml.isStartElement() && xml.name() == QLatin1String("forecastGroup")); while (!xml.atEnd()) { xml.readNext(); const QStringRef elementName = xml.name(); if (xml.isEndElement() && elementName == QLatin1String("forecastGroup")) { break; } if (xml.isStartElement()) { if (elementName == QLatin1String("dateTime")) { parseDateTime(data, xml); } else if (elementName == QLatin1String("regionalNormals")) { parseRegionalNormals(data, xml); } else if (elementName == QLatin1String("forecast")) { parseForecast(data, xml, forecast); forecast = new WeatherData::ForecastInfo; } else { parseUnknownElement(xml); } } } delete forecast; } void EnvCanadaIon::parseRegionalNormals(WeatherData& data, QXmlStreamReader& xml) { Q_ASSERT(xml.isStartElement() && xml.name() == QLatin1String("regionalNormals")); while (!xml.atEnd()) { xml.readNext(); if (xml.isEndElement()) { break; } const QStringRef elementName = xml.name(); if (xml.isStartElement()) { if (elementName == QLatin1String("textSummary")) { xml.readElementText(); } else if (elementName == QLatin1String("temperature") && xml.attributes().value(QStringLiteral("class")) == QLatin1String("high")) { // prevent N/A text to result in 0.0 value parseFloat(data.normalHigh, xml); } else if (elementName == QLatin1String("temperature") && xml.attributes().value(QStringLiteral("class")) == QLatin1String("low")) { // prevent N/A text to result in 0.0 value parseFloat(data.normalLow, xml); } } } } void EnvCanadaIon::parseForecast(WeatherData& data, QXmlStreamReader& xml, WeatherData::ForecastInfo *forecast) { Q_ASSERT(xml.isStartElement() && xml.name() == QLatin1String("forecast")); while (!xml.atEnd()) { xml.readNext(); const QStringRef elementName = xml.name(); if (xml.isEndElement() && elementName == QLatin1String("forecast")) { data.forecasts.append(forecast); break; } if (xml.isStartElement()) { if (elementName == QLatin1String("period")) { forecast->forecastPeriod = xml.attributes().value(QStringLiteral("textForecastName")).toString(); } else if (elementName == QLatin1String("textSummary")) { forecast->forecastSummary = xml.readElementText(); } else if (elementName == QLatin1String("abbreviatedForecast")) { parseShortForecast(forecast, xml); } else if (elementName == QLatin1String("temperatures")) { parseForecastTemperatures(forecast, xml); } else if (elementName == QLatin1String("winds")) { parseWindForecast(forecast, xml); } else if (elementName == QLatin1String("precipitation")) { parsePrecipitationForecast(forecast, xml); } else if (elementName == QLatin1String("uv")) { data.UVRating = xml.attributes().value(QStringLiteral("category")).toString(); parseUVIndex(data, xml); // else if (elementName == QLatin1String("frost")) { FIXME: Wait until winter to see what this looks like. // parseFrost(xml, forecast); } else { if (elementName != QLatin1String("forecast")) { parseUnknownElement(xml); } } } } } void EnvCanadaIon::parseShortForecast(WeatherData::ForecastInfo *forecast, QXmlStreamReader& xml) { Q_ASSERT(xml.isStartElement() && xml.name() == QLatin1String("abbreviatedForecast")); QString shortText; while (!xml.atEnd()) { xml.readNext(); const QStringRef elementName = xml.name(); if (xml.isEndElement() && elementName == QLatin1String("abbreviatedForecast")) { break; } if (xml.isStartElement()) { if (elementName == QLatin1String("pop")) { parseFloat(forecast->popPrecent, xml); } if (elementName == QLatin1String("textSummary")) { shortText = xml.readElementText(); QMap forecastList = forecastIcons(); if ((forecast->forecastPeriod == QLatin1String("tonight")) || (forecast->forecastPeriod.contains(QLatin1String("night")))) { forecastList.insert(QStringLiteral("a few clouds"), FewCloudsNight); forecastList.insert(QStringLiteral("cloudy periods"), PartlyCloudyNight); forecastList.insert(QStringLiteral("chance of drizzle mixed with rain"), ChanceShowersNight); forecastList.insert(QStringLiteral("chance of drizzle"), ChanceShowersNight); forecastList.insert(QStringLiteral("chance of drizzle or rain"), ChanceShowersNight); forecastList.insert(QStringLiteral("chance of flurries"), ChanceSnowNight); forecastList.insert(QStringLiteral("chance of light snow"), ChanceSnowNight); forecastList.insert(QStringLiteral("chance of flurries at times heavy"), ChanceSnowNight); forecastList.insert(QStringLiteral("chance of showers or drizzle"), ChanceShowersNight); forecastList.insert(QStringLiteral("chance of showers"), ChanceShowersNight); forecastList.insert(QStringLiteral("clearing"), ClearNight); } else { forecastList.insert(QStringLiteral("a few clouds"), FewCloudsDay); forecastList.insert(QStringLiteral("cloudy periods"), PartlyCloudyDay); forecastList.insert(QStringLiteral("chance of drizzle mixed with rain"), ChanceShowersDay); forecastList.insert(QStringLiteral("chance of drizzle"), ChanceShowersDay); forecastList.insert(QStringLiteral("chance of drizzle or rain"), ChanceShowersDay); forecastList.insert(QStringLiteral("chance of flurries"), ChanceSnowDay); forecastList.insert(QStringLiteral("chance of light snow"), ChanceSnowDay); forecastList.insert(QStringLiteral("chance of flurries at times heavy"), ChanceSnowDay); forecastList.insert(QStringLiteral("chance of showers or drizzle"), ChanceShowersDay); forecastList.insert(QStringLiteral("chance of showers"), ChanceShowersDay); forecastList.insert(QStringLiteral("clearing"), ClearDay); } forecast->shortForecast = shortText; forecast->iconName = getWeatherIcon(forecastList, shortText.toLower()); } } } } void EnvCanadaIon::parseUVIndex(WeatherData& data, QXmlStreamReader& xml) { Q_ASSERT(xml.isStartElement() && xml.name() == QLatin1String("uv")); while (!xml.atEnd()) { xml.readNext(); const QStringRef elementName = xml.name(); if (xml.isEndElement() && elementName == QLatin1String("uv")) { break; } if (xml.isStartElement()) { if (elementName == QLatin1String("index")) { data.UVIndex = xml.readElementText(); } if (elementName == QLatin1String("textSummary")) { xml.readElementText(); } } } } void EnvCanadaIon::parseForecastTemperatures(WeatherData::ForecastInfo *forecast, QXmlStreamReader& xml) { Q_ASSERT(xml.isStartElement() && xml.name() == QLatin1String("temperatures")); while (!xml.atEnd()) { xml.readNext(); const QStringRef elementName = xml.name(); if (xml.isEndElement() && elementName == QLatin1String("temperatures")) { break; } if (xml.isStartElement()) { if (elementName == QLatin1String("temperature") && xml.attributes().value(QStringLiteral("class")) == QLatin1String("low")) { parseFloat(forecast->tempLow, xml); } else if (elementName == QLatin1String("temperature") && xml.attributes().value(QStringLiteral("class")) == QLatin1String("high")) { parseFloat(forecast->tempHigh, xml); } else if (elementName == QLatin1String("textSummary")) { xml.readElementText(); } } } } void EnvCanadaIon::parsePrecipitationForecast(WeatherData::ForecastInfo *forecast, QXmlStreamReader& xml) { Q_ASSERT(xml.isStartElement() && xml.name() == QLatin1String("precipitation")); while (!xml.atEnd()) { xml.readNext(); const QStringRef elementName = xml.name(); if (xml.isEndElement() && elementName == QLatin1String("precipitation")) { break; } if (xml.isStartElement()) { if (elementName == QLatin1String("textSummary")) { forecast->precipForecast = xml.readElementText(); } else if (elementName == QLatin1String("precipType")) { forecast->precipType = xml.readElementText(); } else if (elementName == QLatin1String("accumulation")) { parsePrecipTotals(forecast, xml); } } } } void EnvCanadaIon::parsePrecipTotals(WeatherData::ForecastInfo *forecast, QXmlStreamReader& xml) { Q_ASSERT(xml.isStartElement() && xml.name() == QLatin1String("accumulation")); while (!xml.atEnd()) { xml.readNext(); const QStringRef elementName = xml.name(); if (xml.isEndElement() && elementName == QLatin1String("accumulation")) { break; } if (elementName == QLatin1String("name")) { xml.readElementText(); } else if (elementName == QLatin1String("amount")) { forecast->precipTotalExpected = xml.readElementText(); } } } void EnvCanadaIon::parseWindForecast(WeatherData::ForecastInfo *forecast, QXmlStreamReader& xml) { Q_ASSERT(xml.isStartElement() && xml.name() == QLatin1String("winds")); while (!xml.atEnd()) { xml.readNext(); const QStringRef elementName = xml.name(); if (xml.isEndElement() && elementName == QLatin1String("winds")) { break; } if (xml.isStartElement()) { if (elementName == QLatin1String("textSummary")) { forecast->windForecast = xml.readElementText(); } else { if (xml.name() != QLatin1String("winds")) { parseUnknownElement(xml); } } } } } void EnvCanadaIon::parseYesterdayWeather(WeatherData& data, QXmlStreamReader& xml) { Q_ASSERT(xml.isStartElement() && xml.name() == QLatin1String("yesterdayConditions")); while (!xml.atEnd()) { xml.readNext(); if (xml.isEndElement()) { break; } const QStringRef elementName = xml.name(); if (xml.isStartElement()) { if (elementName == QLatin1String("temperature") && xml.attributes().value(QStringLiteral("class")) == QLatin1String("high")) { parseFloat(data.prevHigh, xml); } else if (elementName == QLatin1String("temperature") && xml.attributes().value(QStringLiteral("class")) == QLatin1String("low")) { parseFloat(data.prevLow, xml); } else if (elementName == QLatin1String("precip")) { data.prevPrecipType = xml.attributes().value(QStringLiteral("units")).toString(); if (data.prevPrecipType.isEmpty()) { data.prevPrecipType = QString::number(KUnitConversion::NoUnit); } data.prevPrecipTotal = xml.readElementText(); } } } } void EnvCanadaIon::parseWeatherRecords(WeatherData& data, QXmlStreamReader& xml) { Q_ASSERT(xml.isStartElement() && xml.name() == QLatin1String("almanac")); while (!xml.atEnd()) { xml.readNext(); const QStringRef elementName = xml.name(); if (xml.isEndElement() && elementName == QLatin1String("almanac")) { break; } if (xml.isStartElement()) { if (elementName == QLatin1String("temperature") && xml.attributes().value(QStringLiteral("class")) == QLatin1String("extremeMax")) { parseFloat(data.recordHigh, xml); } else if (elementName == QLatin1String("temperature") && xml.attributes().value(QStringLiteral("class")) == QLatin1String("extremeMin")) { parseFloat(data.recordLow, xml); } else if (elementName == QLatin1String("precipitation") && xml.attributes().value(QStringLiteral("class")) == QLatin1String("extremeRainfall")) { parseFloat(data.recordRain, xml); } else if (elementName == QLatin1String("precipitation") && xml.attributes().value(QStringLiteral("class")) == QLatin1String("extremeSnowfall")) { parseFloat(data.recordSnow, xml); } } } } void EnvCanadaIon::parseAstronomicals(WeatherData& data, QXmlStreamReader& xml) { Q_ASSERT(xml.isStartElement() && xml.name() == QLatin1String("riseSet")); while (!xml.atEnd()) { xml.readNext(); const QStringRef elementName = xml.name(); if (xml.isEndElement() && elementName == QLatin1String("riseSet")) { break; } if (xml.isStartElement()) { if (elementName == QLatin1String("disclaimer")) { xml.readElementText(); } else if (elementName == QLatin1String("dateTime")) { parseDateTime(data, xml); } } } } // handle when no XML tag is found void EnvCanadaIon::parseUnknownElement(QXmlStreamReader& xml) const { while (!xml.atEnd()) { xml.readNext(); if (xml.isEndElement()) { break; } if (xml.isStartElement()) { parseUnknownElement(xml); } } } void EnvCanadaIon::updateWeather(const QString& source) { //qCDebug(IONENGINE_ENVCAN) << "updateWeather()"; const WeatherData& weatherData = m_weatherData[source]; Plasma::DataEngine::Data data; data.insert(QStringLiteral("Country"), weatherData.countryName); data.insert(QStringLiteral("Place"), QVariant(weatherData.cityName + QStringLiteral(", ") + weatherData.shortTerritoryName)); data.insert(QStringLiteral("Region"), weatherData.regionName); data.insert(QStringLiteral("Station"), weatherData.stationID.isEmpty() ? i18n("N/A") : weatherData.stationID.toUpper()); const bool stationCoordValid = (!qIsNaN(weatherData.stationLatitude) && !qIsNaN(weatherData.stationLongitude)); if (stationCoordValid) { data.insert(QStringLiteral("Latitude"), weatherData.stationLatitude); data.insert(QStringLiteral("Longitude"), weatherData.stationLongitude); } // Real weather - Current conditions if (weatherData.observationDateTime.isValid()) { data.insert(QStringLiteral("Observation Timestamp"), weatherData.observationDateTime); } data.insert(QStringLiteral("Observation Period"), weatherData.obsTimestamp); if (!weatherData.condition.isEmpty()) { data.insert(QStringLiteral("Current Conditions"), i18nc("weather condition", weatherData.condition.toUtf8().data())); } //qCDebug(IONENGINE_ENVCAN) << "i18n condition string: " << qPrintable(condition(source)); QMap conditionList = conditionIcons(); if (weatherData.isNight) { conditionList.insert(QStringLiteral("decreasing cloud"), FewCloudsNight); conditionList.insert(QStringLiteral("mostly cloudy"), PartlyCloudyNight); conditionList.insert(QStringLiteral("partly cloudy"), PartlyCloudyNight); conditionList.insert(QStringLiteral("fair"), FewCloudsNight); } else { conditionList.insert(QStringLiteral("decreasing cloud"), FewCloudsDay); conditionList.insert(QStringLiteral("mostly cloudy"), PartlyCloudyDay); conditionList.insert(QStringLiteral("partly cloudy"), PartlyCloudyDay); conditionList.insert(QStringLiteral("fair"), FewCloudsDay); } data.insert(QStringLiteral("Condition Icon"), getWeatherIcon(conditionList, weatherData.condition)); if (!qIsNaN(weatherData.temperature)) { data.insert(QStringLiteral("Temperature"), weatherData.temperature); } if (!qIsNaN(weatherData.windchill)) { data.insert(QStringLiteral("Windchill"), weatherData.windchill); } if (!weatherData.humidex.isEmpty()) { data.insert(QStringLiteral("Humidex"), weatherData.humidex); } // Used for all temperatures data.insert(QStringLiteral("Temperature Unit"), KUnitConversion::Celsius); if (!qIsNaN(weatherData.dewpoint)) { data.insert(QStringLiteral("Dewpoint"), weatherData.dewpoint); } if (!qIsNaN(weatherData.pressure)) { data.insert(QStringLiteral("Pressure"), weatherData.pressure); data.insert(QStringLiteral("Pressure Unit"), KUnitConversion::Kilopascal); data.insert(QStringLiteral("Pressure Tendency"), weatherData.pressureTendency); } if (!qIsNaN(weatherData.visibility)) { data.insert(QStringLiteral("Visibility"), weatherData.visibility); data.insert(QStringLiteral("Visibility Unit"), KUnitConversion::Kilometer); } if (!qIsNaN(weatherData.humidity)) { data.insert(QStringLiteral("Humidity"), weatherData.humidity); data.insert(QStringLiteral("Humidity Unit"), KUnitConversion::Percent); } if (!qIsNaN(weatherData.windSpeed)) { data.insert(QStringLiteral("Wind Speed"), weatherData.windSpeed); } if (!qIsNaN(weatherData.windGust)) { data.insert(QStringLiteral("Wind Gust"), weatherData.windGust); } if (!qIsNaN(weatherData.windSpeed) || !qIsNaN(weatherData.windGust)) { data.insert(QStringLiteral("Wind Speed Unit"), KUnitConversion::KilometerPerHour); } if (!qIsNaN(weatherData.windSpeed) && static_cast(weatherData.windSpeed) == 0) { data.insert(QStringLiteral("Wind Direction"), QStringLiteral("VR")); // Variable/calm } else if (!weatherData.windDirection.isEmpty()) { data.insert(QStringLiteral("Wind Direction"), weatherData.windDirection); } if (!qIsNaN(weatherData.normalHigh)) { data.insert(QStringLiteral("Normal High"), weatherData.normalHigh); } if (!qIsNaN(weatherData.normalLow)) { data.insert(QStringLiteral("Normal Low"), weatherData.normalLow); } // Check if UV index is available for the location if (!weatherData.UVIndex.isEmpty()) { data.insert(QStringLiteral("UV Index"), weatherData.UVIndex); } if (!weatherData.UVRating.isEmpty()) { data.insert(QStringLiteral("UV Rating"), weatherData.UVRating); } const QVector& watches = weatherData.watches; // Set number of forecasts per day/night supported data.insert(QStringLiteral("Total Watches Issued"), watches.size()); // Check if we have warnings or watches for (int i = 0; i < watches.size(); ++i) { const WeatherData::WeatherEvent* watch = watches.at(i); const QString number = QString::number(i); data.insert(QStringLiteral("Watch Priority ") + number, watch->priority); data.insert(QStringLiteral("Watch Description ") + number, watch->description); data.insert(QStringLiteral("Watch Info ") + number, watch->url); data.insert(QStringLiteral("Watch Timestamp ") + number, watch->timestamp); } const QVector& warnings = weatherData.warnings; data.insert(QStringLiteral("Total Warnings Issued"), warnings.size()); for (int k = 0; k < warnings.size(); ++k) { const WeatherData::WeatherEvent* warning = warnings.at(k); const QString number = QString::number(k); data.insert(QStringLiteral("Warning Priority ") + number, warning->priority); data.insert(QStringLiteral("Warning Description ") + number, warning->description); data.insert(QStringLiteral("Warning Info ") + number, warning->url); data.insert(QStringLiteral("Warning Timestamp ") + number, warning->timestamp); } const QVector & forecasts = weatherData.forecasts; // Set number of forecasts per day/night supported data.insert(QStringLiteral("Total Weather Days"), forecasts.size()); int i = 0; for (const WeatherData::ForecastInfo* forecastInfo : forecasts) { QString forecastPeriod = forecastInfo->forecastPeriod; if (forecastPeriod.isEmpty()) { forecastPeriod = i18n("N/A"); } else { // We need to shortform the day/night strings. forecastPeriod.replace(QStringLiteral("Today"), i18n("day")); forecastPeriod.replace(QStringLiteral("Tonight"), i18nc("Short for tonight", "nite")); forecastPeriod.replace(QStringLiteral("night"), i18nc("Short for night, appended to the end of the weekday", "nt")); forecastPeriod.replace(QStringLiteral("Saturday"), i18nc("Short for Saturday", "Sat")); forecastPeriod.replace(QStringLiteral("Sunday"), i18nc("Short for Sunday", "Sun")); forecastPeriod.replace(QStringLiteral("Monday"), i18nc("Short for Monday", "Mon")); forecastPeriod.replace(QStringLiteral("Tuesday"), i18nc("Short for Tuesday", "Tue")); forecastPeriod.replace(QStringLiteral("Wednesday"), i18nc("Short for Wednesday", "Wed")); forecastPeriod.replace(QStringLiteral("Thursday"), i18nc("Short for Thursday", "Thu")); forecastPeriod.replace(QStringLiteral("Friday"), i18nc("Short for Friday", "Fri")); } const QString shortForecast = forecastInfo->shortForecast.isEmpty() ? i18n("N/A") : i18nc("weather forecast", forecastInfo->shortForecast.toUtf8().data()); const QString tempHigh = qIsNaN(forecastInfo->tempHigh) ? QString() : QString::number(forecastInfo->tempHigh); const QString tempLow = qIsNaN(forecastInfo->tempLow) ? QString() : QString::number(forecastInfo->tempLow); const QString popPrecent = qIsNaN(forecastInfo->popPrecent) ? QString() : QString::number(forecastInfo->popPrecent); data.insert(QStringLiteral("Short Forecast Day %1").arg(i), QStringLiteral("%1|%2|%3|%4|%5|%6").arg( forecastPeriod, forecastInfo->iconName, shortForecast, tempHigh, tempLow, popPrecent)); //qCDebug(IONENGINE_ENVCAN) << "i18n summary string: " << qPrintable(i18n(forecastInfo->shortForecast.toUtf8())); /* data.insert(QString("Long Forecast Day %1").arg(i), QString("%1|%2|%3|%4|%5|%6|%7|%8") \ .arg(fieldList[0]).arg(fieldList[2]).arg(fieldList[3]).arg(fieldList[4]).arg(fieldList[6]) \ .arg(fieldList[7]).arg(fieldList[8]).arg(fieldList[9])); */ ++i; } // yesterday if (!qIsNaN(weatherData.prevHigh)) { data.insert(QStringLiteral("Yesterday High"), weatherData.prevHigh); } if (!qIsNaN(weatherData.prevLow)) { data.insert(QStringLiteral("Yesterday Low"), weatherData.prevLow); } const QString& prevPrecipTotal = weatherData.prevPrecipTotal; if (prevPrecipTotal == QLatin1String("Trace")) { data.insert(QStringLiteral("Yesterday Precip Total"), i18nc("precipitation total, very little", "Trace")); } else if (!prevPrecipTotal.isEmpty()) { data.insert(QStringLiteral("Yesterday Precip Total"), prevPrecipTotal); const QString& prevPrecipType = weatherData.prevPrecipType; const KUnitConversion::UnitId unit = (prevPrecipType == QLatin1String("mm")) ? KUnitConversion::Millimeter : (prevPrecipType == QLatin1String("cm")) ? KUnitConversion::Centimeter : /*else*/ KUnitConversion::NoUnit; data.insert(QStringLiteral("Yesterday Precip Unit"), unit); } // records if (!qIsNaN(weatherData.recordHigh)) { data.insert(QStringLiteral("Record High Temperature"), weatherData.recordHigh); } if (!qIsNaN(weatherData.recordLow)) { data.insert(QStringLiteral("Record Low Temperature"), weatherData.recordLow); } if (!qIsNaN(weatherData.recordRain)) { data.insert(QStringLiteral("Record Rainfall"), weatherData.recordRain); data.insert(QStringLiteral("Record Rainfall Unit"), KUnitConversion::Millimeter); } if (!qIsNaN(weatherData.recordSnow)) { data.insert(QStringLiteral("Record Snowfall"), weatherData.recordSnow); data.insert(QStringLiteral("Record Snowfall Unit"), KUnitConversion::Centimeter); } data.insert(QStringLiteral("Credit"), i18nc("credit line, keep string short", "Data from Environment and Climate Change\302\240Canada")); data.insert(QStringLiteral("Credit Url"), weatherData.creditUrl); setData(source, data); } void EnvCanadaIon::dataUpdated(const QString& sourceName, const Plasma::DataEngine::Data& data) { const bool isNight = (data.value(QStringLiteral("Corrected Elevation")).toDouble() < 0.0); for (auto end = m_weatherData.end(), it = m_weatherData.begin(); it != end; ++it) { auto& weatherData = it.value(); if (weatherData.solarDataTimeEngineSourceName == sourceName) { weatherData.isNight = isNight; updateWeather(it.key()); } } } K_EXPORT_PLASMA_DATAENGINE_WITH_JSON(envcan, EnvCanadaIon, "ion-envcan.json") #include "ion_envcan.moc" diff --git a/dataengines/weather/ions/noaa/ion_noaa.cpp b/dataengines/weather/ions/noaa/ion_noaa.cpp index 718639c4e..b778fe26f 100644 --- a/dataengines/weather/ions/noaa/ion_noaa.cpp +++ b/dataengines/weather/ions/noaa/ion_noaa.cpp @@ -1,970 +1,970 @@ /*************************************************************************** * Copyright (C) 2007-2009,2019 by Shawn Starr * * * * 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, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA * ***************************************************************************/ /* Ion for NOAA's National Weather Service XML data */ #include "ion_noaa.h" #include "ion_noaadebug.h" #include #include #include #include #include WeatherData::WeatherData() : stationLatitude(qQNaN()) , stationLongitude(qQNaN()) , temperature_F(qQNaN()) , temperature_C(qQNaN()) , humidity(qQNaN()) , windSpeed(qQNaN()) , windGust(qQNaN()) , pressure(qQNaN()) , dewpoint_F(qQNaN()) , dewpoint_C(qQNaN()) , heatindex_F(qQNaN()) , heatindex_C(qQNaN()) , windchill_F(qQNaN()) , windchill_C(qQNaN()) , visibility(qQNaN()) { } QMap NOAAIon::setupWindIconMappings() const { return QMap { { QStringLiteral("north"), N }, { QStringLiteral("northeast"), NE }, { QStringLiteral("south"), S }, { QStringLiteral("southwest"), SW }, { QStringLiteral("east"), E }, { QStringLiteral("southeast"), SE }, { QStringLiteral("west"), W }, { QStringLiteral("northwest"), NW }, { QStringLiteral("calm"), VR }, }; } QMap NOAAIon::setupConditionIconMappings() const { QMap conditionList; return conditionList; } QMap const& NOAAIon::conditionIcons() const { static QMap const condval = setupConditionIconMappings(); return condval; } QMap const& NOAAIon::windIcons() const { static QMap const wval = setupWindIconMappings(); return wval; } // ctor, dtor NOAAIon::NOAAIon(QObject *parent, const QVariantList &args) : IonInterface(parent, args) { // Get the real city XML URL so we can parse this getXMLSetup(); } void NOAAIon::reset() { m_sourcesToReset = sources(); getXMLSetup(); } NOAAIon::~NOAAIon() { //seems necessary to avoid crash removeAllSources(); } QStringList NOAAIon::validate(const QString& source) const { QStringList placeList; QString station; QString sourceNormalized = source.toUpper(); QHash::const_iterator it = m_places.constBegin(); // If the source name might look like a station ID, check these too and return the name bool checkState = source.count() == 2; while (it != m_places.constEnd()) { if (checkState) { if (it.value().stateName == source) { placeList.append(QStringLiteral("place|").append(it.key())); } } else if (it.key().toUpper().contains(sourceNormalized)) { placeList.append(QStringLiteral("place|").append(it.key())); } else if (it.value().stationID == sourceNormalized) { station = QStringLiteral("place|").append(it.key()); } ++it; } placeList.sort(); if (!station.isEmpty()) { placeList.prepend(station); } return placeList; } bool NOAAIon::updateIonSource(const QString& source) { // We expect the applet to send the source in the following tokenization: // ionname:validate:place_name - Triggers validation of place // ionname:weather:place_name - Triggers receiving weather of place QStringList sourceAction = source.split(QLatin1Char('|')); // Guard: if the size of array is not 2 then we have bad data, return an error if (sourceAction.size() < 2) { setData(source, QStringLiteral("validate"), QStringLiteral("noaa|malformed")); return true; } if (sourceAction[1] == QLatin1String("validate") && sourceAction.size() > 2) { QStringList result = validate(sourceAction[2]); if (result.size() == 1) { setData(source, QStringLiteral("validate"), QStringLiteral("noaa|valid|single|").append(result.join(QLatin1Char('|')))); return true; } if (result.size() > 1) { setData(source, QStringLiteral("validate"), QStringLiteral("noaa|valid|multiple|").append(result.join(QLatin1Char('|')))); return true; } // result.size() == 0 setData(source, QStringLiteral("validate"), QStringLiteral("noaa|invalid|single|").append(sourceAction[2])); return true; } if (sourceAction[1] == QLatin1String("weather") && sourceAction.size() > 2) { getXMLData(source); return true; } setData(source, QStringLiteral("validate"), QStringLiteral("noaa|malformed")); return true; } // Parses city list and gets the correct city based on ID number void NOAAIon::getXMLSetup() const { const QUrl url(QStringLiteral("https://www.weather.gov/data/current_obs/index.xml")); KIO::TransferJob* getJob = KIO::get(url, KIO::NoReload, KIO::HideProgressInfo); connect(getJob, &KIO::TransferJob::data, this, &NOAAIon::setup_slotDataArrived); connect(getJob, &KJob::result, this, &NOAAIon::setup_slotJobFinished); } // Gets specific city XML data void NOAAIon::getXMLData(const QString& source) { for (const QString& fetching : qAsConst(m_jobList)) { if (fetching == source) { // already getting this source and awaiting the data return; } } QString dataKey = source; dataKey.remove(QStringLiteral("noaa|weather|")); const QUrl url(m_places[dataKey].XMLurl); // If this is empty we have no valid data, send out an error and abort. if (url.url().isEmpty()) { setData(source, QStringLiteral("validate"), QStringLiteral("noaa|malformed")); return; } KIO::TransferJob* getJob = KIO::get(url, KIO::Reload, KIO::HideProgressInfo); m_jobXml.insert(getJob, new QXmlStreamReader); m_jobList.insert(getJob, source); connect(getJob, &KIO::TransferJob::data, this, &NOAAIon::slotDataArrived); connect(getJob, &KJob::result, this, &NOAAIon::slotJobFinished); } void NOAAIon::setup_slotDataArrived(KIO::Job *job, const QByteArray &data) { Q_UNUSED(job) if (data.isEmpty()) { return; } // Send to xml. m_xmlSetup.addData(data); } void NOAAIon::slotDataArrived(KIO::Job *job, const QByteArray &data) { if (data.isEmpty() || !m_jobXml.contains(job)) { return; } // Send to xml. m_jobXml[job]->addData(data); } void NOAAIon::slotJobFinished(KJob *job) { // Dual use method, if we're fetching location data to parse we need to do this first const QString source(m_jobList.value(job)); removeAllData(source); QXmlStreamReader *reader = m_jobXml.value(job); if (reader) { readXMLData(m_jobList[job], *reader); } // Now that we have the longitude and latitude, fetch the seven day forecast. getForecast(m_jobList[job]); m_jobList.remove(job); m_jobXml.remove(job); delete reader; } void NOAAIon::setup_slotJobFinished(KJob *job) { Q_UNUSED(job) const bool success = readXMLSetup(); setInitialized(success); for (const QString& source : qAsConst(m_sourcesToReset)) { updateSourceEvent(source); } } void NOAAIon::parseFloat(float& value, const QString& string) { bool ok = false; const float result = string.toFloat(&ok); if (ok) { value = result; } } void NOAAIon::parseFloat(float& value, QXmlStreamReader& xml) { bool ok = false; const float result = xml.readElementText().toFloat(&ok); if (ok) { value = result; } } void NOAAIon::parseDouble(double& value, QXmlStreamReader& xml) { bool ok = false; const double result = xml.readElementText().toDouble(&ok); if (ok) { value = result; } } void NOAAIon::parseStationID() { QString state; QString stationName; QString stationID; QString xmlurl; while (!m_xmlSetup.atEnd()) { m_xmlSetup.readNext(); const QStringRef elementName = m_xmlSetup.name(); if (m_xmlSetup.isEndElement() && elementName == QLatin1String("station")) { if (!xmlurl.isEmpty()) { NOAAIon::XMLMapInfo info; info.stateName = state; info.stationName = stationName; info.stationID = stationID; info.XMLurl = xmlurl; QString tmp = stationName + QLatin1String(", ") + state; // Build the key name. m_places[tmp] = info; } break; } if (m_xmlSetup.isStartElement()) { if (elementName == QLatin1String("station_id")) { stationID = m_xmlSetup.readElementText(); } else if (elementName == QLatin1String("state")) { state = m_xmlSetup.readElementText(); } else if (elementName == QLatin1String("station_name")) { stationName = m_xmlSetup.readElementText(); } else if (elementName == QLatin1String("xml_url")) { xmlurl = m_xmlSetup.readElementText().replace(QStringLiteral("http://"), QStringLiteral("http://www.")); } else { parseUnknownElement(m_xmlSetup); } } } } void NOAAIon::parseStationList() { while (!m_xmlSetup.atEnd()) { m_xmlSetup.readNext(); if (m_xmlSetup.isEndElement()) { break; } if (m_xmlSetup.isStartElement()) { if (m_xmlSetup.name() == QLatin1String("station")) { parseStationID(); } else { parseUnknownElement(m_xmlSetup); } } } } // Parse the city list and store into a QMap bool NOAAIon::readXMLSetup() { bool success = false; while (!m_xmlSetup.atEnd()) { m_xmlSetup.readNext(); if (m_xmlSetup.isStartElement()) { if (m_xmlSetup.name() == QLatin1String("wx_station_index")) { parseStationList(); success = true; } } } return (!m_xmlSetup.error() && success); } void NOAAIon::parseWeatherSite(WeatherData& data, QXmlStreamReader& xml) { data.temperature_C = qQNaN(); data.temperature_F = qQNaN(); data.dewpoint_C = qQNaN(); data.dewpoint_F = qQNaN(); data.weather = QStringLiteral("N/A"); data.stationID = i18n("N/A"); data.pressure = qQNaN(); data.visibility = qQNaN(); data.humidity = qQNaN(); data.windSpeed = qQNaN(); data.windGust = qQNaN(); data.windchill_F = qQNaN(); data.windchill_C = qQNaN(); data.heatindex_F = qQNaN(); data.heatindex_C = qQNaN(); while (!xml.atEnd()) { xml.readNext(); const QStringRef elementName = xml.name(); if (xml.isStartElement()) { if (elementName == QLatin1String("location")) { data.locationName = xml.readElementText(); } else if (elementName == QLatin1String("station_id")) { data.stationID = xml.readElementText(); } else if (elementName == QLatin1String("latitude")) { parseDouble(data.stationLatitude, xml); } else if (elementName == QLatin1String("longitude")) { parseDouble(data.stationLongitude, xml); } else if (elementName == QLatin1String("observation_time_rfc822")) { data.observationDateTime = QDateTime::fromString(xml.readElementText(), Qt::RFC2822Date); } else if (elementName == QLatin1String("observation_time")) { data.observationTime = xml.readElementText(); QStringList tmpDateStr = data.observationTime.split(QLatin1Char(' ')); data.observationTime = QStringLiteral("%1 %2").arg(tmpDateStr[6], tmpDateStr[7]); } else if (elementName == QLatin1String("weather")) { const QString weather = xml.readElementText(); data.weather = (weather.isEmpty() || weather == QLatin1String("NA")) ? QStringLiteral("N/A") : weather; // Pick which icon set depending on period of day } else if (elementName == QLatin1String("temp_f")) { parseFloat(data.temperature_F, xml); } else if (elementName == QLatin1String("temp_c")) { parseFloat(data.temperature_C, xml); } else if (elementName == QLatin1String("relative_humidity")) { parseFloat(data.humidity, xml); } else if (elementName == QLatin1String("wind_dir")) { data.windDirection = xml.readElementText(); } else if (elementName == QLatin1String("wind_mph")) { const QString windSpeed = xml.readElementText(); if (windSpeed == QLatin1String("NA")) { data.windSpeed = 0.0; } else { parseFloat(data.windSpeed, windSpeed); } } else if (elementName == QLatin1String("wind_gust_mph")) { const QString windGust = xml.readElementText(); if (windGust == QLatin1String("NA") || windGust == QLatin1String("N/A")) { data.windGust = 0.0; } else { parseFloat(data.windGust, windGust); } } else if (elementName == QLatin1String("pressure_in")) { parseFloat(data.pressure, xml); } else if (elementName == QLatin1String("dewpoint_f")) { parseFloat(data.dewpoint_F, xml); } else if (elementName == QLatin1String("dewpoint_c")) { parseFloat(data.dewpoint_C, xml); } else if (elementName == QLatin1String("heat_index_f")) { parseFloat(data.heatindex_F, xml); } else if (elementName == QLatin1String("heat_index_c")) { parseFloat(data.heatindex_C, xml); } else if (elementName == QLatin1String("windchill_f")) { parseFloat(data.windchill_F, xml); } else if (elementName == QLatin1String("windchill_c")) { parseFloat(data.windchill_C, xml); } else if (elementName == QLatin1String("visibility_mi")) { parseFloat(data.visibility, xml); } else { parseUnknownElement(xml); } } } } -// Parse Weather data main loop, from here we have to decend into each tag pair +// Parse Weather data main loop, from here we have to descend into each tag pair bool NOAAIon::readXMLData(const QString& source, QXmlStreamReader& xml) { WeatherData data; data.isForecastsDataPending = true; while (!xml.atEnd()) { xml.readNext(); if (xml.isEndElement()) { break; } if (xml.isStartElement()) { if (xml.name() == QLatin1String("current_observation")) { parseWeatherSite(data, xml); } else { parseUnknownElement(xml); } } } bool solarDataSourceNeedsConnect = false; Plasma::DataEngine* timeEngine = dataEngine(QStringLiteral("time")); if (timeEngine) { const bool canCalculateElevation = (data.observationDateTime.isValid() && (!qIsNaN(data.stationLatitude) && !qIsNaN(data.stationLongitude))); if (canCalculateElevation) { data.solarDataTimeEngineSourceName = QStringLiteral("%1|Solar|Latitude=%2|Longitude=%3|DateTime=%4") .arg(QString::fromUtf8(data.observationDateTime.timeZone().id())) .arg(data.stationLatitude) .arg(data.stationLongitude) .arg(data.observationDateTime.toString(Qt::ISODate)); solarDataSourceNeedsConnect = true; } // check any previous data const auto it = m_weatherData.constFind(source); if (it != m_weatherData.constEnd()) { const QString& oldSolarDataTimeEngineSource = it.value().solarDataTimeEngineSourceName; if (oldSolarDataTimeEngineSource == data.solarDataTimeEngineSourceName) { // can reuse elevation source (if any), copy over data data.isNight = it.value().isNight; solarDataSourceNeedsConnect = false; } else if (!oldSolarDataTimeEngineSource.isEmpty()) { // drop old elevation source timeEngine->disconnectSource(oldSolarDataTimeEngineSource, this); } } } m_weatherData[source] = data; // connect only after m_weatherData has the data, so the instant data push handling can see it if (solarDataSourceNeedsConnect) { data.isSolarDataPending = true; timeEngine->connectSource(data.solarDataTimeEngineSourceName, this); } return !xml.error(); } // handle when no XML tag is found void NOAAIon::parseUnknownElement(QXmlStreamReader& xml) const { while (!xml.atEnd()) { xml.readNext(); if (xml.isEndElement()) { break; } if (xml.isStartElement()) { parseUnknownElement(xml); } } } void NOAAIon::updateWeather(const QString& source) { const WeatherData& weatherData = m_weatherData[source]; if (weatherData.isForecastsDataPending || weatherData.isSolarDataPending) { return; } Plasma::DataEngine::Data data; data.insert(QStringLiteral("Place"), weatherData.locationName); data.insert(QStringLiteral("Station"), weatherData.stationID); const bool stationCoordValid = (!qIsNaN(weatherData.stationLatitude) && !qIsNaN(weatherData.stationLongitude)); if (stationCoordValid) { data.insert(QStringLiteral("Latitude"), weatherData.stationLatitude); data.insert(QStringLiteral("Longitude"), weatherData.stationLongitude); } // Real weather - Current conditions if (weatherData.observationDateTime.isValid()) { data.insert(QStringLiteral("Observation Timestamp"), weatherData.observationDateTime); } data.insert(QStringLiteral("Observation Period"), weatherData.observationTime); const QString conditionI18n = weatherData.weather == QLatin1String("N/A") ? i18n("N/A") : i18nc("weather condition", weatherData.weather.toUtf8().data()); data.insert(QStringLiteral("Current Conditions"), conditionI18n); qCDebug(IONENGINE_NOAA) << "i18n condition string: " << qPrintable(conditionI18n); const QString weather = weatherData.weather.toLower(); ConditionIcons condition = getConditionIcon(weather, !weatherData.isNight); data.insert(QStringLiteral("Condition Icon"), getWeatherIcon(condition)); if (!qIsNaN(weatherData.temperature_F)) { data.insert(QStringLiteral("Temperature"), weatherData.temperature_F); } // Used for all temperatures data.insert(QStringLiteral("Temperature Unit"), KUnitConversion::Fahrenheit); if (!qIsNaN(weatherData.windchill_F)) { data.insert(QStringLiteral("Windchill"), weatherData.windchill_F); } if (!qIsNaN(weatherData.heatindex_F)) { data.insert(QStringLiteral("Heat Index"), weatherData.heatindex_F); } if (!qIsNaN(weatherData.dewpoint_F)) { data.insert(QStringLiteral("Dewpoint"), weatherData.dewpoint_F); } if (!qIsNaN(weatherData.pressure)) { data.insert(QStringLiteral("Pressure"), weatherData.pressure); data.insert(QStringLiteral("Pressure Unit"), KUnitConversion::InchesOfMercury); } if (!qIsNaN(weatherData.visibility)) { data.insert(QStringLiteral("Visibility"), weatherData.visibility); data.insert(QStringLiteral("Visibility Unit"), KUnitConversion::Mile); } if (!qIsNaN(weatherData.humidity)) { data.insert(QStringLiteral("Humidity"), weatherData.humidity); data.insert(QStringLiteral("Humidity Unit"), KUnitConversion::Percent); } if (!qIsNaN(weatherData.windSpeed)) { data.insert(QStringLiteral("Wind Speed"), weatherData.windSpeed); } if (!qIsNaN(weatherData.windSpeed) || !qIsNaN(weatherData.windGust)) { data.insert(QStringLiteral("Wind Speed Unit"), KUnitConversion::MilePerHour); } if (!qIsNaN(weatherData.windGust)) { data.insert(QStringLiteral("Wind Gust"), weatherData.windGust); } if (!qIsNaN(weatherData.windSpeed) && static_cast(weatherData.windSpeed) == 0) { data.insert(QStringLiteral("Wind Direction"), QStringLiteral("VR")); // Variable/calm } else if (!weatherData.windDirection.isEmpty()) { data.insert(QStringLiteral("Wind Direction"), getWindDirectionIcon(windIcons(), weatherData.windDirection.toLower())); } // Set number of forecasts per day/night supported data.insert(QStringLiteral("Total Weather Days"), weatherData.forecasts.size()); int i = 0; for (const WeatherData::Forecast& forecast : weatherData.forecasts) { ConditionIcons icon = getConditionIcon(forecast.summary.toLower(), true); QString iconName = getWeatherIcon(icon); /* Sometimes the forecast for the later days is unavailable, if so skip remianing days * since their forecast data is probably unavailable. */ if (forecast.low.isEmpty() || forecast.high.isEmpty()) { break; } // Get the short day name for the forecast data.insert(QStringLiteral("Short Forecast Day %1").arg(i), QStringLiteral("%1|%2|%3|%4|%5|%6") .arg(forecast.day, iconName, i18nc("weather forecast", forecast.summary.toUtf8().data()), forecast.high, forecast.low, QString())); ++i; } data.insert(QStringLiteral("Credit"), i18nc("credit line, keep string short)", "Data from NOAA National\302\240Weather\302\240Service")); setData(source, data); } /** * Determine the condition icon based on the list of possible NOAA weather conditions as defined at * and * * Since the number of NOAA weather conditions need to be fitted into the narowly defined groups in IonInterface::ConditionIcons, we * try to group the NOAA conditions as best as we can based on their priorities/severity. * TODO: summaries "Hot" & "Cold" have no proper matching entry in ConditionIcons, consider extending it */ IonInterface::ConditionIcons NOAAIon::getConditionIcon(const QString& weather, bool isDayTime) const { IonInterface::ConditionIcons result; // Consider any type of storm, tornado or funnel to be a thunderstorm. if (weather.contains(QLatin1String("thunderstorm")) || weather.contains(QLatin1String("funnel")) || weather.contains(QLatin1String("tornado")) || weather.contains(QLatin1String("storm")) || weather.contains(QLatin1String("tstms"))) { if (weather.contains(QLatin1String("vicinity")) || weather.contains(QLatin1String("chance"))) { result = isDayTime ? IonInterface::ChanceThunderstormDay : IonInterface::ChanceThunderstormNight; } else { result = IonInterface::Thunderstorm; } } else if (weather.contains(QLatin1String("pellets")) || weather.contains(QLatin1String("crystals")) || weather.contains(QLatin1String("hail"))) { result = IonInterface::Hail; } else if (((weather.contains(QLatin1String("rain")) || weather.contains(QLatin1String("drizzle")) || weather.contains(QLatin1String("showers"))) && weather.contains(QLatin1String("snow"))) || weather.contains(QLatin1String("wintry mix"))) { result = IonInterface::RainSnow; } else if (weather.contains(QLatin1String("flurries"))) { result = IonInterface::Flurries; } else if (weather.contains(QLatin1String("snow")) && weather.contains(QLatin1String("light"))) { result = IonInterface::LightSnow; } else if (weather.contains(QLatin1String("snow"))) { if (weather.contains(QLatin1String("vicinity")) || weather.contains(QLatin1String("chance"))) { result = isDayTime ? IonInterface::ChanceSnowDay : IonInterface::ChanceSnowNight; } else { result = IonInterface::Snow; } } else if (weather.contains(QLatin1String("freezing rain"))) { result = IonInterface::FreezingRain; } else if (weather.contains(QLatin1String("freezing drizzle"))) { result = IonInterface::FreezingDrizzle; } else if (weather.contains(QLatin1String("cold"))) { // temperature condition has not hint about air ingredients, so let's assume chance of snow result = isDayTime ? IonInterface::ChanceSnowDay : IonInterface::ChanceSnowNight; } else if (weather.contains(QLatin1String("showers"))) { if (weather.contains(QLatin1String("vicinity")) || weather.contains(QLatin1String("chance"))) { result = isDayTime ? IonInterface::ChanceShowersDay : IonInterface::ChanceShowersNight; } else { result = IonInterface::Showers; } } else if (weather.contains(QLatin1String("light rain")) || weather.contains(QLatin1String("drizzle"))) { result = IonInterface::LightRain; } else if (weather.contains(QLatin1String("rain"))) { result = IonInterface::Rain; } else if (weather.contains(QLatin1String("few clouds")) || weather.contains(QLatin1String("mostly sunny")) || weather.contains(QLatin1String("mostly clear")) || weather.contains(QLatin1String("increasing clouds")) || weather.contains(QLatin1String("becoming cloudy")) || weather.contains(QLatin1String("clearing")) || weather.contains(QLatin1String("decreasing clouds")) || weather.contains(QLatin1String("becoming sunny"))) { if (weather.contains(QLatin1String("breezy")) || weather.contains(QLatin1String("wind")) || weather.contains (QLatin1String("gust"))) { result = isDayTime ? IonInterface::FewCloudsWindyDay : IonInterface::FewCloudsWindyNight; } else { result = isDayTime ? IonInterface::FewCloudsDay : IonInterface::FewCloudsNight; } } else if (weather.contains(QLatin1String("partly cloudy")) || weather.contains(QLatin1String("partly sunny")) || weather.contains(QLatin1String("partly clear"))) { if (weather.contains(QLatin1String("breezy")) || weather.contains(QLatin1String("wind")) || weather.contains (QLatin1String("gust"))) { result = isDayTime ? IonInterface::PartlyCloudyWindyDay : IonInterface::PartlyCloudyWindyNight; } else { result = isDayTime ? IonInterface::PartlyCloudyDay : IonInterface::PartlyCloudyNight; } } else if (weather.contains(QLatin1String("overcast")) || weather.contains(QLatin1String("cloudy"))) { if (weather.contains(QLatin1String("breezy")) || weather.contains(QLatin1String("wind")) || weather.contains (QLatin1String("gust"))) { result = IonInterface::OvercastWindy; } else { result = IonInterface::Overcast; } } else if (weather.contains(QLatin1String("haze")) || weather.contains(QLatin1String("smoke")) || weather.contains(QLatin1String("dust")) || weather.contains(QLatin1String("sand"))) { result = IonInterface::Haze; } else if (weather.contains(QLatin1String("fair")) || weather.contains(QLatin1String("clear")) || weather.contains(QLatin1String("sunny"))) { if (weather.contains(QLatin1String("breezy")) || weather.contains(QLatin1String("wind")) || weather.contains (QLatin1String("gust"))) { result = isDayTime ? IonInterface::ClearWindyDay : IonInterface::ClearWindyNight; } else { result = isDayTime ? IonInterface::ClearDay : IonInterface::ClearNight; } } else if (weather.contains(QLatin1String("fog"))) { result = IonInterface::Mist; } else if (weather.contains(QLatin1String("hot"))) { // temperature condition has not hint about air ingredients, so let's assume the sky is clear when it is hot if (weather.contains(QLatin1String("breezy")) || weather.contains(QLatin1String("wind")) || weather.contains (QLatin1String("gust"))) { result = isDayTime ? IonInterface::ClearWindyDay : IonInterface::ClearWindyNight; } else { result = isDayTime ? IonInterface::ClearDay : IonInterface::ClearNight; } } else if (weather.contains (QLatin1String("breezy")) || weather.contains (QLatin1String("wind")) || weather.contains (QLatin1String("gust"))) { // Assume a clear sky when it's windy but no clouds have been mentioned result = isDayTime ? IonInterface::ClearWindyDay : IonInterface::ClearWindyNight; } else { result = IonInterface::NotAvailable; } return result; } void NOAAIon::getForecast(const QString& source) { const double lat = m_weatherData[source].stationLatitude; const double lon = m_weatherData[source].stationLongitude; if (qIsNaN(lat) || qIsNaN(lon)) { return; } /* Assuming that we have the latitude and longitude data at this point, get the 7-day * forecast. */ const QUrl url(QLatin1String("https://graphical.weather.gov/xml/sample_products/browser_interface/" "ndfdBrowserClientByDay.php?lat=") + QString::number(lat) + QLatin1String("&lon=") + QString::number(lon) + QLatin1String("&format=24+hourly&numDays=7")); KIO::TransferJob* getJob = KIO::get(url, KIO::Reload, KIO::HideProgressInfo); m_jobXml.insert(getJob, new QXmlStreamReader); m_jobList.insert(getJob, source); connect(getJob, &KIO::TransferJob::data, this, &NOAAIon::forecast_slotDataArrived); connect(getJob, &KJob::result, this, &NOAAIon::forecast_slotJobFinished); } void NOAAIon::forecast_slotDataArrived(KIO::Job *job, const QByteArray &data) { if (data.isEmpty() || !m_jobXml.contains(job)) { return; } // Send to xml. m_jobXml[job]->addData(data); } void NOAAIon::forecast_slotJobFinished(KJob *job) { QXmlStreamReader *reader = m_jobXml.value(job); const QString source = m_jobList.value(job); if (reader) { readForecast(source, *reader); updateWeather(source); } m_jobList.remove(job); delete m_jobXml[job]; m_jobXml.remove(job); if (m_sourcesToReset.contains(source)) { m_sourcesToReset.removeAll(source); // so the weather engine updates it's data forceImmediateUpdateOfAllVisualizations(); // update the clients of our engine emit forceUpdate(this, source); } } void NOAAIon::readForecast(const QString& source, QXmlStreamReader& xml) { WeatherData& weatherData = m_weatherData[source]; QVector& forecasts = weatherData.forecasts; // Clear the current forecasts forecasts.clear(); while (!xml.atEnd()) { xml.readNext(); if (xml.isStartElement()) { /* Read all reported days from . We check for existence of a specific * which indicates the separate day listings. The schema defines it to be * the first item before the day listings. */ if (xml.name() == QLatin1String("layout-key") && xml.readElementText() == QLatin1String("k-p24h-n7-1")) { // Read days until we get to end of parent ()tag while (! (xml.isEndElement() && xml.name() == QLatin1String("time-layout"))) { xml.readNext(); if (xml.name() == QLatin1String("start-valid-time")) { QString data = xml.readElementText(); QDateTime date = QDateTime::fromString(data, Qt::ISODate); WeatherData::Forecast forecast; forecast.day = QLocale().toString(date.date().day()); forecasts.append(forecast); //qCDebug(IONENGINE_NOAA) << forecast.day; } } } else if (xml.name() == QLatin1String("temperature") && xml.attributes().value(QStringLiteral("type")) == QLatin1String("maximum")) { // Read max temps until we get to end tag int i = 0; while (! (xml.isEndElement() && xml.name() == QLatin1String("temperature")) && i < forecasts.count()) { xml.readNext(); if (xml.name() == QLatin1String("value")) { forecasts[i].high = xml.readElementText(); //qCDebug(IONENGINE_NOAA) << forecasts[i].high; i++; } } } else if (xml.name() == QLatin1String("temperature") && xml.attributes().value(QStringLiteral("type")) == QLatin1String("minimum")) { // Read min temps until we get to end tag int i = 0; while (! (xml.isEndElement() && xml.name() == QLatin1String("temperature")) && i < forecasts.count()) { xml.readNext(); if (xml.name() == QLatin1String("value")) { forecasts[i].low = xml.readElementText(); //qCDebug(IONENGINE_NOAA) << forecasts[i].low; i++; } } } else if (xml.name() == QLatin1String("weather")) { // Read weather conditions until we get to end tag int i = 0; while (! (xml.isEndElement() && xml.name() == QLatin1String("weather")) && i < forecasts.count()) { xml.readNext(); if (xml.name() == QLatin1String("weather-conditions") && xml.isStartElement()) { QString summary = xml.attributes().value(QStringLiteral("weather-summary")).toString(); forecasts[i].summary = summary; //qCDebug(IONENGINE_NOAA) << forecasts[i].summary; qCDebug(IONENGINE_NOAA) << "i18n summary string: " << i18nc("weather forecast", forecasts[i].summary.toUtf8().data()); i++; } } } } } weatherData.isForecastsDataPending = false; } void NOAAIon::dataUpdated(const QString& sourceName, const Plasma::DataEngine::Data& data) { const bool isNight = (data.value(QStringLiteral("Corrected Elevation")).toDouble() < 0.0); for (auto end = m_weatherData.end(), it = m_weatherData.begin(); it != end; ++it) { auto& weatherData = it.value(); if (weatherData.solarDataTimeEngineSourceName == sourceName) { weatherData.isNight = isNight; weatherData.isSolarDataPending = false; updateWeather(it.key()); } } } K_EXPORT_PLASMA_DATAENGINE_WITH_JSON(noaa, NOAAIon, "ion-noaa.json") #include "ion_noaa.moc" diff --git a/klipper/historyimageitem.cpp b/klipper/historyimageitem.cpp index 3ebf1b56f..596f8b7e1 100644 --- a/klipper/historyimageitem.cpp +++ b/klipper/historyimageitem.cpp @@ -1,78 +1,78 @@ /* This file is part of the KDE project Copyright (C) 2004 Esben Mose Hansen 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; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "historyimageitem.h" #include "historymodel.h" #include #include #include #include namespace { QByteArray compute_uuid(const QPixmap& data) { QByteArray buffer; QDataStream out(&buffer, QIODevice::WriteOnly); out << data; return QCryptographicHash::hash(buffer, QCryptographicHash::Sha1); } } HistoryImageItem::HistoryImageItem( const QPixmap& data ) : HistoryItem(compute_uuid(data)) , m_data( data ) { } QString HistoryImageItem::text() const { if (m_text.isNull()) { m_text = QStringLiteral("▨ ") + - i18n("%1x%2 %3bpp") - .arg(m_data.width()) - .arg(m_data.height()) - .arg(m_data.depth()); + i18n("%1x%2 %3bpp", + m_data.width(), + m_data.height(), + m_data.depth()); } return m_text; } /* virtual */ void HistoryImageItem::write( QDataStream& stream ) const { stream << QStringLiteral( "image" ) << m_data; } QMimeData* HistoryImageItem::mimeData() const { QMimeData *data = new QMimeData(); data->setImageData(m_data.toImage()); return data; } const QPixmap& HistoryImageItem::image() const { if (m_model->displayImages()) { return m_data; } static QPixmap imageIcon( QIcon::fromTheme(QStringLiteral("view-preview")).pixmap(QSize(48, 48)) ); return imageIcon; } diff --git a/klipper/klipperpopup.h b/klipper/klipperpopup.h index cf70ab977..faf699f53 100644 --- a/klipper/klipperpopup.h +++ b/klipper/klipperpopup.h @@ -1,136 +1,136 @@ /* This file is part of the KDE project Copyright (C) 2004 Esben Mose Hansen - Copytight (C) by Andrew Stanley-Jones + Copyright (C) by Andrew Stanley-Jones 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; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KLIPPERPOPUP_H #define KLIPPERPOPUP_H #include #include class QAction; class QWidgetAction; class QKeyEvent; class KHelpMenu; class KLineEdit; class PopupProxy; class History; /** * Default view of clipboard history. * */ class KlipperPopup : public QMenu { Q_OBJECT public: explicit KlipperPopup( History* history ); ~KlipperPopup() override; void plugAction( QAction* action ); /** * Normally, the popupmenu is only rebuilt just before showing. * If you need the pixel-size or similar of the this menu, call * this beforehand. */ void ensureClean(); History* history() { return m_history; } const History* history() const { return m_history; } void setShowHelp(bool show) { m_showHelp = show; } public Q_SLOTS: void slotHistoryChanged() { m_dirty = true; } void slotTopIsUserSelectedSet(); void slotAboutToShow(); /** * set the top history item active, to easy kb navigation */ void slotSetTopActive(); private: void rebuild( const QString& filter = QString() ); void buildFromScratch(); protected: void keyPressEvent( QKeyEvent* e ) override; private: bool m_dirty : 1; // true if menu contents needs to be rebuild. /** * Contains the string shown if the menu is empty. */ QString m_textForEmptyHistory; /** * Contains the string shown if the search string has no * matches and the menu is not empty. */ QString m_textForNoMatch; /** * The "document" (clipboard history) */ History* m_history; /** * The help menu */ KHelpMenu* m_helpMenu; /** * (unowned) actions to plug into the primary popup menu */ QList m_actions; /** * Proxy helper object used to track history items */ PopupProxy* m_popupProxy; /** * search filter widget */ KLineEdit* m_filterWidget; /** * Action of search widget */ QWidgetAction* m_filterWidgetAction; /** * The current number of history items in the clipboard */ int m_nHistoryItems; bool m_showHelp; /** * The last event which was received. Used to avoid an infinite event loop */ QKeyEvent* m_lastEvent; }; #endif diff --git a/libnotificationmanager/jobsmodel.h b/libnotificationmanager/jobsmodel.h index a57a29bc5..45de9be57 100644 --- a/libnotificationmanager/jobsmodel.h +++ b/libnotificationmanager/jobsmodel.h @@ -1,98 +1,98 @@ /* * Copyright 2019 Kai Uwe Broulik * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) version 3, or any * later version accepted by the membership of KDE e.V. (or its * successor approved by the membership of KDE e.V.), which shall * act as a proxy defined in Section 6 of version 3 of the license. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . */ #pragma once #include #include #include #include "notifications.h" #include "notificationmanager_export.h" namespace NotificationManager { class JobsModelPrivate; class NOTIFICATIONMANAGER_EXPORT JobsModel : public QAbstractListModel { Q_OBJECT public: ~JobsModel() override; using Ptr = QSharedPointer; static Ptr createJobsModel(); /** * Registers the JobView service on DBus. * * @return true if succeeded, false otherwise. */ bool init(); /** - * Whether the notification service could be reigstered + * Whether the notification service could be registered */ bool isValid() const; QVariant data(const QModelIndex &index, int role) const override; bool setData(const QModelIndex &index, const QVariant &value, int role) override; int rowCount(const QModelIndex &parent = QModelIndex()) const override; /** * @brief Close a job * * This removes the job from the model. This will not cancel the job! * Use @c kill if you want to cancel a job. */ void close(const QModelIndex &idx); void expire(const QModelIndex &idx); /** * @brief Suspend a job */ void suspend(const QModelIndex &idx); /** * @brief Resume a job */ void resume(const QModelIndex &idx); /** * @brief Kill a job * * This cancels the job. */ void kill(const QModelIndex &idx); void clear(Notifications::ClearFlags flags); signals: void serviceOwnershipLost(); private: JobsModel(); Q_DISABLE_COPY(JobsModel) JobsModelPrivate *d; }; } // namespace NotificationManager diff --git a/libnotificationmanager/notification.cpp b/libnotificationmanager/notification.cpp index 1dd437f00..06ce55986 100644 --- a/libnotificationmanager/notification.cpp +++ b/libnotificationmanager/notification.cpp @@ -1,755 +1,755 @@ /* * Copyright 2008 Dmitry Suzdalev * Copyright 2017 David Edmundson * Copyright 2018-2019 Kai Uwe Broulik * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) version 3, or any * later version accepted by the membership of KDE e.V. (or its * successor approved by the membership of KDE e.V.), which shall * act as a proxy defined in Section 6 of version 3 of the license. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . */ #include "notification.h" #include "notification_p.h" #include "notifications.h" #include #include #include #include #include #include #include #include #include #include #include #include "debug.h" using namespace NotificationManager; Notification::Private::Private() { } Notification::Private::~Private() = default; QString Notification::Private::sanitize(const QString &text) { // replace all \ns with
QString t = text; t.replace(QLatin1String("\n"), QStringLiteral("
")); // Now remove all inner whitespace (\ns are already
s) t = t.simplified(); // Finally, check if we don't have multiple
s following, // can happen for example when "\n \n" is sent, this replaces - // all
s in succsession with just one + // all
s in succession with just one t.replace(QRegularExpression(QStringLiteral("
\\s*
(\\s|
)*")), QLatin1String("
")); // This fancy RegExp escapes every occurrence of & since QtQuick Text will blatantly cut off // text where it finds a stray ampersand. // Only &{apos, quot, gt, lt, amp}; as well as { character references will be allowed t.replace(QRegularExpression(QStringLiteral("&(?!(?:apos|quot|[gl]t|amp);|#)")), QLatin1String("&")); // Don't bother adding some HTML structure if the body is now empty if (t.isEmpty()) { return t; } QXmlStreamReader r(QStringLiteral("") + t + QStringLiteral("")); QString result; QXmlStreamWriter out(&result); const QVector allowedTags = {"b", "i", "u", "img", "a", "html", "br", "table", "tr", "td"}; out.writeStartDocument(); while (!r.atEnd()) { r.readNext(); if (r.tokenType() == QXmlStreamReader::StartElement) { const QString name = r.name().toString(); if (!allowedTags.contains(name)) { continue; } out.writeStartElement(name); if (name == QLatin1String("img")) { auto src = r.attributes().value("src").toString(); auto alt = r.attributes().value("alt").toString(); const QUrl url(src); if (url.isLocalFile()) { out.writeAttribute(QStringLiteral("src"), src); } else { //image denied for security reasons! Do not copy the image src here! } out.writeAttribute(QStringLiteral("alt"), alt); } if (name == QLatin1Char('a')) { out.writeAttribute(QStringLiteral("href"), r.attributes().value("href").toString()); } } if (r.tokenType() == QXmlStreamReader::EndElement) { const QString name = r.name().toString(); if (!allowedTags.contains(name)) { continue; } out.writeEndElement(); } if (r.tokenType() == QXmlStreamReader::Characters) { const auto text = r.text().toString(); out.writeCharacters(text); //this auto escapes chars -> HTML entities } } out.writeEndDocument(); if (r.hasError()) { qCWarning(NOTIFICATIONMANAGER) << "Notification to send to backend contains invalid XML: " << r.errorString() << "line" << r.lineNumber() << "col" << r.columnNumber(); } // The Text.StyledText format handles only html3.2 stuff and ' is html4 stuff // so we need to replace it here otherwise it will not render at all. result.replace(QLatin1String("'"), QChar('\'')); return result; } QImage Notification::Private::decodeNotificationSpecImageHint(const QDBusArgument &arg) { int width, height, rowStride, hasAlpha, bitsPerSample, channels; QByteArray pixels; char* ptr; char* end; arg.beginStructure(); arg >> width >> height >> rowStride >> hasAlpha >> bitsPerSample >> channels >> pixels; arg.endStructure(); #define SANITY_CHECK(condition) \ if (!(condition)) { \ qCWarning(NOTIFICATIONMANAGER) << "Image decoding sanity check failed on" << #condition; \ return QImage(); \ } SANITY_CHECK(width > 0); SANITY_CHECK(width < 2048); SANITY_CHECK(height > 0); SANITY_CHECK(height < 2048); SANITY_CHECK(rowStride > 0); #undef SANITY_CHECK auto copyLineRGB32 = [](QRgb* dst, const char* src, int width) { const char* end = src + width * 3; for (; src != end; ++dst, src+=3) { *dst = qRgb(src[0], src[1], src[2]); } }; auto copyLineARGB32 = [](QRgb* dst, const char* src, int width) { const char* end = src + width * 4; for (; src != end; ++dst, src+=4) { *dst = qRgba(src[0], src[1], src[2], src[3]); } }; QImage::Format format = QImage::Format_Invalid; void (*fcn)(QRgb*, const char*, int) = nullptr; if (bitsPerSample == 8) { if (channels == 4) { format = QImage::Format_ARGB32; fcn = copyLineARGB32; } else if (channels == 3) { format = QImage::Format_RGB32; fcn = copyLineRGB32; } } if (format == QImage::Format_Invalid) { qCWarning(NOTIFICATIONMANAGER) << "Unsupported image format (hasAlpha:" << hasAlpha << "bitsPerSample:" << bitsPerSample << "channels:" << channels << ")"; return QImage(); } QImage image(width, height, format); ptr = pixels.data(); end = ptr + pixels.length(); for (int y=0; y end) { qCWarning(NOTIFICATIONMANAGER) << "Image data is incomplete. y:" << y << "height:" << height; break; } fcn((QRgb*)image.scanLine(y), ptr, width); } return image; } void Notification::Private::sanitizeImage(QImage &image) { if (image.isNull()) { return; } const QSize max = maximumImageSize(); if (image.size().width() > max.width() || image.size().height() > max.height()) { image = image.scaled(max, Qt::KeepAspectRatio, Qt::SmoothTransformation); } } void Notification::Private::loadImagePath(const QString &path) { // image_path and appIcon should either be a URL with file scheme or the name of a themed icon. // We're lenient and also allow local paths. image = QImage(); // clear icon.clear(); QUrl imageUrl; if (path.startsWith(QLatin1Char('/'))) { imageUrl = QUrl::fromLocalFile(path); } else if (path.contains(QLatin1Char('/'))) { // bad heuristic to detect a URL imageUrl = QUrl(path); if (!imageUrl.isLocalFile()) { qCDebug(NOTIFICATIONMANAGER) << "Refused to load image from" << path << "which isn't a valid local location."; return; } } if (!imageUrl.isValid()) { // try icon path instead; icon = path; return; } QImageReader reader(imageUrl.toLocalFile()); reader.setAutoTransform(true); const QSize imageSize = reader.size(); if (imageSize.isValid() && (imageSize.width() > maximumImageSize().width() || imageSize.height() > maximumImageSize().height())) { const QSize thumbnailSize = imageSize.scaled(maximumImageSize(), Qt::KeepAspectRatio); reader.setScaledSize(thumbnailSize); } image = reader.read(); } QString Notification::Private::defaultComponentName() { // NOTE Keep in sync with KNotification return QStringLiteral("plasma_workspace"); } QSize Notification::Private::maximumImageSize() { return QSize(256, 256); } KService::Ptr Notification::Private::serviceForDesktopEntry(const QString &desktopEntry) { KService::Ptr service; if (desktopEntry.startsWith(QLatin1Char('/'))) { service = KService::serviceByDesktopPath(desktopEntry); } else { service = KService::serviceByDesktopName(desktopEntry); } if (!service) { const QString lowerDesktopEntry = desktopEntry.toLower(); service = KService::serviceByDesktopName(lowerDesktopEntry); } // Try if it's a renamed flatpak if (!service) { const QString desktopId = desktopEntry + QLatin1String(".desktop"); // HACK Querying for XDG lists in KServiceTypeTrader does not work, do it manually const auto services = KServiceTypeTrader::self()->query(QStringLiteral("Application"), QStringLiteral("exist Exec and exist [X-Flatpak-RenamedFrom]")); for (auto it = services.constBegin(); it != services.constEnd() && !service; ++it) { const QVariant renamedFrom = (*it)->property(QStringLiteral("X-Flatpak-RenamedFrom"), QVariant::String); const auto names = renamedFrom.toString().split(QChar(';')); for (const QString &name : names) { if (name == desktopId) { service = *it; break; } } } } return service; } void Notification::Private::setDesktopEntry(const QString &desktopEntry) { QString serviceName; configurableService = false; KService::Ptr service = serviceForDesktopEntry(desktopEntry); if (service) { this->desktopEntry = service->desktopEntryName(); serviceName = service->name(); applicationIconName = service->icon(); configurableService = !service->noDisplay(); } const bool isDefaultEvent = (notifyRcName == defaultComponentName()); configurableNotifyRc = false; if (!notifyRcName.isEmpty()) { // Check whether the application actually has notifications we can configure KConfig config(notifyRcName + QStringLiteral(".notifyrc"), KConfig::NoGlobals); config.addConfigSources(QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("knotifications5/") + notifyRcName + QStringLiteral(".notifyrc"))); KConfigGroup globalGroup(&config, "Global"); const QString iconName = globalGroup.readEntry("IconName"); // For default events we try to show the application name from the desktop entry if possible // This will have us show e.g. "Dr Konqi" instead of generic "Plasma Desktop" if (isDefaultEvent && !serviceName.isEmpty()) { applicationName = serviceName; } // also only overwrite application icon name for non-default events (or if we don't have a service icon) if (!iconName.isEmpty() && (!isDefaultEvent || applicationIconName.isEmpty())) { applicationIconName = iconName; } const QRegularExpression regexp(QStringLiteral("^Event/([^/]*)$")); configurableNotifyRc = !config.groupList().filter(regexp).isEmpty(); } } void Notification::Private::processHints(const QVariantMap &hints) { auto end = hints.end(); notifyRcName = hints.value(QStringLiteral("x-kde-appname")).toString(); setDesktopEntry(hints.value(QStringLiteral("desktop-entry")).toString()); // Special override for KDE Connect since the notification is sent by kdeconnectd // but actually comes from a different app on the phone const QString applicationDisplayName = hints.value(QStringLiteral("x-kde-display-appname")).toString(); if (!applicationDisplayName.isEmpty()) { applicationName = applicationDisplayName; } originName = hints.value(QStringLiteral("x-kde-origin-name")).toString(); eventId = hints.value(QStringLiteral("x-kde-eventId")).toString(); bool ok; const int urgency = hints.value(QStringLiteral("urgency")).toInt(&ok); // DBus type is actually "byte" if (ok) { // FIXME use separate enum again switch (urgency) { case 0: setUrgency(Notifications::LowUrgency); break; case 1: setUrgency(Notifications::NormalUrgency); break; case 2: setUrgency(Notifications::CriticalUrgency); break; } } userActionFeedback = hints.value(QStringLiteral("x-kde-user-action-feedback")).toBool(); if (userActionFeedback) { // A confirmation of an explicit user interaction is assumed to have been seen by the user. read = true; } urls = QUrl::fromStringList(hints.value(QStringLiteral("x-kde-urls")).toStringList()); replyPlaceholderText = hints.value(QStringLiteral("x-kde-reply-placeholder-text")).toString(); replySubmitButtonText = hints.value(QStringLiteral("x-kde-reply-submit-button-text")).toString(); replySubmitButtonIconName = hints.value(QStringLiteral("x-kde-reply-submit-button-icon-name")).toString(); // Underscored hints was in use in version 1.1 of the spec but has been // replaced by dashed hints in version 1.2. We need to support it for // users of the 1.2 version of the spec. auto it = hints.find(QStringLiteral("image-data")); if (it == end) { it = hints.find(QStringLiteral("image_data")); } if (it == end) { // This hint was in use in version 1.0 of the spec but has been // replaced by "image_data" in version 1.1. We need to support it for // users of the 1.0 version of the spec. it = hints.find(QStringLiteral("icon_data")); } if (it != end) { image = decodeNotificationSpecImageHint(it->value()); } if (image.isNull()) { it = hints.find(QStringLiteral("image-path")); if (it == end) { it = hints.find(QStringLiteral("image_path")); } if (it != end) { loadImagePath(it->toString()); } } sanitizeImage(image); } void Notification::Private::setUrgency(Notifications::Urgency urgency) { this->urgency = urgency; // Critical notifications must not time out // TODO should we really imply this here and not on the view side? // are there usecases for critical but can expire? // "critical updates available"? if (urgency == Notifications::CriticalUrgency) { timeout = 0; } } Notification::Notification(uint id) : d(new Private()) { d->id = id; d->created = QDateTime::currentDateTimeUtc(); } Notification::Notification(const Notification &other) : d(new Private(*other.d)) { } Notification::Notification(Notification &&other) : d(other.d) { other.d = nullptr; } Notification &Notification::operator=(const Notification &other) { d = new Private(*other.d); return *this; } Notification &Notification::operator=(Notification &&other) { d = other.d; other.d = nullptr; return *this; } Notification::~Notification() { delete d; } uint Notification::id() const { return d->id; } QString Notification::dBusService() const { return d->dBusService; } void Notification::setDBusService(const QString &dBusService) { d->dBusService = dBusService; } QDateTime Notification::created() const { return d->created; } QDateTime Notification::updated() const { return d->updated; } void Notification::resetUpdated() { d->updated = QDateTime::currentDateTimeUtc(); } bool Notification::read() const { return d->read; } void Notification::setRead(bool read) { d->read = read; } QString Notification::summary() const { return d->summary; } void Notification::setSummary(const QString &summary) { d->summary = summary; } QString Notification::body() const { return d->body; } void Notification::setBody(const QString &body) { d->body = Private::sanitize(body.trimmed()); } QString Notification::icon() const { return d->icon; } void Notification::setIcon(const QString &icon) { d->loadImagePath(icon); Private::sanitizeImage(d->image); } QImage Notification::image() const { return d->image; } void Notification::setImage(const QImage &image) { d->image = image; } QString Notification::desktopEntry() const { return d->desktopEntry; } void Notification::setDesktopEntry(const QString &desktopEntry) { d->setDesktopEntry(desktopEntry); } QString Notification::notifyRcName() const { return d->notifyRcName; } QString Notification::eventId() const { return d->eventId; } QString Notification::applicationName() const { return d->applicationName; } void Notification::setApplicationName(const QString &applicationName) { d->applicationName = applicationName; } QString Notification::applicationIconName() const { return d->applicationIconName; } void Notification::setApplicationIconName(const QString &applicationIconName) { d->applicationIconName = applicationIconName; } QString Notification::originName() const { return d->originName; } QStringList Notification::actionNames() const { return d->actionNames; } QStringList Notification::actionLabels() const { return d->actionLabels; } bool Notification::hasDefaultAction() const { return d->hasDefaultAction; } QString Notification::defaultActionLabel() const { return d->defaultActionLabel; } void Notification::setActions(const QStringList &actions) { if (actions.count() % 2 != 0) { qCWarning(NOTIFICATIONMANAGER) << "List of actions must contain an even number of items, tried to set actions to" << actions; return; } d->hasDefaultAction = false; d->hasConfigureAction = false; d->hasReplyAction = false; QStringList names; QStringList labels; for (int i = 0; i < actions.count(); i += 2) { const QString &name = actions.at(i); const QString &label = actions.at(i + 1); if (!d->hasDefaultAction && name == QLatin1String("default")) { d->hasDefaultAction = true; d->defaultActionLabel = label; continue; } if (!d->hasConfigureAction && name == QLatin1String("settings")) { d->hasConfigureAction = true; d->configureActionLabel = label; continue; } if (!d->hasReplyAction && name == QLatin1String("inline-reply")) { d->hasReplyAction = true; d->replyActionLabel = label; continue; } names << name; labels << label; } d->actionNames = names; d->actionLabels = labels; } QList Notification::urls() const { return d->urls; } void Notification::setUrls(const QList &urls) { d->urls = urls; } Notifications::Urgency Notification::urgency() const { return d->urgency; } bool Notification::userActionFeedback() const { return d->userActionFeedback; } int Notification::timeout() const { return d->timeout; } void Notification::setTimeout(int timeout) { d->timeout = timeout; } bool Notification::configurable() const { return d->hasConfigureAction || d->configurableNotifyRc || d->configurableService; } QString Notification::configureActionLabel() const { return d->configureActionLabel; } bool Notification::hasReplyAction() const { return d->hasReplyAction; } QString Notification::replyActionLabel() const { return d->replyActionLabel; } QString Notification::replyPlaceholderText() const { return d->replyPlaceholderText; } QString Notification::replySubmitButtonText() const { return d->replySubmitButtonText; } QString Notification::replySubmitButtonIconName() const { return d->replySubmitButtonIconName; } bool Notification::expired() const { return d->expired; } void Notification::setExpired(bool expired) { d->expired = expired; } bool Notification::dismissed() const { return d->dismissed; } void Notification::setDismissed(bool dismissed) { d->dismissed = dismissed; } void Notification::processHints(const QVariantMap &hints) { d->processHints(hints); } diff --git a/libnotificationmanager/notificationsmodel.cpp b/libnotificationmanager/notificationsmodel.cpp index fa46e2774..2a475641e 100644 --- a/libnotificationmanager/notificationsmodel.cpp +++ b/libnotificationmanager/notificationsmodel.cpp @@ -1,530 +1,530 @@ /* * Copyright 2018-2019 Kai Uwe Broulik * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) version 3, or any * later version accepted by the membership of KDE e.V. (or its * successor approved by the membership of KDE e.V.), which shall * act as a proxy defined in Section 6 of version 3 of the license. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . */ #include "notificationsmodel.h" #include "debug.h" #include "server.h" #include "notifications.h" #include "notification.h" #include "notification_p.h" #include #include #include #include #include #include static const int s_notificationsLimit = 1000; using namespace NotificationManager; class Q_DECL_HIDDEN NotificationsModel::Private { public: explicit Private(NotificationsModel *q); ~Private(); void onNotificationAdded(const Notification ¬ification); void onNotificationReplaced(uint replacedId, const Notification ¬ification); void onNotificationRemoved(uint notificationId, Server::CloseReason reason); void setupNotificationTimeout(const Notification ¬ification); int rowOfNotification(uint id) const; NotificationsModel *q; QVector notifications; // Fallback timeout to ensure all notifications expire eventually // otherwise when it isn't shown to the user and doesn't expire // an app might wait indefinitely for the notification to do so QHash notificationTimeouts; QDateTime lastRead; }; NotificationsModel::Private::Private(NotificationsModel *q) : q(q) , lastRead(QDateTime::currentDateTimeUtc()) { } NotificationsModel::Private::~Private() { qDeleteAll(notificationTimeouts); notificationTimeouts.clear(); } void NotificationsModel::Private::onNotificationAdded(const Notification ¬ification) { // Once we reach a certain insane number of notifications discard some old ones // as we keep pixmaps around etc if (notifications.count() >= s_notificationsLimit) { const int cleanupCount = s_notificationsLimit / 2; qCDebug(NOTIFICATIONMANAGER) << "Reached the notification limit of" << s_notificationsLimit << ", discarding the oldest" << cleanupCount << "notifications"; q->beginRemoveRows(QModelIndex(), 0, cleanupCount - 1); for (int i = 0 ; i < cleanupCount; ++i) { notifications.removeAt(0); // TODO close gracefully? } q->endRemoveRows(); } setupNotificationTimeout(notification); q->beginInsertRows(QModelIndex(), notifications.count(), notifications.count()); notifications.append(std::move(notification)); q->endInsertRows(); } void NotificationsModel::Private::onNotificationReplaced(uint replacedId, const Notification ¬ification) { const int row = rowOfNotification(replacedId); if (row == -1) { qCWarning(NOTIFICATIONMANAGER) << "Trying to replace notification with id" << replacedId << "which doesn't exist, creating a new one. This is an application bug!"; onNotificationAdded(notification); return; } setupNotificationTimeout(notification); notifications[row] = notification; const QModelIndex idx = q->index(row, 0); emit q->dataChanged(idx, idx); } void NotificationsModel::Private::onNotificationRemoved(uint removedId, Server::CloseReason reason) { const int row = rowOfNotification(removedId); if (row == -1) { return; } q->stopTimeout(removedId); // When a notification expired, keep it around in the history and mark it as such if (reason == Server::CloseReason::Expired) { const QModelIndex idx = q->index(row, 0); Notification ¬ification = notifications[row]; notification.setExpired(true); // Since the notification is "closed" it cannot have any actions // unless it is "resident" which we don't support notification.setActions(QStringList()); emit q->dataChanged(idx, idx, { Notifications::ExpiredRole, // TODO only emit those if actually changed? Notifications::ActionNamesRole, Notifications::ActionLabelsRole, Notifications::HasDefaultActionRole, Notifications::DefaultActionLabelRole, Notifications::ConfigurableRole }); return; } // Otherwise if explicitly closed by either user or app, remove it q->beginRemoveRows(QModelIndex(), row, row); notifications.removeAt(row); q->endRemoveRows(); } void NotificationsModel::Private::setupNotificationTimeout(const Notification ¬ification) { if (notification.timeout() == 0) { // In case it got replaced by a persistent notification q->stopTimeout(notification.id()); return; } QTimer *timer = notificationTimeouts.value(notification.id()); if (!timer) { timer = new QTimer(); timer->setSingleShot(true); connect(timer, &QTimer::timeout, q, [this, timer] { const uint id = timer->property("notificationId").toUInt(); q->expire(id); }); notificationTimeouts.insert(notification.id(), timer); } timer->stop(); timer->setProperty("notificationId", notification.id()); timer->setInterval(60000 /*1min*/ + (notification.timeout() == -1 ? 120000 /*2min, max configurable default timeout*/ : notification.timeout())); timer->start(); } int NotificationsModel::Private::rowOfNotification(uint id) const { auto it = std::find_if(notifications.constBegin(), notifications.constEnd(), [id](const Notification &item) { return item.id() == id; }); if (it == notifications.constEnd()) { return -1; } return std::distance(notifications.constBegin(), it); } NotificationsModel::NotificationsModel() : QAbstractListModel(nullptr) , d(new Private(this)) { connect(&Server::self(), &Server::notificationAdded, this, [this](const Notification ¬ification) { d->onNotificationAdded(notification); }); connect(&Server::self(), &Server::notificationReplaced, this, [this](uint replacedId, const Notification ¬ification) { d->onNotificationReplaced(replacedId, notification); }); connect(&Server::self(), &Server::notificationRemoved, this, [this](uint removedId, Server::CloseReason reason) { d->onNotificationRemoved(removedId, reason); }); connect(&Server::self(), &Server::serviceOwnershipLost, this, [this] { // Expire all notifications as we're defunct now const auto notifications = d->notifications; for (const Notification ¬ification : notifications) { if (!notification.expired()) { d->onNotificationRemoved(notification.id(), Server::CloseReason::Expired); } } }); Server::self().init(); } NotificationsModel::~NotificationsModel() = default; NotificationsModel::Ptr NotificationsModel::createNotificationsModel() { static QWeakPointer s_instance; if (!s_instance) { QSharedPointer ptr(new NotificationsModel()); s_instance = ptr.toWeakRef(); return ptr; } return s_instance.toStrongRef(); } QDateTime NotificationsModel::lastRead() const { return d->lastRead; } void NotificationsModel::setLastRead(const QDateTime &lastRead) { if (d->lastRead != lastRead) { d->lastRead = lastRead; emit lastReadChanged(); } } QVariant NotificationsModel::data(const QModelIndex &index, int role) const { if (!checkIndex(index)) { return QVariant(); } const Notification ¬ification = d->notifications.at(index.row()); switch (role) { case Notifications::IdRole: return notification.id(); case Notifications::TypeRole: return Notifications::NotificationType; case Notifications::CreatedRole: if (notification.created().isValid()) { return notification.created(); } break; case Notifications::UpdatedRole: if (notification.updated().isValid()) { return notification.updated(); } break; case Notifications::SummaryRole: return notification.summary(); case Notifications::BodyRole: return notification.body(); case Notifications::IconNameRole: if (notification.image().isNull()) { return notification.icon(); } break; case Notifications::ImageRole: if (!notification.image().isNull()) { return notification.image(); } break; case Notifications::DesktopEntryRole: return notification.desktopEntry(); case Notifications::NotifyRcNameRole: return notification.notifyRcName(); case Notifications::ApplicationNameRole: return notification.applicationName(); case Notifications::ApplicationIconNameRole: return notification.applicationIconName(); case Notifications::OriginNameRole: return notification.originName(); case Notifications::ActionNamesRole: return notification.actionNames(); case Notifications::ActionLabelsRole: return notification.actionLabels(); case Notifications::HasDefaultActionRole: return notification.hasDefaultAction(); case Notifications::DefaultActionLabelRole: return notification.defaultActionLabel(); case Notifications::UrlsRole: return QVariant::fromValue(notification.urls()); case Notifications::UrgencyRole: return static_cast(notification.urgency()); case Notifications::UserActionFeedbackRole: return notification.userActionFeedback(); case Notifications::TimeoutRole: return notification.timeout(); case Notifications::ClosableRole: return true; case Notifications::ConfigurableRole: return notification.configurable(); case Notifications::ConfigureActionLabelRole: return notification.configureActionLabel(); case Notifications::ExpiredRole: return notification.expired(); case Notifications::ReadRole: return notification.read(); case Notifications::HasReplyActionRole: return notification.hasReplyAction(); case Notifications::ReplyActionLabelRole: return notification.replyActionLabel(); case Notifications::ReplyPlaceholderTextRole: return notification.replyPlaceholderText(); case Notifications::ReplySubmitButtonTextRole: return notification.replySubmitButtonText(); case Notifications::ReplySubmitButtonIconNameRole: return notification.replySubmitButtonIconName(); } return QVariant(); } bool NotificationsModel::setData(const QModelIndex &index, const QVariant &value, int role) { if (!checkIndex(index)) { return false; } Notification ¬ification = d->notifications[index.row()]; switch (role) { case Notifications::ReadRole: if (value.toBool() != notification.read()) { notification.setRead(value.toBool()); return true; } break; } return false; } int NotificationsModel::rowCount(const QModelIndex &parent) const { if (parent.isValid()) { return 0; } return d->notifications.count(); } void NotificationsModel::expire(uint notificationId) { if (d->rowOfNotification(notificationId) > -1) { Server::self().closeNotification(notificationId, Server::CloseReason::Expired); } } void NotificationsModel::close(uint notificationId) { if (d->rowOfNotification(notificationId) > -1) { Server::self().closeNotification(notificationId, Server::CloseReason::DismissedByUser); } } void NotificationsModel::configure(uint notificationId) { const int row = d->rowOfNotification(notificationId); if (row == -1) { return; } const Notification ¬ification = d->notifications.at(row); if (notification.d->hasConfigureAction) { Server::self().invokeAction(notificationId, QStringLiteral("settings")); // FIXME make a static Notification::configureActionName() or something return; } if (!notification.desktopEntry().isEmpty() || !notification.notifyRcName().isEmpty()) { configure(notification.desktopEntry(), notification.notifyRcName(), notification.eventId()); return; } qCWarning(NOTIFICATIONMANAGER) << "Trying to configure notification" << notificationId << "which isn't configurable"; } void NotificationsModel::configure(const QString &desktopEntry, const QString ¬ifyRcName, const QString &eventId) { // TODO would be nice to just have a signal but since NotificationsModel is shared, // if we connect to this from Notifications you would get a signal in every instance - // and potentialy open the config dialog multiple times. + // and potentially open the config dialog multiple times. QStringList args; if (!desktopEntry.isEmpty()) { args.append(QStringLiteral("--desktop-entry")); args.append(desktopEntry); } if (!notifyRcName.isEmpty()) { args.append(QStringLiteral("--notifyrc")); args.append(notifyRcName); } if (!eventId.isEmpty()) { args.append(QStringLiteral("--event-id")); args.append(eventId); } QProcess::startDetached(QStringLiteral("kcmshell5"), { QStringLiteral("notifications"), QStringLiteral("--args"), KShell::joinArgs(args) }); } void NotificationsModel::invokeDefaultAction(uint notificationId) { const int row = d->rowOfNotification(notificationId); if (row == -1) { return; } const Notification ¬ification = d->notifications.at(row); if (!notification.hasDefaultAction()) { qCWarning(NOTIFICATIONMANAGER) << "Trying to invoke default action on notification" << notificationId << "which doesn't have one"; return; } Server::self().invokeAction(notificationId, QStringLiteral("default")); // FIXME make a static Notification::defaultActionName() or something } void NotificationsModel::invokeAction(uint notificationId, const QString &actionName) { const int row = d->rowOfNotification(notificationId); if (row == -1) { return; } const Notification ¬ification = d->notifications.at(row); if (!notification.actionNames().contains(actionName)) { qCWarning(NOTIFICATIONMANAGER) << "Trying to invoke action" << actionName << "on notification" << notificationId << "which it doesn't have"; return; } Server::self().invokeAction(notificationId, actionName); } void NotificationsModel::reply(uint notificationId, const QString &text) { const int row = d->rowOfNotification(notificationId); if (row == -1) { return; } const Notification ¬ification = d->notifications.at(row); if (!notification.hasReplyAction()) { qCWarning(NOTIFICATIONMANAGER) << "Trying to reply to a notification which doesn't have a reply action"; return; } Server::self().reply(notification.dBusService(), notificationId, text); } void NotificationsModel::startTimeout(uint notificationId) { const int row = d->rowOfNotification(notificationId); if (row == -1) { return; } const Notification ¬ification = d->notifications.at(row); if (!notification.timeout() || notification.expired()) { return; } d->setupNotificationTimeout(notification); } void NotificationsModel::stopTimeout(uint notificationId) { delete d->notificationTimeouts.take(notificationId); } void NotificationsModel::clear(Notifications::ClearFlags flags) { if (d->notifications.isEmpty()) { return; } // Tries to remove a contiguous group if possible as the likely case is // you have n unread notifications at the end of the list, we don't want to // remove and signal each item individually QVector> clearQueue; QPair clearRange{-1, -1}; for (int i = d->notifications.count() - 1; i >= 0; --i) { const Notification ¬ification = d->notifications.at(i); bool clear = (flags.testFlag(Notifications::ClearExpired) && notification.expired()); if (clear) { if (clearRange.second == -1) { clearRange.second = i; } clearRange.first = i; } else { if (clearRange.first != -1) { clearQueue.append(clearRange); clearRange.first = -1; clearRange.second = -1; } } } if (clearRange.first != -1) { clearQueue.append(clearRange); clearRange.first = -1; clearRange.second = -1; } for (const auto &range : clearQueue) { beginRemoveRows(QModelIndex(), range.first, range.second); for (int i = range.second; i >= range.first; --i) { d->notifications.removeAt(i); } endRemoveRows(); } } diff --git a/libnotificationmanager/server.h b/libnotificationmanager/server.h index 039fb310b..c5ebc8af6 100644 --- a/libnotificationmanager/server.h +++ b/libnotificationmanager/server.h @@ -1,233 +1,233 @@ /* * Copyright 2018 Kai Uwe Broulik * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) version 3, or any * later version accepted by the membership of KDE e.V. (or its * successor approved by the membership of KDE e.V.), which shall * act as a proxy defined in Section 6 of version 3 of the license. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . */ #pragma once #include #include "notificationmanager_export.h" namespace NotificationManager { class Notification; class ServerInfo; class ServerPrivate; /** * @short A notification DBus server * * @author Kai Uwe Broulik **/ class NOTIFICATIONMANAGER_EXPORT Server : public QObject { Q_OBJECT /** * Whether the notification service could be registered. * Call @c init() to register. */ Q_PROPERTY(bool valid READ isValid NOTIFY validChanged) /** * Information about the current owner of the Notification service. * * This can be used to tell the user which application is currently * owning the service in case service registration failed. * * This is never null, even if there is no notification service running. * * @since 5.18 */ Q_PROPERTY(NotificationManager::ServerInfo *currentOwner READ currentOwner CONSTANT) /** * Whether notifications are currently inhibited. * - * This is what is announced to other applicatons on the bus. + * This is what is announced to other applications on the bus. * * @note This does not keep track of inhibitions on its own, * you need to calculate this yourself and update the property accordingly. */ Q_PROPERTY(bool inhibited READ inhibited WRITE setInhibited NOTIFY inhibitedChanged) public: ~Server() override; /** * The reason a notification was closed */ enum class CloseReason { Expired = 1, ///< The notification timed out DismissedByUser = 2, ///< The user explicitly closed or acknowledged the notification Revoked = 3 ///< The notification was revoked by the issuing app because it is no longer relevant }; Q_ENUM(CloseReason) static Server &self(); /** * Registers the Notification Service on DBus. * * @return true if it succeeded, false otherwise. */ bool init(); /** * Whether the notification service could be registered */ bool isValid() const; /** * Information about the current owner of the Notification service. * @since 5.18 */ ServerInfo *currentOwner() const; /** * Whether notifications are currently inhibited. * @since 5.17 */ bool inhibited() const; /** * Whether notifications are currently effectively inhibited. * * @note You need to keep track of inhibitions and call this * yourself when appropriate. * @since 5.17 */ void setInhibited(bool inhibited); /** * Whether an application requested to inhibit notifications. */ bool inhibitedByApplication() const; // should we return a struct or pair or something? QStringList inhibitionApplications() const; QStringList inhibitionReasons() const; /** * Remove all inhibitions. * * @note The applications are not explicitly informed about this. */ void clearInhibitions(); /** * Sends a notification closed event * * @param id The notification ID * @param reason The reason why it was closed */ void closeNotification(uint id, CloseReason reason); /** * Sends an action invocation request * * @param id The notification ID * @param actionName The name of the action, e.g. "Action 1", or "default" */ void invokeAction(uint id, const QString &actionName); /** * Sends a notification reply text * * @param dbusService The bus name of the receiving application * @param id The notification ID * @param text The reply message text * @since 5.18 */ void reply(const QString &dbusService, uint id, const QString &text); /** * Adds a notification * * @note The notification isn't actually broadcast * but just emitted locally. * * @return the ID of the notification */ uint add(const Notification ¬ification); Q_SIGNALS: /** * Emitted when the notification service validity changes, - * because it sucessfully registered the service or lost + * because it successfully registered the service or lost * ownership of it. * @since 5.18 */ void validChanged(); /** * Emitted when a notification was added. * This is emitted regardless of any filtering rules or user settings. * @param notification The notification */ void notificationAdded(const Notification ¬ification); /** * Emitted when a notification is supposed to be updated * This is emitted regardless of any filtering rules or user settings. * @param replacedId The ID of the notification it replaces * @param notification The new notification to use instead */ void notificationReplaced(uint replacedId, const Notification ¬ification); /** * Emitted when a notification got removed (closed) * @param id The notification ID * @param reason The reason why it was closed */ void notificationRemoved(uint id, CloseReason reason); /** * Emitted when the inhibited state changed. */ void inhibitedChanged(bool inhibited); /** * Emitted when inhibitions by application have been changed. * Becomes true as soon as there is one inhibition and becomes * false again when all inhibitions have been lifted. * @since 5.17 */ void inhibitedByApplicationChanged(bool inhibited); /** * Emitted when the list of applications holding a notification * inhibition changes. * Normally you would only want to listen do @c inhibitedChanged */ void inhibitionApplicationsChanged(); /** * Emitted when the ownership of the Notification DBus Service is lost. */ void serviceOwnershipLost(); private: explicit Server(QObject *parent = nullptr); Q_DISABLE_COPY(Server) // FIXME we also need to disable move and other stuff? QScopedPointer d; }; } // namespace NotificationManager diff --git a/plasma-windowed/plasmawindowedcorona.cpp b/plasma-windowed/plasmawindowedcorona.cpp index a589dd03f..4ac91d85a 100644 --- a/plasma-windowed/plasmawindowedcorona.cpp +++ b/plasma-windowed/plasmawindowedcorona.cpp @@ -1,181 +1,181 @@ /* * Copyright 2014 Bhushan Shah * Copyright 2014 Marco Martin * * 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) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * 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 "plasmawindowedcorona.h" #include "plasmawindowedview.h" #include #include #include #include #include #include #include #include PlasmaWindowedCorona::PlasmaWindowedCorona(QObject *parent) : Plasma::Corona(parent) { KPackage::Package package = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("Plasma/Shell")); package.setPath(QStringLiteral("org.kde.plasma.desktop")); setKPackage(package); //QMetaObject::invokeMethod(this, "load", Qt::QueuedConnection); load(); } void PlasmaWindowedCorona::loadApplet(const QString &applet, const QVariantList &arguments) { if (containments().isEmpty()) { return; } Plasma::Containment *cont = containments().first(); - //forbid more instances per applet (todo: activate the correpsponding already loaded applet) + //forbid more instances per applet (todo: activate the corresponding already loaded applet) for (Plasma::Applet *a : cont->applets()) { if (a->pluginMetaData().pluginId() == applet) { return; } } PlasmaWindowedView *v = new PlasmaWindowedView(); v->setHasStatusNotifier(m_hasStatusNotifier); v->show(); KConfigGroup appletsGroup(KSharedConfig::openConfig(), "Applets"); QString plugin; for (const QString &group : appletsGroup.groupList()) { KConfigGroup cg(&appletsGroup, group); plugin = cg.readEntry("plugin", QString()); if (plugin == applet) { Plasma::Applet *a = Plasma::PluginLoader::self()->loadApplet(applet, group.toInt(), arguments); if (!a) { qWarning() << "Unable to load applet" << applet << "with arguments" <deleteLater(); return; } a->restore(cg); //Access a->config() before adding to containment //will cause applets to be saved in palsmawindowedrc //so applets will only be created on demand KConfigGroup cg2 = a->config(); cont->addApplet(a); v->setApplet(a); return; } } Plasma::Applet *a = Plasma::PluginLoader::self()->loadApplet(applet, 0, arguments); if (!a) { qWarning() << "Unable to load applet" << applet << "with arguments" <deleteLater(); return; } //Access a->config() before adding to containment //will cause applets to be saved in palsmawindowedrc //so applets will only be created on demand KConfigGroup cg2 = a->config(); cont->addApplet(a); v->setApplet(a); } void PlasmaWindowedCorona::activateRequested(const QStringList &arguments, const QString &workingDirectory) { Q_UNUSED(workingDirectory) if (arguments.count() <= 1) { return; } QCommandLineParser parser; parser.setApplicationDescription(i18n("Plasma Windowed")); parser.addOption(QCommandLineOption(QStringLiteral("statusnotifier"), i18n("Makes the plasmoid stay alive in the Notification Area, even when the window is closed."))); parser.addPositionalArgument(QStringLiteral("applet"), i18n("The applet to open.")); parser.addPositionalArgument(QStringLiteral("args"), i18n("Arguments to pass to the plasmoid."), QStringLiteral("[args...]")); parser.addVersionOption(); parser.addHelpOption(); parser.process(arguments); if (parser.positionalArguments().isEmpty()) { parser.showHelp(1); } const QStringList positionalArguments = parser.positionalArguments(); QVariantList args; QStringList::const_iterator constIterator = positionalArguments.constBegin() + 1; for (; constIterator != positionalArguments.constEnd(); ++constIterator) { args << (*constIterator); } loadApplet(positionalArguments.first(), args); } QRect PlasmaWindowedCorona::screenGeometry(int id) const { Q_UNUSED(id); //TODO? return QRect(); } void PlasmaWindowedCorona::load() { /*this won't load applets, since applets are in plasmawindowedrc*/ loadLayout(QStringLiteral("plasmawindowed-appletsrc")); bool found = false; for (auto c : containments()) { if (c->containmentType() == Plasma::Types::DesktopContainment) { found = true; break; } } if (!found) { qDebug() << "Loading default layout"; createContainment(QStringLiteral("empty")); saveLayout(QStringLiteral("plasmawindowed-appletsrc")); } for (auto c : containments()) { if (c->containmentType() == Plasma::Types::DesktopContainment) { m_containment = c; m_containment->setFormFactor(Plasma::Types::Application); QAction *removeAction = c->actions()->action(QStringLiteral("remove")); if(removeAction) { removeAction->deleteLater(); } break; } } } void PlasmaWindowedCorona::setHasStatusNotifier(bool stay) { m_hasStatusNotifier = stay; } diff --git a/runners/calculator/calculatorrunner.cpp b/runners/calculator/calculatorrunner.cpp index bb93fa1b1..98686f260 100644 --- a/runners/calculator/calculatorrunner.cpp +++ b/runners/calculator/calculatorrunner.cpp @@ -1,357 +1,357 @@ /* * Copyright (C) 2007 Barış Metin * Copyright (C) 2006 David Faure * Copyright (C) 2007 Richard Moore * Copyright (C) 2010 Matteo Agostinelli * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License version 2 as * published by the Free Software Foundation * * 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 Library General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "calculatorrunner.h" #ifdef ENABLE_QALCULATE #include "qalculate_engine.h" #else #include #include #include #endif #include #include #include #include static const QString s_copyToClipboardId = QStringLiteral("copyToClipboard"); K_EXPORT_PLASMA_RUNNER(calculatorrunner, CalculatorRunner) CalculatorRunner::CalculatorRunner( QObject* parent, const QVariantList &args ) : Plasma::AbstractRunner(parent, args) { Q_UNUSED(args) #ifdef ENABLE_QALCULATE m_engine = new QalculateEngine; setSpeed(SlowSpeed); #endif setObjectName( QStringLiteral("Calculator" )); setIgnoredTypes(Plasma::RunnerContext::Directory | Plasma::RunnerContext::File | Plasma::RunnerContext::NetworkLocation | Plasma::RunnerContext::Executable | Plasma::RunnerContext::ShellCommand); QString description = i18n("Calculates the value of :q: when :q: is made up of numbers and " "mathematical symbols such as +, -, /, * and ^."); addSyntax(Plasma::RunnerSyntax(QStringLiteral(":q:"), description)); addSyntax(Plasma::RunnerSyntax(QStringLiteral("=:q:"), description)); addSyntax(Plasma::RunnerSyntax(QStringLiteral(":q:="), description)); addAction(s_copyToClipboardId, QIcon::fromTheme(QStringLiteral("edit-copy")), i18n("Copy to Clipboard")); } CalculatorRunner::~CalculatorRunner() { #ifdef ENABLE_QALCULATE delete m_engine; #endif } void CalculatorRunner::powSubstitutions(QString& cmd) { if (cmd.contains(QLatin1String("e+"), Qt::CaseInsensitive)) { cmd.replace(QLatin1String("e+"), QLatin1String("*10^"), Qt::CaseInsensitive); } if (cmd.contains(QLatin1String("e-"), Qt::CaseInsensitive)) { cmd.replace(QLatin1String("e-"), QLatin1String("*10^-"), Qt::CaseInsensitive); } // the below code is scary mainly because we have to honor priority // honor decimal numbers and parenthesis. while (cmd.contains(QLatin1Char('^'))) { int where = cmd.indexOf(QLatin1Char('^')); cmd.replace(where, 1, QLatin1Char(',')); int preIndex = where - 1; int postIndex = where + 1; int count = 0; QChar decimalSymbol = QLocale().decimalPoint(); //avoid out of range on weird commands preIndex = qMax(0, preIndex); postIndex = qMin(postIndex, cmd.length()-1); //go backwards looking for the beginning of the number or expression while (preIndex != 0) { QChar current = cmd.at(preIndex); QChar next = cmd.at(preIndex-1); //qDebug() << "index " << preIndex << " char " << current; if (current == QLatin1Char(')')) { count++; } else if (current == QLatin1Char('(')) { count--; } else { if (((next <= QLatin1Char('9') ) && (next >= QLatin1Char('0'))) || next == decimalSymbol) { preIndex--; continue; } } if (count == 0) { //check for functions if (!((next <= QLatin1Char('z') ) && (next >= QLatin1Char('a')))) { break; } } preIndex--; } //go forwards looking for the end of the number or expression count = 0; while (postIndex != cmd.size() - 1) { QChar current=cmd.at(postIndex); QChar next=cmd.at(postIndex + 1); //check for functions if ((count == 0) && (current <= QLatin1Char('z')) && (current >= QLatin1Char('a'))) { postIndex++; continue; } if (current == QLatin1Char('(')) { count++; } else if (current == QLatin1Char(')')) { count--; } else { if (((next <= QLatin1Char('9') ) && (next >= QLatin1Char('0'))) || next == decimalSymbol) { postIndex++; continue; } } if (count == 0) { break; } postIndex++; } preIndex = qMax(0, preIndex); postIndex = qMin(postIndex, cmd.length()); cmd.insert(preIndex,QLatin1String("pow(")); // +1 +4 == next position to the last number after we add 4 new characters pow( cmd.insert(postIndex + 1 + 4, QLatin1Char(')')); //qDebug() << "from" << preIndex << " to " << postIndex << " got: " << cmd; } } void CalculatorRunner::hexSubstitutions(QString& cmd) { if (cmd.contains(QLatin1String("0x"))) { //Append +0 so that the calculator can serve also as a hex converter cmd.append(QLatin1String("+0")); bool ok; int pos = 0; QString hex; while (cmd.contains(QLatin1String("0x"))) { hex.clear(); pos = cmd.indexOf(QLatin1String("0x"), pos); for (int q = 0; q < cmd.size(); q++) {//find end of hex number QChar current = cmd[pos+q+2]; if (((current <= QLatin1Char('9') ) && (current >= QLatin1Char('0'))) || ((current <= QLatin1Char('F') ) && (current >= QLatin1Char('A'))) || ((current <= QLatin1Char('f') ) && (current >= QLatin1Char('a')))) { //Check if valid hex sign hex[q] = current; } else { break; } } cmd = cmd.replace(pos, 2+hex.length(), QString::number(hex.toInt(&ok,16))); //replace hex with decimal } } } void CalculatorRunner::userFriendlySubstitutions(QString& cmd) { if (cmd.contains(QLocale().decimalPoint(), Qt::CaseInsensitive)) { cmd.replace(QLocale().decimalPoint(), QLatin1Char('.'), Qt::CaseInsensitive); } // the following substitutions are not needed with libqalculate #ifndef ENABLE_QALCULATE hexSubstitutions(cmd); powSubstitutions(cmd); if (cmd.contains(QRegExp(QStringLiteral("\\d+and\\d+")))) { cmd.replace(QRegExp(QStringLiteral("(\\d+)and(\\d+)")), QStringLiteral("\\1&\\2")); } if (cmd.contains(QRegExp(QStringLiteral("\\d+or\\d+")))) { cmd.replace(QRegExp(QStringLiteral("(\\d+)or(\\d+)")), QStringLiteral("\\1|\\2")); } if (cmd.contains(QRegExp(QStringLiteral("\\d+xor\\d+")))) { cmd.replace(QRegExp(QStringLiteral("(\\d+)xor(\\d+)")), QStringLiteral("\\1^\\2")); } #endif } void CalculatorRunner::match(Plasma::RunnerContext &context) { const QString term = context.query(); QString cmd = term; //no meanless space between friendly guys: helps simplify code cmd = cmd.trimmed().remove(QLatin1Char(' ')); if (cmd.length() < 3) { return; } if (cmd.toLower() == QLatin1String("universe") || cmd.toLower() == QLatin1String("life")) { Plasma::QueryMatch match(this); match.setType(Plasma::QueryMatch::InformationalMatch); match.setIconName(QStringLiteral("accessories-calculator")); match.setText(QStringLiteral("42")); match.setData(QStringLiteral("42")); match.setId(term); context.addMatch(match); return; } bool toHex = cmd.startsWith(QLatin1String("hex=")); bool startsWithEquals = !toHex && cmd[0] == QLatin1Char('='); if (toHex || startsWithEquals) { cmd.remove(0, cmd.indexOf(QLatin1Char('=')) + 1); } else if (cmd.endsWith(QLatin1Char('='))) { cmd.chop(1); } else { bool foundDigit = false; for (int i = 0; i < cmd.length(); ++i) { QChar c = cmd.at(i); if (c.isLetter()) { // not just numbers and symbols, so we return return; } if (c.isDigit()) { foundDigit = true; } } if (!foundDigit) { return; } } if (cmd.isEmpty()) { return; } userFriendlySubstitutions(cmd); #ifndef ENABLE_QALCULATE - cmd.replace(QRegExp(QStringLiteral("([a-zA-Z]+)")), QStringLiteral("Math.\\1")); //needed for accessing math funktions like sin(),.... + cmd.replace(QRegExp(QStringLiteral("([a-zA-Z]+)")), QStringLiteral("Math.\\1")); //needed for accessing math functions like sin(),.... #endif bool isApproximate = false; QString result = calculate(cmd, &isApproximate); if (!result.isEmpty() && result != cmd) { if (toHex) { result = QLatin1String("0x") + QString::number(result.toInt(), 16).toUpper(); } Plasma::QueryMatch match(this); match.setType(Plasma::QueryMatch::InformationalMatch); match.setIconName(QStringLiteral("accessories-calculator")); match.setText(result); if (isApproximate) { match.setSubtext(i18nc("The result of the calculation is only an approximation", "Approximation")); } match.setData(result); match.setId(term); context.addMatch(match); } } QString CalculatorRunner::calculate(const QString& term, bool *isApproximate) { #ifdef ENABLE_QALCULATE QString result; try { result = m_engine->evaluate(term, isApproximate); } catch(std::exception& e) { qDebug() << "qalculate error: " << e.what(); } return result.replace(QLatin1Char('.'), QLocale().decimalPoint(), Qt::CaseInsensitive); #else Q_UNUSED(isApproximate); //qDebug() << "calculating" << term; QJSEngine eng; QJSValue result = eng.evaluate(QStringLiteral("var result = %1; result").arg(term)); if (result.isError()) { return QString(); } const QString resultString = result.toString(); if (resultString.isEmpty()) { return QString(); } if (!resultString.contains(QLatin1Char('.'))) { return resultString; } //ECMAScript has issues with the last digit in simple rational computations //This script rounds off the last digit; see bug 167986 QString roundedResultString = eng.evaluate(QStringLiteral("var exponent = 14-(1+Math.floor(Math.log(Math.abs(result))/Math.log(10)));\ var order=Math.pow(10,exponent);\ (order > 0? Math.round(result*order)/order : 0)")).toString(); roundedResultString.replace(QLatin1Char('.'), QLocale().decimalPoint(), Qt::CaseInsensitive); return roundedResultString; #endif } void CalculatorRunner::run(const Plasma::RunnerContext &context, const Plasma::QueryMatch &match) { Q_UNUSED(context); if (match.selectedAction() == action(s_copyToClipboardId)) { #ifdef ENABLE_QALCULATE m_engine->copyToClipboard(); #else QGuiApplication::clipboard()->setText(match.text()); #endif } } QList CalculatorRunner::actionsForMatch(const Plasma::QueryMatch &match) { Q_UNUSED(match) return {action(s_copyToClipboardId)}; } QMimeData * CalculatorRunner::mimeDataForMatch(const Plasma::QueryMatch &match) { //qDebug(); QMimeData *result = new QMimeData(); result->setText(match.text()); return result; } #include "calculatorrunner.moc" diff --git a/runners/services/servicerunner.h b/runners/services/servicerunner.h index 8d9ad1a75..395db3ffb 100644 --- a/runners/services/servicerunner.h +++ b/runners/services/servicerunner.h @@ -1,57 +1,57 @@ /* * Copyright (C) 2006 Aaron Seigo * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License version 2 as * published by the Free Software Foundation * * 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 Library General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef SERVICERUNNER_H #define SERVICERUNNER_H #include //#include #include /** * This class looks for matches in the set of .desktop files installed by * applications. This way the user can type exactly what they see in the - * appications menu and have it start the appropriate app. Essentially anything + * applications menu and have it start the appropriate app. Essentially anything * that KService knows about, this runner can launch */ class ServiceRunner : public Plasma::AbstractRunner { Q_OBJECT public: ServiceRunner(QObject *parent, const QVariantList &args); ~ServiceRunner() override; void match(Plasma::RunnerContext &context) override; void run(const Plasma::RunnerContext &context, const Plasma::QueryMatch &action) override; QStringList categories() const override; QIcon categoryIcon(const QString& category) const override; protected Q_SLOTS: QMimeData * mimeDataForMatch(const Plasma::QueryMatch &match) override; protected: void setupMatch(const KService::Ptr &service, Plasma::QueryMatch &action); }; #endif diff --git a/runners/windowedwidgets/windowedwidgetsrunner.h b/runners/windowedwidgets/windowedwidgetsrunner.h index fbc8006ea..782c86a9d 100644 --- a/runners/windowedwidgets/windowedwidgetsrunner.h +++ b/runners/windowedwidgets/windowedwidgetsrunner.h @@ -1,55 +1,55 @@ /* * Copyright (C) 2006 Aaron Seigo * Copyright (C) 2010 Marco Martin * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License version 2 as * published by the Free Software Foundation * * 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 Library General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef WINDOWEDWIDGETSRUNNER_H #define WINDOWEDWIDGETSRUNNER_H #include #include /** * This class looks for matches in the set of .desktop files installed by * applications. This way the user can type exactly what they see in the - * appications menu and have it start the appropriate app. Essentially anything + * applications menu and have it start the appropriate app. Essentially anything * that KService knows about, this runner can launch */ class WindowedWidgetsRunner : public Plasma::AbstractRunner { Q_OBJECT public: WindowedWidgetsRunner(QObject *parent, const QVariantList &args); ~WindowedWidgetsRunner() override; void match(Plasma::RunnerContext &context) override; void run(const Plasma::RunnerContext &context, const Plasma::QueryMatch &action) override; protected Q_SLOTS: QMimeData * mimeDataForMatch(const Plasma::QueryMatch &match) override; protected: void setupMatch(const KPluginMetaData &md, Plasma::QueryMatch &action); }; #endif diff --git a/xembed-sni-proxy/sniproxy.cpp b/xembed-sni-proxy/sniproxy.cpp index 09f00464d..c72a88a46 100644 --- a/xembed-sni-proxy/sniproxy.cpp +++ b/xembed-sni-proxy/sniproxy.cpp @@ -1,620 +1,620 @@ /* * Holds one embedded window, registers as DBus entry * Copyright (C) 2015 David Edmundson * Copyright (C) 2019 Konrad Materka * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * */ #include "sniproxy.h" #include #include #include #include #include #include "xcbutils.h" #include "debug.h" #include #include #include #include #include #include #include #include "statusnotifieritemadaptor.h" #include "statusnotifierwatcher_interface.h" #include "xtestsender.h" //#define VISUAL_DEBUG #define SNI_WATCHER_SERVICE_NAME "org.kde.StatusNotifierWatcher" #define SNI_WATCHER_PATH "/StatusNotifierWatcher" static uint16_t s_embedSize = 32; //max size of window to embed. We no longer resize the embedded window as Chromium acts stupidly. static unsigned int XEMBED_VERSION = 0; int SNIProxy::s_serviceCount = 0; void xembed_message_send(xcb_window_t towin, long message, long d1, long d2, long d3) { xcb_client_message_event_t ev; ev.response_type = XCB_CLIENT_MESSAGE; ev.window = towin; ev.format = 32; ev.data.data32[0] = XCB_CURRENT_TIME; ev.data.data32[1] = message; ev.data.data32[2] = d1; ev.data.data32[3] = d2; ev.data.data32[4] = d3; ev.type = Xcb::atoms->xembedAtom; xcb_send_event(QX11Info::connection(), false, towin, XCB_EVENT_MASK_NO_EVENT, (char *) &ev); } SNIProxy::SNIProxy(xcb_window_t wid, QObject* parent): QObject(parent), //Work round a bug in our SNIWatcher with multiple SNIs per connection. //there is an undocumented feature that you can register an SNI by path, however it doesn't detect an object on a service being removed, only the entire service closing //instead lets use one DBus connection per SNI m_dbus(QDBusConnection::connectToBus(QDBusConnection::SessionBus, QStringLiteral("XembedSniProxy%1").arg(s_serviceCount++))), m_windowId(wid), sendingClickEvent(false), m_injectMode(Direct) { //create new SNI new StatusNotifierItemAdaptor(this); m_dbus.registerObject(QStringLiteral("/StatusNotifierItem"), this); auto statusNotifierWatcher = new org::kde::StatusNotifierWatcher(QStringLiteral(SNI_WATCHER_SERVICE_NAME), QStringLiteral(SNI_WATCHER_PATH), QDBusConnection::sessionBus(), this); auto reply = statusNotifierWatcher->RegisterStatusNotifierItem(m_dbus.baseService()); reply.waitForFinished(); if (reply.isError()) { qCWarning(SNIPROXY) << "could not register SNI:" << reply.error().message(); } auto c = QX11Info::connection(); //create a container window auto screen = xcb_setup_roots_iterator (xcb_get_setup (c)).data; m_containerWid = xcb_generate_id(c); uint32_t values[3]; uint32_t mask = XCB_CW_BACK_PIXEL | XCB_CW_OVERRIDE_REDIRECT | XCB_CW_EVENT_MASK; values[0] = screen->black_pixel; //draw a solid background so the embedded icon doesn't get garbage in it values[1] = true; //bypass wM values[2] = XCB_EVENT_MASK_VISIBILITY_CHANGE | // receive visibility change, to handle KWin restart #357443 // Redirect and handle structure (size, position) requests from the embedded window. XCB_EVENT_MASK_STRUCTURE_NOTIFY | XCB_EVENT_MASK_SUBSTRUCTURE_NOTIFY | XCB_EVENT_MASK_SUBSTRUCTURE_REDIRECT; xcb_create_window (c, /* connection */ XCB_COPY_FROM_PARENT, /* depth */ m_containerWid, /* window Id */ screen->root, /* parent window */ 0, 0, /* x, y */ s_embedSize, s_embedSize, /* width, height */ 0, /* border_width */ XCB_WINDOW_CLASS_INPUT_OUTPUT,/* class */ screen->root_visual, /* visual */ mask, values); /* masks */ /* We need the window to exist and be mapped otherwise the child won't render it's contents We also need it to exist in the right place to get the clicks working as GTK will check sendEvent locations to see if our window is in the right place. So even though our contents are drawn via compositing we still put this window in the right place We can't composite it away anything parented owned by the root window (apparently) Stack Under works in the non composited case, but it doesn't seem to work in kwin's composited case (probably need set relevant NETWM hint) As a last resort set opacity to 0 just to make sure this container never appears */ #ifndef VISUAL_DEBUG stackContainerWindow(XCB_STACK_MODE_BELOW); NETWinInfo wm(c, m_containerWid, screen->root, NET::Properties(), NET::Properties2()); wm.setOpacity(0); #endif xcb_flush(c); xcb_map_window(c, m_containerWid); xcb_reparent_window(c, wid, m_containerWid, 0, 0); /* * Render the embedded window offscreen */ xcb_composite_redirect_window(c, wid, XCB_COMPOSITE_REDIRECT_MANUAL); /* we grab the window, but also make sure it's automatically reparented back * to the root window if we should die. */ xcb_change_save_set(c, XCB_SET_MODE_INSERT, wid); //tell client we're embedding it xembed_message_send(wid, XEMBED_EMBEDDED_NOTIFY, 0, m_containerWid, XEMBED_VERSION); //move window we're embedding const uint32_t windowMoveConfigVals[2] = { 0, 0 }; xcb_configure_window(c, wid, XCB_CONFIG_WINDOW_X | XCB_CONFIG_WINDOW_Y, windowMoveConfigVals); QSize clientWindowSize = calculateClientWindowSize(); //show the embedded window otherwise nothing happens xcb_map_window(c, wid); xcb_clear_area(c, 0, wid, 0, 0, clientWindowSize.width(), clientWindowSize.height()); xcb_flush(c); //guess which input injection method to use //we can either send an X event to the client or XTest //some don't support direct X events (GTK3/4), and some don't support XTest because reasons //note also some clients might not have the XTest extension. We may as well assume it does and just fail to send later. //we query if the client selected button presses in the event mask //if the client does supports that we send directly, otherwise we'll use xtest auto waCookie = xcb_get_window_attributes(c, wid); QScopedPointer windowAttributes(xcb_get_window_attributes_reply(c, waCookie, nullptr)); if (windowAttributes && ! (windowAttributes->all_event_masks & XCB_EVENT_MASK_BUTTON_PRESS)) { m_injectMode = XTest; } //there's no damage event for the first paint, and sometimes it's not drawn immediately //not ideal, but it works better than nothing //test with xchat before changing QTimer::singleShot(500, this, &SNIProxy::update); } SNIProxy::~SNIProxy() { auto c = QX11Info::connection(); xcb_destroy_window(c, m_containerWid); QDBusConnection::disconnectFromBus(m_dbus.name()); } void SNIProxy::update() { const QImage image = getImageNonComposite(); if (image.isNull()) { qCDebug(SNIPROXY) << "No xembed icon for" << m_windowId << Title(); return; } int w = image.width(); int h = image.height(); m_pixmap = QPixmap::fromImage(image); if (w > s_embedSize || h > s_embedSize) { qCDebug(SNIPROXY) << "Scaling pixmap of window" << m_windowId << Title() << "from w*h" << w << h; m_pixmap = m_pixmap.scaled(s_embedSize, s_embedSize, Qt::KeepAspectRatio, Qt::SmoothTransformation); } emit NewIcon(); emit NewToolTip(); } void SNIProxy::resizeWindow(const uint16_t width, const uint16_t height) const { auto connection = QX11Info::connection(); uint16_t widthNormalized = std::min(width, s_embedSize); uint16_t heighNormalized = std::min(height, s_embedSize); const uint32_t windowSizeConfigVals[2] = { widthNormalized, heighNormalized }; xcb_configure_window(connection, m_windowId, XCB_CONFIG_WINDOW_WIDTH | XCB_CONFIG_WINDOW_HEIGHT, windowSizeConfigVals); xcb_flush(connection); } void SNIProxy::hideContainerWindow(xcb_window_t windowId) const { if (m_containerWid == windowId && !sendingClickEvent) { qDebug() << "Container window visible, stack below"; stackContainerWindow(XCB_STACK_MODE_BELOW); } } QSize SNIProxy::calculateClientWindowSize() const { auto c = QX11Info::connection(); auto cookie = xcb_get_geometry(c, m_windowId); QScopedPointer clientGeom(xcb_get_geometry_reply(c, cookie, nullptr)); QSize clientWindowSize; if (clientGeom) { clientWindowSize = QSize(clientGeom->width, clientGeom->height); } //if the window is a clearly stupid size resize to be something sensible //this is needed as chromium and such when resized just fill the icon with transparent space and only draw in the middle //however KeePass2 does need this as by default the window size is 273px wide and is not transparent - //use an artbitrary heuristic to make sure icons are always sensible + //use an arbitrary heuristic to make sure icons are always sensible if (clientWindowSize.isEmpty() || clientWindowSize.width() > s_embedSize || clientWindowSize.height() > s_embedSize) { qCDebug(SNIPROXY) << "Resizing window" << m_windowId << Title() << "from w*h" << clientWindowSize; resizeWindow(s_embedSize, s_embedSize); clientWindowSize = QSize(s_embedSize, s_embedSize); } return clientWindowSize; } void sni_cleanup_xcb_image(void *data) { xcb_image_destroy(static_cast(data)); } bool SNIProxy::isTransparentImage(const QImage& image) const { int w = image.width(); int h = image.height(); // check for the center and sub-center pixels first and avoid full image scan if (! (qAlpha(image.pixel(w >> 1, h >> 1)) + qAlpha(image.pixel(w >> 2, h >> 2)) == 0)) return false; // skip scan altogether if sub-center pixel found to be opaque // and break out from the outer loop too on full scan for (int x = 0; x < w; ++x) { for (int y = 0; y < h; ++y) { if (qAlpha(image.pixel(x, y))) { // Found an opaque pixel. return false; } } } return true; } QImage SNIProxy::getImageNonComposite() const { auto c = QX11Info::connection(); QSize clientWindowSize = calculateClientWindowSize(); xcb_image_t *image = xcb_image_get(c, m_windowId, 0, 0, clientWindowSize.width(), clientWindowSize.height(), 0xFFFFFFFF, XCB_IMAGE_FORMAT_Z_PIXMAP); // Don't hook up cleanup yet, we may use a different QImage after all QImage naiveConversion; if (image) { naiveConversion = QImage(image->data, image->width, image->height, QImage::Format_ARGB32); } else { qCDebug(SNIPROXY) << "Skip NULL image returned from xcb_image_get() for" << m_windowId << Title(); return QImage(); } if (isTransparentImage(naiveConversion)) { QImage elaborateConversion = QImage(convertFromNative(image)); // Update icon only if it is at least partially opaque. // This is just a workaround for X11 bug: xembed icon may suddenly // become transparent for a one or few frames. Reproducible at least // with WINE applications. if (isTransparentImage(elaborateConversion)) { qCDebug(SNIPROXY) << "Skip transparent xembed icon for" << m_windowId << Title(); return QImage(); } else return elaborateConversion; } else { // Now we are sure we can eventually delete the xcb_image_t with this version return QImage(image->data, image->width, image->height, image->stride, QImage::Format_ARGB32, sni_cleanup_xcb_image, image); } } QImage SNIProxy::convertFromNative(xcb_image_t *xcbImage) const { QImage::Format format = QImage::Format_Invalid; switch (xcbImage->depth) { case 1: format = QImage::Format_MonoLSB; break; case 16: format = QImage::Format_RGB16; break; case 24: format = QImage::Format_RGB32; break; case 30: { // Qt doesn't have a matching image format. We need to convert manually quint32 *pixels = reinterpret_cast(xcbImage->data); for (uint i = 0; i < (xcbImage->size / 4); i++) { int r = (pixels[i] >> 22) & 0xff; int g = (pixels[i] >> 12) & 0xff; int b = (pixels[i] >> 2) & 0xff; pixels[i] = qRgba(r, g, b, 0xff); } // fall through, Qt format is still Format_ARGB32_Premultiplied Q_FALLTHROUGH(); } case 32: format = QImage::Format_ARGB32_Premultiplied; break; default: return QImage(); // we don't know } QImage image(xcbImage->data, xcbImage->width, xcbImage->height, xcbImage->stride, format, sni_cleanup_xcb_image, xcbImage); if (image.isNull()) { return QImage(); } if (format == QImage::Format_RGB32 && xcbImage->bpp == 32) { QImage m = image.createHeuristicMask(); QBitmap mask(QPixmap::fromImage(m)); QPixmap p = QPixmap::fromImage(image); p.setMask(mask); image = p.toImage(); } // work around an abort in QImage::color if (image.format() == QImage::Format_MonoLSB) { image.setColorCount(2); image.setColor(0, QColor(Qt::white).rgb()); image.setColor(1, QColor(Qt::black).rgb()); } return image; } /* Wine is using XWindow Shape Extension for transparent tray icons. We need to find first clickable point starting from top-left. */ QPoint SNIProxy::calculateClickPoint() const { QPoint clickPoint = QPoint(0, 0); auto c = QX11Info::connection(); // request extent to check if shape has been set xcb_shape_query_extents_cookie_t extentsCookie = xcb_shape_query_extents(c, m_windowId); // at the same time make the request for rectangles (even if this request isn't needed) xcb_shape_get_rectangles_cookie_t rectaglesCookie = xcb_shape_get_rectangles(c, m_windowId, XCB_SHAPE_SK_BOUNDING); QScopedPointer extentsReply(xcb_shape_query_extents_reply(c, extentsCookie, nullptr)); QScopedPointer rectanglesReply(xcb_shape_get_rectangles_reply(c, rectaglesCookie, nullptr)); if (!extentsReply || !rectanglesReply || !extentsReply->bounding_shaped) { return clickPoint; } xcb_rectangle_t *rectangles = xcb_shape_get_rectangles_rectangles(rectanglesReply.get()); if (!rectangles) { return clickPoint; } const QImage image = getImageNonComposite(); double minLength = sqrt(pow(image.height(), 2) + pow(image.width(), 2)); const int nRectangles = xcb_shape_get_rectangles_rectangles_length(rectanglesReply.get()); for (int i = 0; i < nRectangles; ++i) { double length = sqrt(pow(rectangles[i].x, 2) + pow(rectangles[i].y, 2)); if (length < minLength) { minLength = length; clickPoint = QPoint(rectangles[i].x, rectangles[i].y); } } qCDebug(SNIPROXY) << "Click point:" << clickPoint; return clickPoint; } void SNIProxy::stackContainerWindow(const uint32_t stackMode) const { auto c = QX11Info::connection(); const uint32_t stackData[] = {stackMode}; xcb_configure_window(c, m_containerWid, XCB_CONFIG_WINDOW_STACK_MODE, stackData); } //____________properties__________ QString SNIProxy::Category() const { return QStringLiteral("ApplicationStatus"); } QString SNIProxy::Id() const { const auto title = Title(); //we always need /some/ ID so if no window title exists, just use the winId. if (title.isEmpty()) { return QString::number(m_windowId); } return title; } KDbusImageVector SNIProxy::IconPixmap() const { KDbusImageStruct dbusImage(m_pixmap.toImage()); return KDbusImageVector() << dbusImage; } bool SNIProxy::ItemIsMenu() const { return false; } QString SNIProxy::Status() const { return QStringLiteral("Active"); } QString SNIProxy::Title() const { KWindowInfo window (m_windowId, NET::WMName); return window.name(); } int SNIProxy::WindowId() const { return m_windowId; } //____________actions_____________ void SNIProxy::Activate(int x, int y) { sendClick(XCB_BUTTON_INDEX_1, x, y); } void SNIProxy::SecondaryActivate(int x, int y) { sendClick(XCB_BUTTON_INDEX_2, x, y); } void SNIProxy::ContextMenu(int x, int y) { sendClick(XCB_BUTTON_INDEX_3, x, y); } void SNIProxy::Scroll(int delta, const QString& orientation) { if (orientation == QLatin1String("vertical")) { sendClick(delta > 0 ? XCB_BUTTON_INDEX_4: XCB_BUTTON_INDEX_5, 0, 0); } else { sendClick(delta > 0 ? 6: 7, 0, 0); } } void SNIProxy::sendClick(uint8_t mouseButton, int x, int y) { //it's best not to look at this code //GTK doesn't like send_events and double checks the mouse position matches where the window is and is top level //in order to solve this we move the embed container over to where the mouse is then replay the event using send_event //if patching, test with xchat + xchat context menus //note x,y are not actually where the mouse is, but the plasmoid //ideally we should make this match the plasmoid hit area qCDebug(SNIPROXY) << "Received click" << mouseButton << "with passed x*y" << x << y; sendingClickEvent = true; auto c = QX11Info::connection(); auto cookieSize = xcb_get_geometry(c, m_windowId); QScopedPointer clientGeom(xcb_get_geometry_reply(c, cookieSize, nullptr)); if (!clientGeom) { return; } auto cookie = xcb_query_pointer(c, m_windowId); QScopedPointer pointer(xcb_query_pointer_reply(c, cookie, nullptr)); /*qCDebug(SNIPROXY) << "samescreen" << pointer->same_screen << endl << "root x*y" << pointer->root_x << pointer->root_y << endl << "win x*y" << pointer->win_x << pointer->win_y;*/ //move our window so the mouse is within its geometry uint32_t configVals[2] = {0, 0}; const QPoint clickPoint = calculateClickPoint(); if (mouseButton >= XCB_BUTTON_INDEX_4) { //scroll event, take pointer position configVals[0] = pointer->root_x; configVals[1] = pointer->root_y; } else { if (pointer->root_x > x + clientGeom->width) configVals[0] = pointer->root_x - clientGeom->width + 1; else configVals[0] = static_cast(x - clickPoint.x()); if (pointer->root_y > y + clientGeom->height) configVals[1] = pointer->root_y - clientGeom->height + 1; else configVals[1] = static_cast(y - clickPoint.y()); } xcb_configure_window(c, m_containerWid, XCB_CONFIG_WINDOW_X | XCB_CONFIG_WINDOW_Y, configVals); //pull window up stackContainerWindow(XCB_STACK_MODE_ABOVE); //mouse down if (m_injectMode == Direct) { xcb_button_press_event_t* event = new xcb_button_press_event_t; memset(event, 0x00, sizeof(xcb_button_press_event_t)); event->response_type = XCB_BUTTON_PRESS; event->event = m_windowId; event->time = QX11Info::getTimestamp(); event->same_screen = 1; event->root = QX11Info::appRootWindow(); event->root_x = x; event->root_y = y; event->event_x = static_cast(clickPoint.x()); event->event_y = static_cast(clickPoint.y()); event->child = 0; event->state = 0; event->detail = mouseButton; xcb_send_event(c, false, m_windowId, XCB_EVENT_MASK_BUTTON_PRESS, (char *) event); delete event; } else { sendXTestPressed(QX11Info::display(), mouseButton); } //mouse up if (m_injectMode == Direct) { xcb_button_release_event_t* event = new xcb_button_release_event_t; memset(event, 0x00, sizeof(xcb_button_release_event_t)); event->response_type = XCB_BUTTON_RELEASE; event->event = m_windowId; event->time = QX11Info::getTimestamp(); event->same_screen = 1; event->root = QX11Info::appRootWindow(); event->root_x = x; event->root_y = y; event->event_x = static_cast(clickPoint.x()); event->event_y = static_cast(clickPoint.y()); event->child = 0; event->state = 0; event->detail = mouseButton; xcb_send_event(c, false, m_windowId, XCB_EVENT_MASK_BUTTON_RELEASE, (char *) event); delete event; } else { sendXTestReleased(QX11Info::display(), mouseButton); } #ifndef VISUAL_DEBUG stackContainerWindow(XCB_STACK_MODE_BELOW); #endif sendingClickEvent = false; }