diff --git a/AppMenu.cxx b/AppMenu.cxx index 546ebb0..86431d1 100644 --- a/AppMenu.cxx +++ b/AppMenu.cxx @@ -1,204 +1,189 @@ // SPDX-License-Identifier: GPL-3.0-or-later /* Copyright 2017 - 2020 Martin Koller, kollix@aon.at This file is part of liquidshell. liquidshell is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. liquidshell is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with liquidshell. If not, see . */ #include #include +#include #include #include #include #include #include #include #include -#include -#include #include #include #include #include //-------------------------------------------------------------------------------- AppMenu::AppMenu(DesktopPanel *parent) : Launcher(parent, "AppMenu") { button = new QToolButton; // QToolButton is smaller than QPushButton adjustIconSize(); button->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding); - popup = new Menu(button); connect(button, &QToolButton::pressed, this, &AppMenu::showMenu); layout()->addWidget(button); loadConfig(QStandardPaths::writableLocation(QStandardPaths::DesktopLocation)); connect(parent, &DesktopPanel::rowsChanged, this, &AppMenu::adjustIconSize); connect(KIconLoader::global(), &KIconLoader::iconLoaderSettingsChanged, this, &AppMenu::adjustIconSize); connect(KIconLoader::global(), &KIconLoader::iconLoaderSettingsChanged, this, &AppMenu::fill); - connect(QApplication::primaryScreen(), &QScreen::geometryChanged, this, &AppMenu::fill); - connect(qApp, &QApplication::primaryScreenChanged, this, &AppMenu::fill); } //-------------------------------------------------------------------------------- void AppMenu::adjustIconSize() { const int MAX_ROWS = qobject_cast(parentWidget())->getRows(); if ( MAX_ROWS > 1 ) button->setIconSize(QSize(48, 48)); else { int size = KIconLoader::global()->currentSize(KIconLoader::Panel); button->setIconSize(QSize(size, size)); } } //-------------------------------------------------------------------------------- void AppMenu::fill() { - if ( QApplication::primaryScreen()->availableSize().height() == QApplication::primaryScreen()->size().height() ) - { - // when the desktop panel size was not set yet, delay the creation - QTimer::singleShot(0, this, &AppMenu::fill); - return; - } - KDesktopFile desktopFile(dirPath + "/.directory"); if ( !desktopFile.readIcon().isEmpty() ) button->setIcon(QIcon::fromTheme(desktopFile.readIcon())); else if ( dirPath == QStandardPaths::writableLocation(QStandardPaths::DesktopLocation) ) button->setIcon(QIcon::fromTheme("user-desktop")); else // fallback button->setIcon(QIcon::fromTheme("folder")); +} - QLayoutItem *child; - while ( (child = popup->layout()->takeAt(0)) ) - { - delete child->widget(); - delete child; - } +//-------------------------------------------------------------------------------- + +void AppMenu::showMenu() +{ + Menu *popup = new Menu(button); QDir dir(dirPath); QFileInfoList entries = dir.entryInfoList(QDir::AllEntries | QDir::NoDotDot); int row = 0, col = 0, h = 0; - const int maxHeight = QApplication::primaryScreen()->availableSize().height(); + const int maxHeight = DesktopWidget::availableSize().height(); for (const QFileInfo &info : entries) { QUrl url(QUrl::fromLocalFile(info.absoluteFilePath())); QString name; QIcon icon; KFileItem item(url); if ( item.isDesktopFile() ) { KDesktopFile desktopFile(info.absoluteFilePath()); if ( desktopFile.noDisplay() ) continue; name = desktopFile.readName(); if ( name.isEmpty() ) name = desktopFile.readGenericName(); QString iconName = desktopFile.readIcon(); icon = QIcon::fromTheme(iconName.isEmpty() ? name : iconName); } else if ( info.isDir() ) { if ( info.fileName() == "." ) { name = info.dir().dirName(); icon = button->icon(); } else icon = QIcon::fromTheme("folder"); } else { QMimeDatabase db; icon = QIcon::fromTheme(db.mimeTypeForFile(info.absoluteFilePath()).iconName()); } if ( name.isEmpty() ) name = info.fileName(); IconButton *entryButton = new IconButton(this, icon, 32, name); - connect(entryButton, &IconButton::clicked, [this, url]() { popup->close(); new KRun(url, nullptr); }); + connect(entryButton, &IconButton::clicked, [this, url, popup]() { popup->close(); new KRun(url, nullptr); }); h += entryButton->sizeHint().height(); if ( h >= maxHeight ) { h = entryButton->sizeHint().height(); col++; row = 0; } static_cast(popup->layout())->addWidget(entryButton, row++, col); } -} -//-------------------------------------------------------------------------------- - -void AppMenu::showMenu() -{ popup->exec(); button->setDown(false); + delete popup; } //-------------------------------------------------------------------------------- //-------------------------------------------------------------------------------- //-------------------------------------------------------------------------------- Menu::Menu(QWidget *parent) : QMenu(parent) { QGridLayout *grid = new QGridLayout(this); grid->setContentsMargins(QMargins()); grid->setSpacing(0); } //-------------------------------------------------------------------------------- void Menu::hideEvent(QHideEvent *event) { Q_UNUSED(event); eventLoop.exit(); } //-------------------------------------------------------------------------------- void Menu::exec() { adjustSize(); QPoint p = parentWidget()->mapToGlobal(QPoint(0, 0)); move(p.x(), p.y() - height()); show(); eventLoop.exec(); } //-------------------------------------------------------------------------------- diff --git a/AppMenu.hxx b/AppMenu.hxx index 9fd30bf..c426962 100644 --- a/AppMenu.hxx +++ b/AppMenu.hxx @@ -1,71 +1,70 @@ // SPDX-License-Identifier: GPL-3.0-or-later /* - Copyright 2017 Martin Koller, kollix@aon.at + Copyright 2017 - 2020 Martin Koller, kollix@aon.at This file is part of liquidshell. liquidshell is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. liquidshell is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with liquidshell. If not, see . */ #ifndef _AppMenu_H_ #define _AppMenu_H_ #include #include #include #include #include #include //-------------------------------------------------------------------------------- class AppMenu : public Launcher { Q_OBJECT public: AppMenu(DesktopPanel *parent); private Q_SLOTS: void adjustIconSize(); void fill() override; void showMenu(); private: QToolButton *button; - class Menu *popup; }; //-------------------------------------------------------------------------------- //-------------------------------------------------------------------------------- class Menu : public QMenu { Q_OBJECT public: Menu(QWidget *parent); void exec(); protected: void hideEvent(QHideEvent *event) override; private: QEventLoop eventLoop; }; //-------------------------------------------------------------------------------- #endif diff --git a/ClockWidget.cxx b/ClockWidget.cxx index a1e5c27..250f3cd 100644 --- a/ClockWidget.cxx +++ b/ClockWidget.cxx @@ -1,248 +1,249 @@ // SPDX-License-Identifier: GPL-3.0-or-later /* - Copyright 2017 - 2019 Martin Koller, kollix@aon.at + Copyright 2017 - 2020 Martin Koller, kollix@aon.at This file is part of liquidshell. liquidshell is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. liquidshell is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with liquidshell. If not, see . */ #include #include +#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include //-------------------------------------------------------------------------------- CalendarPopup::CalendarPopup(QWidget *parent) : QFrame(parent) { setWindowFlags(windowFlags() | Qt::Popup); setFrameShape(QFrame::StyledPanel); QVBoxLayout *vbox = new QVBoxLayout(this); vbox->setContentsMargins(QMargins()); cal = new QCalendarWidget; vbox->addWidget(cal); QPushButton *today = new QPushButton(QIcon::fromTheme("go-jump-today"), QString()); vbox->addWidget(today); connect(today, &QPushButton::clicked, this, &CalendarPopup::goToday); } //-------------------------------------------------------------------------------- void CalendarPopup::goToday() { cal->showToday(); cal->setSelectedDate(QDate::currentDate()); } //-------------------------------------------------------------------------------- //-------------------------------------------------------------------------------- //-------------------------------------------------------------------------------- ClockWidget::ClockWidget(DesktopPanel *parent) : QFrame(parent), calendar(nullptr) { ensurePolished(); // make sure we already have the css applied timer = new QTimer(this); // if seconds are shown, update every second, else less often if ( timeFormat.contains('s') ) timer->setInterval(1000); else timer->setInterval(5000); timer->start(); connect(timer, &QTimer::timeout, this, &ClockWidget::tick); connect(parent, &DesktopPanel::rowsChanged, this, &ClockWidget::fill); timeLabel = new QLabel(this); dayLabel = new QLabel(this); dateLabel = new QLabel(this); timeLabel->setObjectName("time"); dayLabel->setObjectName("day"); dateLabel->setObjectName("date"); timeLabel->setTextFormat(Qt::PlainText); dayLabel->setTextFormat(Qt::PlainText); dateLabel->setTextFormat(Qt::PlainText); timeLabel->setAlignment(Qt::AlignCenter); dayLabel->setAlignment(Qt::AlignCenter); dateLabel->setAlignment(Qt::AlignCenter); QFont f = font(); f.setPointSizeF(fontInfo().pointSizeF() * 1.5); f.setBold(true); timeLabel->setFont(f); fill(); timeLabel->setVisible(!timeFormat.isEmpty()); dayLabel->setVisible(!dayFormat.isEmpty()); dateLabel->setVisible(!dateFormat.isEmpty()); // context menu QAction *action = new QAction(this); action->setIcon(QIcon::fromTheme("configure")); action->setText(i18n("Select Timezones...")); addAction(action); connect(action, &QAction::triggered, [this]() { ClockWidgetConfigureDialog dialog(parentWidget(), timeZoneIds); dialog.setWindowTitle(i18n("Select Timezones")); dialog.resize(600, 400); if ( dialog.exec() == QDialog::Accepted ) { timeZoneIds = dialog.getSelectedTimeZoneIds(); KConfig config; KConfigGroup group = config.group("ClockWidget"); QStringList list; for (const QByteArray &id : timeZoneIds) list.append(id); group.writeEntry("timeZoneIds", list); tick(); // update tooltip } } ); action = new QAction(this); action->setIcon(QIcon::fromTheme("preferences-system-time")); action->setText(i18n("Configure Date & Time...")); addAction(action); connect(action, &QAction::triggered, [this]() { auto dialog = new KCMultiDialog(parentWidget()); dialog->setAttribute(Qt::WA_DeleteOnClose); dialog->setWindowTitle(i18n("Date & Time")); dialog->addModule("clock"); dialog->adjustSize(); dialog->show(); } ); setContextMenuPolicy(Qt::ActionsContextMenu); // load config KConfig config; KConfigGroup group = config.group("ClockWidget"); QStringList list; list = group.readEntry("timeZoneIds", QStringList()); for (const QString &id : list) timeZoneIds.append(id.toLatin1()); tick(); timeLabel->setFixedHeight(timeLabel->fontMetrics().tightBoundingRect(timeLabel->text()).height() + 2); dayLabel->setFixedHeight(dayLabel->fontMetrics().tightBoundingRect(dayLabel->text()).height() + 4); dateLabel->setFixedHeight(dateLabel->fontMetrics().tightBoundingRect(dateLabel->text()).height() + 4); } //-------------------------------------------------------------------------------- void ClockWidget::fill() { delete layout(); const int MAX_ROWS = qobject_cast(parentWidget())->getRows(); QBoxLayout *box; if ( MAX_ROWS >= 2 ) { box = new QVBoxLayout(this); box->setSpacing(0); } else { box = new QHBoxLayout(this); } box->setContentsMargins(QMargins()); box->addWidget(timeLabel); box->addWidget(dayLabel); box->addWidget(dateLabel); } //-------------------------------------------------------------------------------- void ClockWidget::tick() { QDateTime dateTimeUtc = QDateTime::currentDateTimeUtc(); QDateTime dateTime = dateTimeUtc.toLocalTime(); timeLabel->setText(dateTime.time().toString(timeFormat)); dayLabel->setText(dateTime.date().toString(dayFormat)); dateLabel->setText(dateTime.date().toString(dateFormat)); if ( !timeZoneIds.isEmpty() ) { QString tip = ""; for (const QByteArray &id : timeZoneIds) { QTimeZone timeZone(id); QDateTime dt = dateTimeUtc; dateTime = dt.toTimeZone(timeZone); tip += QString("") .arg(QLatin1String(id)) .arg(dateTime.time().toString(timeFormat)) .arg(dateTime.date().toString(dayFormat)) .arg(dateTime.date().toString(dateFormat)); } tip += "
%1 %2 %3 %4
"; setToolTip(tip); } } //-------------------------------------------------------------------------------- void ClockWidget::mousePressEvent(QMouseEvent *event) { if ( event->button() == Qt::LeftButton ) { if ( !calendar ) calendar = new CalendarPopup(this); calendar->goToday(); QPoint point = mapToGlobal(pos()); - QRect screen = QApplication::primaryScreen()->availableGeometry(); + QRect screen = DesktopWidget::availableGeometry(); point.setX(std::min(point.x(), screen.x() + screen.width() - calendar->sizeHint().width())); point.setY(point.y() - calendar->sizeHint().height()); calendar->move(point); calendar->show(); } } //-------------------------------------------------------------------------------- diff --git a/DesktopWidget.cxx b/DesktopWidget.cxx index d5e8d97..4ec52de 100644 --- a/DesktopWidget.cxx +++ b/DesktopWidget.cxx @@ -1,399 +1,421 @@ // SPDX-License-Identifier: GPL-3.0-or-later /* - Copyright 2017 - 2019 Martin Koller, kollix@aon.at + Copyright 2017 - 2020 Martin Koller, kollix@aon.at This file is part of liquidshell. liquidshell is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. liquidshell is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with liquidshell. If not, see . */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include //-------------------------------------------------------------------------------- int DesktopWidget::appletNum = 0; +DesktopWidget *DesktopWidget::instance = nullptr; //-------------------------------------------------------------------------------- DesktopWidget::DesktopWidget() : QWidget(nullptr, Qt::WindowDoesNotAcceptFocus) { + instance = this; + setAttribute(Qt::WA_AlwaysShowToolTips); setAutoFillBackground(true); setFixedSize(QApplication::primaryScreen()->virtualSize()); setWindowIcon(QIcon::fromTheme("liquidshell")); KWindowSystem::setType(winId(), NET::Desktop); panel = new DesktopPanel(nullptr); panel->show(); connect(panel, &DesktopPanel::resized, this, &DesktopWidget::placePanel); placePanel(); connect(KWindowSystem::self(), &KWindowSystem::currentDesktopChanged, this, &DesktopWidget::desktopChanged); connect(KWindowSystem::self(), &KWindowSystem::numberOfDesktopsChanged, this, &DesktopWidget::loadSettings); setContextMenuPolicy(Qt::ActionsContextMenu); QAction *action = new QAction(QIcon::fromTheme("configure"), i18n("Configure Wallpaper..."), this); connect(action, &QAction::triggered, this, &DesktopWidget::configureWallpaper); addAction(action); action = new QAction(i18n("Add Applet"), this); addAction(action); QMenu *menu = new QMenu; action->setMenu(menu); action = menu->addAction(QIcon::fromTheme("weather-clouds"), i18n("Weather")); connect(action, &QAction::triggered, [this]() { addApplet("Weather"); }); action = menu->addAction(QIcon::fromTheme("drive-harddisk"), i18n("Disk Usage")); connect(action, &QAction::triggered, [this]() { addApplet("DiskUsage"); }); action = menu->addAction(QIcon::fromTheme("image-x-generix"), i18n("Picture Frame")); connect(action, &QAction::triggered, [this]() { addApplet("PictureFrame"); }); action = new QAction(QIcon::fromTheme("preferences-desktop-display"), i18n("Configure Display..."), this); connect(action, &QAction::triggered, this, &DesktopWidget::configureDisplay); addAction(action); action = new QAction(QIcon::fromTheme("system-run"), i18n("Run Command..."), this); connect(action, &QAction::triggered, []() { QDBusConnection::sessionBus().send( QDBusMessage::createMethodCall("org.kde.krunner", "/App", "org.kde.krunner.App", "display")); }); addAction(action); action = new QAction(i18n("Help"), this); KHelpMenu *helpMenu = new KHelpMenu(this); helpMenu->action(KHelpMenu::menuHelpContents)->setVisible(false); // no handbook helpMenu->action(KHelpMenu::menuWhatsThis)->setVisible(false); action->setMenu(helpMenu->menu()); addAction(action); // restore applets KConfig config; KConfigGroup group = config.group("DesktopApplets"); QStringList appletNames = group.readEntry("applets", QStringList()); for (const QString &appletName : appletNames) { DesktopApplet *applet = nullptr; if ( appletName.startsWith("Weather_") ) applet = new WeatherApplet(this, appletName); else if ( appletName.startsWith("DiskUsage_") ) applet = new DiskUsageApplet(this, appletName); else if ( appletName.startsWith("PictureFrame_") ) applet = new PictureFrameApplet(this, appletName); if ( applet ) { int num = appletName.mid(appletName.indexOf('_') + 1).toInt(); if ( num > appletNum ) appletNum = num; applet->loadConfig(); applets.append(applet); connect(applet, &DesktopApplet::removeThis, this, &DesktopWidget::removeApplet); } } loadSettings(); connect(qApp, &QApplication::primaryScreenChanged, this, &DesktopWidget::placePanel); connect(qApp, &QApplication::screenAdded, [this]() { setFixedSize(QApplication::primaryScreen()->virtualSize()); desktopChanged(); }); connect(qApp, &QApplication::screenRemoved, [this]() { setFixedSize(QApplication::primaryScreen()->virtualSize()); desktopChanged(); }); connect(QApplication::primaryScreen(), &QScreen::virtualGeometryChanged, [this]() { setFixedSize(QApplication::primaryScreen()->virtualSize()); placePanel(); desktopChanged(); }); new OnScreenVolume(this); } //-------------------------------------------------------------------------------- void DesktopWidget::loadSettings() { // simple check for default files matching current desktop size QStringList dirNames = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, "wallpapers", QStandardPaths::LocateDirectory); QStringList defaultFiles; const QString geometryString = QString("%1x%2").arg(width()).arg(height()); for (const QString &dirName : dirNames) { for (const QString &subdir : QDir(dirName).entryList(QDir::Dirs | QDir::NoDotAndDotDot | QDir::Readable)) { QDir dir(dirName + '/' + subdir + "/contents/images"); for (const QString &fileName : dir.entryList(QDir::Files | QDir::Readable)) { if ( fileName.startsWith(geometryString) ) defaultFiles.append(dir.absoluteFilePath(fileName)); } } } KConfig config; for (int i = wallpapers.count() + 1; i <= KWindowSystem::numberOfDesktops(); i++) { KConfigGroup group = config.group(QString("Desktop_%1").arg(i)); Wallpaper wallpaper; wallpaper.color = group.readEntry("color", QColor(Qt::black)); wallpaper.mode = group.readEntry("wallpaperMode", QString()); int idx = defaultFiles.count() ? ((i - 1) % defaultFiles.count()) : -1; wallpaper.fileName = group.readEntry("wallpaper", (idx != -1) ? defaultFiles[idx] : QString()); wallpapers.append(wallpaper); } wallpapers.resize(KWindowSystem::numberOfDesktops()); // if we had more before, reduce size wallpapers.squeeze(); desktopChanged(); } //-------------------------------------------------------------------------------- void DesktopWidget::configureWallpaper() { if ( dialog ) // already open, do nothing return; bool showingDesktop = KWindowSystem::showingDesktop(); KWindowSystem::setShowingDesktop(true); int desktopNum = currentDesktop; Wallpaper origWallpaper = wallpapers[desktopNum]; dialog = new ConfigureDesktopDialog(nullptr, wallpapers[desktopNum]); connect(dialog, &ConfigureDesktopDialog::changed, [=]() { wallpapers[desktopNum] = dialog->getWallpaper(); desktopChanged(); }); connect(dialog, &QDialog::finished, [=](int result) { if ( result == QDialog::Rejected ) { wallpapers[desktopNum] = origWallpaper; desktopChanged(); } else // store in config file { const Wallpaper &wallpaper = wallpapers[desktopNum]; KConfig config; KConfigGroup group = config.group(QString("Desktop_%1").arg(desktopNum + 1)); group.writeEntry("color", wallpaper.color); group.writeEntry("wallpaper", wallpaper.fileName); group.writeEntry("wallpaperMode", wallpaper.mode); } KWindowSystem::setShowingDesktop(showingDesktop); // restore dialog->deleteLater(); dialog = nullptr; }); dialog->show(); // not modal } //-------------------------------------------------------------------------------- void DesktopWidget::configureDisplay() { KCMultiDialog *dialog = new KCMultiDialog(this); dialog->setAttribute(Qt::WA_DeleteOnClose); dialog->addModule("kcm_kscreen"); dialog->adjustSize(); dialog->setWindowTitle(i18n("Configure Display")); dialog->show(); // not modal } //-------------------------------------------------------------------------------- void DesktopWidget::placePanel() { int panelHeight = qMin(panel->sizeHint().height(), panel->height()); QRect r = QApplication::primaryScreen()->geometry(); + QSize vs = QApplication::primaryScreen()->virtualSize(); panel->setGeometry(r.x(), r.y() + r.height() - panelHeight, r.width(), panelHeight); - KWindowSystem::setStrut(panel->winId(), 0, 0, 0, panelHeight); + + // struts are to the combined screen geoms, not the single screen + KWindowSystem::setStrut(panel->winId(), 0, 0, 0, panelHeight + vs.height() - r.bottom()); +} + +//-------------------------------------------------------------------------------- + +QRect DesktopWidget::availableGeometry() +{ + QRect geom = QApplication::primaryScreen()->geometry(); + geom.setHeight(geom.height() - instance->panel->height()); + return geom; +} + +//-------------------------------------------------------------------------------- + +QSize DesktopWidget::availableSize() +{ + return availableGeometry().size(); } //-------------------------------------------------------------------------------- void DesktopWidget::desktopChanged() { // free memory in previous shown desktop if ( (currentDesktop >= 0) && (currentDesktop < wallpapers.count()) ) wallpapers[currentDesktop].pixmaps.clear(); currentDesktop = KWindowSystem::currentDesktop() - 1; // num to index if ( currentDesktop >= wallpapers.count() ) return; Wallpaper &wallpaper = wallpapers[currentDesktop]; if ( wallpaper.color.isValid() ) { QPalette pal = palette(); pal.setColor(backgroundRole(), wallpaper.color); setPalette(pal); } if ( !wallpaper.fileName.isEmpty() ) { QPixmap pixmap(wallpaper.fileName); if ( !pixmap.isNull() ) { for (int i = 0; i < QApplication::screens().count(); i++) { QRect r = QApplication::screens().at(i)->geometry(); QPixmap scaledPixmap = pixmap; if ( wallpaper.mode == "Scaled" ) scaledPixmap = pixmap.scaled(r.size(), Qt::IgnoreAspectRatio, Qt::SmoothTransformation); else if ( wallpaper.mode == "ScaledKeepRatio" ) scaledPixmap = pixmap.scaled(r.size(), Qt::KeepAspectRatio, Qt::SmoothTransformation); else if ( wallpaper.mode == "ScaledKeepRatioExpand" ) scaledPixmap = pixmap.scaled(r.size(), Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation); wallpaper.pixmaps.append(scaledPixmap); } } } update(); // show applets for new desktop for (DesktopApplet *applet : applets) { if ( !applet->isConfiguring() ) applet->setVisible(applet->isOnDesktop(currentDesktop + 1)); } } //-------------------------------------------------------------------------------- void DesktopWidget::paintEvent(QPaintEvent *event) { Q_UNUSED(event) if ( currentDesktop < wallpapers.count() ) { Wallpaper &wallpaper = wallpapers[currentDesktop]; QPainter painter(this); for (int i = 0; i < QApplication::screens().count(); i++) { if ( i < wallpaper.pixmaps.count() ) { QRect r = QApplication::screens().at(i)->geometry(); painter.setClipRect(r); painter.drawPixmap(r.x() + (r.width() - wallpaper.pixmaps[i].width()) / 2, r.y() + (r.height() - wallpaper.pixmaps[i].height()) / 2, wallpaper.pixmaps[i]); } } } } //-------------------------------------------------------------------------------- void DesktopWidget::addApplet(const QString &type) { DesktopApplet *applet = nullptr; if ( type == "Weather" ) { applet = new WeatherApplet(this, QString("%1_%2").arg(type).arg(++appletNum)); } else if ( type == "DiskUsage" ) { applet = new DiskUsageApplet(this, QString("%1_%2").arg(type).arg(++appletNum)); } else if ( type == "PictureFrame" ) { applet = new PictureFrameApplet(this, QString("%1_%2").arg(type).arg(++appletNum)); } if ( applet ) { applets.append(applet); applet->loadConfig(); // defaults + size applet->move(QCursor::pos()); applet->saveConfig(); applet->show(); connect(applet, &DesktopApplet::removeThis, this, &DesktopWidget::removeApplet); } saveAppletsList(); } //-------------------------------------------------------------------------------- void DesktopWidget::removeApplet(DesktopApplet *applet) { applets.removeOne(applet); applet->deleteLater(); saveAppletsList(); } //-------------------------------------------------------------------------------- void DesktopWidget::saveAppletsList() { KConfig config; KConfigGroup group = config.group("DesktopApplets"); QStringList appletNames; for (DesktopApplet *applet : applets) appletNames.append(applet->getId()); group.writeEntry("applets", appletNames); } //-------------------------------------------------------------------------------- diff --git a/DesktopWidget.hxx b/DesktopWidget.hxx index 8587dc4..b708faa 100644 --- a/DesktopWidget.hxx +++ b/DesktopWidget.hxx @@ -1,71 +1,77 @@ // SPDX-License-Identifier: GPL-3.0-or-later /* - Copyright 2017 Martin Koller, kollix@aon.at + Copyright 2017 - 2020 Martin Koller, kollix@aon.at This file is part of liquidshell. liquidshell is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. liquidshell is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with liquidshell. If not, see . */ #ifndef DESKTOP_WIDGET_H #define DESKTOP_WIDGET_H #include #include #include class DesktopPanel; class DesktopApplet; class ConfigureDesktopDialog; class DesktopWidget : public QWidget { Q_OBJECT public: DesktopWidget(); struct Wallpaper { QString fileName, mode; QColor color; QVector pixmaps; // per screen }; + // since on X11 QScreen:available* methods do not deliver the true + // values (according to Qt doc: This is a limitation in X11 window manager specification.) + static QRect availableGeometry(); + static QSize availableSize(); + protected: void paintEvent(QPaintEvent *event) override; private Q_SLOTS: void loadSettings(); void placePanel(); void desktopChanged(); void configureWallpaper(); void configureDisplay(); void addApplet(const QString &type); void removeApplet(DesktopApplet *applet); private: void saveAppletsList(); private: DesktopPanel *panel = nullptr; ConfigureDesktopDialog *dialog = nullptr; QVector wallpapers; int currentDesktop = -1; QVector applets; static int appletNum; + static DesktopWidget *instance; }; #endif diff --git a/NetworkList.cxx b/NetworkList.cxx index e9ea86b..34f293f 100644 --- a/NetworkList.cxx +++ b/NetworkList.cxx @@ -1,427 +1,430 @@ // SPDX-License-Identifier: GPL-3.0-or-later /* Copyright 2017 - 2020 Martin Koller, kollix@aon.at This file is part of liquidshell. liquidshell is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. liquidshell is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with liquidshell. If not, see . */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include //-------------------------------------------------------------------------------- NetworkButton::NetworkButton(NetworkManager::Connection::Ptr c, NetworkManager::Device::Ptr dev, NetworkManager::AccessPoint::Ptr accessPoint) : connection(c), device(dev) { setCheckable(true); if ( connection ) { for (const NetworkManager::ActiveConnection::Ptr &ac : NetworkManager::activeConnections()) { if ( ac->uuid() == c->uuid() ) { setChecked(true); break; } } } else if ( accessPoint ) { ssid = accessPoint->ssid(); rawSsid = accessPoint->rawSsid(); wpaFlags = accessPoint->wpaFlags(); } connect(this, &NetworkButton::toggled, this, &NetworkButton::toggleNetworkStatus); } //-------------------------------------------------------------------------------- void NetworkButton::toggleNetworkStatus(bool on) { if ( on ) { if ( !connection ) // no connection yet -> create one { // the connMap content was "reverse-engineered" by using qdbusviewer and the result of getting // GetSettings of one of theSettings.Connection elements NMVariantMapMap connMap; QVariantMap map; map.insert("id", ssid); // ensure to not need root password by creating only for the current user struct passwd *pwd = getpwuid(geteuid()); if ( pwd ) map.insert("permissions", QStringList(QString("user:") + QString::fromUtf8(pwd->pw_name))); connMap.insert("connection", map); QVariantMap wirelessMap; wirelessMap.insert("ssid", rawSsid); if ( wpaFlags ) { wirelessMap.insert("security", "802-11-wireless-security"); QVariantMap security; if ( wpaFlags & NetworkManager::AccessPoint::KeyMgmtPsk ) security.insert("key-mgmt", QString("wpa-psk")); #if (NETWORKMANAGERQT_VERSION >= QT_VERSION_CHECK(5, 63, 0)) else if ( wpaFlags & NetworkManager::AccessPoint::KeyMgmtSAE ) security.insert("key-mgmt", QString("sae")); #endif else { // TODO: other types - find value names } connMap.insert("802-11-wireless-security", security); } connMap.insert("802-11-wireless", wirelessMap); QDBusPendingReply call = NetworkManager::addAndActivateConnection(connMap, device->uni(), QString()); /* QDBusPendingCallWatcher *pendingCallWatcher = new QDBusPendingCallWatcher(call, this); connect(pendingCallWatcher, &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *w) { w->deleteLater(); QDBusPendingReply reply = *w; qDebug() << reply.error(); } ); */ // without closing our popup, the user can not enter the password in the password dialog which appears window()->close(); return; } switch ( connection->settings()->connectionType() ) { case NetworkManager::ConnectionSettings::Wired: { NetworkManager::activateConnection(connection->path(), connection->settings()->interfaceName(), QString()); break; } case NetworkManager::ConnectionSettings::Wireless: { NetworkManager::activateConnection(connection->path(), device->uni(), QString()); break; } case NetworkManager::ConnectionSettings::Vpn: { NetworkManager::ActiveConnection::Ptr conn(NetworkManager::primaryConnection()); if ( conn && !conn->devices().isEmpty() ) NetworkManager::activateConnection(connection->path(), conn->devices()[0], QString()); break; } default: ; // TODO } } else if ( connection ) { for (const NetworkManager::ActiveConnection::Ptr &ac : NetworkManager::activeConnections()) { if ( ac->uuid() == connection->uuid() ) { NetworkManager::deactivateConnection(ac->path()); break; } } } } //-------------------------------------------------------------------------------- //-------------------------------------------------------------------------------- //-------------------------------------------------------------------------------- NetworkList::NetworkList(QWidget *parent) : QFrame(parent) { setWindowFlags(windowFlags() | Qt::Popup); setFrameShape(QFrame::StyledPanel); QVBoxLayout *vbox = new QVBoxLayout(this); hbox = new QHBoxLayout; vbox->addLayout(hbox); network = new QToolButton; network->setIcon(QIcon::fromTheme("network-wired")); + network->setIconSize(QSize(22, 22)); network->setToolTip(i18n("Enable Networking")); network->setCheckable(true); connect(network, &QToolButton::clicked, [](bool on) { NetworkManager::setNetworkingEnabled(on); }); connect(NetworkManager::notifier(), &NetworkManager::Notifier::networkingEnabledChanged, this, &NetworkList::statusUpdate); hbox->addWidget(network); wireless = new QToolButton; wireless->setIcon(QIcon::fromTheme("network-wireless")); + wireless->setIconSize(QSize(22, 22)); wireless->setToolTip(i18n("Enable Wireless Networking")); wireless->setCheckable(true); connect(wireless, &QToolButton::clicked, [](bool on) { NetworkManager::setWirelessEnabled(on); }); connect(NetworkManager::notifier(), &NetworkManager::Notifier::wirelessEnabledChanged, this, &NetworkList::statusUpdate); hbox->addWidget(wireless); statusUpdate(); hbox->addStretch(); QToolButton *configure = new QToolButton; configure->setIcon(QIcon::fromTheme("configure")); + configure->setIconSize(QSize(22, 22)); configure->setToolTip(i18n("Configure Network Connections")); connect(configure, &QToolButton::clicked, this, &NetworkList::openConfigureDialog); hbox->addWidget(configure); // show connections QWidget *widget = new QWidget; connectionsVbox = new QVBoxLayout(widget); connectionsVbox->setContentsMargins(QMargins()); connectionsVbox->setSizeConstraint(QLayout::SetMinAndMaxSize); scroll = new QScrollArea; scroll->setWidgetResizable(true); scroll->setWidget(widget); vbox->addWidget(scroll); fillConnections(); QTimer *checkConnectionsTimer = new QTimer(this); checkConnectionsTimer->setInterval(1000); connect(checkConnectionsTimer, &QTimer::timeout, this, &NetworkList::fillConnections); checkConnectionsTimer->start(); } //-------------------------------------------------------------------------------- void NetworkList::openConfigureDialog() { // newer plasma has already a KCM KService::Ptr service = KService::serviceByDesktopName("kcm_networkmanagement"); if ( service ) KRun::runApplication(*service, QList(), this); else KRun::run("kde5-nm-connection-editor", QList(), this); close(); } //-------------------------------------------------------------------------------- void NetworkList::statusUpdate() { network->setChecked(NetworkManager::isNetworkingEnabled()); wireless->setChecked(NetworkManager::isWirelessEnabled()); } //-------------------------------------------------------------------------------- void NetworkList::fillConnections() { if ( underMouse() ) // avoid to delete the widget we are possibly hovering over return; QLayoutItem *child; while ( (child = connectionsVbox->takeAt(0)) ) { delete child->widget(); delete child; } NetworkManager::Connection::List allConnections = NetworkManager::listConnections(); // show VPN networks on top for (const NetworkManager::Connection::Ptr c : allConnections) { if ( !c->isValid() ) continue; if ( c->settings()->connectionType() == NetworkManager::ConnectionSettings::Vpn ) { NetworkButton *vpn = new NetworkButton(c); vpn->setText(c->name()); vpn->setIcon(QIcon::fromTheme("security-high")); connectionsVbox->addWidget(vpn); vpn->show(); } } // wired networks for (const NetworkManager::Connection::Ptr c : allConnections) { if ( !c->isValid() ) continue; if ( (c->settings()->connectionType() == NetworkManager::ConnectionSettings::Wired) && !c->uuid().isEmpty() ) { NetworkButton *net = new NetworkButton(c); net->setText(c->name()); net->setIcon(QIcon::fromTheme("network-wired")); connectionsVbox->addWidget(net); net->show(); } } // show available wifi networks if ( NetworkManager::isWirelessEnabled() ) { for (const NetworkManager::Device::Ptr &device : NetworkManager::networkInterfaces()) { if ( device->type() != NetworkManager::Device::Wifi ) continue; NetworkManager::WirelessDevice::Ptr wifiDevice = device.objectCast(); for (const NetworkManager::WirelessNetwork::Ptr &network : wifiDevice->networks()) { NetworkManager::AccessPoint::Ptr accessPoint = network->referenceAccessPoint(); if ( !accessPoint ) continue; // check if we have a connection for this SSID NetworkManager::Connection::Ptr conn; for (const NetworkManager::Connection::Ptr c : allConnections) { if ( c->isValid() && (c->settings()->connectionType() == NetworkManager::ConnectionSettings::Wireless) ) { NetworkManager::Setting::Ptr setting = c->settings()->setting(NetworkManager::Setting::Wireless); NetworkManager::WirelessSetting::Ptr s = setting.staticCast(); if ( s->ssid() == network->ssid() ) { conn = c; break; } } } NetworkButton *net; if ( conn ) { net = new NetworkButton(conn, device); net->setText(QString("%1 (%2%)").arg(conn->name()).arg(network->signalStrength())); } else { net = new NetworkButton(conn, device, accessPoint); net->setText(QString("%1 (%2%)").arg(network->ssid()).arg(network->signalStrength())); } net->setIcon(QIcon::fromTheme("network-wireless")); if ( accessPoint->wpaFlags() ) net->setIcon2(QIcon::fromTheme("object-locked")); else net->setIcon2(QIcon::fromTheme("object-unlocked")); connectionsVbox->addWidget(net); net->show(); } } } #if 0 // TEST static int count = 15; for (int i = 0; i < count; i++) { NetworkButton *net = new NetworkButton(); net->setText(QString("dummy %1").arg(i)); net->setIcon(QIcon::fromTheme("network-wired")); connectionsVbox->addWidget(net); net->show(); } count -= 3; if ( count <= 0 ) count = 15; #endif connectionsVbox->addStretch(); adjustSize(); emit changed(); } //-------------------------------------------------------------------------------- QSize NetworkList::sizeHint() const { QWidget *w = scroll->widget(); QSize s; s.setHeight(frameWidth() + contentsMargins().top() + layout()->contentsMargins().top() + hbox->sizeHint().height() + ((layout()->spacing() == -1) ? style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing) : layout()->spacing()) + scroll->frameWidth() + scroll->contentsMargins().top() + w->sizeHint().height() + scroll->contentsMargins().bottom() + scroll->frameWidth() + layout()->contentsMargins().bottom() + contentsMargins().bottom() + frameWidth() ); s.setWidth(frameWidth() + contentsMargins().left() + layout()->contentsMargins().left() + scroll->frameWidth() + scroll->contentsMargins().left() + w->sizeHint().width() + scroll->verticalScrollBar()->sizeHint().width() + scroll->contentsMargins().right() + scroll->frameWidth() + layout()->contentsMargins().right() + contentsMargins().right() + frameWidth() ); return s; } //-------------------------------------------------------------------------------- diff --git a/NotificationList.cxx b/NotificationList.cxx index 828660b..15526d4 100644 --- a/NotificationList.cxx +++ b/NotificationList.cxx @@ -1,332 +1,333 @@ // SPDX-License-Identifier: GPL-3.0-or-later /* - Copyright 2017 - 2019 Martin Koller, kollix@aon.at + Copyright 2017 - 2020 Martin Koller, kollix@aon.at This file is part of liquidshell. liquidshell is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. liquidshell is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with liquidshell. If not, see . */ #include +#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static const Qt::WindowFlags POPUP_FLAGS = Qt::WindowStaysOnTopHint | Qt::FramelessWindowHint; //-------------------------------------------------------------------------------- NotifyItem::NotifyItem(QWidget *parent, uint theId, const QString &app, const QString &theSummary, const QString &theBody, const QIcon &icon, const QStringList &theActions) : QFrame(parent, POPUP_FLAGS), id(theId), appName(app), summary(theSummary), body(theBody), actions(theActions) { setAttribute(Qt::WA_ShowWithoutActivating); // avoid focus stealing setFrameShape(QFrame::StyledPanel); QMargins margins = contentsMargins(); margins.setRight(0); // allow the close button to reach to the right screen border - easier to click setContentsMargins(margins); QVBoxLayout *vbox = new QVBoxLayout; timeLabel = new QLabel; iconLabel = new QLabel; vbox->addWidget(timeLabel, 0, Qt::AlignTop | Qt::AlignHCenter); vbox->addWidget(iconLabel, 0, Qt::AlignTop | Qt::AlignHCenter); QVBoxLayout *centerBox = new QVBoxLayout; QHBoxLayout *hbox = new QHBoxLayout(this); margins = hbox->contentsMargins(); margins.setRight(0); // allow the close button to reach to the right screen border - easier to click hbox->setContentsMargins(margins); textLabel = new QLabel; QToolButton *closeButton = new QToolButton; // easier to click with larger size closeButton->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); closeButton->setMinimumWidth(40); closeButton->setAutoRaise(true); closeButton->setIcon(QIcon::fromTheme("window-close")); connect(closeButton, &QToolButton::clicked, this, &NotifyItem::deleteLater); textLabel->setWordWrap(true); textLabel->setTextInteractionFlags(Qt::TextBrowserInteraction); textLabel->setOpenExternalLinks(true); textLabel->setMinimumWidth(300); textLabel->setMaximumWidth(600); textLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); centerBox->addWidget(textLabel); if ( actions.count() ) { QHBoxLayout *actionBox = new QHBoxLayout; centerBox->addLayout(actionBox); for (int i = 0; i < actions.count(); i++) { if ( ((i % 2) != 0) && !actions[i].isEmpty() ) // id { QPushButton *button = new QPushButton; button->setText(actions[i]); actionBox->addWidget(button); QString key = actions[i - 1]; connect(button, &QPushButton::clicked, this, [this, key]() { QDBusMessage msg = QDBusMessage::createSignal("/org/freedesktop/Notifications", "org.freedesktop.Notifications", "ActionInvoked"); msg << id << key; QDBusConnection::sessionBus().send(msg); }); } } } hbox->addLayout(vbox); hbox->addLayout(centerBox); hbox->addWidget(closeButton); iconLabel->setFixedSize(32, 32); iconLabel->setPixmap(icon.pixmap(iconLabel->size())); QString title = (appName == summary) ? appName : (appName + ": " + summary); textLabel->setText(QString("
%1

%2") .arg(title) .arg(body)); timeLabel->setText(QTime::currentTime().toString(Qt::SystemLocaleShortDate)); } //-------------------------------------------------------------------------------- void NotifyItem::destroySysResources() { destroy(); } //-------------------------------------------------------------------------------- //-------------------------------------------------------------------------------- //-------------------------------------------------------------------------------- NotificationList::NotificationList(QWidget *parent) : QWidget(parent) { setWindowFlags(windowFlags() | Qt::Tool); setWindowTitle(i18n("Notifications")); scrollArea = new QScrollArea; scrollArea->setWidgetResizable(true); QWidget *listWidget = new QWidget; listVbox = new QVBoxLayout(listWidget); listVbox->setContentsMargins(QMargins()); listVbox->addStretch(); scrollArea->setWidget(listWidget); QVBoxLayout *vbox = new QVBoxLayout(this); vbox->setContentsMargins(QMargins()); vbox->addWidget(scrollArea); QPushButton *clearButton = new QPushButton; clearButton->setIcon(QIcon::fromTheme("edit-clear-list")); connect(clearButton, &QPushButton::clicked, this, [this]() { for (NotifyItem *item : items) item->deleteLater(); }); vbox->addWidget(clearButton); resize(500, 300); } //-------------------------------------------------------------------------------- NotificationList::~NotificationList() { for (NotifyItem *item : items) item->disconnect(); // make sure destroyed() is no longer triggered } //-------------------------------------------------------------------------------- void NotificationList::addItem(uint id, const QString &appName, const QString &summary, const QString &body, const QIcon &icon, const QStringList &actions, const QVariantMap &hints, int timeout) { QPointer item = new NotifyItem(nullptr, id, appName, summary, body, icon, actions); item->resize(500, item->sizeHint().height()); KWindowSystem::setState(item->winId(), NET::SkipTaskbar | NET::SkipPager); items.append(item.data()); placeItems(); int wordCount = body.splitRef(' ', QString::SkipEmptyParts).count(); bool neverExpires = timeout == 0; // according to spec if ( timeout <= 0 ) { timeout = 4000 + 250 * wordCount; if ( actions.count() ) timeout += 15000; // give user more time to think ... } // 0=low, 1=normal, 2=critical if ( hints.contains("urgency") && (hints["urgency"].toInt() == 2) ) { neverExpires = true; timeout = 20000; // just show it longer since its urgent } bool transient = hints.contains("transient") ? hints["transient"].toBool() : false; // if there are actions, show it longer if ( actions.count() || neverExpires ) transient = false; // exceptions ... if ( transient && hints.contains("x-kde-eventId") ) { // I'd like to keep this much longer if ( hints["x-kde-eventId"].toString() == "new-email" ) transient = false; } // I often get the same notification multiple times (e.g. KDE-connect or EWS akonadi resource) // but I don't want to fill up the screen with useless duplicate information. Therefore // whenever the same notification is received while another instance of it is still visible, // only put it in the list-view (it has a different id, therefore still store it), but don't show it as popup for (NotifyItem *it : items) { if ( (it != item) && // not the new item already in items !it->parentWidget() && // temporary item not in the list-view yet (appName == it->appName) && (summary == it->summary) && (body == it->body) && (actions == it->actions) ) { timeout = 0; } } numItems++; emit itemsCountChanged(); connect(item.data(), &NotifyItem::destroyed, this, [this](QObject *obj) { items.removeOne(static_cast(obj)); placeItems(); if ( --numItems == 0 ) { hide(); emit listNowEmpty(); } else emit itemsCountChanged(); } ); QTimer::singleShot(timeout, [=]() { if ( item ) { // sometimes there were some leftover/half-functioning windows. This seems to avoid that item->destroySysResources(); listVbox->insertWidget(listVbox->count() - 1, item); // insert before stretch item->show(); placeItems(); // reorder the remaining ones scrollArea->setWidgetResizable(true); // to update scrollbars, else next line does not work scrollArea->ensureWidgetVisible(item); if ( !neverExpires ) { if ( !appTimeouts.contains(appName) ) appTimeouts.insert(appName, 10); // default: 10 minutes lifetime int expireTimeout; if ( transient ) expireTimeout = 2 * 60 * 1000; // still keep it a short time else expireTimeout = appTimeouts[appName] * 60 * 1000; QTimer::singleShot(expireTimeout, item.data(), &NotifyItem::deleteLater); } } } ); } //-------------------------------------------------------------------------------- void NotificationList::closeItem(uint id) { for (NotifyItem *item : items) { if ( item->id == id ) { item->deleteLater(); break; } } } //-------------------------------------------------------------------------------- void NotificationList::placeItems() { - QRect screen = QApplication::primaryScreen()->availableGeometry(); + QRect screen = DesktopWidget::availableGeometry(); QPoint point = parentWidget()->mapToGlobal(parentWidget()->pos()); int x = point.x(); int y = screen.bottom(); for (NotifyItem *item : items) { if ( !item->parentWidget() ) // temporary item not in the list yet { y -= item->sizeHint().height(); y -= 5; // a small space point.setX(std::min(x, screen.x() + screen.width() - item->sizeHint().width())); point.setY(y); item->move(point); item->show(); } } } //-------------------------------------------------------------------------------- diff --git a/PkUpdateList.cxx b/PkUpdateList.cxx index c202261..85311dc 100644 --- a/PkUpdateList.cxx +++ b/PkUpdateList.cxx @@ -1,656 +1,657 @@ // SPDX-License-Identifier: GPL-3.0-or-later /* - Copyright 2017 - 2019 Martin Koller, kollix@aon.at + Copyright 2017 - 2020 Martin Koller, kollix@aon.at This file is part of liquidshell. liquidshell is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. liquidshell is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with liquidshell. If not, see . */ #include +#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include //#define TEST_LOGOUT //#define TEST_REBOOT //-------------------------------------------------------------------------------- PkUpdateListItem::PkUpdateListItem(QWidget *parent, PackageKit::Transaction::Info info, const PkUpdates::PackageData &data) : QWidget(parent), package(data) { setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); QGridLayout *grid = new QGridLayout(this); grid->setContentsMargins(QMargins()); grid->setSpacing(0); checkBox = new QCheckBox; checkBox->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); grid->addWidget(checkBox, 0, 0); QString icon; switch ( info ) { case PackageKit::Transaction::InfoSecurity: icon = "update-high"; break; case PackageKit::Transaction::InfoImportant: icon = "update-medium"; break; case PackageKit::Transaction::InfoBugfix: icon = "update-low"; break; default: ; } QLabel *iconLabel = new QLabel; iconLabel->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); iconLabel->setPixmap(QIcon::fromTheme(icon).pixmap(16)); grid->addWidget(iconLabel, 0, 1); checkBox->setChecked(!icon.isEmpty()); // auto-check the important ones connect(checkBox, &QCheckBox::toggled, this, &PkUpdateListItem::toggled); label = new QToolButton; label->setAutoRaise(true); label->setText(package.summary + " [" + PackageKit::Daemon::packageName(package.id) + ", " + PackageKit::Daemon::packageVersion(package.id) + ']'); grid->addWidget(label, 0, 2, Qt::AlignLeft); detailsLabel = new QLabel; detailsLabel->setTextInteractionFlags(Qt::TextBrowserInteraction); detailsLabel->setIndent(30); detailsLabel->setAlignment(Qt::AlignLeft); grid->addWidget(detailsLabel, 1, 2); detailsLabel->hide(); connect(label, &QToolButton::clicked, this, &PkUpdateListItem::getUpdateDetails); { QHBoxLayout *progressHbox = new QHBoxLayout; progressHbox->setSpacing(10); progress = new QProgressBar; progress->setFixedWidth(300); progressHbox->addWidget(progress); errorLabel = new QLabel; errorLabel->setTextInteractionFlags(Qt::TextBrowserInteraction); errorLabel->hide(); progressHbox->addWidget(errorLabel); packageLabel = new QLabel; packageLabel->setTextInteractionFlags(Qt::TextBrowserInteraction); packageLabel->hide(); progressHbox->addWidget(packageLabel); grid->addLayout(progressHbox, 2, 2, Qt::AlignLeft); showProgress(false); } } //-------------------------------------------------------------------------------- void PkUpdateListItem::showProgress(bool yes) { progress->setValue(0); progress->setVisible(yes); } //-------------------------------------------------------------------------------- void PkUpdateListItem::getUpdateDetails() { if ( !detailsLabel->isHidden() ) { detailsLabel->hide(); return; } //qDebug() << "getUpdateDetails" << package.id; PackageKit::Transaction *transaction = PackageKit::Daemon::getUpdateDetail(package.id); detailsLabel->setText(i18n("Getting details ...")); detailsLabel->show(); connect(transaction, &PackageKit::Transaction::updateDetail, this, &PkUpdateListItem::updateDetail); connect(transaction, &PackageKit::Transaction::errorCode, this, [this](PackageKit::Transaction::Error error, const QString &details) { Q_UNUSED(error) detailsLabel->setText(details); }); } //-------------------------------------------------------------------------------- void PkUpdateListItem::updateDetail(const QString &packageID, const QStringList &updates, const QStringList &obsoletes, const QStringList &vendorUrls, const QStringList &bugzillaUrls, const QStringList &cveUrls, PackageKit::Transaction::Restart restart, const QString &updateText, const QString &changelog, PackageKit::Transaction::UpdateState state, const QDateTime &issued, const QDateTime &updated) { Q_UNUSED(packageID) Q_UNUSED(updates) Q_UNUSED(obsoletes) Q_UNUSED(vendorUrls) Q_UNUSED(bugzillaUrls) Q_UNUSED(cveUrls) Q_UNUSED(changelog) Q_UNUSED(state) Q_UNUSED(issued) Q_UNUSED(updated) QString text; if ( restart == PackageKit::Transaction::RestartSession ) text = i18n("Session restart required
"); else if ( restart == PackageKit::Transaction::RestartSystem ) text = i18n("Reboot required
"); text += updateText.trimmed(); text.replace("\n", "
"); detailsLabel->setText(text); } //-------------------------------------------------------------------------------- //-------------------------------------------------------------------------------- //-------------------------------------------------------------------------------- PkUpdateList::PkUpdateList(QWidget *parent) : QWidget(parent), restart(PackageKit::Transaction::RestartNone) { setWindowFlags(windowFlags() | Qt::Tool); setWindowTitle(i18n("Software Updates")); vbox = new QVBoxLayout(this); vbox->setContentsMargins(QMargins(0, -1, 0, 0)); // action buttons QHBoxLayout *hbox = new QHBoxLayout; checkAllBox = new QCheckBox(i18n("All")); connect(checkAllBox, &QCheckBox::toggled, this, &PkUpdateList::checkAll); filterEdit = new QLineEdit; filterEdit->setPlaceholderText(i18n("Filter")); filterEdit->setClearButtonEnabled(true); connect(filterEdit, &QLineEdit::textEdited, this, &PkUpdateList::filterChanged); // create "busy" indicator progressBar = new QProgressBar; installButton = new QPushButton(i18n("Install")); installButton->setEnabled(false); connect(installButton, &QPushButton::clicked, this, &PkUpdateList::install); refreshButton = new QPushButton(i18n("Refresh")); connect(refreshButton, &QPushButton::clicked, this, [this]() { setPackages(PkUpdates::PackageList()); emit refreshRequested(); }); hbox->addWidget(checkAllBox); hbox->addWidget(filterEdit); hbox->addWidget(progressBar); hbox->addWidget(installButton); hbox->addWidget(refreshButton); vbox->addLayout(hbox); // list of items in the order: security, important, bugfix, others scrollArea = new QScrollArea; scrollArea->setWidgetResizable(true); vbox->addWidget(scrollArea); QWidget *w = new QWidget; vbox = new QVBoxLayout(w); itemsLayout = new QVBoxLayout; itemsLayout->setSpacing(0); vbox->addLayout(itemsLayout); vbox->addStretch(); scrollArea->setWidget(w); } //-------------------------------------------------------------------------------- QSize PkUpdateList::sizeHint() const { QSize s = scrollArea->widget()->sizeHint() + QSize(2 * scrollArea->frameWidth(), 2 * scrollArea->frameWidth()); s.setWidth(s.width() + scrollArea->verticalScrollBar()->sizeHint().width()); s.setHeight(layout()->contentsMargins().top() + installButton->sizeHint().height() + ((layout()->spacing() == -1) ? style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing) : layout()->spacing()) + s.height() + layout()->contentsMargins().bottom()); s = s.expandedTo(QSize(500, 300)); // Qt doc: The size of top-level widgets are constrained to 2/3 of the desktop's height and width. - QSize screen = QApplication::primaryScreen()->availableSize(); + QSize screen = DesktopWidget::availableSize(); s = s.boundedTo(screen * 2 / 3); return s; } //-------------------------------------------------------------------------------- void PkUpdateList::setRefreshProgress(int progress) { if ( progress >= 100 ) { filterEdit->show(); progressBar->hide(); refreshButton->setEnabled(true); } else if ( progress == 0 ) { // we don't know if there will be a value between 0..100 // Until the first value arrives, use a busy indicator progressBar->setMinimum(0); progressBar->setMaximum(0); progressBar->setValue(0); progressBar->show(); filterEdit->hide(); refreshButton->setEnabled(false); } else { progressBar->setMinimum(0); progressBar->setMaximum(100); progressBar->setValue(progress); } } //-------------------------------------------------------------------------------- void PkUpdateList::setPackages(const PkUpdates::PackageList &packages) { QLayoutItem *child; while ( (child = itemsLayout->takeAt(0)) ) { delete child->widget(); delete child; } checkAllBox->setChecked(false); QList list = packages.values(PackageKit::Transaction::InfoSecurity); for (const PkUpdates::PackageData &data : list) { auto *item = new PkUpdateListItem(itemsLayout->parentWidget(), PackageKit::Transaction::InfoSecurity, data); connect(item, &PkUpdateListItem::toggled, this, &PkUpdateList::countChecked); itemsLayout->addWidget(item); item->show(); } list = packages.values(PackageKit::Transaction::InfoImportant); for (const PkUpdates::PackageData &data : list) { auto *item = new PkUpdateListItem(itemsLayout->parentWidget(), PackageKit::Transaction::InfoImportant, data); connect(item, &PkUpdateListItem::toggled, this, &PkUpdateList::countChecked); itemsLayout->addWidget(item); item->show(); } list = packages.values(PackageKit::Transaction::InfoBugfix); for (const PkUpdates::PackageData &data : list) { auto *item = new PkUpdateListItem(itemsLayout->parentWidget(), PackageKit::Transaction::InfoBugfix, data); connect(item, &PkUpdateListItem::toggled, this, &PkUpdateList::countChecked); itemsLayout->addWidget(item); item->show(); } // all the others for (PkUpdates::PackageList::const_iterator it = packages.constBegin(); it != packages.constEnd(); ++it) { if ( (it.key() != PackageKit::Transaction::InfoSecurity) && (it.key() != PackageKit::Transaction::InfoImportant) && (it.key() != PackageKit::Transaction::InfoBugfix) ) { auto *item = new PkUpdateListItem(itemsLayout->parentWidget(), it.key(), it.value()); connect(item, &PkUpdateListItem::toggled, this, &PkUpdateList::countChecked); itemsLayout->addWidget(item); item->show(); } } filterChanged(filterEdit->text()); countChecked(); } //-------------------------------------------------------------------------------- void PkUpdateList::checkAll(bool on) { for (int i = 0; i < itemsLayout->count(); i++) { PkUpdateListItem *item = qobject_cast(itemsLayout->itemAt(i)->widget()); if ( item && !item->isHidden() ) { item->checkBox->blockSignals(true); item->checkBox->setChecked(on); item->checkBox->blockSignals(false); } } countChecked(); } //-------------------------------------------------------------------------------- void PkUpdateList::countChecked() { int count = 0; for (int i = 0; i < itemsLayout->count(); i++) { PkUpdateListItem *item = qobject_cast(itemsLayout->itemAt(i)->widget()); if ( item && !item->isHidden() && item->checkBox->isChecked() ) count++; } if ( count ) { installButton->setText(i18np("Install %1 package", "Install %1 packages", count)); installButton->setEnabled(true); } else { installButton->setText(i18n("Install")); installButton->setEnabled(false); } installButton->setIcon(QIcon()); refreshButton->setEnabled(true); } //-------------------------------------------------------------------------------- void PkUpdateList::filterChanged(const QString &text) { itemsLayout->parentWidget()->layout()->setEnabled(false); for (int i = 0; i < itemsLayout->count(); i++) { PkUpdateListItem *item = qobject_cast(itemsLayout->itemAt(i)->widget()); item->setVisible(text.isEmpty() || (item->label->text().indexOf(text, 0, Qt::CaseInsensitive) != -1)); } itemsLayout->parentWidget()->layout()->setEnabled(true); countChecked(); } //-------------------------------------------------------------------------------- void PkUpdateList::install() { if ( !installQ.isEmpty() ) // installation in progress; cancel it { QPointer currentItem = installQ.head(); if ( transaction ) { QDBusPendingReply<> reply = transaction->cancel(); reply.waitForFinished(); if ( reply.isError() && currentItem ) { currentItem->errorLabel->setText(reply.error().message()); currentItem->errorLabel->show(); } } for (int i = 0; i < itemsLayout->count(); i++) { PkUpdateListItem *item = qobject_cast(itemsLayout->itemAt(i)->widget()); if ( !transaction || (item != currentItem) ) { item->showProgress(false); item->errorLabel->hide(); } } installQ.clear(); emit packageCountToInstall(0); countChecked(); return; } restart = PackageKit::Transaction::RestartNone; for (int i = 0; i < itemsLayout->count(); i++) { QPointer item = qobject_cast(itemsLayout->itemAt(i)->widget()); if ( item && !item->isHidden() && item->checkBox->isChecked() ) { item->showProgress(true); item->errorLabel->hide(); item->detailsLabel->hide(); installQ.enqueue(item); } } installOne(); } //-------------------------------------------------------------------------------- void PkUpdateList::installOne() { emit packageCountToInstall(installQ.count()); if ( installQ.isEmpty() ) // installation finished { if ( restart != PackageKit::Transaction::RestartNone ) { QString text; if ( (restart == PackageKit::Transaction::RestartSystem) || (restart == PackageKit::Transaction::RestartSecuritySystem) ) { KNotification *notif = new KNotification("restart needed", parentWidget(), KNotification::Persistent); notif->setTitle(i18n("System Reboot Required")); notif->setText(i18n("One of the installed packages requires a system reboot")); notif->setActions(QStringList() << i18n("Reboot System")); connect(notif, &KNotification::action1Activated, this, []() { QDBusMessage msg = QDBusMessage::createMethodCall("org.kde.ksmserver", "/KSMServer", "org.kde.KSMServerInterface", "logout"); msg << 0/*no confirm*/ << 1/*reboot*/ << 0; // plasma-workspace/libkworkspace/kworkspace.h QDBusConnection::sessionBus().send(msg); }); notif->sendEvent(); } else if ( (restart == PackageKit::Transaction::RestartSession) || (restart == PackageKit::Transaction::RestartSecuritySession) ) { KNotification *notif = new KNotification("restart needed", parentWidget(), KNotification::Persistent); notif->setTitle(i18n("Session Restart Required")); notif->setText(i18n("One of the installed packages requires you to logout")); notif->setActions(QStringList() << i18n("Logout")); connect(notif, &KNotification::action1Activated, this, []() { QDBusMessage msg = QDBusMessage::createMethodCall("org.kde.ksmserver", "/KSMServer", "org.kde.KSMServerInterface", "logout"); msg << 0/*no confirm*/ << 0/*logout*/ << 0; // plasma-workspace/libkworkspace/kworkspace.h QDBusConnection::sessionBus().send(msg); }); notif->sendEvent(); } } countChecked(); return; } QPointer item = installQ.head(); if ( !item ) return; installButton->setText(i18n("Cancel Installation")); installButton->setIcon(QIcon::fromTheme("process-stop")); refreshButton->setEnabled(false); PackageKit::Transaction::TransactionFlag flag = PackageKit::Transaction::TransactionFlagOnlyTrusted; #ifdef TEST_LOGOUT flag |= PackageKit::Transaction::TransactionFlagSimulate; restart = PackageKit::Transaction::RestartSession; #elif defined TEST_REBOOT flag |= PackageKit::Transaction::TransactionFlagSimulate; restart = PackageKit::Transaction::RestartSystem; #endif transaction = PackageKit::Daemon::updatePackage(item->package.id, flag); packageNoLongerAvailable = false; //qDebug() << "installing" << item->package.id; connect(transaction.data(), &PackageKit::Transaction::statusChanged, this, [item, this]() { if ( !item ) // already deleted return; //qDebug() << "status" << QMetaEnum::fromType().valueToKey(transaction->status()); QString text; switch ( transaction->status() ) { case PackageKit::Transaction::StatusWait: text = i18n("Waiting"); break; case PackageKit::Transaction::StatusWaitingForAuth: text = i18n("Waiting for authentication"); break; case PackageKit::Transaction::StatusDepResolve: text = i18n("Resolving dependencies"); break; case PackageKit::Transaction::StatusUpdate: text = i18n("Updating"); break; case PackageKit::Transaction::StatusInstall: text = i18n("Installing"); break; case PackageKit::Transaction::StatusDownload: text = i18n("Downloading"); break; case PackageKit::Transaction::StatusCancel: text = i18n("Canceling"); break; case PackageKit::Transaction::StatusFinished: return; // don't hide error label default: return; } if ( text.isEmpty() ) item->errorLabel->hide(); else { item->errorLabel->setText(text); item->errorLabel->show(); } }); connect(transaction.data(), &PackageKit::Transaction::itemProgress, this, [item](const QString &itemID, PackageKit::Transaction::Status status, uint percentage) { Q_UNUSED(status) if ( !item ) // already deleted return; item->packageLabel->setText(PackageKit::Daemon::packageName(itemID)); item->packageLabel->show(); if ( percentage <= 100 ) // 101 .. unknown item->progress->setValue(percentage); }); connect(transaction.data(), &PackageKit::Transaction::requireRestart, this, [this](PackageKit::Transaction::Restart type, const QString &/*packageID*/) { // keep most important restart type: System, Session if ( (type == PackageKit::Transaction::RestartSystem) || (type == PackageKit::Transaction::RestartSecuritySystem) || (((type == PackageKit::Transaction::RestartSession) || (type == PackageKit::Transaction::RestartSecuritySession)) && (restart != PackageKit::Transaction::RestartSystem) && (restart != PackageKit::Transaction::RestartSecuritySystem)) ) restart = type; }); connect(transaction.data(), &PackageKit::Transaction::errorCode, this, [item, this](PackageKit::Transaction::Error error, const QString &details) { if ( !item ) return; item->showProgress(false); item->errorLabel->setText(details); item->errorLabel->show(); //qDebug() << "errorCode" << details << QMetaEnum::fromType().valueToKey(error); if ( (error == PackageKit::Transaction::ErrorDepResolutionFailed) && (transaction->status() == PackageKit::Transaction::StatusSetup) ) { // when a package was already installed due to another dependency, then it's no more an update candidate. // Still the finished() signal will arrive which will do cleanup (delete item, dequeue, etc.). packageNoLongerAvailable = true; } }); connect(transaction.data(), &PackageKit::Transaction::finished, this, [item, this](PackageKit::Transaction::Exit status, uint runtime) { Q_UNUSED(runtime) if ( !item ) return; if ( (status == PackageKit::Transaction::ExitSuccess) || packageNoLongerAvailable ) { item->hide(); item->deleteLater(); emit packageInstalled(item->package.id); } else { item->showProgress(false); item->detailsLabel->setText(i18n("Update failed")); item->detailsLabel->show(); } if ( !installQ.isEmpty() ) // might have been cancelled, q cleared installQ.dequeue(); installOne(); }); } //-------------------------------------------------------------------------------- diff --git a/SysTrayItem.cxx b/SysTrayItem.cxx index 770ae89..8de37a7 100644 --- a/SysTrayItem.cxx +++ b/SysTrayItem.cxx @@ -1,91 +1,92 @@ // SPDX-License-Identifier: GPL-3.0-or-later /* - Copyright 2017 - 2019 Martin Koller, kollix@aon.at + Copyright 2017 - 2020 Martin Koller, kollix@aon.at This file is part of liquidshell. liquidshell is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. liquidshell is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with liquidshell. If not, see . */ #include +#include #include #include #include #include #include #include //-------------------------------------------------------------------------------- SysTrayItem::SysTrayItem(QWidget *parent, const QString &icon) : QLabel(parent), iconName(icon) { setFixedSize(QSize(22, 22)); if ( !iconName.isEmpty() ) { setPixmap(QIcon::fromTheme(iconName).pixmap(size())); connect(KIconLoader::global(), &KIconLoader::iconLoaderSettingsChanged, this, [this]() { setPixmap(QIcon::fromTheme(iconName).pixmap(size())); }); } } //-------------------------------------------------------------------------------- void SysTrayItem::mousePressEvent(QMouseEvent *event) { if ( event->button() != Qt::LeftButton ) return; toggleDetailsList(); } //-------------------------------------------------------------------------------- void SysTrayItem::showDetailsList() { QWidget *detailsList = getDetailsList(); if ( !detailsList ) return; QPoint point = mapToGlobal(pos()); - QRect screen = QApplication::primaryScreen()->availableGeometry(); + QRect screen = DesktopWidget::availableGeometry(); QSize size = detailsList->frameSize(); point.setX(std::min(point.x(), screen.x() + screen.width() - size.width())); point.setY(screen.bottom() - size.height()); detailsList->move(point); detailsList->show(); KWindowSystem::raiseWindow(detailsList->winId()); } //-------------------------------------------------------------------------------- void SysTrayItem::toggleDetailsList() { QWidget *detailsList = getDetailsList(); if ( !detailsList ) return; if ( detailsList->isVisible() ) detailsList->close(); else showDetailsList(); } //--------------------------------------------------------------------------------